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( '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 ); | ||
} ); | } ); | ||
} | } ).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 | ||
mw.log.warn( '[FDCodeEditor] CodeMirror | // 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 ); | ||
} ); | } ); | ||
} ); | } ); | ||
}() ); | }() ); | ||