MediaWiki:Common.js

From Geometry Dash Expanded Wiki
Revision as of 14:25, 11 October 2026 by Bot994 (talk | contribs) (Add edit recovery header shortcut and refresh the main page article count from actual pages)
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Use the physical device width instead of Vector legacy's 1120px canvas. */
(function () {
    var viewport = document.querySelector('meta[name="viewport"]');
    if (!viewport) {
        viewport = document.createElement('meta');
        viewport.name = 'viewport';
        document.head.appendChild(viewport);
    }
    viewport.content = 'width=device-width, initial-scale=1';
}());

/* Any JavaScript here will be loaded for all users on every page load. */

/***************************/
/* Interface Modification **/
/***************************/

/* [[MediaWiki:Custom-Category:Games]] */
$(function () {
    var config = {
        targetCategory: 'Games (base content)',
        sourceContent: 'MediaWiki:Custom-Category:Games',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the content list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-Category:Gameplay]] */
$(function () {
    var config = {
        targetCategory: 'Gameplay (base content)',
        sourceContent: 'MediaWiki:Custom-Category:Gameplay',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the content list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-Category:Rewards]] */
$(function () {
    var config = {
        targetCategory: 'Rewards (base content)',
        sourceContent: 'MediaWiki:Custom-Category:Rewards',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the content list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-Category:Rooms]] */
$(function () {
    var config = {
        targetCategory: 'Rooms (base content)',
        sourceContent: 'MediaWiki:Custom-Category:Rooms',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the content list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-Category:LevelEditing]] */
$(function () {
    var config = {
        targetCategory: 'Level editing (base content)',
        sourceContent: 'MediaWiki:Custom-Category:LevelEditing',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the content list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-Category:AccountFeatures]] */
$(function () {
    var config = {
        targetCategory: 'Account features (base content)',
        sourceContent: 'MediaWiki:Custom-Category:AccountFeatures',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the content list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-Category:Updates]] */
$(function () {
    var config = {
        targetCategory: 'Updates (base content)',
        sourceContent: 'MediaWiki:Custom-Category:Updates',
        insertionPoint: '#mw-normal-catlinks'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-page-list" style="font-weight:normal;">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the page list from ' + config.sourceContent);
    });
});

/***************************/

/* [[MediaWiki:Custom-MainLevels]] */
$(function () {
    var config = {
        targetCategory: 'Main levels (base content)',
        sourceContent: 'MediaWiki:Custom-MainLevels',
        insertionPoint: '#mw-content-text'
    };

    // Initial checks: Run only on page views (not edit mode) and for existing pages
    if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgArticleId') === 0) return;

    // Category check: Verify if the article belongs to the target category
    var categories = mw.config.get('wgCategories');
    if (categories.indexOf(config.targetCategory) === -1) return;

    // Load and inject content
    var api = new mw.Api();
    api.get({
        action: 'parse',
        page: config.sourceContent,
        prop: 'text',
        disablelimitreport: true,
        disableeditsection: true
    }).done(function (data) {
        if (data && data.parse && data.parse.text) {
            var insertHtml = data.parse.text['*'];
            // Wrap in a div for styling and append
            $(config.insertionPoint).append(
                $('<div id="custom-category-footer">')
                .html(insertHtml)
            );
        }
    }).fail(function () {
        console.error('Failed to load the footer from ' + config.sourceContent);
    });
});

/***************************/

/* [[Template:USERNAME]] */
$(function () { 
	mw.config.get('wgUserName');
	if (mw.config.get('wgUserName') !== null) ('span.username-insert').text(mw.config.get('wgUserName'));
});

/***************************/

/* [[Template:Discord]] (DiscordCompact.js v0.0.2 - https://support.wiki.gg/wiki/DiscordCompact) */
$(function () {
	var widget = $("#content #discord-compact-widget");
    // Cancel when no widget detected
    if (!widget.length) return;
    // Ensure that ID is not blank
	var id = widget.attr("data-id");
	if (id === "") throw new Error("DiscordCompact has a blank server id!");
	// Ensure that ID consists only of numbers with 17 character minimum
	if (!new RegExp("^[0-9]{17}[0-9]+$").test(id)) throw new Error("DiscordCompact has an invalid server id!");
	var apiBase = "https://discord.com/api/guilds/" + id;
	// Get information about the server, such as online member count and invite url, as well as limits of the server existing at all or having widgets disabled
	$.ajax(apiBase + "/widget.json").fail(function(req) {
        if (!req.responseJSON) throw new Error("DiscordCompact encountered an unknown error whilst fetching widget.json (status: " + req.status);
		switch (req.responseJSON.code) {
            case 10004:
                throw new Error("DiscordCompact has a valid server id, but no such server exists!");
            case 50004:
                throw new Error("DiscordCompact has a valid server id, but that server has widgets disabled!");
            default:
                throw new Error("DiscordCompact encountered an unknown error whilst fetching widget.json (status: " + req.status + "; code: " + req.responseJSON.code + ")");
        }
	}).done(function(res) {
		const inviteURL = res.instant_invite;
        // Warn when unable to retrieve invite URL
        if (inviteURL == null) console.warn("DiscordCompact cannot get an invite URL; does this server have an invite channel set in Widget settings?");
        // Generate widget image
        $.ajax({url: apiBase + "/widget.png?style=banner2", xhrFields: {responseType: "blob"}}).fail(function(req) {
            if (!req.responseJSON) throw new Error("DiscordCompact encountered an unknown error whilst fetching widget.png! (status: " + req.status);
            throw new Error("DiscordCompact encountered an unknown error whilst fetching widget.png! (status: " + req.status + "; code: " + req.responseJSON.code + ")");
        }).done(function(blob) {
            // Convert the image data into base64, preventing having to make the client request it a second time
            const imageReader = new FileReader();
            imageReader.readAsDataURL(blob);
            imageReader.onloadend = function() {
                const b64Data = imageReader.result;
                // Convert the widget into a selectable link
                widget.replaceWith(function() {
                    return $("<a>", {
                        id: widget.attr("id"),
                        class: widget.attr("class"),
                        style: widget.attr("style"),
                        alt: "Discord server widget",
                        href: inviteURL
                    });
                });
                // Select the widget
                widget = $("#discord-compact-widget");
                // CSS
                widget.css("display", "block");
                widget.css("max-height", "76px");
                widget.css("max-width", "320px");
                // Create image
                const widgetImage = document.createElement("img");
                widgetImage.src = b64Data;
                widgetImage.style.width = "100%";
                widgetImage.style.height = "100%";
                widgetImage.style.borderRadius = "4px";
                widget.append(widgetImage);
                // Prevent image dragging
                widget.on("dragstart", function(e) {e.preventDefault();});
                // Console message
                console.log("DiscordCompact loaded successfully!");
            };
        });
	});
});

/***************************/

/* [[Template:Discord]] (DiscordWidget.js - https://support.wiki.gg/wiki/DiscordWidget) */
$(function () {
	'use strict';
	var mconfig = mw.config.get([
		'wgContentLanguage',
		'wgUserLanguage',
		'wgUserName'
	]);
	if (window.DiscordIntegratorLoaded) {
		return;
	}
	window.DiscordIntegratorLoaded = true;
	// Main object, @static
	var DiscordIntegrator = {
		// Initialising
		init: function() {
			mw.hook('wikipage.content').add($.proxy(this.insertToContent, this));
		},
		//Finding the designated places in content in which to place the widget and placing it
		insertToContent: function($content) {
			$content.find('.DiscordIntegrator:not(.loaded)').each($.proxy(function(cabbage, el) {
				el = $(el);
				el.html(this.generateContent(el.data())).addClass('loaded');
			}, this));
		},
		// Determines the theme of the widget, @param {string} config Configured theme, @return {string} 'light' or 'dark' depending on the wiki theme and configuration
		determineTheme: function(config) {
			// If explicitly configured to light or dark
			if (config === 'dark') {
				return 'dark';
			}
			if (config === 'light') {
				return 'light';
			}
			// If not configured, try to determine based on wiki theme (set by themeToggle):
			return $(':root').hasClass('view-dark') ? 'dark' : 'light';
		},
		// Generate widget content from an object, @return {string} content of the widget
		generateContent: function(config) {
			if (!config.id || !String(config.id).match(/\d{17,19}/)) {
				return "Error: ID of the widget is malformed or not supplied, please see <a href='https://support.wiki.gg/wiki/DiscordWidget' title='the instructions'>the instructions</a> for how to find your server's ID. Please make sure you are not inserting <strong>the DiscordIntegrator template</strong> when asked for <strong>your widget ID</strong>.";
			}
			if (
				(
					config.loggedIn === true ||
					Boolean(config['logged-in']) === true &&
					config['logged-in'] !== 'false' &&
					config['logged-in'] !== '{{{loggedIn}}}'
				) && !mconfig.wgUserName
			) {
				return "Please <a href='/Special:UserLogin' title='log in'>log in</a> to see this widget.";
			}
			var username = config.username === '@disabled' ?
				'' :
				config.username === '@function' &&
				typeof window.DiscordIntegratorGetUsername === 'function' ?
					window.DiscordIntegratorGetUsername() :
					config.username || mconfig.wgUserName;
			return mw.html.element('iframe', {
				src: 'https://discord.com/widget?id=' + config.id +
					'&theme=' + this.determineTheme(config.theme) +
					'&username=' + encodeURIComponent(username),
				width: config.width || '100%',
				height: config.height || '400px',
				style: 'max-width:100%;',
				allowtransparency: 'true',
				frameborder: '0',
				title: "Discord server widget"
			});
		}
	};
	DiscordIntegrator.init();
});

/***************************/

/* CSS classes for daytime periods */
$(function() {
    'use strict';
    var currentTime = new Date().getHours();
	if (0 <= currentTime && currentTime < 6) {
		if (document.body) {document.body.classList.add('night');}
	}
	else if (6 <= currentTime && currentTime < 12) {
		if (document.body) {document.body.classList.add('morning');}
	}
	else if (12 <= currentTime && currentTime < 18) {
		if (document.body) {document.body.classList.add('afternoon');}
	}
	else {
		if (document.body) {document.body.classList.add('evening');}
	}
});

/* Geometry Dash Expanded Wiki footer: source attribution, licensing, and local contact. */
mw.loader.using('mediawiki.util').then(function () {
$(function () {
    var footer = document.querySelector('#footer');
    var information = document.querySelector('#footer-info');
    if (!footer || !information || document.getElementById('footer-info-ewiki-sources')) return;
    function link(text, href) {
        var element = document.createElement('a');
        element.textContent = text;
        element.href = href;
        return element;
    }
    var sources = document.createElement('li');
    sources.id = 'footer-info-ewiki-sources';
    sources.append('Imported material is adapted from the ',
        link('Official Wiki', 'https://geometrydash.wiki.gg/'),
        ' and, where applicable, the ',
        link('Geometry Dash Fandom', 'https://geometry-dash.fandom.com/'),
        ' and ', link('Geometry Dash Fan Fandom', 'https://geometry-dash-fan.fandom.com/'),
        ' wikis. Source and contributor links are recorded in import edit summaries.');
    var license = document.createElement('li');
    license.id = 'footer-info-ewiki-license';
    license.append('Imported wiki text retains its original licensing, including ',
        link('Creative Commons Attribution-ShareAlike 4.0', 'https://creativecommons.org/licenses/by-sa/4.0/'),
        '. Images and separately licensed code may have different terms. ',
        link('Sources and licensing', mw.util.getUrl('Project:Copyrights')), '.');
    var independent = document.createElement('li');
    independent.id = 'footer-info-ewiki-independent';
    independent.textContent = 'Geometry Dash Expanded Wiki is an independent community wiki and is not affiliated with wiki.gg or RobTop Games.';
    information.append(sources, license, independent);
    var places = document.querySelector('#footer-places');
    if (places && !document.getElementById('footer-places-ewiki-contact')) {
        var contact = document.createElement('li');
        contact.id = 'footer-places-ewiki-contact';
        contact.appendChild(link('Contact Geometry Dash Expanded Wiki', mw.util.getUrl('User talk:Josh994')));
        places.appendChild(contact);
    }
});

});

/* Staff content classification: persisted as dedicated hidden categories. */
(function () {
    'use strict';
    if (mw.config.get('wgNamespaceNumber') !== 0 || mw.config.get('wgAction') !== 'view') { return; }
    var heading = document.getElementById('firstHeading');
    if (!heading || document.getElementById('gd-content-status')) { return; }
    var states = [
        {key: 'pending', label: 'community content (pending review)', category: 'Wiki content status - pending review'},
        {key: 'reviewed', label: 'community content', category: 'Wiki content status - reviewed community'},
        {key: 'base', label: 'base content', category: 'Wiki content status - base'}
    ];
    var categories = mw.config.get('wgCategories') || [];
    var selected = states.filter(function (state) { return categories.indexOf(state.category) !== -1; })[0];
    if (!selected) {
        selected = categories.some(function (name) { return /\(base content\)$/i.test(name); }) ? states[2] : states[0];
    }
    var staff = (mw.config.get('wgUserGroups') || []).some(function (group) {
        return ['sysop', 'bureaucrat'].indexOf(group) !== -1;
    });
    var badge = document.createElement(staff ? 'button' : 'span');
    badge.id = 'gd-content-status';
    badge.className = 'gd-content-status';
    badge.textContent = selected.label;
    badge.dataset.status = selected.key;
    if (staff) {
        badge.type = 'button';
        badge.setAttribute('aria-haspopup', 'dialog');
        badge.setAttribute('aria-expanded', 'false');
        badge.title = 'Change content classification';
    }
    heading.appendChild(badge);
    document.documentElement.classList.add('gd-content-status-ready');
    // Non-staff badges have no click handler or interactive controls.
    if (!staff) { return; }
    var panel;
    badge.addEventListener('click', function () {
        if (panel) { close(); return; }
        panel = document.createElement('div');
        panel.className = 'gd-content-status-menu';
        panel.setAttribute('role', 'dialog');
        panel.setAttribute('aria-label', 'Content classification');
        var title = document.createElement('strong');
        title.textContent = 'Content classification';
        panel.appendChild(title);
        states.forEach(function (state) {
            var button = document.createElement('button');
            button.type = 'button';
            button.textContent = state.label;
            button.disabled = state.key === selected.key;
            button.addEventListener('click', function () { save(state); });
            panel.appendChild(button);
        });
        var cancel = document.createElement('button');
        cancel.type = 'button'; cancel.textContent = 'Cancel';
        cancel.addEventListener('click', close); panel.appendChild(cancel);
        document.body.appendChild(panel);
        var rect = badge.getBoundingClientRect();
        panel.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - panel.offsetWidth - 8)) + 'px';
        panel.style.top = Math.max(8, Math.min(rect.bottom + 8, window.innerHeight - panel.offsetHeight - 8)) + 'px';
        badge.setAttribute('aria-expanded', 'true');
        panel.addEventListener('keydown', function (event) { if (event.key === 'Escape') { close(); } });
        cancel.focus();
    });
    function close() {
        if (panel) { panel.remove(); panel = null; }
        badge.setAttribute('aria-expanded', 'false'); badge.focus();
    }
    function applyBadge(state) {
        selected = state; badge.textContent = state.label; badge.dataset.status = state.key;
    }
    function replaceStatusCategory(text, state) {
        return text.replace(/\[\[\s*Category\s*:\s*Wiki[ _]content[ _]status[ _]-[ _](?:pending[ _]review|reviewed[ _]community|base)\s*(?:\|[^\]]*)?\]\]/gi, '').replace(/\s+$/, '') + '\n[[Category:' + state.category + ']]\n';
    }
    var publishedState = selected;
    mw.hook('ve.activationComplete').add(function () { publishedState = selected; });
    mw.hook('ve.deactivationComplete').add(function () {
        applyBadge(publishedState); close();
    });
    function save(state) {
        var editorActive = document.documentElement.classList.contains('ve-active');
        var target = window.ve && ve.init && ve.init.target;
        var surface = editorActive && target && target.getSurface && target.getSurface();
        var textbox = document.getElementById('wpTextbox1');
        if (editorActive || textbox) {
            try {
                if (surface && surface.getMode() === 'visual') {
                    var model = surface.getModel();
                    var fragment = model.getFragment();
                    var items = model.getDocument().getMetaList().getItemsInGroup('mwCategory').slice();
                    items.forEach(function (item) {
                        var category = item.getAttribute('category').replace(/_/g, ' ').replace(/^Category:/i, '');
                        if (states.some(function (entry) { return entry.category === category; })) {
                            fragment.removeMeta(item);
                        }
                    });
                    fragment.insertMeta({type: 'mwCategory', attributes: {category: 'Category:' + state.category, sortkey: ''}});
                } else if (surface && surface.getMode() === 'source') {
                    var sourceModel = surface.getModel();
                    var text = sourceModel.getDocument().data.getText(true);
                    sourceModel.getFragment().selectAll().insertContent(replaceStatusCategory(text, state));
                } else if (textbox) {
                    textbox.value = replaceStatusCategory(textbox.value, state);
                    textbox.dispatchEvent(new Event('input', {bubbles: true}));
                    textbox.dispatchEvent(new Event('change', {bubbles: true}));
                } else {
                    throw new Error('Editor is not ready');
                }
                applyBadge(state); close();
                mw.notify('Classification changed in this draft. Save changes to publish it.');
            } catch (error) {
                mw.notify('Could not change classification in this draft. No changes were published.', {type: 'error'});
            }
            return;
        }
        var currentPanel = panel;
        Array.prototype.forEach.call(currentPanel.querySelectorAll('button'), function (button) { button.disabled = true; });
        var status = document.createElement('p'); status.setAttribute('role', 'status');
        status.textContent = 'Saving...'; currentPanel.appendChild(status);
        mw.loader.using('mediawiki.api').then(function () {
            var api = new mw.Api();
            return api.get({action: 'query', prop: 'revisions', rvprop: 'ids|timestamp|content', rvslots: 'main',
                titles: mw.config.get('wgPageName'), curtimestamp: 1, formatversion: 2}).then(function (data) {
                var page = data.query.pages[0];
                if (page.missing || !page.revisions) { throw new Error('Page could not be read.'); }
                var revision = page.revisions[0];
                var text = revision.slots.main.content;
                text = text.replace(/\[\[\s*Category\s*:\s*Wiki[ _]content[ _]status[ _]-[ _](?:pending[ _]review|reviewed[ _]community|base)\s*(?:\|[^\]]*)?\]\]/gi, '');
                text = text.replace(/\s+$/, '') + '\n[[Category:' + state.category + ']]\n';
                return api.postWithEditToken({action: 'edit', title: page.title, text: text,
                    baserevid: revision.revid, basetimestamp: revision.timestamp, starttimestamp: data.curtimestamp,
                    nocreate: 1, assertuser: mw.config.get('wgUserName'), watchlist: 'nochange',
                    summary: 'Set content classification: ' + state.label}).then(function () {
                        selected = state; badge.textContent = state.label; badge.dataset.status = state.key;
                        close(); mw.notify('Content classification saved.');
                    });
            });
        }).catch(function () {
            if (panel !== currentPanel) { return; }
            status.textContent = 'Could not save. Reload the page and try again.';
            Array.prototype.forEach.call(currentPanel.querySelectorAll('button'), function (button) {
                button.disabled = button.textContent === selected.label;
            });
        });
    }
}());

/* Expanded Wiki Wraith browser-tab icon. */
(function () {
    document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(function (link) { link.remove(); });
    var icon = document.createElement('link');
    icon.rel = 'icon';
    icon.type = 'image/x-icon';
    icon.href = "https://geometrydashwiki.com/images/7/74/ExpandedWiki.ico";
    document.head.appendChild(icon);
}());
/* Keep Togglebox click targets attached to their wrapped headings. */
mw.hook('wikipage.content').add(function ($content) {
    mw.loader.using('jquery.makeCollapsible').then(function () {
        $content.find('.tabber.mw-collapsible').each(function () {
            var $box = $(this);
            $box.makeCollapsible();
            var $heading = $box.children('.mw-heading').first();
            var $toggle = $box.children('.mw-collapsible-toggle').first();
            if ($heading.length && $toggle.length) { $heading.append($toggle); }
        });
    });
});

/* Multiple-file uploads on Special:Upload. */
(function () {
    'use strict';
    if (mw.config.get('wgCanonicalSpecialPageName') !== 'Upload') { return; }
    mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
        $(function () {
            var source = document.getElementById('wpUploadFile');
            var form = source && source.closest('form');
            if (!form || document.getElementById('expanded-multiple-upload')) { return; }
            if (new URLSearchParams(location.search).has('wpForReUpload')) { return; }
            var sourceRow = source.closest('tr') || source.parentElement;
            var toggle = document.createElement(sourceRow.tagName === 'TR' ? 'tr' : 'div');
            toggle.id = 'expanded-multiple-upload';
            var label = document.createElement(sourceRow.tagName === 'TR' ? 'td' : 'span');
            label.textContent = 'Upload multiple files: ';
            var controls = document.createElement(sourceRow.tagName === 'TR' ? 'td' : 'span');
            toggle.append(label, controls);
            ['Yes', 'No'].forEach(function (name) {
                var item = document.createElement('label');
                var radio = document.createElement('input');
                radio.type = 'radio'; radio.name = 'expandedMultiple'; radio.value = name;
                radio.checked = name === 'No';
                item.append(radio, document.createTextNode(' ' + name + ' '));
                controls.appendChild(item);
            });
            sourceRow.before(toggle);
            var multi = document.createElement('input');
            multi.type = 'file'; multi.multiple = true; multi.hidden = true;
            multi.setAttribute('aria-label', 'Source filenames');
            source.after(multi);
            var destination = document.getElementById('wpDestFile');
            var destinationRow = destination && (destination.closest('tr') || destination.parentElement);
            var nativeSubmit = form.querySelector('[name="wpUpload"]');
            if (!nativeSubmit) { toggle.remove(); multi.remove(); return; }
            var button = nativeSubmit.cloneNode(true);
            button.id = 'expanded-upload-files'; button.removeAttribute('name');
            if (button.tagName === 'INPUT') { button.value = 'Upload files'; }
            else { button.textContent = 'Upload files'; }
            function setVisible(element, visible) {
                element.hidden = !visible;
                element.style.setProperty('display', visible ? '' : 'none', 'important');
            }
            setVisible(button, false);
            nativeSubmit.after(button);
            var results = document.createElement('div');
            results.setAttribute('aria-live', 'polite'); form.after(results);
            var busy = false;
            controls.addEventListener('change', function () {
                var enabled = controls.querySelector('input:checked').value === 'Yes';
                setVisible(source, !enabled); setVisible(multi, enabled);
                if (destinationRow) { setVisible(destinationRow, !enabled); }
                setVisible(nativeSubmit, !enabled); setVisible(button, enabled);
            });
            form.addEventListener('submit', function (event) {
                if (controls.querySelector('input:checked').value === 'Yes') {
                    event.preventDefault(); button.click();
                }
            });
            window.addEventListener('beforeunload', function (event) {
                if (busy) { event.preventDefault(); event.returnValue = ''; }
            });
            button.addEventListener('click', async function (event) {
                event.preventDefault();
                if (busy) { return; }
                var files = Array.from(multi.files);
                if (!files.length) { mw.notify('Choose at least one file.', {type: 'error'}); return; }
                var license = document.getElementById('wpLicense');
                var option = license && license.options[license.selectedIndex];
                if (!option || !option.value || option.value === '-') {
                    mw.notify('Choose a license before uploading.', {type: 'error'}); return;
                }
                var licenseText = option.title;
                if (!licenseText || licenseText.indexOf('{{') < 0) {
                    if (!/^[\w :.-]+$/.test(option.value)) {
                        mw.notify('Could not read the selected license.', {type: 'error'}); return;
                    }
                    licenseText = '{{' + option.value + '}}';
                }
                var description = document.getElementById('wpUploadDescription');
                var text = (description ? description.value : '') + '\n\n== Licensing ==\n' + licenseText;
                var watch = document.getElementById('wpWatchthis');
                var ignore = document.getElementById('wpIgnoreWarning');
                var params = {text: text, watchlist: watch && watch.checked ? 'watch' : 'nochange',
                    assertuser: mw.config.get('wgUserName')};
                if (ignore && ignore.checked) { params.ignorewarnings = 1; }
                busy = true; button.disabled = true;
                var inputs = Array.from(form.querySelectorAll('input, select, textarea, button'));
                var disabled = inputs.map(function (el) { return el.disabled; });
                inputs.forEach(function (el) { el.disabled = true; });
                results.replaceChildren();
                var status = document.createElement('p'); results.appendChild(status);
                var list = document.createElement('ul'); results.appendChild(list);
                var ready = 0, failed = 0;
                var api = new mw.Api();
                try {
                    for (var i = 0; i < files.length; i++) {
                        var file = files[i];
                        status.textContent = 'Uploading ' + (i + 1) + '/' + files.length + ': ' + file.name;
                        var row = document.createElement('li');
                        row.textContent = file.name + ': uploading…'; list.appendChild(row);
                        try {
                            var data = await new Promise(function (resolve, reject) {
                                api.upload(file, Object.assign({}, params, {filename: file.name}))
                                    .done(resolve).fail(function (code, details) {
                                        reject({code: code, details: details});
                                    });
                            });
                            var upload = data.upload;
                            if (!upload || upload.result !== 'Success') {
                                throw {code: 'upload-warning', details: data};
                            }
                            row.replaceChildren(document.createTextNode('Uploaded: '));
                            var link = document.createElement('a');
                            link.href = mw.util.getUrl('File:' + upload.filename);
                            link.textContent = upload.filename; row.appendChild(link); ready++;
                        } catch (error) {
                            failed++;
                            var details = error.details || {};
                            var warnings = details.upload && details.upload.warnings;
                            var message = details.error && details.error.info;
                            row.textContent = file.name + ': Failed — ' +
                                (warnings ? JSON.stringify(warnings) : message || error.code || 'Upload failed.');
                        }
                    }
                    status.textContent = 'Upload complete: ' + ready + ' uploaded, ' + failed + ' failed.';
                } finally {
                    inputs.forEach(function (el, index) { el.disabled = disabled[index]; });
                    busy = false; button.disabled = false;
                }
            });
        });
    });
}());
/* Use native browser audio controls in infoboxes. */
(function () {
    function nativeInfoboxAudio() {
        document.querySelectorAll('.portable-infobox td[data-source="audio"], .portable-infobox td[data-source="fullaudio"], .expanded-ve-infobox div[data-source="audio"] > .pi-data-value, .expanded-ve-infobox div[data-source="fullaudio"] > .pi-data-value').forEach(function (cell) {
            if (cell.querySelector('audio.expanded-native-audio')) { return; }
            var original = cell.querySelector('audio');
            if (!original) { return; }
            var sources = Array.prototype.slice.call(original.querySelectorAll('source'));
            if (!original.getAttribute('src') && !sources.length) { return; }
            var audio = document.createElement('audio');
            audio.className = 'expanded-native-audio';
            audio.controls = true;
            audio.preload = 'metadata';
            audio.setAttribute('aria-label', (cell.getAttribute('data-source') || cell.parentElement.getAttribute('data-source')) === 'fullaudio' ? 'Full audio' : 'Audio');
            if (original.getAttribute('src')) { audio.src = original.getAttribute('src'); }
            sources.forEach(function (source) {
                var copy = document.createElement('source');
                copy.src = source.getAttribute('src');
                if (source.getAttribute('type')) { copy.type = source.getAttribute('type'); }
                audio.appendChild(copy);
            });
            original.pause();
            cell.replaceChildren(audio);
        });
    }
    mw.hook('wikipage.content').add(nativeInfoboxAudio);
    $(function () {
        nativeInfoboxAudio();
        var content = document.getElementById('mw-content-text');
        if (content) {
            new MutationObserver(nativeInfoboxAudio).observe(content, { childList: true, subtree: true });
        }
    });
}());
if (mw.config.get('wgPageName') === 'Main_Page' && mw.config.get('wgAction') === 'view') {
  var mainPageHeading = document.getElementById('firstHeading');
  if (mainPageHeading) { mainPageHeading.style.setProperty('display', 'none', 'important'); }
}

/* Header shortcut to the built-in draft recovery page, and accurate article totals. */
$(function () {
    var item = mw.util.addPortletLink('p-personal', mw.util.getUrl('Special:EditRecovery'),
        '\u2692', 'pt-expanded-edit-recovery', 'Edit recovery: restore unsaved source edits');
    if (item) {
        item.parentNode.insertBefore(item, item.parentNode.firstChild);
        item.querySelector('a').setAttribute('aria-label', 'Edit recovery');
        if (window.indexedDB && indexedDB.databases) {
            indexedDB.databases().then(function (databases) {
                var name = mw.config.get('wgDBname') + '_editRecovery';
                if (!databases.some(function (db) { return db.name === name; })) { return; }
                var request = indexedDB.open(name);
                request.onsuccess = function () {
                    var db = request.result;
                    if (!db.objectStoreNames.contains('unsaved-page-data')) { db.close(); return; }
                    var read = db.transaction('unsaved-page-data', 'readonly').objectStore('unsaved-page-data').getAll();
                    read.onsuccess = function () {
                        var count = read.result.filter(function (draft) { return draft.expiry > Date.now() / 1000; }).length;
                        if (count) {
                            item.classList.add('expanded-recovery-pending');
                            item.querySelector('a').textContent = '\u26a0 \u2692';
                            item.querySelector('a').title = count + ' unsaved edit(s) available for recovery';
                        }
                        db.close();
                    };
                    read.onerror = function () { db.close(); };
                };
            }).catch(function () {});
        }
    }
    var counter = document.querySelector('.expanded-article-count a');
    if (!counter) { return; }
    mw.loader.using('mediawiki.api').then(function () {
        var api = new mw.Api(), count = 0;
        function next(continuation) {
            return api.get(Object.assign({action: 'query', list: 'allpages', apnamespace: 0,
                apfilterredir: 'nonredirects', aplimit: 'max', formatversion: 2}, continuation || {}))
                .then(function (data) {
                    count += data.query.allpages.length;
                    if (data.continue) { return next(data.continue); }
                    counter.textContent = count.toLocaleString();
                    counter.title = 'Non-redirect article pages in the main namespace';
                });
        }
        next().catch(function () {});
    });
});