MediaWiki:Timeless.js:修订间差异
来自「荏苒之境」
无编辑摘要 |
无编辑摘要 |
||
| (未显示同一用户的2个中间版本) | |||
| 第103行: | 第103行: | ||
const userAgent = navigator.userAgent; | const userAgent = navigator.userAgent; | ||
const isSafari = userAgent.includes("Safari"); | // Chrome/Edge 的 UA 里同样包含 "Safari",需要排除掉,否则宽度阈值会误用 | ||
const isSafari = userAgent.includes("Safari") && !/(Chrome|CriOS|Edg|OPR|Android)/i.test(userAgent); | |||
mw.hook('ve.newTarget').add(target => { | mw.hook('ve.newTarget').add(target => { | ||
| 第121行: | 第122行: | ||
const updateToCParent = () => { | const updateToCParent = () => { | ||
if (siteNavigation == null) { | // 两个依赖元素都就绪后才真正搬移,避免竞态导致 ToC 一直留在文章容器里 | ||
if (siteNavigation == null || toc == null) { | |||
return; | return; | ||
} | } | ||
| 第154行: | 第156行: | ||
function waitForElement(selector, callback) { | function waitForElement(selector, callback) { | ||
const observer = new MutationObserver( | // 元素如果已经存在于 DOM(ResourceLoader 的脚本常在页面渲染后才执行), | ||
// 必须立刻回调;否则只挂 MutationObserver 会永远等不到变化,整段逻辑都不运行。 | |||
const existing = document.querySelector(selector); | |||
if (existing) { | |||
callback(existing); | |||
return; | |||
} | |||
const observer = new MutationObserver(() => { | |||
const element = document.querySelector(selector); | const element = document.querySelector(selector); | ||
if (element) { | if (element) { | ||
observer.disconnect(); // | observer.disconnect(); // 找到后停止观察 | ||
callback(element); | callback(element); | ||
} | } | ||
| 第166行: | 第175行: | ||
waitForElement('#mw-site-navigation', elem => { | waitForElement('#mw-site-navigation', elem => { | ||
siteNavigation = elem; | siteNavigation = elem; | ||
// 侧栏就绪后必须重新触发一次搬移判断:如果 ToC 先加载完成, | |||
// 上面那次 updateToCParent 会因 siteNavigation 仍为 null 提前 return, | |||
// 之后没有任何代码再搬它,卡片就会一直卡在文章容器里。 | |||
updateToCParent(); | |||
}); | }); | ||
| 第174行: | 第187行: | ||
updateToCParent(); | updateToCParent(); | ||
}); | }); | ||
// 窗口宽度变化时重新判断 ToC 该留在侧栏还是回到正文,避免卡片停在错误的容器里 | |||
window.addEventListener('resize', updateToCParent); | |||
2026年9月3日 (四) 21:55的最新版本
/* 将为Timeless皮肤的用户加载此处的所有JavaScript */
class HeadingTracker {
constructor(rootElement) {
this.rootElement = rootElement
this.headings = [];
this.cacheExpired = true;
this.init();
}
init() {
this.collectHeadings();
window.addEventListener('resize', () => this.cacheExpired = true);
window.addEventListener('DOMContentLoaded', () => this.cacheExpired = true);
}
collectHeadings() {
const rawHeadings = Array.from(this.rootElement.querySelectorAll('h1, h2, h3, h4, h5, h6'));
this.headings = rawHeadings
.map(heading => ({
element: heading,
top: heading.getBoundingClientRect().top + window.scrollY
}))
.sort((a, b) => a.top - b.top);
this.cacheExpired = false;
}
getNearestHeadingAboveViewport() {
if (this.cacheExpired) this.collectHeadings();
const scrollY = window.scrollY + 60;
let left = 0, right = this.headings.length - 1;
let result = null;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (this.headings[mid].top <= scrollY) {
result = this.headings[mid].element;
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
}
class FloatingToC {
constructor(toc) {
this.element = toc;
this.ignoreScrollUpdate = false;
let tocTextList = Array.from(toc.querySelectorAll(".toctext"));
this.tocTexts = new Map(tocTextList.map(t => {
let textContent = t.textContent
t.parentElement.onclick = () => {
this.ignoreScrollUpdate = true;
this.setCurrentHeading(textContent);
};
return [textContent, t];
}));
this.tracker = new HeadingTracker(
document.getElementById('mw-content-text'));
this.scrollHandler = this.onScroll.bind(this);
window.addEventListener('scroll', this.scrollHandler);
}
remove() {
window.removeEventListener('scroll', this.scrollHandler);
this.element.remove();
}
setCurrentHeading(heading) {
const lastHeading = this.lastHeading;
if (heading == lastHeading) {
return;
}
if (lastHeading) {
let text = this.tocTexts.get(lastHeading)
if (text) text.classList.remove('current-heading');
}
this.lastHeading = heading;
if (heading) {
let text = this.tocTexts.get(heading)
if (text) text.classList.add('current-heading');
}
};
onScroll() {
if (window.innerWidth < 1100) {
return;
}
if (this.ignoreScrollUpdate) {
this.ignoreScrollUpdate = false;
return;
}
let heading = this.tracker.getNearestHeadingAboveViewport();
this.setCurrentHeading(heading ? heading.textContent : null);
}
}
const userAgent = navigator.userAgent;
// Chrome/Edge 的 UA 里同样包含 "Safari",需要排除掉,否则宽度阈值会误用
const isSafari = userAgent.includes("Safari") && !/(Chrome|CriOS|Edg|OPR|Android)/i.test(userAgent);
mw.hook('ve.newTarget').add(target => {
if (target.constructor.static.name !== 'article') {
return;
}
target.on('teardown', () => {
var event = new CustomEvent("pageupdate");
document.dispatchEvent(event);
});
});
let toc = null;
let tocInitialParent = null;
let floatingToC = null;
let siteNavigation = null;
const updateToCParent = () => {
// 两个依赖元素都就绪后才真正搬移,避免竞态导致 ToC 一直留在文章容器里
if (siteNavigation == null || toc == null) {
return;
}
let width = window.innerWidth;
if (isSafari && width > 1354 || !isSafari && width > 1339) {
if (toc.parentNode != siteNavigation) {
toc.parentNode.removeChild(toc);
siteNavigation.appendChild(toc);
}
}
else {
if (toc.parentNode == tocInitialParent || toc.parentNode == siteNavigation) {
toc.parentNode.removeChild(toc);
siteNavigation.parentNode.lastChild.before(toc);
}
}
}
document.addEventListener('pageupdate', () => {
let newToC = document.querySelectorAll('.mw-parser-output > #toc')[0];
if (newToC == null) {
return;
}
if (floatingToC != null) {
floatingToC.remove();
}
toc = newToC;
tocInitialParent = toc.parentNode;
floatingToC = new FloatingToC(toc);
updateToCParent();
});
function waitForElement(selector, callback) {
// 元素如果已经存在于 DOM(ResourceLoader 的脚本常在页面渲染后才执行),
// 必须立刻回调;否则只挂 MutationObserver 会永远等不到变化,整段逻辑都不运行。
const existing = document.querySelector(selector);
if (existing) {
callback(existing);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect(); // 找到后停止观察
callback(element);
}
});
observer.observe(document.body, { childList: true, subtree: true }); // 观察整个文档的变化
}
waitForElement('#mw-site-navigation', elem => {
siteNavigation = elem;
// 侧栏就绪后必须重新触发一次搬移判断:如果 ToC 先加载完成,
// 上面那次 updateToCParent 会因 siteNavigation 仍为 null 提前 return,
// 之后没有任何代码再搬它,卡片就会一直卡在文章容器里。
updateToCParent();
});
waitForElement('#toc', loadedToc => {
toc = loadedToc;
tocInitialParent = toc.parentElement;
floatingToC = new FloatingToC(toc);
updateToCParent();
});
// 窗口宽度变化时重新判断 ToC 该留在侧栏还是回到正文,避免卡片停在错误的容器里
window.addEventListener('resize', updateToCParent);