MediaWiki:Gadget-FDCodeEditor.js

From RECESSIM, A Reverse Engineering Community
Revision as of 18:26, 21 September 2026 by Hash (talk | contribs) (FDCodeEditor gadget: attach CodeMirror 6 to the FlexDiagrams Mermaid/DOT edit textarea)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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 = '  ';

	function attach( require, textarea ) {
		var CodeMirror = require( 'ext.CodeMirror.v6' );
		var cm6 = require( 'ext.CodeMirror.v6.lib' );
		var $textarea = $( textarea );
		var 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;
				}
				$textarea
					.val( update.state.doc.toString() )
					.trigger( 'keyup' )      // FlexDiagrams live preview
					.trigger( 'keypress' );  // FD_editWarning unsaved-changes flag
			} )
		] );
	}

	// 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 );
			} );
		}, function ( err ) {
			// Never fail silently: a guard that bails without saying so is
			// indistinguishable from the gadget not being installed at all.
			mw.log.warn( '[FDCodeEditor] CodeMirror failed to load; the diagram ' +
				'editor stays a plain textarea.', err );
		} );
	} );
}() );