1/* DokuWiki MoaiEditor Layout.js file 2 Version : 0.5a (May 6, 2026) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6/* Layout class 7 ‾‾‾‾‾‾‾‾‾‾‾‾ 8 Umbrella class for the different layouts of the editor (desktop, phone, etc). 9 10 It receives all the needed HTML elements found by the template-specific class and then: 11 - Creates the needed HTML elements common to all layouts. 12 - Tweaks the HTML and BODY elements to support the editor. 13 - Tweaks some existing elements like the TEXTAREA to support the editor. 14 - Hides the original editor. 15 16 It also monitors window resize events and switches to the correct layout (desktop, phone, etc) 17 as needed. 18*/ 19MoaiEditor.Layout = class { 20 21 constructor(elements) { 22 23 // Arguments 24 this.elements = elements; 25 26 // Variables 27 this.mode = null; // String identifying the current layout ('desktop', 'vphone', etc) 28 this.layout = null; // Object managing the current layout 29 30 // Settings 31 this.settings = {hide_intro_message: true}; 32 33 // Objects 34 this.desktop = new MoaiEditor.LayoutDesktop(this); 35 this.vphone = new MoaiEditor.LayoutPhoneVertical(this); 36 37 // Constants 38 this.modes = { 39 desktop : null, 40 vphone : 600 41 }; 42 } 43 // ──────────────────────────────────── 44 init() { 45 46 // Common setup for all layouts (desktop, phone, etc) 47 this.setup(); 48 49 // Activate the specific layout for the current screen resolution 50 this.onWindowResize(); 51 } 52 // ──────────────────────────────────── 53 onWindowResize() { 54 var w = window.innerWidth; 55 var h = window.innerHeight; 56 // Determine the current mode 57 var mode = 'desktop'; 58 for (let m in this.modes) { 59 const maxres = this.modes[m]; 60 if (maxres !== null && w <= maxres) 61 mode = m; 62 } 63 // If the mode is the same 64 if (this.mode === mode) { 65 // Update things based on window size 66 this.layout.onWindowResize(); 67 // Return 68 return; 69 } 70 // Store the current mode 71 this.mode = mode; 72 // Make the previous layout do any deactivation tasks 73 if (this.layout !== null) 74 this.layout.deactivate(); 75 // Remove the direct children of the editor container 76 while (this.editor.hasChildNodes()) 77 this.editor.firstChild.remove(); 78 // Set the current layout mode and activate it 79 this.layout = this[mode]; 80 this.layout.activate(); 81 // Update the editor toggle button 82 this.updateEditorBtn(); 83 // Update things based on window size 84 this.layout.onWindowResize(); 85 // Update plugins which depend on layout mode 86 for (let name in moaiEditor.plugins) 87 if (moaiEditor.plugins[name].onSpecificLayout) 88 moaiEditor.plugins[name].onSpecificLayout(mode); 89 } 90 // ──────────────────────────────────── 91 setup (elements) { 92 93 // Chrome uses this tag in the head in order to behave like most browsers on Android 94 var meta = document.createElement('meta'); 95 meta.name = "viewport"; 96 meta.content = "width=device-width, initial-scale=1.0, interactive-widget=resizes-content"; 97 document.getElementsByTagName('head')[0].appendChild(meta); 98 99 // Hide elements that take space 100 const no = document.body.querySelector('body > .no'); 101 if (no !== null) 102 no.style.display = 'none'; 103 104 // Style the HTML element 105 const html = document.documentElement; 106 html.style.overflow = 'hidden'; 107 html.style.height = '100%'; 108 //html.style.height = '100dvh'; 109 //html.style.setProperty('height', '100% !important'); 110 html.style.width = '100%'; 111 html.style.margin = '0'; 112 html.style.padding = '0'; 113 114 // Style the BODY element 115 const body = document.body; 116 body.style.height = '100%'; 117 //body.style.height = '100dvh'; 118 //body.style.setProperty('height', '100% !important'); 119 body.style.width = '100%'; 120 body.style.margin = '0'; 121 body.style.padding = '0'; 122 body.style.minWidth = '0px'; 123 body.style.overflowX = 'hidden'; 124 body.style.boxSizing = 'border-box'; 125 body.style.scrollBehavior = 'smooth'; 126 //body.style.background = 'rgba(0,0,255,0.1)'; 127 128 // Add a class to the html and body elements to be able to style them after the editor starts (but not before) 129 html.classList.add("moaied"); 130 body.classList.add("moaied"); 131 132 // Set font size of the root element (rem) if its not set 133 if (html.style.fontSize == '') 134 html.style.fontSize = '16px'; 135 136 // Button attributes for Save and Cancel 137 moaiEditor.buttons.save.handle.name = 'do[save]'; 138 moaiEditor.buttons.cancel.handle.name = 'do[cancel]'; 139 moaiEditor.buttons.save.handle.value = '1'; 140 moaiEditor.buttons.cancel.handle.value = '1'; 141 moaiEditor.buttons.save.handle.setAttribute ("form",'dw__editform'); 142 moaiEditor.buttons.cancel.handle.setAttribute("form",'dw__editform'); 143 144 // Disable "Confirm that you want to leave" popup when clicking 'Save' 145 moaiEditor.buttons.save.handle.addEventListener("click", function(){ window.onbeforeunload=''; textChanged=false; }); 146 147 // Create the pageid (example: animals:mammals:dogs) 148 var path = JSINFO.id.split(":"); 149 path[path.length-1] = '<b>'+path[path.length-1]+'</b>'; 150 var title = "Document id (path in the wiki namespace)"; 151 this.pageid = moaiEditor.createHTML('<div id="moaied__pageid"></div>'); 152 this.pageid.innerHTML = '<a title="'+title+'" href="doku.php?id='+JSINFO.id+'">' + path.join('<span>»</span>') + '</a>'; 153 154 // Create sidebar buttons 155 this.btn_editor = this.createSidebarButton ('editor', '--', this.onClickEditorBtn.bind(this) ); 156 this.btn_linewrap = this.createSidebarButton ('linewrap', 'Line wrap', moaiEditor.editor.toggleWrapLines.bind(moaiEditor.editor)); 157 this.btn_fullscreen = this.createSidebarButton ('fullscreen', 'Full screen', this.toggleFullscreen ); 158 this.btn_scrolltop = this.createSidebarButton ('scrolltop', 'Go to top', moaiEditor.scroll.toTop.bind(moaiEditor.scroll) ); 159 this.btn_scrollbottom = this.createSidebarButton ('scrollbottom', 'Go to bottom', moaiEditor.scroll.toBottom.bind(moaiEditor.scroll) ); 160 161 // Create automatic-scroll-in-progress visual indicator 162 this.indicatorScrolling = moaiEditor.createHTML('<div id="moaied__autoscrolling_indicator">'+moaiEditor.icons.ico_autoscroll2+'</div>'); 163 164 // Create container element positioned at the bottom right (for logo, codemirror menu, etc) 165 this.bottomRight = moaiEditor.createHTML('<div id="moaied__bottom_right"></div>'); 166 167 // Create the container for dokuwiki messages (info, error, success, notify) usually rendered by inc/html.php -> html_msgarea(). 168 this.msgarea = moaiEditor.createHTML('<div id="moaied__msg_area"></div>'); 169 for (let message of this.elements.messages) 170 this.msgarea.appendChild(message); 171 172 // Add the editor intro message 173 var intromsg = JSINFO.plugin_moaieditor.intromsg; 174 console.warn(); 175 if (intromsg !== null) 176 if (intromsg.type == 'editrev' || !this.settings.hide_intro_message) { 177 const element = moaiEditor.createHTML(intromsg.html); 178 this.msgarea.appendChild(element); 179 } 180 // Editor version (and moai logo) 181 var title; 182 title = 'MoaiEditor :'; 183 title+= '\n info@moaitools.org'; 184 title+= '\n Version: ' + moaiEditor.version_number + ' ('+moaiEditor.version_date+')'; 185 title+= '\n Detected template: "'+ moaiEditor.template.name+'"'; 186 title+= '\n Page id: "'+ JSINFO.id+'"'; 187 title+= '\n Available editors: \n'+ moaiEditor.editor.getEditors(); 188 this.logo = moaiEditor.createHTML('<div id="moaied__logo"></div>'); 189 this.logo.title = title; 190 this.logo.innerHTML = moaiEditor.icons.logo_moai+'<div>moai</div><div>editor</div><i>'+moaiEditor.version_number+'</i>'; 191 this.bottomRight.appendChild(this.logo); 192 193 // Setup the edit-summary input fields 194 this.summary = this.elements.editSummary; 195 196 // Assign the form to the input fields 197 var editSummary = document.body.querySelector('#edit__summary'); 198 var minorEdit = document.body.querySelector('#edit__minoredit'); 199 editSummary.setAttribute('form', 'dw__editform'); 200 if (minorEdit) 201 minorEdit.setAttribute('form', 'dw__editform'); 202 203 // Remove size attribute (control via CSS) 204 editSummary.removeAttribute('size'); 205 206 // Tooltips 207 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."; 208 if (this.summary.childNodes[2]) 209 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."; 210 211 // Style the textarea 212 this.textarea = this.elements.textarea; 213 this.textarea.setAttribute('form', 'dw__editform'); 214 var style = "position: absolute;"; 215 style += "box-sizing: border-box;"; 216 style += "top:0; left:0;"; 217 style += "margin:0; height:100%; width:100%;"; 218 style += "box-shadow:none !important;"; 219 style += "border:none !important;"; 220 style += "border-radius:0 !important;"; 221 style += "outline:none !important;"; 222 style += "resize: none;"; 223 style += "scroll-behavior:auto;"; 224 style += "overflow-y:scroll;"; // Should prevent the scrollbar from reflowing the text 225 style += "overflow-x:auto;"; 226 style += "padding: 5px 5px;"; 227 style += "z-index: 10;"; 228 style += "background:none; "; 229 //style += "color:transparent;"; 230 style += "color:rgba(0,0,0,0.2);"; 231 //style += "pointer-events:none;"; 232 this.textarea.setAttribute('style', style); // Remove current inline style and apply the new 233 234 // Prepare the dokuwiki edit form to be the editor pane 235 // * We have to use the Dokuwiki form as the container for the texarea because 'locktimer.js' require it. 236 // * It would break otherwise. 237 this.editpane = this.elements.editForm; 238 this.editpane.classList.add('pane'); 239 240 // Create an element where to dump template elements we want to hide 241 this.hidden = moaiEditor.createHTML('<div id="moaied__hidden_template_elements"></div>'); 242 document.body.appendChild(this.hidden); 243 244 // Remove all elements from the form except the textarea and the hidden input fields 245 var nodes = []; 246 for (let node of this.elements.editForm.childNodes) { 247 if (node.tagName == 'TEXAREA') continue; 248 if (node.tagName == 'INPUT' && node.type == "hidden") continue; 249 nodes.push(node) 250 } 251 for (let node of nodes) 252 this.hidden.appendChild(node); 253 254 // Create the main wrapper 255 this.wrapper = moaiEditor.createHTML('<div id="moaied__wrapper"></div>'); 256 //document.body.appendChild(this.wrapper); 257 document.body.insertBefore (this.wrapper, document.body.firstChild); 258 259 // Create the editor container 260 this.editor = moaiEditor.createHTML('<div id="moaied__editor" class="dokuwiki"></div>'); 261 this.wrapper.appendChild(this.editor); 262 263 // Create both panes 264 this.panes = moaiEditor.createHTML('<div id="moaied__panes"></div>'); // Dual pane container 265 //this.editpane = moaiEditor.createHTML('<div id="moaied__editpane" class="pane"></div>'); // Container for textarea and mirror 266 this.preview = moaiEditor.createHTML('<div id="moaied__preview" class="pane page"></div>'); // Container for preview 267 this.panes.appendChild(this.editpane); 268 this.panes.appendChild(this.preview); 269 270 // Add the textarea 271 this.textarea = this.elements.textarea; 272 this.editpane.appendChild(this.textarea); 273 274 // Hide the original editor 275 if (this.elements.hide.constructor === Array) { 276 for (let element of this.elements.hide) 277 element.style.display = 'none'; 278 } else 279 this.elements.hide.style.display = 'none'; 280 } 281 // ──────────────────────────────────── 282 addButtons (container, names) { 283 var i = 0; 284 for (let name of names) 285 if (name == 'sep') { 286 i += 1; 287 container.appendChild(moaiEditor.createHTML('<div id="moaied__sep_'+i+'" class="moaied-button-separator"></div>')); 288 } else 289 container.appendChild(moaiEditor.buttons[name].handle); 290 291 } 292 // ──────────────────────────────────── 293 createSidebarButton (icon, tooltip=null, onclick=null) { 294 295 if (tooltip === null) 296 tooltip = ''; 297 else 298 tooltip = '<span class="moaied-tooltip-text moaied-tooltip-left">'+tooltip+'</span>'; 299 const element = moaiEditor.createHTML('<a id="moaied__sidebar_btn_'+icon+'" class="moaied-tooltip moaied-sidebutton moaied-zindex-30">' + tooltip + moaiEditor.icons['ico_'+icon] + '</a>'); 300 if (onclick !== null) 301 element.addEventListener("click", onclick); 302 return element; 303 } 304 // ──────────────────────────────────── 305 setupPageTools () { 306 307 // Position the container 308 this.elements.pagetools.style.top = '100px'; 309 this.elements.pagetools.style.right = '8px'; 310 311 // Currently we are just hiding it 312 this.elements.pagetools.style.display = 'none'; 313 314 // Remove some buttons that are out of place in edit mode 315 /* 316 const keepbuttons = ['top']; 317 var items = this.elements.pagetools.querySelectorAll('li'); 318 for (let li of items) 319 if (!this.hasAnyClass(li, keepbuttons)) 320 li.remove(); 321 */ 322 } 323 // ──────────────────────────────────── 324 hasAnyClass (element, classes) { 325 for (let c of classes) 326 if (element.classList.contains(c)) 327 return true; 328 return false; 329 } 330 // ──────────────────────────────────── 331 toggleFullscreen() { 332 333 if (!document.fullscreenElement && // alternative standard method 334 !document.mozFullScreenElement && !document.webkitFullscreenElement) { // current working methods 335 if (document.documentElement.requestFullscreen) { 336 document.documentElement.requestFullscreen(); 337 } else if (document.documentElement.mozRequestFullScreen) { 338 document.documentElement.mozRequestFullScreen(); 339 } else if (document.documentElement.webkitRequestFullscreen) { 340 document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); 341 } 342 } else { 343 if (document.cancelFullScreen) { 344 document.cancelFullScreen(); 345 } else if (document.mozCancelFullScreen) { 346 document.mozCancelFullScreen(); 347 } else if (document.webkitCancelFullScreen) { 348 document.webkitCancelFullScreen(); 349 } 350 } 351 moaiEditor.mirror.onTextareaResize(); 352 } 353 // ──────────────────────────────────── 354 goRight() { 355 if (this.mode == 'vphone') 356 this.layout.goRight(); 357 } 358 goLeft() { 359 if (this.mode == 'vphone') 360 this.layout.goLeft(); 361 } 362 // ──────────────────────────────────── 363 onClickEditorBtn() { 364 moaiEditor.editor.toggle(); 365 this.updateEditorBtn(); 366 } 367 updateEditorBtn() { 368 this.btn_editor.querySelector("span").textContent = moaiEditor.editor.name; 369 if (moaiEditor.editor.count < 2) 370 this.btn_editor.classList.add('moaied-display-none'); 371 else 372 this.btn_editor.classList.remove('moaied-display-none'); 373 } 374 375}; // End Class 376 377 378// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 379// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 380/* 381 This class detects swipe gestures over an element on touch screens. 382*/ 383MoaiEditor.Swipe = class { 384 385 constructor(element, callback, distance=160) { 386 387 this.callback = callback; 388 this.distance = distance; 389 this.touchstartX = 0; 390 this.touchendX = 0; 391 element.addEventListener('touchstart', e => { 392 this.touchstartX = e.changedTouches[0].screenX; 393 }); 394 element.addEventListener('touchend', e => { 395 this.touchendX = e.changedTouches[0].screenX; 396 this.checkDirection(); 397 }) 398 } 399 checkDirection() { 400 if (this.touchendX < this.touchstartX - this.distance) 401 this.callback('left'); 402 if (this.touchendX > this.touchstartX + this.distance) 403 this.callback('right'); 404 } 405}; // End Class 406 407 408 409 410 411 412 413 414