MediaWiki:Gadget-FDVEDiagramLink.js: Difference between revisions

VE plugin: double-click an embedded diagram to open its editor in a new tab
 
Restore "Change which diagram is shown" for template-wrapped embeds, where the page name is now an editable parameter
 
(3 intermediate revisions by the same user not shown)
Line 28: Line 28:
'use strict';
'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 (
if (
typeof ve === 'undefined' || !ve.ui || !ve.dm ||
!ve.ui.MWTransclusionContextItem ||
!ve.ui.MWTransclusionContextItem ||
!ve.ui.contextItemFactory ||
!ve.ui.contextItemFactory ||
!ve.dm.MWTransclusionNode
!ve.dm.MWTransclusionNode
) {
) {
// Not inside VisualEditor, or VE moved on - do nothing.
// 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;
return;
}
}
Line 47: Line 65:
* upgrade reverts that patch.
* upgrade reverts that patch.
*
*
target.wt       "#display_diagram:Mermaid:Some_Page"
* Two shapes are recognised:
target.function "display_diagram"
{{Diagram|page=Mermaid:Foo}}    -> params.page.wt       (VE-editable)
*  {{#display_diagram:Mermaid:Foo}} -> target.wt after ':'  (legacy, still valid)
*
*
* @param {ve.dm.MWTransclusionNode} model
* @param {ve.dm.MWTransclusionNode} model
Line 54: Line 73:
*/
*/
function diagramPageFromModel( model ) {
function diagramPageFromModel( model ) {
var mwData, parts, i, target, wt, colon;
var info = diagramInfoFromModel( model );
return info ? info.page : null;
}
 
/**
* @param {ve.dm.MWTransclusionNode} model
* @return {Object|null} { page: string, isTemplate: boolean }, or null.
*  isTemplate distinguishes {{Diagram|page=}} - whose page name is an
*  editable parameter - from a bare {{#display_diagram:}}, whose page name
*  is part of the parser-function target and cannot be edited in VE.
*/
function diagramInfoFromModel( model ) {
var mwData, parts, i, part, target, params, wt, colon;


try {
try {
Line 67: Line 98:


for ( i = 0; i < parts.length; i++ ) {
for ( i = 0; i < parts.length; i++ ) {
target = parts[ i ] && parts[ i ].template && parts[ i ].template.target;
part = parts[ i ] && parts[ i ].template;
if ( !target || target.function !== 'display_diagram' ) {
target = part && part.target;
if ( !target ) {
continue;
continue;
}
}
wt = String( target.wt || '' );
 
colon = wt.indexOf( ':' );
// Shape 1 - {{Diagram|page=Mermaid:Foo}}. Parsoid gives a template part
if ( colon === -1 ) {
// 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 { page: wt, isTemplate: true };
}
continue;
continue;
}
}
// Everything after "#display_diagram:" is the diagram page, which
 
// itself contains a namespace colon.
// Shape 2 - a bare {{#display_diagram:Mermaid:Foo}}. Still supported:
wt = wt.slice( colon + 1 ).trim();
// embeds predating the template use it, and it remains legal wikitext.
if ( wt ) {
if ( target.function === 'display_diagram' ) {
return wt;
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 { page: wt, isTemplate: false };
}
}
}
}
}
return null;
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();
}
}


Line 142: Line 214:
FDDiagramContextItem.prototype.renderBody = function () {
FDDiagramContextItem.prototype.renderBody = function () {
var item = this,
var item = this,
page = diagramPageFromModel( this.model ),
info = diagramInfoFromModel( this.model ),
url = this.getDiagramEditUrl(),
url = this.getDiagramEditUrl(),
$edit, $change;
$edit, $change;


if ( !page || !url ) {
if ( !info || !url ) {
// Should be unreachable - isCompatibleWith already required a page.
// Should be unreachable - isCompatibleWith already required a page.
FDDiagramContextItem.super.prototype.renderBody.call( this );
FDDiagramContextItem.super.prototype.renderBody.call( this );
Line 152: Line 224:
}
}


// A plain anchor is fine here: the context popup is outside the
// Plain anchors are fine here: the context popup lives OUTSIDE the
// contenteditable surface, so VE does not suppress the click.
// contenteditable surface, so VE does not suppress the click the way it
// does inside the node itself.
$edit = $( '<a>' )
$edit = $( '<a>' )
.addClass( 'fd-ve-diagramContextItem-edit' )
.addClass( 'fd-ve-diagramContextItem-edit' )
.attr( { target: '_blank', rel: 'noopener' } )
.attr( { target: '_blank', rel: 'noopener' } )
.text( 'Edit “' + page.replace( /_/g, ' ' ) + '” ↗' );
.text( 'Edit “' + info.page.replace( /_/g, ' ' ) + '” ↗' );
ve.setAttributeSafe( $edit[ 0 ], 'href', url, '#' );
ve.setAttributeSafe( $edit[ 0 ], 'href', url, '#' );


// Overriding onEditButtonClick costs the normal route to changing which
this.$body.empty().append( $( '<div>' ).append( $edit ) );
// 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(
// Offered ONLY for {{Diagram|page=}}, where the page name is a real
$( '<div>' ).append( $edit ),
// template parameter that VE's transclusion dialog can edit. For a bare
$( '<div>' ).append( $change )
// {{#display_diagram:}} the same dialog opens empty - the page name is part
);
// of the parser-function target, and VE has no setter for a target - so
// showing this there would be a control that looks actionable and is not.
//
// This item replaces the stock transclusion context item (ModeledFactory
// keeps only the most specific class in an inheritance chain), so without
// this link there is no route at all to the parameter dialog.
if ( info.isTemplate ) {
$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.append( $( '<div>' ).append( $change ) );
}
};
};