/* DokuWiki MoaiEditor Layout.js file Version : 0.5 (May 5, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ /* Layout class ‾‾‾‾‾‾‾‾‾‾‾‾ Umbrella class for the different layouts of the editor (desktop, phone, etc). It receives all the needed HTML elements found by the template-specific class and then: - Creates the needed HTML elements common to all layouts. - Tweaks the HTML and BODY elements to support the editor. - Tweaks some existing elements like the TEXTAREA to support the editor. - Hides the original editor. It also monitors window resize events and switches to the correct layout (desktop, phone, etc) as needed. */ MoaiEditor.Layout = class { constructor(elements) { // Arguments this.elements = elements; // Variables this.mode = null; // String identifying the current layout ('desktop', 'vphone', etc) this.layout = null; // Object managing the current layout // Settings this.settings = {hide_intro_message: true}; // Objects this.desktop = new MoaiEditor.LayoutDesktop(this); this.vphone = new MoaiEditor.LayoutPhoneVertical(this); // Constants this.modes = { desktop : null, vphone : 600 }; } // ──────────────────────────────────── init() { // Common setup for all layouts (desktop, phone, etc) this.setup(); // Activate the specific layout for the current screen resolution this.onWindowResize(); } // ──────────────────────────────────── onWindowResize() { var w = window.innerWidth; var h = window.innerHeight; // Determine the current mode var mode = 'desktop'; for (let m in this.modes) { const maxres = this.modes[m]; if (maxres !== null && w <= maxres) mode = m; } // If the mode is the same if (this.mode === mode) { // Update things based on window size this.layout.onWindowResize(); // Return return; } // Store the current mode this.mode = mode; // Make the previous layout do any deactivation tasks if (this.layout !== null) this.layout.deactivate(); // Remove the direct children of the editor container while (this.editor.hasChildNodes()) this.editor.firstChild.remove(); // Set the current layout mode and activate it this.layout = this[mode]; this.layout.activate(); // Update the editor toggle button this.updateEditorBtn(); // Update things based on window size this.layout.onWindowResize(); // Update plugins which depend on layout mode for (let name in moaiEditor.plugins) if (moaiEditor.plugins[name].onSpecificLayout) moaiEditor.plugins[name].onSpecificLayout(mode); } // ──────────────────────────────────── setup (elements) { // Chrome uses this tag in the head in order to behave like most browsers on Android var meta = document.createElement('meta'); meta.name = "viewport"; meta.content = "width=device-width, initial-scale=1.0, interactive-widget=resizes-content"; document.getElementsByTagName('head')[0].appendChild(meta); // Hide elements that take space const no = document.body.querySelector('body > .no'); if (no !== null) no.style.display = 'none'; // Style the HTML element const html = document.documentElement; html.style.overflow = 'hidden'; html.style.height = '100%'; //html.style.height = '100dvh'; //html.style.setProperty('height', '100% !important'); html.style.width = '100%'; html.style.margin = '0'; html.style.padding = '0'; // Style the BODY element const body = document.body; body.style.height = '100%'; //body.style.height = '100dvh'; //body.style.setProperty('height', '100% !important'); body.style.width = '100%'; body.style.margin = '0'; body.style.padding = '0'; body.style.minWidth = '0px'; body.style.overflowX = 'hidden'; body.style.boxSizing = 'border-box'; body.style.scrollBehavior = 'smooth'; //body.style.background = 'rgba(0,0,255,0.1)'; // Add a class to the html and body elements to be able to style them after the editor starts (but not before) html.classList.add("moaied"); body.classList.add("moaied"); // Set font size of the root element (rem) if its not set if (html.style.fontSize == '') html.style.fontSize = '16px'; // Button attributes for Save and Cancel moaiEditor.buttons.save.handle.name = 'do[save]'; moaiEditor.buttons.cancel.handle.name = 'do[cancel]'; moaiEditor.buttons.save.handle.value = '1'; moaiEditor.buttons.cancel.handle.value = '1'; moaiEditor.buttons.save.handle.setAttribute ("form",'dw__editform'); moaiEditor.buttons.cancel.handle.setAttribute("form",'dw__editform'); // Disable "Confirm that you want to leave" popup when clicking 'Save' moaiEditor.buttons.save.handle.addEventListener("click", function(){ window.onbeforeunload=''; textChanged=false; }); // Create the pageid (example: animals:mammals:dogs) var path = JSINFO.id.split(":"); path[path.length-1] = ''+path[path.length-1]+''; var title = "Document id (path in the wiki namespace)"; this.pageid = moaiEditor.createHTML('
'); this.pageid.innerHTML = '' + path.join('»') + ''; // Create sidebar buttons this.btn_editor = this.createSidebarButton ('editor', '--', this.onClickEditorBtn.bind(this) ); this.btn_linewrap = this.createSidebarButton ('linewrap', 'Line wrap', moaiEditor.editor.toggleWrapLines.bind(moaiEditor.editor)); this.btn_fullscreen = this.createSidebarButton ('fullscreen', 'Full screen', this.toggleFullscreen ); this.btn_scrolltop = this.createSidebarButton ('scrolltop', 'Go to top', moaiEditor.scroll.toTop.bind(moaiEditor.scroll) ); this.btn_scrollbottom = this.createSidebarButton ('scrollbottom', 'Go to bottom', moaiEditor.scroll.toBottom.bind(moaiEditor.scroll) ); // Create automatic-scroll-in-progress visual indicator this.indicatorScrolling = moaiEditor.createHTML('
'+moaiEditor.icons.ico_autoscroll2+'
'); // Create container element positioned at the bottom right (for logo, codemirror menu, etc) this.bottomRight = moaiEditor.createHTML('
'); // Create the container for dokuwiki messages (info, error, success, notify) usually rendered by inc/html.php -> html_msgarea(). this.msgarea = moaiEditor.createHTML('
'); for (let message of this.elements.messages) this.msgarea.appendChild(message); // Add the editor intro message var intromsg = JSINFO.plugin_moaieditor.intromsg; console.warn(); if (intromsg !== null) if (intromsg.type == 'editrev' || !this.settings.hide_intro_message) { const element = moaiEditor.createHTML(intromsg.html); this.msgarea.appendChild(element); } // Editor version (and moai logo) var title; title = 'MoaiEditor :'; title+= '\n info@moaitools.org'; title+= '\n Version: ' + moaiEditor.version_number + ' ('+moaiEditor.version_date+')'; title+= '\n Detected template: "'+ moaiEditor.template.name+'"'; title+= '\n Page id: "'+ JSINFO.id+'"'; title+= '\n Available editors: \n'+ moaiEditor.editor.getEditors(); this.logo = moaiEditor.createHTML(''); this.logo.title = title; this.logo.innerHTML = moaiEditor.icons.logo_moai+'
moai
editor
'+moaiEditor.version_number+''; this.bottomRight.appendChild(this.logo); // Setup the edit-summary input fields this.summary = this.elements.editSummary; // Assign the form to the input fields var editSummary = document.body.querySelector('#edit__summary'); var minorEdit = document.body.querySelector('#edit__minoredit'); editSummary.setAttribute('form', 'dw__editform'); if (minorEdit) minorEdit.setAttribute('form', 'dw__editform'); // Remove size attribute (control via CSS) editSummary.removeAttribute('size'); // Tooltips this.summary.childNodes[0].title = "You can enter a short description of the\ncurrent modifications to the document \nto be shown in the list of old revisions\nof this document."; if (this.summary.childNodes[2]) this.summary.childNodes[2].title = "Check this box if the modifications are just minor\nin order for this revision to be shown dimed in\nthe list of old revisions."; // Style the textarea this.textarea = this.elements.textarea; this.textarea.setAttribute('form', 'dw__editform'); var style = "position: absolute;"; style += "box-sizing: border-box;"; style += "top:0; left:0;"; style += "margin:0; height:100%; width:100%;"; style += "box-shadow:none !important;"; style += "border:none !important;"; style += "border-radius:0 !important;"; style += "outline:none !important;"; style += "resize: none;"; style += "scroll-behavior:auto;"; style += "overflow-y:scroll;"; // Should prevent the scrollbar from reflowing the text style += "overflow-x:auto;"; style += "padding: 5px 5px;"; style += "z-index: 10;"; style += "background:none; "; //style += "color:transparent;"; style += "color:rgba(0,0,0,0.2);"; //style += "pointer-events:none;"; this.textarea.setAttribute('style', style); // Remove current inline style and apply the new // Prepare the dokuwiki edit form to be the editor pane // * We have to use the Dokuwiki form as the container for the texarea because 'locktimer.js' require it. // * It would break otherwise. this.editpane = this.elements.editForm; this.editpane.classList.add('pane'); // Create an element where to dump template elements we want to hide this.hidden = moaiEditor.createHTML('
'); document.body.appendChild(this.hidden); // Remove all elements from the form except the textarea and the hidden input fields var nodes = []; for (let node of this.elements.editForm.childNodes) { if (node.tagName == 'TEXAREA') continue; if (node.tagName == 'INPUT' && node.type == "hidden") continue; nodes.push(node) } for (let node of nodes) this.hidden.appendChild(node); // Create the main wrapper this.wrapper = moaiEditor.createHTML('
'); //document.body.appendChild(this.wrapper); document.body.insertBefore (this.wrapper, document.body.firstChild); // Create the editor container this.editor = moaiEditor.createHTML('
'); this.wrapper.appendChild(this.editor); // Create both panes this.panes = moaiEditor.createHTML('
'); // Dual pane container //this.editpane = moaiEditor.createHTML('
'); // Container for textarea and mirror this.preview = moaiEditor.createHTML('
'); // Container for preview this.panes.appendChild(this.editpane); this.panes.appendChild(this.preview); // Add the textarea this.textarea = this.elements.textarea; this.editpane.appendChild(this.textarea); // Hide the original editor if (this.elements.hide.constructor === Array) { for (let element of this.elements.hide) element.style.display = 'none'; } else this.elements.hide.style.display = 'none'; } // ──────────────────────────────────── addButtons (container, names) { var i = 0; for (let name of names) if (name == 'sep') { i += 1; container.appendChild(moaiEditor.createHTML('
')); } else container.appendChild(moaiEditor.buttons[name].handle); } // ──────────────────────────────────── createSidebarButton (icon, tooltip=null, onclick=null) { if (tooltip === null) tooltip = ''; else tooltip = ''+tooltip+''; const element = moaiEditor.createHTML('' + tooltip + moaiEditor.icons['ico_'+icon] + ''); if (onclick !== null) element.addEventListener("click", onclick); return element; } // ──────────────────────────────────── setupPageTools () { // Position the container this.elements.pagetools.style.top = '100px'; this.elements.pagetools.style.right = '8px'; // Currently we are just hiding it this.elements.pagetools.style.display = 'none'; // Remove some buttons that are out of place in edit mode /* const keepbuttons = ['top']; var items = this.elements.pagetools.querySelectorAll('li'); for (let li of items) if (!this.hasAnyClass(li, keepbuttons)) li.remove(); */ } // ──────────────────────────────────── hasAnyClass (element, classes) { for (let c of classes) if (element.classList.contains(c)) return true; return false; } // ──────────────────────────────────── toggleFullscreen() { if (!document.fullscreenElement && // alternative standard method !document.mozFullScreenElement && !document.webkitFullscreenElement) { // current working methods if (document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen(); } else if (document.documentElement.mozRequestFullScreen) { document.documentElement.mozRequestFullScreen(); } else if (document.documentElement.webkitRequestFullscreen) { document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } } else { if (document.cancelFullScreen) { document.cancelFullScreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitCancelFullScreen) { document.webkitCancelFullScreen(); } } moaiEditor.mirror.onTextareaResize(); } // ──────────────────────────────────── goRight() { if (this.mode == 'vphone') this.layout.goRight(); } goLeft() { if (this.mode == 'vphone') this.layout.goLeft(); } // ──────────────────────────────────── onClickEditorBtn() { moaiEditor.editor.toggle(); this.updateEditorBtn(); } updateEditorBtn() { this.btn_editor.querySelector("span").textContent = moaiEditor.editor.name; if (moaiEditor.editor.count < 2) this.btn_editor.classList.add('moaied-display-none'); else this.btn_editor.classList.remove('moaied-display-none'); } }; // End Class // ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ // ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ /* This class detects swipe gestures over an element on touch screens. */ MoaiEditor.Swipe = class { constructor(element, callback, distance=160) { this.callback = callback; this.distance = distance; this.touchstartX = 0; this.touchendX = 0; element.addEventListener('touchstart', e => { this.touchstartX = e.changedTouches[0].screenX; }); element.addEventListener('touchend', e => { this.touchendX = e.changedTouches[0].screenX; this.checkDirection(); }) } checkDirection() { if (this.touchendX < this.touchstartX - this.distance) this.callback('left'); if (this.touchendX > this.touchstartX + this.distance) this.callback('right'); } }; // End Class