VisualEditor/Gadgets/Add a tool

This page shows you a commented example for writing gadgets for the VisualEditor: The code creates an entry in the toolbar to insert a template with parameters.

Example code edit

To test the following code, you can execute it in your browser's console before VE is loaded and then start editing in VE, i.e., click "Edit". The new tool will be available under "Insert" → "More" → "My tool".

function makeMyTool() {
	//Create and register command
	var myTemplate = [ {
		type: 'mwTransclusionBlock',
		attributes: {
			mw: {
				parts: [ {
					template: {
						target: {
							href: 'Template:MyTemplate',
							wt: 'MyTemplate'
						},
						params: {
							1: {
								wt: 'my parameter'
							}
						}
					}
				} ]
			}
		}
	}, {
		type: '/mwTransclusionBlock'
	} ];

	ve.ui.commandRegistry.register(
		new ve.ui.Command( 'mycommand', 'content', 'insert', {
			args: [ myTemplate, false, true ],
			supportedSelections: [ 'linear' ]
		} )
	);

	//Create and register wikitext command
	if ( ve.ui.wikitextCommandRegistry ) {
		ve.ui.wikitextCommandRegistry.register(
			new ve.ui.Command( 'mycommand', 'mwWikitext', 'wrapSelection', {
				args: [ '{{MyTemplate|', '}}', 'my parameter' ],
				supportedSelections: [ 'linear' ]
			} )
		);
	}

	//Create and register tool
	function MyTool() {
		MyTool.parent.apply( this, arguments );
	}
	OO.inheritClass( MyTool, ve.ui.MWTransclusionDialogTool );

	MyTool.static.name = 'mytool';
	MyTool.static.group = 'insert';
	MyTool.static.title = 'My tool';
	MyTool.static.commandName = 'mycommand';
	ve.ui.toolFactory.register( MyTool );

}

// Initialize
mw.hook( 've.loadModules' ).add( function( addPlugin ) {
	addPlugin( makeMyTool );
} );

Explanation edit

Let's have a look at what the above code does:

Create and register command edit

First we create and register a command to insert our template. The variable myTemplate stores the VE-representation of the template we want to insert, it corresponds to the wikitext {{MyTemplate|my parameter}}. The format is similar to XML: It starts with an opening tag (mwTransclusionBlock, use mwTransclusionInline for an inline template) with some attributes (the name of the template and the parameters) and is followed by a closing tag (preceded by a slash). If you want to see the data for the current page, just execute ve.init.target.getSurface().getModel().getDocument().data in the browser console while editing.

To create and register the command we create a new instance of ve.ui.Command (documentation) and register it with the ve.ui.commandRegistry. Let's have a look at some of the parameters:

  • 'mycommand': That's the internal name of the tool. You should choose it in a way that makes it unique. If you are using scripts by other authors, too, you could prepend your username to make sure nobody else uses the same identifier.
  • 'content', 'insert': That's the action and method we want to execute. In this case we use the insert method of ve.ui.ContentAction (documentation) to insert our template.

The parameters for the method are specified in the args array:

  • The first entry is the content we want to insert. We stored it in the myTemplate variable above.
  • Next comes a flag whether the new content should take the current annotation. We set it to false. This means that even if the cursor is inside an italic or bold text when the user inserts the template, the template will not take that text format.
  • Last comes another flag whether the selection should be collapsed, or whether the new content should be selected. We set it to true, so the template will not be selected.

Create and register wikitext command edit

A source code editor mode inside VE is currently under development. Actually the following code isn't necessary, if you don't add it, the source code editor will use the above command and just work as expected. But you can tweak the command a bit for wikitext if you want to.

We wrap the code in an if-clause, so it will not break when the wikitext editor isn't available. The code itself is very similar to the code that creates and registers the default command. We use the same name for the command. Actually we could even use the insert method from above, only with wikitext instead of our myTemplate variable. But let's use the wrapSelection method from ve.ui.MWWikitextAction (documentation) instead. As above we specify the parameters in the args array: The content that should go before the cursor, the content to go after it, and the content that should be inserted and selected in the middle unless there is already a selection. If you programmed with the traditional wikitext editor before, you will now these parameters as pre, post and peri.

Create and register tool edit

Now that we have our command we create and register a tool to invoke it from the toolbar. To do so we create a new class MyTool, inheriting from ve.ui.MWTransclusionDialogTool (documentation). To learn more about how inheriting works, you should read the OOjs documentation.

Note the title, which will be shown in the toolbar, and the command, which links the tool to our command. Registering the tool is all we need to include it in the toolbar, as the definition has a “catch all” rule to include all tools that haven't been included explicitly (code). There are some more options you can configure, e.g. the icon (list of available icons, make sure to use a lowercase first letter, but otherwise keep the capitalization).

If you want the tool to be shown in some other place, you could change the group. E.g. setting it to 'textStyle' will show the tool among the other text style tools.

Initialize edit

The initialization code makes sure the code is executed at the right time: After all necessary modules have been loaded, but before the editor is initialized.

function makeMyTool() {
 // ...
}

mw.hook( 've.loadModules' ).add( function( addPlugin ) {
	addPlugin( makeMyTool );
} );

Alternatively, you can split it up into two scripts. First, an unconditional script that listens for when VisualEditor is used, and then calls addPlugin() to load the second script:

mw.hook( 've.loadModules' ).add( function( addPlugin ) {
	addPlugin( function() {
		return mw.loader.getScript('https://www.mediawiki.org/w/index.php?title=User:Me/myScript.js&action=raw&ctype=text/javascript');
	} );
} );

The second script then only contains the main plugin content, which would otherwise be inside the makeMyTool function.

If you need to use additional modules as dependencies within the registration statements of your plugin, you can delay it by calling mw.loader.using first:

mw.hook( 've.loadModules' ).add( function( addPlugin ) {
	addPlugin( function() {
		return mw.loader.using( [ 'mediawiki.util', 'ext.visualEditor.mwtransclusion' ] ).then( function () {
			return mw.loader.getScript('https://www.mediawiki.org/w/index.php?title=User:Me/myScript.js&action=raw&ctype=text/javascript');
		} );
	} );
} );

More ideas edit

Here are some more ideas to try out yourself.

Keyboard shortcut edit

Perhaps you don't want to use the mouse to insert the template but want to use a keyboard shortcut. No problem, create and register a trigger (documentation):

ve.ui.triggerRegistry.register(
    'mycommand', {
        mac: new ve.ui.Trigger('cmd+shift+t'),
        pc: new ve.ui.Trigger('ctrl+shift+t')
    }
);

Keyboard sequence edit

Or you want the template to be inserted when you type {T}. Create and register a sequence (documentation):

ve.ui.sequenceRegistry.register(
	new ve.ui.Sequence('mysequnce', 'mytool', '{T}', 3)
);

The 3 at the end is the number of characters that should be deleted, in this case the length of {T}.

Help entry edit

The sequence isn't obvious, so you might want to add it to the help dialog. You probably guessed how to do that: Register an entry with the correct registry. The registry is called ve.ui.commandHelpRegistry, why don't you look up the documentation and try it yourself?

Open a dialog edit

Perhaps you want to open the dialog to edit the template parameters after inserting the template. To do so we use a custom command:

function InsertAndOpenCommand( name, options ) {
	InsertAndOpenCommand.parent.call( this, name, null, null, options );
}
OO.inheritClass( InsertAndOpenCommand, ve.ui.Command );

InsertAndOpenCommand.prototype.execute = function( surface, args ) {
	args = args || this.args;
	surface.getModel().getFragment().collapseToEnd().insertContent( args[0], args[1] ).select();
	surface.execute( 'window', 'open', 'transclusion' );
	return true;
};

This command just inserts the content as above, but then also opens the transclusion dialog for the selected content. Instead of the original command you now have to register this new command:

ve.ui.commandRegistry.register(
	new InsertAndOpenCommand( 'mycommand', {
		args: [ myTemplate, false ],
		supportedSelections: [ 'linear' ]
	} )
);

We defined the command in such a way that the parameters are almost the same, you have to leave out the action and method, and you don't need to specify the “collapse to end” option, because the template will be selected anyway.

Add your own "toolgroup" edit

Adding a top-level group to the toolbar in a specific position is slightly complicated, due to the initialization order involved and how the toolbar's contents are specified. There's two approaches that you can take, depending on whether you can be confident your code will be loaded before or after VE is initialized.

If before, you can just add this to the example above. It will define a group and add it to the toolbar configuration that'll be used to create the toolbars.

mw.loader.using( [ 'ext.visualEditor.mediawiki' ] ).then( function() {
	function addGroup( target ) {
		target.static.toolbarGroups.push( {
			name: 'mytoolgroup',
			label: 'My Group',
			type: 'list',
			indicator: 'down',
			include: [ { group: 'mytoolgroup' } ],
		} );
	}
	for ( var n in ve.init.mw.targetFactory.registry ) {
		addGroup( ve.init.mw.targetFactory.lookup( n ) );
	}
	ve.init.mw.targetFactory.on( 'register', function ( name, target ) {
		addGroup( target );
	} );
} );

...and adjust your tool definition to:

MyTool.static.group = 'mytoolgroup';
MyTool.static.autoAddToCatchall = false;
MyTool.static.autoAddToGroup = true;

If you can't guarantee that your code will be loaded before the VE surface is initialized, you can instead listen for the hook ve.activationComplete and then use the addItems method of the already-initialized VE's toolbar object to add additional toolbar groups.

function integrateIntoVE() {

    function Tool1() {
        Tool1.super.apply( this, arguments );
    }
    OO.inheritClass( Tool1, OO.ui.Tool );
    Tool1.static.name = 'mytool1';
    Tool1.static.title = 'Tool 1';
    Tool1.prototype.onUpdateState = function () {};
    Tool1.prototype.onSelect = function () {
        //Implement me
    };

    function Tool2() {
        Tool2.super.apply( this, arguments );
    }
    OO.inheritClass( Tool2, OO.ui.Tool );
    Tool2.static.name = 'mytool2';
    Tool2.static.title = 'Tool 2';
    Tool2.prototype.onUpdateState = function () {};
    Tool2.prototype.onSelect = function () {
        //Implement me
    };

    toolbar = ve.init.target.getToolbar();
    myToolGroup = new OO.ui.ListToolGroup( toolbar, {
        title: 'My tools',
        include: [ 'mytool1', 'mytool2' ]
    } );
    ve.ui.toolFactory.register( Tool1 );
    ve.ui.toolFactory.register( Tool2 );
    toolbar.addItems( [ myToolGroup ] );
}

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