VisualEditor/Gadgets
This is a non formal guide for writing gadgets for VisualEditor.
- Motivation: VisualEditor is an interface for editing articles in Wikimedia projects. 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 all technical details, read the live-updated documentation and for an overview the VisualEditor/API pages (which aren't yet fully written). However, if you just want to create gadgets that improve the user experience, read this guide.
Getting around
editFirst of all you need to access the code with one (or more) of the following options:
cd extensions
git clone https://gerrit.wikimedia.org/r/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 go to the Sources or Debugger tab. You should look at "load.php....ext.visualEditor...". Since MediaWiki ResourceLoader gives you the compressed JS you should use a tool to "Pretty print" or "Beautify" the code (such options exist in Chrome and Firefox).
- 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
editSome or all of the information on this page is inconsistent, irrelevant or confusing. Please help clean it up if you are able. |
A good debugger is the most useful thing that you can have as a gadget writer. Since there is no good documentation yet for VisualEditor, to understand how everything works you will have to dive into debugging.
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.onAddTemplateButtonClick" - 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
editThe 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
editRegistering 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 | ||
---|---|---|---|
Desktop only | Definition | GadgetLoaderNAME[ResourceLoader|dependencies=ext.visualEditor.desktopArticleTarget.init]|GadgetLoaderNAME.js | RealGadgetName[ResourceLoader|hidden|dependencies=ext.visualEditor.core]|RealGadgetName.js |
Code | mw.libs.ve.addPlugin( 'ext.gadget.RealGadgetName' );
|
(gadget-specific code. see examples below) | |
Desktop and mobile | Definition | GadgetLoaderNAME[ResourceLoader|dependencies=ext.visualEditor.targetLoader]|GadgetLoaderNAME.js | RealGadgetName[ResourceLoader|hidden|dependencies=ext.visualEditor.core]|RealGadgetName.js |
Code | mw.libs.ve.targetLoader.addPlugin( 'ext.gadget.RealGadgetName' );
|
(gadget-specific code. see examples below) |
User script
editIf you don't have admin rights, or 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.
Desktop | mw.loader.using( 'ext.visualEditor.desktopArticleTarget.init', () => {
// Register plugins to VE. Will be loaded once the user opens VE
mw.libs.ve.addPlugin(
() => mw.loader.getScript( /* URL to user script */ )
);
} );
|
---|---|
Desktop and mobile | mw.loader.using( 'ext.visualEditor.targetLoader', () => {
// Register plugins to VE. Will be loaded once the user opens VE
mw.libs.ve.targetLoader.addPlugin(
() => mw.loader.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
editRunning code after VisualEditor is activated
editTo run code once VisualEditor is activated and ready to use:
mw.hook( 've.activationComplete' ).add( () => {
// Some code to run when edit surface is ready
const surface = ve.init.target.getSurface();
} );
To run code before the target starts to initialise, you can register a plugin module:
mw.libs.ve.addPlugin( ( target ) => {
ve.ui.MyTool = function () { /* ... */ }
} );
If you just want to run your code on the source editor, you can listen to the 've.wikitextInteractive' hook, like so:
mw.hook( 've.wikitextInteractive' ).add( () => {
// Some code to run when the 2017 wikitext editor is ready
} );
Checking if VisualEditor is in regular 'visual' mode or 'source' mode
editNB: This is currently a beta feature so the APIs may not yet be finalised
const surface = ve.init.target.getSurface();
if ( surface.getMode() === 'visual' ) {
// Visual mode
} else if ( surface.getMode() === 'source' ) {
// Source mode
}
Checking whether VisualEditor is currently open
editIf 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
editconst surfaceModel = ve.init.target.getSurface().getModel();
const 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:
const range = selection.getRange();
// Get the current position "from"
const selectedRange = new ve.Range( range.from );
Adding templates
editThe following code adds Template:cite web as an example for adding template to page:
const 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// note that this will be treated as raw text in "visual" mode (not as list)
// will become a list in "source" mode though
const newText = `Some text or wikicode to insert:
* like maybe
* some lists
* and stuff...
`
// Replace text
const start = 1;
const end = 3;
const rangeToRemove = new ve.Range( start, end );
const surfaceModel = ve.init.target.getSurface().getModel();
const fragment = surfaceModel.getLinearFragment( rangeToRemove );
fragment.insertContent( newText );
// Insert text at start
const startPostion = new ve.Range( 1 );
const surfaceModel = ve.init.target.getSurface().getModel();
const fragment = surfaceModel.getLinearFragment( startPostion );
fragment.insertContent( newText );
Adding a table
editconst surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment()
.adjustLinearSelection( 1 )
.collapseToStart()
.insertContent( [
{ type: 'table' },
{ type: 'tableCaption' },
{ type: 'paragraph' }, ...'Caption', { 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 a link
editLinks in VisualEditor are implemented as "annotations" (additional information that can be applied to a part of text) of type link/mwInternal
or link/mwExternal
. 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":
const title = mw.Title.newFromText( 'VisualEditor#Status' ),
linkAnnotation = ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title ),
surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().annotateContent( 'set', 'link/mwInternal', linkAnnotation );
To remove internal links in selected text:
const surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().annotateContent( 'clear', 'link/mwInternal' );
To convert selected text to an external link:
var surfaceModel = ve.init.target.getSurface().getModel(),
linkAnnotation = new ve.dm.MWExternalLinkAnnotation( {
type: 'link/mwExternal',
attributes: { href: 'https://www.example.com' }
} );
surfaceModel.getFragment().annotateContent( 'set', 'link/mwExternal', linkAnnotation );
To make selected text bold:
const surfaceModel = ve.init.target.getSurface().getModel();
surfaceModel.getFragment().annotateContent( 'set', 'textStyle/bold' );
Implementing a custom command
editThings that the user can do in the editor are represented by commands. They can be associated with toolbar tools, keyboard shortcuts (triggers) or text sequences to allow them to be used (see below). A single command can be associated with multiple tools/triggers/sequences (e.g. bold
command is used by the trigger for Ctrl+B and by the Bold tool).
See also complete documented example scripts:
- How to add a custom tool that inserts a template with default parameters
- How to create a custom command to modify page content (in visual and source mode)
Most commands execute built-in actions: for example, the command below inserts a newline (<br>
) at cursor position.
ve.ui.commandRegistry.register(
new ve.ui.Command(
// Command name
'myBreak',
// Type and name of the action to execute
'content', 'insert', // Calls the ve.ui.ContentAction#insert method
{
// Extra arguments for the action
args: [
// Content to insert
[
{ type: 'break' },
{ type: '/break' }
],
// Annotate content to match surrounding annotations?
true,
// Move cursor to after the new content? (otherwise - select it)
true
],
supportedSelections: [ 'linear' ]
}
)
);
However, commands can execute arbitrary code. For example, the command below displays a warning popup.
ve.ui.MyWarningPopupCommand = function VeUiMyWarningPopupCommand() {
ve.ui.MyWarningPopupCommand.super.call(
this,
'myWarningPopup' // Command name
);
};
OO.inheritClass( ve.ui.MyWarningPopupCommand, ve.ui.Command );
// Code to run when this command is executed
ve.ui.MyWarningPopupCommand.prototype.execute = function () {
mw.notify( 'You did something wrong!' );
return true; // true means the command executed correctly
};
ve.ui.commandRegistry.register( new ve.ui.MyWarningPopupCommand() );
Triggering a command using a toolbar tool
editThis adds a new tool to the toolbar (under the "Insert" menu) to insert a line break at cursor position, using the command we defined above.
ve.ui.MyBreakTool = function VeUiMyBreakTool() {
ve.ui.MyBreakTool.super.apply( this, arguments );
};
OO.inheritClass( ve.ui.MyBreakTool, ve.ui.Tool );
ve.ui.MyBreakTool.static.name = 'myBreak';
ve.ui.MyBreakTool.static.group = 'insert';
ve.ui.MyBreakTool.static.title = 'Line break';
ve.ui.MyBreakTool.static.commandName = 'myBreak';
ve.ui.toolFactory.register( ve.ui.MyBreakTool );
Use a wiki Message instead of a fixed title
editIf you want to get the title of you toolbar button from MediaWiki:my-message, you can use :
ve.ui.MyBreakTool.static.title = OO.ui.deferMsg( 'my-message' );
But you have to load the messages (here the syntax to load several messages) :
mw.loader.using( [ 'mediawiki.api', 'mediawiki.jqueryMsg' ] )
.then( () => new mw.Api().loadMessagesIfMissing( [ 'my-message','my-message2' ] ) );
Triggering a command using a keyboard shortcut
editThis adds a new keyboard shortcut Ctrl+Shift+Y (or Cmd+Shift+Y on Mac) to insert a line break at cursor position, using the command we defined above.
ve.ui.triggerRegistry.register(
'myBreak', // Command name
{
mac: new ve.ui.Trigger( 'cmd+shift+y' ),
pc: new ve.ui.Trigger( 'ctrl+shift+y' )
}
);
Triggering a command using a text sequence
editThis adds a new text sequence 'kitten'. When the user types that, a warning message pops up using the command we defined above.
ve.ui.sequenceRegistry.register(
new ve.ui.Sequence(
'myWarningPopup', // Sequence name
'myWarningPopup', // Command name
'kitten', // Text to detect
6 // Number of characters to delete after the sequence is matched:
// in this case, remove the entire 'kitten'
)
);
Add 'Center' to the Format list
editThis script adds an option 'Center' in the Format dropdown menu:
mw.loader.using( [ 'ext.visualEditor.core', 'ext.visualEditor.mwtransclusion' ] ).then( () => {
// --------- (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 () {
const surfaceModel = this.surface.getModel(),
selection = surfaceModel.getSelection();
currentFragment = surfaceModel.getFragment( null, true );
if ( !( selection instanceof ve.dm.LinearSelection ) ) {
return false;
}
if ( !this.isWrapped() ) {
return false;
}
const leaves = currentFragment.getSelectedLeafNodes();
const leavesRange = new ve.Range(
leaves[ 0 ].getRange().start,
leaves[ leaves.length - 1 ].getRange().end
);
const leavesFragment = surfaceModel.getLinearFragment( leavesRange, true );
leavesFragment
// 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' ] }
)
);
} );
Add a 'Reference list' when the first reference is added and there is no Reference list yet
editUnfortunately this feature that could be useful has been refused in core VE, so you can use this script:
function addReferencesIfMissing() {
// This function is based on gerrit:149117, original author: Alex Monk
const surfaceModel = this.getFragment().getSurface(),
refGroup = this.referenceGroupInput.$input.val();
this.referenceModel.setGroup( refGroup );
// TODO: This is probably the wrong way to do this.
const existingRefLists = $.grep(
surfaceModel.getDocument().getFullData(),
( element ) => element.type === 'mwReferencesList' && element.attributes.refGroup === refGroup
);
if ( !existingRefLists.length ) {
surfaceModel
.getFragment( null, true )
.expandLinearSelection( 'root' )
.collapseToEnd()
.insertContent( [
{
type: 'mwReferencesList',
attributes: {
listGroup: 'mwReference/' + refGroup,
refGroup: refGroup
}
},
{ type: '/mwReferencesList' }
]
);
}
}
mw.loader.using( 'ext.cite.visualEditor' ).then( () => {
// origFunc() is what is normally called when you click "Insert"
// in the Reference dialog (Cite+VisualEditor).
// We wrap around this function, calling first the original function (which adds <ref>),
// and then addReferencesIfMissing(), which adds <references/>.
const origFunc = ve.ui.MWReferenceDialog.prototype.getActionProcess;
ve.ui.MWReferenceDialog.prototype.getActionProcess = function ( action ) {
let result = origFunc.apply( this, arguments );
if ( action === 'insert' || action === 'done' ) {
result = result.next( addReferencesIfMissing.bind( this ) );
}
return result;
};
} );
Real examples for gadgets/scripts that interact with VE
editHere are some real world gadgets/scripts for VE. You can use them as is or use them as example for building your own gadgets!
- en:MediaWiki:Gadget-defaultsummaries.js – Adds two new dropdown boxes below the edit summary box with some useful default summaries.
- pl:MediaWiki:Gadget-edit-summaries.js – edit summary buttons for both VE and standard editor.
- he:MediaWiki:Gadget-VeDirectionMarkTool.js, he:MediaWiki:Gadget-VeExtendedBar.js – Adds a button for inserting a common template (this is also an example of how to use addPlugin).
- en:User:Eran/refToolbarVe.js, en:User:Eran/refToolbarVeLoader.js – Adds a button for cite dialog, in which user can select a cite template to be included in article.
- en:User:ערן/veReplace.js – Search and replace. Demonstration of how to interact with the underlying model and create dialogs.
- meta:User:Matma Rex/visualeditor-signature.js – A tool to insert a signature (~~~~) that is correctly rendered and formatted in the editor.
- de:Benutzer:Schnark/js/veAutocorrect.js – User script that implements an autocorrect feature, mostly for typographical replacements.
- de:Benutzer:Schnark/js/veSummary.js — "Very hackish" user script for providing access to previous edit summaries.
- zh:MediaWiki:Gadget-EditorAPIs.js — A library that presents a common interface for wikitext editing. Useful for 2017 source editor.
- pl:MediaWiki:Gadget-ref-klawiatura.js — A gadget that inserts <ref name="" /> in any code editor (including 2017 VE Wikicode Editor)
See also
edit- m:Wikimedia Blog/Drafts/VisualEditor gadgets for an overview of the project
- m:Grants:IEG/Visual editor- gadgets compatibility for the Grants page of the project