MediaWiki:Timeless.js: Difference between revisions
From Game Wiki - VortanMU
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* ============================== | /* ============================== | ||
VortanMU - Timeless | VortanMU - Timeless Sidebar V3 | ||
- Normaliza sidebar esquerda | |||
- Remove resíduos (has-mega/submenu inline) | |||
- Accordion moderno + persistência (localStorage) | |||
============================== */ | ============================== */ | ||
(function () { | (function () { | ||
'use strict'; | 'use strict'; | ||
const STORAGE_KEY = 'vm_sidebar_open_portlets_v3'; | |||
function qsa(root, sel) { | function qsa(root, sel) { | ||
| Line 15: | Line 15: | ||
} | } | ||
function | function readState() { | ||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } | |||
catch (e) { return []; } | |||
} | |||
function writeState(ids) { | |||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)); } | |||
catch (e) {} | |||
} | |||
function removeInjectedSubmenus(portlet) { | |||
// remove any injected submenu lists | // remove any injected submenu lists | ||
qsa(portlet, 'ul.submenu').forEach | qsa(portlet, 'ul.submenu, ul[class*="submenu"]').forEach(ul => ul.remove()); | ||
// | // ensure original UL is visible (remove inline display none) | ||
const body = portlet.querySelector('.mw-portlet-body'); | |||
if (!body) return; | if (!body) return; | ||
const ul = body.querySelector('ul'); | |||
if (!ul) return; | if (!ul) return; | ||
ul.style.display = ''; | ul.style.display = ''; | ||
ul.hidden = false; | ul.hidden = false; | ||
} | } | ||
function | function sanitizeHeader(header) { | ||
header.classList.remove('has-mega', 'active', 'open', 'is-open'); | |||
header.classList.add('vm-portlet__header'); | |||
header.setAttribute('tabindex', '0'); | |||
header.setAttribute('role', 'button'); | |||
} | |||
if (! | function ensurePortletId(portlet, idx) { | ||
if (!portlet.id) portlet.id = 'vm-portlet-' + idx; | |||
return portlet.id; | |||
} | |||
function setOpen(portlet, header, open) { | |||
portlet.classList.toggle('is-open', open); | |||
header.setAttribute('aria-expanded', open ? 'true' : 'false'); | |||
} | } | ||
function normalizeLeftSidebar() { | function normalizeLeftSidebar() { | ||
const nav = document.getElementById('mw-site-navigation'); | |||
if (!nav) return; | if (!nav) return; | ||
nav.classList.add('vm-nav'); | nav.classList.add('vm-nav'); | ||
const inner = nav.querySelector('.sidebar-inner') || nav; | |||
portlets.forEach( | const portlets = qsa(inner, '.mw-portlet[role="navigation"]'); | ||
const saved = new Set(readState()); | |||
portlets.forEach((p, idx) => { | |||
p.classList.add('vm-portlet'); | p.classList.add('vm-portlet'); | ||
// default: open first | const header = p.querySelector('h3'); | ||
const body = p.querySelector('.mw-portlet-body'); | |||
if (!header || !body) return; | |||
const id = ensurePortletId(p, idx); | |||
sanitizeHeader(header); | |||
removeInjectedSubmenus(p); | |||
// default open: use saved state, otherwise open only first | |||
const shouldOpen = saved.size ? saved.has(id) : (idx === 0); | |||
setOpen(p, header, shouldOpen); | |||
if (!header.__vmBound) { | |||
const toggle = () => { | |||
const willOpen = !p.classList.contains('is-open'); | |||
// accordion behavior (open one at a time) | |||
portlets.forEach(other => { | |||
if (other !== p) { | |||
const h = other.querySelector('h3.vm-portlet__header'); | |||
if (h) setOpen(other, h, false); | |||
} | |||
}); | |||
setOpen(p, header, willOpen); | |||
// persist open ones (only one open) | |||
const openIds = portlets | |||
.filter(x => x.classList.contains('is-open')) | |||
.map(x => x.id) | |||
.filter(Boolean); | |||
writeState(openIds); | |||
}; | |||
header.addEventListener('click', toggle); | |||
header.addEventListener('keydown', (e) => { | |||
if (e.key === 'Enter' || e.key === ' ') { | |||
e.preventDefault(); | |||
toggle(); | |||
} | |||
}); | |||
header.__vmBound = true; | |||
} | |||
}); | }); | ||
// if some | // guard: if some script tries to inject submenu again, delete it | ||
if (!inner.__vmObserver) { | if (!inner.__vmObserver) { | ||
const obs = new MutationObserver(() => { | |||
const injected = inner.querySelector('ul.submenu, ul[class*="submenu"]'); | |||
if (injected) { | if (injected) { | ||
portlets.forEach(p => removeInjectedSubmenus(p)); | |||
} | } | ||
}); | }); | ||
| Line 93: | Line 133: | ||
function runSoon() { | function runSoon() { | ||
normalizeLeftSidebar(); | normalizeLeftSidebar(); | ||
setTimeout(normalizeLeftSidebar, | setTimeout(normalizeLeftSidebar, 200); | ||
setTimeout(normalizeLeftSidebar, | setTimeout(normalizeLeftSidebar, 900); | ||
} | } | ||
if (window.mw && mw.hook) { | if (window.mw && mw.hook) { | ||
mw.hook('wikipage.content').add( | mw.hook('wikipage.content').add(() => runSoon()); | ||
} else if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', runSoon); | |||
} else { | } else { | ||
runSoon(); | |||
} | } | ||
})(); | })(); | ||
Revision as of 06:59, 16 January 2026
/* ==============================
VortanMU - Timeless Sidebar V3
- Normaliza sidebar esquerda
- Remove resíduos (has-mega/submenu inline)
- Accordion moderno + persistência (localStorage)
============================== */
(function () {
'use strict';
const STORAGE_KEY = 'vm_sidebar_open_portlets_v3';
function qsa(root, sel) {
return Array.prototype.slice.call((root || document).querySelectorAll(sel));
}
function readState() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
catch (e) { return []; }
}
function writeState(ids) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)); }
catch (e) {}
}
function removeInjectedSubmenus(portlet) {
// remove any injected submenu lists
qsa(portlet, 'ul.submenu, ul[class*="submenu"]').forEach(ul => ul.remove());
// ensure original UL is visible (remove inline display none)
const body = portlet.querySelector('.mw-portlet-body');
if (!body) return;
const ul = body.querySelector('ul');
if (!ul) return;
ul.style.display = '';
ul.hidden = false;
}
function sanitizeHeader(header) {
header.classList.remove('has-mega', 'active', 'open', 'is-open');
header.classList.add('vm-portlet__header');
header.setAttribute('tabindex', '0');
header.setAttribute('role', 'button');
}
function ensurePortletId(portlet, idx) {
if (!portlet.id) portlet.id = 'vm-portlet-' + idx;
return portlet.id;
}
function setOpen(portlet, header, open) {
portlet.classList.toggle('is-open', open);
header.setAttribute('aria-expanded', open ? 'true' : 'false');
}
function normalizeLeftSidebar() {
const nav = document.getElementById('mw-site-navigation');
if (!nav) return;
nav.classList.add('vm-nav');
const inner = nav.querySelector('.sidebar-inner') || nav;
const portlets = qsa(inner, '.mw-portlet[role="navigation"]');
const saved = new Set(readState());
portlets.forEach((p, idx) => {
p.classList.add('vm-portlet');
const header = p.querySelector('h3');
const body = p.querySelector('.mw-portlet-body');
if (!header || !body) return;
const id = ensurePortletId(p, idx);
sanitizeHeader(header);
removeInjectedSubmenus(p);
// default open: use saved state, otherwise open only first
const shouldOpen = saved.size ? saved.has(id) : (idx === 0);
setOpen(p, header, shouldOpen);
if (!header.__vmBound) {
const toggle = () => {
const willOpen = !p.classList.contains('is-open');
// accordion behavior (open one at a time)
portlets.forEach(other => {
if (other !== p) {
const h = other.querySelector('h3.vm-portlet__header');
if (h) setOpen(other, h, false);
}
});
setOpen(p, header, willOpen);
// persist open ones (only one open)
const openIds = portlets
.filter(x => x.classList.contains('is-open'))
.map(x => x.id)
.filter(Boolean);
writeState(openIds);
};
header.addEventListener('click', toggle);
header.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggle();
}
});
header.__vmBound = true;
}
});
// guard: if some script tries to inject submenu again, delete it
if (!inner.__vmObserver) {
const obs = new MutationObserver(() => {
const injected = inner.querySelector('ul.submenu, ul[class*="submenu"]');
if (injected) {
portlets.forEach(p => removeInjectedSubmenus(p));
}
});
obs.observe(inner, { childList: true, subtree: true });
inner.__vmObserver = obs;
}
}
function runSoon() {
normalizeLeftSidebar();
setTimeout(normalizeLeftSidebar, 200);
setTimeout(normalizeLeftSidebar, 900);
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(() => runSoon());
} else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runSoon);
} else {
runSoon();
}
})();