User:Polygnotus/Scripts/VEbuttons.js
Appearance
From Wikipedia, the free encyclopedia
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/VEbuttons.
// 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(typeofOO!=='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(); }); } }); } functioninitCustomButtons(){ 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 asyncfunctionloadButtonsConfig(){ console.log('VE Button Script: Attempting to load button configuration'); constusername=mw.config.get('wgUserName'); if(!username){ console.log('VE Button Script: No username found, cannot load configuration'); return[]; } constapi=newmw.Api(); try{ // For testing/debugging, we can check if we have a hardcoded config in local storage constdebugConfig=localStorage.getItem('veButtonsDebugConfig'); if(debugConfig){ console.log('VE Button Script: Using debug configuration from localStorage'); try{ returnJSON.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`); constresult=awaitapi.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[]; } constcontent=result.query.pages[0].revisions[0].slots.main.content; console.log('VE Button Script: Raw configuration content',content); // Use more robust JSON parsing try{ constparsed=JSON.parse(content); console.log('VE Button Script: Configuration parsed successfully',parsed); returnparsed; }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 functionregisterButtonTool(config){ // Add custom icon CSS if URL is provided if(config.icon&&config.icon.startsWith('http')){ addCustomIconCSS(config.name,config.icon); } // Create command constCommandClass=function(){ ve.ui.Command.call(this,config.name); }; OO.inheritClass(CommandClass,ve.ui.Command); CommandClass.prototype.execute=function(surface){ try{ constsurfaceModel=surface.getModel(); letcontent=config.insertText; // Handle the string concatenation pattern found in the JSON if(typeofcontent==='string'){ // This handles patterns like "text" + ":more" or "pre~~" + "~~post" content=content.replace(/'\s*\+\s*'/g,''); } surfaceModel.getFragment() .collapseToEnd() .insertContent(content) .collapseToEnd() .select(); returntrue; }catch(error){ console.error(`Error executing command ${config.name}:`,error); returnfalse; } }; ve.ui.commandRegistry.register(newCommandClass()); // Create tool constToolClass=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 functionaddCustomIconCSS(name,iconUrl){ conststyleId=`custom-icon-${name}`; if(!document.getElementById(styleId)){ conststyle=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 functionaddCustomToolbarGroup(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 constbuttonNames=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=functionVeUiCustomToolbarGroup(toolFactory,config){ // Ensure this is being called as a constructor if(!(thisinstanceofVeUiCustomToolbarGroup)){ returnnewVeUiCustomToolbarGroup(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&&typeofve.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'); thrownewError('toolGroupFactory not available'); } }catch(error){ console.error('VE Button Script: Error defining CustomToolbarGroup:',error); tryJQueryFallback(buttons); return; } } // Add the group to the toolbar consttoolbar=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 lettargetIndex=-1; if(toolbar.items&&toolbar.items.length){ // Safer way to find insertion point using items collection for(leti=0;i<toolbar.items.length;i++){ constgroup=toolbar.items[i]; if(group.name==='format'||group.name==='structure'){ targetIndex=i+1; break; } } }elseif(toolbar.getToolGroups&&typeoftoolbar.getToolGroups==='function'){ // Legacy way consttoolGroups=toolbar.getToolGroups(); for(leti=0;i<toolGroups.length;i++){ constgroup=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 constgroupConfig={ 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(); }elseif(toolbar.setup&&typeoftoolbar.setup==='function'){ // Alternative method for some VE versions console.log('VE Button Script: Using toolbar.setup method'); consttoolbarConfig=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 functiontryJQueryFallback(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=$('<div>') .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=$('<div>') .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=$('<span>') .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=$('<a>') .addClass('oo-ui-tool-link') .attr('role','button') .attr('tabindex','0') .attr('title',config.title||config.name) .appendTo($button); // Add the icon structure $('<span>') .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 $('<span>') .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{ $('<span>') .addClass('oo-ui-iconElement-icon oo-ui-icon-'+config.icon) .appendTo($link); } }else{ $('<span>') .addClass('oo-ui-iconElement-icon oo-ui-icon-help') .appendTo($link); } $('<span>') .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{ constsurface=ve.init.target.getSurface(); constsurfaceModel=surface.getModel(); letcontent=config.insertText; // Handle the concatenation pattern with single quotes if(typeofcontent==='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); } }); })();