Contents

MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 1,549: Line 1,549:


     }
     }
} );
/* ============================================================
  WIKIPARTNER — PAGE TOOL MENUS
  Draft tools + Admin tools
============================================================ */
mw.loader.using( [
    'mediawiki.api',
    'mediawiki.util'
] ).then( function () {
    $( function () {
        var namespace =
            Number( mw.config.get( 'wgNamespaceNumber' ) );
        var articleId =
            Number( mw.config.get( 'wgArticleId' ) || 0 );
        var action =
            mw.config.get( 'wgAction' );
        var pageName =
            mw.config.get( 'wgPageName' );
        var pageTitle =
            mw.config.get( 'wgTitle' );
        var groups =
            mw.config.get( 'wgUserGroups' ) || [];
        /* ----------------------------------------------------
          PERMISSION GROUPS
          Later we can add:
          'reviewer'
          'editor'
          without rebuilding Draft tools.
        ---------------------------------------------------- */
        var draftToolGroups = [
            'sysop'
        ];
        var adminToolGroups = [
            'sysop'
        ];
        function userHasGroup( allowedGroups ) {
            return allowedGroups.some(
                function ( group ) {
                    return groups.indexOf( group ) !== -1;
                }
            );
        }
        var canUseDraftTools =
            userHasGroup( draftToolGroups );
        var canUseAdminTools =
            userHasGroup( adminToolGroups );
        /*
        * Existing pages in normal view only.
        */
        if (
            action !== 'view' ||
            !articleId ||
            namespace === -1
        ) {
            return;
        }
        var heading =
            document.querySelector(
                '.wp-article-header .firstHeading, .firstHeading, #firstHeading'
            );
        if ( !heading ) {
            return;
        }
        var header =
            heading.closest( '.wp-article-header' ) ||
            heading.parentElement;
        if ( !header ) {
            return;
        }
        /* ----------------------------------------------------
          SHARED ACTION AREA
        ---------------------------------------------------- */
        var actions =
            document.createElement( 'div' );
        actions.className =
            'wp-title-tools';
        header.appendChild( actions );
        /* ====================================================
          SHARED DROPDOWN FUNCTION
        ==================================================== */
        function createToolMenu(
            className,
            label
        ) {
            var wrapper =
                document.createElement( 'div' );
            wrapper.className =
                'wp-tool-menu ' + className;
            var trigger =
                document.createElement( 'button' );
            trigger.type =
                'button';
            trigger.className =
                'wp-tool-menu-trigger';
            trigger.setAttribute(
                'aria-expanded',
                'false'
            );
            trigger.setAttribute(
                'aria-haspopup',
                'true'
            );
            var triggerText =
                document.createElement( 'span' );
            triggerText.textContent =
                label;
            var arrow =
                document.createElement( 'span' );
            arrow.className =
                'wp-tool-menu-arrow';
            arrow.setAttribute(
                'aria-hidden',
                'true'
            );
            arrow.textContent =
                '▾';
            trigger.append(
                triggerText,
                arrow
            );
            var menu =
                document.createElement( 'div' );
            menu.className =
                'wp-tool-menu-dropdown';
            menu.hidden = true;
            wrapper.append(
                trigger,
                menu
            );
            trigger.addEventListener(
                'click',
                function ( event ) {
                    event.stopPropagation();
                    /*
                    * Close every other menu first.
                    */
                    document
                        .querySelectorAll(
                            '.wp-tool-menu-dropdown'
                        )
                        .forEach(
                            function ( otherMenu ) {
                                if ( otherMenu !== menu ) {
                                    otherMenu.hidden = true;
                                    var otherTrigger =
                                        otherMenu
                                            .parentElement
                                            .querySelector(
                                                '.wp-tool-menu-trigger'
                                            );
                                    if ( otherTrigger ) {
                                        otherTrigger.setAttribute(
                                            'aria-expanded',
                                            'false'
                                        );
                                    }
                                }
                            }
                        );
                    var opening =
                        menu.hidden;
                    menu.hidden =
                        !opening;
                    trigger.setAttribute(
                        'aria-expanded',
                        opening ? 'true' : 'false'
                    );
                }
            );
            return {
                wrapper: wrapper,
                trigger: trigger,
                menu: menu
            };
        }
        /* ====================================================
          DRAFT TOOLS
        ==================================================== */
        if (
            namespace === 3000 &&
            canUseDraftTools
        ) {
            document.body.classList.add(
                'wp-has-draft-tools'
            );
            var draftTools =
                createToolMenu(
                    'wp-draft-tools',
                    'Draft tools'
                );
            /* ------------------------------------------------
              Publish
            ------------------------------------------------ */
            var publish =
                document.createElement( 'button' );
            publish.type =
                'button';
            publish.className =
                'wp-tool-menu-item wp-draft-publish-item';
            publish.textContent =
                'Publish';
            draftTools.menu.appendChild(
                publish
            );
            actions.appendChild(
                draftTools.wrapper
            );
            /* ================================================
              PUBLISH MODAL
            ================================================ */
            var overlay =
                document.createElement( 'div' );
            overlay.className =
                'wp-publish-modal-overlay';
            overlay.hidden = true;
            var modal =
                document.createElement( 'div' );
            modal.className =
                'wp-publish-modal';
            modal.setAttribute(
                'role',
                'dialog'
            );
            modal.setAttribute(
                'aria-modal',
                'true'
            );
            modal.setAttribute(
                'aria-labelledby',
                'wp-publish-modal-title'
            );
            var modalTitle =
                document.createElement( 'div' );
            modalTitle.id =
                'wp-publish-modal-title';
            modalTitle.className =
                'wp-publish-modal-title';
            modalTitle.textContent =
                'Publish this draft?';
            var modalText =
                document.createElement( 'div' );
            modalText.className =
                'wp-publish-modal-text';
            var targetName =
                document.createElement( 'strong' );
            targetName.textContent =
                pageTitle ||
                'this article';
            modalText.append(
                'This will publish ',
                targetName,
                ' in the main WikiPartner article space. The “Draft:” prefix will be removed automatically.'
            );
            var modalNote =
                document.createElement( 'div' );
            modalNote.className =
                'wp-publish-modal-note';
            modalNote.textContent =
                'The draft will be moved with its revision history.';
            var modalActions =
                document.createElement( 'div' );
            modalActions.className =
                'wp-publish-modal-actions';
            var cancel =
                document.createElement( 'button' );
            cancel.type =
                'button';
            cancel.className =
                'wp-publish-modal-cancel';
            cancel.textContent =
                'Cancel';
            var confirm =
                document.createElement( 'button' );
            confirm.type =
                'button';
            confirm.className =
                'wp-publish-modal-confirm';
            confirm.textContent =
                'Publish article';
            modalActions.append(
                cancel,
                confirm
            );
            modal.append(
                modalTitle,
                modalText,
                modalNote,
                modalActions
            );
            overlay.appendChild(
                modal
            );
            document.body.appendChild(
                overlay
            );
            function openPublishModal() {
                draftTools.menu.hidden = true;
                draftTools.trigger.setAttribute(
                    'aria-expanded',
                    'false'
                );
                modalNote.classList.remove(
                    'wp-publish-modal-error'
                );
                modalNote.textContent =
                    'The draft will be moved with its revision history.';
                overlay.hidden = false;
                document.body.classList.add(
                    'wp-publish-modal-open'
                );
                confirm.focus();
            }
            function closePublishModal() {
                if ( confirm.disabled ) {
                    return;
                }
                overlay.hidden = true;
                document.body.classList.remove(
                    'wp-publish-modal-open'
                );
                draftTools.trigger.focus();
            }
            publish.addEventListener(
                'click',
                openPublishModal
            );
            cancel.addEventListener(
                'click',
                closePublishModal
            );
            overlay.addEventListener(
                'click',
                function ( event ) {
                    if ( event.target === overlay ) {
                        closePublishModal();
                    }
                }
            );
            /* ------------------------------------------------
              ACTUAL PUBLISH
            ------------------------------------------------ */
            confirm.addEventListener(
                'click',
                function () {
                    if (
                        !pageName ||
                        !pageTitle
                    ) {
                        return;
                    }
                    confirm.disabled = true;
                    cancel.disabled = true;
                    confirm.textContent =
                        'Publishing…';
                    var api =
                        new mw.Api();
                    api.postWithToken(
                        'csrf',
                        {
                            action: 'move',
                            from:
                                pageName,
                            to:
                                pageTitle,
                            reason:
                                'Published approved WikiPartner draft',
                            movetalk:
                                1,
                            noredirect:
                                1,
                            format:
                                'json'
                        }
                    )
                    .done(
                        function () {
                            window.location.href =
                                mw.util.getUrl(
                                    pageTitle
                                );
                        }
                    )
                    .fail(
                        function (
                            code,
                            data
                        ) {
                            confirm.disabled =
                                false;
                            cancel.disabled =
                                false;
                            confirm.textContent =
                                'Publish article';
                            var message =
                                'The draft could not be published.';
                            if (
                                data &&
                                data.error &&
                                data.error.info
                            ) {
                                message +=
                                    ' ' +
                                    data.error.info;
                            }
                            modalNote.textContent =
                                message;
                            modalNote.classList.add(
                                'wp-publish-modal-error'
                            );
                        }
                    );
                }
            );
        }
        /* ====================================================
          ADMIN TOOLS
        ==================================================== */
        if ( canUseAdminTools ) {
            document.body.classList.add(
                'wp-has-admin-tools'
            );
            var adminTools =
                createToolMenu(
                    'wp-admin-tools',
                    'Admin tools'
                );
            /* Move */
            var move =
                document.createElement( 'a' );
            move.className =
                'wp-tool-menu-item';
            move.href =
                mw.util.getUrl(
                    'Special:MovePage/' +
                    pageName
                );
            move.textContent =
                'Move';
            /* Delete */
            var remove =
                document.createElement( 'a' );
            remove.className =
                'wp-tool-menu-item wp-tool-menu-delete';
            remove.href =
                mw.util.getUrl(
                    pageName,
                    {
                        action: 'delete'
                    }
                );
            remove.textContent =
                'Delete';
            adminTools.menu.append(
                move,
                remove
            );
            actions.appendChild(
                adminTools.wrapper
            );
        }
        /* ====================================================
          GLOBAL MENU CLOSING
        ==================================================== */
        document.addEventListener(
            'click',
            function ( event ) {
                document
                    .querySelectorAll(
                        '.wp-tool-menu'
                    )
                    .forEach(
                        function ( wrapper ) {
                            if (
                                wrapper.contains(
                                    event.target
                                )
                            ) {
                                return;
                            }
                            var menu =
                                wrapper.querySelector(
                                    '.wp-tool-menu-dropdown'
                                );
                            var trigger =
                                wrapper.querySelector(
                                    '.wp-tool-menu-trigger'
                                );
                            if ( menu ) {
                                menu.hidden = true;
                            }
                            if ( trigger ) {
                                trigger.setAttribute(
                                    'aria-expanded',
                                    'false'
                                );
                            }
                        }
                    );
            }
        );
        document.addEventListener(
            'keydown',
            function ( event ) {
                if ( event.key !== 'Escape' ) {
                    return;
                }
                if ( !overlay || overlay.hidden ) {
                    document
                        .querySelectorAll(
                            '.wp-tool-menu-dropdown'
                        )
                        .forEach(
                            function ( menu ) {
                                menu.hidden = true;
                            }
                        );
                    document
                        .querySelectorAll(
                            '.wp-tool-menu-trigger'
                        )
                        .forEach(
                            function ( trigger ) {
                                trigger.setAttribute(
                                    'aria-expanded',
                                    'false'
                                );
                            }
                        );
                } else {
                    closePublishModal();
                }
            }
        );
    } );


} );
} );

Revision as of 15:56, 1 September 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'
    );


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

    var firstItem =
        list.firstElementChild;


    if (firstItem) {

        list.insertBefore(
            themeItem,
            firstItem
        );

    } 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();
    }

})();

/* ============================================================
   WIKIPARTNER
   ARTICLE SUGGESTION — REVIEWER CONTROL
============================================================ */

(function () {
    'use strict';

    function initializeSuggestionReviewerControl() {
        var control = document.getElementById(
            'wp-review-suggestion-control'
        );

        if (!control) {
            return;
        }

        var groups = mw.config.get(
            'wgUserGroups'
        ) || [];

        var isReviewer =
            groups.indexOf('reviewer') !== -1;

        if (isReviewer) {
            control.style.display = '';
        }
    }


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

})();

/* ============================================================
   WIKIPARTNER
   ISSUE REPORT — REVIEWER CONTROL
============================================================ */

(function () {
    'use strict';

    function initializeIssueReviewerControl() {
        var control = document.getElementById(
            'wp-review-issue-control'
        );

        if (!control) {
            return;
        }

        var groups = mw.config.get(
            'wgUserGroups'
        ) || [];

        var isReviewer =
            groups.indexOf('reviewer') !== -1;

        if (isReviewer) {
            control.style.display = '';
        }
    }


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

})();

/* ============================================================
   WIKIPARTNER — CREATE DRAFT
   Send the Create draft form directly to Draft: source editor.
============================================================ */

mw.loader.using( 'mediawiki.util' ).then( function () {

    if ( mw.config.get( 'wgPageName' ) !== 'WikiPartner:Create_a_draft' ) {
        return;
    }

    var form = document.getElementById( 'wp-create-draft-form' );

    if ( !form ) {
        return;
    }

    form.addEventListener( 'submit', function ( event ) {

        var input = form.querySelector( 'input[name="title"]' );

        if ( !input ) {
            return;
        }

        var title = input.value.trim();

        if ( !title ) {
            event.preventDefault();
            input.focus();
            return;
        }

        /*
         * Prevent Draft:Draft:Article if somebody manually
         * enters the namespace.
         */
        title = title.replace( /^Draft\s*:\s*/i, '' ).trim();

        if ( !title ) {
            event.preventDefault();
            input.focus();
            return;
        }

        event.preventDefault();

        var draftTitle =
            'Draft:' +
            title.replace( /\s+/g, '_' );

        var url =
            mw.config.get( 'wgScript' ) +
            '?title=' +
            encodeURIComponent( draftTitle ) +
            '&action=edit';

        window.location.assign( url );
    } );

} );

/* ============================================================
   WIKIPARTNER — MOVED / DELETED PAGE STATES
============================================================ */

$( function () {

    if ( mw.config.get( 'wgAction' ) !== 'view' ) {
        return;
    }

    var content =
        document.getElementById( 'mw-content-text' );

    if ( !content ) {
        return;
    }

    var logBox =
        content.querySelector(
            '.mw-warning-with-logexcerpt'
        );

    var noArticle =
        content.querySelector(
            '.noarticletext'
        );

    if ( !logBox || !noArticle ) {
        return;
    }

    var moveLine =
        logBox.querySelector(
            '.mw-logline-move'
        );

    var deleteLine =
        logBox.querySelector(
            '[class*="mw-logline-delete"]'
        );

    var type = 'history';

    if ( moveLine ) {
        type = 'moved';
    } else if ( deleteLine ) {
        type = 'deleted';
    }

    document.body.classList.add(
        'wp-historical-missing-page',
        'wp-page-' + type
    );

    var messageContent =
        logBox.querySelector(
            '.cdx-message__content'
        ) || logBox;

    var inner =
        messageContent.querySelector(
            '.mw-content-ltr'
        ) || messageContent;

    if (
        inner.querySelector(
            '.wp-history-status'
        )
    ) {
        return;
    }

    var originalIntro =
        inner.querySelector( 'p' );

    if ( originalIntro ) {
        originalIntro.classList.add(
            'wp-history-original-intro'
        );
    }

    var status =
        document.createElement( 'div' );

    status.className =
        'wp-history-status';

    var label =
        document.createElement( 'div' );

    label.className =
        'wp-history-label';

    label.textContent =
        'PAGE STATUS';

    var title =
        document.createElement( 'div' );

    title.className =
        'wp-history-title';

    var description =
        document.createElement( 'div' );

    description.className =
        'wp-history-description';

    if ( type === 'moved' ) {

        title.textContent =
            'This page has moved';

        description.textContent =
            'This page now exists under a different title. The move record is shown below.';

    } else if ( type === 'deleted' ) {

        title.textContent =
            'This page was deleted';

        description.textContent =
            'This title does not currently contain a WikiPartner page. The deletion record is shown below for reference.';

    } else {

        title.textContent =
            'This page is no longer available';

        description.textContent =
            'This title has a recent deletion or move record. Details are shown below.';

    }

    status.append(
        label,
        title,
        description
    );

    if ( moveLine ) {

        var links =
            Array.prototype.slice.call(
                moveLine.querySelectorAll( 'a' )
            );

        var sourceIndex =
            links.findIndex(
                function ( link ) {
                    return link.classList.contains( 'new' );
                }
            );

        var targetLink = null;

        if (
            sourceIndex !== -1 &&
            links[ sourceIndex + 1 ]
        ) {
            targetLink =
                links[ sourceIndex + 1 ];
        }

        if (
            targetLink &&
            targetLink.href
        ) {

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

            currentButton.className =
                'wp-history-current-button';

            currentButton.href =
                targetLink.href;

            currentButton.textContent =
                'View current page';

            status.appendChild(
                currentButton
            );

        }

    }

    if ( type === 'deleted' ) {

        var logLink =
            logBox.querySelector(
                'a[title="Special:Log"]'
            );

        if ( logLink ) {

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

            historyButton.className =
                'wp-history-log-button';

            historyButton.href =
                logLink.href;

            historyButton.textContent =
                'View log entry';

            status.appendChild(
                historyButton
            );

        }

    }

    inner.insertBefore(
        status,
        inner.firstChild
    );

} );
/* ============================================================
   WIKIPARTNER — DRAFT RECREATE WARNING
============================================================ */

$( function () {

    if (
        Number( mw.config.get( 'wgNamespaceNumber' ) ) !== 3000 ||
        mw.config.get( 'wgAction' ) !== 'edit' ||
        mw.config.get( 'wgArticleId' )
    ) {
        return;
    }

    var warning =
        document.querySelector(
            '.mw-warning-with-logexcerpt, .cdx-message--warning'
        );

    if ( !warning ) {
        return;
    }

    var moveLine =
        warning.querySelector( '.mw-logline-move' );

    var deleteLine =
        warning.querySelector(
            '[class*="mw-logline-delete"]'
        );

    if ( !moveLine && !deleteLine ) {
        return;
    }

    document.body.classList.add(
        'wp-draft-recreate-page'
    );

    var content =
        warning.querySelector(
            '.cdx-message__content'
        ) || warning;

    var inner =
        content.querySelector(
            '.mw-content-ltr'
        ) || content;

    var paragraphs =
        Array.prototype.slice.call(
            inner.querySelectorAll( ':scope > p' )
        );

    if ( paragraphs[ 0 ] ) {

        paragraphs[ 0 ].innerHTML =
            '<span class="wp-recreate-label">Notice</span>' +
            '<strong class="wp-recreate-title">' +
            (
                moveLine
                    ? 'This draft was previously moved'
                    : 'This draft was previously deleted'
            ) +
            '</strong>';

    }

    if ( paragraphs[ 1 ] ) {

        paragraphs[ 1 ].textContent =
            moveLine
                ? 'Check the move record below before recreating this draft.'
                : 'Check the deletion record below before recreating this draft.';

        paragraphs[ 1 ].classList.add(
            'wp-recreate-description'
        );

    }

} );

/* ============================================================
   WIKIPARTNER — PAGE TOOL MENUS
   Draft tools + Admin tools
============================================================ */

mw.loader.using( [
    'mediawiki.api',
    'mediawiki.util'
] ).then( function () {

    $( function () {

        var namespace =
            Number( mw.config.get( 'wgNamespaceNumber' ) );

        var articleId =
            Number( mw.config.get( 'wgArticleId' ) || 0 );

        var action =
            mw.config.get( 'wgAction' );

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

        var pageTitle =
            mw.config.get( 'wgTitle' );

        var groups =
            mw.config.get( 'wgUserGroups' ) || [];


        /* ----------------------------------------------------
           PERMISSION GROUPS

           Later we can add:
           'reviewer'
           'editor'

           without rebuilding Draft tools.
        ---------------------------------------------------- */

        var draftToolGroups = [
            'sysop'
        ];

        var adminToolGroups = [
            'sysop'
        ];


        function userHasGroup( allowedGroups ) {

            return allowedGroups.some(
                function ( group ) {
                    return groups.indexOf( group ) !== -1;
                }
            );

        }


        var canUseDraftTools =
            userHasGroup( draftToolGroups );

        var canUseAdminTools =
            userHasGroup( adminToolGroups );


        /*
         * Existing pages in normal view only.
         */

        if (
            action !== 'view' ||
            !articleId ||
            namespace === -1
        ) {
            return;
        }


        var heading =
            document.querySelector(
                '.wp-article-header .firstHeading, .firstHeading, #firstHeading'
            );

        if ( !heading ) {
            return;
        }


        var header =
            heading.closest( '.wp-article-header' ) ||
            heading.parentElement;

        if ( !header ) {
            return;
        }


        /* ----------------------------------------------------
           SHARED ACTION AREA
        ---------------------------------------------------- */

        var actions =
            document.createElement( 'div' );

        actions.className =
            'wp-title-tools';

        header.appendChild( actions );


        /* ====================================================
           SHARED DROPDOWN FUNCTION
        ==================================================== */

        function createToolMenu(
            className,
            label
        ) {

            var wrapper =
                document.createElement( 'div' );

            wrapper.className =
                'wp-tool-menu ' + className;


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

            trigger.type =
                'button';

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

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

            trigger.setAttribute(
                'aria-haspopup',
                'true'
            );


            var triggerText =
                document.createElement( 'span' );

            triggerText.textContent =
                label;


            var arrow =
                document.createElement( 'span' );

            arrow.className =
                'wp-tool-menu-arrow';

            arrow.setAttribute(
                'aria-hidden',
                'true'
            );

            arrow.textContent =
                '▾';


            trigger.append(
                triggerText,
                arrow
            );


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

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

            menu.hidden = true;


            wrapper.append(
                trigger,
                menu
            );


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

                    event.stopPropagation();

                    /*
                     * Close every other menu first.
                     */
                    document
                        .querySelectorAll(
                            '.wp-tool-menu-dropdown'
                        )
                        .forEach(
                            function ( otherMenu ) {

                                if ( otherMenu !== menu ) {
                                    otherMenu.hidden = true;

                                    var otherTrigger =
                                        otherMenu
                                            .parentElement
                                            .querySelector(
                                                '.wp-tool-menu-trigger'
                                            );

                                    if ( otherTrigger ) {
                                        otherTrigger.setAttribute(
                                            'aria-expanded',
                                            'false'
                                        );
                                    }
                                }

                            }
                        );


                    var opening =
                        menu.hidden;

                    menu.hidden =
                        !opening;

                    trigger.setAttribute(
                        'aria-expanded',
                        opening ? 'true' : 'false'
                    );

                }
            );


            return {
                wrapper: wrapper,
                trigger: trigger,
                menu: menu
            };

        }


        /* ====================================================
           DRAFT TOOLS
        ==================================================== */

        if (
            namespace === 3000 &&
            canUseDraftTools
        ) {

            document.body.classList.add(
                'wp-has-draft-tools'
            );


            var draftTools =
                createToolMenu(
                    'wp-draft-tools',
                    'Draft tools'
                );


            /* ------------------------------------------------
               Publish
            ------------------------------------------------ */

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

            publish.type =
                'button';

            publish.className =
                'wp-tool-menu-item wp-draft-publish-item';

            publish.textContent =
                'Publish';


            draftTools.menu.appendChild(
                publish
            );

            actions.appendChild(
                draftTools.wrapper
            );


            /* ================================================
               PUBLISH MODAL
            ================================================ */

            var overlay =
                document.createElement( 'div' );

            overlay.className =
                'wp-publish-modal-overlay';

            overlay.hidden = true;


            var modal =
                document.createElement( 'div' );

            modal.className =
                'wp-publish-modal';

            modal.setAttribute(
                'role',
                'dialog'
            );

            modal.setAttribute(
                'aria-modal',
                'true'
            );

            modal.setAttribute(
                'aria-labelledby',
                'wp-publish-modal-title'
            );


            var modalTitle =
                document.createElement( 'div' );

            modalTitle.id =
                'wp-publish-modal-title';

            modalTitle.className =
                'wp-publish-modal-title';

            modalTitle.textContent =
                'Publish this draft?';


            var modalText =
                document.createElement( 'div' );

            modalText.className =
                'wp-publish-modal-text';


            var targetName =
                document.createElement( 'strong' );

            targetName.textContent =
                pageTitle ||
                'this article';


            modalText.append(
                'This will publish ',
                targetName,
                ' in the main WikiPartner article space. The “Draft:” prefix will be removed automatically.'
            );


            var modalNote =
                document.createElement( 'div' );

            modalNote.className =
                'wp-publish-modal-note';

            modalNote.textContent =
                'The draft will be moved with its revision history.';


            var modalActions =
                document.createElement( 'div' );

            modalActions.className =
                'wp-publish-modal-actions';


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

            cancel.type =
                'button';

            cancel.className =
                'wp-publish-modal-cancel';

            cancel.textContent =
                'Cancel';


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

            confirm.type =
                'button';

            confirm.className =
                'wp-publish-modal-confirm';

            confirm.textContent =
                'Publish article';


            modalActions.append(
                cancel,
                confirm
            );


            modal.append(
                modalTitle,
                modalText,
                modalNote,
                modalActions
            );


            overlay.appendChild(
                modal
            );

            document.body.appendChild(
                overlay
            );


            function openPublishModal() {

                draftTools.menu.hidden = true;

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

                modalNote.classList.remove(
                    'wp-publish-modal-error'
                );

                modalNote.textContent =
                    'The draft will be moved with its revision history.';

                overlay.hidden = false;

                document.body.classList.add(
                    'wp-publish-modal-open'
                );

                confirm.focus();

            }


            function closePublishModal() {

                if ( confirm.disabled ) {
                    return;
                }

                overlay.hidden = true;

                document.body.classList.remove(
                    'wp-publish-modal-open'
                );

                draftTools.trigger.focus();

            }


            publish.addEventListener(
                'click',
                openPublishModal
            );


            cancel.addEventListener(
                'click',
                closePublishModal
            );


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

                    if ( event.target === overlay ) {
                        closePublishModal();
                    }

                }
            );


            /* ------------------------------------------------
               ACTUAL PUBLISH
            ------------------------------------------------ */

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

                    if (
                        !pageName ||
                        !pageTitle
                    ) {
                        return;
                    }


                    confirm.disabled = true;
                    cancel.disabled = true;

                    confirm.textContent =
                        'Publishing…';


                    var api =
                        new mw.Api();


                    api.postWithToken(
                        'csrf',
                        {
                            action: 'move',

                            from:
                                pageName,

                            to:
                                pageTitle,

                            reason:
                                'Published approved WikiPartner draft',

                            movetalk:
                                1,

                            noredirect:
                                1,

                            format:
                                'json'
                        }
                    )
                    .done(
                        function () {

                            window.location.href =
                                mw.util.getUrl(
                                    pageTitle
                                );

                        }
                    )
                    .fail(
                        function (
                            code,
                            data
                        ) {

                            confirm.disabled =
                                false;

                            cancel.disabled =
                                false;

                            confirm.textContent =
                                'Publish article';


                            var message =
                                'The draft could not be published.';


                            if (
                                data &&
                                data.error &&
                                data.error.info
                            ) {

                                message +=
                                    ' ' +
                                    data.error.info;

                            }


                            modalNote.textContent =
                                message;

                            modalNote.classList.add(
                                'wp-publish-modal-error'
                            );

                        }
                    );

                }
            );

        }


        /* ====================================================
           ADMIN TOOLS
        ==================================================== */

        if ( canUseAdminTools ) {

            document.body.classList.add(
                'wp-has-admin-tools'
            );


            var adminTools =
                createToolMenu(
                    'wp-admin-tools',
                    'Admin tools'
                );


            /* Move */

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

            move.className =
                'wp-tool-menu-item';

            move.href =
                mw.util.getUrl(
                    'Special:MovePage/' +
                    pageName
                );

            move.textContent =
                'Move';


            /* Delete */

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

            remove.className =
                'wp-tool-menu-item wp-tool-menu-delete';

            remove.href =
                mw.util.getUrl(
                    pageName,
                    {
                        action: 'delete'
                    }
                );

            remove.textContent =
                'Delete';


            adminTools.menu.append(
                move,
                remove
            );


            actions.appendChild(
                adminTools.wrapper
            );

        }


        /* ====================================================
           GLOBAL MENU CLOSING
        ==================================================== */

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

                document
                    .querySelectorAll(
                        '.wp-tool-menu'
                    )
                    .forEach(
                        function ( wrapper ) {

                            if (
                                wrapper.contains(
                                    event.target
                                )
                            ) {
                                return;
                            }


                            var menu =
                                wrapper.querySelector(
                                    '.wp-tool-menu-dropdown'
                                );

                            var trigger =
                                wrapper.querySelector(
                                    '.wp-tool-menu-trigger'
                                );


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


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

                        }
                    );

            }
        );


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

                if ( event.key !== 'Escape' ) {
                    return;
                }


                if ( !overlay || overlay.hidden ) {

                    document
                        .querySelectorAll(
                            '.wp-tool-menu-dropdown'
                        )
                        .forEach(
                            function ( menu ) {
                                menu.hidden = true;
                            }
                        );


                    document
                        .querySelectorAll(
                            '.wp-tool-menu-trigger'
                        )
                        .forEach(
                            function ( trigger ) {
                                trigger.setAttribute(
                                    'aria-expanded',
                                    'false'
                                );
                            }
                        );

                } else {

                    closePublishModal();

                }

            }
        );

    } );

} );