MediaWiki:Vector.js: Difference between revisions

From Geometry Dash Expanded Wiki
Jump to navigation Jump to search
Use clean language URLs and the standard Recent Changes path
Clean navigation URLs across pages and match the language selector arrow and contrast
Line 385: Line 385:
         if (url.origin!==location.origin) return raw;
         if (url.origin!==location.origin) return raw;
         url.searchParams.delete('uselang');
         url.searchParams.delete('uselang');
        if (/\/index\.php$/.test(url.pathname) && !url.searchParams.has('title') && url.searchParams.has('search')) {url.pathname='/wiki/Special:Search';}
         if (/\/index\.php$/.test(url.pathname) && url.searchParams.has('title')) {
         if (/\/index\.php$/.test(url.pathname) && url.searchParams.has('title')) {
             var title=url.searchParams.get('title');url.searchParams.delete('title');
             var title=url.searchParams.get('title');url.searchParams.delete('title');
Line 397: Line 398:
     // The current request has already chosen its interface language.
     // The current request has already chosen its interface language.
     var address=new URL(location.href);
     var address=new URL(location.href);
     if (address.searchParams.has('uselang')) {
    var cleanAddress=clean(location.href);
        address.searchParams.delete('uselang');history.replaceState(null,'',address.pathname+address.search+address.hash);
     if (cleanAddress!==address.pathname+address.search+address.hash) history.replaceState(null,'',cleanAddress);
    }
     function links() {
     function links() {
         document.querySelectorAll('a[href]').forEach(function (link) {
         document.querySelectorAll('a[href]').forEach(function (link) {
Line 408: Line 408:
         });
         });
     }
     }
     $(links);
     $(function () {
        links();
        var pending=false;
        new MutationObserver(function () {
            if(pending)return;pending=true;
            requestAnimationFrame(function () {pending=false;links();});
        }).observe(document.body,{childList:true,subtree:true});
    });
     mw.hook('wikipage.content').add(links);
     mw.hook('wikipage.content').add(links);
     // Also handles asynchronously built language and account menus.
     // Also handles asynchronously built language and account menus.

Revision as of 23:43, 13 October 2026

/* All JavaScript here will be loaded for users of the Vector skin */

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

/* JavaScript Load Class (REQUIRED BY DEFAULT VECTOR.CSS) */
$(function () {
	document.body.classList.add('has-vector-mobile-menu');
});

/* Adapt standard Vector's structure for the imported stylesheet. */
$(function () {
    if (!document.body.classList.contains('skin-vector-legacy')) return;

    var content = document.getElementById('content');
    var navigation = document.getElementById('mw-navigation');
    if (!content || !navigation) return;

    if (!content.parentElement.classList.contains('content-wrapper')) {
        var wrapper = document.createElement('div');
        wrapper.className = 'content-wrapper';
        content.before(wrapper);
        wrapper.append(navigation, content);
    }

    if (!content.querySelector('.content-body')) {
        $(content).wrapInner('<div class="content-body"></div>');
    }
});

/* Search Toggle Button */
$(function () {
	var buttonClass = 'button-toggle button-toggle-search';
	var expandedClass = 'search--expanded';
	let searchButton = document.createElement('button');
	let sidebar = document.getElementById('mw-navigation');
	searchButton.className = buttonClass;
	searchButton.addEventListener('click', function () {
		searchButton.classList.toggle(expandedClass);
		sidebar.classList.toggle(expandedClass);
	});
	sidebar.prepend(searchButton);
});


/* Mobile Sidebar Toggle Button */
$(function () {
	var buttonClass = 'button-toggle button-toggle-sidebar-mobile';
	var expandedClass = 'sidebar-mobile--expanded';
	let mobileSidebarButton = document.createElement('button');
	let sidebar = document.getElementById('mw-navigation');
	mobileSidebarButton.className = buttonClass;
	mobileSidebarButton.addEventListener('click', function () {
		mobileSidebarButton.classList.toggle(expandedClass);
		sidebar.classList.toggle(expandedClass);
	});
	sidebar.prepend(mobileSidebarButton);
});


/* Sidebar Toggle Button */
$(function () {
	var buttonClass = 'button-toggle button-toggle-sidebar';
	var expandedClass = 'sidebar--collapsed';
	let sidebarButton = document.createElement('button');
	let sidebar = document.getElementById('mw-navigation');
	sidebarButton.className = buttonClass;
	sidebarButton.addEventListener('click', function () {
		sidebarButton.classList.toggle(expandedClass);
		sidebar.classList.toggle(expandedClass);
	});
	sidebar.prepend(sidebarButton);
});


/* Sidebar Box Content Collaspible Toggles */
$(function() {
    // configure sections (by header string) to initially collapse for all users by default
    var initiallyCollapsedIDs = []; /* e.g. ['Character_Classes', 'Items', 'External_Links'] */

    // add the p- onto ids for ease of use, if not present
    initiallyCollapsedIDs.forEach(function(id, index) {
        if(!id.startsWith('p-')) {initiallyCollapsedIDs[index] = 'p-' + id;}
    });
	var $portals = $("#mw-panel .vector-menu-portal");
	var setup = function() {
		$portals.each(function(index, el) {
			if(window.innerWidth < 721) {return;}
			var $el = $(el);
			var $id = $el.attr("id");
			if(!$id) {return;}
			// check if this portal should be collapsed - portal is set to be initially collapsed and user has not indicated wanting it open || user has indicated wanting this portal collapsed
			if(
				(initiallyCollapsedIDs.includes($id) && localStorage.getItem('sidebar_c_'+$id) != "n") || (localStorage.getItem('sidebar_c_'+$id) === "y")
			) {$el.addClass('collapsed').find('.vector-menu-content').slideUp(0);}
		});
	};
	setup();
	$(window).on('resize', setup);
	$portals.on("click", "h3", function(event) {
		if(window.innerWidth < 721) {return;}
		var $el = $(this).parent();
		var $id = $el.attr("id");
		if(!$id) {return;}
		$el.toggleClass('collapsed');
		if($el.hasClass('collapsed')) { // more consistent between class and slide status
			localStorage.setItem('sidebar_c_'+$id, "y");
			$el.find('.vector-menu-content').slideUp('fast');
		}
		else {
			localStorage.setItem('sidebar_c_'+$id, "n");
			$el.find('.vector-menu-content').slideDown('fast');
		}
	});
});


/* Header Dropdown Menu Purge Cache Link */
$(function() {
	if (mw.config.get('wgNamespaceNumber') === -1 ) {return;} // disable on special pages
	var url = new URL(window.location.href);
	url.searchParams.set('action', 'purge');
	var button = mw.util.addPortletLink('p-cactions', url.href, 'Purge cache', 'ca-gadget-purgecache', 'Purge the server cache for this page', '1');
	button.addEventListener('click', (function(event) {
		event.preventDefault();
		var api = new mw.Api();
		api.post({
			action: 'purge',
			titles: mw.config.get('wgPageName'),
			forcerecursivelinkupdate: true
		}).then(function() {
			window.location.reload(true);
		}).fail(function(code) {
			mw.notify('Page purge unsuccessful. Error code: $1'.replace('$1', code));
		});
	}));
	console.log('purge loaded');
});

/* Independent GD Wiki header: themes and existing account links. */
(function () {
    'use strict';
    var themes = [
        ['blue', 'Geometry Dash'], ['green', 'Vault'],
        ['purple', 'Vault of Secrets'], ['red', 'Chamber of Time'],
        ['dark', 'Dark'], ['blackout', 'Blackout']
    ];
    var storageKey = 'gdwiki-theme';
    function applyTheme(key) {
        if (!themes.some(function (theme) { return theme[0] === key; })) key = 'blue';
        var root = document.documentElement;
        themes.forEach(function (theme) { root.classList.remove('theme-' + theme[0]); });
        root.classList.remove('view-light');
        root.classList.add('view-dark', 'theme-' + key);
        return key;
    }
    var selected = 'blue';
    try { selected = localStorage.getItem(storageKey) || selected; } catch (error) {}
    selected = applyTheme(selected);
    $(function () {
        if (!document.body.classList.contains('skin-vector-legacy')) return;
        var personal = document.getElementById('p-personal');
        if (!personal || personal.querySelector('.gd-header-controls')) return;
        var links = personal.querySelector('ul');
        if (!links) return;
        var controls = document.createElement('div');
        controls.className = 'gd-header-controls';
        var appearance = document.createElement('details');
        appearance.className = 'gd-header-menu';
        var appearanceLabel = document.createElement('summary');
        appearanceLabel.textContent = 'Appearance';
        appearance.appendChild(appearanceLabel);
        var themePanel = document.createElement('div');
        themePanel.className = 'gd-header-dropdown';
        themePanel.setAttribute('role', 'group');
        themePanel.setAttribute('aria-label', 'Theme');
        themes.forEach(function (theme) {
            var button = document.createElement('button');
            button.type = 'button';
            button.className = 'gd-theme-option';
            button.textContent = theme[1];
            button.setAttribute('aria-pressed', String(theme[0] === selected));
            button.addEventListener('click', function () {
                selected = applyTheme(theme[0]);
                try { localStorage.setItem(storageKey, selected); } catch (error) {}
                themePanel.querySelectorAll('button').forEach(function (item) {
                    item.setAttribute('aria-pressed', String(item === button));
                });
                appearance.open = false;
            });
            themePanel.appendChild(button);
        });
        appearance.appendChild(themePanel);
        controls.appendChild(appearance);
        var icons = {
            gear: '<path d="m10 2 .5 2a8 8 0 0 1 3 0l.5-2 3 1 0 2a8 8 0 0 1 2 2l2 0 1 3-2 .5a8 8 0 0 1 0 3l2 .5-1 3-2 0a8 8 0 0 1-2 2l0 2-3 1-.5-2a8 8 0 0 1-3 0L10 22l-3-1 0-2a8 8 0 0 1-2-2l-2 0-1-3 2-.5a8 8 0 0 1 0-3L2 10l1-3 2 0a8 8 0 0 1 2-2l0-2z"/><circle cx="12" cy="12" r="3" fill="#03052f"/>',
            watch: '<path d="M3 3h18v15h-6l-2 3h-2l-2-3H3zm2 2v11h5l2 3 2-3h5V5z"/>',
            user: '<circle cx="12" cy="7" r="4"/><path d="M3 22v-2a9 9 0 0 1 18 0v2z"/>'
        };
        function iconMarkup(name) {
            return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + icons[name] + '</svg>';
        }
        function iconLink(id, name, label) {
            var original = document.querySelector('#' + id + ' a');
            if (!original) return;
            var link = original.cloneNode(false);
            link.className = 'gd-header-icon';
            link.title = label;
            link.setAttribute('aria-label', label);
            link.innerHTML = iconMarkup(name);
            controls.appendChild(link);
        }
        iconLink('pt-preferences', 'gear', 'Preferences');
        // Use Echo's actual widgets so alerts and notices retain their behavior.
        ['pt-notifications-alert', 'pt-notifications-notice'].forEach(function (id) {
            var notification = document.getElementById(id);
            if (notification) {
                notification.classList.add('gd-header-notification');
                controls.appendChild(notification);
            }
        });
        var account = document.createElement('details');
        account.className = 'gd-header-menu gd-account-menu';
        var accountLabel = document.createElement('summary');
        accountLabel.setAttribute('aria-label', 'Account menu');
        accountLabel.title = 'Account menu';
        var accountGroup = document.createElement('div');
        accountGroup.className = 'gd-account-group';
        var accountName = document.createElement('a');
        accountName.className = 'gd-account-talk-link';
        var userName = mw.config.get('wgUserName');
        accountName.href = mw.util.getUrl(userName ? 'User:' + userName : 'Special:UserLogin');
        accountName.innerHTML = iconMarkup('user');
        var nameText = document.createElement('span');
        nameText.textContent = userName || 'Account';
        accountName.appendChild(nameText);
        accountName.title = userName ? 'Your user page' : 'Log in';
        accountGroup.appendChild(accountName);
        account.appendChild(accountLabel);
        var duplicateUserPage = links.querySelector('#pt-userpage');
        if (duplicateUserPage) duplicateUserPage.remove();
        links.classList.add('gd-account-links');
        account.appendChild(links);
        accountGroup.appendChild(account);
        controls.appendChild(accountGroup);
        var barLogo = document.createElement('a');
        barLogo.className = 'gd-header-bar-logo';
        barLogo.href = mw.util.getUrl(mw.config.get('wgMainPageTitle') || 'Main Page');
        barLogo.title = mw.config.get('wgSiteName') || 'Geometry Dash Expanded Wiki';
        barLogo.setAttribute('aria-label', barLogo.title + ' main page');
        var logoImage = document.createElement('img');
        logoImage.src = 'https://geometrydashwiki.com/images/d/d4/Logo.jpg';
        logoImage.alt = ''; logoImage.width = 32; logoImage.height = 32;
        barLogo.appendChild(logoImage);
        personal.appendChild(barLogo);
        personal.appendChild(controls);
        personal.classList.add('gd-header-ready');
        document.addEventListener('click', function (event) {
            [appearance, account].forEach(function (menu) {
                if (!menu.contains(event.target)) menu.open = false;
            });
        });
        document.addEventListener('keydown', function (event) {
            if (event.key === 'Escape') { appearance.open = false; account.open = false; }
        });
    });
}());
/* Expanded multilingual navigation: shared accounts, files and Recent Changes. */
(function () {
    'use strict';
    mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
        $(function () {
            var controls = document.querySelector('.gd-header-controls');
            if (!controls || document.getElementById('gd-language-menu')) return;
            var page = (mw.config.get('wgPageName') || 'Main_Page').replace(/_/g, ' ');
            var match = /^(Spa|Rus|Pol)\/(.+)$/.exec(page);
            var pathCode = /^\/(eng|spa|rus|pol)(?:\/|$)/.exec(location.pathname);
            var code = match ? match[1].toLowerCase() : (pathCode ? pathCode[1] : 'eng');
            try {if (pathCode || match) localStorage.setItem('gdwiki-language',code);} catch(error) {}
            var base = match ? match[2] : page;
            var spanish = code === 'spa' || mw.config.get('wgUserLanguage') === 'es';
            var isArticle = mw.config.get('wgNamespaceNumber') === 0;
            document.documentElement.setAttribute('data-gd-language', code);
            var definitions = [
                {code:'eng', name:'English', lang:'en', flag:'/images/4/49/Flag_gb.svg'},
                {code:'spa', name:'Español', lang:'es', flag:'/images/2/2d/Flag_es.svg'}
            ];
            var menu = document.createElement('details');
            menu.id = 'gd-language-menu'; menu.className = 'gd-header-menu gd-language-menu';
            var summary = document.createElement('summary');
            summary.setAttribute('aria-label', spanish ? 'Cambiar idioma' : 'Change language');
            summary.title = summary.getAttribute('aria-label');
            var current = definitions[spanish ? 1 : 0];
            var image = document.createElement('img'); image.src=current.flag; image.alt=''; image.width=24; image.height=16;
            summary.appendChild(image);
            var label=document.createElement('span'); label.textContent=current.name; summary.appendChild(label);
            menu.appendChild(summary);
            var panel=document.createElement('div'); panel.className='gd-header-dropdown gd-language-options';
            panel.setAttribute('aria-label', spanish ? 'Idiomas' : 'Languages');
            menu.appendChild(panel);
            var personal = document.getElementById('p-personal');
            personal.appendChild(menu);
            var api=new mw.Api();
            function route(title, lang) {
                if (!mw.config.get('wgExpandedLanguageRoutes')) return mw.util.getUrl(title);
                var m=/^(Spa|Rus|Pol)\/(.+)$/.exec(title);
                var language=m ? m[1].toLowerCase() : 'eng';
                var name=m ? m[2] : title;
                if (name.indexOf(':')!==-1 && !m) return mw.util.getUrl(title);
                return '/'+language+'/'+encodeURIComponent(name.replace(/ /g,'_')).replace(/%2F/g,'/');
            }
            function build(mapping) {
                var entry=mapping[base] || {};
                Object.keys(mapping).some(function (key) {
                    if (mapping[key][code]===page) {base=key;entry=mapping[key];return true;}
                    return false;
                });
                var titles=definitions.map(function (d) {
                    return isArticle ? (entry[d.code] || (d.code==='eng' ? base : 'Spa/'+base)) : page;
                });
                api.get({action:'query',titles:titles.join('|'),prop:'info',formatversion:2}).then(function (data) {
                    var existing={};
                    (data.query.pages || []).forEach(function (p) {existing[p.title]=!p.missing && !p.invalid;});
                    definitions.forEach(function (d,i) {
                        var target=titles[i], available=existing[target];
                        var link=document.createElement('a'); link.lang=d.lang; link.hreflang=d.lang;
                        var flag=document.createElement('img'); flag.src=d.flag; flag.alt=''; flag.width=24; flag.height=16;
                        link.appendChild(flag);
                        var text=document.createElement('span'); text.textContent=d.name;
                        link.appendChild(text);
                        if (d.code===code) {link.setAttribute('aria-current','page');}
                        if (available || !isArticle) {
                            link.href=route(target,d.lang);
                        } else {
                            link.className='gd-language-missing';
                            link.href=mw.config.get('wgExpandedLanguageRoutes') ? route(target,d.lang)+'?action=edit' : mw.util.getUrl(target,{action:'edit'});
                            var note=document.createElement('small');note.textContent=spanish ? 'Crear traducción' : 'Create translation';link.appendChild(note);
                        }
                        link.addEventListener('click',function () {document.cookie='gdwiki-language='+d.code+'; Path=/; Max-Age=31536000; SameSite=Lax; Secure';});
                        panel.appendChild(link);
                    });
                    var home=document.createElement('a');home.textContent=spanish ? 'Inicio en español' : 'Spanish home';home.href=route('Spa/Main Page','es');panel.appendChild(home);
                    var guide=document.createElement('a');guide.textContent=spanish ? 'Guía de traducción' : 'Translation guide';guide.href=mw.util.getUrl('Help:Translations');panel.appendChild(guide);
                }, function () {panel.textContent=spanish ? 'No se pudieron cargar los idiomas. Recarga la página.' : 'Languages could not load. Reload the page.';});
            }
            api.get({action:'query',titles:'MediaWiki:Expanded-language-pages.json',prop:'revisions',rvprop:'content',rvslots:'main',formatversion:2}).then(function (data) {
                try {build(JSON.parse(data.query.pages[0].revisions[0].slots.main.content));} catch (error) {build({});}
            },function () {build({});});
            document.addEventListener('click',function (event) {if (!menu.contains(event.target)) menu.open=false;});
            document.addEventListener('keydown',function (event) {if (event.key==='Escape') menu.open=false;});
            if (spanish) {
                var interfaceLabels={"Navigation": "Navegación", "Main page": "Página principal", "Recent changes": "Cambios recientes", "New files": "Archivos nuevos", "Random page": "Página aleatoria", "New here? Get started": "¿Eres nuevo? Empieza aquí", ">> Site policy": ">> Normas del sitio", ">> Staff": ">> Administración", "Help using MediaWiki": "Ayuda sobre MediaWiki", "GD Base Content": "Contenido base de GD", "Games": "Juegos", "Main levels": "Niveles principales", "Gameplay": "Jugabilidad", "Rewards": "Recompensas", "Rooms (vaults etc.)": "Salas (bóvedas, etc.)", "Level editing": "Editor de niveles", "Account features": "Funciones de la cuenta", "Updates": "Actualizaciones", "GD Community Content": "Contenido de la comunidad de GD", "Demon List levels": "Niveles de la Demon List", "Gauntlet levels": "Niveles de gauntlets", "Map Pack levels": "Niveles de paquetes de mapas", "Classic levels": "Niveles clásicos", "Platformer levels": "Niveles de plataformas", "Players": "Jugadores", "Community activities": "Actividades de la comunidad", "Wiki Resources": "Recursos de la wiki", "Propose new articles": "Proponer artículos nuevos", "Submit edits for review": "Enviar cambios para revisión", "Admin Dashboard": "Panel de administración", "Images": "Imágenes", "Videos": "Vídeos", "Audio": "Audio", "PDFs": "PDF", "Templates": "Plantillas", "Sandbox": "Zona de pruebas", "Discuss the wiki": "Debatir sobre la wiki", "Guestbook": "Libro de visitas", "Tools": "Herramientas", "What links here": "Lo que enlaza aquí", "Related changes": "Cambios relacionados", "Upload file": "Subir archivo", "Printable version": "Versión para imprimir", "Permanent link": "Enlace permanente", "Page information": "Información de la página", "Account": "Cuenta", "Preferences": "Preferencias", "Your user page": "Tu página de usuario", "Log in": "Iniciar sesión", "Account menu": "Menú de cuenta", "Appearance": "Apariencia"};
                document.querySelectorAll('#mw-panel h3, #mw-panel h3 span, #mw-panel a, .gd-account-links a, .gd-account-talk-link span').forEach(function (element) {
                    var key=element.textContent.trim();
                    if (interfaceLabels[key]) element.textContent=interfaceLabels[key];
                });
                document.querySelectorAll('#p-personal [title], #p-personal [aria-label]').forEach(function (element) {
                    ['title','aria-label'].forEach(function (attr) {var key=element.getAttribute(attr);if(interfaceLabels[key])element.setAttribute(attr,interfaceLabels[key]);});
                });
                var appearance=controls.querySelector('.gd-header-menu summary');
                if (appearance) {appearance.textContent='Apariencia';appearance.title='Apariencia';appearance.setAttribute('aria-label','Apariencia');}
                var account=controls.querySelector('.gd-account-menu summary');
                if (account) {account.title='Menú de cuenta';account.setAttribute('aria-label','Menú de cuenta');}
                var titles={'Blue':'Azul','Purple':'Morado','Green':'Verde','Red':'Rojo','Dark':'Oscuro','Blackout':'Negro','Vault':'La bóveda','Vault of Secrets':'La bóveda de los secretos','Chamber of Time':'La cámara del tiempo'};
                controls.querySelectorAll('.gd-theme-option').forEach(function (button) {if(titles[button.textContent])button.textContent=titles[button.textContent];});
            }
            // Optional translated assets use the same existing theme filters.
            var logos=mw.config.get('wgExpandedLanguageLogos') || {};
            if (logos[code]) {
                if (logos[code].header) {
                    var header=document.querySelector('.gd-header-bar-logo img');if(header)header.src=logos[code].header;
                }
                if (logos[code].banner) document.documentElement.style.setProperty('--wiki-logo-image','url("'+logos[code].banner.replace(/["\\]/g,'')+'")');
            }
        });
    });
}());
/* Keep language parameters and script endpoints out of visible navigation URLs. */
(function () {
    'use strict';
    function clean(raw) {
        var url;
        try {url=new URL(raw,location.href);} catch(error) {return raw;}
        if (url.origin!==location.origin) return raw;
        url.searchParams.delete('uselang');
        if (/\/index\.php$/.test(url.pathname) && !url.searchParams.has('title') && url.searchParams.has('search')) {url.pathname='/wiki/Special:Search';}
        if (/\/index\.php$/.test(url.pathname) && url.searchParams.has('title')) {
            var title=url.searchParams.get('title');url.searchParams.delete('title');
            var match=/^(Spa|Rus|Pol)\/(.+)$/.exec(title.replace(/_/g,' '));
            var ns=title.indexOf(':')!==-1;
            var prefix=match ? '/'+match[1].toLowerCase()+'/' : (ns ? '/wiki/' : '/eng/');
            var name=match ? match[2] : title;
            url.pathname=prefix+encodeURIComponent(name.replace(/ /g,'_')).replace(/%2F/g,'/').replace(/%3A/g,':');
        }
        return url.pathname+url.search+url.hash;
    }
    // The current request has already chosen its interface language.
    var address=new URL(location.href);
    var cleanAddress=clean(location.href);
    if (cleanAddress!==address.pathname+address.search+address.hash) history.replaceState(null,'',cleanAddress);
    function links() {
        document.querySelectorAll('a[href]').forEach(function (link) {
            var href=link.getAttribute('href');
            if (href && !/^(?:#|javascript:|mailto:)/.test(href)) {
                var replacement=clean(href);if(replacement!==href)link.setAttribute('href',replacement);
            }
        });
    }
    $(function () {
        links();
        var pending=false;
        new MutationObserver(function () {
            if(pending)return;pending=true;
            requestAnimationFrame(function () {pending=false;links();});
        }).observe(document.body,{childList:true,subtree:true});
    });
    mw.hook('wikipage.content').add(links);
    // Also handles asynchronously built language and account menus.
    document.addEventListener('click',function(event) {
        var link=event.target.closest && event.target.closest('a[href]');
        if(link)link.setAttribute('href',clean(link.getAttribute('href')));
    },true);
}());