Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
No edit summary
Tags: Mobile edit Mobile web edit
No edit summary
Tags: Mobile edit Mobile web edit
Line 1: Line 1:
/* Common.js — Spirit Vale Wiki
/* Common.js — Spirit Vale Wiki
   Phase 4.1 rebuild (incremental)
   Phase 4.1 — Core client scripts


   Included (standalone modules):
   Included (standalone modules):
   1) Definitions (v1)
   1) Level Selector / Descriptions (data-series)
  2) Level Selector / Descriptions (data-series)
   2) Universal Popups (tips, discloses, definitions)
   3) Popups (Phase 4.1 span toggles + legacy <details> support)
   3) Tabs
   4) Tabs (Phase 4.1 key-based; uses .sv-hidden + [hidden])
*/
*/
(function () {
  /* ================================================================== */
  /* MODULE: Definitions (v1)                                          */
  /* ================================================================== */
  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});
  if (COMMON.definitionsInit) return;
  COMMON.definitionsInit = 1;
  var DEF_SEL = ".sv-def";
  var TIP_ATTR = "data-sv-def-tip";
  var LINK_ATTR = "data-sv-def-link";
  var HOVER_CAPABLE = false;
  try {
    HOVER_CAPABLE =
      !!window.matchMedia &&
      window.matchMedia("(hover:hover) and (pointer:fine)").matches;
  } catch (e) {
    HOVER_CAPABLE = false;
  }
  var tipEl = null;
  var state = {
    visible: false,
    pinned: false,
    anchor: null,
    lastX: 0,
    lastY: 0,
    suppressClickUntil: 0,
  };
  function closest(el, sel) {
    if (!el || el.nodeType !== 1) return null;
    if (el.closest) return el.closest(sel);
    while (el && el.nodeType === 1) {
      if (el.matches && el.matches(sel)) return el;
      el = el.parentElement;
    }
    return null;
  }
  function getTipText(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(TIP_ATTR);
    if (t == null) return "";
    t = String(t);
    if (!t.replace(/\s+/g, "")) return "";
    return t;
  }
  function getLinkTitle(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(LINK_ATTR);
    if (t == null) return "";
    return String(t).trim();
  }
  function ensureTipEl() {
    if (tipEl) return tipEl;
    var el = document.createElement("div");
    el.className = "sv-def-tip sv-hidden";
    el.setAttribute("role", "tooltip");
    el.setAttribute("aria-hidden", "true");
    el.style.position = "fixed";
    el.style.zIndex = "99999";
    el.style.maxWidth = "360px";
    el.style.boxSizing = "border-box";
    el.style.padding = "8px 10px";
    el.style.borderRadius = "10px";
    el.style.background = "rgba(10, 14, 22, 0.92)";
    el.style.color = "#fff";
    el.style.fontSize = "14px";
    el.style.lineHeight = "1.25";
    el.style.whiteSpace = "pre-line";
    el.style.pointerEvents = "none";
    el.style.display = "none";
    document.body.appendChild(el);
    tipEl = el;
    return tipEl;
  }
  function setHidden(el, hidden) {
    if (!el) return;
    if (hidden) {
      el.classList.add("sv-hidden");
      el.setAttribute("aria-hidden", "true");
      el.style.display = "none";
    } else {
      el.classList.remove("sv-hidden");
      el.setAttribute("aria-hidden", "false");
      el.style.display = "block";
    }
  }
  function clamp(n, min, max) {
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }
  function positionTipAt(clientX, clientY) {
    var el = ensureTipEl();
    if (!el) return;
    var vw = window.innerWidth || document.documentElement.clientWidth || 0;
    var vh = window.innerHeight || document.documentElement.clientHeight || 0;
    var gapY = state.pinned ? 10 : 16;
    var gapX = 12;
    el.style.left = "0px";
    el.style.top = "0px";
    requestAnimationFrame(function () {
      if (!state.visible) return;
      var r = el.getBoundingClientRect();
      var w = r.width || 240;
      var h = r.height || 60;
      var left = clientX - w / 2;
      var top = clientY - h - gapY;
      left = clamp(left, gapX, Math.max(gapX, vw - w - gapX));
      if (top < gapY) top = clientY + gapY;
      top = clamp(top, gapY, Math.max(gapY, vh - h - gapY));
      el.style.left = Math.round(left) + "px";
      el.style.top = Math.round(top) + "px";
    });
  }
  function showTip(defEl, text, clientX, clientY, pinned) {
    var el = ensureTipEl();
    if (!el) return;
    state.visible = true;
    state.pinned = !!pinned;
    state.anchor = defEl || null;
    state.lastX = clientX || 0;
    state.lastY = clientY || 0;
    el.textContent = text;
    el.style.pointerEvents = state.pinned ? "auto" : "none";
    el.classList.toggle("sv-def-tip--pinned", state.pinned);
    setHidden(el, false);
    positionTipAt(state.lastX, state.lastY);
  }
  function hideTip() {
    if (!tipEl) return;
    state.visible = false;
    state.pinned = false;
    state.anchor = null;
    setHidden(tipEl, true);
  }
  function navigateToTitle(title) {
    if (!title) return;
    if (window.mw && mw.util && typeof mw.util.getUrl === "function") {
      window.location.href = mw.util.getUrl(title);
      return;
    }
    window.location.href = String(title);
  }
  function sameAnchor(el) {
    return !!(el && state.anchor && el === state.anchor);
  }
  function shouldSuppressClick() {
    return Date.now() < state.suppressClickUntil;
  }
  if (HOVER_CAPABLE) {
    document.addEventListener(
      "mouseover",
      function (e) {
        if (state.pinned) return;
        var defEl = closest(e.target, DEF_SEL);
        if (!defEl) {
          if (state.visible && !state.pinned) hideTip();
          return;
        }
        var rel = e.relatedTarget;
        if (rel && defEl.contains(rel)) return;
        var text = getTipText(defEl);
        if (!text) {
          if (state.visible && !state.pinned) hideTip();
          return;
        }
        showTip(defEl, text, e.clientX, e.clientY, false);
      },
      true
    );
    document.addEventListener(
      "mousemove",
      function (e) {
        if (!state.visible || state.pinned) return;
        var defEl = closest(e.target, DEF_SEL);
        if (!defEl || !sameAnchor(defEl)) return;
        state.lastX = e.clientX;
        state.lastY = e.clientY;
        positionTipAt(state.lastX, state.lastY);
      },
      true
    );
    document.addEventListener(
      "mouseout",
      function (e) {
        if (!state.visible || state.pinned) return;
        var defEl = closest(e.target, DEF_SEL);
        if (!defEl || !sameAnchor(defEl)) return;
        var rel = e.relatedTarget;
        if (rel && defEl.contains(rel)) return;
        hideTip();
      },
      true
    );
  }
  document.addEventListener(
    "pointerdown",
    function (e) {
      var defEl = closest(e.target, DEF_SEL);
      if (!defEl) return;
      var text = getTipText(defEl);
      if (!text) {
        if (state.visible) hideTip();
        return;
      }
      var isTouchLike = e.pointerType && e.pointerType !== "mouse";
      if (!isTouchLike) return;
      state.suppressClickUntil = Date.now() + 450;
      var link = getLinkTitle(defEl);
      if (state.pinned && sameAnchor(defEl) && link) {
        e.preventDefault();
        navigateToTitle(link);
        return;
      }
      e.preventDefault();
      showTip(defEl, text, e.clientX, e.clientY, true);
    },
    true
  );
  document.addEventListener(
    "click",
    function (e) {
      if (shouldSuppressClick()) return;
      var defEl = closest(e.target, DEF_SEL);
      if (!defEl) return;
      var text = getTipText(defEl);
      if (!text) {
        if (state.visible) hideTip();
        return;
      }
      var link = getLinkTitle(defEl);
      if (link) {
        e.preventDefault();
        navigateToTitle(link);
        return;
      }
      e.preventDefault();
      if (state.pinned && sameAnchor(defEl)) hideTip();
      else showTip(defEl, text, e.clientX, e.clientY, true);
    },
    true
  );
  document.addEventListener(
    "click",
    function (e) {
      if (!state.visible || !state.pinned) return;
      if (
        tipEl &&
        (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))
      )
        return;
      if (closest(e.target, DEF_SEL)) return;
      hideTip();
    },
    true
  );
  document.addEventListener(
    "keydown",
    function (e) {
      if (e.key !== "Escape") return;
      if (state.visible) hideTip();
    },
    true
  );
  window.addEventListener(
    "scroll",
    function () {
      if (!state.visible) return;
      positionTipAt(state.lastX, state.lastY);
    },
    true
  );
  window.addEventListener(
    "resize",
    function () {
      if (!state.visible) return;
      positionTipAt(state.lastX, state.lastY);
    },
    true
  );
  function initAllDefs(root) {
    var container = root || document;
    var defs = container.querySelectorAll(DEF_SEL + "[title]");
    for (var i = 0; i < defs.length; i++) {
      defs[i].removeAttribute("title");
    }
  }
  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      initAllDefs($content && $content[0]);
    });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllDefs(document);
    });
  } else {
    initAllDefs(document);
  }
})();


(function () {
(function () {
Line 382: Line 16:
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});


   // --------- VERSIONED INIT (fixes "old script blocks new script") -----
   // Versioned init guard (allows safe updates without stale blocks)
   var LEVELS_VERSION = 2;
   var LEVELS_VERSION = 2;
  // If an older Common.js set levelsInit=1, we still want THIS one to run.
   if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return;
   if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return;
   COMMON.levelsInit = LEVELS_VERSION;
   COMMON.levelsInit = LEVELS_VERSION;
Line 390: Line 23:
   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";


   // Be a bit forgiving: accept the custom slider even if the data attr changes.
   // Accept the native range input and the custom slider variants.
   var LEVEL_RANGE_SEL =
   var LEVEL_RANGE_SEL =
     "input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']";
     "input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']";
Line 461: Line 94:
     if (!el || !el.getAttribute) return false;
     if (!el || !el.getAttribute) return false;
     if (el.getAttribute("data-sv-slider") === "1") return true;
     if (el.getAttribute("data-sv-slider") === "1") return true;
    // fallback: class-based detection
     return !!(el.classList && el.classList.contains("sv-level-range--custom"));
     return !!(el.classList && el.classList.contains("sv-level-range--custom"));
   }
   }


   function parseMaxFromUi(card) {
   function parseMaxFromUi(card) {
    // fallback if attributes drift: parse "... / 10" out of the UI text
     var ui = card && card.querySelector ? card.querySelector(".sv-level-ui") : null;
     var ui = card && card.querySelector ? card.querySelector(".sv-level-ui") : null;
     if (!ui) return null;
     if (!ui) return null;
Line 646: Line 277:
       }
       }
     } else if (slider && isCustomSlider(slider)) {
     } else if (slider && isCustomSlider(slider)) {
      // Even if max==1, keep the visual consistent
       setCustomBounds(slider, 1, maxLevel);
       setCustomBounds(slider, 1, maxLevel);
       setCustomValue(slider, level);
       setCustomValue(slider, level);
Line 663: Line 293:
       var card = cards[i];
       var card = cards[i];


      // Versioned per-card init (fixes "old init stamp blocks new logic")
       var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
       var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
       if (stamped >= LEVELS_VERSION) continue;
       if (stamped >= LEVELS_VERSION) continue;
Line 771: Line 400:
       if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") {
       if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") {
         next = cur - 1;
         next = cur - 1;
       } else if (
       } else if (e.key === "ArrowRight" || e.key === "Right" || e.key === "ArrowUp") {
        e.key === "ArrowRight" ||
        e.key === "Right" ||
        e.key === "ArrowUp"
      ) {
         next = cur + 1;
         next = cur + 1;
       } else if (e.key === "Home") {
       } else if (e.key === "Home") {
Line 800: Line 425:
   }
   }


  // Expose for debugging / manual re-init if needed
   COMMON.initAllLevels = initAllLevels;
   COMMON.initAllLevels = initAllLevels;


Line 816: Line 440:
     initAllLevels(document);
     initAllLevels(document);
   }
   }
})();
/* ====================================================================== */
/* LEGACY POPUPS (disabled)                                              */
/*                                                                        */
/* This section is intentionally isolated so it can be deleted in one cut. */
/* To temporarily re-enable legacy behavior, set:                          */
/*  SV.common.enableLegacyPopups = true                                  */
/* ====================================================================== */
(function () {
  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});
  if (!COMMON.enableLegacyPopups) return;
  // Place any legacy popup code blocks here verbatim if needed during transition.
})();
})();


(function () {
(function () {
   /* ================================================================== */
   /* ================================================================== */
   /* MODULE: Universal Popups (v3.1)                                    */
   /* MODULE: Universal Popups                                            */
  /*  - Linked definitions: header title becomes a real wiki link      */
  /*  - No action button row                                           */
  /*  - Fires wikipage.content hook on injected popup for Popups ext    */
   /* ================================================================== */
   /* ================================================================== */


Line 829: Line 467:
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});


   var POP_VERSION = 31; // v3.1
   var UPOP_VERSION = 31; // Universal Popups v3.1
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POP_VERSION) return;
   if (typeof COMMON.universalPopupsInit === "number" && COMMON.universalPopupsInit >= UPOP_VERSION) return;
   COMMON.popupsV2Init = POP_VERSION;
   COMMON.universalPopupsInit = UPOP_VERSION;


   try { document.documentElement.classList.add("sv-uipop-v3"); } catch (e) {}
   try { document.documentElement.classList.add("sv-uipop-v3"); } catch (e) {}
Line 842: Line 480:
   var DISCLOSE_BTN_SEL = ".sv-disclose-btn[data-sv-toggle='1'], details.sv-disclose > summary";
   var DISCLOSE_BTN_SEL = ".sv-disclose-btn[data-sv-toggle='1'], details.sv-disclose > summary";


   var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
   var WRAP_SEL = ".sv-tip, .sv-disclose";
   var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
  var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";


   var HOVER_CAPABLE = false;
   var HOVER_CAPABLE = false;
Line 856: Line 493:
     open: false,
     open: false,
     pinned: false,
     pinned: false,
    mode: "hover",
    size: "sm",
     trigger: null,
     trigger: null,
     hideTimer: 0,
     hideTimer: 0,
Line 883: Line 518:
   function safeIdSelector(id) {
   function safeIdSelector(id) {
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
  }
  function isInsidePopup(target) {
    return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
  }
  function isSummaryTrigger(trigger) {
    return !!(trigger && trigger.tagName === "SUMMARY");
   }
   }


Line 892: Line 535:
     el.setAttribute("role", "dialog");
     el.setAttribute("role", "dialog");
     el.setAttribute("aria-hidden", "true");
     el.setAttribute("aria-hidden", "true");
    document.body.appendChild(el);
    el.addEventListener("mouseenter", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;
    });
    el.addEventListener("mouseleave", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = setTimeout(function () {
        if (state.open && !state.pinned) hidePopup();
      }, 80);
    });


    document.body.appendChild(el);
     popEl = el;
     popEl = el;
     return popEl;
     return popEl;
Line 912: Line 571:
   function unhideTree(root) {
   function unhideTree(root) {
     if (!root || root.nodeType !== 1) return;
     if (!root || root.nodeType !== 1) return;
     if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
     if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
     if (root.classList) root.classList.remove("sv-hidden");
     if (root.classList) root.classList.remove("sv-hidden");
     if (root.style && root.style.display === "none") root.style.display = "";
     if (root.style && root.style.display === "none") root.style.display = "";
     var kids = root.children || [];
     var kids = root.children || [];
     for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
     for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
Line 942: Line 603:
     if (t == null) return "";
     if (t == null) return "";
     return String(t).trim();
     return String(t).trim();
  }
  function isInsidePopup(target) {
    return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
  }
  function isSummaryTrigger(trigger) {
    return !!(trigger && trigger.tagName === "SUMMARY");
   }
   }


Line 956: Line 609:


     if (trigger.tagName === "SUMMARY") {
     if (trigger.tagName === "SUMMARY") {
       var det = closest(trigger, POPUP_DETAILS_SEL);
       var det = closest(trigger, "details.sv-tip, details.sv-disclose");
       if (!det) return null;
       if (!det) return null;
       var pop = det.querySelector(POPUP_POP_SEL);
       var pop0 = det.querySelector(POP_SEL);
       if (!pop) return null;
       if (!pop0) return null;
       var body =
       return (
         pop.querySelector(".sv-tip-pop-body") ||
         pop0.querySelector(".sv-tip-pop-body") ||
         pop.querySelector(".sv-disclose-pop-body") ||
         pop0.querySelector(".sv-disclose-pop-body") ||
         pop.querySelector(".sv-disclose-list");
         pop0.querySelector(".sv-disclose-list") ||
       return body || pop;
        pop0
       );
     }
     }


     var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
     var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
     if (id) {
     if (id) {
       var wrap = closest(trigger, POPUP_WRAP_SEL);
       var wrap = closest(trigger, WRAP_SEL);
       if (wrap) {
       if (wrap) {
         try {
         try {
           var local = wrap.querySelector(safeIdSelector(id));
           var local = wrap.querySelector(safeIdSelector(id));
           if (local) {
           if (local) {
             var b0 =
             return (
               local.querySelector(".sv-tip-pop-body") ||
               local.querySelector(".sv-tip-pop-body") ||
               local.querySelector(".sv-disclose-pop-body") ||
               local.querySelector(".sv-disclose-pop-body") ||
               local.querySelector(".sv-disclose-list");
               local.querySelector(".sv-disclose-list") ||
            return b0 || local;
              local
            );
           }
           }
         } catch (e) {}
         } catch (e) {}
Line 985: Line 640:
       var global = document.getElementById(id);
       var global = document.getElementById(id);
       if (global) {
       if (global) {
         var b1 =
         return (
           global.querySelector(".sv-tip-pop-body") ||
           global.querySelector(".sv-tip-pop-body") ||
           global.querySelector(".sv-disclose-pop-body") ||
           global.querySelector(".sv-disclose-pop-body") ||
           global.querySelector(".sv-disclose-list");
           global.querySelector(".sv-disclose-list") ||
        return b1 || global;
          global
        );
       }
       }
     }
     }


     var w = closest(trigger, POPUP_WRAP_SEL);
     var w = closest(trigger, WRAP_SEL);
     if (w) {
     if (w) {
       var p = w.querySelector(POPUP_POP_SEL);
       var pop = w.querySelector(POP_SEL);
       if (p) {
       if (pop) {
         var b2 =
         return (
           p.querySelector(".sv-tip-pop-body") ||
           pop.querySelector(".sv-tip-pop-body") ||
           p.querySelector(".sv-disclose-pop-body") ||
           pop.querySelector(".sv-disclose-pop-body") ||
           p.querySelector(".sv-disclose-list");
           pop.querySelector(".sv-disclose-list") ||
         return b2 || p;
          pop
         );
       }
       }
     }
     }
Line 1,010: Line 667:
   function guessTitle(trigger, sourceNode) {
   function guessTitle(trigger, sourceNode) {
     var pop = null;
     var pop = null;
     if (sourceNode && sourceNode.nodeType === 1) pop = closest(sourceNode, POPUP_POP_SEL);
     if (sourceNode && sourceNode.nodeType === 1) pop = closest(sourceNode, POP_SEL);
     if (pop) {
     if (pop) {
       var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
       var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
       if (t && t.textContent) return String(t.textContent).trim();
       if (t && t.textContent) return String(t.textContent).trim();
     }
     }
     var aria = trigger && trigger.getAttribute ? trigger.getAttribute("aria-label") : "";
     var aria = trigger && trigger.getAttribute ? trigger.getAttribute("aria-label") : "";
     if (aria) return String(aria).trim();
     if (aria) return String(aria).trim();
     var txt = trigger && trigger.textContent ? String(trigger.textContent).trim() : "";
     var txt = trigger && trigger.textContent ? String(trigger.textContent).trim() : "";
     if (txt) return txt.replace(/\s+/g, " ").trim();
     if (txt) return txt.replace(/\s+/g, " ").trim();
     return "Info";
     return "Info";
   }
   }


   function buildHeaderLinkForTitle(linkTitle, displayText) {
   function buildHeaderLink(linkTitle, displayText) {
     if (!linkTitle) return null;
     if (!linkTitle) return null;


Line 1,032: Line 692:
     else a.href = String(linkTitle);
     else a.href = String(linkTitle);


    // Prevent the header click-to-close handler from swallowing link clicks
     a.addEventListener("click", function (e) { e.stopPropagation(); });
     a.addEventListener("click", function (e) { e.stopPropagation(); });


Line 1,055: Line 714:


     var head = document.createElement("div");
     var head = document.createElement("div");
     head.className = "sv-uipop-head" + (opts.mode === "click" ? " sv-uipop-head--clickable" : "");
     head.className = "sv-uipop-head" + (opts.pinned ? " sv-uipop-head--clickable" : "");


     var title = document.createElement("div");
     var title = document.createElement("div");
Line 1,080: Line 739:
     el.appendChild(body);
     el.appendChild(body);


     if (opts.mode === "click") {
     if (opts.pinned) {
       head.addEventListener("click", function (e) {
       head.addEventListener("click", function (e) {
         e.preventDefault();
         e.preventDefault();
         hidePopup(true);
         hidePopup();
       }, { once: true });
       }, { once: true });
     }
     }


    // Let Popups/Page Previews bind to links inside the injected popup
     fireContentHook(el);
     fireContentHook(el);
   }
   }
Line 1,155: Line 813:
     state.open = false;
     state.open = false;
     state.pinned = false;
     state.pinned = false;
    state.mode = "hover";
    state.size = "sm";
     state.trigger = null;
     state.trigger = null;
     state.anchorKind = "trigger";
     state.anchorKind = "trigger";


     var el = ensurePopEl();
     setHidden(ensurePopEl(), true);
    setHidden(el, true);
   }
   }


Line 1,167: Line 822:
     if (!trigger) return;
     if (!trigger) return;


     if (state.open && state.trigger && state.trigger !== trigger) hidePopup(false);
     if (state.open && state.trigger && state.trigger !== trigger) hidePopup();


     state.open = true;
     state.open = true;
     state.pinned = !!opts.pinned;
     state.pinned = !!opts.pinned;
    state.mode = opts.mode;
    state.size = opts.size;
     state.trigger = trigger;
     state.trigger = trigger;


Line 1,187: Line 840:


     var defEl = closest(t, DEF_SEL);
     var defEl = closest(t, DEF_SEL);
     if (defEl) return defEl;
     if (defEl && defEl.getAttribute && defEl.getAttribute(DEF_TIP_ATTR) != null) return defEl;


     var tip = closest(t, TIP_BTN_SEL);
     var tip = closest(t, TIP_BTN_SEL);
Line 1,198: Line 851:
   }
   }


   function resolveOptsForTrigger(trigger) {
   function resolveOpts(trigger) {
     var isDef = trigger && trigger.matches && trigger.matches(DEF_SEL);
     var isDef = trigger && trigger.matches && trigger.matches(DEF_SEL);
     var isTip = trigger && trigger.matches && trigger.matches(TIP_BTN_SEL);
     var isTip = trigger && trigger.matches && trigger.matches(TIP_BTN_SEL);
Line 1,215: Line 868:
       if (isDisclose) mode = "click";
       if (isDisclose) mode = "click";
       else if (isTip) mode = "hover";
       else if (isTip) mode = "hover";
       else if (isDef) {
       else if (isDef) mode = getDefLinkTitle(trigger) ? "click" : "hover";
        var link = getDefLinkTitle(trigger);
        mode = link ? "click" : "hover";
      }
     }
     }


Line 1,236: Line 886:


       return {
       return {
         mode: mode,
         pinned: (mode === "click"),
         size: size,
         size: size,
        pinned: (mode === "click"),
         title: titleText,
         title: titleText,
         titleLink: linkTitle ? buildHeaderLinkForTitle(linkTitle, titleText) : null,
         titleLink: linkTitle ? buildHeaderLink(linkTitle, titleText) : null,
         bodyText: tipText,
         bodyText: tipText,
         bodyPre: true
         bodyPre: true,
        bodyNode: null
       };
       };
     }
     }


     var sourceNode = getContentNodeFromTrigger(trigger);
     var source = getContentNodeFromTrigger(trigger);
     if (isBlankContent(sourceNode)) return null;
     if (isBlankContent(source)) return null;


     return {
     return {
       mode: mode,
       pinned: (mode === "click"),
       size: size,
       size: size,
      pinned: (mode === "click"),
       title: guessTitle(trigger, source),
       title: guessTitle(trigger, sourceNode),
       titleLink: null,
       titleLink: null,
       bodyNode: sourceNode,
       bodyNode: source,
       bodyPre: false
       bodyPre: false,
      bodyText: null
     };
     };
   }
   }
Line 1,262: Line 912:
   window.addEventListener("pointerdown", function (e) {
   window.addEventListener("pointerdown", function (e) {
     if (!e) return;
     if (!e) return;
     if (isInsidePopup(e.target)) return;
     if (isInsidePopup(e.target)) return;


Line 1,267: Line 918:
     if (!trigger) return;
     if (!trigger) return;


     var opts = resolveOptsForTrigger(trigger);
     var opts = resolveOpts(trigger);
     if (!opts) return;
     if (!opts) return;


Line 1,276: Line 927:
     state.anchorY = (typeof e.clientY === "number") ? e.clientY : 0;
     state.anchorY = (typeof e.clientY === "number") ? e.clientY : 0;


     if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
     if (HOVER_CAPABLE && !opts.pinned && (e.pointerType === "mouse" || !e.pointerType)) {
       e.stopPropagation();
       e.stopPropagation();
       return;
       return;
Line 1,285: Line 936:


     if (state.open && state.pinned && state.trigger === trigger) {
     if (state.open && state.pinned && state.trigger === trigger) {
       hidePopup(true);
       hidePopup();
       return;
       return;
     }
     }
Line 1,298: Line 949:
     var trigger = findTrigger(e.target);
     var trigger = findTrigger(e.target);
     if (!trigger) {
     if (!trigger) {
       if (state.open && !state.pinned) hidePopup(false);
       if (state.open && !state.pinned) hidePopup();
       return;
       return;
     }
     }


     var opts = resolveOptsForTrigger(trigger);
     var opts = resolveOpts(trigger);
     if (!opts || opts.mode !== "hover") return;
     if (!opts || opts.pinned) return;


     state.anchorKind = "trigger";
     state.anchorKind = "trigger";
     e.stopPropagation();
     e.stopPropagation();
     clearTimeout(state.hideTimer);
     clearTimeout(state.hideTimer);
Line 1,321: Line 973:


     e.stopPropagation();
     e.stopPropagation();
     clearTimeout(state.hideTimer);
     clearTimeout(state.hideTimer);
     state.hideTimer = setTimeout(function () {
     state.hideTimer = setTimeout(function () {
       if (state.open && !state.pinned) hidePopup(false);
       if (state.open && !state.pinned) hidePopup();
     }, 60);
     }, 80);
   }, true);
   }, true);


Line 1,338: Line 991:


     if (state.pinned || !HOVER_CAPABLE) {
     if (state.pinned || !HOVER_CAPABLE) {
       hidePopup(true);
       hidePopup();
       e.stopPropagation();
       e.stopPropagation();
     }
     }
Line 1,346: Line 999:
     if (e.key !== "Escape") return;
     if (e.key !== "Escape") return;
     if (state.open) {
     if (state.open) {
       hidePopup(true);
       hidePopup();
       e.stopPropagation();
       e.stopPropagation();
     }
     }
Line 1,377: Line 1,030:
   } else {
   } else {
     stripDefTitles(document);
     stripDefTitles(document);
  }
})();
(function () {
  /* ================================================================== */
  /* MODULE: Tabs                                                      */
  /* ================================================================== */
  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});
  if (COMMON.tabsInit) return;
  COMMON.tabsInit = 1;
  var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
  var TAB_SEL = ".sv-tab";
  var PANEL_SEL = ".sv-tabpanel";
  var _tabsUidCounter = 0;
  function closest(el, sel) {
    if (!el || el.nodeType !== 1) return null;
    if (el.closest) return el.closest(sel);
    while (el && el.nodeType === 1) {
      if (el.matches && el.matches(sel)) return el;
      el = el.parentElement;
    }
    return null;
  }
  function getTabs(root) {
    return root.querySelectorAll(TAB_SEL);
  }
  function getPanels(root) {
    return root.querySelectorAll(PANEL_SEL);
  }
  function getPanelByKey(root, key) {
    var panels = getPanels(root);
    for (var i = 0; i < panels.length; i++) {
      if (panels[i].getAttribute("data-panel") === key) return panels[i];
    }
    return null;
  }
  function ensureUniqueTabIds(root) {
    var uid = root.getAttribute("data-tabs-uid");
    if (!uid) {
      _tabsUidCounter++;
      uid = (root.getAttribute("data-tabs-root") || "sv-tabs") + "-" + _tabsUidCounter;
      root.setAttribute("data-tabs-uid", uid);
    }
    var tabs = getTabs(root);
    for (var i = 0; i < tabs.length; i++) {
      var tab = tabs[i];
      var key = tab.getAttribute("data-tab") || String(i + 1);
      var panel = getPanelByKey(root, key);
      var tabId = uid + "-tab-" + key;
      tab.id = tabId;
      if (panel) {
        var panelId = uid + "-panel-" + key;
        panel.id = panelId;
        tab.setAttribute("aria-controls", panelId);
        panel.setAttribute("aria-labelledby", tabId);
      }
    }
  }
  function showPanel(panel) {
    if (!panel) return;
    panel.setAttribute("data-active", "1");
    if (panel.classList) panel.classList.remove("sv-hidden");
    panel.removeAttribute("hidden");
  }
  function hidePanel(panel) {
    if (!panel) return;
    panel.setAttribute("data-active", "0");
    if (panel.classList) panel.classList.add("sv-hidden");
    panel.setAttribute("hidden", "hidden");
  }
  function activateTab(root, btn) {
    if (!root || !btn) return;
    var tabs = getTabs(root);
    var key = btn.getAttribute("data-tab") || "";
    for (var i = 0; i < tabs.length; i++) {
      var t = tabs[i];
      var active = t === btn;
      t.setAttribute("aria-selected", active ? "true" : "false");
      t.setAttribute("tabindex", active ? "0" : "-1");
      if (t.classList) t.classList.toggle("sv-tab--active", active);
    }
    var panels = getPanels(root);
    for (var j = 0; j < panels.length; j++) {
      var p = panels[j];
      var isTarget = p.getAttribute("data-panel") === key;
      if (isTarget) showPanel(p);
      else hidePanel(p);
    }
    root.setAttribute("data-active-tab", key);
  }
  function focusTab(tabs, idx) {
    if (idx < 0) idx = 0;
    if (idx >= tabs.length) idx = tabs.length - 1;
    var t = tabs[idx];
    if (t && t.focus) t.focus();
    return t;
  }
  function indexOfTab(tabs, btn) {
    for (var i = 0; i < tabs.length; i++) {
      if (tabs[i] === btn) return i;
    }
    return -1;
  }
  function normalizeTabsRoot(root) {
    if (!root) return;
    if (root.getAttribute("data-gi-tabs-init") === "1") return;
    ensureUniqueTabIds(root);
    var tabs = getTabs(root);
    var panels = getPanels(root);
    if (!tabs.length || !panels.length) {
      root.setAttribute("data-gi-tabs-init", "1");
      return;
    }
    var activeBtn = null;
    for (var i = 0; i < tabs.length; i++) {
      if (tabs[i].getAttribute("aria-selected") === "true") {
        activeBtn = tabs[i];
        break;
      }
    }
    if (!activeBtn) activeBtn = tabs[0];
    activateTab(root, activeBtn);
    root.setAttribute("data-gi-tabs-init", "1");
  }
  function initTabs(root) {
    var container = root || document;
    var roots = container.querySelectorAll(TABS_ROOT_SEL);
    for (var i = 0; i < roots.length; i++) normalizeTabsRoot(roots[i]);
  }
  document.addEventListener(
    "click",
    function (e) {
      var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
      if (!btn) return;
      var root = closest(btn, ".sv-tabs");
      if (!root || root.getAttribute("data-tabs") !== "1") return;
      e.preventDefault();
      activateTab(root, btn);
    },
    true
  );
  document.addEventListener(
    "keydown",
    function (e) {
      var btn =
        document.activeElement && document.activeElement.closest
          ? document.activeElement.closest(TAB_SEL)
          : null;
      if (!btn) return;
      var root = closest(btn, ".sv-tabs");
      if (!root || root.getAttribute("data-tabs") !== "1") return;
      var tabs = getTabs(root);
      if (!tabs || !tabs.length) return;
      var idx = indexOfTab(tabs, btn);
      if (idx < 0) return;
      if (e.key === "ArrowLeft" || e.key === "Left") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, idx - 1));
        return;
      }
      if (e.key === "ArrowRight" || e.key === "Right") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, idx + 1));
        return;
      }
      if (e.key === "Home") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, 0));
        return;
      }
      if (e.key === "End") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, tabs.length - 1));
        return;
      }
      if (e.key === "Enter" || e.key === " ") {
        e.preventDefault();
        activateTab(root, btn);
        return;
      }
    },
    true
  );
  function initAllTabs(root) {
    initTabs(root);
  }
  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      initAllTabs($content && $content[0]);
    });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllTabs(document);
    });
  } else {
    initAllTabs(document);
   }
   }
})();
})();