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);
}
});
})();