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: Manual revert Mobile edit Mobile web edit
No edit summary
Tags: Mobile edit Mobile web edit
Line 1,576: Line 1,576:
(function () {
(function () {
   /* ================================================================== */
   /* ================================================================== */
   /* MODULE: Universal Popups (v2)                                      */
   /* MODULE: Universal Popups (v3)                                      */
   /*  - Popup B aesthetic everywhere                                  */
   /*  - Compact width; popups grow vertically                          */
   /*  - Small = hover/focus (non-click open on hover-capable devices)  */
   /*  - Header centered + bigger; NO hint text                          */
   /*  - Large = click/tap open, header click closes, outside closes    */
   /*  - Mobile/touch: popup appears ABOVE finger, centered to viewport  */
   /*  - Supersedes legacy injected black tips by intercepting on window */
   /*  - Desktop hover: anchored to trigger                              */
   /* ================================================================== */
   /* ================================================================== */


Line 1,586: Line 1,586:
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});


   var POPV2_VERSION = 1;
   var POP_VERSION = 3;
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POP_VERSION) return;
   COMMON.popupsV2Init = POPV2_VERSION;
   COMMON.popupsV2Init = POP_VERSION;


   try {
   try { document.documentElement.classList.add("sv-uipop-v3"); } catch (e) {}
    document.documentElement.classList.add("sv-uipop-v2");
  } catch (e) {}


   var DEF_SEL = ".sv-def";
   var DEF_SEL = ".sv-def";
Line 1,607: Line 1,605:
   var HOVER_CAPABLE = false;
   var HOVER_CAPABLE = false;
   try {
   try {
     HOVER_CAPABLE =
     HOVER_CAPABLE = !!window.matchMedia && window.matchMedia("(hover:hover) and (pointer:fine)").matches;
      !!window.matchMedia &&
   } catch (e) { HOVER_CAPABLE = false; }
      window.matchMedia("(hover:hover) and (pointer:fine)").matches;
   } catch (e) {
    HOVER_CAPABLE = false;
  }


   var popEl = null;
   var popEl = null;
Line 1,623: Line 1,617:
     trigger: null,
     trigger: null,
     lastPointerType: "mouse",
     lastPointerType: "mouse",
     hideTimer: 0
     hideTimer: 0,
 
    // anchor info for positioning
    anchorKind: "trigger", // trigger | finger | center
    anchorX: 0,
    anchorY: 0
   };
   };


Line 1,686: Line 1,685:
     var t = node.textContent;
     var t = node.textContent;
     if (t == null) return true;
     if (t == null) return true;
 
     var compact = String(t).replace(/\s+/g, "").replace(/[—–-]+/g, "").trim();
     var compact = String(t)
      .replace(/\s+/g, "")
      .replace(/[—–-]+/g, "")
      .trim();
 
     return !compact;
     return !compact;
   }
   }
Line 1,700: Line 1,694:
     if (t == null) return "";
     if (t == null) return "";
     t = String(t);
     t = String(t);
    // Convert literal "\n" sequences into real newlines for pre-line rendering
    t = t.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n");
     if (!t.replace(/\s+/g, "")) return "";
     if (!t.replace(/\s+/g, "")) return "";
     return t;
     return t;
Line 1,709: Line 1,707:
     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 1,721: Line 1,727:
       if (!pop) return null;
       if (!pop) return null;


      // Prefer body node if present
       var body =
       var body =
         pop.querySelector(".sv-tip-pop-body") ||
         pop.querySelector(".sv-tip-pop-body") ||
Line 1,772: Line 1,777:


   function guessTitle(trigger, sourceNode) {
   function guessTitle(trigger, sourceNode) {
    // Prefer existing pop title nodes when the source is a pop container
     var pop = null;
     var pop = null;
     if (sourceNode && sourceNode.nodeType === 1) {
     if (sourceNode && sourceNode.nodeType === 1) pop = closest(sourceNode, POPUP_POP_SEL);
      pop = closest(sourceNode, POPUP_POP_SEL);
    }
     if (pop) {
     if (pop) {
       var t =
       var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
        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();
     }
     }
Line 1,803: Line 1,803:
     a.textContent = "Open page";
     a.textContent = "Open page";


     if (window.mw && mw.util && typeof mw.util.getUrl === "function") {
     if (window.mw && mw.util && typeof mw.util.getUrl === "function") a.href = mw.util.getUrl(linkTitle);
      a.href = mw.util.getUrl(linkTitle);
     else a.href = String(linkTitle);
     } else {
      a.href = String(linkTitle);
    }


     wrap.appendChild(a);
     wrap.appendChild(a);
Line 1,820: Line 1,817:
     el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm");
     el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm");


    // Clear
     while (el.firstChild) el.removeChild(el.firstChild);
     while (el.firstChild) el.removeChild(el.firstChild);


    // Head
     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.mode === "click" ? " sv-uipop-head--clickable" : "");
Line 1,830: Line 1,825:
     title.className = "sv-uipop-title";
     title.className = "sv-uipop-title";
     title.textContent = opts.title || "Info";
     title.textContent = opts.title || "Info";
    var hint = document.createElement("div");
    hint.className = "sv-uipop-hint";
    hint.textContent = opts.hint || "";


     head.appendChild(title);
     head.appendChild(title);
    head.appendChild(hint);
     el.appendChild(head);
     el.appendChild(head);


    // Body
     var body = document.createElement("div");
     var body = document.createElement("div");
     body.className = "sv-uipop-body";
     body.className = "sv-uipop-body";
     if (opts.bodyPre) body.classList.add("sv-uipop-body--pre");
     if (opts.bodyPre) body.classList.add("sv-uipop-body--pre");


Line 1,849: Line 1,837:
       stripIds(clone);
       stripIds(clone);
       unhideTree(clone);
       unhideTree(clone);
      // If the clone is a list, keep it; CSS normalizes list styles inside popup
       body.appendChild(clone);
       body.appendChild(clone);
     } else if (opts.bodyText != null) {
     } else if (opts.bodyText != null) {
Line 1,856: Line 1,842:
     }
     }


     if (opts.actionsNode) {
     if (opts.actionsNode) body.appendChild(opts.actionsNode);
      body.appendChild(opts.actionsNode);
    }
 
     el.appendChild(body);
     el.appendChild(body);


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


   function positionPopupNearTrigger(trigger) {
   function positionPopup(trigger) {
     var el = ensurePopEl();
     var el = ensurePopEl();
     if (!el || !trigger || !trigger.getBoundingClientRect) return;
     if (!el) return;


    // Reset so we can measure
     el.style.left = "0px";
     el.style.left = "0px";
     el.style.top = "0px";
     el.style.top = "0px";
    // Temporarily show for measurement
     setHidden(el, false);
     setHidden(el, false);


Line 1,892: Line 1,867:
       var vh = window.innerHeight || document.documentElement.clientHeight || 0;
       var vh = window.innerHeight || document.documentElement.clientHeight || 0;
       var margin = 10;
       var margin = 10;
       var gap = 8;
       var gap = 12;


      var tr = trigger.getBoundingClientRect();
       var pr = el.getBoundingClientRect();
       var pr = el.getBoundingClientRect();
      var w = pr.width || 240;
      var h = pr.height || 180;
      var left = margin;
      var top = margin;


       var w = pr.width || 320;
       if (state.anchorKind === "center") {
       var h = pr.height || 180;
        left = (vw - w) / 2;
        top = (vh - h) / 2;
      } else if (state.anchorKind === "finger") {
        // Center to viewport, appear above finger
        left = (vw - w) / 2;
 
        var y = state.anchorY || (vh / 2);
        top = y - h - gap;
 
        // If above would go offscreen, flip below finger
        if (top < margin) top = y + gap;
       } else {
        // Anchor to trigger (desktop hover/click)
        if (!trigger || !trigger.getBoundingClientRect) return;
 
        var tr = trigger.getBoundingClientRect();
        var isIconish = (tr.width || 0) <= 40;


      // Prefer below, left-aligned; icon triggers align right
        left = isIconish ? (tr.right - w) : tr.left;
      var isIconish = (tr.width || 0) <= 40;
        top = tr.bottom + gap;
      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));
      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;
      if (top + h + margin > vh) {
        top = tr.top - h - gap;
       }
       }
      left = clamp(left, margin, Math.max(margin, vw - w - margin));
       top = clamp(top, margin, Math.max(margin, vh - h - margin));
       top = clamp(top, margin, Math.max(margin, vh - h - margin));


Line 1,924: Line 1,916:
   }
   }


   function hidePopup(fromUser) {
   function hidePopup() {
     if (!state.open) return;
     if (!state.open) return;


Line 1,937: Line 1,929:
     state.size = "sm";
     state.size = "sm";
     state.trigger = null;
     state.trigger = null;
    state.anchorKind = "trigger";


     var el = ensurePopEl();
     var el = ensurePopEl();
Line 1,945: Line 1,938:
     if (!trigger) return;
     if (!trigger) return;


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


     state.open = true;
     state.open = true;
Line 1,959: Line 1,950:
     renderPopup(opts);
     renderPopup(opts);
     setHidden(popEl, false);
     setHidden(popEl, false);
     positionPopupNearTrigger(trigger);
     positionPopup(trigger);
   }
   }


   function findTrigger(t) {
   function findTrigger(t) {
     if (!t) return null;
     if (!t) return null;
    if (isInsidePopup(t)) return null;


     var defEl = closest(t, DEF_SEL);
     var defEl = closest(t, DEF_SEL);
Line 1,978: Line 1,970:


   function resolveOptsForTrigger(trigger) {
   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 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,990: Line 1,977:
     var size = "sm";
     var size = "sm";


    // explicit overrides
     var attrMode = trigger.getAttribute && trigger.getAttribute("data-sv-pop");
     var attrMode = trigger.getAttribute && trigger.getAttribute("data-sv-pop");
     if (attrMode === "hover" || attrMode === "click") mode = attrMode;
     if (attrMode === "hover" || attrMode === "click") mode = attrMode;
Line 2,006: Line 1,992:
     }
     }


     if (!attrSize) {
     if (!attrSize) size = (mode === "click") ? "lg" : "sm";
      size = mode === "click" ? "lg" : "sm";
 
    // On non-hover devices, treat hover-mode triggers as click-open so they close reliably.
    if (!HOVER_CAPABLE && mode === "hover") {
      mode = "click";
      size = "sm";
     }
     }


     var sourceNode = null;
     if (isDef) {
    var title = "Info";
      var tipText = getDefTipText(trigger);
    var hint = "";
      if (!tipText) return null;


    if (isDef) {
      var tip = getDefTipText(trigger);
       var linkTitle = getDefLinkTitle(trigger);
       var linkTitle = getDefLinkTitle(trigger);
 
       var title = String(trigger.textContent || "").trim() || "Definition";
      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 {
       return {
         mode: mode,
         mode: mode,
         size: size,
         size: size,
         pinned: mode === "click" ? true : (!HOVER_CAPABLE && state.lastPointerType !== "mouse"),
         pinned: (mode === "click"),
         title: title,
         title: title,
        hint: hint,
         bodyText: tipText,
         bodyText: tip,
         bodyPre: true,
         bodyPre: true,
         actionsNode: mode === "click" ? buildActionsForDefinition(linkTitle) : null
         actionsNode: (mode === "click") ? buildActionsForDefinition(linkTitle) : null
       };
       };
     }
     }


     sourceNode = getContentNodeFromTrigger(trigger);
     var sourceNode = getContentNodeFromTrigger(trigger);
     if (isBlankContent(sourceNode)) return null;
     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 {
     return {
       mode: mode,
       mode: mode,
       size: size,
       size: size,
       pinned: pinned,
       pinned: (mode === "click"),
       title: title,
       title: guessTitle(trigger, sourceNode),
      hint: hint,
       bodyNode: sourceNode,
       bodyNode: sourceNode,
       bodyPre: false,
       bodyPre: false,
       actionsNode: null
       actionsNode: null
     };
     };
  }
  function isInsidePopup(target) {
    return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
   }
   }


   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
   /* Event Intercepts (window capture) — prevents legacy handlers running */
   /* Event Intercepts (window capture)                                   */
   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */


   window.addEventListener(
   window.addEventListener("pointerdown", function (e) {
    "pointerdown",
     if (!e) return;
     function (e) {
    if (e && e.pointerType) state.lastPointerType = e.pointerType;
      if (e && e.pointerType) state.lastPointerType = e.pointerType;


      var trigger = findTrigger(e.target);
    if (isInsidePopup(e.target)) return;
      if (!trigger) return;


      var opts = resolveOptsForTrigger(trigger);
    var trigger = findTrigger(e.target);
      if (!opts) return;
    if (!trigger) return;


      // For hover-mode on hover-capable devices, do not open on click
    var opts = resolveOptsForTrigger(trigger);
      if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
    if (!opts) return;
        // Let hover handlers manage it; suppress click-open
 
        e.stopPropagation();
    // Prevent native <summary> toggling regardless of mode
        return;
    if (isSummaryTrigger(trigger)) e.preventDefault();
      }
 
    // Decide anchor kind:
    // - touch / no-hover: finger + centered
    // - desktop hover: trigger anchored
    state.anchorKind = (!HOVER_CAPABLE || (e.pointerType && e.pointerType !== "mouse")) ? "finger" : "trigger";
    state.anchorX = (typeof e.clientX === "number") ? e.clientX : 0;
    state.anchorY = (typeof e.clientY === "number") ? e.clientY : 0;


      e.preventDefault();
    // On hover-capable mouse, hover-mode should NOT open on click
    if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
       e.stopPropagation();
       e.stopPropagation();
      return;
    }


      // Toggle if already open + pinned + same trigger
    e.preventDefault();
      if (state.open && state.pinned && state.trigger === trigger) {
    e.stopPropagation();
        hidePopup(true);
        return;
      }


       openPopup(trigger, opts);
    if (state.open && state.pinned && state.trigger === trigger) {
     },
       hidePopup(true);
    true
      return;
  );
     }


  // Hover open/close (small)
    openPopup(trigger, opts);
   window.addEventListener(
   }, true);
    "mouseover",
    function (e) {
      if (!HOVER_CAPABLE) return;


      var trigger = findTrigger(e.target);
  // Hover open/close (desktop only)
      if (!trigger) {
  window.addEventListener("mouseover", function (e) {
        if (state.open && !state.pinned) hidePopup(false);
    if (!HOVER_CAPABLE) return;
        return;
    if (isInsidePopup(e.target)) return;
      }


      var opts = resolveOptsForTrigger(trigger);
    var trigger = findTrigger(e.target);
       if (!opts || opts.mode !== "hover") return;
    if (!trigger) {
       if (state.open && !state.pinned) hidePopup(false);
      return;
    }


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


      clearTimeout(state.hideTimer);
    state.anchorKind = "trigger";
      state.hideTimer = 0;


      openPopup(trigger, opts);
    e.stopPropagation();
     },
     clearTimeout(state.hideTimer);
     true
     state.hideTimer = 0;
  );


  window.addEventListener(
    openPopup(trigger, opts);
    "mouseout",
  }, true);
    function (e) {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;


      var trigger = findTrigger(e.target);
  window.addEventListener("mouseout", function (e) {
      if (!trigger || trigger !== state.trigger) return;
    if (!HOVER_CAPABLE) return;
    if (!state.open || state.pinned) return;


      e.stopPropagation();
    var trigger = findTrigger(e.target);
    if (!trigger || trigger !== state.trigger) return;


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


  // Click outside closes pinned
    clearTimeout(state.hideTimer);
  window.addEventListener(
    state.hideTimer = setTimeout(function () {
    "click",
       if (state.open && !state.pinned) hidePopup(false);
    function (e) {
    }, 60);
       if (!state.open) return;
  }, true);


      var trigger = findTrigger(e.target);
  // Click outside closes (always on touch; pinned on desktop)
  window.addEventListener("click", function (e) {
    if (!state.open) return;
    if (isInsidePopup(e.target)) return;


      // Clicking the trigger is handled by pointerdown toggle
    var trigger = findTrigger(e.target);
      if (trigger) {
    if (trigger) {
        e.stopPropagation();
      e.stopPropagation();
        return;
      return;
      }
    }


       // Let links inside popup work
    if (state.pinned || !HOVER_CAPABLE) {
       if (isInsidePopup(e.target)) return;
       hidePopup(true);
       e.stopPropagation();
    }
  }, true);


      if (state.pinned) {
  window.addEventListener("keydown", function (e) {
        hidePopup(true);
    if (e.key !== "Escape") return;
        e.stopPropagation();
    if (state.open) {
      }
      hidePopup(true);
    },
      e.stopPropagation();
    true
    }
  );
  }, true);


   window.addEventListener(
   window.addEventListener("scroll", function () {
    "keydown",
     if (!state.open || !state.trigger) return;
    function (e) {
    positionPopup(state.trigger);
      if (e.key !== "Escape") return;
  }, true);
      if (state.open) {
        hidePopup(true);
        e.stopPropagation();
      }
     },
    true
  );
 
  window.addEventListener(
    "scroll",
    function () {
      if (!state.open || !state.trigger) return;
      positionPopupNearTrigger(state.trigger);
    },
    true
  );


   window.addEventListener(
   window.addEventListener("resize", function () {
    "resize",
    if (!state.open || !state.trigger) return;
    function () {
    positionPopup(state.trigger);
      if (!state.open || !state.trigger) return;
  }, true);
      positionPopupNearTrigger(state.trigger);
    },
    true
  );


   // Optional: remove native browser tooltips on defs if any remain
   // Remove native browser tooltips on defs if any remain
   function stripDefTitles(root) {
   function stripDefTitles(root) {
     var container = root || document;
     var container = root || document;
Line 2,215: Line 2,162:


   if (document.readyState === "loading") {
   if (document.readyState === "loading") {
     document.addEventListener("DOMContentLoaded", function () {
     document.addEventListener("DOMContentLoaded", function () { stripDefTitles(document); });
      stripDefTitles(document);
    });
   } else {
   } else {
     stripDefTitles(document);
     stripDefTitles(document);
   }
   }
})();
})();