MediaWiki:Gadget-FDCodeEditor.js: Difference between revisions

FDCodeEditor gadget: attach CodeMirror 6 to the FlexDiagrams Mermaid/DOT edit textarea
 
FDCodeEditor: catch throws from attach() (then()s 2nd arg never saw them) + guard against double-attach
 
(One intermediate revision by the same user not shown)
Line 37: Line 37:
// Mermaid's own documentation, and every diagram on this wiki, indents by two.
// Mermaid's own documentation, and every diagram on this wiki, indents by two.
var INDENT = '  ';
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 ) {
function attach( require, textarea ) {
Line 42: Line 57:
var cm6 = require( 'ext.CodeMirror.v6.lib' );
var cm6 = require( 'ext.CodeMirror.v6.lib' );
var $textarea = $( textarea );
var $textarea = $( textarea );
var cm = new CodeMirror( 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( [
cm.initialize( [
Line 62: Line 90:
return;
return;
}
}
// Synchronous, every time: this is what save and the
// unsaved-changes warning read.
$textarea
$textarea
.val( update.state.doc.toString() )
.val( update.state.doc.toString() )
.trigger( 'keyup' )      // FlexDiagrams live preview
.trigger( 'keypress' );  // FD_editWarning unsaved-changes flag
.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 );
} )
} )
] );
] );
Line 88: Line 124:
attach( require, textarea );
attach( require, textarea );
} );
} );
}, function ( err ) {
} ).catch( function ( err ) {
// Never fail silently: a guard that bails without saying so is
// .catch(), NOT then()'s second argument: a rejection handler passed to
// indistinguishable from the gadget not being installed at all.
// then() does not see errors THROWN INSIDE its own success callback, so
mw.log.warn( '[FDCodeEditor] CodeMirror failed to load; the diagram ' +
// 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 );
'editor stays a plain textarea.', err );
} );
} );
} );
} );
}() );
}() );