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