MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary Tags: Mobile edit Mobile web edit |
No edit summary Tags: Reverted Mobile edit Mobile web edit |
||
| Line 1,576: | Line 1,576: | ||
(function () { | (function () { | ||
/* ================================================================== */ | /* ================================================================== */ | ||
/* MODULE: Universal Popups (v2) | /* MODULE: Universal Popups (v2) — refined */ | ||
/* - | /* - Removes redundant hint for click-open (large) popups */ | ||
/* - | /* - Leaves hover hints optional (but CSS currently hides legacy) */ | ||
/* - | /* - Keeps links inside popups fully interactive (Popups friendly) */ | ||
/* ================================================================== */ | /* ================================================================== */ | ||
| Line 1,586: | Line 1,585: | ||
var COMMON = (SV.common = SV.common || {}); | var COMMON = (SV.common = SV.common || {}); | ||
var POPV2_VERSION = | var POPV2_VERSION = 2; // bumped due to header/hint changes | ||
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 { | try { document.documentElement.classList.add("sv-uipop-v2"); } catch (e) {} | ||
var DEF_SEL = ".sv-def"; | var DEF_SEL = ".sv-def"; | ||
| Line 1,607: | Line 1,604: | ||
var HOVER_CAPABLE = false; | var HOVER_CAPABLE = false; | ||
try { | try { | ||
HOVER_CAPABLE = | HOVER_CAPABLE = !!window.matchMedia && window.matchMedia("(hover:hover) and (pointer:fine)").matches; | ||
} catch (e) { | } catch (e) { | ||
HOVER_CAPABLE = false; | HOVER_CAPABLE = false; | ||
| Line 1,619: | Line 1,614: | ||
open: false, | open: false, | ||
pinned: false, | pinned: false, | ||
mode: "hover", // hover | click | mode: "hover", // hover | click | ||
size: "sm", | size: "sm", // sm | lg | ||
trigger: null, | trigger: null, | ||
lastPointerType: "mouse", | lastPointerType: "mouse", | ||
| Line 1,687: | Line 1,682: | ||
if (t == null) return true; | if (t == null) return true; | ||
var compact = String(t) | var compact = String(t).replace(/\s+/g, "").replace(/[—–-]+/g, "").trim(); | ||
return !compact; | return !compact; | ||
} | } | ||
| Line 1,721: | Line 1,712: | ||
if (!pop) return null; | if (!pop) return null; | ||
var body = | var body = | ||
pop.querySelector(".sv-tip-pop-body") || | pop.querySelector(".sv-tip-pop-body") || | ||
| Line 1,772: | Line 1,762: | ||
function guessTitle(trigger, sourceNode) { | function guessTitle(trigger, sourceNode) { | ||
var pop = null; | var pop = null; | ||
if (sourceNode && sourceNode.nodeType === 1) { | if (sourceNode && sourceNode.nodeType === 1) { | ||
| Line 1,778: | Line 1,767: | ||
} | } | ||
if (pop) { | if (pop) { | ||
var t = | 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,803: | Line 1,790: | ||
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); | ||
else a.href = String(linkTitle); | |||
// IMPORTANT: 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,820: | Line 1,805: | ||
el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm"); | el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm"); | ||
while (el.firstChild) el.removeChild(el.firstChild); | while (el.firstChild) el.removeChild(el.firstChild); | ||
// | // Header | ||
var head = document.createElement("div"); | var head = document.createElement("div"); | ||
head.className = "sv-uipop-head" + (opts.mode === "click" | head.className = "sv-uipop-head"; | ||
// 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); | |||
var hint = document.createElement("div"); | // Hover popups: hint is allowed (but not required) | ||
if (opts.mode !== "click" && opts.hint) { | |||
var hint = document.createElement("div"); | |||
hint.className = "sv-uipop-hint"; | |||
hint.textContent = opts.hint; | |||
head.appendChild(hint); | |||
} | |||
el.appendChild(head); | el.appendChild(head); | ||
| Line 1,842: | Line 1,832: | ||
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,838: | ||
stripIds(clone); | stripIds(clone); | ||
unhideTree(clone); | unhideTree(clone); | ||
body.appendChild(clone); | body.appendChild(clone); | ||
} else if (opts.bodyText != null) { | } else if (opts.bodyText != null) { | ||
| Line 1,856: | Line 1,843: | ||
} | } | ||
if (opts.actionsNode) | if (opts.actionsNode) body.appendChild(opts.actionsNode); | ||
el.appendChild(body); | el.appendChild(body); | ||
// Header | // Header click closes for click-open | ||
if (opts.mode === "click") { | if (opts.mode === "click") { | ||
head.addEventListener( | head.addEventListener("click", function (e) { | ||
e.preventDefault(); | |||
hidePopup(true); | |||
}, { once: true }); | |||
} | } | ||
} | } | ||
| Line 1,879: | Line 1,859: | ||
if (!el || !trigger || !trigger.getBoundingClientRect) return; | if (!el || !trigger || !trigger.getBoundingClientRect) return; | ||
el.style.left = "0px"; | el.style.left = "0px"; | ||
el.style.top = "0px"; | el.style.top = "0px"; | ||
setHidden(el, false); | setHidden(el, false); | ||
| Line 1,900: | Line 1,877: | ||
var h = pr.height || 180; | var h = pr.height || 180; | ||
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; | ||
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 (top + h + margin > vh) | |||
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,896: | ||
} | } | ||
function hidePopup( | function hidePopup() { | ||
if (!state.open) return; | if (!state.open) return; | ||
| Line 1,945: | Line 1,917: | ||
if (!trigger) return; | if (!trigger) return; | ||
if (state.open && state.trigger && state.trigger !== trigger) | if (state.open && state.trigger && state.trigger !== trigger) hidePopup(); | ||
state.open = true; | state.open = true; | ||
| Line 1,964: | Line 1,934: | ||
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,978: | Line 1,951: | ||
function resolveOptsForTrigger(trigger) { | function resolveOptsForTrigger(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,990: | Line 1,958: | ||
var size = "sm"; | var size = "sm"; | ||
// | // Manual overrides (optional) | ||
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,974: | ||
} | } | ||
if (!attrSize) | if (!attrSize) size = (mode === "click") ? "lg" : "sm"; | ||
if (isDef) { | if (isDef) { | ||
var tip = getDefTipText(trigger); | var tip = getDefTipText(trigger); | ||
if (!tip) return null; | if (!tip) return null; | ||
title = String(trigger.textContent || "").trim() || "Definition" | var linkTitle = getDefLinkTitle(trigger); | ||
var title = String(trigger.textContent || "").trim() || "Definition"; | |||
return { | return { | ||
mode: mode, | mode: mode, | ||
size: size, | size: size, | ||
pinned: mode === "click | pinned: (mode === "click"), | ||
title: title, | title: title, | ||
hint: | hint: (mode === "click") ? "" : (HOVER_CAPABLE ? "Hover to view" : "Tap to view"), | ||
bodyText: tip, | 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; | ||
return { | return { | ||
mode: mode, | mode: mode, | ||
size: size, | size: size, | ||
pinned: | pinned: (mode === "click"), | ||
title: | title: guessTitle(trigger, sourceNode), | ||
hint: | hint: (mode === "click") ? "" : (HOVER_CAPABLE ? "Hover to view" : "Tap to view"), | ||
bodyNode: sourceNode, | bodyNode: sourceNode, | ||
bodyPre: false, | bodyPre: false, | ||
| Line 2,067: | Line 2,015: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Event Intercepts (window capture) — | /* Event Intercepts (window capture) — supersedes legacy systems */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
window.addEventListener( | window.addEventListener("pointerdown", function (e) { | ||
if (e && e.pointerType) state.lastPointerType = e.pointerType; | |||
// If user clicks inside popup, allow normal interactions (links, selection) | |||
if (isInsidePopup(e.target)) return; | |||
var trigger = findTrigger(e.target); | |||
if (!trigger) return; | |||
var opts = resolveOptsForTrigger(trigger); | |||
if (!opts) return; | |||
// 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(); | |||
e.stopPropagation(); | |||
if (state.open && state.pinned && state.trigger === trigger) { | |||
hidePopup(true); | |||
return; | |||
} | |||
openPopup(trigger, opts); | |||
}, true); | |||
// Hover open/close (small) | // Hover open/close (small) | ||
window.addEventListener( | window.addEventListener("mouseover", function (e) { | ||
if (!HOVER_CAPABLE) return; | |||
if (isInsidePopup(e.target)) return; // allow moving into click popups without side effects | |||
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( | 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); | |||
// | // Outside click closes pinned | ||
window.addEventListener( | window.addEventListener("click", function (e) { | ||
if (!state.open) return; | |||
// If click inside popup, do nothing (links should work; Popups can hook them) | |||
if (isInsidePopup(e.target)) return; | |||
// Clicking a trigger is handled by pointerdown | |||
var trigger = findTrigger(e.target); | |||
if (trigger) { | |||
e.stopPropagation(); | |||
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( | |||
window.addEventListener( | window.addEventListener("scroll", function () { | ||
if (!state.open || !state.trigger) return; | |||
positionPopupNearTrigger(state.trigger); | |||
}, true); | |||
window.addEventListener( | window.addEventListener("resize", function () { | ||
if (!state.open || !state.trigger) return; | |||
positionPopupNearTrigger(state.trigger); | |||
}, true); | |||
// | // Remove native title 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,136: | ||
if (document.readyState === "loading") { | if (document.readyState === "loading") { | ||
document.addEventListener("DOMContentLoaded", function () { | document.addEventListener("DOMContentLoaded", function () { stripDefTitles(document); }); | ||
} else { | } else { | ||
stripDefTitles(document); | stripDefTitles(document); | ||
} | } | ||
})(); | })(); | ||