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: Reverted Mobile edit Mobile web edit
No edit summary
Tags: Manual revert Mobile edit Mobile web edit
Line 1,576: Line 1,576:
(function () {
(function () {
   /* ================================================================== */
   /* ================================================================== */
   /* MODULE: Universal Popups (v2.1) — refined                          */
   /* MODULE: Universal Popups (v2)                                     */
   /*  Fixes: mobile close, keyboard open, summary default toggle,       */
   /*  - Popup B aesthetic everywhere                                  */
   /*         hover enter/leave timer handling                            */
  /*  - 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 */
   /* ================================================================== */
   /* ================================================================== */


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


   var POPV2_VERSION = 21; // 2.1
   var POPV2_VERSION = 1;
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
   COMMON.popupsV2Init = POPV2_VERSION;
   COMMON.popupsV2Init = POPV2_VERSION;


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


   var DEF_SEL = ".sv-def";
   var DEF_SEL = ".sv-def";
Line 1,603: Line 1,607:
   var HOVER_CAPABLE = false;
   var HOVER_CAPABLE = false;
   try {
   try {
     HOVER_CAPABLE = !!window.matchMedia && window.matchMedia("(hover:hover) and (pointer:fine)").matches;
     HOVER_CAPABLE =
      !!window.matchMedia &&
      window.matchMedia("(hover:hover) and (pointer:fine)").matches;
   } catch (e) {
   } catch (e) {
     HOVER_CAPABLE = false;
     HOVER_CAPABLE = false;
Line 1,638: Line 1,644:
   function safeIdSelector(id) {
   function safeIdSelector(id) {
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
  }
  function setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
   }
   }


Line 1,652: Line 1,653:
     el.setAttribute("role", "dialog");
     el.setAttribute("role", "dialog");
     el.setAttribute("aria-hidden", "true");
     el.setAttribute("aria-hidden", "true");
    // Keep hover popups from closing while pointer is over the popup itself
    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);
     document.body.appendChild(el);
     popEl = el;
     popEl = el;
     return popEl;
     return popEl;
  }
  function setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
   }
   }


Line 1,698: Line 1,687:
     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,716: Line 1,709:
     if (t == null) return "";
     if (t == null) return "";
     return String(t).trim();
     return String(t).trim();
  }
  function isSummaryTrigger(trigger) {
    return !!(trigger && trigger.tagName === "SUMMARY");
   }
   }


Line 1,732: Line 1,721:
       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,782: Line 1,772:


   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) pop = closest(sourceNode, POPUP_POP_SEL);
     if (sourceNode && sourceNode.nodeType === 1) {
 
      pop = closest(sourceNode, POPUP_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();
     }
     }
Line 1,809: Line 1,803:
     a.textContent = "Open page";
     a.textContent = "Open page";


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


    // Do not prevent default/capture on this link — Popups extension can hook it.
     wrap.appendChild(a);
     wrap.appendChild(a);
     return wrap;
     return wrap;
Line 1,824: Line 1,820:
     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);


     // Header
     // Head
     var head = document.createElement("div");
     var head = document.createElement("div");
     head.className = "sv-uipop-head";
     head.className = "sv-uipop-head" + (opts.mode === "click" ? " sv-uipop-head--clickable" : "");
 
    // Click-open popups: center title + NO hint element
    if (opts.mode === "click") head.className += " sv-uipop-head--clickable sv-uipop-head--center";


     var title = document.createElement("div");
     var title = document.createElement("div");
     title.className = "sv-uipop-title";
     title.className = "sv-uipop-title";
     title.textContent = opts.title || "Info";
     title.textContent = opts.title || "Info";
    head.appendChild(title);


     // Hover popups: hint is allowed (but not required)
     var hint = document.createElement("div");
    if (opts.mode !== "click" && opts.hint) {
    hint.className = "sv-uipop-hint";
      var hint = document.createElement("div");
    hint.textContent = opts.hint || "";
      hint.className = "sv-uipop-hint";
      hint.textContent = opts.hint;
      head.appendChild(hint);
    }


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


Line 1,851: Line 1,842:
     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,857: Line 1,849:
       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,862: Line 1,856:
     }
     }


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


     // Header click closes for click-open
     // Header close (large/click)
     if (opts.mode === "click") {
     if (opts.mode === "click") {
       head.addEventListener("click", function (e) {
       head.addEventListener(
        e.preventDefault();
        "click",
        hidePopup();
        function (e) {
      }, { once: true });
          e.preventDefault();
          hidePopup(true);
        },
        { once: true }
      );
     }
     }
   }
   }
Line 1,878: Line 1,879:
     if (!el || !trigger || !trigger.getBoundingClientRect) return;
     if (!el || !trigger || !trigger.getBoundingClientRect) 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,896: Line 1,900:
       var h = pr.height || 180;
       var h = pr.height || 180;


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


Line 1,915: Line 1,924:
   }
   }


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


Line 1,936: Line 1,945:
     if (!trigger) return;
     if (!trigger) return;


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


     state.open = true;
     state.open = true;
Line 1,953: Line 1,964:
   function findTrigger(t) {
   function findTrigger(t) {
     if (!t) return null;
     if (!t) return null;
    // Never treat clicks/hover inside the popup as trigger changes
    if (popEl && (t === popEl || (popEl.contains && popEl.contains(t)))) return null;


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


   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,977: Line 1,990:
     var size = "sm";
     var size = "sm";


     // Manual overrides (optional)
     // 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 1,993: Line 2,006:
     }
     }


     if (!attrSize) size = (mode === "click") ? "lg" : "sm";
     if (!attrSize) {
      size = mode === "click" ? "lg" : "sm";
    }


     // No-hover devices: “hover” becomes tap-to-open (click-mode) so it can close properly.
     var sourceNode = null;
     if (!HOVER_CAPABLE && mode === "hover") {
     var title = "Info";
      mode = "click";
    var hint = "";
      size = (attrSize === "lg") ? "lg" : "sm";
    }


     if (isDef) {
     if (isDef) {
       var tip = getDefTipText(trigger);
       var tip = getDefTipText(trigger);
      var linkTitle = getDefLinkTitle(trigger);
       if (!tip) return null;
       if (!tip) return null;


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


     var sourceNode = getContentNodeFromTrigger(trigger);
     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: (mode === "click"),
       pinned: pinned,
       title: guessTitle(trigger, sourceNode),
       title: title,
       hint: (mode === "click") ? "" : (HOVER_CAPABLE ? "Hover to view" : "Tap to view"),
       hint: hint,
       bodyNode: sourceNode,
       bodyNode: sourceNode,
       bodyPre: false,
       bodyPre: false,
Line 2,040: Line 2,067:


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


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


    // ignore right/middle clicks
      var trigger = findTrigger(e.target);
    if (typeof e.button === "number" && e.button !== 0) return;
      if (!trigger) return;


    if (e.pointerType) state.lastPointerType = e.pointerType;
      var opts = resolveOptsForTrigger(trigger);
      if (!opts) return;


    // If user clicks inside popup, allow normal interactions (links, selection)
      // For hover-mode on hover-capable devices, do not open on click
    if (isInsidePopup(e.target)) return;
      if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
        // Let hover handlers manage it; suppress click-open
        e.stopPropagation();
        return;
      }


    var trigger = findTrigger(e.target);
      e.preventDefault();
    if (!trigger) return;
 
    var opts = resolveOptsForTrigger(trigger);
    if (!opts) return;
 
    // Prevent native <details>/<summary> toggling whenever summary is used
    if (isSummaryTrigger(trigger)) e.preventDefault();
 
    // Hover-mode on mouse: don’t click-open; let hover do it
    if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
       e.stopPropagation();
       e.stopPropagation();
      return;
    }


    e.preventDefault();
      // Toggle if already open + pinned + same trigger
    e.stopPropagation();
      if (state.open && state.pinned && state.trigger === trigger) {
 
        hidePopup(true);
    if (state.open && state.pinned && state.trigger === trigger) {
        return;
      hidePopup();
      return;
    }
 
    openPopup(trigger, opts);
  }, true);
 
  // Keyboard open (Enter/Space) for accessibility + summary safety
  window.addEventListener("keydown", function (e) {
    if (!e) return;
 
    if (e.key === "Escape") {
      if (state.open) {
        hidePopup();
        e.stopPropagation();
       }
       }
      return;
    }
    var isActivate = (e.key === "Enter" || e.key === " ");
    if (!isActivate) return;
    if (isInsidePopup(e.target)) return;
    var trigger = findTrigger(e.target);
    if (!trigger) return;


    var opts = resolveOptsForTrigger(trigger);
      openPopup(trigger, opts);
    if (!opts) return;
     },
 
     true
    // Keyboard activation should behave like click-open so user can read/interact
   );
    if (opts.mode === "hover") {
      opts.mode = "click";
      opts.pinned = true;
      opts.size = "sm";
      opts.hint = "";
      opts.actionsNode = opts.actionsNode || null;
     }
 
    e.preventDefault();
    e.stopPropagation();
 
    // Stop native summary toggling as well
    if (isSummaryTrigger(trigger)) {
      try {
        var det = closest(trigger, POPUP_DETAILS_SEL);
        if (det && det.open) det.open = false;
      } catch (x) {}
     }
 
    openPopup(trigger, opts);
   }, true);


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


    // If entering the popup itself, keep it open
      var trigger = findTrigger(e.target);
    if (isInsidePopup(e.target)) {
      if (!trigger) {
      if (state.open && !state.pinned) {
        if (state.open && !state.pinned) hidePopup(false);
        clearTimeout(state.hideTimer);
         return;
         state.hideTimer = 0;
       }
       }
      return;
    }


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


    var opts = resolveOptsForTrigger(trigger);
      clearTimeout(state.hideTimer);
    if (!opts || opts.mode !== "hover") return;
      state.hideTimer = 0;


     e.stopPropagation();
      openPopup(trigger, opts);
     },
    true
  );


     clearTimeout(state.hideTimer);
  window.addEventListener(
    state.hideTimer = 0;
    "mouseout",
     function (e) {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;


    openPopup(trigger, opts);
      var trigger = findTrigger(e.target);
  }, true);
      if (!trigger || trigger !== state.trigger) return;


  window.addEventListener("mouseout", function (e) {
      e.stopPropagation();
    if (!HOVER_CAPABLE) return;
    if (!state.open || state.pinned) return;


    var trigger = findTrigger(e.target);
      clearTimeout(state.hideTimer);
    if (!trigger || trigger !== state.trigger) return;
      state.hideTimer = setTimeout(function () {
        if (state.open && !state.pinned) hidePopup(false);
      }, 60);
    },
    true
  );


     e.stopPropagation();
  // Click outside closes pinned
  window.addEventListener(
    "click",
     function (e) {
      if (!state.open) return;


    clearTimeout(state.hideTimer);
      var trigger = findTrigger(e.target);
    state.hideTimer = setTimeout(function () {
      if (state.open && !state.pinned) hidePopup();
    }, 80);
  }, true);


  // Outside click closes (pinned always; and also closes “tap-open” popups on no-hover devices)
      // Clicking the trigger is handled by pointerdown toggle
  window.addEventListener("click", function (e) {
      if (trigger) {
    if (!state.open) return;
        e.stopPropagation();
        return;
      }


    if (isInsidePopup(e.target)) return;
      // Let links inside popup work
      if (isInsidePopup(e.target)) return;


    var trigger = findTrigger(e.target);
      if (state.pinned) {
    if (trigger) {
        hidePopup(true);
      e.stopPropagation();
        e.stopPropagation();
       return;
       }
     }
     },
    true
  );


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


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


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


   // Remove native title tooltips on defs if any remain
   // Optional: 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,213: Line 2,215:


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