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