MediaWiki:Gadget-FDCodeEditor.js
Jump to navigation
Jump to search
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.
/**
* FDCodeEditor - a real code editor for the FlexDiagrams "edit diagram" screen.
*
* FlexDiagrams renders the Mermaid and DOT source as a bare <textarea>
* (FD_SpecialEditDiagram.php:121,143), so there is no tab-to-indent, no line
* numbers, no undo history and no find-in-editor. This gadget attaches MediaWiki's
* CodeMirror 6 to that textarea via its documented, @stable
* `new CodeMirror( textarea )` / `cm.initialize()` API.
*
* THE PART THAT IS NOT OBVIOUS - the sync bridge.
*
* CodeMirror hides the original textarea and copies content back to it ONLY on
* `form.submit` (codemirror.js:429-441). FlexDiagrams has no form: it saves over
* AJAX by reading `$( '.mermaidCode' ).val()`, drives its live preview from a
* `keyup` handler, and arms its unsaved-changes warning from a `keypress` handler.
* So without the updateListener below, the preview freezes, the leave-page warning
* never arms, and SAVE WRITES THE PRE-EDIT TEXT - silent data loss. Keeping the
* hidden textarea in sync instead means every existing FlexDiagrams code path keeps
* working untouched, and nothing under extensions/FlexDiagrams needs patching -
* which matters, because a FlexDiagrams upgrade silently reverts files there.
*
* `jquery.textSelection` is a REQUIRED dependency even though it looks unrelated:
* ext.CodeMirror.v6 does not declare it, but codemirror.js:443 calls
* `$( ... ).textSelection( 'register', ... )` unconditionally, so initialize()
* throws without it. Upstream gets away with this because CodeMirror normally loads
* alongside WikiEditor, which does declare it.
*
* Registered in [[MediaWiki:Gadgets-definition]]; styles in
* [[MediaWiki:Gadget-FDCodeEditor.css]].
*/
( function () {
'use strict';
// Both diagram editors use identical markup, so one gadget covers both.
var SELECTOR = 'textarea.mermaidCode, textarea.dotCode';
// Mermaid's own documentation, and every diagram on this wiki, indents by two.
var INDENT = ' ';
// How long to sit on the synthetic `keyup` that drives the live preview.
//
// This is not cosmetic. FlexDiagrams' DOT preview has an upstream race:
// ext.flexdiagrams.dot.js clears the pane with a synchronous $( '.dot' ).empty()
// but renders through an ASYNCHRONOUS Viz.instance().then( ... appendChild ),
// and unlike the Mermaid path it has no debounce of its own. So N keyups in
// flight produce N stacked copies of the diagram after the last empty(). That
// is reproducible on a plain textarea with this gadget disabled - it is not
// ours - but we synthesise these events, so we should not make it worse.
//
// The textarea value is still updated SYNCHRONOUSLY below; only the preview
// nudge is delayed. Hitting save mid-debounce therefore still saves the
// current text.
var PREVIEW_DEBOUNCE_MS = 250;
function attach( require, textarea ) {
var CodeMirror = require( 'ext.CodeMirror.v6' );
var cm6 = require( 'ext.CodeMirror.v6.lib' );
var $textarea = $( textarea );
var previewTimer = null;
var cm;
// Never attach twice to the same textarea. CodeMirror's initialize() calls
// $.fn.textSelection( 'register', ... ), which THROWS "Another textSelection
// API was already registered" the second time - and because that throw
// happens inside a promise callback it is easy for it to disappear
// silently, leaving a plain textarea and no explanation.
if ( $textarea.data( 'fdCodeEditorAttached' ) ) {
return;
}
$textarea.data( 'fdCodeEditorAttached', true );
cm = new CodeMirror( textarea );
cm.initialize( [
// Line numbers, undo/redo, bracket matching, find-and-replace,
// multiple cursors, line wrapping.
cm.defaultExtensions,
// Tab-to-indent is deliberately absent from defaultExtensions, because
// binding Tab traps keyboard navigation. Press Escape first, then Tab,
// to move focus out of the editor.
cm6.keymap.of( [ cm6.indentWithTab ] ),
cm6.indentUnit.of( INDENT ),
cm6.highlightActiveLine(),
// The sync bridge - see the header comment. Without this, saving
// silently writes the pre-edit text.
cm6.EditorView.updateListener.of( function ( update ) {
if ( !update.docChanged ) {
return;
}
// Synchronous, every time: this is what save and the
// unsaved-changes warning read.
$textarea
.val( update.state.doc.toString() )
.trigger( 'keypress' ); // FD_editWarning unsaved-changes flag
// Debounced: drives the live preview. See PREVIEW_DEBOUNCE_MS.
clearTimeout( previewTimer );
previewTimer = setTimeout( function () {
$textarea.trigger( 'keyup' );
}, PREVIEW_DEBOUNCE_MS );
} )
] );
}
// Driven from DOM-ready rather than mw.hook( 'wikipage.content' ), which has
// been seen not firing on diagram pages.
$( function () {
var textareas = $( SELECTOR ).toArray();
if ( !textareas.length ) {
// Any page that is not the "edit diagram" UI.
return;
}
mw.loader.using( [
'ext.CodeMirror.v6',
'ext.CodeMirror.v6.lib',
'jquery.textSelection'
] ).then( function ( require ) {
textareas.forEach( function ( textarea ) {
attach( require, textarea );
} );
} ).catch( function ( err ) {
// .catch(), NOT then()'s second argument: a rejection handler passed to
// then() does not see errors THROWN INSIDE its own success callback, so
// anything attach() threw used to vanish without trace - the editor just
// stayed a plain textarea with every module reporting "ready".
// Never fail silently.
mw.log.warn( '[FDCodeEditor] could not attach CodeMirror; the diagram ' +
'editor stays a plain textarea.', err );
} );
} );
}() );