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 ||
!ve.ui.MWTransclusionContextItem ||
!ve.ui.contextItemFactory ||
!ve.dm.MWTransclusionNode
) {
// Not inside VisualEditor, or VE moved on - do nothing.
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.
*
* target.wt "#display_diagram:Mermaid:Some_Page"
* target.function "display_diagram"
*
* @param {ve.dm.MWTransclusionNode} model
* @return {string|null} e.g. "Mermaid:Some_Page"
*/
function diagramPageFromModel( model ) {
var mwData, parts, i, target, 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++ ) {
target = parts[ i ] && parts[ i ].template && parts[ i ].template.target;
if ( !target || target.function !== 'display_diagram' ) {
continue;
}
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;
}
/**
* 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 item = this,
page = diagramPageFromModel( this.model ),
url = this.getDiagramEditUrl(),
$edit, $change;
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, '#' );
// Overriding onEditButtonClick costs the normal route to changing which
// diagram is embedded, so offer it explicitly.
$change = $( '<a>' )
.addClass( 'fd-ve-diagramContextItem-change' )
.attr( 'href', '#' )
.text( 'Change which diagram is shown' )
.on( 'click', function ( e ) {
e.preventDefault();
ve.ui.MWTransclusionContextItem.prototype.onEditButtonClick.call( item );
} );
this.$body.empty().append(
$( '<div>' ).append( $edit ),
$( '<div>' ).append( $change )
);
};
/* Registration */
ve.ui.contextItemFactory.register( FDDiagramContextItem );
}() );