User:Polygnotus/Scripts/VEbuttons.js

// 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 with more dependencies to ensure we have access to all needed components

mw.loader.using([

'ext.visualEditor.desktopArticleTarget',

'ext.visualEditor.core',

'oojs-ui',

'mediawiki.api'

]).then(function() {

console.log('VE Button Script: Dependencies loaded');

// Better activation hooks that catch both initial load and editor reactivation

mw.hook('ve.activationComplete').add(initCustomButtons);

// Also hook into surface ready events which fire when switching between VE and source mode

if (typeof OO !== 'undefined' && OO.ui) {

// Wait for OO.ui to be fully initialized

$(function() {

if (ve.init && ve.init.target) {

ve.init.target.on('surfaceReady', function() {

console.log('VE Button Script: Surface ready event triggered');

initCustomButtons();

});

}

});

}

function initCustomButtons() {

console.log('VE Button Script: Initializing custom buttons');

loadButtonsConfig().then(function(buttons) {

if (Array.isArray(buttons) && buttons.length > 0) {

console.log('VE Button Script: Loaded ' + buttons.length + ' button configs');

// Register all tools and commands

buttons.forEach(registerButtonTool);

// Add timeout to ensure toolbar is fully initialized

setTimeout(function() {

addCustomToolbarGroup(buttons);

}, 500);

} else {

console.log('VE Button Script: No button configurations found or empty array');

}

}).catch(function(error) {

console.error('VE Button Script: Failed to load custom buttons configuration:', error);

});

}

// Load buttons configuration from user's JSON page with better error handling

async function loadButtonsConfig() {

console.log('VE Button Script: Attempting to load button configuration');

const username = mw.config.get('wgUserName');

if (!username) {

console.log('VE Button Script: No username found, cannot load configuration');

return [];

}

const api = new mw.Api();

try {

// For testing/debugging, we can check if we have a hardcoded config in local storage

const debugConfig = localStorage.getItem('veButtonsDebugConfig');

if (debugConfig) {

console.log('VE Button Script: Using debug configuration from localStorage');

try {

return JSON.parse(debugConfig);

} catch (e) {

console.error('VE Button Script: Invalid debug configuration:', e);

}

}

console.log(`VE Button Script: Fetching configuration from User:${username}/VEbuttonsJSON.json`);

const result = await api.get({

action: 'query',

prop: 'revisions',

titles: `User:${username}/VEbuttonsJSON.json`,

rvslots: '*',

rvprop: 'content',

formatversion: '2',

uselang: 'content', // Enable caching

smaxage: '86400', // Cache for 1 day

maxage: '86400' // Cache for 1 day

});

// Log the full API response for debugging

console.log('VE Button Script: API response received', result);

if (!result.query || !result.query.pages || !result.query.pages.length) {

console.log('VE Button Script: No pages returned from API');

return [];

}

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

console.log('VE Button Script: Configuration page not found');

return [];

}

if (!result.query.pages[0].revisions || !result.query.pages[0].revisions.length) {

console.log('VE Button Script: No revisions found for configuration page');

return [];

}

const content = result.query.pages[0].revisions[0].slots.main.content;

console.log('VE Button Script: Raw configuration content', content);

// Use more robust JSON parsing

try {

const parsed = JSON.parse(content);

console.log('VE Button Script: Configuration parsed successfully', parsed);

return parsed;

} catch (jsonError) {

console.error('VE Button Script: JSON parsing error:', jsonError);

return [];

}

} catch (error) {

console.error('VE Button Script: 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 - improved with better error handling and jQuery fallback

function addCustomToolbarGroup(buttons) {

console.log('VE Button Script: Attempting to add custom toolbar group');

if (!ve.init.target) {

console.warn('VE Button Script: Visual editor target not found');

return;

}

if (!ve.init.target.toolbar) {

console.warn('VE Button Script: Visual editor toolbar not found');

return;

}

// Get button names for the group

const buttonNames = buttons.map(config => config.name);

console.log('VE Button Script: Button names for toolbar group:', buttonNames);

// Check if OO and ve.ui are properly defined

if (!OO || !ve.ui || !ve.ui.ToolGroup) {

console.error('VE Button Script: Required OOUI components are not available');

tryJQueryFallback(buttons);

return;

}

// First, ensure our custom toolbar group class is defined

// Important: Only define it once to avoid errors

if (!ve.ui.CustomToolbarGroup) {

try {

console.log('VE Button Script: Defining CustomToolbarGroup class');

// Define a custom toolbar group

ve.ui.CustomToolbarGroup = function VeUiCustomToolbarGroup(toolFactory, config) {

// Ensure this is being called as a constructor

if (!(this instanceof VeUiCustomToolbarGroup)) {

return new VeUiCustomToolbarGroup(toolFactory, config);

}

// Call parent constructor

ve.ui.ToolGroup.call(this, toolFactory, config);

};

// Safe inheritance with fallbacks

if (OO.inheritClass) {

OO.inheritClass(ve.ui.CustomToolbarGroup, ve.ui.BarToolGroup);

} else {

// Fallback inheritance if OO.inheritClass is not available

ve.ui.CustomToolbarGroup.prototype = Object.create(ve.ui.BarToolGroup.prototype);

ve.ui.CustomToolbarGroup.prototype.constructor = ve.ui.CustomToolbarGroup;

// Copy static properties

if (ve.ui.BarToolGroup.static) {

ve.ui.CustomToolbarGroup.static = Object.assign({}, ve.ui.BarToolGroup.static);

} else {

ve.ui.CustomToolbarGroup.static = {};

}

}

ve.ui.CustomToolbarGroup.static.name = 'customTools';

ve.ui.CustomToolbarGroup.static.title = 'Custom tools';

// Register the toolbar group

if (ve.ui.toolGroupFactory && typeof ve.ui.toolGroupFactory.register === 'function') {

ve.ui.toolGroupFactory.register(ve.ui.CustomToolbarGroup);

console.log('VE Button Script: CustomToolbarGroup registered successfully');

} else {

console.error('VE Button Script: toolGroupFactory not available');

throw new Error('toolGroupFactory not available');

}

} catch (error) {

console.error('VE Button Script: Error defining CustomToolbarGroup:', error);

tryJQueryFallback(buttons);

return;

}

}

// Add the group to the toolbar

const toolbar = ve.init.target.toolbar;

// Only add if the group doesn't exist yet

try {

if (!toolbar.getToolGroupByName || !toolbar.getToolGroupByName('customTools')) {

console.log('VE Button Script: Adding customTools group to toolbar');

// Get the target index to insert the group

let targetIndex = -1;

if (toolbar.items && toolbar.items.length) {

// Safer way to find insertion point using items collection

for (let i = 0; i < toolbar.items.length; i++) {

const group = toolbar.items[i];

if (group.name === 'format' || group.name === 'structure') {

targetIndex = i + 1;

break;

}

}

} else if (toolbar.getToolGroups && typeof toolbar.getToolGroups === 'function') {

// Legacy way

const toolGroups = toolbar.getToolGroups();

for (let i = 0; i < toolGroups.length; i++) {

const group = toolGroups[i];

if (group.name === 'format' || group.name === 'structure') {

targetIndex = i + 1;

break;

}

}

}

console.log('VE Button Script: Target index for insertion:', targetIndex);

// Create the group config

const groupConfig = {

name: 'customTools',

type: 'customTools',

include: buttonNames,

label: 'Custom'

};

// Add the group at the desired position

if (toolbar.getItems && toolbar.getItems()[0] && toolbar.getItems()[0].addItems) {

// Standard method

if (targetIndex !== -1) {

console.log('VE Button Script: Adding at specific position', targetIndex);

toolbar.getItems()[0].addItems([groupConfig], targetIndex);

} else {

console.log('VE Button Script: Adding at end of toolbar');

toolbar.getItems()[0].addItems([groupConfig]);

}

// Rebuild the toolbar to show the new group

console.log('VE Button Script: Rebuilding toolbar');

toolbar.rebuild();

} else if (toolbar.setup && typeof toolbar.setup === 'function') {

// Alternative method for some VE versions

console.log('VE Button Script: Using toolbar.setup method');

const toolbarConfig = toolbar.getDefaultConfig();

toolbarConfig.push(groupConfig);

toolbar.setup(toolbarConfig);

} else {

// Last resort - try using jQuery to manually append our buttons

console.log('VE Button Script: Using jQuery fallback for toolbar manipulation');

tryJQueryFallback(buttons);

}

} else {

console.log('VE Button Script: customTools group already exists');

}

} catch (error) {

console.error('VE Button Script: Error adding toolbar group:', error);

// Try jQuery fallback if the normal methods fail

tryJQueryFallback(buttons);

}

}

// jQuery fallback method for when the normal VE integration fails

function tryJQueryFallback(buttons) {

console.log('VE Button Script: Attempting jQuery fallback for button insertion');

// Wait a moment to ensure the UI is stable

setTimeout(function() {

try {

// Create a proper new group for our buttons

const $toolGroup = $('

')

.addClass('ve-ui-toolbar-group-custom oo-ui-widget oo-ui-toolGroup oo-ui-barToolGroup oo-ui-widget-enabled')

.attr('title', 'Custom Tools');

const $toolsContainer = $('

')

.addClass('oo-ui-toolGroup-tools oo-ui-barToolGroup-tools oo-ui-toolGroup-enabled-tools')

.appendTo($toolGroup);

// Add each button

buttons.forEach(function(config) {

const $button = $('')

.addClass('oo-ui-widget oo-ui-iconElement oo-ui-tool-with-icon oo-ui-tool oo-ui-tool-name-' + config.name + ' oo-ui-widget-enabled')

.appendTo($toolsContainer);

const $link = $('')

.addClass('oo-ui-tool-link')

.attr('role', 'button')

.attr('tabindex', '0')

.attr('title', config.title || config.name)

.appendTo($button);

// Add the icon structure

$('')

.addClass('oo-ui-tool-checkIcon oo-ui-widget oo-ui-widget-enabled oo-ui-iconElement oo-ui-iconElement-icon oo-ui-icon-check oo-ui-labelElement-invisible oo-ui-iconWidget')

.appendTo($link);

if (config.icon) {

if (config.icon.startsWith('http')) {

// Custom icon handling

$('')

.addClass('oo-ui-iconElement-icon custom-' + config.name)

.css({

'background-image': 'url(' + config.icon + ')',

'background-size': 'contain',

'background-position': 'center',

'background-repeat': 'no-repeat'

})

.appendTo($link);

} else {

$('')

.addClass('oo-ui-iconElement-icon oo-ui-icon-' + config.icon)

.appendTo($link);

}

} else {

$('')

.addClass('oo-ui-iconElement-icon oo-ui-icon-help')

.appendTo($link);

}

$('')

.addClass('oo-ui-tool-title')

.text(config.title || config.name)

.appendTo($link);

// Add click event handler

$button.on('click', function(e) {

e.preventDefault();

e.stopPropagation();

try {

const surface = ve.init.target.getSurface();

const surfaceModel = surface.getModel();

let content = config.insertText;

// Handle the concatenation pattern with single quotes

if (typeof content === 'string') {

content = content.replace(/'\s*\+\s*'/g, '');

}

surfaceModel.getFragment()

.collapseToEnd()

.insertContent(content)

.collapseToEnd()

.select();

} catch (error) {

console.error('VE Button Script: Error executing button action:', error);

}

});

});

// Insert our group at an appropriate location in the toolbar

var $insertPosition = $('.ve-ui-toolbar-group-structure, .ve-ui-toolbar-group-format').last();

if ($insertPosition.length) {

$toolGroup.insertAfter($insertPosition);

console.log('VE Button Script: jQuery fallback - button group added successfully after', $insertPosition.attr('class'));

} else {

// Fallback: add to main toolbar

$('.oo-ui-toolbar-tools').first().append($toolGroup);

console.log('VE Button Script: jQuery fallback - button group added to main toolbar');

}

} catch (error) {

console.error('VE Button Script: jQuery fallback failed:', error);

}

}, 1000);

}

});

})();