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