MediaWiki:Common.js
来自Mindustry中文wiki
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。
- 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。
/* MediaWiki:Common.js */
(function () {
'use strict';
function fallbackCopy(text) {
if (!window.jQuery) return;
var $ = window.jQuery;
var $temp = $('<textarea>');
$('body').append($temp);
$temp.val(text).select();
document.execCommand('copy');
$temp.remove();
alert('复制成功!');
}
// 点击按钮复制内容:<button class="copy-button" data-text="...">复制</button>
if (window.jQuery) {
window.jQuery(function ($) {
$(document).on('click', '.copy-button', function () {
var text = $(this).attr('data-text') || '';
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
alert('复制成功!');
}, function () {
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
});
});
}
// Timeless:把 Sidebar 里以 "-- " 开头的条目变成可折叠子项
mw.loader.using(['mediawiki.util'], function () {
if (mw.config.get('skin') !== 'timeless') return;
function initNavTree() {
var siteNav = document.getElementById('mw-site-navigation');
if (!siteNav) return;
var pNav = siteNav.querySelector('#p-navigation');
if (!pNav) return;
var root = pNav.querySelector('.mw-portlet-body > ul') ||
pNav.querySelector('ul');
if (!root) return;
if (root.getAttribute('data-mdtNav') === '1') return;
root.setAttribute('data-mdtNav', '1');
var KEY = 'mdtNavTreeOpen';
var store = {};
try {
store = JSON.parse(localStorage.getItem(KEY) || '{}');
} catch (e) {
store = {};
}
function getKey(li) {
if (li && li.id) return li.id;
var a = li ? li.querySelector('a') : null;
return a ? (a.getAttribute('href') || '') : '';
}
function setStored(li, open) {
var k = getKey(li);
if (!k) return;
store[k] = open;
try {
localStorage.setItem(KEY, JSON.stringify(store));
} catch (e) {}
}
function setOpen(li, open, persist) {
li.classList.toggle('mdtNavOpen', open);
if (persist) setStored(li, open);
}
function ensureGroup(li) {
li.classList.add('mdtNavGroup');
var sub = li.querySelector('ul.mdtNavSublist');
if (!sub) {
sub = document.createElement('ul');
sub.className = 'mdtNavSublist';
li.appendChild(sub);
}
var btn = li.querySelector('button.mdtNavToggle');
if (!btn) {
btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mdtNavToggle';
btn.setAttribute('aria-label', '展开/收起');
btn.setAttribute('aria-expanded', 'false');
li.insertBefore(btn, sub);
btn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var isOpen = li.classList.contains('mdtNavOpen');
setOpen(li, !isOpen, true);
btn.setAttribute('aria-expanded', String(!isOpen));
});
}
var k = getKey(li);
if (store[k] === true) {
li.classList.add('mdtNavOpen');
btn.setAttribute('aria-expanded', 'true');
}
return sub;
}
var items = Array.prototype.slice.call(root.children);
var lastAtDepth = [];
for (var i = 0; i < items.length; i++) {
var li = items[i];
if (!li || li.tagName !== 'LI') continue;
var a = li.querySelector('a');
if (!a) continue;
var raw = (a.textContent || '').trim();
var m = raw.match(/^(-{2,})\s*(.*)$/);
var depth = 0;
if (m) {
depth = Math.min(Math.floor(m[1].length / 2), 3);
a.textContent = m[2];
}
li.classList.add('mdtNavItem');
if (depth > 0 && lastAtDepth[depth - 1]) {
li.classList.add('mdtNavSubItem');
li.classList.add('mdtNavDepth' + String(depth));
var sublist = ensureGroup(lastAtDepth[depth - 1]);
sublist.appendChild(li);
} else {
depth = 0;
}
lastAtDepth[depth] = li;
lastAtDepth.length = depth + 1;
}
// 当前页面高亮 + 自动展开父级
var current = (mw.config.get('wgPageName') || '').replace(/ /g, '_');
var links = pNav.querySelectorAll('a');
function titleFromHref(href) {
var t = mw.util.getParamValue('title', href);
if (t) return t.replace(/ /g, '_');
var mark = '/index.php/';
var pos = href.indexOf(mark);
if (pos !== -1) {
return decodeURIComponent(href.slice(pos + mark.length))
.replace(/ /g, '_');
}
return '';
}
for (var j = 0; j < links.length; j++) {
var href = links[j].getAttribute('href') || '';
var page = titleFromHref(href);
if (page && page === current) {
links[j].classList.add('mdtNavActive');
var n = links[j].parentNode;
while (n && n !== root) {
if (n.tagName === 'LI' && n.classList.contains('mdtNavGroup')) {
setOpen(n, true, false);
var btn = n.querySelector('button.mdtNavToggle');
if (btn) btn.setAttribute('aria-expanded', 'true');
}
n = n.parentNode;
}
break;
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNavTree);
} else {
initNavTree();
}
});
// 可折叠标题:在标题旁边加一个展开/收起按钮
// 用法:把任意标题写成:== 战役区块 <span class="mdtFold"></span> ==
// 默认收起:== 战役区块 <span class="mdtFold mdtFoldClosed"></span> ==
mw.loader.using(['mediawiki.util'], function () {
var skin = String(mw.config.get('skin') || '').toLowerCase();
if (skin !== 'timeless') return;
var action = String(mw.config.get('wgAction') || 'view').toLowerCase();
if (action === 'edit' || action === 'submit') return;
function findHeading(node) {
while (node && node !== document.body) {
if (node.nodeType === 1 && /^H[1-6]$/.test(node.tagName)) return node;
node = node.parentNode;
}
return null;
}
var KEY = 'mdtFoldSections';
var store = {};
try {
store = JSON.parse(localStorage.getItem(KEY) || '{}');
} catch (e) {
store = {};
}
function saveStore() {
try {
localStorage.setItem(KEY, JSON.stringify(store));
} catch (e) {}
}
function getHeadingBlock(headingEl) {
if (!headingEl || !headingEl.parentElement) return headingEl;
// MediaWiki 新版可能会把 h2 包在 <div class="mw-heading"> 里。
// 如果不处理这个 wrapper,折叠只会影响 wrapper 内部(看起来“没效果”)。
var p = headingEl.parentElement;
if (p.classList && p.classList.contains('mw-heading')) return p;
return headingEl;
}
function getHeadingLevelFromNode(node) {
if (!node || node.nodeType !== 1) return null;
if (/^H[1-6]$/.test(node.tagName)) {
return parseInt(String(node.tagName).slice(1), 10) || 6;
}
if (node.classList && node.classList.contains('mw-heading')) {
var h = node.querySelector('h1,h2,h3,h4,h5,h6');
if (h && /^H[1-6]$/.test(h.tagName)) {
return parseInt(String(h.tagName).slice(1), 10) || 6;
}
}
return null;
}
function initFoldSections(root) {
var content = root || document.getElementById('mw-content-text') ||
document.querySelector('.mw-parser-output') ||
document.body;
if (!content) return;
var nodes = [];
var markers = content.querySelectorAll('.mdtFold');
var headingNodes = content.querySelectorAll('h1.mdtFoldHeading,h2.mdtFoldHeading,h3.mdtFoldHeading,h4.mdtFoldHeading,h5.mdtFoldHeading,h6.mdtFoldHeading');
for (var h = 0; h < headingNodes.length; h++) nodes.push(headingNodes[h]);
for (var m = 0; m < markers.length; m++) nodes.push(markers[m]);
if (!nodes.length) return;
for (var i = 0; i < nodes.length; i++) {
(function (nodeOrMarker) {
var marker = null;
var heading = null;
if (nodeOrMarker && nodeOrMarker.nodeType === 1 && /^H[1-6]$/.test(nodeOrMarker.tagName)) {
heading = nodeOrMarker;
} else {
marker = nodeOrMarker;
heading = findHeading(marker);
}
if (!heading) return;
// 兼容:如果是通过 h2.mdtFoldHeading 触发,但里面也有 span.mdtFold,
// 也要把 marker 当作触发点,这样可以读取默认收起并移除 marker。
if (!marker) {
marker = heading.querySelector('.mdtFold');
}
var headingBlock = getHeadingBlock(heading);
if (!headingBlock) return;
if (headingBlock.getAttribute('data-mdtFoldInit') === '1') return;
headingBlock.setAttribute('data-mdtFoldInit', '1');
var level = parseInt(String(heading.tagName).slice(1), 10) || 2;
var headline = heading.querySelector('.mw-headline');
// Body: 优先折叠“紧跟着”的图标网格(你这个场景就是 h2 + .mdtIconGrid)。
// 也支持手动标记:<div class="mdtFoldBody">...</div>
var body = null;
var nextEl = headingBlock.nextElementSibling;
if (nextEl && nextEl.classList) {
if (nextEl.classList.contains('mdtFoldBody') || nextEl.classList.contains('mdtIconGrid')) {
body = nextEl;
body.classList.add('mdtFoldBody');
}
}
// 自动模式:把标题后面直到下一个“同级或更高级标题”的所有节点包起来。
if (!body) {
body = document.createElement('div');
body.className = 'mdtFoldBody';
var node = headingBlock.nextSibling;
while (node) {
var next = node.nextSibling;
var nodeLevel = getHeadingLevelFromNode(node);
if (nodeLevel !== null && nodeLevel <= level) break;
body.appendChild(node);
node = next;
}
headingBlock.parentNode.insertBefore(body, node);
}
// Toggle button
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mdtFoldToggle';
btn.setAttribute('aria-label', '展开/收起');
btn.setAttribute('aria-expanded', 'true');
btn.title = '展开/收起';
btn.textContent = '\u25BE';
// Inline style fallback: in case Timeless.css is not loaded yet.
btn.style.width = '1.6rem';
btn.style.height = '1.6rem';
btn.style.marginLeft = '0.25rem';
btn.style.border = '0';
btn.style.padding = '0';
btn.style.borderRadius = '10px';
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'center';
btn.style.background = 'rgba(0, 175, 137, 0.10)';
btn.style.color = 'var(--mdtBrand)';
btn.style.cursor = 'pointer';
btn.style.fontFamily = 'inherit';
btn.style.fontSize = '16px';
btn.style.fontWeight = '700';
btn.style.lineHeight = '1';
btn.style.boxShadow = 'none';
btn.style.outline = 'none';
btn.style.appearance = 'none';
btn.style.webkitAppearance = 'none';
if (headline) {
headline.classList.add('mdtFoldHeadline');
headline.appendChild(btn);
} else {
var wrap = document.createElement('span');
wrap.className = 'mdtFoldHeadline';
while (heading.firstChild) {
wrap.appendChild(heading.firstChild);
}
heading.appendChild(wrap);
wrap.appendChild(btn);
headline = wrap;
}
// Marker/heading default state
var defaultClosed = false;
if (marker && marker.classList && marker.classList.contains('mdtFoldClosed')) {
defaultClosed = true;
}
if (heading.classList && heading.classList.contains('mdtFoldClosed')) {
defaultClosed = true;
}
// Remove marker to avoid extra spacing
if (marker && marker.parentNode) marker.parentNode.removeChild(marker);
var page = (mw.config.get('wgPageName') || '').replace(/ /g, '_');
var id = '';
if (headline && headline.id) id = headline.id;
if (!id) id = (heading.textContent || '').trim();
var storageKey = page + '::' + id;
function setCollapsed(collapsed, persist) {
headingBlock.classList.toggle('mdtFoldCollapsed', collapsed);
btn.setAttribute('aria-expanded', String(!collapsed));
btn.textContent = collapsed ? '\u25B8' : '\u25BE';
if (body) {
body.classList.toggle('mdtFoldHidden', collapsed);
// Ensure it works even if CSS is missing/cached.
body.style.display = collapsed ? 'none' : '';
}
if (persist) {
store[storageKey] = collapsed;
saveStore();
}
}
var collapsed = false;
if (typeof store[storageKey] === 'boolean') {
collapsed = store[storageKey];
} else if (defaultClosed) {
collapsed = true;
}
setCollapsed(collapsed, false);
btn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var isCollapsed = headingBlock.classList.contains('mdtFoldCollapsed');
setCollapsed(!isCollapsed, true);
});
})(nodes[i]);
}
}
function runOnPage() {
initFoldSections(document.getElementById('mw-content-text') ||
document.querySelector('.mw-parser-output') ||
document.body);
}
// MediaWiki 推荐:内容渲染/替换后触发(兼容预览、部分皮肤/插件)。
if (mw.hook && window.jQuery) {
mw.hook('wikipage.content').add(function ($content) {
initFoldSections($content && $content[0] ? $content[0] : null);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runOnPage);
} else {
runOnPage();
}
});
})();
(function () {
'use strict';
/* Timeless: 左/右侧栏抽屉 + 首页按钮 */
mw.loader.using(['mediawiki.util'], function () {
if (mw.config.get('skin') !== 'timeless') return;
function onReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
var ICON_HOME =
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>';
var ICON_MENU =
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/></svg>';
var ICON_GEAR =
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96c-.5-.38-1.04-.7-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 2h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.59.24-1.13.56-1.63.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 8.48a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.83 14.52a.5.5 0 0 0-.12.64l1.92 3.32c.13.22.39.31.6.22l2.39-.96c.5.38 1.04.7 1.63.94l.36 2.54c.05.24.25.42.49.42h3.8c.24 0 .44-.18.49-.42l.36-2.54c.59-.24 1.13-.56 1.63-.94l2.39.96c.22.09.47 0 .6-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7z"/></svg>';
function wrapIcon(svg) {
var span = document.createElement('span');
span.className = 'mdtDockIcon';
span.innerHTML = svg;
return span;
}
onReady(function () {
if (document.getElementById('mdtDockLeft')) return;
var siteNav = document.getElementById('mw-site-navigation');
var relatedNav = document.getElementById('mw-related-navigation');
if (!siteNav && !relatedNav) return;
document.body.classList.add('mdtDockEnabled');
var backdrop = document.createElement('div');
backdrop.id = 'mdtDockBackdrop';
function syncBackdrop() {
var open =
document.body.classList.contains('mdtDockNavOpen') ||
document.body.classList.contains('mdtDockToolsOpen');
backdrop.classList.toggle('is-active', open);
}
function closeAll() {
document.body.classList.remove('mdtDockNavOpen', 'mdtDockToolsOpen');
syncBackdrop();
}
backdrop.addEventListener('click', closeAll);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeAll();
});
// 左侧:홈 + 汉堡
var left = document.createElement('div');
left.id = 'mdtDockLeft';
var home = document.createElement('a');
home.className = 'mdtDockBtn';
home.href = mw.util.getUrl('首页');
home.title = '首页';
home.setAttribute('aria-label', '首页');
home.appendChild(wrapIcon(ICON_HOME));
left.appendChild(home);
if (siteNav) {
var navBtn = document.createElement('button');
navBtn.type = 'button';
navBtn.className = 'mdtDockBtn';
navBtn.title = '导航';
navBtn.setAttribute('aria-label', '导航');
navBtn.appendChild(wrapIcon(ICON_MENU));
navBtn.addEventListener('click', function () {
var nowOpen = document.body.classList.toggle('mdtDockNavOpen');
if (nowOpen) document.body.classList.remove('mdtDockToolsOpen');
syncBackdrop();
});
left.appendChild(navBtn);
}
// 右侧:齿轮
var right = document.createElement('div');
right.id = 'mdtDockRight';
if (relatedNav) {
var toolsBtn = document.createElement('button');
toolsBtn.type = 'button';
toolsBtn.className = 'mdtDockBtn';
toolsBtn.title = '页面工具';
toolsBtn.setAttribute('aria-label', '页面工具');
toolsBtn.appendChild(wrapIcon(ICON_GEAR));
toolsBtn.addEventListener('click', function () {
var nowOpen = document.body.classList.toggle('mdtDockToolsOpen');
if (nowOpen) document.body.classList.remove('mdtDockNavOpen');
syncBackdrop();
});
left.appendChild(toolsBtn);
}
document.body.appendChild(backdrop);
document.body.appendChild(left);
document.body.appendChild(backdrop);
document.body.appendChild(left);
document.body.appendChild(right);
syncBackdrop();
});
});
})();
(function () {
'use strict';
function isTimeless() {
try {
return String(mw.config.get('skin') || '').toLowerCase() === 'timeless';
} catch (e) {
return false;
}
}
function isViewMode() {
var a = String(mw.config.get('wgAction') || 'view').toLowerCase();
return a === 'view';
}
function isMobileUA() {
var ua = String(navigator.userAgent || '');
return /Mobi|Android|iPhone|iPad|iPod/i.test(ua);
}
function normalizeText(text) {
return String(text || '')
.replace(/\s+/g, ' ')
.replace(/[\u25B8\u25BE]/g, '')
.trim();
}
function collectGroups(root) {
var nodes = root.querySelectorAll('h2, h3');
var groups = [];
var current = null;
for (var i = 0; i < nodes.length; i++) {
var h = nodes[i];
if (!h) continue;
var id = h.id || '';
if (!id) continue;
var text = normalizeText(h.textContent);
if (!text) continue;
if (h.tagName === 'H2') {
current = { id: id, text: text, children: [] };
groups.push(current);
} else {
if (!current) continue;
current.children.push({ id: id, text: text });
}
}
return groups;
}
function flattenIds(groups) {
var ids = [];
for (var i = 0; i < groups.length; i++) {
ids.push(groups[i].id);
var children = groups[i].children || [];
for (var j = 0; j < children.length; j++) {
ids.push(children[j].id);
}
}
return ids;
}
function buildPanel(groups) {
var wrap = document.createElement('div');
wrap.id = 'mdtFloatToc';
wrap.className = 'mdtFloatToc';
wrap.innerHTML =
"<button type='button' class='mdtFloatTocBtn' aria-label='目录'>≡</button>" +
"<div class='mdtFloatTocPanel' role='dialog' aria-label='目录'>" +
" <div class='mdtFloatTocHead'>" +
" <button type='button' class='mdtFloatTocTop'><span aria-hidden='true'>↑</span>返回顶部</button>" +
" <div class='mdtFloatTocTitle'>目录</div>" +
" <button type='button' class='mdtFloatTocClose' aria-label='关闭'>✕</button>" +
" </div>" +
" <ul class='mdtFloatTocList'></ul>" +
"</div>";
var ul = wrap.querySelector('.mdtFloatTocList');
for (var i = 0; i < groups.length; i++) {
(function (g) {
var li = document.createElement('li');
li.className = 'mdtFloatTocItem mdtFloatTocGroup';
var row = document.createElement('div');
row.className = 'mdtFloatTocRow';
var a = document.createElement('a');
a.href = '#' + encodeURIComponent(g.id).replace(/%2F/g, '/');
a.textContent = g.text;
row.appendChild(a);
if (g.children && g.children.length) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mdtFloatTocGroupToggle';
btn.setAttribute('aria-label', '展开/收起');
btn.setAttribute('aria-expanded', 'false');
btn.textContent = '▸';
row.appendChild(btn);
var sub = document.createElement('ul');
sub.className = 'mdtFloatTocSublist';
for (var j = 0; j < g.children.length; j++) {
var c = g.children[j];
var cli = document.createElement('li');
cli.className = 'mdtFloatTocItem';
cli.innerHTML = "<a href='#" + encodeURIComponent(c.id).replace(/%2F/g, '/') + "'>" +
mw.html.escape(c.text) + "</a>";
sub.appendChild(cli);
}
li.appendChild(row);
li.appendChild(sub);
btn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var open = li.classList.toggle('mdtFloatTocGroupOpen');
btn.setAttribute('aria-expanded', String(open));
btn.textContent = open ? '▾' : '▸';
});
} else {
li.appendChild(row);
}
ul.appendChild(li);
})(groups[i]);
}
function openPanel(open) {
wrap.classList.toggle('mdtFloatTocOpen', open);
}
wrap.querySelector('.mdtFloatTocBtn').addEventListener('click', function () {
openPanel(true);
});
wrap.querySelector('.mdtFloatTocClose').addEventListener('click', function () {
openPanel(false);
});
wrap.querySelector('.mdtFloatTocTop').addEventListener('click', function () {
openPanel(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// Click a toc item -> close + smooth scroll
wrap.addEventListener('click', function (ev) {
var a = ev.target && ev.target.closest ? ev.target.closest('a') : null;
if (!a) return;
var href = a.getAttribute('href') || '';
if (href.charAt(0) !== '#') return;
ev.preventDefault();
openPanel(false);
var id = decodeURIComponent(href.slice(1));
var target = document.getElementById(id);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
history.replaceState(null, '', '#' + encodeURIComponent(id));
}
});
// Close when tapping outside
document.addEventListener('click', function (ev) {
if (!wrap.classList.contains('mdtFloatTocOpen')) return;
if (wrap.contains(ev.target)) return;
openPanel(false);
});
// Highlight active heading
var linkById = {};
var links = wrap.querySelectorAll('a');
for (var k = 0; k < links.length; k++) {
var id2 = decodeURIComponent((links[k].getAttribute('href') || '').slice(1));
linkById[id2] = links[k];
}
var ids = flattenIds(groups);
var ticking = false;
function updateActive() {
ticking = false;
var bestId = '';
var bestTop = -Infinity;
for (var i2 = 0; i2 < ids.length; i2++) {
var el = document.getElementById(ids[i2]);
if (!el) continue;
var rect = el.getBoundingClientRect();
if (rect.top <= 120 && rect.top > bestTop) {
bestTop = rect.top;
bestId = ids[i2];
}
}
for (var idKey in linkById) {
linkById[idKey].classList.toggle('mdtFloatTocActive', idKey === bestId);
}
}
window.addEventListener('scroll', function () {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(updateActive);
}, { passive: true });
return wrap;
}
function ensureHeadingIds(root) {
var headings = root.querySelectorAll('h2, h3');
for (var i = 0; i < headings.length; i++) {
var h = headings[i];
if (h.id) continue;
var hl = h.querySelector('.mw-headline');
if (hl && hl.id) h.id = hl.id;
}
}
function initFloatToc($content) {
if (!isTimeless() || !isViewMode() || !isMobileUA()) return;
var content = $content && $content[0] ? $content[0] : null;
var root = content || document.querySelector('.mw-parser-output') || document.getElementById('mw-content-text');
if (!root) return;
if (document.getElementById('mdtFloatToc')) return;
ensureHeadingIds(root);
var groups = collectGroups(root);
if (!groups.length) return;
document.body.appendChild(buildPanel(groups));
}
mw.loader.using(['mediawiki.util', 'mediawiki.html']).then(function () {
if (mw.hook && window.jQuery) {
mw.hook('wikipage.content').add(initFloatToc);
}
initFloatToc(null);
});
})();
/*
MDT Float TOC (desktop: dock to top-right gutter, default open)
Paste this whole file into: MediaWiki:Common.js (very bottom)
*/
(function () {
'use strict';
var DESKTOP_MIN_WIDTH = 1000;
var GAP_PX = 12;
var FALLBACK_TOP_REM = 4.1;
function isViewMode() {
try {
return String(mw.config.get('wgAction') || 'view').toLowerCase() === 'view';
} catch (e) {
return true;
}
}
function isDesktop() {
try {
return (
window.matchMedia &&
window.matchMedia('(min-width: ' + String(DESKTOP_MIN_WIDTH) + 'px)').matches
);
} catch (e) {
return false;
}
}
function remToPx(rem) {
var fs = 16;
try {
fs = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
} catch (e) {}
return rem * fs;
}
function getDockTopPx() {
var dock = document.getElementById('mdtDockLeft') || document.getElementById('mdtDockRight');
if (dock && dock.getBoundingClientRect) {
return Math.round(dock.getBoundingClientRect().top);
}
return Math.round(remToPx(FALLBACK_TOP_REM));
}
function normalizeText(text) {
return String(text || '')
.replace(/\s+/g, ' ')
.replace(/[\u25B8\u25BE]/g, '')
.trim();
}
function ensureHeadingIds(root) {
var headings = root.querySelectorAll('h2, h3');
for (var i = 0; i < headings.length; i++) {
var h = headings[i];
if (!h || h.id) continue;
var hl = h.querySelector('.mw-headline');
if (hl && hl.id) h.id = hl.id;
}
}
function shouldSkipHeading(h) {
if (!h) return true;
if (!h.id) return true;
if (h.closest('#mw-site-navigation, #mw-related-navigation, .mw-portlet, .mdtRightRail')) return true;
return false;
}
function collectGroups(root) {
var nodes = root.querySelectorAll('h2, h3');
var groups = [];
var current = null;
for (var i = 0; i < nodes.length; i++) {
var h = nodes[i];
if (shouldSkipHeading(h)) continue;
var id = h.id;
var headline = h.querySelector('.mw-headline');
var text = normalizeText(headline ? headline.textContent : h.textContent);
if (!text) continue;
if (h.tagName === 'H2') {
current = { id: id, text: text, children: [] };
groups.push(current);
} else {
if (!current) continue;
current.children.push({ id: id, text: text });
}
}
return groups;
}
function flattenIds(groups) {
var ids = [];
for (var i = 0; i < groups.length; i++) {
ids.push(groups[i].id);
var children = groups[i].children || [];
for (var j = 0; j < children.length; j++) ids.push(children[j].id);
}
return ids;
}
function injectCssOnce() {
if (document.getElementById('mdtFloatTocStyle')) return;
var css = [
'#mw-content-text .mw-headline{scroll-margin-top:5rem;}',
/* wrapper (mobile default: bottom-right) */
'#mdtFloatToc{position:fixed;right:0.9rem;bottom:1.1rem;top:auto;z-index:9999;font-family:inherit;display:block !important;}',
/* open button */
'#mdtFloatToc .mdtFloatTocBtn{width:3rem;height:3rem;border-radius:999px;border:1px solid rgba(255,255,255,0.14);background:rgba(0,0,0,0.70);color:rgba(255,255,255,0.92);box-shadow:0 12px 28px rgba(0,0,0,0.28);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-weight:900;}',
'#mdtFloatToc .mdtFloatTocBtn:hover{border-color:rgba(0,207,160,0.60);box-shadow:0 12px 28px rgba(0,0,0,0.28),0 0 0 3px rgba(0,207,160,0.18);}',
'#mdtFloatToc.mdtFloatTocOpen .mdtFloatTocBtn{opacity:0;pointer-events:none;}',
/* panel (mobile default: popup) */
'#mdtFloatToc .mdtFloatTocPanel{position:absolute;right:0;bottom:3.75rem;width:min(22rem,88vw);max-height:min(70vh,34rem);overflow:auto;padding:0.85rem 0.9rem;border-radius:16px;background:rgba(36,46,45,0.92);border:1px solid rgba(255,255,255,0.14);box-shadow:0 18px 44px rgba(0,0,0,0.32);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:rgba(255,255,255,0.92);display:none;}',
'#mdtFloatToc.mdtFloatTocOpen .mdtFloatTocPanel{display:block;}',
/* head */
'#mdtFloatToc .mdtFloatTocHead{display:flex;align-items:center;justify-content:space-between;gap:0.75rem;padding:0 0 0.55rem 0;margin:0 0 0.6rem 0;border-bottom:1px solid rgba(255,255,255,0.10);}',
'#mdtFloatToc .mdtFloatTocTitle{font-weight:900;opacity:0.9;letter-spacing:0.06em;}',
'#mdtFloatToc .mdtFloatTocTop,#mdtFloatToc .mdtFloatTocClose{border:0;background:transparent;color:inherit;cursor:pointer;padding:0;font-family:inherit;}',
'#mdtFloatToc .mdtFloatTocTop{display:inline-flex;align-items:center;gap:0.55rem;font-weight:900;color:var(--mdtBrand2,#00cfa0);}',
'#mdtFloatToc .mdtFloatTocClose{width:2rem;height:2rem;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);}',
'#mdtFloatToc .mdtFloatTocClose:hover{background:rgba(255,255,255,0.12);}',
/* list */
'#mdtFloatToc .mdtFloatTocList{list-style:none;margin:0;padding:0 0 0 0.85rem;border-left:1px solid rgba(255,255,255,0.12);}',
'#mdtFloatToc .mdtFloatTocRow{display:flex;align-items:center;gap:0.35rem;}',
'#mdtFloatToc .mdtFloatTocItem{margin:0.18rem 0;}',
'#mdtFloatToc .mdtFloatTocItem a,#mdtFloatToc .mdtFloatTocItem a:visited{color:rgba(255,255,255,0.86);text-decoration:none;font-weight:750;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'#mdtFloatToc .mdtFloatTocItem a:hover{color:var(--mdtBrand2,#00cfa0);text-decoration:underline;}',
'#mdtFloatToc .mdtFloatTocItem a.mdtFloatTocActive{color:var(--mdtBrand2,#00cfa0);text-decoration:underline;}',
/* sub list */
'#mdtFloatToc .mdtFloatTocGroupToggle{border:0;background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.80);cursor:pointer;padding:0;width:2rem;height:2rem;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;}',
'#mdtFloatToc .mdtFloatTocGroupToggle:hover{background:rgba(255,255,255,0.12);}',
'#mdtFloatToc .mdtFloatTocGroup.mdtFloatTocGroupOpen .mdtFloatTocGroupToggle{color:var(--mdtBrand2,#00cfa0);}',
'#mdtFloatToc .mdtFloatTocSublist{list-style:none;margin:0.35rem 0 0.25rem;padding:0 0 0 0.85rem;border-left:1px solid rgba(255,255,255,0.12);display:none;}',
'#mdtFloatToc .mdtFloatTocGroup.mdtFloatTocGroupOpen .mdtFloatTocSublist{display:block;}'
].join('\n');
var style = document.createElement('style');
style.id = 'mdtFloatTocStyle';
style.type = 'text/css';
style.textContent = css;
document.head.appendChild(style);
}
function applyDesktopDock(wrap) {
if (!wrap) return;
var panel = wrap.querySelector('.mdtFloatTocPanel');
if (!panel) return;
if (!isDesktop()) {
wrap.style.top = '';
wrap.style.right = '';
wrap.style.bottom = '';
panel.style.position = '';
panel.style.top = '';
panel.style.right = '';
panel.style.bottom = '';
panel.style.left = '';
panel.style.width = '';
panel.style.maxHeight = '';
return;
}
var topPx = getDockTopPx();
wrap.style.top = String(topPx) + 'px';
wrap.style.right = String(GAP_PX) + 'px';
wrap.style.bottom = 'auto';
// Fit into the right gutter: don't cover the white content card
var content = document.getElementById('mw-content') || document.getElementById('content');
if (!content || !content.getBoundingClientRect) return;
var rect = content.getBoundingClientRect();
var leftPx = Math.round(rect.right + GAP_PX);
var available = Math.round(window.innerWidth - leftPx - GAP_PX);
// No right gutter -> keep mobile popup style
if (available < 220) return;
panel.style.position = 'fixed';
panel.style.top = String(topPx) + 'px';
panel.style.right = String(GAP_PX) + 'px';
panel.style.bottom = 'auto';
panel.style.left = String(leftPx) + 'px';
panel.style.width = 'auto';
panel.style.maxHeight = 'calc(100vh - ' + String(topPx + GAP_PX) + 'px)';
// Default open once per page load
if (wrap.getAttribute('data-mdtDefaultOpen') !== '1') {
wrap.classList.add('mdtFloatTocOpen');
wrap.setAttribute('data-mdtDefaultOpen', '1');
}
}
function buildPanel(groups) {
injectCssOnce();
var wrap = document.createElement('div');
wrap.id = 'mdtFloatToc';
wrap.className = 'mdtFloatToc';
var openBtn = document.createElement('button');
openBtn.type = 'button';
openBtn.className = 'mdtFloatTocBtn';
openBtn.setAttribute('aria-label', '\u76ee\u5f55');
openBtn.textContent = '\u2261';
wrap.appendChild(openBtn);
var panel = document.createElement('div');
panel.className = 'mdtFloatTocPanel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-label', '\u76ee\u5f55');
wrap.appendChild(panel);
var head = document.createElement('div');
head.className = 'mdtFloatTocHead';
panel.appendChild(head);
var topBtn = document.createElement('button');
topBtn.type = 'button';
topBtn.className = 'mdtFloatTocTop';
topBtn.textContent = '\u2191 \u8fd4\u56de\u9876\u90e8';
head.appendChild(topBtn);
var title = document.createElement('div');
title.className = 'mdtFloatTocTitle';
title.textContent = '\u76ee\u5f55';
head.appendChild(title);
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'mdtFloatTocClose';
closeBtn.setAttribute('aria-label', '\u5173\u95ed');
closeBtn.textContent = 'X';
head.appendChild(closeBtn);
var ul = document.createElement('ul');
ul.className = 'mdtFloatTocList';
panel.appendChild(ul);
function openPanel(open) {
wrap.classList.toggle('mdtFloatTocOpen', open);
}
openBtn.addEventListener('click', function () {
openPanel(true);
applyDesktopDock(wrap);
});
closeBtn.addEventListener('click', function () {
openPanel(false);
});
topBtn.addEventListener('click', function () {
if (!isDesktop()) openPanel(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
for (var i = 0; i < groups.length; i++) {
(function (g) {
var li = document.createElement('li');
li.className = 'mdtFloatTocItem mdtFloatTocGroup';
var row = document.createElement('div');
row.className = 'mdtFloatTocRow';
li.appendChild(row);
var a = document.createElement('a');
a.href = '#' + encodeURIComponent(g.id).replace(/%2F/g, '/');
a.textContent = g.text;
row.appendChild(a);
if (g.children && g.children.length) {
var toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'mdtFloatTocGroupToggle';
toggle.setAttribute('aria-label', '\u5c55\u5f00/\u6536\u8d77');
toggle.setAttribute('aria-expanded', 'false');
toggle.textContent = '>';
row.appendChild(toggle);
var sub = document.createElement('ul');
sub.className = 'mdtFloatTocSublist';
li.appendChild(sub);
for (var j = 0; j < g.children.length; j++) {
var c = g.children[j];
var cli = document.createElement('li');
cli.className = 'mdtFloatTocItem';
var ca = document.createElement('a');
ca.href = '#' + encodeURIComponent(c.id).replace(/%2F/g, '/');
ca.textContent = c.text;
cli.appendChild(ca);
sub.appendChild(cli);
}
toggle.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var open = li.classList.toggle('mdtFloatTocGroupOpen');
toggle.setAttribute('aria-expanded', String(open));
toggle.textContent = open ? 'v' : '>';
});
}
ul.appendChild(li);
})(groups[i]);
}
// Click a toc item -> smooth scroll
wrap.addEventListener('click', function (ev) {
var target = ev.target;
if (!target) return;
var a = target.closest ? target.closest('a') : null;
if (!a) return;
var href = a.getAttribute('href') || '';
if (href.charAt(0) !== '#') return;
ev.preventDefault();
if (!isDesktop()) openPanel(false);
var id = decodeURIComponent(href.slice(1));
var el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
try {
history.replaceState(null, '', '#' + encodeURIComponent(id));
} catch (e) {}
}
});
// Mobile only: tap outside to close. Desktop: do nothing.
document.addEventListener('click', function (ev) {
if (isDesktop()) return;
if (!wrap.classList.contains('mdtFloatTocOpen')) return;
if (wrap.contains(ev.target)) return;
openPanel(false);
});
// Highlight active heading
var linkById = {};
var links = wrap.querySelectorAll('a');
for (var k = 0; k < links.length; k++) {
var id2 = decodeURIComponent((links[k].getAttribute('href') || '').slice(1));
linkById[id2] = links[k];
}
var ids = flattenIds(groups);
var ticking = false;
function updateActive() {
ticking = false;
var bestId = '';
var bestTop = -Infinity;
for (var i2 = 0; i2 < ids.length; i2++) {
var el2 = document.getElementById(ids[i2]);
if (!el2) continue;
var rect = el2.getBoundingClientRect();
if (rect.top <= 120 && rect.top > bestTop) {
bestTop = rect.top;
bestId = ids[i2];
}
}
for (var idKey in linkById) {
linkById[idKey].classList.toggle('mdtFloatTocActive', idKey === bestId);
}
}
window.addEventListener(
'scroll',
function () {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(updateActive);
},
{ passive: true }
);
return wrap;
}
var resizeBound = false;
function initFloatToc($content) {
if (!isViewMode()) return;
if (document.getElementById('mdtFloatToc')) {
applyDesktopDock(document.getElementById('mdtFloatToc'));
return;
}
var content = $content && $content[0] ? $content[0] : null;
var root =
content || document.querySelector('.mw-parser-output') || document.getElementById('mw-content-text');
if (!root) return;
ensureHeadingIds(root);
var groups = collectGroups(root);
if (!groups.length) return;
var wrap = buildPanel(groups);
document.body.appendChild(wrap);
applyDesktopDock(wrap);
if (!resizeBound) {
resizeBound = true;
window.addEventListener(
'resize',
function () {
var el = document.getElementById('mdtFloatToc');
if (el) applyDesktopDock(el);
},
{ passive: true }
);
window.addEventListener('load', function () {
var el = document.getElementById('mdtFloatToc');
if (el) applyDesktopDock(el);
});
}
}
mw.loader.using(['mediawiki.util']).then(function () {
if (mw.hook && window.jQuery) {
mw.hook('wikipage.content').add(initFloatToc);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
initFloatToc(null);
});
} else {
initFloatToc(null);
}
});
})();
