MediaWiki:Common.js: Difference between revisions

From Geometry Dash Expanded Wiki
Jump to navigation Jump to search
Place classification dialog above the editor and infobox
Keep native audio controls in compatible infobox markup
Line 788: Line 788:
(function () {
(function () {
     function nativeInfoboxAudio() {
     function nativeInfoboxAudio() {
         document.querySelectorAll('.portable-infobox td[data-source="audio"], .portable-infobox td[data-source="fullaudio"]').forEach(function (cell) {
         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; }
             if (cell.querySelector('audio.expanded-native-audio')) { return; }
             var original = cell.querySelector('audio');
             var original = cell.querySelector('audio');
Line 798: Line 798:
             audio.controls = true;
             audio.controls = true;
             audio.preload = 'metadata';
             audio.preload = 'metadata';
             audio.setAttribute('aria-label', cell.getAttribute('data-source') === 'fullaudio' ? 'Full audio' : 'Audio');
             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'); }
             if (original.getAttribute('src')) { audio.src = original.getAttribute('src'); }
             sources.forEach(function (source) {
             sources.forEach(function (source) {

Revision as of 01:24, 11 October 2026

/* 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 save(state) {
        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 });
        }
    });
}());