MediaWiki:Common.js: Difference between revisions

From UO Renaissance Wiki
Jump to navigation Jump to search
Add floating scroll to top button across all wiki pages
Reposition scroll to top button outside main box and above navbar
Line 230: Line 230:


     document.body.appendChild(btn);
     document.body.appendChild(btn);
    function repositionButton() {
      // Find main content container
      var mainBox = document.querySelector('#content') || document.querySelector('.mw-body') || document.querySelector('#mw-content-text');
      var footer = document.querySelector('#footer') || document.querySelector('#mw-footer');
     
      // Compute bottom offset to ensure it's comfortably above footer / bottom navbar
      var bottomOffset = 56;
      if (footer) {
        var fRect = footer.getBoundingClientRect();
        if (fRect.top < window.innerHeight) {
          bottomOffset = Math.max(56, window.innerHeight - fRect.top + 16);
        }
      }
      btn.style.bottom = bottomOffset + 'px';
      // Position horizontally: outside the main box
      if (window.innerWidth > 960 && mainBox) {
        var rect = mainBox.getBoundingClientRect();
        // Check if there is space outside the left or right of the main box
        var spaceRight = window.innerWidth - rect.right;
        var spaceLeft = rect.left;
        if (spaceRight >= 56) {
          // Sit just outside the right edge of the main box
          btn.style.left = (rect.right + 12) + 'px';
          btn.style.right = 'auto';
        } else if (spaceLeft >= 56) {
          // If squeezed on right, sit just outside the left edge
          btn.style.left = Math.max(16, rect.left - 54) + 'px';
          btn.style.right = 'auto';
        } else {
          btn.style.left = 'auto';
          btn.style.right = '20px';
        }
      } else {
        btn.style.left = 'auto';
        btn.style.right = '20px';
      }
    }


     var ticking = false;
     var ticking = false;
     function updateVisibility() {
     function updateVisibility() {
       var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
       var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
       if (scrollY > 300) {
       if (scrollY > 280) {
         btn.classList.add('is-visible');
         btn.classList.add('is-visible');
        repositionButton();
       } else {
       } else {
         btn.classList.remove('is-visible');
         btn.classList.remove('is-visible');
Line 248: Line 289:
       }
       }
     }, { passive: true });
     }, { passive: true });
    window.addEventListener('resize', repositionButton);


     updateVisibility();
     updateVisibility();

Revision as of 18:15, 9 September 2026

/* UO Renaissance Wiki Common.js */
(function() {
  var compendiumPortal = document.getElementById('p-Compendium');
  if (compendiumPortal) {
    function attachSidebarSubmenu(liSelector, titleMatch, items) {
      var li = compendiumPortal.querySelector(liSelector) ||
               compendiumPortal.querySelector('a[href*="' + titleMatch + '"]');
      if (li && li.tagName.toLowerCase() === 'a') {
        li = li.closest('li');
      }
      if (!li || li.querySelector('.uor-sidebar-submenu')) return;

      li.classList.add('uor-sidebar-has-submenu');

      var link = li.querySelector(':scope > a');
      if (link && !link.querySelector('.uor-submenu-arrow')) {
        var arrow = document.createElement('span');
        arrow.className = 'uor-submenu-arrow';
        arrow.innerHTML = '&#9656;';
        arrow.title = 'View Sections';
        arrow.addEventListener('click', function(e) {
          e.preventDefault();
          e.stopPropagation();
          li.classList.toggle('is-open');
        });
        link.appendChild(arrow);
      }

      var submenu = document.createElement('ul');
      submenu.className = 'uor-sidebar-submenu';
      var html = '';
      for (var i = 0; i < items.length; i++) {
        html += '<li><a href="' + items[i].href + '">' + items[i].label + '</a></li>';
      }
      submenu.innerHTML = html;
      li.appendChild(submenu);
    }

    // 1. The World Submenu
    attachSidebarSubmenu('li#n-The-World', '/wiki/index.php/The_World', [
      { label: 'Towns', href: '/wiki/index.php/The_World#Towns' },
      { label: 'Dungeons', href: '/wiki/index.php/The_World#Dungeons' },
      { label: 'Points of Interest', href: '/wiki/index.php/The_World#Points_of_Interest' }
    ]);

    // 2. Skills Submenu
    attachSidebarSubmenu('li#n-Skills', '/wiki/index.php/Skills', [
      { label: 'Combat', href: '/wiki/index.php/Skills#Combat' },
      { label: 'Crafting', href: '/wiki/index.php/Skills#Crafting' },
      { label: 'Creatures', href: '/wiki/index.php/Skills#Creatures' },
      { label: 'Lore & Knowledge', href: '/wiki/index.php/Skills#Lore_&_Knowledge' },
      { label: 'Miscellaneous', href: '/wiki/index.php/Skills#Miscellaneous' },
      { label: 'Rogue', href: '/wiki/index.php/Skills#Rogue' },
      { label: 'Spells & Magic', href: '/wiki/index.php/Spells' }
    ]);

    // 3. Factions & Guilds Submenu
    attachSidebarSubmenu('li[id*="Factions"], li#n-Factions', '/wiki/index.php/Factions', [
      { label: 'Factions', href: '/wiki/index.php/Factions_%26_Guilds#Factions' },
      { label: 'Order & Chaos', href: '/wiki/index.php/Factions_%26_Guilds#Order_&_Chaos' },
      { label: 'Player run Guilds', href: '/wiki/index.php/Factions_%26_Guilds#Player_run_Guilds' }
    ]);

    // 4. Champion Spawns Submenu
    attachSidebarSubmenu('li#n-Champion-Spawns', '/wiki/index.php/Champion_Spawns', [
      { label: 'Overview & Altars', href: '/wiki/index.php/Champion_Spawns#Altars' },
      { label: 'The Six Champions', href: '/wiki/index.php/Champion_Spawns#Champions' },
      { label: 'The Harrower', href: '/wiki/index.php/Champion_Spawns#The_Harrower' },
      { label: 'Rewards & Titles', href: '/wiki/index.php/Champion_Spawns#Rewards' }
    ]);

    // 5. Bestiary Submenu
    attachSidebarSubmenu('li#n-Bestiary', '/wiki/index.php/Bestiary', [
      { label: 'Bosses & Champions', href: '/wiki/index.php/Bestiary#Bosses_and_Champions' },
      { label: 'Daemons & Elementals', href: '/wiki/index.php/Bestiary#Daemons_and_Elementals' },
      { label: 'Reptiles & Monstrosities', href: '/wiki/index.php/Bestiary#Reptiles_and_Monstrosities' },
      { label: 'Undead & Tomb Horrors', href: '/wiki/index.php/Bestiary#Undead_and_Horrors' },
      { label: 'Humanoids, Tribes & Beasts', href: '/wiki/index.php/Bestiary#Humanoids_and_Beasts' },
      { label: 'Holiday & Event Specials', href: '/wiki/index.php/Bestiary#Holiday_and_Events' }
    ]);

    // 6. Housing & Vendors Submenu
    attachSidebarSubmenu('li[id*="Housing"], li#n-Housing-Vendors', 'Housing', [
      { label: 'Ownership Rules', href: '/wiki/index.php/Housing_System#Home_Ownership_Fundamentals' },
      { label: 'House Catalog', href: '/wiki/index.php/Housing_System#Complete_House_Catalog' },
      { label: 'Security & Tactics', href: '/wiki/index.php/Housing_System#House_Security_&_Fortification' },
      { label: 'Classic Vendors', href: '/wiki/index.php/Housing_System#Classic_Player_Vendors' },
      { label: 'Enhanced Vendors', href: '/wiki/index.php/Housing_System#Enhanced_Vendor_System' },
      { label: 'Quick Reference', href: '/wiki/index.php/Housing_System#Housing_&_Vendor_Quick_Reference' }
    ]);

    // 7. Murder & Bounties Submenu
    attachSidebarSubmenu('li[id*="Murder"], li#n-Murder-Bounties', 'Murder', [
      { label: 'Murder Counts', href: '/wiki/index.php/The_Murder_System#Understanding_Murder_Counts' },
      { label: 'Stat Loss Formula', href: '/wiki/index.php/The_Murder_System#Stat_Loss_Formula_&_Penalties' },
      { label: 'Reporting & Bounties', href: '/wiki/index.php/The_Murder_System#Reporting_a_Murder_&_Placing_Bounties' },
      { label: 'The Bounty Board', href: '/wiki/index.php/The_Murder_System#The_Bounty_Board_System' },
      { label: 'Claiming Heads', href: '/wiki/index.php/The_Murder_System#Claiming_Bounties_&_Taking_Heads' },
      { label: 'Quick Reference', href: '/wiki/index.php/The_Murder_System#Murder_&_Bounty_Quick_Reference' }
    ]);

    // 8. Patch Notes Archive Submenu
    attachSidebarSubmenu('li#n-Patch-Notes-Archive', '/wiki/index.php/Patch_Notes_Archive', [
      { label: 'Era 1: Alpha & Beta (2012)', href: '/wiki/index.php/Patch_Notes_Archive#Era_1:_Alpha_.26_Beta_Foundations_.28May_2012_-_Sept_2012.29' },
      { label: 'Era 2: Launch & Maturation (2012-2014)', href: '/wiki/index.php/Patch_Notes_Archive#Era_2:_Shard_Launch_.26_Server_Maturation_.28Sept_2012_-_Sept_2014.29' },
      { label: 'Era 3: Golden Age (2014-2016)', href: '/wiki/index.php/Patch_Notes_Archive#Era_3:_The_Golden_Age_.26_Expansions_.28Oct_2014_-_Oct_2016.29' },
      { label: 'Era 4: Veteran & Modern (2016-2020)', href: '/wiki/index.php/Patch_Notes_Archive#Era_4:_Veteran_Shard_.26_Modernization_.28Nov_2016_-_June_2020.29' }
    ]);

    // 9. Unique Shard Systems Submenu
    var uniqueSystemsItems = [
      { label: "The Zookeeper's Quest", href: "/wiki/index.php/The_Zookeeper's_Quest" },
      { label: "Ancient MIB's", href: "/wiki/index.php/Ancient_MIB's" }
    ];

    function normalizeSystemUrl(url) {
      if (!url) return '';
      try {
        return decodeURIComponent(url).replace(/\/index\.php\//, '/index.php/').replace(/\/+$/, '').toLowerCase();
      } catch(e) {
        return url.toLowerCase();
      }
    }

    function isExcludedOrDuplicate(item, list) {
      if (!item || !item.href) return true;
      var nHref = normalizeSystemUrl(item.href);
      var nLbl = (item.label || '').trim().toLowerCase();
      // Exclude the parent page itself
      if (nHref.indexOf('unique_systems') !== -1 || nLbl === 'unique shard systems' || nLbl === 'unique systems') {
        return true;
      }
      return list.some(function(existing) {
        return normalizeSystemUrl(existing.href) === nHref ||
               (existing.label && existing.label.trim().toLowerCase() === nLbl);
      });
    }

    // Automatically incorporate any additional unique systems stored in localStorage
    try {
      var storedUnique = localStorage.getItem('uor_unique_shard_systems');
      if (storedUnique) {
        var parsedUnique = JSON.parse(storedUnique);
        if (Array.isArray(parsedUnique)) {
          var filteredStorage = [];
          parsedUnique.forEach(function(item) {
            if (!isExcludedOrDuplicate(item, uniqueSystemsItems)) {
              uniqueSystemsItems.push(item);
              filteredStorage.push(item);
            }
          });
          // Update localStorage to strip any invalid/duplicate entries
          localStorage.setItem('uor_unique_shard_systems', JSON.stringify(filteredStorage));
        }
      }
    } catch (e) {}

    // Check if the DOM already has sub-items (e.g. from MediaWiki:Sidebar)
    var uniqueLi = compendiumPortal.querySelector('li[id*="Unique"], li#n-Unique-Shard-Systems') ||
                   compendiumPortal.querySelector('a[href*="/wiki/index.php/Unique_Systems"]');
    if (uniqueLi) {
      if (uniqueLi.tagName.toLowerCase() === 'a') uniqueLi = uniqueLi.closest('li');
      if (uniqueLi) {
        var domSubLinks = uniqueLi.querySelectorAll('ul li a');
        domSubLinks.forEach(function(sl) {
          var lbl = sl.textContent.trim();
          var hrf = sl.getAttribute('href');
          var candidate = { label: lbl, href: hrf };
          if (!isExcludedOrDuplicate(candidate, uniqueSystemsItems)) {
            uniqueSystemsItems.push(candidate);
          }
        });
      }
    }

    attachSidebarSubmenu('li[id*="Unique"], li#n-Unique-Shard-Systems', '/wiki/index.php/Unique_Systems', uniqueSystemsItems);

    // Sync any custom unique system links dynamically from the Unique Systems hub page
    if (window.location.pathname.indexOf('/Unique_Systems') !== -1) {
      try {
        var pageLinks = document.querySelectorAll('.uor-unique-system-link, [data-shard-system]');
        if (pageLinks.length > 0) {
          var collected = [];
          pageLinks.forEach(function(pl) {
            var plLabel = pl.getAttribute('data-shard-system') || pl.textContent.trim();
            var plHref = pl.getAttribute('href');
            var candidate = { label: plLabel, href: plHref };
            if (!isExcludedOrDuplicate(candidate, collected) && !isExcludedOrDuplicate(candidate, uniqueSystemsItems)) {
              collected.push(candidate);
            }
          });
          if (collected.length > 0) {
            localStorage.setItem('uor_unique_shard_systems', JSON.stringify(collected));
          }
        }
      } catch (e) {}
    }

    document.addEventListener('click', function(e) {
      if (!e.target.closest('.uor-sidebar-has-submenu')) {
        var openMenus = document.querySelectorAll('.uor-sidebar-has-submenu.is-open');
        for (var i = 0; i < openMenus.length; i++) {
          openMenus[i].classList.remove('is-open');
        }
      }
    });
  }
})();

/* Floating Scroll to Top Arrow Button */
(function() {
  function initScrollToTop() {
    if (document.getElementById('uor-scroll-top')) return;

    var btn = document.createElement('a');
    btn.id = 'uor-scroll-top';
    btn.className = 'uor-scroll-top-btn';
    btn.href = '#';
    btn.title = 'Scroll to Top';
    btn.setAttribute('aria-label', 'Scroll to top of page');
    btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 4l-8 8h5v8h6v-8h5z"/></svg>';

    btn.addEventListener('click', function(e) {
      e.preventDefault();
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    });

    document.body.appendChild(btn);

    function repositionButton() {
      // Find main content container
      var mainBox = document.querySelector('#content') || document.querySelector('.mw-body') || document.querySelector('#mw-content-text');
      var footer = document.querySelector('#footer') || document.querySelector('#mw-footer');
      
      // Compute bottom offset to ensure it's comfortably above footer / bottom navbar
      var bottomOffset = 56;
      if (footer) {
        var fRect = footer.getBoundingClientRect();
        if (fRect.top < window.innerHeight) {
          bottomOffset = Math.max(56, window.innerHeight - fRect.top + 16);
        }
      }
      btn.style.bottom = bottomOffset + 'px';

      // Position horizontally: outside the main box
      if (window.innerWidth > 960 && mainBox) {
        var rect = mainBox.getBoundingClientRect();
        // Check if there is space outside the left or right of the main box
        var spaceRight = window.innerWidth - rect.right;
        var spaceLeft = rect.left;

        if (spaceRight >= 56) {
          // Sit just outside the right edge of the main box
          btn.style.left = (rect.right + 12) + 'px';
          btn.style.right = 'auto';
        } else if (spaceLeft >= 56) {
          // If squeezed on right, sit just outside the left edge
          btn.style.left = Math.max(16, rect.left - 54) + 'px';
          btn.style.right = 'auto';
        } else {
          btn.style.left = 'auto';
          btn.style.right = '20px';
        }
      } else {
        btn.style.left = 'auto';
        btn.style.right = '20px';
      }
    }

    var ticking = false;
    function updateVisibility() {
      var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
      if (scrollY > 280) {
        btn.classList.add('is-visible');
        repositionButton();
      } else {
        btn.classList.remove('is-visible');
      }
      ticking = false;
    }

    window.addEventListener('scroll', function() {
      if (!ticking) {
        window.requestAnimationFrame(updateVisibility);
        ticking = true;
      }
    }, { passive: true });

    window.addEventListener('resize', repositionButton);

    updateVisibility();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initScrollToTop);
  } else {
    initScrollToTop();
  }
})();