匿名
未登录
中文(中国大陆)
登录
深色模式
「荏苒之境」
搜索
查看“︁MediaWiki:Timeless.js”︁的源代码
来自「荏苒之境」
命名空间
系统消息
讨论
更多
更多
页面操作
阅读
查看源代码
历史
清除缓存
←
MediaWiki:Timeless.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。 如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* 将为Timeless皮肤的用户加载此处的所有JavaScript */ // 目标容器:确认一下你们布局里“右侧侧边栏”对应哪个元素。 // Timeless 默认布局中,#mw-site-navigation 是左侧站点导航, // #mw-related-navigation 才是右侧栏;如果确实要放右侧,把下面的选择器换掉。 const SIDEBAR_SELECTOR = '#mw-site-navigation'; 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; const tocTextList = Array.from(toc.querySelectorAll('.toctext')); this.tocTexts = new Map(tocTextList.map(t => { const 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) { const text = this.tocTexts.get(lastHeading); if (text) text.classList.remove('current-heading'); } this.lastHeading = heading; if (heading) { const 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; } const heading = this.tracker.getNearestHeadingAboveViewport(); this.setCurrentHeading(heading ? heading.textContent : null); } } const userAgent = navigator.userAgent; // 原来的 isSafari 会把 Chrome/Edge 也误判成 Safari(它们的 UA 里都带 Safari),这里排除一下 const isSafari = /Safari/i.test(userAgent) && !/Chrome|Chromium|CriOS|Edg|OPR/i.test(userAgent); mw.hook('ve.newTarget').add(target => { if (target.constructor.static.name !== 'article') { return; } target.on('teardown', () => { const event = new CustomEvent('pageupdate'); document.dispatchEvent(event); }); }); let toc = null; let tocInitialParent = null; let tocAnchor = null; // toc 在文章里的原始“下一个兄弟节点”,用于放回去 let floatingToC = null; let siteNavigation = null; // 关键修复1:先查一次初始 DOM,元素已经存在就直接回调; // MutationObserver 只在元素是“之后”被插入时才兜底。 // 原来只挂 observer,而皮肤脚本运行时 DOM 早已解析完, // #toc / #mw-site-navigation 已经存在,observer 永远不会触发,整段逻辑成了死代码。 function waitForElement(selector, callback) { const existing = document.querySelector(selector); if (existing) { callback(existing); return; } // body 还没生成时先观察 documentElement,避免 observe(null) 直接抛错中断脚本 const root = document.body || document.documentElement; const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); callback(element); } }); observer.observe(root, { childList: true, subtree: true }); } const isWideEnough = () => (isSafari ? window.innerWidth > 1354 : window.innerWidth > 1339); const updateToCParent = () => { if (siteNavigation == null || toc == null) return; if (isWideEnough()) { // 宽屏:确保 toc 挂在侧边栏里 if (toc.parentNode !== siteNavigation) { siteNavigation.appendChild(toc); } } else if (toc.parentNode === siteNavigation) { // 窄屏:放回文章里原来的位置。 // 原来那句 siteNavigation.parentNode.lastChild.before(toc) // 会把它插到导航容器的某个兄弟节点前面,既不是文章原位,位置也不可控。 if (tocAnchor && tocAnchor.parentNode === tocInitialParent) { tocInitialParent.insertBefore(toc, tocAnchor); } else { tocInitialParent.appendChild(toc); } } }; const initToC = (tocElement) => { if (floatingToC) { floatingToC.remove(); floatingToC = null; } toc = tocElement; tocInitialParent = tocElement.parentNode; tocAnchor = tocElement.nextSibling; floatingToC = new FloatingToC(tocElement); updateToCParent(); }; document.addEventListener('pageupdate', () => { const newToC = document.querySelector('.mw-parser-output > #toc'); if (!newToC) return; // 同一颗 toc(比如事件重复派发),没必要先移除再重建 if (newToC === toc && floatingToC) { updateToCParent(); return; } initToC(newToC); }); // 关键修复2:侧边栏回调里也补一次 updateToCParent。 // 如果 #toc 先初始化而 #mw-site-navigation 还没就位, // 原来 updateToCParent 会直接 return,之后没有任何地方再触发它,toc 就永远卡在文章里。 waitForElement(SIDEBAR_SELECTOR, elem => { siteNavigation = elem; updateToCParent(); }); waitForElement('#toc', initToC); // 关键修复3:窗口在断点两侧变化时也要重新摆放 toc let resizeTimer = null; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(updateToCParent, 150); });
返回
MediaWiki:Timeless.js
。
导航
导航
最近更改
随机页面
特殊页面
模板列表
wiki工具
wiki工具
Cargo数据
页面工具
页面工具
用户页面工具
更多
链入页面
相关更改
页面信息
页面日志