Contents

MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 18: Line 18:
     function getSavedTheme() {
     function getSavedTheme() {
         try {
         try {
             var saved = window.localStorage.getItem(STORAGE_KEY);
             var saved = window.localStorage.getItem(
                STORAGE_KEY
            );


             if (saved === LIGHT || saved === DARK) {
             if (
                saved === LIGHT ||
                saved === DARK
            ) {
                 return saved;
                 return saved;
             }
             }
Line 48: Line 53:


     function getCurrentTheme() {
     function getCurrentTheme() {
         return document.documentElement.classList.contains(
         return document.documentElement
            'wp-theme-dark'
            .classList.contains(
        )
                'wp-theme-dark'
            )
             ? DARK
             ? DARK
             : LIGHT;
             : LIGHT;
Line 61: Line 67:


     function applyTheme(theme, shouldSave) {
     function applyTheme(theme, shouldSave) {
         var html = document.documentElement;
         var html =
         var isDark = theme === DARK;
            document.documentElement;
 
         var isDark =
            theme === DARK;
 


         html.classList.toggle(
         html.classList.toggle(
Line 74: Line 84:
         );
         );


        /*
        * Keep MediaWiki-compatible theme classes too.
        */
         html.classList.remove(
         html.classList.remove(
             'skin-theme-clientpref-day',
             'skin-theme-clientpref-day',
Line 79: Line 93:
             'skin-theme-clientpref-os'
             'skin-theme-clientpref-os'
         );
         );


         html.classList.add(
         html.classList.add(
Line 85: Line 100:
                 : 'skin-theme-clientpref-day'
                 : 'skin-theme-clientpref-day'
         );
         );


         html.setAttribute(
         html.setAttribute(
             'data-wikipartner-theme',
             'data-wikipartner-theme',
             isDark ? DARK : LIGHT
             isDark
                ? DARK
                : LIGHT
         );
         );


         if (shouldSave !== false) {
         if (shouldSave !== false) {
             saveTheme(theme);
             saveTheme(theme);
         }
         }


         updateThemeButton();
         updateThemeButton();
Line 106: Line 126:
         return [
         return [
             '<svg viewBox="0 0 24 24" aria-hidden="true">',
             '<svg viewBox="0 0 24 24" aria-hidden="true">',
                 '<path d="M20.3 15.2A8.4 8.4 0 0 1 8.8 3.7 8.5 8.5 0 1 0 11.5 11.5Z"></path>',
                 '<path d="M20.3 15.2A8.4 8.4 0 0 1 8.8 3.7 8.5 8.5 0 1 0 11.5 11.5Z"></path>',
                 '<path class="wp-theme-star" d="M17.5 4v3M16 5.5h3"></path>',
                 '<path class="wp-theme-star" d="M17.5 4v3M16 5.5h3"></path>',
             '</svg>'
             '</svg>'
         ].join('');
         ].join('');
Line 116: Line 139:
         return [
         return [
             '<svg viewBox="0 0 24 24" aria-hidden="true">',
             '<svg viewBox="0 0 24 24" aria-hidden="true">',
                 '<circle cx="12" cy="12" r="3.8"></circle>',
                 '<circle cx="12" cy="12" r="3.8"></circle>',
                 '<path d="M12 2.3v2.1"></path>',
                 '<path d="M12 2.3v2.1"></path>',
                 '<path d="M12 19.6v2.1"></path>',
                 '<path d="M12 19.6v2.1"></path>',
                 '<path d="M4.93 4.93l1.5 1.5"></path>',
                 '<path d="M4.93 4.93l1.5 1.5"></path>',
                 '<path d="M17.57 17.57l1.5 1.5"></path>',
                 '<path d="M17.57 17.57l1.5 1.5"></path>',
                 '<path d="M2.3 12h2.1"></path>',
                 '<path d="M2.3 12h2.1"></path>',
                 '<path d="M19.6 12h2.1"></path>',
                 '<path d="M19.6 12h2.1"></path>',
                 '<path d="M4.93 19.07l1.5-1.5"></path>',
                 '<path d="M4.93 19.07l1.5-1.5"></path>',
                 '<path d="M17.57 6.43l1.5-1.5"></path>',
                 '<path d="M17.57 6.43l1.5-1.5"></path>',
             '</svg>'
             '</svg>'
         ].join('');
         ].join('');
Line 135: Line 164:


     function updateThemeButton() {
     function updateThemeButton() {
         var button = document.getElementById(
         var button =
            'wikipartner-theme-toggle'
            document.getElementById(
        );
                'wikipartner-theme-toggle'
            );
 


         if (!button) {
         if (!button) {
             return;
             return;
         }
         }


         var isDark =
         var isDark =
             getCurrentTheme() === DARK;
             getCurrentTheme() === DARK;


         button.innerHTML =
         button.innerHTML =
Line 154: Line 187:
                 ) +
                 ) +
             '</span>';
             '</span>';


         button.setAttribute(
         button.setAttribute(
Line 161: Line 195:
                 : 'Switch to dark mode'
                 : 'Switch to dark mode'
         );
         );


         button.setAttribute(
         button.setAttribute(
Line 176: Line 211:


     function createThemeToggle(list) {
     function createThemeToggle(list) {
         var existing = document.getElementById(
         var existing =
            'wikipartner-theme-item'
            document.getElementById(
        );
                'wikipartner-theme-item'
            );
 


         if (existing) {
         if (existing) {
Line 184: Line 221:
         }
         }


         var item = document.createElement('li');
 
         var item =
            document.createElement('li');
 


         item.id =
         item.id =
             'wikipartner-theme-item';
             'wikipartner-theme-item';


         item.className =
         item.className =
             'mw-list-item wikipartner-theme-item';
             'mw-list-item ' +
            'wikipartner-theme-item';




         var button =
         var button =
             document.createElement('button');
             document.createElement(
                'button'
            );
 


         button.id =
         button.id =
             'wikipartner-theme-toggle';
             'wikipartner-theme-toggle';


         button.className =
         button.className =
             'wikipartner-theme-toggle';
             'wikipartner-theme-toggle';


         button.type =
         button.type =
Line 209: Line 256:
             'click',
             'click',
             function () {
             function () {
                 var nextTheme =
                 var nextTheme =
                     getCurrentTheme() === DARK
                     getCurrentTheme() === DARK
                         ? LIGHT
                         ? LIGHT
                         : DARK;
                         : DARK;


                 applyTheme(
                 applyTheme(
Line 222: Line 271:




         item.appendChild(button);
         item.appendChild(
            button
        );
 


         updateThemeButton();
         updateThemeButton();


         return item;
         return item;
Line 246: Line 299:


     /* ========================================================
     /* ========================================================
       CLEAN DUPLICATE USERNAME
       FIND / CLEAN USERNAME
     ======================================================== */
     ======================================================== */


Line 253: Line 306:
         list
         list
     ) {
     ) {
         var username = mw.config.get(
         var username =
            'wgUserName'
            mw.config.get(
        );
                'wgUserName'
            );
 


         if (!username) {
         if (!username) {
             return;
             return;
         }
         }
        /*
        * MediaWiki's data-user-page already puts
        * the username in .wp-user-primary.
        *
        * Some personal-menu configurations can
        * also provide another user-page link.
        * Remove that duplicate from the menu.
        */


         var items =
         var items =
Line 265: Line 330:
                 list.children
                 list.children
             );
             );


         items.forEach(
         items.forEach(
             function (item) {
             function (item) {
                 var link =
                 var link =
                     item.querySelector('a');
                     item.querySelector('a');


                 var linkText =
                 var linkText =
Line 275: Line 343:
                         ? link.textContent.trim()
                         ? link.textContent.trim()
                         : '';
                         : '';


                 var isUserPageId =
                 var isUserPageId =
                     item.id === 'pt-userpage' ||
                     item.id === 'pt-userpage' ||
                     item.id === 'pt-userpage-2';
                     item.id === 'pt-userpage-2';


                 var isUsername =
                 var isUsername =
                     linkText === username;
                     linkText === username;


                 if (
                 if (
Line 287: Line 358:
                     isUsername
                     isUsername
                 ) {
                 ) {
                    /*
                    * If primary somehow has no username,
                    * move this one there instead.
                    */
                     if (
                     if (
                         primary &&
                         primary &&
                         !primary.querySelector('li')
                         !primary.querySelector('li')
                     ) {
                     ) {
                         primary.appendChild(item);
 
                         primary.appendChild(
                            item
                        );
 
                     } else {
                     } else {
                         item.remove();
                         item.remove();
                     }
                     }
                 }
                 }
Line 316: Line 398:
             'wp-is-logged-out'
             'wp-is-logged-out'
         );
         );


         var loginItem =
         var loginItem =
Line 321: Line 404:
                 '#pt-login'
                 '#pt-login'
             );
             );


         /*
         /*
         * Layout:
         * Desired layout:
         *
         *
         * Create account | Theme | Log in
         * Create account | Theme | Log in
Line 329: Line 413:


         if (loginItem) {
         if (loginItem) {
             list.insertBefore(
             list.insertBefore(
                 themeItem,
                 themeItem,
                 loginItem
                 loginItem
             );
             );
         } else {
         } else {
             list.appendChild(
             list.appendChild(
                 themeItem
                 themeItem
             );
             );
         }
         }
     }
     }
Line 366: Line 454:


         /*
         /*
         * Prevent duplicate dropdown.
         * Remove an old dropdown if ResourceLoader
        * initialization somehow runs again.
         */
         */


Line 373: Line 462:
                 '.wp-user-dropdown'
                 '.wp-user-dropdown'
             );
             );


         if (oldDropdown) {
         if (oldDropdown) {
Line 380: Line 470:


         /* ====================================================
         /* ====================================================
           DROPDOWN WRAPPER
           CREATE HAMBURGER WRAPPER
         ==================================================== */
         ==================================================== */


         var dropdown =
         var dropdown =
             document.createElement('li');
             document.createElement('li');


         dropdown.className =
         dropdown.className =
Line 395: Line 486:


         var trigger =
         var trigger =
             document.createElement('button');
             document.createElement(
                'button'
            );
 


         trigger.type =
         trigger.type =
             'button';
             'button';


         trigger.className =
         trigger.className =
             'wp-user-menu-trigger';
             'wp-user-menu-trigger';


         trigger.setAttribute(
         trigger.setAttribute(
Line 407: Line 503:
             'Open user menu'
             'Open user menu'
         );
         );


         trigger.setAttribute(
         trigger.setAttribute(
Line 412: Line 509:
             'User menu'
             'User menu'
         );
         );


         trigger.setAttribute(
         trigger.setAttribute(
Line 417: Line 515:
             'false'
             'false'
         );
         );


         trigger.innerHTML =
         trigger.innerHTML =
Line 423: Line 522:


         /* ====================================================
         /* ====================================================
           DROPDOWN MENU
           DROPDOWN CONTENT
         ==================================================== */
         ==================================================== */


         var menu =
         var menu =
             document.createElement('ul');
             document.createElement('ul');


         menu.className =
         menu.className =
             'wp-user-dropdown-menu';
             'wp-user-dropdown-menu';


         menu.hidden =
         menu.hidden =
Line 437: Line 538:


         /*
         /*
         * Move all user controls into the
         * Move every existing menu item
         * hamburger except the theme button.
         * into ☰.
         *
         *
         * Username already lives separately
         * Username is NOT in this list anymore.
         * inside .wp-user-primary.
         * Theme is not inserted yet.
         */
         */


Line 450: Line 551:
             .forEach(
             .forEach(
                 function (item) {
                 function (item) {
                     if (
                     if (
                         item !== themeItem &&
                         item !== themeItem &&
                         item !== dropdown
                         item !== dropdown
                     ) {
                     ) {
                         menu.appendChild(item);
 
                         menu.appendChild(
                            item
                        );
                     }
                     }
                 }
                 }
Line 460: Line 565:




         dropdown.appendChild(trigger);
         dropdown.appendChild(
         dropdown.appendChild(menu);
            trigger
        );
 
 
         dropdown.appendChild(
            menu
        );




         /*
         /*
         * Final logged-in header layout:
         * Exact layout:
         *
         *
         * Username | Theme | ☰
         * Username | Theme | ☰
        *
        * Username lives in .wp-user-primary,
        * while theme and hamburger live here.
         */
         */


         list.appendChild(themeItem);
         list.appendChild(
         list.appendChild(dropdown);
            themeItem
        );
 
 
         list.appendChild(
            dropdown
        );




Line 482: Line 602:
                 'is-open'
                 'is-open'
             );
             );


             menu.hidden =
             menu.hidden =
                 true;
                 true;


             trigger.setAttribute(
             trigger.setAttribute(
Line 497: Line 619:
                 'is-open'
                 'is-open'
             );
             );


             menu.hidden =
             menu.hidden =
                 false;
                 false;


             trigger.setAttribute(
             trigger.setAttribute(
Line 509: Line 633:


         /* ====================================================
         /* ====================================================
           HAMBURGER CLICK
           CLICK HAMBURGER
         ==================================================== */
         ==================================================== */


Line 515: Line 639:
             'click',
             'click',
             function (event) {
             function (event) {
                 event.preventDefault();
                 event.preventDefault();
                 event.stopPropagation();
                 event.stopPropagation();


                 if (
                 if (
Line 523: Line 649:
                     )
                     )
                 ) {
                 ) {
                     closeMenu();
                     closeMenu();
                 } else {
                 } else {
                     openMenu();
                     openMenu();
                 }
                 }
             }
             }
Line 538: Line 668:
             'click',
             'click',
             function (event) {
             function (event) {
                 event.stopPropagation();
                 event.stopPropagation();
             }
             }
         );
         );
Line 550: Line 682:
             'click',
             'click',
             function (event) {
             function (event) {
                 if (
                 if (
                     !dropdown.contains(
                     !dropdown.contains(
Line 555: Line 688:
                     )
                     )
                 ) {
                 ) {
                     closeMenu();
                     closeMenu();
                 }
                 }
             }
             }
Line 568: Line 703:
             'keydown',
             'keydown',
             function (event) {
             function (event) {
                 if (
                 if (
                     event.key === 'Escape' &&
                     event.key === 'Escape' &&
Line 574: Line 710:
                     )
                     )
                 ) {
                 ) {
                     closeMenu();
                     closeMenu();
                     trigger.focus();
                     trigger.focus();
                 }
                 }
Line 583: Line 721:


     /* ========================================================
     /* ========================================================
       INITIALIZE USER AREA
       INITIALIZE ACCOUNT AREA
     ======================================================== */
     ======================================================== */


Line 591: Line 729:
                 '.wp-user-primary'
                 '.wp-user-primary'
             );
             );


         var list =
         var list =
Line 596: Line 735:
                 '.wp-user-menu-list'
                 '.wp-user-menu-list'
             );
             );


         if (!list) {
         if (!list) {
Line 603: Line 743:


         /*
         /*
         * Hide empty username container
         * Empty username container should not
         * for anonymous users.
         * take up space for anonymous users.
         */
         */


Line 611: Line 751:
             !primary.querySelector('li')
             !primary.querySelector('li')
         ) {
         ) {
             primary.classList.add(
             primary.classList.add(
                 'is-empty'
                 'is-empty'
Line 618: Line 759:


         var themeItem =
         var themeItem =
             createThemeToggle(list);
             createThemeToggle(
                list
            );
 


         var username =
         var username =
Line 627: Line 771:


         if (username) {
         if (username) {
             buildLoggedInLayout(
             buildLoggedInLayout(
                 primary,
                 primary,
Line 632: Line 777:
                 themeItem
                 themeItem
             );
             );
         } else {
         } else {
             buildLoggedOutLayout(
             buildLoggedOutLayout(
                 list,
                 list,
Line 667: Line 814:
         'loading'
         'loading'
     ) {
     ) {
         document.addEventListener(
         document.addEventListener(
             'DOMContentLoaded',
             'DOMContentLoaded',
             initialize
             initialize
         );
         );
     } else {
     } else {
         initialize();
         initialize();
     }
     }


Line 686: Line 837:
     'use strict';
     'use strict';


    function initializeArticleContents() {


    function initializeArticleContents() {
         var toc = document.getElementById('wp-toc');
         var toc =
            document.getElementById(
                'wp-toc'
            );


         var article =
         var article = document.querySelector(
            document.querySelector(
            '.wp-article-body'
                '.wp-article-body'
        );
            );


        var sidebarContentsLink = document.getElementById(
            'wp-sidebar-contents-link'
        );


         if (!toc || !article) {
         if (!toc || !article) {
            if (sidebarContentsLink) {
                sidebarContentsLink.hidden = true;
            }
             return;
             return;
         }
         }




         var linksContainer =
         var linksContainer = toc.querySelector(
            toc.querySelector(
            '.wp-toc-links'
                '.wp-toc-links'
        );
            );
 


         if (!linksContainer) {
         if (!linksContainer) {
            if (sidebarContentsLink) {
                sidebarContentsLink.hidden = true;
            }
             return;
             return;
         }
         }
Line 716: Line 870:


         /* ====================================================
         /* ====================================================
           FIND ARTICLE HEADINGS
           FIND ACTUAL ARTICLE HEADINGS
         ==================================================== */
         ==================================================== */


         var headings =
         var headings = Array.prototype.slice.call(
            Array.prototype.slice.call(
            article.querySelectorAll(
                article.querySelectorAll(
                'h2, h3, h4, h5, h6'
                    'h2, h3, h4, h5, h6'
            )
                )
        );
            );
 
 
        /*
        * Keep only real headings with visible text.
        */


        headings = headings.filter(
            function (heading) {


        headings =
                 var text =
            headings.filter(
                    heading.textContent
                 function (heading) {
                        .replace(/\[edit\]/gi, '')
                    var text =
                        .trim();
                        heading.textContent
                            .replace(/\[edit\]/gi, '')
                            .trim();


                    return text.length > 0;
                return text.length > 0;
                }
            }
            );
        );




         /* ====================================================
         /* ====================================================
           NO HEADINGS = NO CONTENTS BUTTON
           NO HEADINGS
          HIDE BOTH CONTENTS CONTROLS
         ==================================================== */
         ==================================================== */


         if (!headings.length) {
         if (!headings.length) {
             toc.classList.remove(
             toc.classList.remove(
                 'wp-toc-ready'
                 'wp-toc-ready'
             );
             );


             toc.open =
             toc.open = false;
                 false;
 
            if (sidebarContentsLink) {
                 sidebarContentsLink.hidden = true;
            }


             return;
             return;
Line 760: Line 922:
         ==================================================== */
         ==================================================== */


         linksContainer.innerHTML =
         linksContainer.innerHTML = '';
            '';




         var oldHeading =
         /*
            toc.querySelector(
        * Hide the old hard-coded "Contents" heading
                '.wp-toc-heading'
        * if it exists in the Mustache template.
            );
        */


        var oldHeading = toc.querySelector(
            '.wp-toc-heading'
        );


         if (oldHeading) {
         if (oldHeading) {
             oldHeading.style.display =
             oldHeading.style.display = 'none';
                'none';
         }
         }




         /* ====================================================
         /* ====================================================
           BUILD CONTENTS FROM REAL HEADINGS
           BUILD LINKS FROM ARTICLE HEADINGS
         ==================================================== */
         ==================================================== */


         headings.forEach(
         headings.forEach(
             function (
             function (heading, index) {
                heading,
 
                 index
                 /*
            ) {
                * MediaWiki normally gives headings an ID.
                * If one does not exist, create a safe fallback.
                */


                 if (!heading.id) {
                 if (!heading.id) {
                     heading.id =
                     heading.id =
                         'wikipartner-section-' +
                         'wikipartner-section-' +
Line 793: Line 959:




                 var level =
                 var level = parseInt(
                    parseInt(
                    heading.tagName.substring(1),
                        heading.tagName.substring(1),
                    10
                        10
                );
                    );




Line 822: Line 987:
                     headingText;
                     headingText;


                /*
                * Close the dropdown after
                * selecting a heading.
                */


                 link.addEventListener(
                 link.addEventListener(
                     'click',
                     'click',
                     function () {
                     function () {
                         toc.open =
 
                            false;
                         toc.open = false;
 
                     }
                     }
                 );
                 );
Line 835: Line 1,006:
                     link
                     link
                 );
                 );
             }
             }
         );
         );
Line 840: Line 1,012:


         /* ====================================================
         /* ====================================================
           SHOW CONTENTS BUTTON
          HEADINGS FOUND
           SHOW BOTH CONTENTS CONTROLS
         ==================================================== */
         ==================================================== */


Line 846: Line 1,019:
             'wp-toc-ready'
             'wp-toc-ready'
         );
         );
        if (sidebarContentsLink) {
            sidebarContentsLink.hidden = false;
            sidebarContentsLink.addEventListener(
                'click',
                function (event) {
                    event.preventDefault();
                    toc.open = true;
                    toc.scrollIntoView({
                        behavior: 'smooth',
                        block: 'center'
                    });
                }
            );
        }
     }
     }




     if (
     if (document.readyState === 'loading') {
        document.readyState ===
 
        'loading'
    ) {
         document.addEventListener(
         document.addEventListener(
             'DOMContentLoaded',
             'DOMContentLoaded',
             initializeArticleContents
             initializeArticleContents
         );
         );
     } else {
     } else {
         initializeArticleContents();
         initializeArticleContents();
     }
     }


Line 866: Line 1,058:
/* ============================================================
/* ============================================================
   WIKIPARTNER
   WIKIPARTNER
   WHAT LINKS HERE — CURRENT PAGE
   WHAT LINKS HERE — CURRENT PAGE URL
============================================================ */
============================================================ */


(function () {
(function () {
     'use strict';
     'use strict';


     function initializeWhatLinksHere() {
     function initializeWhatLinksHere() {
         var link =
         var link = document.getElementById(
            document.getElementById(
            'wp-what-links-here'
                'wp-what-links-here'
        );
            );
 


         if (!link) {
         if (!link) {
Line 884: Line 1,073:
         }
         }


 
         var pageName = mw.config.get(
         var pageName =
            'wgPageName'
            mw.config.get(
        );
                'wgPageName'
            );
 


         if (!pageName) {
         if (!pageName) {
Line 895: Line 1,081:
         }
         }


 
         mw.loader.using(
        /*
            'mediawiki.util'
        * Use MediaWiki's own URL builder
        ).then(
        * instead of html-title.
            function () {
        */
                link.href = mw.util.getUrl(
 
                    'Special:WhatLinksHere/' + pageName
         mw.loader
                );
            .using(
            }
                'mediawiki.util'
        );
            )
            .then(
                function () {
                    link.href =
                        mw.util.getUrl(
                            'Special:WhatLinksHere/' +
                            pageName
                        );
                }
            );
     }
     }


 
     if (document.readyState === 'loading') {
     if (
        document.readyState ===
        'loading'
    ) {
         document.addEventListener(
         document.addEventListener(
             'DOMContentLoaded',
             'DOMContentLoaded',

Revision as of 02:19, 20 August 2026

/* ============================================================
   WIKIPARTNER
   THEME + USER MENU
============================================================ */

(function () {
    'use strict';

    var STORAGE_KEY = 'wikipartner-theme';
    var LIGHT = 'light';
    var DARK = 'dark';


    /* ========================================================
       THEME STORAGE
    ======================================================== */

    function getSavedTheme() {
        try {
            var saved = window.localStorage.getItem(
                STORAGE_KEY
            );

            if (
                saved === LIGHT ||
                saved === DARK
            ) {
                return saved;
            }
        } catch (e) {
            /* localStorage unavailable */
        }

        return LIGHT;
    }


    function saveTheme(theme) {
        try {
            window.localStorage.setItem(
                STORAGE_KEY,
                theme
            );
        } catch (e) {
            /* localStorage unavailable */
        }
    }


    /* ========================================================
       CURRENT THEME
    ======================================================== */

    function getCurrentTheme() {
        return document.documentElement
            .classList.contains(
                'wp-theme-dark'
            )
            ? DARK
            : LIGHT;
    }


    /* ========================================================
       APPLY THEME
    ======================================================== */

    function applyTheme(theme, shouldSave) {
        var html =
            document.documentElement;

        var isDark =
            theme === DARK;


        html.classList.toggle(
            'wp-theme-dark',
            isDark
        );

        html.classList.toggle(
            'wp-theme-light',
            !isDark
        );


        /*
         * Keep MediaWiki-compatible theme classes too.
         */
        html.classList.remove(
            'skin-theme-clientpref-day',
            'skin-theme-clientpref-night',
            'skin-theme-clientpref-os'
        );


        html.classList.add(
            isDark
                ? 'skin-theme-clientpref-night'
                : 'skin-theme-clientpref-day'
        );


        html.setAttribute(
            'data-wikipartner-theme',
            isDark
                ? DARK
                : LIGHT
        );


        if (shouldSave !== false) {
            saveTheme(theme);
        }


        updateThemeButton();
    }


    /* ========================================================
       THEME ICONS
    ======================================================== */

    function getMoonIcon() {
        return [
            '<svg viewBox="0 0 24 24" aria-hidden="true">',

                '<path d="M20.3 15.2A8.4 8.4 0 0 1 8.8 3.7 8.5 8.5 0 1 0 11.5 11.5Z"></path>',

                '<path class="wp-theme-star" d="M17.5 4v3M16 5.5h3"></path>',

            '</svg>'
        ].join('');
    }


    function getSunIcon() {
        return [
            '<svg viewBox="0 0 24 24" aria-hidden="true">',

                '<circle cx="12" cy="12" r="3.8"></circle>',

                '<path d="M12 2.3v2.1"></path>',
                '<path d="M12 19.6v2.1"></path>',

                '<path d="M4.93 4.93l1.5 1.5"></path>',
                '<path d="M17.57 17.57l1.5 1.5"></path>',

                '<path d="M2.3 12h2.1"></path>',
                '<path d="M19.6 12h2.1"></path>',

                '<path d="M4.93 19.07l1.5-1.5"></path>',
                '<path d="M17.57 6.43l1.5-1.5"></path>',

            '</svg>'
        ].join('');
    }


    /* ========================================================
       UPDATE THEME BUTTON
    ======================================================== */

    function updateThemeButton() {
        var button =
            document.getElementById(
                'wikipartner-theme-toggle'
            );


        if (!button) {
            return;
        }


        var isDark =
            getCurrentTheme() === DARK;


        button.innerHTML =
            '<span class="wp-theme-icon">' +
                (
                    isDark
                        ? getSunIcon()
                        : getMoonIcon()
                ) +
            '</span>';


        button.setAttribute(
            'aria-label',
            isDark
                ? 'Switch to light mode'
                : 'Switch to dark mode'
        );


        button.setAttribute(
            'title',
            isDark
                ? 'Light mode'
                : 'Dark mode'
        );
    }


    /* ========================================================
       CREATE THEME BUTTON
    ======================================================== */

    function createThemeToggle(list) {
        var existing =
            document.getElementById(
                'wikipartner-theme-item'
            );


        if (existing) {
            return existing;
        }


        var item =
            document.createElement('li');


        item.id =
            'wikipartner-theme-item';


        item.className =
            'mw-list-item ' +
            'wikipartner-theme-item';


        var button =
            document.createElement(
                'button'
            );


        button.id =
            'wikipartner-theme-toggle';


        button.className =
            'wikipartner-theme-toggle';


        button.type =
            'button';


        button.addEventListener(
            'click',
            function () {

                var nextTheme =
                    getCurrentTheme() === DARK
                        ? LIGHT
                        : DARK;


                applyTheme(
                    nextTheme,
                    true
                );
            }
        );


        item.appendChild(
            button
        );


        updateThemeButton();


        return item;
    }


    /* ========================================================
       HAMBURGER ICON
    ======================================================== */

    function getMenuIcon() {
        return [
            '<span class="wp-menu-bars" aria-hidden="true">',
                '<span></span>',
                '<span></span>',
                '<span></span>',
            '</span>'
        ].join('');
    }


    /* ========================================================
       FIND / CLEAN USERNAME
    ======================================================== */

    function cleanUsernameDuplicates(
        primary,
        list
    ) {
        var username =
            mw.config.get(
                'wgUserName'
            );


        if (!username) {
            return;
        }


        /*
         * MediaWiki's data-user-page already puts
         * the username in .wp-user-primary.
         *
         * Some personal-menu configurations can
         * also provide another user-page link.
         * Remove that duplicate from the menu.
         */

        var items =
            Array.prototype.slice.call(
                list.children
            );


        items.forEach(
            function (item) {

                var link =
                    item.querySelector('a');


                var linkText =
                    link
                        ? link.textContent.trim()
                        : '';


                var isUserPageId =
                    item.id === 'pt-userpage' ||
                    item.id === 'pt-userpage-2';


                var isUsername =
                    linkText === username;


                if (
                    isUserPageId ||
                    isUsername
                ) {

                    /*
                     * If primary somehow has no username,
                     * move this one there instead.
                     */
                    if (
                        primary &&
                        !primary.querySelector('li')
                    ) {

                        primary.appendChild(
                            item
                        );

                    } else {

                        item.remove();

                    }
                }
            }
        );
    }


    /* ========================================================
       LOGGED OUT LAYOUT
    ======================================================== */

    function buildLoggedOutLayout(
        list,
        themeItem
    ) {
        list.classList.remove(
            'wp-is-logged-in'
        );

        list.classList.add(
            'wp-is-logged-out'
        );


        var loginItem =
            list.querySelector(
                '#pt-login'
            );


        /*
         * Desired layout:
         *
         * Create account | Theme | Log in
         */

        if (loginItem) {

            list.insertBefore(
                themeItem,
                loginItem
            );

        } else {

            list.appendChild(
                themeItem
            );

        }
    }


    /* ========================================================
       LOGGED IN LAYOUT
    ======================================================== */

    function buildLoggedInLayout(
        primary,
        list,
        themeItem
    ) {
        list.classList.remove(
            'wp-is-logged-out'
        );

        list.classList.add(
            'wp-is-logged-in'
        );


        cleanUsernameDuplicates(
            primary,
            list
        );


        /*
         * Remove an old dropdown if ResourceLoader
         * initialization somehow runs again.
         */

        var oldDropdown =
            list.querySelector(
                '.wp-user-dropdown'
            );


        if (oldDropdown) {
            return;
        }


        /* ====================================================
           CREATE HAMBURGER WRAPPER
        ==================================================== */

        var dropdown =
            document.createElement('li');


        dropdown.className =
            'wp-user-dropdown';


        /* ====================================================
           HAMBURGER BUTTON
        ==================================================== */

        var trigger =
            document.createElement(
                'button'
            );


        trigger.type =
            'button';


        trigger.className =
            'wp-user-menu-trigger';


        trigger.setAttribute(
            'aria-label',
            'Open user menu'
        );


        trigger.setAttribute(
            'title',
            'User menu'
        );


        trigger.setAttribute(
            'aria-expanded',
            'false'
        );


        trigger.innerHTML =
            getMenuIcon();


        /* ====================================================
           DROPDOWN CONTENT
        ==================================================== */

        var menu =
            document.createElement('ul');


        menu.className =
            'wp-user-dropdown-menu';


        menu.hidden =
            true;


        /*
         * Move every existing menu item
         * into ☰.
         *
         * Username is NOT in this list anymore.
         * Theme is not inserted yet.
         */

        Array.prototype
            .slice.call(
                list.children
            )
            .forEach(
                function (item) {

                    if (
                        item !== themeItem &&
                        item !== dropdown
                    ) {

                        menu.appendChild(
                            item
                        );
                    }
                }
            );


        dropdown.appendChild(
            trigger
        );


        dropdown.appendChild(
            menu
        );


        /*
         * Exact layout:
         *
         * Username | Theme | ☰
         *
         * Username lives in .wp-user-primary,
         * while theme and hamburger live here.
         */

        list.appendChild(
            themeItem
        );


        list.appendChild(
            dropdown
        );


        /* ====================================================
           OPEN / CLOSE
        ==================================================== */

        function closeMenu() {
            dropdown.classList.remove(
                'is-open'
            );


            menu.hidden =
                true;


            trigger.setAttribute(
                'aria-expanded',
                'false'
            );
        }


        function openMenu() {
            dropdown.classList.add(
                'is-open'
            );


            menu.hidden =
                false;


            trigger.setAttribute(
                'aria-expanded',
                'true'
            );
        }


        /* ====================================================
           CLICK HAMBURGER
        ==================================================== */

        trigger.addEventListener(
            'click',
            function (event) {

                event.preventDefault();
                event.stopPropagation();


                if (
                    dropdown.classList.contains(
                        'is-open'
                    )
                ) {

                    closeMenu();

                } else {

                    openMenu();

                }
            }
        );


        /* ====================================================
           CLICK INSIDE MENU
        ==================================================== */

        menu.addEventListener(
            'click',
            function (event) {

                event.stopPropagation();

            }
        );


        /* ====================================================
           CLICK OUTSIDE
        ==================================================== */

        document.addEventListener(
            'click',
            function (event) {

                if (
                    !dropdown.contains(
                        event.target
                    )
                ) {

                    closeMenu();

                }
            }
        );


        /* ====================================================
           ESCAPE
        ==================================================== */

        document.addEventListener(
            'keydown',
            function (event) {

                if (
                    event.key === 'Escape' &&
                    dropdown.classList.contains(
                        'is-open'
                    )
                ) {

                    closeMenu();

                    trigger.focus();
                }
            }
        );
    }


    /* ========================================================
       INITIALIZE ACCOUNT AREA
    ======================================================== */

    function initializeUserArea() {
        var primary =
            document.querySelector(
                '.wp-user-primary'
            );


        var list =
            document.querySelector(
                '.wp-user-menu-list'
            );


        if (!list) {
            return;
        }


        /*
         * Empty username container should not
         * take up space for anonymous users.
         */

        if (
            primary &&
            !primary.querySelector('li')
        ) {

            primary.classList.add(
                'is-empty'
            );
        }


        var themeItem =
            createThemeToggle(
                list
            );


        var username =
            mw.config.get(
                'wgUserName'
            );


        if (username) {

            buildLoggedInLayout(
                primary,
                list,
                themeItem
            );

        } else {

            buildLoggedOutLayout(
                list,
                themeItem
            );
        }


        updateThemeButton();
    }


    /* ========================================================
       APPLY SAVED THEME
    ======================================================== */

    applyTheme(
        getSavedTheme(),
        false
    );


    /* ========================================================
       START
    ======================================================== */

    function initialize() {
        initializeUserArea();
    }


    if (
        document.readyState ===
        'loading'
    ) {

        document.addEventListener(
            'DOMContentLoaded',
            initialize
        );

    } else {

        initialize();

    }

})();


/* ============================================================
   WIKIPARTNER
   ARTICLE CONTENTS MENU
============================================================ */

(function () {
    'use strict';

    function initializeArticleContents() {

        var toc = document.getElementById('wp-toc');

        var article = document.querySelector(
            '.wp-article-body'
        );

        var sidebarContentsLink = document.getElementById(
            'wp-sidebar-contents-link'
        );

        if (!toc || !article) {
            if (sidebarContentsLink) {
                sidebarContentsLink.hidden = true;
            }
            return;
        }


        var linksContainer = toc.querySelector(
            '.wp-toc-links'
        );

        if (!linksContainer) {
            if (sidebarContentsLink) {
                sidebarContentsLink.hidden = true;
            }
            return;
        }


        /* ====================================================
           FIND ACTUAL ARTICLE HEADINGS
        ==================================================== */

        var headings = Array.prototype.slice.call(
            article.querySelectorAll(
                'h2, h3, h4, h5, h6'
            )
        );


        /*
         * Keep only real headings with visible text.
         */

        headings = headings.filter(
            function (heading) {

                var text =
                    heading.textContent
                        .replace(/\[edit\]/gi, '')
                        .trim();

                return text.length > 0;
            }
        );


        /* ====================================================
           NO HEADINGS
           HIDE BOTH CONTENTS CONTROLS
        ==================================================== */

        if (!headings.length) {

            toc.classList.remove(
                'wp-toc-ready'
            );

            toc.open = false;

            if (sidebarContentsLink) {
                sidebarContentsLink.hidden = true;
            }

            return;
        }


        /* ====================================================
           CLEAR OLD CONTENTS
        ==================================================== */

        linksContainer.innerHTML = '';


        /*
         * Hide the old hard-coded "Contents" heading
         * if it exists in the Mustache template.
         */

        var oldHeading = toc.querySelector(
            '.wp-toc-heading'
        );

        if (oldHeading) {
            oldHeading.style.display = 'none';
        }


        /* ====================================================
           BUILD LINKS FROM ARTICLE HEADINGS
        ==================================================== */

        headings.forEach(
            function (heading, index) {

                /*
                 * MediaWiki normally gives headings an ID.
                 * If one does not exist, create a safe fallback.
                 */

                if (!heading.id) {

                    heading.id =
                        'wikipartner-section-' +
                        (index + 1);
                }


                var level = parseInt(
                    heading.tagName.substring(1),
                    10
                );


                var headingText =
                    heading.textContent
                        .replace(/\[edit\]/gi, '')
                        .trim();


                var link =
                    document.createElement('a');


                link.href =
                    '#' + heading.id;


                link.className =
                    'wp-toc-level-' +
                    level;


                link.textContent =
                    headingText;


                /*
                 * Close the dropdown after
                 * selecting a heading.
                 */

                link.addEventListener(
                    'click',
                    function () {

                        toc.open = false;

                    }
                );


                linksContainer.appendChild(
                    link
                );

            }
        );


        /* ====================================================
           HEADINGS FOUND
           SHOW BOTH CONTENTS CONTROLS
        ==================================================== */

        toc.classList.add(
            'wp-toc-ready'
        );

        if (sidebarContentsLink) {
            sidebarContentsLink.hidden = false;

            sidebarContentsLink.addEventListener(
                'click',
                function (event) {
                    event.preventDefault();

                    toc.open = true;

                    toc.scrollIntoView({
                        behavior: 'smooth',
                        block: 'center'
                    });
                }
            );
        }
    }


    if (document.readyState === 'loading') {

        document.addEventListener(
            'DOMContentLoaded',
            initializeArticleContents
        );

    } else {

        initializeArticleContents();

    }

})();


/* ============================================================
   WIKIPARTNER
   WHAT LINKS HERE — CURRENT PAGE URL
============================================================ */

(function () {
    'use strict';

    function initializeWhatLinksHere() {
        var link = document.getElementById(
            'wp-what-links-here'
        );

        if (!link) {
            return;
        }

        var pageName = mw.config.get(
            'wgPageName'
        );

        if (!pageName) {
            return;
        }

        mw.loader.using(
            'mediawiki.util'
        ).then(
            function () {
                link.href = mw.util.getUrl(
                    'Special:WhatLinksHere/' + pageName
                );
            }
        );
    }

    if (document.readyState === 'loading') {
        document.addEventListener(
            'DOMContentLoaded',
            initializeWhatLinksHere
        );
    } else {
        initializeWhatLinksHere();
    }

})();