MediaWiki:Timeless.js: Difference between revisions

From Game Wiki - VortanMU
No edit summary
No edit summary
Line 1: Line 1:
/* ==============================
/* ==============================
   VortanMU - Timeless sidebar fix
   VortanMU - Timeless Sidebar V3
   Purpose:
   - Normaliza sidebar esquerda
   1) Remove legacy "MEGAMENU" DOM mutations (ul.submenu + display:none on original lists)
   - Remove resíduos (has-mega/submenu inline)
   2) Make left portlets collapsible (modern button-like headers)
   - Accordion moderno + persistência (localStorage)
 
  Paste this into: MediaWiki:Timeless.js
   ============================== */
   ============================== */


(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 removeLegacySubmenus(portlet) {
   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(function (ul) { ul.remove(); });
     qsa(portlet, 'ul.submenu, ul[class*="submenu"]').forEach(ul => ul.remove());


     // re-show the original list if a legacy script hid it
     // ensure original UL is visible (remove inline display none)
     var body = portlet.querySelector('.mw-portlet-body');
     const body = portlet.querySelector('.mw-portlet-body');
     if (!body) return;
     if (!body) return;


     var ul = body.querySelector('ul');
     const ul = body.querySelector('ul');
     if (!ul) return;
     if (!ul) return;


    // clear inline styles like: style="display:none"
     ul.style.display = '';
     ul.style.display = '';
     ul.hidden = false;
     ul.hidden = false;
   }
   }


   function ensureHeaderIsToggle(portlet) {
   function sanitizeHeader(header) {
     var header = portlet.querySelector('h3');
     header.classList.remove('has-mega', 'active', 'open', 'is-open');
     var body = portlet.querySelector('.mw-portlet-body');
     header.classList.add('vm-portlet__header');
     if (!header || !body) return;
     header.setAttribute('tabindex', '0');
    header.setAttribute('role', 'button');
  }


     if (!header.classList.contains('vm-portlet__header')) {
  function ensurePortletId(portlet, idx) {
      header.classList.add('vm-portlet__header');
     if (!portlet.id) portlet.id = 'vm-portlet-' + idx;
      header.setAttribute('tabindex', '0');
    return portlet.id;
      header.setAttribute('role', 'button');
  }
      header.setAttribute('aria-expanded', 'false');
 
      var onToggle = function () {
        var isOpen = portlet.classList.toggle('is-open');
        header.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
      };


      header.addEventListener('click', onToggle);
  function setOpen(portlet, header, open) {
      header.addEventListener('keydown', function (e) {
    portlet.classList.toggle('is-open', open);
        if (e.key === 'Enter' || e.key === ' ') {
    header.setAttribute('aria-expanded', open ? 'true' : 'false');
          e.preventDefault();
          onToggle();
        }
      });
    }
   }
   }


   function normalizeLeftSidebar() {
   function normalizeLeftSidebar() {
     var nav = document.getElementById('mw-site-navigation');
     const nav = document.getElementById('mw-site-navigation');
     if (!nav) return;
     if (!nav) return;


     nav.classList.add('vm-nav');
     nav.classList.add('vm-nav');


     var portlets = qsa(nav, '.sidebar-inner .mw-portlet[role="navigation"]');
     const inner = nav.querySelector('.sidebar-inner') || nav;
     portlets.forEach(function (p, idx) {
    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');
      removeLegacySubmenus(p);
      ensureHeaderIsToggle(p);


       // default: open first group only (can change to open all)
      const header = p.querySelector('h3');
      if (idx === 0) p.classList.add('is-open');
      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 other script tries to inject mega-menu again after load, kill it
     // guard: if some script tries to inject submenu again, delete it
    var inner = nav.querySelector('.sidebar-inner');
    if (!inner) return;
 
     if (!inner.__vmObserver) {
     if (!inner.__vmObserver) {
       var obs = new MutationObserver(function () {
       const obs = new MutationObserver(() => {
         var injected = inner.querySelector('ul.submenu');
         const injected = inner.querySelector('ul.submenu, ul[class*="submenu"]');
         if (injected) {
         if (injected) {
           qsa(inner, '.mw-portlet').forEach(function (p) {
           portlets.forEach(p => removeInjectedSubmenus(p));
            removeLegacySubmenus(p);
          });
         }
         }
       });
       });
Line 93: Line 133:
   function runSoon() {
   function runSoon() {
     normalizeLeftSidebar();
     normalizeLeftSidebar();
     setTimeout(normalizeLeftSidebar, 250);
     setTimeout(normalizeLeftSidebar, 200);
     setTimeout(normalizeLeftSidebar, 1000);
     setTimeout(normalizeLeftSidebar, 900);
   }
   }


   if (window.mw && mw.hook) {
   if (window.mw && mw.hook) {
     mw.hook('wikipage.content').add(function () {
     mw.hook('wikipage.content').add(() => runSoon());
      runSoon();
  } else if (document.readyState === 'loading') {
     });
     document.addEventListener('DOMContentLoaded', runSoon);
   } else {
   } else {
     if (document.readyState === 'loading') {
     runSoon();
      document.addEventListener('DOMContentLoaded', runSoon);
    } 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();
  }
})();