/* DokuWiki MoaiEditor Main.js file Version : 0.5a (May 6, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ /* This is main class of MoaiEditor. */ MoaiEditor.Main = class { constructor() { // We are only supporting 'edit' mode for now (prehaps 'preview' mode might be worth too, e.g on captcha errors) if (JSINFO.ACT !== 'edit') return; // Exit if there is no textarea (e.g edittable plugin) if (!document.body.querySelector("#wiki__text")) return; // Handle the 'hide_editor_by_default' option (available in the Dokuwiki configuration manager) if (this.editorIsHidden()) return; // Specify release this.version_number = '0.5a'; this.version_date = 'May 6, 2026'; // Settings this.strict = false; // strict mode will show more errors and crash the script on some errors (disable before release) // Variables this.layoutReady = false; // Prevent actions before the layout is ready // Continue initialization after the constructor has finished (so that the object exists already) setTimeout(this.init.bind(this), 30); } // ──────────────────────────────────── init() { // Create the button that starts the editor this.start = new MoaiEditor.StartButton(); // Initialize the settings variable which controls if the editor starts enabled by default (exposed to the user by this.buttons.enabled) this.enabled = new MoaiEditor.LocalStorage('btn_enabled', 'off', ['on','off']); // Detect the template in use (aka theme or skin) and find needed DOM elements (textarea, form, etc) this.template = this.detectTemplate(); // Detect the template in use this.template.findElements(); // Try to find the HTML elements we need (asynchronous process, will call 'startEditor' if successful and editor is enabled) this.loadCSS('templates/'+this.template.name+'.css'); // Load an optional template-specific CSS file // Handle compatibility with other plugins this.plugins = { 'captcha' : new MoaiEditor.Captcha(), 'codemirror' : new MoaiEditor.Codemirror(), }; } // ──────────────────────────────────── detectTemplate() { // Try to identify the template currently in use and create an object to handle it for (let name of moaiEditor_templates) { var template = eval("new MoaiEditor.Template_"+name+"(name);"); if (template.detectTemplate()) { return template; } } // If the template currently in use was not identified, use the default class (in templates/default.js) return eval("new MoaiEditor.Template('default');"); } // ──────────────────────────────────── loadCSS(name) { // Load an on-demand CSS file and add it to the head of the document const link = this.createHTML(''); document.head.appendChild(link); } // ──────────────────────────────────── startEditor() { /* This function starts the editor. It will get called whenever: - the editor is enabled by default and the asynchronous process started by 'this.template.findElements()' finishes successfuly. - the 'start editor' button is clicked. The following conditions must be met for the editor to start: - The needed elements of the DOM must have been found (signaled by 'this.template.ready') - The editor must not have started already (signaled by '!this.layoutReady') */ // Return if conditions are not met if (!this.template.ready || this.layoutReady) return; // Remember some user settings we don't want to get changed outside MoaiEditor. this.persistCookies = { 'cm-nativeeditor': DokuCookie.getValue('cm-nativeeditor') }; // ────────────────── Create objects ─────────────────── // Create SVG icons this.icons = new MoaiEditor.Icons(); // Handle AJAX request for previews this.ajax = new MoaiEditor.Ajax(this); // Handle the preview pane this.preview = new MoaiEditor.Preview(this); // Create buttons this.buttons = new MoaiEditor.Buttons(this); // Manage the native editor (textarea mirroring for syntax highlighting and synchronized scrolling) this.mirror = new MoaiEditor.TextAreaMirror(this); // Manage html-to-markup matching (for synchronized scrolling, syntax highlight, and for faster previews by rendering only parts of the document) this.matches = new MoaiEditor.Matches(this); // Manage partial previews (which means rendering only changed areas of the document for faster previews) this.dirty = new MoaiEditor.Dirty(this); // Manage automated scrolling (synchronized panes, clickable html headers, table of contents) this.scroll = new MoaiEditor.Scroll(this); // Manage table of contents this.toc = new MoaiEditor.ToC(this); // Manage visual inidicator whenever a draft is saved this.draft = new MoaiEditor.Draft(this); // Setup the new DOM layout this.layout = new MoaiEditor.Layout(this.template.elements); // Manage the current editor and switch between them (native, codemirror, prosemirror, etc) this.editor = new MoaiEditor.Editor(); // ─────────── Initialize (done only once) ──────────── this.initPluginsBeforeLayout(); this.editor.init(); this.layout.init(); this.initPluginsAfterLayout(); this.mirror.init(); this.preview.init(); this.scroll.init(); // ─────────── Add various event listeners ──────────── // Window resize window.addEventListener("resize", this.onWindowResize.bind(this)); // Preview this.preview.container.addEventListener("scroll", this.preview.onScroll.bind(this.preview), {passive:true}); // Textarea var textarea = this.template.elements.textarea; textarea.addEventListener("scroll", this.mirror.onTextareaScroll.bind(this.mirror), {passive:true}); textarea.addEventListener("input", this.mirror.onTextareaInput.bind(this.mirror)); textarea.addEventListener("keydown", this.mirror.onTextareaKeydown.bind(this.mirror)); new ResizeObserver(this.mirror.onTextareaResize.bind(this.mirror)).observe(textarea); // Dokuwiki toolbar (catch text changes by toolbar button actions) for (let button of document.querySelectorAll("button.toolbutton")) button.addEventListener("click", this.editor.onToolbarButtonInput.bind(this.editor)); // OnBeforeUnload window.addEventListener("beforeunload", this.onBeforeUnload.bind(this)); // ────────────────────── Start ─────────────────────── requestAnimationFrame(() => { // Update the style of the exit buttons (Save, Cancel, Back) this.buttons.styleExitButtons(); // Allow actions now that the layout is ready this.layoutReady = true; // Start the editor pane this.editor.start(); // Show initial preview this.ajax.request(); }); // Create debug line //this.createDebugLine(); } // ──────────────────────────────────── initPluginsBeforeLayout() { for (let name in this.plugins) if (this.plugins[name].initBeforeLayout) this.plugins[name].initBeforeLayout(); } initPluginsAfterLayout() { for (let name in this.plugins) if (this.plugins[name].initAfterLayout) this.plugins[name].initAfterLayout(); } // ──────────────────────────────────── onWindowResize() { this.layout.onWindowResize(); this.toc.redraw(); // The width of the dummy dropdown needs refreshing } // ──────────────────────────────────── onBeforeUnload() { // Restore some settings cookies we don't want to interfer with outside MoaiEditor if (this.persistCookies) for (let name in this.persistCookies) DokuCookie.setValue(name, this.persistCookies[name]); // If MoaiEditor is enabled by default prevent CodeMirror to start by default to improve startup speed on big documents if (moaiEditor.enabled.value == 'on') DokuCookie.setValue('cm-nativeeditor', '1'); } // ──────────────────────────────────── createHTML(htmlString) { var div = document.createElement('div'); div.innerHTML = htmlString.trim(); return div.firstChild; // Change this to div.childNodes to support multiple top-level nodes. } // ──────────────────────────────────── editorIsHidden() { /* This function makes it possible to hide MoaiEditor from regular users and only make it available on a per-browser basis. Useful if you want to test the plugin in a plublic wiki but not make it available to all users yet. You can enable/disable this option in the DokuWiki configuration manager. */ // Return false if the setting is not enabled in Dokuwiki if (JSINFO.plugin_moaieditor.hide_editor_by_default == '0') return false; // Initialize the local-storage variable const isHidden = new MoaiEditor.LocalStorage('editor_is_hidden', '1', [0,1]); // Change local-storage variable depending on user query-string variable if (window.location.href.includes("showeditor=0")) isHidden.value = '1'; if (window.location.href.includes("showeditor=1")) isHidden.value = '0'; // Return boolean if (isHidden.value == '1') return true; else return false; } // ──────────────────────────────────── dokuMessage(message, lvl) { /* Displays a dokuwiki message, similar to inc/infoutils.php -> msg() */ const levels = { '-1': 'error', '0' : 'info', '1' : 'success', '2' : 'notify' }; const cls = levels[lvl]; const msg = moaiEditor.createHTML(`
${message}
`); document.querySelector("#moaied__msg_area").appendChild(msg); } // ──────────────────────────────────── createDebugLine() { const line = this.createHTML("
"); document.querySelector("html").appendChild(line); } }; // End Class