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