MediaWiki:Common.js

From HADES
Revision as of 00:52, 15 April 2026 by Possession (talk | contribs) (Created page with "/** * ╔═══════════════════════════════════════════════════════════════╗ * ║ HADES WIKI — MediaWiki Theme JavaScript ║ * ║ The Free Encyclopedia of the Underworld ║ * ║ Based on: Hades (2026) by Melanie Martinez ║ * ║ Install in: MediaWiki:Common.js...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * ╔═══════════════════════════════════════════════════════════════╗
 * ║         HADES WIKI — MediaWiki Theme JavaScript               ║
 * ║     The Free Encyclopedia of the Underworld                   ║
 * ║     Based on: Hades (2026) by Melanie Martinez                ║
 * ║     Install in: MediaWiki:Common.js                           ║
 * ╚═══════════════════════════════════════════════════════════════╝
 */

(function () {
  'use strict';

  /* ─────────────────────────────────────────
     CONFIGURATION
  ───────────────────────────────────────── */
  const HADES_CONFIG = {
    candleCount: 4,
    particleCount: 15,
    particleColors: ['#c8922a', '#ff8800', '#ffcc44', '#ff6600', '#ffd700'],
    goldColor: '#c8922a',
    pageFadeInDelay: 100,
    navHighlightDelay: 200,
    siteSubtitle: 'The Free Encyclopedia of the Underworld',
  };

  /* ─────────────────────────────────────────
     UTILITY: Wait for DOM element
  ───────────────────────────────────────── */
  function waitFor(selector, callback, timeout = 5000) {
    const el = document.querySelector(selector);
    if (el) return callback(el);
    const start = Date.now();
    const interval = setInterval(() => {
      const found = document.querySelector(selector);
      if (found || Date.now() - start > timeout) {
        clearInterval(interval);
        if (found) callback(found);
      }
    }, 100);
  }

  /* ─────────────────────────────────────────
     1. ANIMATED CANDLES IN HEADER
  ───────────────────────────────────────── */
  function injectCandles() {
    const header = document.querySelector('#mw-head, .mw-header, .vector-header');
    if (!header) return;

    // Remove existing candle container if present
    const existing = document.getElementById('hades-candles');
    if (existing) existing.remove();

    const container = document.createElement('div');
    container.id = 'hades-candles';
    container.style.cssText = `
      display: flex;
      align-items: flex-end;
      gap: 6px;
      padding: 8px 16px;
      position: absolute;
      right: 80px;
      top: 50%;
      transform: translateY(-50%);
      z-index: 50;
      pointer-events: none;
    `;

    const candleHeights = [50, 75, 60, 42];
    const candleDelays  = [0.3, 0, 0.6, 1.0];

    candleHeights.forEach((height, i) => {
      const candleEl = createCandleElement(height, candleDelays[i]);
      container.appendChild(candleEl);
    });

    header.style.position = 'relative';
    header.appendChild(container);
  }

  function createCandleElement(height, delay) {
    const wrapper = document.createElement('div');
    wrapper.style.cssText = 'display:flex;flex-direction:column;align-items:center;position:relative;';

    // Smoke
    const smoke = document.createElement('div');
    smoke.style.cssText = `
      position:absolute;
      bottom:${height + 36}px;
      left:50%;
      width:4px;
      height:10px;
      background:rgba(200,180,150,0.25);
      border-radius:50%;
      transform:translateX(-50%);
      animation:hadesSmoke ${2 + delay * 0.5}s ${delay * 0.3}s ease-out infinite;
    `;
    wrapper.appendChild(smoke);

    // Outer flame
    const outerFlame = document.createElement('div');
    outerFlame.style.cssText = `
      width:14px;
      height:22px;
      background:radial-gradient(ellipse at 50% 80%,#ff5500 0%,#ff9900 40%,#ffcc00 70%,rgba(255,220,100,0) 100%);
      border-radius:50% 50% 30% 30%/60% 60% 40% 40%;
      animation:hadesCandle ${1.4 + delay * 0.3}s ${delay * 0.2}s ease-in-out infinite;
      transform-origin:bottom center;
      filter:blur(0.5px);
      margin-bottom:0;
    `;
    wrapper.appendChild(outerFlame);

    // Inner flame
    const innerFlame = document.createElement('div');
    innerFlame.style.cssText = `
      width:5px;
      height:13px;
      background:radial-gradient(ellipse at 50% 80%,#fff 0%,#ffe066 50%,rgba(255,200,0,0) 100%);
      border-radius:50% 50% 30% 30%/60% 60% 40% 40%;
      animation:hadesCandle ${1.1 + delay * 0.2}s ${delay * 0.15}s ease-in-out infinite;
      transform-origin:bottom center;
      margin-top:-13px;
      align-self:center;
    `;
    wrapper.appendChild(innerFlame);

    // Glow orb
    const glow = document.createElement('div');
    glow.style.cssText = `
      width:22px;
      height:22px;
      border-radius:50%;
      background:rgba(255,160,50,0.1);
      animation:hadesGlowOrb ${2.2 + delay * 0.4}s ${delay * 0.1}s ease-in-out infinite;
      position:absolute;
      top:0;
      left:50%;
      transform:translateX(-50%);
    `;
    wrapper.appendChild(glow);

    // Wick
    const wick = document.createElement('div');
    wick.style.cssText = 'width:2px;height:5px;background:#1a0800;margin-bottom:-1px;position:relative;z-index:2;';
    wrapper.appendChild(wick);

    // Wax body
    const waxColors = ['#d4c4a0', '#e8d8b0', '#ddd0a8', '#c8b890'];
    const wax = document.createElement('div');
    wax.style.cssText = `
      width:16px;
      height:${height}px;
      background:linear-gradient(to right,${waxColors[Math.floor(Math.random() * waxColors.length)]}bb 0%,${waxColors[0]} 35%,${waxColors[0]} 70%,${waxColors[0]}88 100%);
      border-radius:2px 2px 3px 3px;
      position:relative;
      box-shadow:inset -3px 0 6px rgba(0,0,0,0.2),inset 3px 0 3px rgba(255,255,255,0.08);
    `;

    // Wax drip 1
    const drip1 = document.createElement('div');
    drip1.style.cssText = `
      position:absolute;top:0;left:30%;width:4px;
      background:inherit;border-radius:0 0 3px 3px;
      animation:hadesWaxDrip ${4 + delay}s ${delay + 0.5}s ease-in-out infinite alternate;
      height:10px;opacity:0.6;
    `;
    wax.appendChild(drip1);

    // Wax drip 2
    const drip2 = document.createElement('div');
    drip2.style.cssText = `
      position:absolute;top:0;right:25%;width:3px;
      background:inherit;border-radius:0 0 3px 3px;
      animation:hadesWaxDrip ${5 + delay}s ${delay + 1.5}s ease-in-out infinite alternate;
      height:7px;opacity:0.5;
    `;
    wax.appendChild(drip2);

    wrapper.appendChild(wax);
    return wrapper;
  }

  /* ─────────────────────────────────────────
     2. INJECT EXTRA CSS KEYFRAMES (JS version)
  ───────────────────────────────────────── */
  function injectExtraStyles() {
    const style = document.createElement('style');
    style.id = 'hades-js-styles';
    style.textContent = `
      @keyframes hadesSmoke {
        0%   { opacity: 0.4; transform: translateX(-50%) translateY(0) scaleX(1); }
        50%  { opacity: 0.2; transform: translateX(-50%) translateY(-16px) scaleX(1.5); }
        100% { opacity: 0;   transform: translateX(-50%) translateY(-32px) scaleX(2); }
      }
      @keyframes hadesGlowOrb {
        0%,100% { box-shadow:0 0 8px 3px rgba(255,160,50,0.4),0 0 20px 8px rgba(200,100,20,0.2); opacity:0.8; }
        50%     { box-shadow:0 0 14px 6px rgba(255,190,70,0.7),0 0 35px 14px rgba(210,130,35,0.4); opacity:1; }
      }
      @keyframes hadesWaxDrip {
        0%   { height:0px; opacity:0.8; }
        100% { height:18px; opacity:0.3; }
      }
      @keyframes hadesParticleAnim {
        0%   { transform:translateY(0) translateX(0) rotate(0deg) scale(1); opacity:0.9; }
        50%  { transform:translateY(-50vh) translateX(30px) rotate(180deg) scale(0.5); opacity:0.4; }
        100% { transform:translateY(-100vh) translateX(-20px) rotate(360deg) scale(0.1); opacity:0; }
      }
      @keyframes hadesPageReveal {
        from { opacity:0; transform:translateY(16px); }
        to   { opacity:1; transform:translateY(0); }
      }
      @keyframes hadesNavLine {
        from { width:0; left:50%; }
        to   { width:100%; left:0; }
      }
      @keyframes hadesTitlePulse {
        0%,100% { filter:brightness(1); }
        50%     { filter:brightness(1.15) drop-shadow(0 0 8px rgba(200,146,42,0.5)); }
      }
      @keyframes hadesLinkShimmer {
        0%   { background-position:-200% center; }
        100% { background-position:200% center; }
      }
      @keyframes hadesScrollIndicator {
        0%,100% { opacity:0.4; transform:translateY(0); }
        50%     { opacity:1; transform:translateY(4px); }
      }
      @keyframes hadesCrownFloat {
        0%,100% { transform:translateY(0) rotate(-2deg); filter:drop-shadow(0 0 6px rgba(200,146,42,0.5)); }
        50%     { transform:translateY(-5px) rotate(2deg); filter:drop-shadow(0 0 12px rgba(200,146,42,0.8)); }
      }
      @keyframes hadesSubtitleReveal {
        from { opacity:0; letter-spacing:8px; }
        to   { opacity:1; letter-spacing:5px; }
      }

      /* Hover glow for wiki links */
      .hades-link-glow {
        position:relative;
        transition:all 0.3s ease !important;
      }
      .hades-link-glow:hover {
        text-shadow:0 0 12px rgba(200,146,42,0.6) !important;
      }

      /* Section reveal animation */
      .hades-section-reveal {
        animation:hadesPageReveal 0.6s ease forwards;
        opacity:0;
      }

      /* Candle hover effect */
      #hades-candles:hover {
        filter:brightness(1.2);
      }

      /* Ornament spin */
      .hades-spinning-ornament {
        display:inline-block;
        animation:hadesOrnamentSpin 8s linear infinite;
      }

      /* Progress bar shimmer */
      .hades-progress {
        height:2px;
        background:linear-gradient(to right, transparent, #c8922a, transparent);
        position:fixed;
        top:0;
        left:0;
        z-index:9999;
        transition:width 0.3s ease;
      }
    `;
    document.head.appendChild(style);
  }

  /* ─────────────────────────────────────────
     3. FLOATING EMBER PARTICLES
  ───────────────────────────────────────── */
  function createParticles() {
    const container = document.createElement('div');
    container.id = 'hades-particles';
    container.style.cssText = `
      position:fixed;
      inset:0;
      pointer-events:none;
      z-index:1;
      overflow:hidden;
    `;

    for (let i = 0; i < HADES_CONFIG.particleCount; i++) {
      const p = document.createElement('div');
      const size = 1.5 + Math.random() * 2.5;
      const color = HADES_CONFIG.particleColors[Math.floor(Math.random() * HADES_CONFIG.particleColors.length)];
      const left = Math.random() * 100;
      const delay = Math.random() * 8;
      const duration = 6 + Math.random() * 8;

      p.style.cssText = `
        position:absolute;
        bottom:0;
        left:${left}%;
        width:${size}px;
        height:${size}px;
        border-radius:50%;
        background:radial-gradient(circle, ${color} 0%, rgba(200,100,20,0.2) 70%, transparent 100%);
        animation:hadesParticleAnim ${duration}s ${delay}s ease-out infinite;
        opacity:0;
      `;
      container.appendChild(p);
    }

    document.body.prepend(container);
  }

  /* ─────────────────────────────────────────
     4. SITE TITLE ENHANCEMENT
  ───────────────────────────────────────── */
  function enhanceSiteTitle() {
    // Add animated crown to logo area
    const logo = document.querySelector('#p-logo, .mw-logo, #mw-head-base');
    if (logo) {
      const crown = document.createElement('div');
      crown.style.cssText = `
        font-size:28px;
        text-align:center;
        animation:hadesCrownFloat 3s ease-in-out infinite;
        filter:drop-shadow(0 0 8px rgba(200,146,42,0.6));
        margin-bottom:4px;
        display:block;
        color:#c8922a;
      `;
      crown.textContent = '♛';
      logo.prepend(crown);
    }

    // Animate site subtitle
    const subtitle = document.querySelector('#p-logo a[title], .mw-wiki-title');
    if (subtitle) {
      const sub = document.createElement('div');
      sub.textContent = HADES_CONFIG.siteSubtitle;
      sub.style.cssText = `
        font-family:'Cinzel',serif;
        font-size:8px;
        letter-spacing:5px;
        color:#8b4a20;
        text-transform:uppercase;
        text-align:center;
        animation:hadesSubtitleReveal 2s ease forwards;
        opacity:0;
        margin-top:2px;
      `;
      subtitle.parentNode.insertBefore(sub, subtitle.nextSibling);
    }
  }

  /* ─────────────────────────────────────────
     5. PAGE CONTENT ANIMATIONS
  ───────────────────────────────────────── */
  function animatePageContent() {
    const content = document.querySelector('#mw-content-text, .mw-body, #content');
    if (content) {
      content.style.animation = 'hadesPageReveal 0.8s 0.1s ease forwards';
      content.style.opacity = '0';
    }

    // Animate each heading with stagger
    const headings = document.querySelectorAll('h2, h3');
    headings.forEach((h, i) => {
      h.style.animation = `hadesPageReveal 0.6s ${0.1 + i * 0.08}s ease forwards`;
      h.style.opacity = '0';
    });

    // Animate infobox
    const infobox = document.querySelector('.infobox');
    if (infobox) {
      infobox.style.animation = 'hadesPageReveal 0.8s 0.3s ease forwards';
      infobox.style.opacity = '0';
    }
  }

  /* ─────────────────────────────────────────
     6. ENHANCED LINK INTERACTIONS
  ───────────────────────────────────────── */
  function enhanceLinks() {
    // Add ripple effect to all article links
    document.querySelectorAll('#mw-content-text a, .mw-body a').forEach(link => {
      link.classList.add('hades-link-glow');

      link.addEventListener('mouseenter', function () {
        this.style.transition = 'all 0.2s ease';
      });

      link.addEventListener('click', function (e) {
        const ripple = document.createElement('span');
        ripple.style.cssText = `
          position:absolute;
          width:12px;
          height:12px;
          background:rgba(200,146,42,0.6);
          border-radius:50%;
          pointer-events:none;
          animation:hadesRipple 0.5s ease forwards;
          transform:translate(-50%,-50%);
          top:${e.offsetY}px;
          left:${e.offsetX}px;
          z-index:9;
        `;
        this.style.position = 'relative';
        this.style.overflow = 'hidden';
        this.appendChild(ripple);
        setTimeout(() => ripple.remove(), 500);
      });
    });
  }

  /* ─────────────────────────────────────────
     7. NAVIGATION ACTIVE INDICATOR
  ───────────────────────────────────────── */
  function enhanceNavigation() {
    const navLinks = document.querySelectorAll(
      '#p-namespaces ul li a, #p-views ul li a, .vector-menu-tabs ul li a'
    );

    navLinks.forEach(link => {
      link.addEventListener('mouseenter', function () {
        const indicator = document.createElement('div');
        indicator.style.cssText = `
          position:absolute;
          bottom:0;
          left:0;
          height:2px;
          background:linear-gradient(to right, transparent, #c8922a, transparent);
          animation:hadesNavLine 0.25s ease forwards;
          pointer-events:none;
        `;
        this.style.position = 'relative';
        this.appendChild(indicator);
      });

      link.addEventListener('mouseleave', function () {
        const indicator = this.querySelector('div[style*="hadesNavLine"]');
        if (indicator) indicator.remove();
      });
    });
  }

  /* ─────────────────────────────────────────
     8. SCROLL PROGRESS BAR
  ───────────────────────────────────────── */
  function createScrollProgressBar() {
    const bar = document.createElement('div');
    bar.className = 'hades-progress';
    bar.style.width = '0%';
    document.body.appendChild(bar);

    window.addEventListener('scroll', () => {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
      bar.style.width = progress + '%';
    }, { passive: true });
  }

  /* ─────────────────────────────────────────
     9. TABLE ENHANCEMENTS
  ───────────────────────────────────────── */
  function enhanceTables() {
    document.querySelectorAll('.wikitable tr').forEach((row, i) => {
      row.addEventListener('mouseenter', function () {
        this.style.background = 'rgba(200,146,42,0.08)';
        this.style.transition = 'background 0.2s ease';
        this.querySelectorAll('td').forEach(td => {
          td.style.color = '#d4c4a0';
          td.style.transition = 'color 0.2s ease';
        });
      });
      row.addEventListener('mouseleave', function () {
        this.style.background = '';
        this.querySelectorAll('td').forEach(td => { td.style.color = ''; });
      });
    });
  }

  /* ─────────────────────────────────────────
     10. SEARCH BAR ENHANCEMENTS
  ───────────────────────────────────────── */
  function enhanceSearch() {
    const searchInput = document.querySelector('#searchInput, .vector-search-box input[type="search"]');
    if (!searchInput) return;

    searchInput.placeholder = 'Search HADES Wiki...';

    searchInput.addEventListener('focus', function () {
      this.parentElement.style.boxShadow = '0 0 0 3px rgba(200,146,42,0.2), 0 4px 20px rgba(0,0,0,0.5)';
      this.style.borderColor = '#c8922a';
    });

    searchInput.addEventListener('blur', function () {
      this.parentElement.style.boxShadow = '';
      this.style.borderColor = '';
    });

    // Add search icon label
    const label = document.createElement('span');
    label.textContent = '🔍';
    label.style.cssText = `
      position:absolute;
      left:12px;
      top:50%;
      transform:translateY(-50%);
      pointer-events:none;
      font-size:14px;
      z-index:5;
    `;
    const searchWrapper = searchInput.parentElement;
    if (searchWrapper) {
      searchWrapper.style.position = 'relative';
      searchWrapper.prepend(label);
    }
  }

  /* ─────────────────────────────────────────
     11. FIRST HEADING SUBTITLE
  ───────────────────────────────────────── */
  function enhanceFirstHeading() {
    const h1 = document.querySelector('#firstHeading, h1.firstHeading');
    if (!h1) return;

    // Add decorative ornaments around h1
    h1.innerHTML = h1.innerHTML;

    const sub = document.createElement('div');
    sub.style.cssText = `
      font-family:'IM Fell English',serif;
      font-style:italic;
      font-size:13px;
      color:#8b7a50;
      margin-top:4px;
      letter-spacing:0.5px;
    `;

    // Add page metadata
    const pageName = h1.textContent.trim();
    const ns = mw.config.get('wgNamespaceNumber');
    if (ns === 0 && pageName) {
      // Main article namespace
      sub.textContent = `Article · Hades Wiki`;
      h1.parentNode.insertBefore(sub, h1.nextSibling);
    }
  }

  /* ─────────────────────────────────────────
     12. FOOTER ENHANCEMENT
  ───────────────────────────────────────── */
  function enhanceFooter() {
    const footer = document.querySelector('#footer, .mw-footer');
    if (!footer) return;

    const logo = document.createElement('div');
    logo.style.cssText = `
      font-family:'Cinzel Decorative',serif;
      font-size:16px;
      color:#8b6914;
      letter-spacing:4px;
      margin-bottom:12px;
      text-align:center;
      animation:hadesTitlePulse 4s ease-in-out infinite;
    `;
    logo.textContent = '⚜ HADES WIKI ⚜';
    footer.prepend(logo);

    const divider = document.createElement('div');
    divider.style.cssText = `
      height:1px;
      background:linear-gradient(to right, transparent, #c8922a, transparent);
      margin:12px 0;
      opacity:0.4;
    `;
    footer.prepend(divider);
  }

  /* ─────────────────────────────────────────
     13. IMAGE LIGHTBOX (simple)
  ───────────────────────────────────────── */
  function initLightbox() {
    document.querySelectorAll('.thumbimage img, .infobox img').forEach(img => {
      img.style.cursor = 'zoom-in';
      img.addEventListener('click', function (e) {
        e.preventDefault();

        const overlay = document.createElement('div');
        overlay.style.cssText = `
          position:fixed;
          inset:0;
          background:rgba(0,0,0,0.92);
          z-index:99999;
          display:flex;
          align-items:center;
          justify-content:center;
          cursor:zoom-out;
          animation:hadesPageReveal 0.3s ease forwards;
        `;

        const clone = this.cloneNode();
        clone.style.cssText = `
          max-width:90vw;
          max-height:90vh;
          object-fit:contain;
          border:1px solid rgba(200,146,42,0.4);
          box-shadow:0 0 60px rgba(200,146,42,0.2),0 0 120px rgba(0,0,0,0.8);
        `;

        const closeBtn = document.createElement('div');
        closeBtn.textContent = '✕';
        closeBtn.style.cssText = `
          position:absolute;
          top:20px;
          right:24px;
          color:#c8922a;
          font-size:24px;
          cursor:pointer;
          font-family:Cinzel,serif;
          letter-spacing:2px;
          opacity:0.8;
          transition:opacity 0.2s;
        `;
        closeBtn.addEventListener('mouseenter', () => closeBtn.style.opacity = '1');

        const close = () => {
          overlay.style.opacity = '0';
          overlay.style.transition = 'opacity 0.3s ease';
          setTimeout(() => overlay.remove(), 300);
        };

        overlay.addEventListener('click', close);
        closeBtn.addEventListener('click', close);
        overlay.appendChild(clone);
        overlay.appendChild(closeBtn);
        document.body.appendChild(overlay);
      });
    });
  }

  /* ─────────────────────────────────────────
     14. MAIN EXECUTION (mw.loader.using)
  ───────────────────────────────────────── */
  function init() {
    injectExtraStyles();
    createParticles();
    injectCandles();
    createScrollProgressBar();
    animatePageContent();
    enhanceLinks();
    enhanceNavigation();
    enhanceTables();
    enhanceSearch();
    enhanceFooter();
    initLightbox();

    // Slight delay for heading enhancement (needs mw object)
    setTimeout(() => {
      try { enhanceFirstHeading(); } catch (e) { /* not critical */ }
    }, 500);

    console.log('%c⚜ HADES WIKI THEME LOADED ⚜', 'color:#c8922a;font-family:Cinzel,serif;font-size:14px;letter-spacing:3px;');
  }

  // Use MediaWiki's jQuery.ready or plain DOMContentLoaded
  if (typeof jQuery !== 'undefined' && typeof mw !== 'undefined') {
    mw.loader.using(['mediawiki.util'], function () {
      jQuery(document).ready(function () {
        init();
      });
    });
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }

})();