MediaWiki:Common.js: Difference between revisions

From Hidden Mickey Wiki

No edit summary
Tag: Reverted
No edit summary
 
(304 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */
/* Reload Checklists when using back/forward buttons */
if (
    mw.config.get('wgPageName') === 'Disneyland_Checklist' ||
    mw.config.get('wgPageName') === 'California_Adventure_Checklist' ||
    mw.config.get('wgPageName') === 'Disneyland_Resort_Checklist' ||
    mw.config.get('wgPageName') === 'Other_Hidden_Characters_Checklist'
) {
    window.addEventListener('pageshow', function ( e ) {
        if ( e.persisted || window.performance.navigation.type === 2 ) {
            location.reload();
        }
    });
}
// Move "This page last edited" to bottom-left of main content
$(document).ready(function() {
    var lastMod = $('#footer-info-lastmod');
    if (lastMod.length) {
        // Append it after the main content container
        $('#mw-content-text').append(lastMod);
        // Apply styling
        lastMod.css({
            'text-align': 'left',
            'margin-top': '1em',  // space above it
            'margin-bottom': '1em'
        });
    }
});
/* Move the privacy/disclaimer/about pages below "Last Edited" */
$(document).ready(function () {
    var privacy    = $('#footer-places-privacy');
    var about      = $('#footer-places-about');
    var disclaimer  = $('#footer-places-disclaimers');
    var lastMod    = $('#footer-info-lastmod');
    if (privacy.length || about.length || disclaimer.length) {
        // Create a container for the one-line disclaimers
        var line = $('<div id="footer-disclaimer-line"></div>').css({
            'text-align': 'left',
            'margin-top': '0.2em',
            'margin-bottom': '0.5em'
        });
        // Move existing elements (links preserved!)
        privacy.detach().appendTo(line);
        about.detach().appendTo(line);
        disclaimer.detach().appendTo(line);
        // Insert below "Last edited"
        if (lastMod.length) {
            lastMod.after(line);
        } else {
            $('#mw-content-text').append(line);
        }
    }
});
// JavaScript code to save checkbox state and restore it when the page loads
// JavaScript code to save checkbox state and restore it when the page loads
$(document).ready(function() {
$(document).ready(function() {
Line 33: Line 95:
});
});


/* === Settings dropdown (site-wide) ===
// Add Edit Source to user dropdown
  Put this in MediaWiki:Common.js
mw.loader.using('mediawiki.util', function () {
*/
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl('Special:Search'), 'Search', 'pt-search' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'edit' } ), 'Edit Source', 'pt-editsource' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'history' } ), 'View History', 'pt-history' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'delete' } ), 'Delete', 'pt-delete' );
    var moveLink = document.getElementById('ca-move');
    if (moveLink) {
        var a = moveLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-move', a.title || 'Move this page' );
        moveLink.remove();
    }
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'protect' } ), 'Protect', 'pt-protect' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'unwatch' } ), 'Unwatch', 'pt-unwatch' );
    var talkLink = document.getElementById('pt-mytalk');
    if (talkLink) {
      talkLink.remove();
    }
    var whatLinksHereLink = document.getElementById('t-whatlinkshere');
    if (whatLinksHereLink) {
        var a = whatLinksHereLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-whatlinkshere', a.title || 'What Links Here' );
        whatLinksHereLink.remove();
    }
    var relatedChangesLink = document.getElementById('t-recentchangeslinked');
    if (relatedChangesLink) {
        var a = relatedChangesLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-recentchanges', a.title || 'Recent Changes' );
        relatedChangesLink.remove();
    }
    var uploadLink = document.getElementById('t-upload');
    if (uploadLink) {
        var a = uploadLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-upload', a.title || 'Upload File' );
        uploadLink.remove();
    }
    var specialPagesLink = document.getElementById('t-specialpages');
    if (specialPagesLink) {
        var a = specialPagesLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-specialpages', a.title || 'Special Pages' );
        specialPagesLink.remove();
    }
    var permanentLink = document.getElementById('t-permalink');
    if (permanentLink) {
        var a = permanentLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-permalink', a.title || 'Permanent Link' );
        permanentLink.remove();
    }
    var pageInfoLink = document.getElementById('t-info');
    if (pageInfoLink) {
        var a = pageInfoLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-info', a.title || 'Page Info' );
        pageInfoLink.remove();
    }
    var printLink = document.getElementById('t-print');
    if (printLink) {
        var a = printLink.querySelector('a');
        printLink.remove();
    }
});


mw.loader.using(['mediawiki.util']).done(function () {
/* Persistent checkboxes and timestamps */
   $(function () {
(function () {
     // Helper to build menu items (edit this array to change links)
  'use strict';
     var menuItems = [
  window.mwTimestamp = window.mwTimestamp || {};
       { title: 'Preferences', page: 'Special:Preferences' },
  window.mwTimestamp.DEBUG = window.mwTimestamp.DEBUG !== undefined ? window.mwTimestamp.DEBUG : true;
       { title: 'Watchlist', page: 'Special:Watchlist' },
  var PREFIX = 'mw-checkbox-ts:';
       { title: 'My talk', page: 'Special:MyTalk' },
  function isLocalStorageAvailable() {
       { title: 'Recent changes', page: 'Special:RecentChanges' }
    try {
     ];
      var testKey = '__test__';
      localStorage.setItem(testKey, testKey);
      localStorage.removeItem(testKey);
      return true;
    } catch (e) {
      if (window.mwTimestamp.DEBUG) console.warn('localStorage not available:', e);
      return false;
    }
  }
  function formatTime(d) {
    return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' });
   }
  function ensureTsBox(cb) {
    if (!cb) { return null; }
     var tr = cb.closest && cb.closest('tr');
     if (tr) {
      var existing = tr.querySelector && tr.querySelector('.mw-ts-box');
       if (existing) { return existing; }
      var lastCell = tr.querySelector('td:last-child, th:last-child') || tr.lastElementChild;
      if (lastCell) {
        var ts = lastCell.querySelector('.mw-ts-box');
        if (!ts) { ts = document.createElement('span'); ts.className = 'mw-ts-box'; lastCell.appendChild(ts); }
        return ts;
      }
    }
    var td = cb.closest && cb.closest('td') || cb.parentElement;
    if (td && td.nextElementSibling) {
      var next = td.nextElementSibling;
      var ts2 = next.querySelector && next.querySelector('.mw-ts-box');
      if (!ts2) { ts2 = document.createElement('span'); ts2.className = 'mw-ts-box'; next.appendChild(ts2); }
      return ts2;
    }
    var span = document.createElement('span');
    span.className = 'mw-ts-box';
    if (cb.parentNode) cb.parentNode.insertBefore(span, cb.nextSibling);
    return span;
  }
  function saveState(cb, tsBox) {
    if (!cb || !cb.id) return;
    if (!isLocalStorageAvailable()) return;
    var data = {
      checked: !!cb.checked,
      timestamp: tsBox ? tsBox.textContent : ''
    };
    try {
      localStorage.setItem(PREFIX + cb.id, JSON.stringify(data));
      if (window.mwTimestamp.DEBUG) console.log('Saved state for', cb.id, data);
    } catch (e) {
      if (window.mwTimestamp.DEBUG) console.warn('Failed to save state:', e);
    }
  }
  function restoreOne(id, data) {
    var cb = document.getElementById(id);
    if (!cb) return false;
    cb.checked = !!data.checked;
    var tsBox = ensureTsBox(cb);
    if (tsBox) tsBox.textContent = data.timestamp || '';
    return true;
  }
  function restoreAllOnce() {
    if (!isLocalStorageAvailable()) return;
    try {
      var keys = Object.keys(localStorage);
       keys.forEach(function (k) {
        if (k.indexOf(PREFIX) !== 0) return;
        var id = k.slice(PREFIX.length);
        var json = localStorage.getItem(k);
        if (!json) return;
        try {
          var data = JSON.parse(json);
          restoreOne(id, data);
          if (window.mwTimestamp.DEBUG) console.log('Restored', id, data);
        } catch (e) {
          if (window.mwTimestamp.DEBUG) console.warn('Failed to parse data for', id);
        }
      });
    } catch (e) {
      if (window.mwTimestamp.DEBUG) console.warn('Error during restoreAllOnce:', e);
    }
  }
  function initRestore() {
    restoreAllOnce();
    if (window.mw && mw.hook) {
      try {
        mw.hook('wikipage.content').add(function () {
          restoreAllOnce();
        });
      } catch (e) {
        if (window.mwTimestamp.DEBUG) console.warn('mw.hook error:', e);
      }
    }
    setTimeout(restoreAllOnce, 200);
    setTimeout(restoreAllOnce, 1200);
    if (window.MutationObserver) {
       try {
        var observer = new MutationObserver(function (mutations) {
          var want = false;
          for (var i = 0; i < mutations.length && !want; i++) {
            var added = mutations[i].addedNodes;
            for (var j = 0; j < added.length && !want; j++) {
              var node = added[j];
              if (node.nodeType !== 1) continue;
              if (node.matches && node.matches('.mw-checkbox-ts')) want = true;
              if (node.querySelector && (node.querySelector('.mw-checkbox-ts') || node.querySelector('.mw-ts-box'))) want = true;
            }
          }
          if (want) restoreAllOnce();
        });
        observer.observe(document.body, { childList: true, subtree: true });
      } catch (e) {
        if (window.mwTimestamp.DEBUG) console.warn('MutationObserver error:', e);
       }
    }
  }
  function handleCheckboxEvent(ev) {
    var cb = ev.target;
    if (!cb || !cb.matches || !cb.matches('.mw-checkbox-ts')) return;
    var tsBox = ensureTsBox(cb);
    tsBox.textContent = cb.checked ? formatTime(new Date()) : '';
    saveState(cb, tsBox);
  }
  document.addEventListener('change', handleCheckboxEvent, false);
  document.addEventListener('click', handleCheckboxEvent, false);
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initRestore);
  } else {
    initRestore();
  }
  window.mwTimestamp.restoreAll = restoreAllOnce;
  window.mwTimestamp.listSaved = function () {
    return Object.keys(localStorage).filter(function (k) { return k.indexOf(PREFIX) === 0; });
  };
  window.mwTimestamp.getSaved = function (id) {
    var j = localStorage.getItem(PREFIX + id);
     try { return j ? JSON.parse(j) : null; } catch (e) { return null; }
  };
})();


    // Build DOM
/* Spinner */
    var $menu = $('<div id="mw-settings-dropdown" class="mw-settings-dropdown" role="navigation" aria-label="Settings"></div>');
mw.loader.using('jquery', function () {
     var $button = $('<button class="mw-settings-button" aria-haspopup="true" aria-expanded="false" aria-controls="mw-settings-list">Settings ▾</button>');
     $('body').append('<div id="mw-page-spinner"></div>');
    var $list = $('<ul id="mw-settings-list" class="mw-settings-list" hidden></ul>');
});


    menuItems.forEach(function (it) {
(function () {
      var href = mw.util.getUrl(it.page);
      var $li = $('<li></li>');
      var $a = $('<a></a>').attr('href', href).text(it.title);
      $li.append($a);
      $list.append($li);
    });


     $menu.append($button, $list);
     function showSpinner() {
        document.body.classList.add('show-spinner');
    }


     // Try several insertion targets (common across skins); first match wins
     function hideSpinner() {
    var targets = [
        document.body.classList.remove('show-spinner');
      '#mw-navbar-right',  // Tweeki / some custom headers
     }
      '#p-personal',        // older Vector / MonoBook personal tools container
      '#mw-head-base',      // some skins
      '#mw-head',          // fallback header container
      '#p-navigation',      // left nav
      'body'                // ultimate fallback
     ];


     var inserted = false;
     /* --------------------------------------------------
    for (var i = 0; i < targets.length; i++) {
      1. ALWAYS clear spinner when MediaWiki finishes
      var $t = $(targets[i]).first();
          loading or re-rendering page content
      if ($t.length) {
    -------------------------------------------------- */
        // If target has a UL (portlet), insert as <li> there; else append the widget
    if (window.mw && mw.hook) {
        var $ul = $t.is('ul') ? $t : $t.find('ul').first();
        mw.hook('wikipage.content').add(hideSpinner);
        if ($ul && $ul.length) {
          // wrap dropdown in an <li> so markup fits portlet lists
          var $liWrap = $('<li id="pt-settings" class="mw-portlet-item"></li>');
          $liWrap.append($menu);
          $ul.append($liWrap);
        } else {
          $t.append($menu);
        }
        inserted = true;
        break;
      }
     }
     }
    if (!inserted) $('body').append($menu);


     // Toggle behavior (click / outside click / keyboard)
     // Safety net for full reloads / BFCache restores
     $button.on('click', function (e) {
    window.addEventListener('pageshow', hideSpinner);
      var expanded = $(this).attr('aria-expanded') === 'true';
 
      if (expanded) {
    /* --------------------------------------------------
         $(this).attr('aria-expanded', 'false');
      2. Show spinner ONLY on real page navigation clicks
         $list.attr('hidden', 'hidden');
    -------------------------------------------------- */
      } else {
     document.addEventListener('click', function (event) {
         $(this).attr('aria-expanded', 'true');
        const link = event.target.closest('a');
        $list.removeAttr('hidden');
        if (!link) return;
         $list.find('a').first().focus();
 
      }
        // Ignore modified clicks
      e.stopPropagation();
        if (
    });
            event.defaultPrevented ||
            event.button !== 0 ||
            event.metaKey ||
            event.ctrlKey ||
            event.shiftKey ||
            event.altKey
        ) return;
 
        // Ignore new tabs/windows
        if (link.target && link.target !== '_self') return;
 
        const href = link.getAttribute('href');
         if (!href || href.startsWith('#')) return;
 
         const currentURL = new URL(location.href);
        const targetURL = new URL(href, location.href);
 
         // Ignore same-document navigation
        if (
            currentURL.origin === targetURL.origin &&
            currentURL.pathname === targetURL.pathname &&
            currentURL.search === targetURL.search
        ) return;
 
        showSpinner();
 
    }, true);
 
})();
 
mw.hook('wikipage.content').add(function () {
    function fixHashScroll() {
        if (!location.hash) return;
 
         var id = decodeURIComponent(location.hash.substring(1));
        var target = document.getElementById(id);
        if (!target) return;
 
        var navbar = document.getElementById('mw-navbar');
        var offset = navbar ? navbar.offsetHeight : 0;
 
        var y =
            target.getBoundingClientRect().top +
            window.pageYOffset -
            offset -
            5;


    // Close if clicking outside
        window.scrollTo(0, y);
    $(document).on('click', function () {
     }
      $button.attr('aria-expanded', 'false');
      $list.attr('hidden', 'hidden');
     });
    $menu.on('click', function (e) { e.stopPropagation(); });


     // Keyboard: Esc closes; Down/Enter/Space opens
     document.addEventListener('click', function (e) {
    $(document).on('keydown', function (e) {
        var a = e.target.closest('a[href^="#"]');
      if (e.key === 'Escape') {
         if (!a) return;
        $button.attr('aria-expanded', 'false');
         setTimeout(fixHashScroll, 0);
         $list.attr('hidden', 'hidden');
      }
    });
    $button.on('keydown', function (e) {
      if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
         $button.click();
      }
     });
     });


  });
    if (location.hash) {
        setTimeout(fixHashScroll, 0);
    }
});
});

Latest revision as of 23:11, 8 February 2026

/* Any JavaScript here will be loaded for all users on every page load. */

/* Reload Checklists when using back/forward buttons */
if (
    mw.config.get('wgPageName') === 'Disneyland_Checklist' ||
    mw.config.get('wgPageName') === 'California_Adventure_Checklist' ||
    mw.config.get('wgPageName') === 'Disneyland_Resort_Checklist' ||
    mw.config.get('wgPageName') === 'Other_Hidden_Characters_Checklist'
) {
    window.addEventListener('pageshow', function ( e ) {
        if ( e.persisted || window.performance.navigation.type === 2 ) {
            location.reload();
        }
    });
}

// Move "This page last edited" to bottom-left of main content
$(document).ready(function() {
    var lastMod = $('#footer-info-lastmod');
    if (lastMod.length) {
        // Append it after the main content container
        $('#mw-content-text').append(lastMod);

        // Apply styling
        lastMod.css({
            'text-align': 'left',
            'margin-top': '1em',   // space above it
            'margin-bottom': '1em'
        });
    }
});

/* Move the privacy/disclaimer/about pages below "Last Edited" */
$(document).ready(function () {
    var privacy     = $('#footer-places-privacy');
    var about       = $('#footer-places-about');
    var disclaimer  = $('#footer-places-disclaimers');
    var lastMod     = $('#footer-info-lastmod');

    if (privacy.length || about.length || disclaimer.length) {

        // Create a container for the one-line disclaimers
        var line = $('<div id="footer-disclaimer-line"></div>').css({
            'text-align': 'left',
            'margin-top': '0.2em',
            'margin-bottom': '0.5em'
        });

        // Move existing elements (links preserved!)
        privacy.detach().appendTo(line);
        about.detach().appendTo(line);
        disclaimer.detach().appendTo(line);

        // Insert below "Last edited"
        if (lastMod.length) {
            lastMod.after(line);
        } else {
            $('#mw-content-text').append(line);
        }
    }
});


// JavaScript code to save checkbox state and restore it when the page loads
$(document).ready(function() {
    // Function to save the state of checkboxes to localStorage
    function saveCheckboxState() {
        $('input[type="checkbox"]').each(function() {
            localStorage.setItem($(this).attr('id'), $(this).prop('checked'));
        });
    }

    // Function to load the state of checkboxes from localStorage
    function loadCheckboxState() {
        $('input[type="checkbox"]').each(function() {
            const savedState = localStorage.getItem($(this).attr('id'));
            if (savedState !== null) {
                $(this).prop('checked', savedState === 'true');
            }
        });
    }

    // Load the saved checkbox state when the page is loaded
    loadCheckboxState();

    // Save the checkbox state whenever a checkbox is changed
    $('input[type="checkbox"]').change(function() {
        saveCheckboxState();
    });
});

// Adjust the search box width
$(document).ready(function () {
    $('#searchInput').css('width', '600px'); // Adjust width as needed
});

// Add Edit Source to user dropdown
mw.loader.using('mediawiki.util', function () {
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl('Special:Search'), 'Search', 'pt-search' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'edit' } ), 'Edit Source', 'pt-editsource' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'history' } ), 'View History', 'pt-history' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'delete' } ), 'Delete', 'pt-delete' );
    var moveLink = document.getElementById('ca-move');
    if (moveLink) {
        var a = moveLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-move', a.title || 'Move this page' );
        moveLink.remove();
    }
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'protect' } ), 'Protect', 'pt-protect' );
    mw.util.addPortletLink( 'p-personal', mw.util.getUrl( mw.config.get('wgPageName'), { action: 'unwatch' } ), 'Unwatch', 'pt-unwatch' );
    var talkLink = document.getElementById('pt-mytalk');
    if (talkLink) {
      talkLink.remove();
    }
    var whatLinksHereLink = document.getElementById('t-whatlinkshere');
    if (whatLinksHereLink) {
        var a = whatLinksHereLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-whatlinkshere', a.title || 'What Links Here' );
        whatLinksHereLink.remove();
    }
    var relatedChangesLink = document.getElementById('t-recentchangeslinked');
    if (relatedChangesLink) {
        var a = relatedChangesLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-recentchanges', a.title || 'Recent Changes' );
        relatedChangesLink.remove();
    }
    var uploadLink = document.getElementById('t-upload');
    if (uploadLink) {
        var a = uploadLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-upload', a.title || 'Upload File' );
        uploadLink.remove();
    }
    var specialPagesLink = document.getElementById('t-specialpages');
    if (specialPagesLink) {
        var a = specialPagesLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-specialpages', a.title || 'Special Pages' );
        specialPagesLink.remove();
    }
    var permanentLink = document.getElementById('t-permalink');
    if (permanentLink) {
        var a = permanentLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-permalink', a.title || 'Permanent Link' );
        permanentLink.remove();
    }
    var pageInfoLink = document.getElementById('t-info');
    if (pageInfoLink) {
        var a = pageInfoLink.querySelector('a');
        mw.util.addPortletLink( 'p-personal', a.href, a.textContent.trim(), 'pt-info', a.title || 'Page Info' );
        pageInfoLink.remove();
    }
    var printLink = document.getElementById('t-print');
    if (printLink) {
        var a = printLink.querySelector('a');
        printLink.remove();
    }
});

/* Persistent checkboxes and timestamps */
(function () {
  'use strict';
  window.mwTimestamp = window.mwTimestamp || {};
  window.mwTimestamp.DEBUG = window.mwTimestamp.DEBUG !== undefined ? window.mwTimestamp.DEBUG : true;
  var PREFIX = 'mw-checkbox-ts:';
  function isLocalStorageAvailable() {
    try {
      var testKey = '__test__';
      localStorage.setItem(testKey, testKey);
      localStorage.removeItem(testKey);
      return true;
    } catch (e) {
      if (window.mwTimestamp.DEBUG) console.warn('localStorage not available:', e);
      return false;
    }
  }
  function formatTime(d) {
    return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' });
  }
  function ensureTsBox(cb) {
    if (!cb) { return null; }
    var tr = cb.closest && cb.closest('tr');
    if (tr) {
      var existing = tr.querySelector && tr.querySelector('.mw-ts-box');
      if (existing) { return existing; }
      var lastCell = tr.querySelector('td:last-child, th:last-child') || tr.lastElementChild;
      if (lastCell) {
        var ts = lastCell.querySelector('.mw-ts-box');
        if (!ts) { ts = document.createElement('span'); ts.className = 'mw-ts-box'; lastCell.appendChild(ts); }
        return ts;
      }
    }
    var td = cb.closest && cb.closest('td') || cb.parentElement;
    if (td && td.nextElementSibling) {
      var next = td.nextElementSibling;
      var ts2 = next.querySelector && next.querySelector('.mw-ts-box');
      if (!ts2) { ts2 = document.createElement('span'); ts2.className = 'mw-ts-box'; next.appendChild(ts2); }
      return ts2;
    }
    var span = document.createElement('span');
    span.className = 'mw-ts-box';
    if (cb.parentNode) cb.parentNode.insertBefore(span, cb.nextSibling);
    return span;
  }
  function saveState(cb, tsBox) {
    if (!cb || !cb.id) return;
    if (!isLocalStorageAvailable()) return;
    var data = {
      checked: !!cb.checked,
      timestamp: tsBox ? tsBox.textContent : ''
    };
    try {
      localStorage.setItem(PREFIX + cb.id, JSON.stringify(data));
      if (window.mwTimestamp.DEBUG) console.log('Saved state for', cb.id, data);
    } catch (e) {
      if (window.mwTimestamp.DEBUG) console.warn('Failed to save state:', e);
    }
  }
  function restoreOne(id, data) {
    var cb = document.getElementById(id);
    if (!cb) return false;
    cb.checked = !!data.checked;
    var tsBox = ensureTsBox(cb);
    if (tsBox) tsBox.textContent = data.timestamp || '';
    return true;
  }
  function restoreAllOnce() {
    if (!isLocalStorageAvailable()) return;
    try {
      var keys = Object.keys(localStorage);
      keys.forEach(function (k) {
        if (k.indexOf(PREFIX) !== 0) return;
        var id = k.slice(PREFIX.length);
        var json = localStorage.getItem(k);
        if (!json) return;
        try {
          var data = JSON.parse(json);
          restoreOne(id, data);
          if (window.mwTimestamp.DEBUG) console.log('Restored', id, data);
        } catch (e) {
          if (window.mwTimestamp.DEBUG) console.warn('Failed to parse data for', id);
        }
      });
    } catch (e) {
      if (window.mwTimestamp.DEBUG) console.warn('Error during restoreAllOnce:', e);
    }
  }
  function initRestore() {
    restoreAllOnce();
    if (window.mw && mw.hook) {
      try {
        mw.hook('wikipage.content').add(function () {
          restoreAllOnce();
        });
      } catch (e) {
        if (window.mwTimestamp.DEBUG) console.warn('mw.hook error:', e);
      }
    }
    setTimeout(restoreAllOnce, 200);
    setTimeout(restoreAllOnce, 1200);
    if (window.MutationObserver) {
      try {
        var observer = new MutationObserver(function (mutations) {
          var want = false;
          for (var i = 0; i < mutations.length && !want; i++) {
            var added = mutations[i].addedNodes;
            for (var j = 0; j < added.length && !want; j++) {
              var node = added[j];
              if (node.nodeType !== 1) continue;
              if (node.matches && node.matches('.mw-checkbox-ts')) want = true;
              if (node.querySelector && (node.querySelector('.mw-checkbox-ts') || node.querySelector('.mw-ts-box'))) want = true;
            }
          }
          if (want) restoreAllOnce();
        });
        observer.observe(document.body, { childList: true, subtree: true });
      } catch (e) {
        if (window.mwTimestamp.DEBUG) console.warn('MutationObserver error:', e);
      }
    }
  }
  function handleCheckboxEvent(ev) {
    var cb = ev.target;
    if (!cb || !cb.matches || !cb.matches('.mw-checkbox-ts')) return;
    var tsBox = ensureTsBox(cb);
    tsBox.textContent = cb.checked ? formatTime(new Date()) : '';
    saveState(cb, tsBox);
  }
  document.addEventListener('change', handleCheckboxEvent, false);
  document.addEventListener('click', handleCheckboxEvent, false);
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initRestore);
  } else {
    initRestore();
  }
  window.mwTimestamp.restoreAll = restoreAllOnce;
  window.mwTimestamp.listSaved = function () {
    return Object.keys(localStorage).filter(function (k) { return k.indexOf(PREFIX) === 0; });
  };
  window.mwTimestamp.getSaved = function (id) {
    var j = localStorage.getItem(PREFIX + id);
    try { return j ? JSON.parse(j) : null; } catch (e) { return null; }
  };
})();

/* Spinner */
mw.loader.using('jquery', function () {
    $('body').append('<div id="mw-page-spinner"></div>');
});

(function () {

    function showSpinner() {
        document.body.classList.add('show-spinner');
    }

    function hideSpinner() {
        document.body.classList.remove('show-spinner');
    }

    /* --------------------------------------------------
       1. ALWAYS clear spinner when MediaWiki finishes
          loading or re-rendering page content
    -------------------------------------------------- */
    if (window.mw && mw.hook) {
        mw.hook('wikipage.content').add(hideSpinner);
    }

    // Safety net for full reloads / BFCache restores
    window.addEventListener('pageshow', hideSpinner);

    /* --------------------------------------------------
       2. Show spinner ONLY on real page navigation clicks
    -------------------------------------------------- */
    document.addEventListener('click', function (event) {
        const link = event.target.closest('a');
        if (!link) return;

        // Ignore modified clicks
        if (
            event.defaultPrevented ||
            event.button !== 0 ||
            event.metaKey ||
            event.ctrlKey ||
            event.shiftKey ||
            event.altKey
        ) return;

        // Ignore new tabs/windows
        if (link.target && link.target !== '_self') return;

        const href = link.getAttribute('href');
        if (!href || href.startsWith('#')) return;

        const currentURL = new URL(location.href);
        const targetURL = new URL(href, location.href);

        // Ignore same-document navigation
        if (
            currentURL.origin === targetURL.origin &&
            currentURL.pathname === targetURL.pathname &&
            currentURL.search === targetURL.search
        ) return;

        showSpinner();

    }, true);

})();

mw.hook('wikipage.content').add(function () {
    function fixHashScroll() {
        if (!location.hash) return;

        var id = decodeURIComponent(location.hash.substring(1));
        var target = document.getElementById(id);
        if (!target) return;

        var navbar = document.getElementById('mw-navbar');
        var offset = navbar ? navbar.offsetHeight : 0;

        var y =
            target.getBoundingClientRect().top +
            window.pageYOffset -
            offset -
            5;

        window.scrollTo(0, y);
    }

    document.addEventListener('click', function (e) {
        var a = e.target.closest('a[href^="#"]');
        if (!a) return;
        setTimeout(fixHashScroll, 0);
    });

    if (location.hash) {
        setTimeout(fixHashScroll, 0);
    }
});