Assist Visual Editor Gadgets

From Calidus HUB

Resources

List of common gadgets

List of common snippets

Example of replace:

First, the gadgets extension must be installed - see Extension:Gadgets.


In general, you have to

  • create the Gadget loader js with a unique name e.g. MediaWiki:Gadget-veNAMELoader.js
mw.libs.ve.addPlugin('ext.gadget.veNAME');
  • Add a description to the loader e.g. MediaWiki:Gadget-veNAMELoader
Adds SOME FUNCTIONALITY to VisualEditor
  • Create the gadget js e.g. MediaWiki:Gadget-veNAME.js
  • Actually load the gadgets into MediaWiki:Gadgets-definition
* veNAMELoader[ResourceLoader|default|dependencies=ext.visualEditor.desktopArticleTarget.init]|veNAMELoader.js
* veNAME[ResourceLoader|default|rights=hidden|hidden|dependencies=ext.visualEditor.core]|veNAME.js


Examples

Center

  • MediaWiki:Gadget-veCenterLoader.js
mw.libs.ve.addPlugin('ext.gadget.veCenter');
  • MediaWiki:Gadget-veCenterLoader
Add Center to the Format list in VisualEditor
  • MediaWiki:Gadget-veCenter.js
mw.loader.using( [ 'ext.visualEditor.core', 'ext.visualEditor.mwtransclusion' ] ).then(function () {

// --------- (start of ve.ui.CenterAction definition) -----------------------------------------------
// This is based on [lib/ve/src/ui/actions/ve.ui.BlockquoteAction.js] from Extension:VisualEditor.

	ve.ui.CenterAction = function VeUiCenterAction() {
		ve.ui.CenterAction.super.apply( this, arguments );
	};
	OO.inheritClass( ve.ui.CenterAction, ve.ui.Action );

	ve.ui.CenterAction.static.name = 'center';
	ve.ui.CenterAction.static.methods = [ 'wrap', 'unwrap', 'toggle' ];

	ve.ui.CenterAction.prototype.isWrapped = function () {
		var fragment = this.surface.getModel().getFragment();
		return fragment.hasMatchingAncestor( 'center' );
	};
	ve.ui.CenterAction.prototype.toggle = function () {
		return this[ this.isWrapped() ? 'unwrap' : 'wrap' ]();
	};
	ve.ui.CenterAction.prototype.wrap = function () {
		var
			surfaceModel = this.surface.getModel(),
			selection = surfaceModel.getSelection(),
			fragment = surfaceModel.getFragment( null, true ),
			leaves, leavesRange;

		if ( !( selection instanceof ve.dm.LinearSelection ) ) {
			return false;
		}

		leaves = fragment.getSelectedLeafNodes();
		leavesRange = new ve.Range(
			leaves[ 0 ].getRange().start,
			leaves[ leaves.length - 1 ].getRange().end
		);
		fragment = surfaceModel.getLinearFragment( leavesRange, true );

		fragment = fragment.expandLinearSelection( 'siblings' );

		while (
			fragment.getCoveredNodes().some( function ( nodeInfo ) {
				return !nodeInfo.node.isAllowedParentNodeType( 'center' ) || nodeInfo.node.isContent();
			} )
		) {
			fragment = fragment.expandLinearSelection( 'parent' );
		}

		// Wrap everything in a <center> tag
		fragment.wrapAllNodes( { type: 'center' } );

		return true;
	};
	ve.ui.CenterAction.prototype.unwrap = function () {
		var
			surfaceModel = this.surface.getModel(),
			selection = surfaceModel.getSelection(),
			fragment = surfaceModel.getFragment( null, true ),
			leaves, leavesRange;

		if ( !( selection instanceof ve.dm.LinearSelection ) ) {
			return false;
		}

		if ( !this.isWrapped() ) {
			return false;
		}

		leaves = fragment.getSelectedLeafNodes();
		leavesRange = new ve.Range(
			leaves[ 0 ].getRange().start,
			leaves[ leaves.length - 1 ].getRange().end
		);
		fragment = surfaceModel.getLinearFragment( leavesRange, true );

		fragment
			// Expand to cover entire <center> tag
			.expandLinearSelection( 'closest', ve.dm.CenterNode )
			// Unwrap it
			.unwrapNodes( 0, 1 );

		return true;
	};
	ve.ui.actionFactory.register( ve.ui.CenterAction );

// --------- (end of ve.ui.CenterAction definition) -------------------------------------------------

	ve.ui.CenterFormatTool = function VeUiCenterFormatTool() {
		ve.ui.CenterFormatTool.super.apply( this, arguments );
	};
	OO.inheritClass( ve.ui.CenterFormatTool, ve.ui.FormatTool );

	ve.ui.CenterFormatTool.static.name = 'center';
	ve.ui.CenterFormatTool.static.group = 'format';
	ve.ui.CenterFormatTool.static.title = 'Center';
	ve.ui.CenterFormatTool.static.format = { type: 'center' };
	ve.ui.CenterFormatTool.static.commandName = 'center';
	ve.ui.toolFactory.register( ve.ui.CenterFormatTool );

	ve.ui.commandRegistry.register(
		new ve.ui.Command(
			'center', 'center', 'toggle',
			{ supportedSelections: [ 'linear' ] }
		)
	);
	ve.ui.triggerRegistry.register(
		'center', {
			mac: new ve.ui.Trigger('cmd+j'),
			pc: new ve.ui.Trigger('ctrl+j')
		}
	);
} );
  • MediaWiki:Gadgets-definition
*veCenterLoader[ResourceLoader|dependencies=ext.visualEditor.desktopArticleTarget.init]|veCenterLoader.js
*veCenter[ResourceLoader|rights=hidden|hidden|dependencies=ext.visualEditor.core]|veCenter.js


Replace - not required - already enabled in latest VE

  • MediaWiki:Gadget-veReplaceLoader.js
mw.libs.ve.addPlugin('ext.gadget.veReplace');
  • MediaWiki:Gadget-veReplaceLoader
Adds replace button to VisualEditor
  • MediaWiki:Gadget-veReplace.js
/* Translate the following to your language: */
mw.loader.using('ext.visualEditor.core').then(function () {

if (!mw.messages.exists( 've-SearchAndReplaceDialog-title' )) {
	mw.messages.set({
		've-SearchAndReplaceDialog-title': 'Search and replace',
		've-SearchAndReplaceDialog-from-label': 'From:',
		've-SearchAndReplaceDialog-to-label': 'To:',
		've-SearchAndReplaceDialog-from-placeholder': 'From text',
		've-SearchAndReplaceDialog-to-placeholder': 'To text',
		've-SearchAndReplaceDialog-replaceAll': 'Replace all',
		've-SearchAndReplaceDialog-replace': 'Replace',
		've-SearchAndReplaceDialog-matchcase': 'Match case',
		've-SearchAndReplaceDialog-replace-complete': 'Found and replaced $1 occurrences',
		've-ReplaceTool-ToolbarButton': 'Replace'
	});
}
/* end of translations */

/*!
 * VisualEditor replace gadget
 *
 * @copyright [[User:ערן|Eranroz]] and [[User:Ravid ziv|Ravid ziv]]
 * @license The MIT License (MIT)
 */
function extractText(){
	var nodes = [];
	var model = ve.init.target.getSurface().getModel();
	function getTextNodes( obj ) {
		var i;
		
		for ( i = 0; i < obj.children.length; i++ ) {
			if ( obj.children[i].type == 'text'){
				nodes.push(obj.children[i]);
			}

			if ( obj.children[i].children ) {
				getTextNodes( obj.children[i] );
			}
		}
	}
	getTextNodes(ve.init.target.getSurface().getModel().getDocument().getDocumentNode());
	return nodes;
}

function searchAndReplace( fromText, toText, replaceAll, matchCase ) {
	var textNodes = extractText();
	var model = ve.init.target.getSurface().getModel();
	var firstIndex = 0;
	var numReplacements = 0;
	for (var nodeI = 0; nodeI < textNodes.length; nodeI++) {
		var node = textNodes[nodeI];
		var nodeRange = node.getRange();
		var nodeText = model.getLinearFragment(nodeRange).getText();

		var fromIndex = matchCase? nodeText.toUpperCase().indexOf( fromText.toUpperCase(), firstIndex ) : nodeText.indexOf( fromText, firstIndex );
		if ( fromIndex == -1 ) {
			firstIndex = 0;
			continue;
		}
		var start = nodeRange.from+fromIndex;
		var end = start+fromText.length;
		if (!replaceAll && model.selection.start > start) {
			continue;//skip replacements before selection
		}
		var removeRange = new ve.Range( start, end );
		var transaction = ve.dm.Transaction.newFromReplacement(
			ve.init.target.getSurface().getView().getDocument().model, 
			removeRange, 
			toText
		);
		var newSelection = new ve.Range(0,0);
		if (!replaceAll) {
			newSelection = new ve.Range( start, start+toText.length );
		}
		ve.init.target.getSurface().getView().changeModel(transaction, newSelection);
		numReplacements++;
		if (!replaceAll) {
			break;
		}
		firstIndex = fromIndex + toText.length;
		nodeI = nodeI -1;
	}
	if (numReplacements==0 || replaceAll) {
		mw.notify( mw.msg( 've-SearchAndReplaceDialog-replace-complete', numReplacements ) );
	}
}

ve.ui.SearchAndReplaceDialog = function( manager, config ) {
	// Parent constructor
	ve.ui.SearchAndReplaceDialog.super.call( this, manager, config );

};
/* Inheritance */

OO.inheritClass( ve.ui.SearchAndReplaceDialog, ve.ui.FragmentDialog );

ve.ui.SearchAndReplaceDialog.prototype.getActionProcess  = function ( action ) {
	var fromVal = this.fromInput.getValue(),
		toVal = this.toInput.getValue(),
		matchCase = this.matchCaseCheckbox.getValue();

	if ( action === 'replace' ) {
		return new OO.ui.Process( function () {
			searchAndReplace( fromVal, toVal, false, matchCase );
		}, this );
	} else if ( action === 'replace-all' ) {
		return new OO.ui.Process( function () {
			searchAndReplace( fromVal, toVal, true, matchCase );
			this.close( );
		}, this );
	}
	return ve.ui.MWMediaDialog.super.prototype.getActionProcess.call( this, action );
};

ve.ui.SearchAndReplaceDialog.prototype.getBodyHeight = function () {
	return 200;
};

/* Static Properties */
ve.ui.SearchAndReplaceDialog.static.name = 'search';
ve.ui.SearchAndReplaceDialog.static.title = mw.msg( 've-SearchAndReplaceDialog-title' );
ve.ui.SearchAndReplaceDialog.static.size = 'medium';

ve.ui.SearchAndReplaceDialog.static.actions = [
	{
		'action': 'replace',
		'label': mw.msg( 've-SearchAndReplaceDialog-replace' ),
		'flags': [ 'constructive' ],
		'modes': 'insert'
	},
	{
		'label': OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),
		'flags': 'safe',
		'modes': [ 'edit', 'insert', 'select' ]
	},
	{
		'action': 'replace-all',
		'label': mw.msg( 've-SearchAndReplaceDialog-replaceAll' ),
		'flags': [ 'constructive' ],
		'modes': 'insert'
	}
];

ve.ui.SearchAndReplaceDialog.prototype.initialize = function () {
	ve.ui.SearchAndReplaceDialog.super.prototype.initialize.call( this );
	this.panel = new OO.ui.PanelLayout( { '$': this.$, 'scrollable': true, 'padded': true } );
	this.inputsFieldset = new OO.ui.FieldsetLayout( {
		'$': this.$
	} );
	// input from
	this.fromInput = new OO.ui.TextInputWidget(
		{ '$': this.$, 'multiline': false, 'placeholder': mw.msg( 've-SearchAndReplaceDialog-from-placeholder' ) }
	);
	//input to
	this.toInput = new OO.ui.TextInputWidget(
		{ '$': this.$, 'multiline': false, 'placeholder': mw.msg( 've-SearchAndReplaceDialog-to-placeholder' ) }
	);
	this.fromField = new OO.ui.FieldLayout( this.fromInput, {
		'$': this.$,
		'label': mw.msg( 've-SearchAndReplaceDialog-from-label' )
	} );
	this.toField = new OO.ui.FieldLayout( this.toInput, {
		'$': this.$,
		'label': mw.msg( 've-SearchAndReplaceDialog-to-label' )
	} );


	this.matchCaseCheckbox = new OO.ui.CheckboxInputWidget( {
		'$': this.$
	} );
	var matchCaseField = new OO.ui.FieldLayout( this.matchCaseCheckbox, {
		'$': this.$,
		'align': 'inline',
		'label': mw.msg( 've-SearchAndReplaceDialog-matchcase' )
	} );

	this.inputsFieldset.$element.append(
		this.fromField.$element,
		this.toField.$element,
		matchCaseField.$element
	);
	this.panel.$element.append(	this.inputsFieldset.$element );
	this.$body.append( this.panel.$element );

};

ve.ui.windowFactory.register( ve.ui.SearchAndReplaceDialog );

//---------- replace tool ------------------
 
function ReplaceTool( toolGroup, config ) {
	OO.ui.Tool.call( this, toolGroup, config );
 
}
OO.inheritClass( ReplaceTool, OO.ui.Tool );
 
ReplaceTool.static.name = 'ReplaceTool';
ReplaceTool.static.title = mw.msg('ve-ReplaceTool-ToolbarButton');
 
ReplaceTool.prototype.onSelect = function () {
	this.toolbar.getSurface().execute( 'window', 'open', 'search', null );
};
 
ReplaceTool.prototype.onUpdateState = function () {
	this.setActive( false );
};
 
ve.ui.toolFactory.register( ReplaceTool );
});
  • MediaWiki:Gadgets-definition
*veReplaceLoader[ResourceLoader|dependencies=ext.visualEditor.viewPageTarget.init]|veReplaceLoader.js
*veReplace[ResourceLoader|rights=hidden|hidden|dependencies=ext.visualEditor.core]|veReplace.js

Note: may need to be desktopArticleTarget for the loader, as follows:

*veReplaceLoader[ResourceLoader|dependencies=ext.visualEditor.desktopArticleTarget.init]|veReplaceLoader.js

Autonumber Headings

The settings for autonumbering of sections has been removed from user preferences in later versions.

The following re-enables it as a gadget.


MediaWiki:Gadgets-definition:

* autonum[ResourceLoader]|autonum.css|autonum.js


MediaWiki:Gadget-autonum

Allow ability to auto-number headings when viewing pages (replacing setting removed from MediaWiki v1.39)

MediaWiki:Gadget-autonum.js

/**
 * Auto-number headings
 *
 * @source https://www.mediawiki.org/wiki/Snippets/Auto-number_headings
 * @author Krinkle
 * @version 2024-07-28
 */
var toc = document.querySelector('#toc');
if (toc) {
  document.body.classList.add('tpl-autonum-attr');
  // Support legacy Parser: <h2><span class=mw-headline id=…>
  // Support Parsoid: <section><div class=mw-heading><h2 id…>
  document.querySelectorAll('.mw-parser-output :is(h1,h2,h3,h4,h5,h6) .mw-headline[id], .mw-parser-output .mw-heading [id]:is(h1,h2,h3,h4,h5,h6)').forEach(function (headline) {
    var num = toc.querySelector('a[href="#' + CSS.escape(headline.id) + '"] .tocnumber');
    if (num) headline.setAttribute('data-autonum', num.textContent);
  });
} else {
  document.body.classList.add('tpl-autonum');
}


MediaWiki:Gadget-autonum.css

/**
 * Auto-number headings
 *
 * @source https://www.mediawiki.org/wiki/Snippets/Auto-number_headings
 * @author Krinkle
 * @version 2024-07-28
 */

/**
 * CSS mode:
 * Insert numbers on pages without a TOC. This could in principle work for all pages,
 * but to ensure consistency between the TOC and heading numbers we let JS follow
 * the TOC if there is one.
 */
.tpl-autonum .mw-parser-output {
  counter-reset: autonum-h2 autonum-h3 autonum-h4 autonum-h5 autonum-h6;
}
.tpl-autonum .mw-parser-output h2 {
  counter-reset: autonum-h3 autonum-h4 autonum-h5 autonum-h6;
}
.tpl-autonum .mw-parser-output h3 {
  counter-reset: autonum-h4 autonum-h5 autonum-h6;
}
.tpl-autonum .mw-parser-output h4 {
  counter-reset: autonum-h5 autonum-h6;
}
.tpl-autonum .mw-parser-output h5 {
  counter-reset: autonum-h6;
}
.tpl-autonum .mw-parser-output h2 .mw-headline:before,
.tpl-autonum .mw-parser-output .mw-heading h2:before {
  counter-increment: autonum-h2;
  content: counter(autonum-h2) " ";
}
.tpl-autonum .mw-parser-output h3 .mw-headline:before,
.tpl-autonum .mw-parser-output .mw-heading h3:before {
  counter-increment: autonum-h3;
  content: counter(autonum-h2) "." counter(autonum-h3) " ";
}
.tpl-autonum .mw-parser-output h4 .mw-headline:before,
.tpl-autonum .mw-parser-output .mw-heading h4:before {
  counter-increment: autonum-h4;
  content: counter(autonum-h2) "." counter(autonum-h3) "." counter(autonum-h4) " ";
}
.tpl-autonum .mw-parser-output h5 .mw-headline:before,
.tpl-autonum .mw-parser-output .mw-heading h5:before {
  counter-increment: autonum-h5;
  content: counter(autonum-h2) "." counter(autonum-h3) "." counter(autonum-h4) "." counter(autonum-h5) " ";
}
.tpl-autonum .mw-parser-output h6 .mw-headline:before,
.tpl-autonum .mw-parser-output .mw-heading h6:before {
  counter-increment: autonum-h6;
  content: counter(autonum-h2) "." counter(autonum-h3) "." counter(autonum-h4) "." counter(autonum-h5) "." counter(autonum-h6) " ";
}

/**
 * JS mode: When a TOC is present, autonum.js sets the data-autonum attribute
 * and we display that instead of an automatic counter.
 */
.tpl-autonum-attr .mw-parser-output .mw-headline[data-autonum]:before,
.tpl-autonum-attr .mw-parser-output h2[data-autonum]:before ,
.tpl-autonum-attr .mw-parser-output h3[data-autonum]:before ,
.tpl-autonum-attr .mw-parser-output h4[data-autonum]:before ,
.tpl-autonum-attr .mw-parser-output h5[data-autonum]:before ,
.tpl-autonum-attr .mw-parser-output h6[data-autonum]:before {
  content: attr(data-autonum) " ";
}