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