xref: /template/mikio/assets/mikio.js (revision c617ba512081e0e8da8afd4720906c671b2e8c77)
1/**
2 * DokuWiki Mikio Template Javascript
3 *
4 * @link    http://dokuwiki.org/template:mikio
5 * @author  James Collins <james.collins@outlook.com.au>
6 * @license GPLv2 (http://www.gnu.org/licenses/gpl-2.0.html)
7 */
8"use strict";
9
10var mikio = {
11    queueResize: false,
12    mikioCSS: false,
13    stickyItems: [],
14    stickyOffset: 0,
15    stickyIndex: 2010,
16    darkMode: 'light',
17
18    ready: function () {
19        var self = this;
20
21        this.initDarkMode();
22        this.addToggleClick('mikio-sidebar-toggle', 'mikio-sidebar-collapse');
23        this.addToggleClick('mikio-navbar-toggle', 'mikio-navbar-collapse');
24        this.addDropdownClick('mikio-nav-dropdown', 'mikio-dropdown');
25        this.indexmenuPatch();
26		this.typeahead();
27
28        var updateStickyItems = function () {
29            window.removeEventListener('scroll', updateStickyScroll);
30
31            var stickyElements = document.getElementsByClassName('mikio-sticky');
32            self.stickyItems = [];
33            if (stickyElements && stickyElements.length > 0) {
34                var stickyOffset = stickyElements[0].offsetTop;
35                var stickyHeightCount = stickyOffset;
36
37                [].forEach.call(stickyElements, (item) => {
38                    var top = stickyOffset;
39                    if (item.offsetTop - stickyHeightCount > stickyHeightCount) {
40                        top = stickyHeightCount;
41                    }
42
43                    self.stickyItems.push({ element: item, offsetYTop: top, debugItemTop: item.offsetTop, debugOffset: stickyOffset, debugHeight: stickyHeightCount });
44                    stickyHeightCount += item.offsetHeight;
45                });
46
47                window.addEventListener('scroll', updateStickyScroll);
48                updateStickyScroll();
49            }
50        };
51
52        var updateStickyScroll = function () {
53            self.stickyItems.forEach((item) => {
54                if (window.pageYOffset > item.offsetYTop) {
55                    if (item.element.style.position != 'fixed') {
56                        var site = document.getElementById('dokuwiki__site');
57                        site.style.paddingTop = ((parseInt(site.style.paddingTop) || 0) + item.element.offsetHeight) + 'px';
58
59                        item.element.style.position = 'fixed';
60                        item.element.style.top = self.stickyOffset + 'px';
61                        item.element.style.zIndex = self.stickyIndex;
62
63                        self.stickyOffset += item.element.offsetHeight;
64                        self.stickyIndex--;
65                    }
66                } else {
67                    if (item.element.style.position == 'fixed') {
68                        var site = document.getElementById('dokuwiki__site');
69                        site.style.paddingTop = ((parseInt(site.style.paddingTop) || 0) - item.element.offsetHeight) + 'px';
70                        self.stickyOffset -= item.element.offsetHeight;
71                        self.stickyIndex++;
72
73                        item.element.style.position = 'relative';
74                        item.element.style.top = null;
75                        item.element.style.zIndex = null;
76                    }
77                }
78            });
79        };
80
81        updateStickyItems();
82
83        window.onresize = function () {
84            if (!this.queueResize) {
85                this.queueResize = true;
86                window.setTimeout(function () {
87                    this.queueResize = false;
88                    Array.from(document.getElementsByClassName('mikio-dropdown')).forEach(function (elem) {
89                        if (!elem.classList.contains('closed')) {
90                            elem.classList.add('closed');
91                        }
92                    });
93
94                    updateStickyItems();
95                }, 100);
96            }
97        };
98
99        // Mikio-Dropdown - Click
100        Array.from(document.getElementsByClassName('mikio-dropdown')).forEach(function (elem) {
101            elem.addEventListener('click', function (event) {
102                event.stopPropagation();
103            });
104        });
105
106        // Mikio-Dropdown - Close when clicked outside dropdown
107        Array.from(document.getElementsByTagName('body')).forEach(function (elem) {
108            elem.addEventListener('click', function (event) {
109                Array.from(document.getElementsByClassName('mikio-dropdown')).forEach(function (elem) {
110                    if (!elem.classList.contains('closed')) {
111                        elem.classList.add('closed');
112                    }
113                });
114            });
115        });
116
117        // Mikio-Navbar-Toggle - Fix
118        Array.from(document.getElementsByClassName('mikio-navbar-toggle')).forEach(function (elem) {
119            elem.classList.add('closed');
120        });
121
122        // Mikio-Dropdown - Fix
123        Array.from(document.getElementsByClassName('mikio-dropdown')).forEach(function (elem) {
124            elem.classList.add('closed');
125        });
126
127        // Input File - Cleanup
128        Array.from(document.querySelectorAll('input[type=file]')).forEach(function (elem) {
129            var style = window.getComputedStyle(elem);
130
131            if (style.display != 'none') {
132                var parentElem = elem.parentElement;
133                var fileRect = elem.getBoundingClientRect();
134                var parentRect = parentElem.getBoundingClientRect();
135                var spanElem = document.createElement('span');
136
137                elem.style.opacity = 0;
138                parentElem.style.position = 'relative';
139                spanElem.innerHTML = 'Choose file...';
140                spanElem.classList.add('mikio-input-file');
141                spanElem.style.left = Math.floor(fileRect.left - parentRect.left) + 'px';
142                spanElem.style.width = Math.floor(fileRect.right - fileRect.left) + 'px';
143                mikio.insertAfter(spanElem, elem);
144
145                spanElem.addEventListener('click', function (event) {
146                    if (event.target.parentElement.tagName.toLowerCase() != 'label') {
147                        let sibling = mikio.getPrevSibling(event.target, 'input');
148                        if (typeof sibling !== 'undefined') {
149                            sibling.click();
150                        }
151                    }
152                });
153
154                elem.addEventListener('change', function () {
155                    if (this.files.length > 0) {
156                        let mikioInput = mikio.getNextSibling(this, '.mikio-input-file');
157                        if (typeof mikioInput !== 'undefined') {
158                            mikioInput.innerHTML = this.files[0].name;
159                        }
160                    }
161                });
162            }
163        });
164
165        // Input - Span (Placeholder) clear when typing
166        Array.from(document.querySelectorAll('.mikio.dokuwiki .mode_login fieldset label.block input.edit, .mikio.dokuwiki .mode_denied fieldset label.block input.edit')).forEach(function (elem) {
167            if (elem.value.length != 0) {
168                var sibling = mikio.getPrevSibling(elem, 'span');
169                if (sibling) {
170                    sibling.style.display = 'none';
171                }
172            }
173
174            elem.addEventListener('keydown', function (event) {
175                var sibling = mikio.getPrevSibling(event.target, 'span');
176
177                setTimeout(function () {
178                    if (sibling) {
179                        if (event.target.value != '') {
180                            sibling.style.display = 'none';
181                        } else {
182                            sibling.style.display = 'block';
183                        }
184                    }
185                }, 50);
186            });
187        });
188
189        // Admin - Exit button
190        Array.from(document.querySelectorAll('a[rel="exit-admin"]')).forEach(function (elem) {
191            elem.addEventListener('click', function (event) {
192                event.preventDefault();
193
194                var href = window.location.protocol + "//" + window.location.host + "/" + window.location.pathname;
195
196                var params = window.location.search;
197                if (params !== '') {
198                    params = params.substr(1).split('&');
199                    if (params.length > 1) {
200                        href += '?';
201                        params.forEach(function (p) {
202                            if (p.substring(0, 3) == 'id=') {
203                                href += p;
204                            }
205                        });
206                    }
207                }
208
209                window.location = href;
210            });
211        });
212
213        // Admin - Back button
214        Array.from(document.querySelectorAll('a[rel="exit-page"]')).forEach(function (elem) {
215            elem.addEventListener('click', function (event) {
216                event.preventDefault();
217
218                var href = window.location.protocol + "//" + window.location.host + "/" + window.location.pathname;
219
220                var params = window.location.search;
221                if (params != '') {
222                    params = params.substr(1).split('&');
223                    if (params.length > 1) {
224                        href += '?';
225                        params.forEach(function (p) {
226                            if (p.substring(0, 5) != 'page=') {
227                                href += p + '&';
228                            }
229                        });
230                    }
231                }
232
233                window.location = href;
234            });
235        });
236
237        // Admin - Resize large text blocks in tasks
238        Array.from(document.querySelectorAll('.admin_tasks span.prompt')).forEach(function (elem) {
239            if (elem.offsetHeight > 48) {
240                elem.style.fontSize = '80%';
241            }
242        });
243
244        // Media Manager - ui-resizable is always auto
245        var mediaChangedObserver = new MutationObserver(function (mutationsList) {
246            for (let mutation of mutationsList) {
247                if (mutation.type === 'childList') {
248                    if (mutation.addedNodes) {
249                        mutation.addedNodes.forEach(function (node) {
250                            if (node.nodeName == 'LI') {
251
252                            }
253                        });
254                    }
255                }
256
257                if (mutation.type === 'attributes' && mutation.attributeName == 'style' && mutation.target && mutation.target.style.height) {
258                    mutation.target.style.height = '';
259                }
260            }
261        });
262
263        var target = document.getElementById('mediamanager__page');
264        if (target) {
265            mediaChangedObserver.observe(target, { attributes: true, childList: true, subtree: true });
266        }
267
268        // Media Manager - file click
269        Array.from(document.querySelectorAll('#mediamanager__page .filelist')).forEach(function (elem) {
270            elem.addEventListener('click', function (event) {
271                var liElem = event.target.closest('li');
272                if (liElem && event.target.closest('ul.thumbs')) {
273                    var aElem = liElem.querySelector('dd.name a');
274                    if (aElem) aElem.click();
275                }
276            });
277        });
278
279        // Popup Media Manager - clean file info
280        var mediaPopupFileInfoClean = function (elem) {
281            var file = { resolution: '', date: '', time: '', size: '' };
282
283            var infoElem = elem.querySelector('span.info');
284            if (infoElem) {
285                var infoText = infoElem.innerText.replace(/(<[^>]*>|[\(\)])/g, '');
286                var detail = infoText.split(' ');
287                while (detail.length < 4) {
288                    detail.unshift('');
289                }
290
291                infoElem.innerHTML = detail[0] + '<br>' + detail[1] + ' ' + detail[2] + '<br>' + detail[3];
292            }
293
294            Array.from(elem.querySelectorAll('img')).forEach(function (elem) {
295                elem.removeAttribute('width');
296                elem.removeAttribute('height');
297            });
298        }
299
300        var mediaPopupObserver = new MutationObserver(function (mutationsList) {
301            for (let mutation of mutationsList) {
302                if (mutation.type === 'childList') {
303                    if (mutation.addedNodes) {
304                        mutation.addedNodes.forEach(function (node) {
305                            if (node.nodeName == 'DIV') {
306                                mediaPopupFileInfoClean(node);
307                            }
308                        });
309                    }
310                }
311            }
312        });
313
314        var target = document.getElementById('media__content');
315        if (target) {
316            Array.from(target.querySelectorAll('div.odd, div.even')).forEach(function (elem) {
317                mediaPopupFileInfoClean(elem);
318            });
319
320            mediaPopupObserver.observe(target, { attributes: false, childList: true });
321        }
322
323        if (typeof mikioFooterRun === "function") mikioFooterRun();
324
325        var mediaChangedObserver = new MutationObserver(function (mutationsList) {
326            for (let mutation of mutationsList) {
327                if (mutation.type === 'attributes' && mutation.attributeName == 'href') {
328                    if (self.mikioCSS != false) {
329                        var elem = self.mikioCSS;
330                        var prev = elem.href;
331
332                        setTimeout(function () {
333                            var url = new URL(prev);
334                            var params = url.searchParams;
335                            params.set('seed', new Date().getTime());
336                            url.search = params.toString();
337                            elem.href = url.toString();
338                        }, 500);
339                    }
340                }
341            }
342        });
343
344        var linkElements = document.getElementsByTagName('link');
345        for (let element of linkElements) {
346            if (element.rel == 'stylesheet' && element.href) {
347                if (element.href.includes('/lib/exe/css.php')) {
348                    mediaChangedObserver.observe(element, { attributes: true, childList: true, subtree: true });
349                } else if (element.href.includes('/lib/tpl/mikio/css.php')) {
350                    this.mikioCSS = element;
351                }
352            }
353        }
354    },
355
356    initDarkMode: function () {
357        let setting = this.getCookie('lightDarkToggle');
358        if (setting == 'dark' || setting == 'light') {
359            this.darkMode = setting;
360        }
361
362        var self = this;
363        this.addEventListenerByClassName('mikio-darklight-button', 'click', function (event) {
364            event.preventDefault();
365            event.stopPropagation();
366
367            if(self.darkMode == 'dark') {
368                self.darkMode = 'light';
369            } else {
370                self.darkMode = 'dark';
371            }
372
373            self.updateDarkMode();
374        });
375
376        this.updateDarkMode();
377    },
378
379    updateDarkMode: function () {
380        const html = document.querySelector('html');
381        html.dataset.theme = `theme-${this.darkMode}`;
382        this.setCookie('lightDarkToggle', this.darkMode);
383    },
384
385    insertAfter: function (newNode, existingNode) {
386        existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
387    },
388
389    addToggleClick: function (elemToggle, elemCollapse) {
390        this.addEventListenerByClassName(elemToggle, 'click', function (event) {
391            event.preventDefault();
392            event.stopPropagation();
393            let nextSibling = mikio.getNextSibling(this, '.' + elemCollapse);
394
395            if (typeof nextSibling !== 'undefined') {
396                mikio.toggleCollapse(this, nextSibling);
397            }
398        });
399    },
400
401    addDropdownClick: function (elemToggle, elemCollapse) {
402        this.addEventListenerByClassName(elemToggle, 'click', function (event) {
403            event.preventDefault();
404            event.stopPropagation();
405
406            var dropdown = this.querySelector('.' + elemCollapse);
407            if (dropdown) {
408                mikio.toggleDropdown(dropdown);
409            }
410        });
411    },
412
413    addEventListenerByClassName: function (className, eventType, callback) {
414        Array.from(document.getElementsByClassName(className)).forEach(function (elem) {
415            elem.addEventListener(eventType, callback);
416        });
417    },
418
419    getNextSibling: function (elem, selector) {
420        var sibling = elem.nextElementSibling;
421
422        while (sibling) {
423            if (sibling.matches(selector)) return sibling;
424            sibling = sibling.nextElementSibling;
425        }
426    },
427
428    getPrevSibling: function (elem, selector) {
429        var sibling = elem.previousElementSibling;
430
431        while (sibling) {
432            if (sibling.matches(selector)) return sibling;
433            sibling = sibling.previousElementSibling;
434        }
435    },
436
437    toggleCollapse: function (objToggle, objCollapse) {
438        if (objToggle.classList.contains('closed')) {
439            objToggle.classList.remove('closed');
440            objToggle.classList.add('open');
441            var height = objCollapse.offsetHeight;
442            objCollapse.style.overflow = 'hidden';
443            objCollapse.style.height = 0;
444            objCollapse.style.paddingTop = 0;
445            objCollapse.style.paddingBottom = 0;
446            objCollapse.style.marginTop = 0;
447            objCollapse.style.marginBottom = 0;
448            objCollapse.offsetHeight;
449            objCollapse.style.boxSizing = 'border-box';
450            objCollapse.style.transitionProperty = "height, margin, padding";
451            objCollapse.style.transitionDuration = '500ms';
452            objCollapse.style.height = height + 'px';
453            objCollapse.style.removeProperty('padding-top');
454            objCollapse.style.removeProperty('padding-bottom');
455            objCollapse.style.removeProperty('margin-top');
456            objCollapse.style.removeProperty('margin-bottom');
457            window.setTimeout(function () {
458                objCollapse.style.removeProperty('height');
459                objCollapse.style.removeProperty('overflow');
460                objCollapse.style.removeProperty('transition-duration');
461                objCollapse.style.removeProperty('transition-property');
462                objCollapse.style.removeProperty('box-sizing');
463            }, 500);
464        } else {
465            objCollapse.style.transitionProperty = 'height, margin, padding';
466            objCollapse.style.transitionDuration = '500ms';
467            objCollapse.style.boxSizing = 'border-box';
468            objCollapse.style.height = objCollapse.offsetHeight + 'px';
469            objCollapse.offsetHeight;
470            objCollapse.style.overflow = 'hidden';
471            objCollapse.style.height = 0;
472            objCollapse.style.paddingTop = 0;
473            objCollapse.style.paddingBottom = 0;
474            objCollapse.style.marginTop = 0;
475            objCollapse.style.marginBottom = 0;
476            window.setTimeout(function () {
477                objToggle.classList.add('closed');
478                objToggle.classList.remove('open');
479                objCollapse.style.removeProperty('height');
480                objCollapse.style.removeProperty('padding-top');
481                objCollapse.style.removeProperty('padding-bottom');
482                objCollapse.style.removeProperty('margin-top');
483                objCollapse.style.removeProperty('margin-bottom');
484                objCollapse.style.removeProperty('overflow');
485                objCollapse.style.removeProperty('transition-duration');
486                objCollapse.style.removeProperty('transition-property');
487                objCollapse.style.removeProperty('box-sizing');
488            }, 500);
489        }
490    },
491
492
493    toggleDropdown: function (objToggle) {
494        if (objToggle.classList.contains('closed')) {
495            objToggle.classList.remove('closed');
496        } else {
497            objToggle.classList.add('closed');
498        }
499
500        Array.from(document.getElementsByClassName('mikio-dropdown')).forEach(function (elem) {
501            if (!elem.classList.contains('closed') && elem != objToggle) {
502                elem.classList.add('closed');
503            }
504        });
505    },
506
507    setHeroSubTitle: function (str) {
508        Array.from(document.getElementsByClassName('mikio-hero-subtitle')).forEach(function (elem) {
509            elem.innerHTML = str;
510        });
511    },
512
513    setHeroImage: function (str) {
514        var heroImages = document.getElementsByClassName('mikio-hero-image');
515
516        if (heroImages.length > 0) {
517            Array.from(document.getElementsByClassName('mikio-hero-image')).forEach(function (elem) {
518                elem.style.backgroundImage = 'url(\'' + str + '\')';
519                elem.classList.add('mikio-hero-image-resize');
520            });
521        } else {
522            Array.from(document.getElementsByClassName('mikio-hero-text')).forEach(function (elem) {
523                elem.insertAdjacentHTML('afterend', '<div class="mikio-hero-image mikio-hero-image-resize" style="background-image:url(\'' + str + '\');"></div>');
524            });
525        }
526    },
527
528    setHeroColor: function (str) {
529        var colors = str.trim().replace(/ +(?= )/g, '').split(/(?!\(.*)\s(?![^(]*?\))/g);
530        if (colors.length > 0 && colors[0] != '') {
531            Array.from(document.getElementsByClassName('mikio-hero')).forEach(function (elem) {
532                elem.style.backgroundColor = colors[0];
533            });
534
535            if (colors.length > 1) {
536                Array.from(document.getElementsByClassName('mikio-hero-title')).forEach(function (elem) {
537                    elem.style.color = colors[1];
538                });
539            }
540
541            if (colors.length > 2) {
542                Array.from(document.getElementsByClassName('mikio-hero-subtitle')).forEach(function (elem) {
543                    elem.style.color = colors[2];
544                });
545            }
546
547            if (colors.length > 3) {
548                Array.from(document.getElementsByClassName('mikio-hero')).forEach(function (parentElem) {
549                    Array.from(parentElem.querySelectorAll('.mikio-breadcrumbs ul li a')).forEach(function (elem) {
550                        elem.style.color = colors[3];
551                    });
552
553                    Array.from(parentElem.querySelectorAll('.mikio-breadcrumbs ul li, .mikio-breadcrumbs ul li a')).forEach(function (elem) {
554                        elem.style.color = colors[3];
555                        elem.onmouseover = function () { this.style.color = (colors.length > 4 ? colors[4] : 'initial'); };
556                        elem.onmouseout = function () { this.style.color = colors[3]; };
557                    });
558                });
559            }
560        }
561    },
562
563    setTags: function (str) {
564        Array.from(document.getElementsByClassName('mikio-tags')).forEach(function (elem) {
565            elem.innerHTML = str;
566        });
567    },
568
569    hidePart: function (part) {
570        var selectorArray = {
571            topheader: '.mikio-page-topheader',
572            header: '.mikio-page-header',
573            contentheader: '.mikio-page-contentheader',
574            contentfooter: '.mikio-page-contentfooter',
575            sidebarheader: '.mikio-sidebar-left .mikio-sidebar-header',
576            sidebarfooter: '.mikio-sidebar-left .mikio-sidebar-footer',
577            rightsidebarheader: '.mikio-sidebar-right .mikio-sidebar-header',
578            rightsidebarfooter: '.mikio-sidebar-right .mikio-sidebar-footer',
579            footer: '.mikio-footer',
580            bottomfooter: '.mikio-page-bottomfooter',
581            navbar: '.mikio-navbar',
582            hero: '.mikio-hero'
583        };
584
585        if (selectorArray.hasOwnProperty(part)) {
586            Array.from(document.querySelectorAll(selectorArray[part])).forEach(function (elem) {
587                elem.style.display = 'none';
588            });
589        }
590    },
591
592    indexmenuPatch: function () {
593        window.setTimeout(function () {
594            Array.from(document.querySelectorAll('a.navSel')).forEach(function (elem) {
595                let prev = mikio.getPrevSibling(elem, 'img');
596                if (prev) {
597                    prev.style.opacity = 1;
598                }
599            });
600        }, 50);
601
602
603        document.addEventListener('mouseover', function (event) {
604            const indexmenuClasses = ['nodeUrl', 'nodeSel', 'node'];
605            if ([...event.target.classList].some(className => indexmenuClasses.indexOf(className) !== -1)) {
606                let prev = mikio.getPrevSibling(event.target, 'img');
607                if (prev) {
608                    prev.style.opacity = 1;
609                }
610            }
611        });
612
613        document.addEventListener('mouseout', function (event) {
614            const indexmenuClasses = ['nodeUrl', 'nodeSel', 'node'];
615            if ([...event.target.classList].some(className => indexmenuClasses.indexOf(className) !== -1)) {
616                let prev = mikio.getPrevSibling(event.target, 'img');
617                if (prev) {
618                    prev.style.opacity = '';
619                }
620            }
621        });
622    },
623
624   // Add typeahead support for quick seach. Taken from bootstrap3 theme.
625    typeahead: function () {
626
627        jQuery(".search_typeahead").typeahead({
628
629            source: function (query, process) {
630
631                return jQuery.post(DOKU_BASE + 'lib/exe/ajax.php',
632                    {
633                        call: 'qsearch',
634                        q: encodeURI(query)
635                    },
636                    function (data) {
637
638                        var results = [];
639
640                        jQuery(data).find('a').each(function () {
641
642                            var page = jQuery(this);
643
644                            results.push({
645                                name: page.text(),
646                                href: page.attr('href'),
647                                title: page.attr('title'),
648                                category: page.attr('title').replace(/:/g, ' : '),
649                            });
650
651                        });
652
653                        return process(results);
654
655                    });
656            },
657
658            itemLink: function (item) {
659                return item.href;
660            },
661
662            itemTitle: function (item) {
663                return item.title;
664            },
665
666            followLinkOnSelect: true,
667            autoSelect: false,
668            items: 10,
669            fitToElement: true,
670            delay: 500,
671			theme: 'bootstrap4',
672
673        });
674    },
675
676    getCookie: function(cname) {
677        let name = cname + "=";
678        let decodedCookie = decodeURIComponent(document.cookie);
679        let ca = decodedCookie.split(';');
680        for (let i = 0; i < ca.length; i++) {
681            let c = ca[i];
682            while (c.charAt(0) == ' ') {
683                c = c.substring(1);
684            }
685            if (c.indexOf(name) == 0) {
686                return c.substring(name.length, c.length);
687            }
688        }
689        return "";
690    },
691
692    setCookie: function(cname, cvalue, exdays) {
693        const d = new Date();
694        d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
695        let expires = "expires=" + d.toUTCString();
696        document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
697    },
698
699    clearCookie: function(cname) {
700        document.cookie = cname + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
701    }
702};
703
704if (document.readyState != 'loading') {
705    mikio.ready();
706} else {
707    document.addEventListener('DOMContentLoaded', function () { mikio.ready() });
708}
709