1/* DokuWiki MoaiEditor Cm_main.js file 2 Version : 0.5 (May 5, 2026) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6/* CodeMirror main class 7 ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 8 Handles the CodeMirror plugin integration into MoaiEditor. 9 10 11 Codemirror DOM structure 12 ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 13 .CodeMirror -- Main container 14 .CodeMirror-scroll -- Scrolling element 15 .CodeMirror-sizer -- Full height element (seems to hold the height of the full document even if few lines are rendered) 16 <div style="position:relative; top:233px;"> -- (↕) Being moved up and down with respect to the parent (has some padding) 17 .CodeMirror-lines -- (=h) Changes height constantly depending on the number of rendered lines (position:static) 18 <div style="position:relative; outline:none;"> -- (=h) (position:relative) 19 .CodeMirror-code -- (=h) (position:static) 20 lines 21 (=h): Same height as parent (margin 0, padding 0) 22*/ 23MoaiEditor.Codemirror = class { 24 25 constructor() { 26 27 // Constants 28 this.name = 'CodeMirror'; // Label to display to the user 29 this.isEditor = true; // Identify editor plugins 30 31 // Variables 32 this.enabled = false; // Flag to indicate if this editor is enabled right now 33 this.numHints = 0; // Show animated hints only a limited number of times 34 this.settings = null; // Settings button element (we fire a click event on it to enable/disable CM) 35 this.container = null; // Codemirror container element (.Codemirror) 36 this.editor = null; // Editor object (or null if CM is disabled) 37 38 // Objects 39 this.watcher = new MoaiEditor.CodemirrorWatcher(this); 40 this.scroll = new MoaiEditor.CodemirrorScroll(this); 41 } 42 // ┌───────────────────────────────────┐ 43 // │ Public │ 44 // └───────────────────────────────────┘ 45 46 exists () { 47 this.toggler = this.getToggler(); 48 if (this.toggler === null) 49 return false; 50 return true; 51 } 52 // ──────────────────────────────────── 53 initBeforeLayout () { 54 55 if (!this.exists()) 56 return; 57 // Put the settings img/button inside a container (because <img> tags cannot have children) and add an animated hint 58 this.settings = this.getSettingsButton(); 59 this.settingsWrapper = moaiEditor.createHTML('<div id="moaied__cm_settings_wrapper"></div>'); 60 this.settingsWrapper.appendChild(this.settings); 61 this.settingsHint = new MoaiEditor.Hint('arrow-right', 'Settings', this.settingsWrapper, 30, -3); 62 63 // Style the settings img/button and wrapper 64 this.settings.style.margin = '0'; 65 this.settings.style.marginBottom = '15px'; 66 this.settingsWrapper.style.display = 'none'; 67 this.settingsWrapper.style.position = 'relative'; 68 69 // Disable CodeMirror (if it was enabled before starting MoaiEditor) 70 if (this.isEnabled()) 71 this.disable(); 72 73 // Hide toggle option from CodeMirror menu (the user should enable/disable CodeMirror only through MoaiEditor now) 74 this.toggler.parentNode.style.display = 'none'; 75 document.querySelector(".cm-settings-menu li.ui-menu-divider").style.display = 'none'; 76 } 77 // ──────────────────────────────────── 78 initAfterLayout () { 79 80 if (!this.exists()) 81 return; 82 // Move the settings menu to the new layout 83 moaiEditor.layout.bottomRight.appendChild(this.settingsWrapper); 84 // Create flash box 85 this.flashbox = moaiEditor.createHTML('<div id="moaied__codemirror_flashbox" class="moaied-flashbox"></div>'); 86 } 87 // ──────────────────────────────────── 88 disable () { 89 if (!this.isEnabled()) 90 return; 91 // Click the toggle button 92 this.toggler.click(); 93 // Hide the settings 94 this.settingsWrapper.style.display = 'none'; 95 // Hide the hint 96 this.settingsHint.disable(); 97 // Set flag 98 this.enabled = false; 99 } 100 // ──────────────────────────────────── 101 enable (clicked=false) { 102 if (this.isEnabled()) 103 return; 104 /* Disable plugin»codemirror»autoheight=1, which sets viewportMargin=Infinty. 105 * This option makes sense in the vanilla editor to avoid nested scrolling, but it is not really 106 * needed in MoaiEditor where there is no nested scrolling anyways, and it will slow down or 107 * freeze the browser on big documents, and therefore it is recommended against in the manual: 108 * https://codemirror.net/5/doc/manual.html#option_viewportMargin 109 * Note: Trying to achieve the same result by doing this: this.editor.setOption('viewportMargin', 20) 110 * inmediately after starting CodeMirror will sometimes trigger the following error: 111 * "can't access property 'setOption', a is null" ← 'a' being a minified name. 112 */ 113 JSINFO.plugin_codemirror.autoheight = 0; 114 // Click the toggle button 115 this.toggler.click(); 116 // Get DOM elements (.Codemirror) 117 this.container = this.getContainer(); 118 this.scroller = this.container.querySelector(".CodeMirror-scroll"); 119 // Create overlay to display dirty area (for debug) 120 this.dirty = moaiEditor.createHTML('<div class="moaied-show-dirty-area"></div>'); 121 this.container.querySelector(".CodeMirror-sizer").appendChild(this.dirty); 122 // Get the editor object 123 this.editor = this.container.CodeMirror; 124 // Style the container 125 this.container.style.position = 'absolute'; 126 this.container.style.top = '0'; 127 this.container.style.left = '0'; 128 this.container.style.width = '100%'; 129 this.container.style.height = '100%'; 130 this.container.style.border = 'none'; 131 this.container.style.margin = '0'; 132 // Display the settings 133 this.settingsWrapper.style.display = 'block'; 134 // Show the hint when the user activates Codemirror (maximum 2 times) 135 if (clicked && this.numHints < 2) { 136 this.settingsHint.start(); 137 this.numHints += 1; 138 } 139 // Add event listeners 140 this.editor.on('scroll', this.onScroll.bind(this)); 141 this.editor.on('refresh', this.onRefresh.bind(this)); // Fires on font size changes 142 this.editor.on('change', this.onDocumentChange.bind(this)); 143 new ResizeObserver(this.onResize.bind(this)).observe(this.container); 144 // Copy textarea lines to the watcher 145 this.watcher.lines = this.editor.getValue().split("\n"); 146 // Recalc scroll points 147 moaiEditor.matches.recalcScrollPoints(); 148 // Set flag 149 this.enabled = true; 150 } 151 // ──────────────────────────────────── 152 onAjax () { 153 // This method is required but we don't use it 154 } 155 // ──────────────────────────────────── 156 getLineRect(linenum, mode='local') { 157 158 // Preparations 159 if (mode == 'viewport') 160 mode = 'window'; 161 const top = this.editor.heightAtLine(linenum, mode); 162 const bottom = this.editor.heightAtLine(linenum+1, mode); 163 const height = bottom-top; 164 return { 165 top: top, 166 bottom: bottom, 167 height: height 168 }; 169 } 170 // ──────────────────────────────────── 171 addMatches(newMatches) { 172 // This method is required but we don't use it 173 } 174 removeMatches(startline, endline) { 175 // This method is required but we don't use it 176 } 177 // ──────────────────────────────────── 178 setWrap(value) { 179 // CodeMirror has a hook on 'dw_editor.setWrap()' 180 dw_editor.setWrap (moaiEditor.layout.elements.textarea, value); 181 moaiEditor.matches.recalcScrollPoints(); 182 } 183 // ──────────────────────────────────── 184 set pointerEvents(boolean) { 185 if (boolean) 186 this.container.style.pointerEvents = 'auto'; 187 else 188 this.container.style.pointerEvents = 'none'; 189 } 190 // ──────────────────────────────────── 191 flash(flash, data=null) { 192 if (flash == 'remove') { 193 this.flashbox.remove(); 194 return; 195 } 196 if (flash === null) 197 return; 198 this.flashbox.remove(); 199 this.flashbox = moaiEditor.createHTML('<div id="moaied__codemirror_flashbox" class="moaied-flashbox"></div>'); 200 if (flash === false) 201 this.flashbox.classList.add('red'); 202 const start = this.getLineRect(data.startline); 203 const end = this.getLineRect(data.endline); 204 const height = end.bottom - start.top; 205 const width = this.editor.getScrollInfo().width; 206 this.flashbox.style.top = start.top + 'px'; 207 this.flashbox.style.left = '0px'; 208 this.flashbox.style.width = width + 'px'; 209 this.flashbox.style.height = height + 'px'; 210 this.scroller.appendChild(this.flashbox); 211 } 212 // ──────────────────────────────────── 213 get text() { 214 return this.editor.getValue(); 215 } 216 // ┌───────────────────────────────────┐ 217 // │ Input events │ 218 // └───────────────────────────────────┘ 219 220 onScroll() { 221 // Make sure we update the scroll position of newly rendered lines to avoid CodeMirror approximation errors 222 this.scroll.onScroll(); 223 // Synchronize preview scroll 224 moaiEditor.scroll.sync.onScroll(); 225 } 226 // ──────────────────────────────────── 227 228 onToolbarButtonInput() { 229 this.onInput(); 230 } 231 // ──────────────────────────────────── 232 onDocumentChange() { 233 this.onInput(); 234 } 235 // ──────────────────────────────────── 236 onInput() { 237 if (this.enabled) { 238 this.watcher.onInput(); 239 } 240 } 241 // ──────────────────────────────────── 242 onRefresh() { 243 moaiEditor.matches.recalcScrollPoints(); 244 } 245 // ──────────────────────────────────── 246 onResize() { 247 moaiEditor.matches.recalcScrollPoints(); 248 } 249 // ┌───────────────────────────────────┐ 250 // │ Private │ 251 // └───────────────────────────────────┘ 252 253 onTextChanged(change) { 254 255 // Update the matches and scroll positions 256 moaiEditor.matches.onTextChanged(change); 257 258 // Keep track of changed text sections (for partial preview) 259 moaiEditor.dirty.onTextChanged(change); 260 261 } 262 // ──────────────────────────────────── 263 isEnabled () { 264 if (this.getContainer() === null) 265 return false; 266 return true; 267 } 268 // ──────────────────────────────────── 269 getToggler () { 270 return document.querySelector("#ui-id-73"); 271 } 272 getContainer () { 273 return document.querySelector(".CodeMirror"); 274 } 275 getSettingsButton () { 276 return document.querySelector("#size__ctl img.cm-settings-button"); 277 } 278 279}; // End Class 280 281 282 283