跳转到内容
打开/关闭搜索
搜索
打开/关闭菜单
1
条目
2
文件
8
用户
633
编辑
ANACHRONISM
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
打开/关闭外观设置菜单
无法加载外观设置。请检查您的网络连接并重试。
重试
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。
user-interface-preferences
个人工具
登录
请求账号
查看“︁MediaWiki:Common.js”︁的源代码
MediaWiki界面页面
查看
阅读
查看源代码
查看历史
相关页面
系统消息
讨论
更多操作
更多
工具
链入页面
相关更改
页面信息
←
MediaWiki:Common.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
旅人
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* ============================================ Anachronism Common.js MediaWiki 1.43 LTS / Citizen 皮肤 ============================================ */ (function () { 'use strict'; /* ============================================ 通用工具 ============================================ */ window.Anachronism = window.Anachronism || {}; /** * DOM 就绪后执行(loading 时挂 DOMContentLoaded,否则立即执行) */ function ready(fn) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn, { once: true }); } else { fn(); } } window.Anachronism.ready = ready; /** * 等待某个条件成立后执行任务。 * 三路触发:立即尝试、定时轮询、DOM 变化时重试。 * 任一成功或超时后自动清理。 * * @param {Function} taskFn 返回 true 表示任务完成 * @param {Object} [options] * @param {number} [options.maxAttempts=100] 轮询最大次数 * @param {number} [options.intervalMs=100] 轮询间隔 * @param {number} [options.timeoutMs=15000] 硬超时 */ window.Anachronism.waitFor = function (taskFn, options = {}) { const maxAttempts = options.maxAttempts || 100; const intervalMs = options.intervalMs || 100; const timeoutMs = options.timeoutMs || 15000; let attempts = 0; let done = false; let timer = null; let observer = null; let timeoutId = null; function cleanup() { if (done) return; done = true; if (timer) clearInterval(timer); if (timeoutId) clearTimeout(timeoutId); if (observer) observer.disconnect(); } function tryRun() { if (done) return false; if (taskFn()) { cleanup(); return true; } return false; } if (tryRun()) return; // 首次尝试 timer = setInterval(function () { if (++attempts >= maxAttempts) { cleanup(); return; } tryRun(); }, intervalMs); observer = new MutationObserver(tryRun); observer.observe(document.body, { childList: true, subtree: true }); timeoutId = setTimeout(cleanup, timeoutMs); }; /** * HTML 转义(用于把用户可控文本插入 innerHTML) */ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); } /* ============================================ 侧边工具栏 Rail 管理器 ============================================ */ window.Rail = (function () { const items = []; let activeId = null; let panelEl, overlayEl, buttonsEl, contentEl, titleEl; function ensureDOM() { if (document.getElementById('rail-container')) return; buttonsEl = document.createElement('div'); buttonsEl.id = 'rail-container'; buttonsEl.className = 'rail-container'; overlayEl = document.createElement('div'); overlayEl.id = 'rail-overlay'; overlayEl.className = 'rail-overlay'; panelEl = document.createElement('aside'); panelEl.id = 'rail-panel'; panelEl.className = 'rail-panel'; panelEl.innerHTML = ` <div class="rail-panel__header"> <span class="rail-panel__title"></span> <button class="rail-panel__close" type="button" aria-label="关闭">×</button> </div> <div class="rail-panel__content" id="rail-panel-content"></div>`; titleEl = panelEl.querySelector('.rail-panel__title'); contentEl = panelEl.querySelector('#rail-panel-content'); document.body.append(overlayEl, panelEl, buttonsEl); panelEl.querySelector('.rail-panel__close').addEventListener('click', close); overlayEl.addEventListener('click', close); document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); }); } function createButton(item) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'rail-button fx-swap fx-pulse'; btn.setAttribute('data-rail-id', item.id); btn.setAttribute('aria-label', item.label); btn.setAttribute('title', item.label); const iconDefault = item.icon || '●'; const iconActive = item.iconActive || iconDefault; btn.innerHTML = ` <span class="fx-swap__a rail-button__icon fx-text-glow">${escapeHtml(iconDefault)}</span> <span class="fx-swap__b rail-button__icon fx-text-glow">${escapeHtml(iconActive)}</span>`; btn.addEventListener('click', () => { if (activeId === item.id) close(); else open(item); }); return btn; } function highlightButtons() { buttonsEl.querySelectorAll('.rail-button').forEach(btn => { btn.classList.toggle('is-active', btn.getAttribute('data-rail-id') === activeId); }); } function loadPanelContent(item) { if (item.html) { contentEl.innerHTML = item.html; return; } if (!item.template) { contentEl.innerHTML = '<p>未配置内容。</p>'; return; } new mw.Api().get({ action: 'parse', page: item.template, prop: 'text', formatversion: '2' }).done(data => { if (activeId !== item.id) return; contentEl.innerHTML = data?.parse?.text || '<p>加载失败。</p>'; }).fail(() => { if (activeId !== item.id) return; contentEl.innerHTML = '<p>加载失败。</p>'; }); } function open(item) { activeId = item.id; titleEl.textContent = item.label; contentEl.innerHTML = '<p class="rail-panel__loading">加载中…</p>'; highlightButtons(); document.body.classList.add('rail-open'); loadPanelContent(item); } function close() { activeId = null; document.body.classList.remove('rail-open'); if (buttonsEl) highlightButtons(); } function init() { ensureDOM(); buttonsEl.innerHTML = ''; items.forEach(item => buttonsEl.appendChild(createButton(item))); } return { register(id, options = {}) { items.push({ id, icon: options.icon || '●', label: options.label || id, html: options.html || null, template: options.template || null }); }, init, open, close }; })(); /* ============================================ 注册工具栏按钮 ============================================ */ Rail.register('navigation', { icon: '☰', label: '导航', template: 'Template:工具栏导航' }); Rail.register('claim', { icon: '!', label: '公告', template: 'Template:工具栏公告' }); Rail.register('help', { icon: '?', label: '帮助', template: 'Template:工具栏帮助' }); Rail.register('settings', { icon: '⚙', label: '设置', template: 'Template:工具栏设置' }); ready(() => Rail.init()); /* ============================================ 工具栏账户信息动态填充 ============================================ */ (function () { // 不显示的隐式用户组(user 也隐藏,但见 getGroupHtml 的兜底逻辑) const HIDDEN_GROUPS = ['*', 'user', 'autoconfirmed', 'emailconfirmed']; // 系统消息未加载时的中文兜底 const GROUP_NAME_FALLBACK = { 'sysop': '策展人', 'bureaucrat': '行政员', 'interface-admin': '织界者', 'interface_admin': '织界者', 'allowguest': '观察者', 'user': '旅人' }; const formatDate = ms => { if (!ms) return '—'; const d = new Date(ms); const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; }; const isMissing = msg => !msg || msg.includes('\u29FC') || msg.includes('⧼'); function getGroupName(g) { const txt = mw.msg('group-' + g + '-member'); return isMissing(txt) ? (GROUP_NAME_FALLBACK[g] || g) : txt; } function getGroupUrl(g) { const page = mw.msg('grouppage-' + g); return isMissing(page) ? mw.util.getUrl('Anachronism:' + getGroupName(g)) : mw.util.getUrl(page); } const buildGroupTag = g => `<a href="${getGroupUrl(g)}" class="rail-group-tag">${escapeHtml(getGroupName(g))}</a>`; function getGroupHtml(groups) { const visible = groups.filter(g => !HIDDEN_GROUPS.includes(g)); if (visible.length) return visible.map(buildGroupTag).join(''); return groups.includes('user') ? buildGroupTag('user') : '无'; } const isStaff = groups => groups.includes('sysop') || groups.includes('bureaucrat'); /* ---- 通知未读数 ---- */ function updateNotificationButton(count) { const btn = document.getElementById('rail-notification-btn'); if (!btn) return; btn.textContent = (typeof count === 'number' && count > 0) ? `通知 (${count})` : '通知'; } function fetchUnreadCount() { const cached = mw.config.get('wgEchoNotifsUnreadCount'); if (typeof cached === 'number') { updateNotificationButton(cached); return; } new mw.Api().get({ action: 'query', meta: 'notifications', notprop: 'count', notfilter: '!read' }).done(data => { updateNotificationButton(data?.query?.notifications?.count || 0); }).fail(() => { /* 静默 */ }); } /* ---- 访客视图 ---- */ function renderGuest(box) { box.innerHTML = ` <div class="rail-account-title">账户</div> <p class="rail-account-guest">您的身份是访客</p> <div class="rail-account-actions"> <a href="${mw.util.getUrl('Special:UserLogin')}" class="rail-action-button">登录</a> <a href="${mw.util.getUrl('Special:RequestAccount')}" class="rail-action-button">申请账户</a> <a href="${mw.util.getUrl('Special:ListUsers')}" class="rail-action-button">全部用户</a> </div>`; } /* ---- 登录用户视图 ---- */ function renderUser(box) { const userName = mw.config.get('wgUserName'); const groups = mw.config.get('wgUserGroups') || []; const editCount = mw.config.get('wgUserEditCount'); const regTime = mw.config.get('wgUserRegistration'); const u = name => mw.util.getUrl(name); const actions = [ `<a href="${u('Special:Contributions/' + userName)}" class="rail-action-button">贡献</a>`, `<a href="${u('Special:Watchlist')}" class="rail-action-button">监视列表</a>`, `<a href="${u('User talk:' + userName)}" class="rail-action-button">用户讨论</a>`, `<a href="${u('Special:Notifications')}" class="rail-action-button" id="rail-notification-btn">通知</a>`, `<a href="${u('Special:ListUsers')}" class="rail-action-button">全部用户</a>` ]; if (isStaff(groups)) { actions.push(`<a href="${u('Special:ConfirmAccounts')}" class="rail-action-button">审批</a>`); } box.innerHTML = ` <div class="rail-account-title">账户:${escapeHtml(userName)}</div> <div class="rail-account-groups"> <span class="rail-account-groups__label">权限组:</span>${getGroupHtml(groups)} </div> <div class="rail-account-actions">${actions.join('')}</div> <div class="rail-account-stats"> <span>编辑数:${editCount != null ? editCount : '—'}</span> <span>加入时间:${formatDate(regTime)}</span> </div> <div class="rail-account-logout"> <a href="${u('Special:UserLogout')}" class="rail-action-button rail-action-button--danger">退出登录</a> </div>`; fetchUnreadCount(); } function fillAccountInfo() { const box = document.getElementById('rail-account-info'); if (!box || box.dataset.filled === '1') return; box.dataset.filled = '1'; mw.config.get('wgUserName') ? renderUser(box) : renderGuest(box); } function observeRailPanel() { const target = document.getElementById('rail-panel-content'); if (!target) return; new MutationObserver(fillAccountInfo) .observe(target, { childList: true, subtree: true }); fillAccountInfo(); } ready(observeRailPanel); })(); /* ============================================ 在 rail 面板中触发 Citizen 偏好设置面板 ============================================ */ (function () { function closePreferences() { document.body.classList.remove('citizen-preferences-shown'); const d = document.getElementById('citizen-preferences-details'); if (d) d.open = false; } function bindRailPreferences() { const trigger = document.getElementById('rail-open-preferences'); if (!trigger || trigger.dataset.bound === '1') return; trigger.dataset.bound = '1'; trigger.addEventListener('click', () => { if (window.Rail?.close) Rail.close(); const details = document.getElementById('citizen-preferences-details'); if (details) details.open = true; document.body.classList.add('citizen-preferences-shown'); // 插入关闭按钮(只插一次) const card = document.getElementById('citizen-preferences-dropdown__card'); if (card && !card.querySelector('.citizen-preferences-close')) { const closeBtn = document.createElement('button'); closeBtn.className = 'citizen-preferences-close'; closeBtn.type = 'button'; closeBtn.textContent = '×'; closeBtn.setAttribute('aria-label', '关闭'); closeBtn.addEventListener('click', closePreferences); card.insertBefore(closeBtn, card.firstChild); } }); } // 点击面板外部关闭 document.addEventListener('click', e => { if (!document.body.classList.contains('citizen-preferences-shown')) return; const card = document.getElementById('citizen-preferences-dropdown__card'); const trigger = document.getElementById('rail-open-preferences'); if (!card) return; if (card.contains(e.target)) return; if (trigger && trigger.contains(e.target)) return; closePreferences(); }); // Esc 关闭 document.addEventListener('keydown', e => { if (e.key !== 'Escape') return; if (!document.body.classList.contains('citizen-preferences-shown')) return; closePreferences(); }); new MutationObserver(bindRailPreferences) .observe(document.body, { childList: true, subtree: true }); bindRailPreferences(); })(); /* ============================================ 评论区重定位:从主内容中移出,成为页面级独立块 ============================================ */ (function () { function tryRelocate() { const existing = document.querySelector('.comments-body'); if (existing && existing.dataset.relocated === '1') return true; const comments = document.querySelector('.mw-parser-output .comments-body'); if (!comments) return false; const mwBody = document.querySelector('.mw-body'); if (!mwBody?.parentNode) return false; comments.dataset.relocated = '1'; comments.classList.add('comments-body--page-level'); mwBody.parentNode.insertBefore(comments, mwBody.nextSibling); return true; } Anachronism.waitFor(tryRelocate); })(); /* ============================================ 评论区:把回复嵌套到父评论内部 Comments 扩展渲染的 DOM 是扁平的: .c-item.full ← 顶层评论 .c-item.reply ← 回复(和 full 平级) 这里用 JS 把 .reply 移到前面最近一个 .full 的 .c-container 内部,形成真正的 DOM 嵌套。 ============================================ */ (function () { function nestReplies() { const body = document.querySelector('.comments-body'); if (!body) return false; if (body.dataset.repliesNested === '1') return true; const items = body.querySelectorAll('.c-item'); if (!items.length) return false; let currentFull = null; items.forEach(item => { if (item.classList.contains('full')) { currentFull = item; } else if (item.classList.contains('reply') && currentFull) { const targetContainer = currentFull.querySelector('.c-container'); if (targetContainer && item.parentElement !== targetContainer) { targetContainer.appendChild(item); item.classList.add('nested-in-parent'); } } }); body.dataset.repliesNested = '1'; return true; } Anachronism.waitFor(nestReplies); })(); /* ============================================ 隐藏 Special:ListGroupRights 里的"常客"行 (autoconfirmed 是核心隐式组,无法通过配置移除) ============================================ */ (function () { const HIDE_GROUP_NAMES = ['常客']; function hideRows() { const table = document.querySelector('.mw-listgrouprights-table'); if (!table) return; table.querySelectorAll('tr').forEach(tr => { const link = tr.querySelector('a'); if (!link) return; if (HIDE_GROUP_NAMES.includes((link.textContent || '').trim())) { tr.style.display = 'none'; } }); } ready(hideRows); })(); /* ============================================ 特殊页面"相关页面"菜单:按有无图标加类 背景:Citizen 在 Special:Watchlist 等特殊页面上会渲染 #p-associated-pages 菜单。链接有的带图标,有的只有文字。 CSS 里若用 :has() 区分,部分解析器不识别会报错。 这里用 JS 预先打类,让 CSS 只需简单类选择器。 - .rail-has-icon → CSS 保持方块样式 - .rail-no-icon → CSS 改成文字按钮 ============================================ */ (function () { function markAssociatedPageButtons() { const buttons = document.querySelectorAll('#p-associated-pages .mw-list-item > a'); if (!buttons.length) return false; buttons.forEach(btn => { if (btn.dataset.railMarked === '1') return; btn.dataset.railMarked = '1'; btn.classList.add( btn.querySelector('.citizen-ui-icon') ? 'rail-has-icon' : 'rail-no-icon' ); }); return true; } ready(() => Anachronism.waitFor(markAssociatedPageButtons, { timeoutMs: 5000 })); })(); /* ============================================ 站点头部:Logo + 站点名 在 .citizen-page-container 顶部插入一个独立 header, 位于主容器(.mw-body)之前。参照 Fandom 的社区头部做法。 数据来源: - 站点名:mw.config 'wgSiteName' - Logo 与首页 URL:优先从 Citizen 现有的 .mw-logo-icon 读取(Citizen header 被 CSS display:none 隐藏,DOM 仍在)。 兜底用 mw.config 'wgLogos' / 'wgLogo'。 ============================================ */ (function () { const HEADER_ID = 'site-header'; function buildHeader() { if (document.getElementById(HEADER_ID)) return true; const container = document.querySelector('.citizen-page-container'); if (!container) return false; const siteName = mw.config.get('wgSiteName') || 'Anachronism'; // 优先从已有 DOM 取 Logo / 首页链接 const logoImg = document.querySelector( '.citizen-header__logo .mw-logo-icon, .mw-logo .mw-logo-icon, img.mw-logo-icon' ); const logoLink = document.querySelector( '.citizen-header__logo a, a.mw-logo' ); let logoUrl = (logoImg?.getAttribute('src') || '').trim(); const homeUrl = logoLink?.getAttribute('href') || '/'; if (!logoUrl) { const logos = mw.config.get('wgLogos') || {}; logoUrl = logos['1x'] || logos['svg'] || mw.config.get('wgLogo') || ''; } const header = document.createElement('header'); header.id = HEADER_ID; header.className = 'site-header'; header.innerHTML = ` <a class="site-header__brand" href="${homeUrl}"> ${logoUrl ? `<img class="site-header__logo" src="${logoUrl}" alt="">` : ''} <span class="site-header__name">${escapeHtml(siteName)}</span> </a>`; container.insertBefore(header, container.firstChild); return true; } ready(() => Anachronism.waitFor(buildHeader, { timeoutMs: 5000 })); })(); /* ============================================ 主容器 + 侧栏展开(真·双列布局) - 创建 .anachronism-rail-wrap,把 .citizen-page-aside 和 .citizen-page-footer 移进去 - 容器插到 .mw-body 之后 - 按钮切换 body.anachronism-aside-open - 展开时主容器收缩、侧栏占位;收起时反向 ============================================ */ (function () { const TOGGLE_ID = 'anachronism-aside-toggle'; const WRAP_ID = 'anachronism-rail-wrap'; function setupSideRail() { const container = document.querySelector('.citizen-page-container'); const mwBody = document.querySelector('.mw-body'); if (!container || !mwBody) return false; // 已处理过 if (document.getElementById(WRAP_ID)) return true; // 1. 建侧栏容器 const wrap = document.createElement('aside'); wrap.id = WRAP_ID; wrap.className = 'anachronism-rail-wrap'; wrap.setAttribute('aria-label', '页面信息'); wrap.setAttribute('aria-hidden', 'true'); // 2. 搬运 aside / footer(appendChild 会自动从原位置 detach) const pageAside = document.querySelector('.citizen-page-aside'); if (pageAside) wrap.appendChild(pageAside); const pageFooter = document.querySelector('.citizen-page-footer'); if (pageFooter) wrap.appendChild(pageFooter); // 3. 插到 .mw-body 之后 mwBody.parentNode.insertBefore(wrap, mwBody.nextSibling); // 4. 切换按钮 const btn = document.createElement('button'); btn.id = TOGGLE_ID; btn.type = 'button'; btn.className = 'anachronism-aside-toggle'; btn.setAttribute('aria-label', '展开/收起页面信息'); btn.setAttribute('title', '展开/收起页面信息'); btn.setAttribute('aria-expanded', 'false'); btn.innerHTML = '<span class="anachronism-aside-toggle__icon">▸</span>'; btn.addEventListener('click', () => { const isOpen = document.body.classList.toggle('anachronism-aside-open'); btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); wrap.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); }); mwBody.appendChild(btn); return true; } Anachronism.waitFor(setupSideRail, { timeoutMs: 5000 }); })(); })(); // 结束大 IIFE
返回
MediaWiki:Common.js
。
查看“︁MediaWiki:Common.js”︁的源代码
MediaWiki界面页面