MediaWiki:Common.js: Difference between revisions

From Geometry Dash Expanded Wiki
Jump to navigation Jump to search
Use Geometry Dash Wiki Expanded as the public name instead of internal e!Wiki shorthand
Add staff-editable content classification badge; leave ordinary reader clicks inert
Line 529: Line 529:


});
});
/* 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);
        heading.appendChild(panel); 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;
            });
        });
    }
}());

Revision as of 17:39, 10 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 Wiki Expanded 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('Geometry Dash 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('Geometry Dash Wiki:Copyrights')), '.');
    var independent = document.createElement('li');
    independent.id = 'footer-info-ewiki-independent';
    independent.textContent = 'Geometry Dash Wiki Expanded 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 Wiki Expanded', 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);
        heading.appendChild(panel); 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;
            });
        });
    }
}());