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