MediaWiki:Gadget-FloatTOC.js
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Internet Explorer或Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
- Opera:按 Ctrl-F5。
/**
* SPDX-License-Identifier: GPL-3.0-or-later
* _addText: '{{Gadget Header|license=GPL-3.0-or-later}}'
*
* @source {@link https://git.qiuwen.net.cn/InterfaceAdmin/QiuwenGadgets/src/branch/master/src/FloatTOC}
* @author 安忆 <i@anyi.in>
* @license GPL-3.0-or-later {@link https://www.qiuwenbaike.cn/wiki/H:GPL-3.0}
*/
/**
* Copyright (C) 安忆
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* +------------------------------------------------------------+
* | === WARNING: GLOBAL GADGET FILE === |
* +------------------------------------------------------------+
* | All changes should be made in the repository, |
* | otherwise they will be lost. |
* +------------------------------------------------------------+
* | Changes to this page may affect many users. |
* | Please discuss changes by opening an issue before editing. |
* +------------------------------------------------------------+
*/
/* <nowiki> */
(() => {
"use strict";
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// node_modules/.pnpm/@mrhenry+core-web@1.2.3/node_modules/@mrhenry/core-web/modules/IntersectionObserver.js
var require_IntersectionObserver = __commonJS({
"node_modules/.pnpm/@mrhenry+core-web@1.2.3/node_modules/@mrhenry/core-web/modules/IntersectionObserver.js"() {
(function(undefined) {
if (!("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype)) {
(function(window2, document2) {
"use strict";
var supportedNatively = "IntersectionObserver" in window2 && "IntersectionObserverEntry" in window2 && "intersectionRatio" in window2.IntersectionObserverEntry.prototype;
if (supportedNatively) {
return;
}
var registry = [];
function IntersectionObserverEntry(entry) {
this.time = entry.time;
this.target = entry.target;
this.rootBounds = entry.rootBounds;
this.boundingClientRect = entry.boundingClientRect;
this.intersectionRect = entry.intersectionRect || getEmptyRect();
try {
this.isIntersecting = !!entry.intersectionRect;
} catch (err) {
}
var targetRect = this.boundingClientRect;
var targetArea = targetRect.width * targetRect.height;
var intersectionRect = this.intersectionRect;
var intersectionArea = intersectionRect.width * intersectionRect.height;
if (targetArea) {
this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
} else {
this.intersectionRatio = this.isIntersecting ? 1 : 0;
}
}
IntersectionObserverEntry.prototype.intersectionRatio = 0;
function IntersectionObserver2(callback, opt_options) {
var options = opt_options || {};
if (typeof callback != "function") {
throw new Error("callback must be a function");
}
if (options.root && options.root.nodeType != 1) {
throw new Error("root must be an Element");
}
this._checkForIntersections = throttle(
this._checkForIntersections.bind(this),
this.THROTTLE_TIMEOUT
);
this._callback = callback;
this._observationTargets = [];
this._queuedEntries = [];
this._rootMarginValues = this._parseRootMargin(options.rootMargin);
this.thresholds = this._initThresholds(options.threshold);
this.root = options.root || null;
this.rootMargin = this._rootMarginValues.map(function(margin) {
return margin.value + margin.unit;
}).join(" ");
}
IntersectionObserver2.prototype.THROTTLE_TIMEOUT = 100;
IntersectionObserver2.prototype.POLL_INTERVAL = null;
IntersectionObserver2.prototype.USE_MUTATION_OBSERVER = true;
IntersectionObserver2.prototype.observe = function(target) {
var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
return item.element == target;
});
if (isTargetAlreadyObserved) {
return;
}
if (!(target && target.nodeType == 1)) {
throw new Error("target must be an Element");
}
this._registerInstance();
this._observationTargets.push({ element: target, entry: null });
this._monitorIntersections();
this._checkForIntersections();
};
IntersectionObserver2.prototype.unobserve = function(target) {
this._observationTargets = this._observationTargets.filter(function(item) {
return item.element != target;
});
if (!this._observationTargets.length) {
this._unmonitorIntersections();
this._unregisterInstance();
}
};
IntersectionObserver2.prototype.disconnect = function() {
this._observationTargets = [];
this._unmonitorIntersections();
this._unregisterInstance();
};
IntersectionObserver2.prototype.takeRecords = function() {
var records = this._queuedEntries.slice();
this._queuedEntries = [];
return records;
};
IntersectionObserver2.prototype._initThresholds = function(opt_threshold) {
var threshold = opt_threshold || [0];
if (!Array.isArray(threshold)) threshold = [threshold];
return threshold.sort().filter(function(t, i, a) {
if (typeof t != "number" || isNaN(t) || t < 0 || t > 1) {
throw new Error("threshold must be a number between 0 and 1 inclusively");
}
return t !== a[i - 1];
});
};
IntersectionObserver2.prototype._parseRootMargin = function(opt_rootMargin) {
var marginString = opt_rootMargin || "0px";
var margins = marginString.split(/\s+/).map(function(margin) {
var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
if (!parts) {
throw new Error("rootMargin must be specified in pixels or percent");
}
return { value: parseFloat(parts[1]), unit: parts[2] };
});
margins[1] = margins[1] || margins[0];
margins[2] = margins[2] || margins[0];
margins[3] = margins[3] || margins[1];
return margins;
};
IntersectionObserver2.prototype._monitorIntersections = function() {
if (!this._monitoringIntersections) {
this._monitoringIntersections = true;
if (this.POLL_INTERVAL) {
this._monitoringInterval = setInterval(
this._checkForIntersections,
this.POLL_INTERVAL
);
} else {
addEvent(window2, "resize", this._checkForIntersections, true);
addEvent(document2, "scroll", this._checkForIntersections, true);
if (this.USE_MUTATION_OBSERVER && "MutationObserver" in window2) {
this._domObserver = new MutationObserver(this._checkForIntersections);
this._domObserver.observe(document2, {
attributes: true,
childList: true,
characterData: true,
subtree: true
});
}
}
}
};
IntersectionObserver2.prototype._unmonitorIntersections = function() {
if (this._monitoringIntersections) {
this._monitoringIntersections = false;
clearInterval(this._monitoringInterval);
this._monitoringInterval = null;
removeEvent(window2, "resize", this._checkForIntersections, true);
removeEvent(document2, "scroll", this._checkForIntersections, true);
if (this._domObserver) {
this._domObserver.disconnect();
this._domObserver = null;
}
}
};
IntersectionObserver2.prototype._checkForIntersections = function() {
var rootIsInDom = this._rootIsInDom();
var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
this._observationTargets.forEach(function(item) {
var target = item.element;
var targetRect = getBoundingClientRect(target);
var rootContainsTarget = this._rootContainsTarget(target);
var oldEntry = item.entry;
var intersectionRect = rootIsInDom && rootContainsTarget && this._computeTargetAndRootIntersection(target, rootRect);
var newEntry = item.entry = new IntersectionObserverEntry({
time: now(),
target,
boundingClientRect: targetRect,
rootBounds: rootRect,
intersectionRect
});
if (!oldEntry) {
this._queuedEntries.push(newEntry);
} else if (rootIsInDom && rootContainsTarget) {
if (this._hasCrossedThreshold(oldEntry, newEntry)) {
this._queuedEntries.push(newEntry);
}
} else {
if (oldEntry && oldEntry.isIntersecting) {
this._queuedEntries.push(newEntry);
}
}
}, this);
if (this._queuedEntries.length) {
this._callback(this.takeRecords(), this);
}
};
IntersectionObserver2.prototype._computeTargetAndRootIntersection = function(target, rootRect) {
if (window2.getComputedStyle(target).display == "none") return;
var targetRect = getBoundingClientRect(target);
var intersectionRect = targetRect;
var parent = getParentNode(target);
var atRoot = false;
while (!atRoot) {
var parentRect = null;
var parentComputedStyle = parent.nodeType == 1 ? window2.getComputedStyle(parent) : {};
if (parentComputedStyle.display == "none") return;
if (parent == this.root || parent == document2) {
atRoot = true;
parentRect = rootRect;
} else {
if (parent != document2.body && parent != document2.documentElement && parentComputedStyle.overflow != "visible") {
parentRect = getBoundingClientRect(parent);
}
}
if (parentRect) {
intersectionRect = computeRectIntersection(parentRect, intersectionRect);
if (!intersectionRect) break;
}
parent = getParentNode(parent);
}
return intersectionRect;
};
IntersectionObserver2.prototype._getRootRect = function() {
var rootRect;
if (this.root) {
rootRect = getBoundingClientRect(this.root);
} else {
var html = document2.documentElement;
var body = document2.body;
rootRect = {
x: 0,
y: 0,
top: 0,
left: 0,
right: html.clientWidth || body.clientWidth,
width: html.clientWidth || body.clientWidth,
bottom: html.clientHeight || body.clientHeight,
height: html.clientHeight || body.clientHeight
};
}
return this._expandRectByRootMargin(rootRect);
};
IntersectionObserver2.prototype._expandRectByRootMargin = function(rect) {
var margins = this._rootMarginValues.map(function(margin, i) {
return margin.unit == "px" ? margin.value : margin.value * (i % 2 ? rect.width : rect.height) / 100;
});
var newRect = {
top: rect.top - margins[0],
right: rect.right + margins[1],
bottom: rect.bottom + margins[2],
left: rect.left - margins[3]
};
newRect.width = newRect.right - newRect.left;
newRect.height = newRect.bottom - newRect.top;
newRect.x = newRect.left;
newRect.y = newRect.top;
return newRect;
};
IntersectionObserver2.prototype._hasCrossedThreshold = function(oldEntry, newEntry) {
var oldRatio = oldEntry && oldEntry.isIntersecting ? oldEntry.intersectionRatio || 0 : -1;
var newRatio = newEntry.isIntersecting ? newEntry.intersectionRatio || 0 : -1;
if (oldRatio === newRatio) return;
for (var i = 0; i < this.thresholds.length; i++) {
var threshold = this.thresholds[i];
if (threshold == oldRatio || threshold == newRatio || threshold < oldRatio !== threshold < newRatio) {
return true;
}
}
};
IntersectionObserver2.prototype._rootIsInDom = function() {
return !this.root || containsDeep(document2, this.root);
};
IntersectionObserver2.prototype._rootContainsTarget = function(target) {
return containsDeep(this.root || document2, target);
};
IntersectionObserver2.prototype._registerInstance = function() {
if (registry.indexOf(this) < 0) {
registry.push(this);
}
};
IntersectionObserver2.prototype._unregisterInstance = function() {
var index = registry.indexOf(this);
if (index != -1) registry.splice(index, 1);
};
function now() {
return window2.performance && performance.now && performance.now();
}
function throttle(fn, timeout) {
var timer = null;
return function() {
if (!timer) {
timer = setTimeout(function() {
fn();
timer = null;
}, timeout);
}
};
}
function addEvent(node, event, fn, opt_useCapture) {
if (typeof node.addEventListener == "function") {
node.addEventListener(event, fn, opt_useCapture || false);
} else if (typeof node.attachEvent == "function") {
node.attachEvent("on" + event, fn);
}
}
function removeEvent(node, event, fn, opt_useCapture) {
if (typeof node.removeEventListener == "function") {
node.removeEventListener(event, fn, opt_useCapture || false);
} else if (typeof node.detatchEvent == "function") {
node.detatchEvent("on" + event, fn);
}
}
function computeRectIntersection(rect1, rect2) {
var top = Math.max(rect1.top, rect2.top);
var bottom = Math.min(rect1.bottom, rect2.bottom);
var left = Math.max(rect1.left, rect2.left);
var right = Math.min(rect1.right, rect2.right);
var width = right - left;
var height = bottom - top;
return width >= 0 && height >= 0 && {
x: left,
y: top,
top,
bottom,
left,
right,
width,
height
};
}
function getBoundingClientRect(el) {
var rect;
try {
rect = el.getBoundingClientRect();
} catch (err) {
}
if (!rect) return getEmptyRect();
if (!(rect.width && rect.height && rect.x && rect.y)) {
rect = {
x: rect.left,
y: rect.top,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.right - rect.left,
height: rect.bottom - rect.top
};
}
return rect;
}
function getEmptyRect() {
return {
x: 0,
y: 0,
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 0,
height: 0
};
}
function containsDeep(parent, child) {
var node = child;
while (node) {
if (node == parent) return true;
node = getParentNode(node);
}
return false;
}
function getParentNode(node) {
var parent = node.parentNode;
if (parent && parent.nodeType == 11 && parent.host) {
return parent.host;
}
if (parent && parent.assignedSlot) {
return parent.assignedSlot.parentNode;
}
return parent;
}
window2.IntersectionObserver = IntersectionObserver2;
window2.IntersectionObserverEntry = IntersectionObserverEntry;
})(window, document);
}
}).call("object" === typeof window && window || "object" === typeof self && self || "object" === typeof global && global || {});
}
});
// dist/FloatTOC/FloatTOC.js
require_IntersectionObserver();
//! src/FloatTOC/options.json
var elementId = "floatTOC";
//! src/FloatTOC/modules/core.ts
var import_ext_gadget2 = require("ext.gadget.Util");
var import_ext_gadget3 = require("ext.gadget.FilterAlteredClicks");
//! src/FloatTOC/modules/i18n.ts
var import_ext_gadget = require("ext.gadget.i18n");
var getI18nMessages = () => {
return {
Close: (0, import_ext_gadget.localize)({
en: "Close",
ja: "閉じる",
"zh-hans": "关闭",
"zh-hant": "關閉"
}),
Contents: (0, import_ext_gadget.localize)({
en: "Contents",
ja: "目次",
zh: "目录"
}),
Collapse: (0, import_ext_gadget.localize)({
en: "Collapse",
ja: "折り畳み",
"zh-hans": "折叠",
"zh-hant": "摺叠"
}),
Expand: (0, import_ext_gadget.localize)({
en: "Expand",
ja: "展開",
"zh-hans": "展开",
"zh-hant": "展開"
})
};
};
var i18nMessages = getI18nMessages();
var getMessage = (key) => {
return i18nMessages[key] || key;
};
//! src/FloatTOC/modules/util/generateElements.ts
var generateElements = (originToc) => {
var _toc$querySelector, _toc$querySelector2;
const toc = originToc.cloneNode(true);
(_toc$querySelector = toc.querySelector("input")) === null || _toc$querySelector === void 0 || _toc$querySelector.remove();
(_toc$querySelector2 = toc.querySelector(".toctogglespan")) === null || _toc$querySelector2 === void 0 || _toc$querySelector2.remove();
const $toc = $(toc);
const $floatToc = $toc.clone().removeAttr("id").prepend($("<span>").addClass("oo-ui-indicatorElement-indicator oo-ui-icon-close").attr({
id: "close",
title: getMessage("Close"),
role: "button",
tabindex: "0"
}));
const $floatTocOpener = $("<div>").addClass("noprint").attr({
id: "floatToc-opener",
title: getMessage("Contents"),
role: "button",
tabindex: "0"
}).append($("<span>").addClass("oo-ui-indicatorElement-indicator oo-ui-icon-reference"), $("<span>").text(getMessage("Contents")));
return {
$floatToc,
$floatTocOpener
};
};
//! src/FloatTOC/modules/util/generateTogglerElement.ts
var generateTogglerElement = (isCollapse) => {
let $toggler = $("<span>").addClass("oo-ui-indicatorElement-indicator oo-ui-icon-downTriangle");
$toggler = isCollapse ? $toggler.attr("title", getMessage("Expand")) : $toggler.attr("title", getMessage("Collapse")).addClass("collapse");
return $toggler;
};
//! src/FloatTOC/modules/getConfig.ts
var getConfig = (id) => {
let config = mw.storage.getObject(id);
config || (config = {
floatTOC: window.outerHeight < window.outerWidth ? "open" : "close",
originTOC: "open"
});
return config;
};
//! src/FloatTOC/modules/setMwNotifyStyle.ts
var setMwNotifyStyle = () => {
const style = mw.loader.addStyleTag(".mw-notification-area{right:unset;width:auto;max-width:20em}.mw-notification{-webkit-transform:translateX(-999px);-moz-transform:translateX(-999px);transform:translateX(-999px)}.mw-notification-visible{-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0)}");
style.disabled = true;
return style;
};
//! src/FloatTOC/modules/core.ts
var floatTOC = ($originToc) => {
const {
skin
} = mw.config.get();
const originToc = $originToc.get(0);
const $body = $originToc.parents("body");
const {
$floatToc,
$floatTocOpener
} = generateElements(originToc);
$floatTocOpener.hide().appendTo($body);
const config = getConfig(elementId);
const mwNotifyStyle = setMwNotifyStyle();
let isShow = false;
const storeState = (target, state) => {
config[target] = state;
mw.storage.setObject(elementId, config);
};
let disableMwNotifyStyleTimer;
const disableMwNotifyStyle = () => {
if (disableMwNotifyStyleTimer) {
clearTimeout(disableMwNotifyStyleTimer);
}
disableMwNotifyStyleTimer = setTimeout(() => {
if (!isShow) {
mwNotifyStyle.disabled = true;
}
}, 5 * 1e3);
};
let notification;
const closeNotification = (currentNotification) => {
currentNotification.close();
$floatTocOpener.fadeIn();
storeState("floatTOC", "close");
disableMwNotifyStyle();
};
const smoothScroll = (event) => {
if (skin === "citizen") {
return;
}
const target = event.target;
const $target = $(target).parent();
const href = $target.attr("href");
if (!href) {
return;
}
const anchorOffset = $(href).offset();
if (!anchorOffset) {
return;
}
event.preventDefault();
(0, import_ext_gadget2.scrollTop)("".concat(anchorOffset.top, "px"));
};
const toggleToc = (currentIsShow = true, preNotification = void 0) => {
var _preNotification;
(_preNotification = preNotification) === null || _preNotification === void 0 || _preNotification.close();
isShow = !!currentIsShow;
switch (currentIsShow) {
case true:
if (config.floatTOC === "close") {
$floatTocOpener.fadeIn();
return;
}
break;
case "open":
$floatTocOpener.fadeOut();
storeState("floatTOC", "open");
break;
default:
$floatTocOpener.fadeOut();
disableMwNotifyStyle();
return;
}
mwNotifyStyle.disabled = false;
if (preNotification) {
preNotification.start();
} else {
preNotification = mw.notification.notify($floatToc, {
classes: "noprint",
id: elementId,
autoHide: false
});
const notificationListener = (event) => {
event.stopPropagation();
if (!(0, import_ext_gadget2.checkA11yConfirmKey)(event)) {
return;
}
const target = event.target;
if (target.id === "close") {
closeNotification(preNotification);
} else {
smoothScroll(event);
}
};
preNotification.$notification.on("click", (0, import_ext_gadget3.filterAlteredClicks)((event) => {
void notificationListener(event);
}));
preNotification.$notification.on("keydown", notificationListener);
}
return preNotification;
};
const observerCallback = (entries) => {
const [entry] = entries;
if (!entry) {
return;
}
const {
intersectionRatio
} = entry;
notification = toggleToc(intersectionRatio === 0, notification);
};
const intersectionObserver = new IntersectionObserver(observerCallback);
intersectionObserver.observe(originToc);
const openerListener = (event) => {
event.preventDefault();
if (!(0, import_ext_gadget2.checkA11yConfirmKey)(event)) {
return;
}
notification = toggleToc("open");
};
$floatTocOpener.on("click", openerListener);
$floatTocOpener.on("keydown", openerListener);
const collapseOriginToc = () => {
if (skin !== "citizen") {
return;
}
const isCollapse = config.originTOC === "close";
const $originTocTitle = $body.find("#toc .toctitle");
const $originTocItem = $body.find("#toc ul");
const $toggler = generateTogglerElement(isCollapse);
$originTocTitle.append($toggler);
const collapseToggle = () => {
const $element = $originTocTitle.find(".oo-ui-indicatorElement-indicator");
$element.toggleClass("collapse");
if (isCollapse) {
$element.attr("title", getMessage("Expand"));
} else {
$element.attr("title", getMessage("Collapse"));
}
};
$originTocTitle.on("click", () => {
storeState("originTOC", isCollapse ? "open" : "close");
collapseToggle();
$originTocItem.fadeToggle();
});
if (isCollapse) {
$originTocItem.fadeOut();
}
};
collapseOriginToc();
};
//! src/FloatTOC/FloatTOC.ts
var import_ext_gadget4 = require("ext.gadget.Util");
void (0, import_ext_gadget4.getBody)().then(($body) => {
const $originToc = $body.find("#toc");
if (!$originToc.length) {
return;
}
floatTOC($originToc);
});
})();