User:Polygnotus/Scripts/VEbuttons.js

This is an old revision of this page, as edited by Polygnotus (talk | contribs) at 20:32, 8 April 2025. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// Add custom buttons to Wikipedia's Visual Editor based on JSON configuration
// Add this code to your common.js page on Wikipedia
// (e.g., https://en.wikipedia.org/wiki/User:YourUsername/common.js)

(function() {
    // Wait for the VisualEditor to be ready
    mw.loader.using(['ext.visualEditor.desktopArticleTarget', 'mediawiki.api']).then(function() {
        // Load JSON configuration and add buttons when VE is activated
        mw.hook('ve.activationComplete').add(function() {
            loadButtonsConfig().then(function(buttons) {
                if (Array.isArray(buttons) && buttons.length > 0) {
                    // Register all tools and commands
                    buttons.forEach(registerButtonTool);
                    
                    // Add custom toolbar group when surface is ready
                    if (ve.init && ve.init.target) {
                        ve.init.target.on('surfaceReady', function() {
                            addCustomToolbarGroup(buttons);
                        });
                    }
                }
            }).catch(function(error) {
                console.error('Failed to load custom buttons configuration:', error);
            });
        });
    });

    // Load buttons configuration from user's JSON page
    async function loadButtonsConfig() {
        const username = mw.config.get('wgUserName');
        if (!username) return [];

        const api = new mw.Api();
        try {
            const result = await api.get({
                action: 'query',
                prop: 'revisions',
                titles: `User:${username}/VisualEditorButtonsJSON`,
                rvslots: '*',
                rvprop: 'content',
                formatversion: '2',
                uselang: 'content', // Enable caching
                smaxage: '86400',   // Cache for 1 day
                maxage: '86400'     // Cache for 1 day
            });

            if (result.query.pages[0].missing) {
                return [];
            }

            const content = result.query.pages[0].revisions[0].slots.main.content;
            return JSON.parse(content);
        } catch (error) {
            console.error('Error loading buttons configuration:', error);
            return [];
        }
    }

    // Register a button tool based on config
    function registerButtonTool(config) {
        // Add custom icon CSS if URL is provided
        if (config.icon && config.icon.startsWith('http')) {
            addCustomIconCSS(config.name, config.icon);
        }

        // Create command
        const CommandClass = function() {
            ve.ui.Command.call(this, config.name);
        };
        OO.inheritClass(CommandClass, ve.ui.Command);
        
        CommandClass.prototype.execute = function(surface) {
            try {
                const surfaceModel = surface.getModel();
                
                let content = config.insertText;
                // Handle the string concatenation pattern found in the JSON
                if (typeof content === 'string') {
                    // This handles patterns like "text" + ":more" or "pre~~" + "~~post"
                    content = content.replace(/"\s*\+\s*"/g, '');
                }
                
                surfaceModel.getFragment()
                    .collapseToEnd()
                    .insertContent(content)
                    .collapseToEnd()
                    .select();
                
                return true;
            } catch (error) {
                console.error(`Error executing command ${config.name}:`, error);
                return false;
            }
        };
        
        ve.ui.commandRegistry.register(new CommandClass());
        
        // Create tool
        const ToolClass = function() {
            ve.ui.Tool.apply(this, arguments);
        };
        OO.inheritClass(ToolClass, ve.ui.Tool);
        
        ToolClass.static.name = config.name;
        ToolClass.static.title = config.title || config.name;
        ToolClass.static.commandName = config.name;
        ToolClass.static.icon = config.icon && config.icon.startsWith('http') 
            ? 'custom-' + config.name 
            : (config.icon || 'help');
        
        ToolClass.prototype.onSelect = function() {
            this.setActive(false);
            this.getCommand().execute(this.toolbar.getSurface());
        };
        
        ToolClass.prototype.onUpdateState = function() {
            this.setActive(false);
        };
        
        ve.ui.toolFactory.register(ToolClass);
    }

    // Add custom CSS for icons
    function addCustomIconCSS(name, iconUrl) {
        const styleId = `custom-icon-${name}`;
        if (!document.getElementById(styleId)) {
            const style = document.createElement('style');
            style.id = styleId;
            style.textContent = `
                .oo-ui-icon-custom-${name} {
                    background-image: url(${iconUrl}) !important;
                    background-size: contain !important;
                    background-position: center !important;
                    background-repeat: no-repeat !important;
                }
            `;
            document.head.appendChild(style);
        }
    }

    // Add a custom toolbar group with our buttons
    function addCustomToolbarGroup(buttons) {
        if (!ve.init.target || !ve.init.target.toolbar) {
            console.warn('Visual editor toolbar not found');
            return;
        }
        
        // Get button names for the group
        const buttonNames = buttons.map(config => config.name);
        
        // Define a custom toolbar group
        function CustomToolbarGroup(toolFactory, config) {
            ve.ui.ToolGroup.call(this, toolFactory, config);
        }
        OO.inheritClass(CustomToolbarGroup, ve.ui.BarToolGroup);
        CustomToolbarGroup.static.name = 'customTools';
        CustomToolbarGroup.static.title = 'Custom tools';
        ve.ui.toolGroupFactory.register(CustomToolbarGroup);
        
        // Add the group to the toolbar
        const toolbar = ve.init.target.toolbar;
        
        // Only add if the group doesn't exist yet
        if (!toolbar.getToolGroupByName('customTools')) {
            // Get the target index to insert the group
            const toolGroups = toolbar.getToolGroups();
            let targetIndex = -1;
            
            for (let i = 0; i < toolGroups.length; i++) {
                const group = toolGroups[i];
                if (group.name === 'format' || group.name === 'structure') {
                    targetIndex = i + 1;
                    break;
                }
            }
            
            // Create the group config
            const groupConfig = {
                name: 'customTools',
                type: 'customTools',
                include: buttonNames
            };
            
            // Add the group at the desired position
            if (targetIndex !== -1) {
                toolbar.getItems()[0].addItems([groupConfig], targetIndex);
            } else {
                toolbar.getItems()[0].addItems([groupConfig]);
            }
            
            // Rebuild the toolbar to show the new group
            toolbar.rebuild();
        }
    }
})();