VisualEditor/Gadgets

From MediaWiki.org
Jump to: navigation, search
Language:Project:Language policy English
DON'T PANIC

Guide to VisualEditor gadgets - a non formal guide for hacking VisualEditor.

  • Motivation: VisualEditor is a new interface for editing articles in Wikimedia projects. This visual editor will never replace the classic wikitext editor, but it is simple to use and preferred by new editors. Gadgets can extend and customize the visual editor and introduce new functionalities: to let more advanced users use more complicated options (such as timeline), to introduce work-flows that are project specific (such as deletion proposals), or to easily insert popular templates such as cite templates.
  • Note: VisualEditor is written in JavaScript, in fully object oriented mind - including the UI (dialogs, widgets, tools etc) and the model (which replace the simple Wikitext). If you want to really understand what's going there - read the live-updated documentation and for an overview the VisualEditor/API pages (which aren't yet fully written) - but if you just want to create cool gadgets that improve the user experience - read this guide. There's also a documented example for how to add a custom tool that inserts a template.

Read the guide
To understand how to hack VE
Go to code snippets
For quick hacking

Getting around[edit]

First of all you need to access the code with one (or more) of the following options:

  • Download the code from git:
cd extensions
git clone https://gerrit.wikimedia.org/r/p/mediawiki/extensions/VisualEditor.git 
cd VisualEditor
git submodule update --init
(for more information see: mw:Extension:VisualEditor)
  • Open an article, and edit it with visual editor. Open Web Developer tools (F12 in most browsers), and get to Scripts tab. You should look on "load.php....ext.visualEditor...". since MediaWiki ResourceLoader give you the compressed JS you most use a tool to "Beautify" the code (such option exist in Chrome and Firefox [Firebug extension]).
    • If you don't see the JS it may come from local cache - run in console localStorage.clear() and reload the page

You may use add ?debug=1 to the url to get non compressed JS - this is a bad idea for VE since loading may take several minutes

Debug diving[edit]

The Hitchhiker’s Guide to the VisualEditor gadgets has a few things to say on the subject: Good debugger is about the most massively useful thing a gadget writer can have. Since there is no good documentation yet for VisualEditor, and may never be, to understand how this black magic works you will have to dive into debugging. Don't worry - it's simple.

Assume we want to understand how transclusion of templates work inside VE:

  • To access VE object: ve.init.target.getSurface() for the active VisualEditor surface on the page.
  • Since we know (as users) that there is a dialog to insert templates, we should find this dialog in the code (search with ctrl+F). You should find a function called "ve.ui.MWTransclusionDialog.prototype.teardown" - place a breakpoint there
  • As user, open the dialog, select a template, fill some parameters, and then add the template to the page - you will hit the break point.
  • Inspecting the transclusion object we can see what it looks like under the hood:
{
	parts: [
		{ template: {
			target: {
				href: 'Template:TEMPLATENAME',
				wt: 'TEMPLATENAME'
			}
			params: {
				ParamName: { wt: Value },
				ParamName2: { wt: Value2 }
			}
		} }
	]
}
  • Now that we know how template code is represented - we can go on with the debugger, step by step and see how it is added to the document:
surfaceModel.getFragment().collapseRangeToEnd().insertContent( [
	{
		type: 'mwTransclusionInline',
		attributes: { mw: obj }
	},
	{ type: '/mwTransclusionInline' }
] ).collapseRangeToEnd().select();

Now that we understand how templates are represented, let's take a look on an example of UI class - toolbar. There is a configuration object for the main toolbar:

ve.init.Target.static.toolbarGroups = [
	// History
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-history' ),
		include: [ 'undo', 'redo' ]
	},
	// Format
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-paragraph-format' ),
		type: 'menu',
		indicator: 'down',
		title: OO.ui.deferMsg( 'visualeditor-toolbar-format-tooltip' ),
		include: [ { group: 'format' } ],
		promote: [ 'paragraph' ],
		demote: [ 'preformatted', 'blockquote' ]
	},
	// Text style
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-text-style' ),
		title: OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),
		include: [ 'bold', 'italic', 'moreTextStyle' ]
	},
	// Link
	{
		header: OO.ui.deferMsg( 'visualeditor-linkinspector-title' ),
		include: [ 'link' ]
	},
	// Structure
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),
		type: 'list',
		icon: 'listBullet',
		indicator: 'down',
		include: [ { group: 'structure' } ],
		demote: [ 'outdent', 'indent' ]
	},
	// Insert
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
		type: 'list',
		icon: 'insert',
		label: '',
		title: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
		indicator: 'down',
		include: '*'
	},
	// Special character toolbar
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
		include: [ 'specialCharacter' ]
	},
	// Table
	{
		header: OO.ui.deferMsg( 'visualeditor-toolbar-table' ),
		type: 'list',
		icon: 'table',
		indicator: 'down',
		include: [ { group: 'table' } ],
		demote: [ 'deleteTable' ]
	}
];

Where * includes all the registered tools. (What are those tools? you can take a look/debug the function OO.ui.ToolGroup.prototype.populate to understand how it works - it loads all the registered commands, removed already used, remove the exclude and order using demote/promote).

Deployment[edit]

The above describes how to code a script for VE. Once your script is ready, you would like to publish it so other editors can use it. As other scripts, this can be done in one of the following options:

  • Gadget - turn it as a gadget that user can select it in their preferences (requires sysop rights). This is preferred method since it allows users to to install the gadget without code editing.
  • User script - users can install your script by adding code snippet to Special:Mypage/common.js

Gadget - Registering VE plugin[edit]

Registering your script in MediaWiki:Gadgets-definition allows other editors to turn your use using Special:Preferences (in Gadgets tab). The following section describes shortly how to do it, and details the specific requirements for a VE gadget. A more detailed explanation for general gadget can be found in Extension:Gadgets#Usage.

Since VE is heavy module, you must not create a gadget that dependent on VE internals. Instead you should create 2 gadgets:

  • A real gadget - that may be dependent on VE internals, but users shouldn't be able to turn it on (use hidden). The gadget code could be derived from #Code snippets below.
  • Create a "gadget loader" - a small gadget that tells VE to load your real gadget once VE is activated by the user

For example:

Gadget loader Real gadget
Definition GadgetLoaderNAME[ResourceLoader|dependencies=ext.visualEditor.desktopArticleTarget.init]|GadgetLoaderNAME.js RealGadgetName[ResourceLoader|rights=hidden|hidden|dependencies=ext.visualEditor.core]|RealGadgetName.js
Code
mw.libs.ve.addPlugin( 'ext.gadget.RealGadgetName' );
(gadget-specific code. see examples below)

User script[edit]

If you don't have admin rights, your script hasn't been tested enough or the target audience for your script is small, you can let users install the script by editing their personal JS (Special:Mypage/common.js). Afterwards you can publish it as a user script in pages such as en:Wikipedia:User scripts.

Here we use client side API for ResourceLoader to do the same as the above section, e.g. to add our script to be loaded when user open VE.

mw.loader.using( 'ext.visualEditor.desktopArticleTarget.init', function () {
	// Register plugins to VE. will be loaded once the user opens VE
	mw.libs.ve.addPlugin( function () { 
		return $.getScript( /* URL to user script */ ); 
	} );
});

Example for a URL for user script:

  • //meta.wikimedia.org/w/index.php?title=User:Jimbo_Wales/MyGadget.js&action=raw&ctype=text/javascript
  • Assuming the gadget code is located in meta:User:Jimbo_Wales/MyGadget.js, and contains code such as derived code from #Code snippets below.

Code snippets[edit]

Running code after VisualEditor is activated[edit]

To run code once VisualEditor is activated and ready to use:

mw.hook( 've.activationComplete' ).add( function () {
	// SOME CODE TO RUN WHEN EDIT SURFACE IS READY
	var surface = ve.init.target.getSurface();
} );

To run code before the target starts to initialise, you can register a plugin module:

mw.libs.ve.addPlugin( function ( target ) {
    ve.ui.MyTool = function () { /* ... */ }
} );

Checking if VisualEditor is in regular 'visual' mode or 'source' mode[edit]

NB: This is currently a beta feature so the APIs may not yet be finalised

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

if ( surface.getMode() === 'visual' ) {
    // Visual mode
} else if ( surface.getMode() === 'source' ) {
    // Source mode
}

Checking whether VisualEditor is currently open[edit]

If you have a button that should act differently depending on whether the wikitext or the visual editor is in use:

if ( window.ve && ve.init && ve.init.target && ve.init.target.active ) {
    // User is currently editing a page using VisualEditor
}

User's position in the model[edit]

var surfaceModel = ve.init.target.getSurface().getModel();
var selection = surfaceModel.getSelection();
// If selection is an instance of ve.dm.LinearSelection (as opposed to NullSelection or TableSelection)
// you can get a range (start and end offset) using:
var range = selection.getRange();
// Get the current position "from"
var selectedRange = new ve.Range(range.from);

Adding templates[edit]

The following code adds Template:cite web as an example for adding template to page:

var surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().collapseToEnd().insertContent(
  [
    {
      type: 'mwTransclusionInline',
      attributes: {
        mw: {
          parts: [
            {
              template: {
                target: {
                  href: 'Template:cite web',
                  wt: 'cite web'
                },
                params: {
                  first: { wt: 'first name' },
                  last: { wt: 'last name' },
                  title: { wt: 'title' },
                  url: { wt: 'http://en.wikipedia.org' }
                }
              }
            }
          ]
        }
      }
    }
  ]
).collapseToEnd().select();

Replacing text[edit]

// range of text to replace (omit the end parameter to just insert at 'start')
var rangeToRemove = new ve.Range( start, end );
var surfaceModel = ve.init.target.getSurface().getModel();
var fragment = surfaceModel.getLinearFragment( rangeToRemove );
fragment.insertContent( newText );

Adding a table[edit]

var surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment()
	.adjustLinearSelection( 1 )
	.collapseToStart()
	.insertContent( [
		{ type: 'table' },
		{ type: 'tableCaption' },
			{ type: 'paragraph' }, 'C', 'a', 'p', 't', 'i', 'o', 'n', { type: '/paragraph' },
		{ type: '/tableCaption' },
		{ type: 'tableSection', attributes: { style: 'body' } },
		{ type: 'tableRow' },
		{ type: 'tableCell', attributes: { style: 'header' } },
			{ type: 'paragraph' }, 'A', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: 'tableCell', attributes: { style: 'data' } },
			{ type: 'paragraph' }, 'A', '1', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: 'tableCell', attributes: { style: 'data' } },
			{ type: 'paragraph' }, 'A', '2', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: 'tableCell', attributes: { style: 'data' } },
			{ type: 'paragraph' }, 'A', '3', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: '/tableRow' },
		{ type: 'tableRow' },
		{ type: 'tableCell', attributes: { style: 'header' } },
			{ type: 'paragraph' }, 'B', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: 'tableCell', attributes: { style: 'data' } },
			{ type: 'paragraph' }, 'B', '1', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: 'tableCell', attributes: { style: 'data' } },
			{ type: 'paragraph' }, 'B', '2', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: 'tableCell', attributes: { style: 'data' } },
			{ type: 'paragraph' }, 'B', '3', { type: '/paragraph' },
		{ type: '/tableCell' },
		{ type: '/tableRow' },
		{ type: '/tableSection' },
		{ type: '/table' }
	] );

Adding/modifying/removing an internal link[edit]

Internal links in VisualEditor are implemented as "annotations" (additional information that can be applied to a part of text) of type link/mwInternal. Text styling like bold and italics are also annotations (textStyle/bold, textStyle/italic).

To convert selected text to a link to page "VisualEditor", section "Status":

var title = mw.Title.newFromText( 'VisualEditor#Status' );
var linkAnnotation = ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title );
var surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().annotateContent( 'set', 'link/mwInternal', linkAnnotation );

To remove internal links in selected text:

var surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().annotateContent( 'clear', 'link/mwInternal' );

To make selected text bold:

var surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().annotateContent( 'set', 'textStyle/bold' );

Triggering a command based on a text sequence[edit]

To use an example already in the codebase, typing two open braces triggers the transclusion command which opens the template dialog. The last argument (2) specifies how many characters to delete after the sequence is matched.

ve.ui.sequenceRegistry.register(
	new ve.ui.Sequence( 'wikitextTemplate', 'transclusion', '{{', 2 )
);

Real examples for gadgets/scripts that interact with VE[edit]

Here are some real world gadgets/scripts for VE. You can use them as is or use them as example for building your own gadgets!