MediaWiki:Common.js: Difference between revisions

From UO Renaissance Wiki
Jump to navigation Jump to search
Reposition scroll to top button outside main box and above navbar
Position scroll to top button next to main container and above navbar
 
(One intermediate revision by the same user not shown)
Line 211: Line 211:
(function() {
(function() {
   function initScrollToTop() {
   function initScrollToTop() {
     if (document.getElementById('uor-scroll-top')) return;
     var existing = document.getElementById('uor-scroll-top');
    if (existing) existing.remove();


     var btn = document.createElement('a');
     var btn = document.createElement('a');
     btn.id = 'uor-scroll-top';
     btn.id = 'uor-scroll-top';
     btn.className = 'uor-scroll-top-btn';
     btn.className = 'uor-scroll-top-btn';
     btn.href = '#';
     btn.href = '#top';
     btn.title = 'Scroll to Top';
     btn.title = 'Scroll to Top';
     btn.setAttribute('aria-label', 'Scroll to top of page');
     btn.setAttribute('aria-label', 'Scroll to top of page');
Line 232: Line 233:


     function repositionButton() {
     function repositionButton() {
      // Find main content container
       var mainBox = document.getElementById('content') || document.querySelector('.mw-body');
       var mainBox = document.querySelector('#content') || document.querySelector('.mw-body') || document.querySelector('#mw-content-text');
       if (mainBox) {
      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();
         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 spaceRight = window.innerWidth - rect.right;
         var spaceLeft = rect.left;
         // If there's at least 54px room to the right of the container, stick right beside it
 
         if (spaceRight >= 54) {
        if (spaceRight >= 56) {
           btn.style.left = Math.round(rect.right + 10) + 'px';
          // 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';
           btn.style.right = 'auto';
         } else {
         } else {
          // Narrow viewport / mobile: dock to right screen edge
           btn.style.left = 'auto';
           btn.style.left = 'auto';
           btn.style.right = '20px';
           btn.style.right = '16px';
         }
         }
       } else {
       } else {
         btn.style.left = 'auto';
         btn.style.left = 'auto';
         btn.style.right = '20px';
         btn.style.right = '16px';
       }
       }
     }
     }


     var ticking = false;
     var ticking = false;
     function updateVisibility() {
     function updateState() {
       var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
       var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
       if (scrollY > 280) {
       if (scrollY > 220) {
         btn.classList.add('is-visible');
         btn.classList.add('is-visible');
        repositionButton();
       } else {
       } else {
         btn.classList.remove('is-visible');
         btn.classList.remove('is-visible');
       }
       }
      repositionButton();
       ticking = false;
       ticking = false;
     }
     }
Line 285: Line 266:
     window.addEventListener('scroll', function() {
     window.addEventListener('scroll', function() {
       if (!ticking) {
       if (!ticking) {
         window.requestAnimationFrame(updateVisibility);
         window.requestAnimationFrame(updateState);
         ticking = true;
         ticking = true;
       }
       }
     }, { passive: true });
     }, { passive: true });


     window.addEventListener('resize', repositionButton);
     window.addEventListener('resize', function() {
      repositionButton();
    }, { passive: true });


     updateVisibility();
     // Initial check
    updateState();
    setTimeout(repositionButton, 200);
    setTimeout(repositionButton, 1000);
   }
   }



Latest revision as of 18:28, 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() {
    var existing = document.getElementById('uor-scroll-top');
    if (existing) existing.remove();

    var btn = document.createElement('a');
    btn.id = 'uor-scroll-top';
    btn.className = 'uor-scroll-top-btn';
    btn.href = '#top';
    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() {
      var mainBox = document.getElementById('content') || document.querySelector('.mw-body');
      if (mainBox) {
        var rect = mainBox.getBoundingClientRect();
        var spaceRight = window.innerWidth - rect.right;
        // If there's at least 54px room to the right of the container, stick right beside it
        if (spaceRight >= 54) {
          btn.style.left = Math.round(rect.right + 10) + 'px';
          btn.style.right = 'auto';
        } else {
          // Narrow viewport / mobile: dock to right screen edge
          btn.style.left = 'auto';
          btn.style.right = '16px';
        }
      } else {
        btn.style.left = 'auto';
        btn.style.right = '16px';
      }
    }

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

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

    window.addEventListener('resize', function() {
      repositionButton();
    }, { passive: true });

    // Initial check
    updateState();
    setTimeout(repositionButton, 200);
    setTimeout(repositionButton, 1000);
  }

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