/* DokuWiki MoaiEditor Cm_main.js file Version : 0.5a (May 6, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ /* CodeMirror main class ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ Handles the CodeMirror plugin integration into MoaiEditor. Codemirror DOM structure ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ .CodeMirror -- Main container .CodeMirror-scroll -- Scrolling element .CodeMirror-sizer -- Full height element (seems to hold the height of the full document even if few lines are rendered)
-- (↕) Being moved up and down with respect to the parent (has some padding) .CodeMirror-lines -- (=h) Changes height constantly depending on the number of rendered lines (position:static)
-- (=h) (position:relative) .CodeMirror-code -- (=h) (position:static) lines (=h): Same height as parent (margin 0, padding 0) */ MoaiEditor.Codemirror = class { constructor() { // Constants this.name = 'CodeMirror'; // Label to display to the user this.isEditor = true; // Identify editor plugins // Variables this.enabled = false; // Flag to indicate if this editor is enabled right now this.numHints = 0; // Show animated hints only a limited number of times this.settings = null; // Settings button element (we fire a click event on it to enable/disable CM) this.container = null; // Codemirror container element (.Codemirror) this.editor = null; // Editor object (or null if CM is disabled) // Objects this.watcher = new MoaiEditor.CodemirrorWatcher(this); this.scroll = new MoaiEditor.CodemirrorScroll(this); } // ┌───────────────────────────────────┐ // │ Public │ // └───────────────────────────────────┘ exists () { this.toggler = this.getToggler(); if (this.toggler === null) return false; return true; } // ──────────────────────────────────── initBeforeLayout () { if (!this.exists()) return; // Put the settings img/button inside a container (because tags cannot have children) and add an animated hint this.settings = this.getSettingsButton(); this.settingsWrapper = moaiEditor.createHTML('
'); this.settingsWrapper.appendChild(this.settings); this.settingsHint = new MoaiEditor.Hint('arrow-right', 'Settings', this.settingsWrapper, 30, -3); // Style the settings img/button and wrapper this.settings.style.margin = '0'; this.settings.style.marginBottom = '15px'; this.settingsWrapper.style.display = 'none'; this.settingsWrapper.style.position = 'relative'; // Disable CodeMirror (if it was enabled before starting MoaiEditor) if (this.isEnabled()) this.disable(); // Hide toggle option from CodeMirror menu (the user should enable/disable CodeMirror only through MoaiEditor now) this.toggler.parentNode.style.display = 'none'; document.querySelector(".cm-settings-menu li.ui-menu-divider").style.display = 'none'; } // ──────────────────────────────────── initAfterLayout () { if (!this.exists()) return; // Move the settings menu to the new layout moaiEditor.layout.bottomRight.appendChild(this.settingsWrapper); // Create flash box this.flashbox = moaiEditor.createHTML('
'); } // ──────────────────────────────────── disable () { if (!this.isEnabled()) return; // Click the toggle button this.toggler.click(); // Hide the settings this.settingsWrapper.style.display = 'none'; // Hide the hint this.settingsHint.disable(); // Set flag this.enabled = false; } // ──────────────────────────────────── enable (clicked=false) { if (this.isEnabled()) return; /* Disable plugin»codemirror»autoheight=1, which sets viewportMargin=Infinty. * This option makes sense in the vanilla editor to avoid nested scrolling, but it is not really * needed in MoaiEditor where there is no nested scrolling anyways, and it will slow down or * freeze the browser on big documents, and therefore it is recommended against in the manual: * https://codemirror.net/5/doc/manual.html#option_viewportMargin * Note: Trying to achieve the same result by doing this: this.editor.setOption('viewportMargin', 20) * inmediately after starting CodeMirror will sometimes trigger the following error: * "can't access property 'setOption', a is null" ← 'a' being a minified name. */ JSINFO.plugin_codemirror.autoheight = 0; // Click the toggle button this.toggler.click(); // Get DOM elements (.Codemirror) this.container = this.getContainer(); this.scroller = this.container.querySelector(".CodeMirror-scroll"); // Create overlay to display dirty area (for debug) this.dirty = moaiEditor.createHTML('
'); this.container.querySelector(".CodeMirror-sizer").appendChild(this.dirty); // Get the editor object this.editor = this.container.CodeMirror; // Style the container this.container.style.position = 'absolute'; this.container.style.top = '0'; this.container.style.left = '0'; this.container.style.width = '100%'; this.container.style.height = '100%'; this.container.style.border = 'none'; this.container.style.margin = '0'; // Display the settings this.settingsWrapper.style.display = 'block'; // Show the hint when the user activates Codemirror (maximum 2 times) if (clicked && this.numHints < 2) { this.settingsHint.start(); this.numHints += 1; } // Add event listeners this.editor.on('scroll', this.onScroll.bind(this)); this.editor.on('refresh', this.onRefresh.bind(this)); // Fires on font size changes this.editor.on('change', this.onDocumentChange.bind(this)); new ResizeObserver(this.onResize.bind(this)).observe(this.container); // Copy textarea lines to the watcher this.watcher.lines = this.editor.getValue().split("\n"); // Recalc scroll points moaiEditor.matches.recalcScrollPoints(); // Set flag this.enabled = true; } // ──────────────────────────────────── onAjax () { // This method is required but we don't use it } // ──────────────────────────────────── getLineRect(linenum, mode='local') { // Preparations if (mode == 'viewport') mode = 'window'; const top = this.editor.heightAtLine(linenum, mode); const bottom = this.editor.heightAtLine(linenum+1, mode); const height = bottom-top; return { top: top, bottom: bottom, height: height }; } // ──────────────────────────────────── addMatches(newMatches) { // This method is required but we don't use it } removeMatches(startline, endline) { // This method is required but we don't use it } // ──────────────────────────────────── setWrap(value) { // CodeMirror has a hook on 'dw_editor.setWrap()' dw_editor.setWrap (moaiEditor.layout.elements.textarea, value); moaiEditor.matches.recalcScrollPoints(); } // ──────────────────────────────────── set pointerEvents(boolean) { if (boolean) this.container.style.pointerEvents = 'auto'; else this.container.style.pointerEvents = 'none'; } // ──────────────────────────────────── flash(flash, data=null) { if (flash == 'remove') { this.flashbox.remove(); return; } if (flash === null) return; this.flashbox.remove(); this.flashbox = moaiEditor.createHTML('
'); if (flash === false) this.flashbox.classList.add('red'); const start = this.getLineRect(data.startline); const end = this.getLineRect(data.endline); const height = end.bottom - start.top; const width = this.editor.getScrollInfo().width; this.flashbox.style.top = start.top + 'px'; this.flashbox.style.left = '0px'; this.flashbox.style.width = width + 'px'; this.flashbox.style.height = height + 'px'; this.scroller.appendChild(this.flashbox); } // ──────────────────────────────────── get text() { return this.editor.getValue(); } // ┌───────────────────────────────────┐ // │ Input events │ // └───────────────────────────────────┘ onScroll() { // Make sure we update the scroll position of newly rendered lines to avoid CodeMirror approximation errors this.scroll.onScroll(); // Synchronize preview scroll moaiEditor.scroll.sync.onScroll(); } // ──────────────────────────────────── onToolbarButtonInput() { this.onInput(); } // ──────────────────────────────────── onDocumentChange() { this.onInput(); } // ──────────────────────────────────── onInput() { if (this.enabled) { this.watcher.onInput(); } } // ──────────────────────────────────── onRefresh() { moaiEditor.matches.recalcScrollPoints(); } // ──────────────────────────────────── onResize() { moaiEditor.matches.recalcScrollPoints(); } // ┌───────────────────────────────────┐ // │ Private │ // └───────────────────────────────────┘ onTextChanged(change) { // Update the matches and scroll positions moaiEditor.matches.onTextChanged(change); // Keep track of changed text sections (for partial preview) moaiEditor.dirty.onTextChanged(change); } // ──────────────────────────────────── isEnabled () { if (this.getContainer() === null) return false; return true; } // ──────────────────────────────────── getToggler () { return document.querySelector("#ui-id-73"); } getContainer () { return document.querySelector(".CodeMirror"); } getSettingsButton () { return document.querySelector("#size__ctl img.cm-settings-button"); } }; // End Class