MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
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 ( | /* MODULE: Universal Popups (v3) */ | ||
/* - | /* - Compact width; popups grow vertically */ | ||
/* - | /* - Header centered + bigger; NO hint text */ | ||
/* - | /* - Mobile/touch: popup appears ABOVE finger, centered to viewport */ | ||
/* - | /* - Desktop hover: anchored to trigger */ | ||
/* ================================================================== */ | /* ================================================================== */ | ||
| Line 1,586: | Line 1,586: | ||
var COMMON = (SV.common = SV.common || {}); | var COMMON = (SV.common = SV.common || {}); | ||
var | var POP_VERSION = 3; | ||
if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= | if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POP_VERSION) return; | ||
COMMON.popupsV2Init = | COMMON.popupsV2Init = POP_VERSION; | ||
try { | try { document.documentElement.classList.add("sv-uipop-v3"); } 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; | ||
} catch (e) { HOVER_CAPABLE = false; } | |||
} catch (e) { | |||
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) | |||
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; | ||
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) { | ||
var pop = null; | var pop = null; | ||
if (sourceNode && sourceNode.nodeType === 1) | if (sourceNode && sourceNode.nodeType === 1) 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"); | ||
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); | ||
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"); | ||
while (el.firstChild) el.removeChild(el.firstChild); | while (el.firstChild) el.removeChild(el.firstChild); | ||
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"; | ||
head.appendChild(title); | head.appendChild(title); | ||
el.appendChild(head); | el.appendChild(head); | ||
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); | ||
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); | ||
el.appendChild(body); | el.appendChild(body); | ||
if (opts.mode === "click") { | if (opts.mode === "click") { | ||
head.addEventListener( | head.addEventListener("click", function (e) { | ||
e.preventDefault(); | |||
hidePopup(true); | |||
}, { once: true }); | |||
} | } | ||
} | } | ||
function | function positionPopup(trigger) { | ||
var el = ensurePopEl(); | var el = ensurePopEl(); | ||
if (!el | if (!el) 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,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 = | var gap = 12; | ||
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 | if (state.anchorKind === "center") { | ||
var | 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; | |||
left = isIconish ? (tr.right - w) : tr.left; | |||
top = tr.bottom + gap; | |||
left = clamp(left, margin, Math.max(margin, vw - w - margin)); | |||
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( | 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); | ||
state.open = true; | state.open = true; | ||
| Line 1,959: | Line 1,950: | ||
renderPopup(opts); | renderPopup(opts); | ||
setHidden(popEl, false); | setHidden(popEl, false); | ||
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) { | ||
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"; | ||
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"; | ||
// On non-hover devices, treat hover-mode triggers as click-open so they close reliably. | |||
if (!HOVER_CAPABLE && mode === "hover") { | |||
mode = "click"; | |||
size = "sm"; | |||
} | } | ||
if (isDef) { | |||
var tipText = getDefTipText(trigger); | |||
if (!tipText) return null; | |||
var linkTitle = getDefLinkTitle(trigger); | var linkTitle = getDefLinkTitle(trigger); | ||
var title = String(trigger.textContent || "").trim() || "Definition"; | |||
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, | ||
bodyText: tipText, | |||
bodyText: | |||
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), | ||
bodyNode: sourceNode, | bodyNode: sourceNode, | ||
bodyPre: false, | bodyPre: false, | ||
actionsNode: null | actionsNode: null | ||
}; | }; | ||
} | } | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Event Intercepts (window capture) | /* Event Intercepts (window capture) */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
window.addEventListener( | window.addEventListener("pointerdown", function (e) { | ||
if (!e) return; | |||
if (e && e.pointerType) state.lastPointerType = e.pointerType; | |||
if (isInsidePopup(e.target)) return; | |||
var trigger = findTrigger(e.target); | |||
if (!trigger) return; | |||
var opts = resolveOptsForTrigger(trigger); | |||
if (!opts) return; | |||
// Prevent native <summary> toggling regardless of mode | |||
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; | |||
// 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; | |||
} | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
if (state.open && state.pinned && state.trigger === trigger) { | |||
} | hidePopup(true); | ||
return; | |||
} | |||
openPopup(trigger, opts); | |||
}, true); | |||
// Hover open/close (desktop only) | |||
window.addEventListener("mouseover", function (e) { | |||
if (!HOVER_CAPABLE) return; | |||
if (isInsidePopup(e.target)) return; | |||
var trigger = findTrigger(e.target); | |||
if (! | if (!trigger) { | ||
if (state.open && !state.pinned) hidePopup(false); | |||
return; | |||
} | |||
var opts = resolveOptsForTrigger(trigger); | |||
if (!opts || opts.mode !== "hover") return; | |||
state.anchorKind = "trigger"; | |||
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); | |||
if (!state. | }, true); | ||
// Click outside closes (always on touch; pinned on desktop) | |||
window.addEventListener("click", function (e) { | |||
if (!state.open) return; | |||
if (isInsidePopup(e.target)) return; | |||
var trigger = findTrigger(e.target); | |||
if (trigger) { | |||
e.stopPropagation(); | |||
return; | |||
} | |||
if (state.pinned || !HOVER_CAPABLE) { | |||
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("scroll", function () { | ||
if (!state.open || !state.trigger) return; | |||
positionPopup(state.trigger); | |||
}, true); | |||
window.addEventListener( | window.addEventListener("resize", function () { | ||
if (!state.open || !state.trigger) return; | |||
positionPopup(state.trigger); | |||
}, true); | |||
// | // 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); }); | ||
} else { | } else { | ||
stripDefTitles(document); | stripDefTitles(document); | ||
} | } | ||
})(); | })(); | ||