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
No edit summary
Tags: Mobile edit Mobile web edit
Line 1,571: Line 1,571:
   } else {
   } else {
     initAllTabs(document);
     initAllTabs(document);
  }
})();
(function () {
  /* ================================================================== */
  /* MODULE: Universal Popups (v2)                                      */
  /*  - Popup B aesthetic everywhere                                  */
  /*  - Small = hover/focus (non-click open on hover-capable devices)  */
  /*  - Large = click/tap open, header click closes, outside closes    */
  /*  - Supersedes legacy injected black tips by intercepting on window */
  /* ================================================================== */
  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});
  var POPV2_VERSION = 1;
  if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
  COMMON.popupsV2Init = POPV2_VERSION;
  try {
    document.documentElement.classList.add("sv-uipop-v2");
  } catch (e) {}
  var DEF_SEL = ".sv-def";
  var DEF_TIP_ATTR = "data-sv-def-tip";
  var DEF_LINK_ATTR = "data-sv-def-link";
  var TIP_BTN_SEL = ".sv-tip-btn[data-sv-toggle='1'], details.sv-tip > 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 POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
  var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
  var HOVER_CAPABLE = false;
  try {
    HOVER_CAPABLE =
      !!window.matchMedia &&
      window.matchMedia("(hover:hover) and (pointer:fine)").matches;
  } catch (e) {
    HOVER_CAPABLE = false;
  }
  var popEl = null;
  var state = {
    open: false,
    pinned: false,
    mode: "hover", // hover | click
    size: "sm",    // sm | lg
    trigger: null,
    lastPointerType: "mouse",
    hideTimer: 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 clamp(n, min, max) {
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }
  function safeIdSelector(id) {
    return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
  }
  function ensurePopEl() {
    if (popEl) return popEl;
    var el = document.createElement("div");
    el.className = "sv-uipop sv-uipop--sm";
    el.setAttribute("role", "dialog");
    el.setAttribute("aria-hidden", "true");
    document.body.appendChild(el);
    popEl = el;
    return popEl;
  }
  function setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
  }
  function stripIds(root) {
    if (!root || root.nodeType !== 1) return;
    if (root.hasAttribute && root.hasAttribute("id")) root.removeAttribute("id");
    var nodes = root.querySelectorAll ? root.querySelectorAll("[id]") : [];
    for (var i = 0; i < nodes.length; i++) nodes[i].removeAttribute("id");
  }
  function unhideTree(root) {
    if (!root || root.nodeType !== 1) return;
    if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
    if (root.classList) root.classList.remove("sv-hidden");
    if (root.style && root.style.display === "none") root.style.display = "";
    var kids = root.children || [];
    for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
  }
  function isBlankContent(node) {
    if (!node) return true;
    var t = node.textContent;
    if (t == null) return true;
    var compact = String(t)
      .replace(/\s+/g, "")
      .replace(/[—–-]+/g, "")
      .trim();
    return !compact;
  }
  function getDefTipText(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(DEF_TIP_ATTR);
    if (t == null) return "";
    t = String(t);
    if (!t.replace(/\s+/g, "")) return "";
    return t;
  }
  function getDefLinkTitle(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(DEF_LINK_ATTR);
    if (t == null) return "";
    return String(t).trim();
  }
  function getContentNodeFromTrigger(trigger) {
    if (!trigger) return null;
    if (trigger.tagName === "SUMMARY") {
      var det = closest(trigger, POPUP_DETAILS_SEL);
      if (!det) return null;
      var pop = det.querySelector(POPUP_POP_SEL);
      if (!pop) return null;
      // Prefer body node if present
      var body =
        pop.querySelector(".sv-tip-pop-body") ||
        pop.querySelector(".sv-disclose-pop-body") ||
        pop.querySelector(".sv-disclose-list");
      return body || pop;
    }
    var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
    if (id) {
      var wrap = closest(trigger, POPUP_WRAP_SEL);
      if (wrap) {
        try {
          var local = wrap.querySelector(safeIdSelector(id));
          if (local) {
            var b0 =
              local.querySelector(".sv-tip-pop-body") ||
              local.querySelector(".sv-disclose-pop-body") ||
              local.querySelector(".sv-disclose-list");
            return b0 || local;
          }
        } catch (e) {}
      }
      var global = document.getElementById(id);
      if (global) {
        var b1 =
          global.querySelector(".sv-tip-pop-body") ||
          global.querySelector(".sv-disclose-pop-body") ||
          global.querySelector(".sv-disclose-list");
        return b1 || global;
      }
    }
    var w = closest(trigger, POPUP_WRAP_SEL);
    if (w) {
      var p = w.querySelector(POPUP_POP_SEL);
      if (p) {
        var b2 =
          p.querySelector(".sv-tip-pop-body") ||
          p.querySelector(".sv-disclose-pop-body") ||
          p.querySelector(".sv-disclose-list");
        return b2 || p;
      }
    }
    return null;
  }
  function guessTitle(trigger, sourceNode) {
    // Prefer existing pop title nodes when the source is a pop container
    var pop = null;
    if (sourceNode && sourceNode.nodeType === 1) {
      pop = closest(sourceNode, POPUP_POP_SEL);
    }
    if (pop) {
      var t =
        pop.querySelector(".sv-tip-pop-title") ||
        pop.querySelector(".sv-disclose-pop-title");
      if (t && t.textContent) return String(t.textContent).trim();
    }
    var aria = trigger && trigger.getAttribute ? trigger.getAttribute("aria-label") : "";
    if (aria) return String(aria).trim();
    var txt = trigger && trigger.textContent ? String(trigger.textContent).trim() : "";
    if (txt) return txt.replace(/\s+/g, " ").trim();
    return "Info";
  }
  function buildActionsForDefinition(linkTitle) {
    if (!linkTitle) return null;
    var wrap = document.createElement("div");
    wrap.className = "sv-uipop-actions";
    var a = document.createElement("a");
    a.className = "sv-uipop-action";
    a.textContent = "Open page";
    if (window.mw && mw.util && typeof mw.util.getUrl === "function") {
      a.href = mw.util.getUrl(linkTitle);
    } else {
      a.href = String(linkTitle);
    }
    wrap.appendChild(a);
    return wrap;
  }
  function renderPopup(opts) {
    var el = ensurePopEl();
    if (!el) return;
    el.classList.remove("sv-uipop--sm", "sv-uipop--lg");
    el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm");
    // Clear
    while (el.firstChild) el.removeChild(el.firstChild);
    // Head
    var head = document.createElement("div");
    head.className = "sv-uipop-head" + (opts.mode === "click" ? " sv-uipop-head--clickable" : "");
    var title = document.createElement("div");
    title.className = "sv-uipop-title";
    title.textContent = opts.title || "Info";
    var hint = document.createElement("div");
    hint.className = "sv-uipop-hint";
    hint.textContent = opts.hint || "";
    head.appendChild(title);
    head.appendChild(hint);
    el.appendChild(head);
    // Body
    var body = document.createElement("div");
    body.className = "sv-uipop-body";
    if (opts.bodyPre) body.classList.add("sv-uipop-body--pre");
    if (opts.bodyNode) {
      var clone = opts.bodyNode.cloneNode(true);
      stripIds(clone);
      unhideTree(clone);
      // If the clone is a list, keep it; CSS normalizes list styles inside popup
      body.appendChild(clone);
    } else if (opts.bodyText != null) {
      body.textContent = String(opts.bodyText);
    }
    if (opts.actionsNode) {
      body.appendChild(opts.actionsNode);
    }
    el.appendChild(body);
    // Header close (large/click)
    if (opts.mode === "click") {
      head.addEventListener(
        "click",
        function (e) {
          e.preventDefault();
          hidePopup(true);
        },
        { once: true }
      );
    }
  }
  function positionPopupNearTrigger(trigger) {
    var el = ensurePopEl();
    if (!el || !trigger || !trigger.getBoundingClientRect) return;
    // Reset so we can measure
    el.style.left = "0px";
    el.style.top = "0px";
    // Temporarily show for measurement
    setHidden(el, false);
    requestAnimationFrame(function () {
      if (!state.open) return;
      var vw = window.innerWidth || document.documentElement.clientWidth || 0;
      var vh = window.innerHeight || document.documentElement.clientHeight || 0;
      var margin = 10;
      var gap = 8;
      var tr = trigger.getBoundingClientRect();
      var pr = el.getBoundingClientRect();
      var w = pr.width || 320;
      var h = pr.height || 180;
      // Prefer below, left-aligned; icon triggers align right
      var isIconish = (tr.width || 0) <= 40;
      var left = isIconish ? (tr.right - w) : tr.left;
      var top = tr.bottom + gap;
      // Clamp horizontally
      left = clamp(left, margin, Math.max(margin, vw - w - margin));
      // If bottom overflow, flip above
      if (top + h + margin > vh) {
        top = tr.top - h - gap;
      }
      top = clamp(top, margin, Math.max(margin, vh - h - margin));
      el.style.left = Math.round(left) + "px";
      el.style.top = Math.round(top) + "px";
    });
  }
  function setExpanded(trigger, expanded) {
    if (!trigger || !trigger.setAttribute) return;
    trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
  }
  function hidePopup(fromUser) {
    if (!state.open) return;
    clearTimeout(state.hideTimer);
    state.hideTimer = 0;
    setExpanded(state.trigger, false);
    state.open = false;
    state.pinned = false;
    state.mode = "hover";
    state.size = "sm";
    state.trigger = null;
    var el = ensurePopEl();
    setHidden(el, true);
  }
  function openPopup(trigger, opts) {
    if (!trigger) return;
    if (state.open && state.trigger && state.trigger !== trigger) {
      hidePopup(false);
    }
    state.open = true;
    state.pinned = !!opts.pinned;
    state.mode = opts.mode;
    state.size = opts.size;
    state.trigger = trigger;
    setExpanded(trigger, true);
    renderPopup(opts);
    setHidden(popEl, false);
    positionPopupNearTrigger(trigger);
  }
  function findTrigger(t) {
    if (!t) return null;
    var defEl = closest(t, DEF_SEL);
    if (defEl) return defEl;
    var tip = closest(t, TIP_BTN_SEL);
    if (tip) return tip;
    var dis = closest(t, DISCLOSE_BTN_SEL);
    if (dis) return dis;
    return null;
  }
  function resolveOptsForTrigger(trigger) {
    // Defaults:
    // - .sv-tip-btn => small hover
    // - .sv-disclose-btn => large click
    // - .sv-def => hover if no link, click if link
    var isDef = trigger && trigger.matches && trigger.matches(DEF_SEL);
    var isTip = trigger && trigger.matches && trigger.matches(TIP_BTN_SEL);
    var isDisclose = trigger && trigger.matches && trigger.matches(DISCLOSE_BTN_SEL);
    var mode = "hover";
    var size = "sm";
    // explicit overrides
    var attrMode = trigger.getAttribute && trigger.getAttribute("data-sv-pop");
    if (attrMode === "hover" || attrMode === "click") mode = attrMode;
    var attrSize = trigger.getAttribute && trigger.getAttribute("data-sv-pop-size");
    if (attrSize === "sm" || attrSize === "lg") size = attrSize;
    if (!attrMode) {
      if (isDisclose) mode = "click";
      else if (isTip) mode = "hover";
      else if (isDef) {
        var link = getDefLinkTitle(trigger);
        mode = link ? "click" : "hover";
      }
    }
    if (!attrSize) {
      size = mode === "click" ? "lg" : "sm";
    }
    var sourceNode = null;
    var title = "Info";
    var hint = "";
    if (isDef) {
      var tip = getDefTipText(trigger);
      var linkTitle = getDefLinkTitle(trigger);
      if (!tip) return null;
      title = String(trigger.textContent || "").trim() || "Definition";
      hint =
        mode === "click"
          ? "Click to close"
          : (HOVER_CAPABLE ? "Hover to view" : "Tap to view");
      return {
        mode: mode,
        size: size,
        pinned: mode === "click" ? true : (!HOVER_CAPABLE && state.lastPointerType !== "mouse"),
        title: title,
        hint: hint,
        bodyText: tip,
        bodyPre: true,
        actionsNode: mode === "click" ? buildActionsForDefinition(linkTitle) : null
      };
    }
    sourceNode = getContentNodeFromTrigger(trigger);
    if (isBlankContent(sourceNode)) return null;
    title = guessTitle(trigger, sourceNode);
    hint =
      mode === "click"
        ? "Click to close"
        : (HOVER_CAPABLE ? "Hover to view" : "Tap to view");
    // For hover-small on hover-capable devices: never pinned
    var pinned = mode === "click" ? true : (!HOVER_CAPABLE && state.lastPointerType !== "mouse");
    return {
      mode: mode,
      size: size,
      pinned: pinned,
      title: title,
      hint: hint,
      bodyNode: sourceNode,
      bodyPre: false,
      actionsNode: null
    };
  }
  function isInsidePopup(target) {
    return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
  }
  /* ------------------------------------------------------------------ */
  /* Event Intercepts (window capture) — prevents legacy handlers running */
  /* ------------------------------------------------------------------ */
  window.addEventListener(
    "pointerdown",
    function (e) {
      if (e && e.pointerType) state.lastPointerType = e.pointerType;
      var trigger = findTrigger(e.target);
      if (!trigger) return;
      var opts = resolveOptsForTrigger(trigger);
      if (!opts) return;
      // For hover-mode on hover-capable devices, do not open on click
      if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
        // Let hover handlers manage it; suppress click-open
        e.stopPropagation();
        return;
      }
      e.preventDefault();
      e.stopPropagation();
      // Toggle if already open + pinned + same trigger
      if (state.open && state.pinned && state.trigger === trigger) {
        hidePopup(true);
        return;
      }
      openPopup(trigger, opts);
    },
    true
  );
  // Hover open/close (small)
  window.addEventListener(
    "mouseover",
    function (e) {
      if (!HOVER_CAPABLE) return;
      var trigger = findTrigger(e.target);
      if (!trigger) {
        if (state.open && !state.pinned) hidePopup(false);
        return;
      }
      var opts = resolveOptsForTrigger(trigger);
      if (!opts || opts.mode !== "hover") return;
      e.stopPropagation();
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;
      openPopup(trigger, opts);
    },
    true
  );
  window.addEventListener(
    "mouseout",
    function (e) {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      var trigger = findTrigger(e.target);
      if (!trigger || trigger !== state.trigger) return;
      e.stopPropagation();
      clearTimeout(state.hideTimer);
      state.hideTimer = setTimeout(function () {
        if (state.open && !state.pinned) hidePopup(false);
      }, 60);
    },
    true
  );
  // Click outside closes pinned
  window.addEventListener(
    "click",
    function (e) {
      if (!state.open) return;
      var trigger = findTrigger(e.target);
      // Clicking the trigger is handled by pointerdown toggle
      if (trigger) {
        e.stopPropagation();
        return;
      }
      // Let links inside popup work
      if (isInsidePopup(e.target)) return;
      if (state.pinned) {
        hidePopup(true);
        e.stopPropagation();
      }
    },
    true
  );
  window.addEventListener(
    "keydown",
    function (e) {
      if (e.key !== "Escape") return;
      if (state.open) {
        hidePopup(true);
        e.stopPropagation();
      }
    },
    true
  );
  window.addEventListener(
    "scroll",
    function () {
      if (!state.open || !state.trigger) return;
      positionPopupNearTrigger(state.trigger);
    },
    true
  );
  window.addEventListener(
    "resize",
    function () {
      if (!state.open || !state.trigger) return;
      positionPopupNearTrigger(state.trigger);
    },
    true
  );
  // Optional: remove native browser tooltips on defs if any remain
  function stripDefTitles(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) {
      stripDefTitles($content && $content[0]);
    });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      stripDefTitles(document);
    });
  } else {
    stripDefTitles(document);
   }
   }
})();
})();