MediaWiki:Gadget-FDVEDiagramLink.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*
* Gadget-FDVEDiagramLink.js
*
* Makes an embedded FlexDiagrams diagram actionable inside VisualEditor:
* double-click it (or use the context popup) to open that diagram's own editor
* in a new tab.
*
* Why a plugin rather than a link: VE deliberately neutralises links inside
* focusable nodes. ve.ce.FocusableNode.js binds a click handler that
* preventDefault()s left-clicks on anything not content-editable, with the
* comment "preventing default on mousedown doesn't suppress click events, so
* link navigation would still occur". The context popup, however, lives OUTSIDE
* the contenteditable surface, which is why plain anchors work there - see
* ve.ui.LinkContextItem.
*
* Loaded into VisualEditor through $wgVisualEditorPluginModules, so this file
* executes before the editor toolbar is built. The compact placeholder that
* replaces the raw diagram source is CSS, in [[MediaWiki:Common.css]].
*
* Maintainer note: VE here is the REL1_43 bundle and reports no version number,
* so there is nothing to gate on - the guard below feature-detects instead.
* APIs relied on: ve.ui.MWTransclusionContextItem, ve.ui.contextItemFactory,
* ve.dm.MWTransclusionNode, ve.ui.ModeledFactory's most-specific-wins rule, and
* ve.ce.MWTransclusionNode.executeCommand delegating to the context item.
* Re-check those after a MediaWiki upgrade.
*/
( function () {
'use strict';
if ( typeof ve === 'undefined' || !ve.ui || !ve.dm ) {
// Not inside VisualEditor - nothing to do, and nothing worth saying.
return;
}
// VE loads plugin modules as SIBLINGS of its own modules, not after them:
// ve.init.mw.ArticleTargetLoader builds
// [ 'ext.visualEditor.articleTarget', ...conf.pluginModules ]
// and loads that as one batch, so there is no ordering guarantee. The classes
// below therefore have to be pulled in through this gadget's own
// dependencies= in MediaWiki:Gadgets-definition
// (ext.visualEditor.core, ext.visualEditor.mwtransclusion).
//
// If that wiring is ever lost, this file would silently do nothing and the
// stock template context item would take over again - which is exactly how
// this failed the first time - so say so out loud.
if (
!ve.ui.MWTransclusionContextItem ||
!ve.ui.contextItemFactory ||
!ve.dm.MWTransclusionNode
) {
// eslint-disable-next-line no-console
console.warn( 'FDVEDiagramLink: VisualEditor classes not available at ' +
'plugin execution time; check dependencies= in MediaWiki:Gadgets-definition. ' +
'Diagram nodes will fall back to the stock template context item.' );
return;
}
/**
* Recover the diagram's page name from a transclusion model.
*
* Read from data-mw rather than the rendered DOM: it is the canonical model
* data, it covers all five diagram formats uniformly, and unlike the
* data-wiki-page attribute it does not depend on the local patch to
* FD_DisplayDiagram.php - so this keeps working even if an extension
* upgrade reverts that patch.
*
* Two shapes are recognised:
* {{Diagram|page=Mermaid:Foo}} -> params.page.wt (VE-editable)
* {{#display_diagram:Mermaid:Foo}} -> target.wt after ':' (legacy, still valid)
*
* @param {ve.dm.MWTransclusionNode} model
* @return {string|null} e.g. "Mermaid:Some_Page"
*/
function diagramPageFromModel( model ) {
var mwData, parts, i, part, target, params, wt, colon;
try {
mwData = model.getAttribute( 'mw' );
} catch ( e ) {
return null;
}
parts = mwData && mwData.parts;
if ( !Array.isArray( parts ) ) {
return null;
}
for ( i = 0; i < parts.length; i++ ) {
part = parts[ i ] && parts[ i ].template;
target = part && part.target;
if ( !target ) {
continue;
}
// Shape 1 - {{Diagram|page=Mermaid:Foo}}. Parsoid gives a template part
// a target.href and NO target.function, putting the page name in
// params.page. This is the shape VE can actually edit - parameters are
// editable, a parser-function target is not - which is the whole reason
// Template:Diagram exists.
if ( normaliseTemplateName( target ) === WRAPPER_TEMPLATE ) {
params = part.params;
wt = params && params.page && params.page.wt;
wt = ( wt === undefined || wt === null ) ? '' : String( wt ).trim();
if ( wt ) {
return wt;
}
continue;
}
// Shape 2 - a bare {{#display_diagram:Mermaid:Foo}}. Still supported:
// embeds predating the template use it, and it remains legal wikitext.
if ( target.function === 'display_diagram' ) {
wt = String( target.wt || '' );
colon = wt.indexOf( ':' );
if ( colon === -1 ) {
continue;
}
// Everything after "#display_diagram:" is the diagram page, which
// itself contains a namespace colon.
wt = wt.slice( colon + 1 ).trim();
if ( wt ) {
return wt;
}
}
}
return null;
}
// Name of the wrapper template, normalised (see normaliseTemplateName).
var WRAPPER_TEMPLATE = 'diagram';
/**
* Reduce a transclusion target to a bare, comparable template name.
* Parsoid hrefs look like "./Template:Diagram"; hand-written wikitext may say
* "Template:Diagram", "diagram" or use underscores.
*
* @param {Object} target
* @return {string} lower-cased name with no namespace prefix
*/
function normaliseTemplateName( target ) {
return String( ( target && ( target.href || target.wt ) ) || '' )
.replace( /^\.\//, '' )
.replace( /^Template:/i, '' )
.replace( /_/g, ' ' )
.trim()
.toLowerCase();
}
/**
* Context item shown when an embedded diagram is selected in VE.
*
* Subclasses the stock transclusion item on purpose: ve.ui.ModeledFactory
* keeps only the most specific class in an inheritance chain, so this
* replaces the template context item for diagram nodes and leaves every
* other template alone.
*
* @class
* @extends ve.ui.MWTransclusionContextItem
* @constructor
*/
function FDDiagramContextItem() {
FDDiagramContextItem.super.apply( this, arguments );
this.$element.addClass( 'fd-ve-diagramContextItem' );
}
OO.inheritClass( FDDiagramContextItem, ve.ui.MWTransclusionContextItem );
FDDiagramContextItem.static.name = 'fdDiagram';
FDDiagramContextItem.static.icon = 'articles';
FDDiagramContextItem.static.label = 'Diagram';
FDDiagramContextItem.static.modelClasses = [ ve.dm.MWTransclusionNode ];
FDDiagramContextItem.static.isCompatibleWith = function ( model ) {
return model instanceof ve.dm.MWTransclusionNode &&
!!diagramPageFromModel( model );
};
/**
* @return {string|null} URL of this diagram's editor
*/
FDDiagramContextItem.prototype.getDiagramEditUrl = function () {
var page = diagramPageFromModel( this.model );
return page ? mw.util.getUrl( page, { action: 'editdiagram' } ) : null;
};
FDDiagramContextItem.prototype.openDiagramEditor = function () {
var url = this.getDiagramEditUrl();
if ( url ) {
window.open( url, '_blank', 'noopener' );
}
};
/**
* Hijack the primary action. ve.ce.MWTransclusionNode.executeCommand looks
* for a context item that is an instanceof ve.ui.MWTransclusionContextItem
* and calls onEditButtonClick() on it, so overriding this also gives us
* double-click and Enter on the node for free.
*/
FDDiagramContextItem.prototype.onEditButtonClick = function () {
this.openDiagramEditor();
};
FDDiagramContextItem.prototype.renderBody = function () {
var page = diagramPageFromModel( this.model ),
url = this.getDiagramEditUrl(),
$edit;
if ( !page || !url ) {
// Should be unreachable - isCompatibleWith already required a page.
FDDiagramContextItem.super.prototype.renderBody.call( this );
return;
}
// A plain anchor is fine here: the context popup is outside the
// contenteditable surface, so VE does not suppress the click.
$edit = $( '<a>' )
.addClass( 'fd-ve-diagramContextItem-edit' )
.attr( { target: '_blank', rel: 'noopener' } )
.text( 'Edit “' + page.replace( /_/g, ' ' ) + '” ↗' );
ve.setAttributeSafe( $edit[ 0 ], 'href', url, '#' );
// Deliberately the only control here. There was also a "Change which
// diagram is shown" link that fell through to the stock transclusion
// dialog, but that dialog is empty for this node: #display_diagram is a
// parser function, so the diagram page is part of the target string
// rather than a parameter, and VE sources its parameter list from
// TemplateData, which a parser function has none of. It looked
// actionable and was not. To point an article at a different diagram,
// edit the page source - it is one obvious line of wikitext.
this.$body.empty().append( $( '<div>' ).append( $edit ) );
};
/* Registration */
ve.ui.contextItemFactory.register( FDDiagramContextItem );
}() );