1/* DokuWiki MoaiEditor Scroll_to.js file 2 Version : 0.5b (2026-05-08) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6/* This is main class of MoaiEditor. 7*/ 8MoaiEditor.Main = class { 9 10 constructor() { 11 12 // We are only supporting 'edit' mode for now (prehaps 'preview' mode might be worth too, e.g on captcha errors) 13 if (JSINFO.ACT !== 'edit') 14 return; 15 16 // Exit if there is no textarea (e.g edittable plugin) 17 if (!document.body.querySelector("#wiki__text")) 18 return; 19 20 // Handle the 'hide_editor_by_default' option (available in the Dokuwiki configuration manager) 21 if (this.editorIsHidden()) 22 return; 23 24 // Specify release 25 this.version_number = '0.5b'; 26 this.version_date = '2026-05-08'; 27 28 // Settings 29 this.strict = false; // strict mode will show more errors and crash the script on some errors (disable before release) 30 31 // Variables 32 this.layoutReady = false; // Prevent actions before the layout is ready 33 34 // Continue initialization after the constructor has finished (so that the object exists already) 35 setTimeout(this.init.bind(this), 30); 36 } 37 // ──────────────────────────────────── 38 init() { 39 40 // Create the button that starts the editor 41 this.start = new MoaiEditor.StartButton(); 42 43 // Initialize the settings variable which controls if the editor starts enabled by default (exposed to the user by this.buttons.enabled) 44 this.enabled = new MoaiEditor.LocalStorage('btn_enabled', 'off', ['on','off']); 45 46 // Detect the template in use (aka theme or skin) and find needed DOM elements (textarea, form, etc) 47 this.template = this.detectTemplate(); // Detect the template in use 48 this.template.findElements(); // Try to find the HTML elements we need (asynchronous process, will call 'startEditor' if successful and editor is enabled) 49 this.loadCSS('templates/'+this.template.name+'.css'); // Load an optional template-specific CSS file 50 51 // Handle compatibility with other plugins 52 this.plugins = { 53 'captcha' : new MoaiEditor.Captcha(), 54 'codemirror' : new MoaiEditor.Codemirror(), 55 }; 56 } 57 // ──────────────────────────────────── 58 detectTemplate() { 59 60 // Try to identify the template currently in use and create an object to handle it 61 for (let name of moaiEditor_templates) { 62 var template = eval("new MoaiEditor.Template_"+name+"(name);"); 63 if (template.detectTemplate()) { 64 return template; 65 } 66 } 67 // If the template currently in use was not identified, use the default class (in templates/default.js) 68 return eval("new MoaiEditor.Template('default');"); 69 } 70 // ──────────────────────────────────── 71 loadCSS(name) { 72 // Load an on-demand CSS file and add it to the head of the document 73 const link = this.createHTML('<link href="lib/plugins/moaieditor/'+name+'" rel="stylesheet" type="text/css"/>'); 74 document.head.appendChild(link); 75 } 76 // ──────────────────────────────────── 77 startEditor() { 78 79 /* This function starts the editor. 80 81 It will get called whenever: 82 - the editor is enabled by default and the asynchronous process started by 'this.template.findElements()' finishes successfuly. 83 - the 'start editor' button is clicked. 84 85 The following conditions must be met for the editor to start: 86 - The needed elements of the DOM must have been found (signaled by 'this.template.ready') 87 - The editor must not have started already (signaled by '!this.layoutReady') */ 88 89 // Return if conditions are not met 90 if (!this.template.ready || this.layoutReady) 91 return; 92 93 // Remember some user settings we don't want to get changed outside MoaiEditor. 94 this.persistCookies = { 95 'cm-nativeeditor': DokuCookie.getValue('cm-nativeeditor') 96 }; 97 // ────────────────── Create objects ─────────────────── 98 99 // Create SVG icons 100 this.icons = new MoaiEditor.Icons(); 101 102 // Handle AJAX request for previews 103 this.ajax = new MoaiEditor.Ajax(this); 104 105 // Handle the preview pane 106 this.preview = new MoaiEditor.Preview(this); 107 108 // Create buttons 109 this.buttons = new MoaiEditor.Buttons(this); 110 111 // Manage the native editor (textarea mirroring for syntax highlighting and synchronized scrolling) 112 this.mirror = new MoaiEditor.TextAreaMirror(this); 113 114 // Manage html-to-markup matching (for synchronized scrolling, syntax highlight, and for faster previews by rendering only parts of the document) 115 this.matches = new MoaiEditor.Matches(this); 116 117 // Manage partial previews (which means rendering only changed areas of the document for faster previews) 118 this.dirty = new MoaiEditor.Dirty(this); 119 120 // Manage automated scrolling (synchronized panes, clickable html headers, table of contents) 121 this.scroll = new MoaiEditor.Scroll(this); 122 123 // Manage table of contents 124 this.toc = new MoaiEditor.ToC(this); 125 126 // Manage visual inidicator whenever a draft is saved 127 this.draft = new MoaiEditor.Draft(this); 128 129 // Setup the new DOM layout 130 this.layout = new MoaiEditor.Layout(this.template.elements); 131 132 // Manage the current editor and switch between them (native, codemirror, prosemirror, etc) 133 this.editor = new MoaiEditor.Editor(); 134 135 // ─────────── Initialize (done only once) ──────────── 136 137 this.initPluginsBeforeLayout(); 138 this.editor.init(); 139 this.layout.init(); 140 this.initPluginsAfterLayout(); 141 this.mirror.init(); 142 this.preview.init(); 143 this.scroll.init(); 144 145 // ─────────── Add various event listeners ──────────── 146 147 // Window resize 148 window.addEventListener("resize", this.onWindowResize.bind(this)); 149 150 // Preview 151 this.preview.container.addEventListener("scroll", this.preview.onScroll.bind(this.preview), {passive:true}); 152 153 // Textarea 154 var textarea = this.template.elements.textarea; 155 /* Scroll */ 156 textarea.addEventListener("scroll", this.mirror.onTextareaScroll.bind (this.mirror), {passive:true}); 157 /* Text change */ 158 textarea.addEventListener("input", this.mirror.onTextChange.bind (this.mirror)); 159 textarea.addEventListener("keydown", this.mirror.onTextChange.bind (this.mirror)); 160 textarea.addEventListener("paste", this.mirror.onTextChange.bind (this.mirror)); 161 textarea.addEventListener("cut", this.mirror.onTextChange.bind (this.mirror)); 162 /* Selection/cursor change */ 163 textarea.addEventListener("keyup", this.mirror.onSelectionChange.bind (this.mirror)); 164 textarea.addEventListener("mouseup", this.mirror.onSelectionChange.bind (this.mirror)); 165 textarea.addEventListener("mousedown", this.mirror.onSelectionChange.bind (this.mirror)); 166 textarea.addEventListener("select", this.mirror.onSelectionChange.bind (this.mirror)); 167 textarea.addEventListener("focus", this.mirror.onSelectionChange.bind (this.mirror)); 168 /* Resize */ 169 new ResizeObserver(this.mirror.onTextareaResize.bind(this.mirror)).observe(textarea); 170 171 //keypress, keydown, keyup, touchstart, mousedown, mousemove, click, input, paste, cut, select, selectstart, and focus. 172 173 // Dokuwiki toolbar (catch text changes by toolbar button actions) 174 for (let button of document.querySelectorAll("button.toolbutton")) 175 button.addEventListener("click", this.editor.onToolbarButtonInput.bind(this.editor)); 176 177 // OnBeforeUnload 178 window.addEventListener("beforeunload", this.onBeforeUnload.bind(this)); 179 180 // ────────────────────── Start ─────────────────────── 181 182 requestAnimationFrame(() => { 183 184 // Update the style of the exit buttons (Save, Cancel, Back) 185 this.buttons.styleExitButtons(); 186 187 // Allow actions now that the layout is ready 188 this.layoutReady = true; 189 190 // Start the editor pane 191 this.editor.start(); 192 193 // Show initial preview 194 this.ajax.request(); 195 }); 196 // Create debug line 197 //this.createDebugLine(); 198 } 199 // ──────────────────────────────────── 200 initPluginsBeforeLayout() { 201 for (let name in this.plugins) 202 if (this.plugins[name].initBeforeLayout) 203 this.plugins[name].initBeforeLayout(); 204 } 205 initPluginsAfterLayout() { 206 for (let name in this.plugins) 207 if (this.plugins[name].initAfterLayout) 208 this.plugins[name].initAfterLayout(); 209 } 210 // ──────────────────────────────────── 211 onWindowResize() { 212 this.layout.onWindowResize(); 213 this.toc.redraw(); // The width of the dummy dropdown needs refreshing 214 } 215 // ──────────────────────────────────── 216 onBeforeUnload() { 217 // Restore some settings cookies we don't want to interfer with outside MoaiEditor 218 if (this.persistCookies) 219 for (let name in this.persistCookies) 220 DokuCookie.setValue(name, this.persistCookies[name]); 221 // If MoaiEditor is enabled by default prevent CodeMirror to start by default to improve startup speed on big documents 222 if (moaiEditor.enabled.value == 'on') 223 DokuCookie.setValue('cm-nativeeditor', '1'); 224 } 225 // ──────────────────────────────────── 226 createHTML(htmlString) { 227 var div = document.createElement('div'); 228 div.innerHTML = htmlString.trim(); 229 return div.firstChild; // Change this to div.childNodes to support multiple top-level nodes. 230 } 231 // ──────────────────────────────────── 232 editorIsHidden() { 233 /* This function makes it possible to hide MoaiEditor from regular users and only make it 234 available on a per-browser basis. Useful if you want to test the plugin in a plublic wiki 235 but not make it available to all users yet. You can enable/disable this option in the DokuWiki 236 configuration manager. 237 */ 238 // Return false if the setting is not enabled in Dokuwiki 239 if (JSINFO.plugin_moaieditor.hide_editor_by_default == '0') 240 return false; 241 242 // Initialize the local-storage variable 243 const isHidden = new MoaiEditor.LocalStorage('editor_is_hidden', '1', [0,1]); 244 245 // Change local-storage variable depending on user query-string variable 246 if (window.location.href.includes("showeditor=0")) 247 isHidden.value = '1'; 248 if (window.location.href.includes("showeditor=1")) 249 isHidden.value = '0'; 250 251 // Return boolean 252 if (isHidden.value == '1') 253 return true; 254 else 255 return false; 256 } 257 // ──────────────────────────────────── 258 dokuMessage(message, lvl) { 259 /* Displays a dokuwiki message, similar to inc/infoutils.php -> msg() 260 */ 261 const levels = { 262 '-1': 'error', 263 '0' : 'info', 264 '1' : 'success', 265 '2' : 'notify' 266 }; 267 const cls = levels[lvl]; 268 const msg = moaiEditor.createHTML(`<div class="${cls}">${message}</div>`); 269 document.querySelector("#moaied__msg_area").appendChild(msg); 270 } 271 // ──────────────────────────────────── 272 createDebugLine() { 273 const line = this.createHTML("<div id='moai__debug'> <div></div> <div></div> <div></div> <div></div> </div>"); 274 document.querySelector("html").appendChild(line); 275 } 276}; // End Class 277 278 279 280