MediaWiki:Common.js:修订间差异
MediaWiki界面页面
更多操作
注入顶栏横向导航 |
auth modal: local form + API submit 标签:手工回退 |
||
| (未显示同一用户的9个中间版本) | |||
| 第1行: | 第1行: | ||
/* 在 Citizen | /* 在 Citizen 顶栏的官方预留槽位 .citizen-header__start 内注入横向主导航。 | ||
注意:不要插到 .citizen-header 根节点上——那里是 logo / 搜索 / 抽屉的 flex 行, | |||
插进去会跟它们抢空间导致错位。__start 是皮肤留空的导航槽。 */ | |||
( function () { | ( function () { | ||
'use strict'; | 'use strict'; | ||
var LINKS = [ | var LINKS = [ | ||
{ text: ' | { text: '首页', page: '首页', match: /^(首页|Main_Page)$/ }, | ||
{ text: ' | { text: '子服一览', page: '子服一览', match: /^(子服一览|趣味生存|粘液空岛|纯净生存|机械动力|职业战争|起床战争|僵尸末日|避暑山庄)(\/|$)/ }, | ||
{ text: ' | { text: '总规章', page: 'KukeMC群组服总规章', match: /^KukeMC群组服总规章(\/|$)/ }, | ||
{ text: ' | { text: '常见问题', page: '服务器常见问题', match: /^服务器常见问题(\/|$)/ }, | ||
{ text: ' | { text: '历史资料', page: '历史资料展览馆', match: /^历史资料展览馆(\/|$)/ }, | ||
{ text: '全部条目', page: 'Special:AllPages', match: /^Special:AllPages$/i }, | |||
{ text: '分类', page: 'Special:Categories', match: /^Special:Categories$/i }, | |||
{ text: '最近更改', page: 'Special:RecentChanges', match: /^Special:RecentChanges$/i } | |||
]; | ]; | ||
function build() { | function build() { | ||
var | var slot = document.querySelector( '.citizen-header__start' ); | ||
if ( ! | if ( !slot || slot.querySelector( '.kuke-nav' ) ) { | ||
return; | return; | ||
} | } | ||
| 第19行: | 第24行: | ||
var page = mw.config.get( 'wgPageName' ); | var page = mw.config.get( 'wgPageName' ); | ||
var nav = document.createElement( 'nav' ); | var nav = document.createElement( 'nav' ); | ||
nav.className = 'kuke- | nav.className = 'kuke-nav'; | ||
nav.setAttribute( 'aria-label', '主导航' ); | nav.setAttribute( 'aria-label', '主导航' ); | ||
LINKS.forEach( function ( item ) { | LINKS.forEach( function ( item ) { | ||
var a = document.createElement( 'a' ); | var a = document.createElement( 'a' ); | ||
a.className = 'kuke- | a.className = 'kuke-nav__link'; | ||
a.href = item. | a.href = mw.util.getUrl( item.page ); | ||
a.textContent = item.text; | a.textContent = item.text; | ||
if ( item.match.test( page ) ) { | if ( item.match.test( page ) ) { | ||
a.classList.add( 'kuke- | a.classList.add( 'kuke-nav__link--active' ); | ||
a.setAttribute( 'aria-current', 'page' ); | a.setAttribute( 'aria-current', 'page' ); | ||
} | } | ||
| 第34行: | 第39行: | ||
} ); | } ); | ||
var logo = | slot.appendChild( nav ); | ||
if ( | } | ||
if ( document.readyState === 'loading' ) { | |||
document.addEventListener( 'DOMContentLoaded', build ); | |||
} else { | |||
build(); | |||
} | |||
}() ); | |||
/* 横版登录 / 注册弹窗。 | |||
表单在本地直接渲染(不再抓取 Special 页面的 HTML),提交走 Action API | |||
clientlogin / createaccount,token 在页面加载时就后台预取, | |||
因此弹窗打开即可用,没有等待。 | |||
独立页面 Special:UserLogin / Special:CreateAccount 保留,无 JS 时仍可用。 */ | |||
( function () { | |||
'use strict'; | |||
var LOGO = 'https://m.ccw.site/gandi_application/user_assets/2a6bb37880317d2bb5525ab560618e04.png'; | |||
var MODES = { | |||
login: { | |||
page: 'Special:UserLogin', | |||
tab: '登录', | |||
title: '登录到 Kuke Wiki', | |||
sub: '登录后即可编辑条目、参与讨论并同步个人偏好设置。', | |||
submit: '登录', | |||
busy: '正在登录…' | |||
}, | |||
signup: { | |||
page: 'Special:CreateAccount', | |||
tab: '注册', | |||
title: '创建账号', | |||
sub: '加入 KukeMC 官方百科的编辑与维护,只需一分钟。', | |||
submit: '创建账号', | |||
busy: '正在创建…' | |||
} | |||
}; | |||
var FIELDS = { | |||
login: [ | |||
{ name: 'username', label: '用户名', type: 'text', autocomplete: 'username', required: true }, | |||
{ name: 'password', label: '密码', type: 'password', autocomplete: 'current-password', required: true }, | |||
{ name: 'rememberMe', label: '在此浏览器上保持登录', type: 'checkbox' } | |||
], | |||
signup: [ | |||
{ name: 'username', label: '用户名', type: 'text', autocomplete: 'username', required: true }, | |||
{ name: 'password', label: '密码', type: 'password', autocomplete: 'new-password', required: true }, | |||
{ name: 'retype', label: '确认密码', type: 'password', autocomplete: 'new-password', required: true }, | |||
{ name: 'email', label: '电子邮件', type: 'email', autocomplete: 'email', hint: '选填,用于找回密码' }, | |||
{ name: 'captchaWord', label: '验证码', type: 'captcha', required: true } | |||
] | |||
}; | |||
var POINTS = [ | |||
{ icon: 'pencil', text: '编辑与创建条目,完善服务器资料' }, | |||
{ icon: 'chat', text: '参与讨论页,与其他编辑者协作' }, | |||
{ icon: 'star', text: '保存个人偏好、监视列表与编辑历史' }, | |||
{ icon: 'clock', text: '追踪最近更改,第一时间了解更新' } | |||
]; | |||
var ICONS = { | |||
pencil: 'M13.5 2.5a2.12 2.12 0 0 1 3 3L7 15l-4 1 1-4Z', | |||
chat: 'M3 5.5A1.5 1.5 0 0 1 4.5 4h11A1.5 1.5 0 0 1 17 5.5v7a1.5 1.5 0 0 1-1.5 1.5H8l-4 3v-3h-.5A1.5 1.5 0 0 1 3 12.5Z', | |||
star: 'm10 2.6 2.3 4.66 5.15.75-3.72 3.63.88 5.12L10 14.34l-4.6 2.42.87-5.12L2.55 8l5.15-.75Z', | |||
clock: 'M10 2.5a7.5 7.5 0 1 1 0 15 7.5 7.5 0 0 1 0-15Zm0 3.6v4.2l3 1.8', | |||
close: 'M5 5l10 10M15 5L5 15', | |||
eye: 'M1.8 10S4.9 4.7 10 4.7 18.2 10 18.2 10 15.1 15.3 10 15.3 1.8 10 1.8 10Zm8.2 2.2a2.2 2.2 0 1 0 0-4.4 2.2 2.2 0 0 0 0 4.4Z', | |||
eyeOff: 'M3 3l14 14M8.2 8.3A2.2 2.2 0 0 0 10 12.2c.6 0 1.1-.2 1.6-.6M6.1 6.2C3.6 7.6 1.8 10 1.8 10s3.1 5.3 8.2 5.3c1.4 0 2.6-.4 3.7-1M15.5 13C17.3 11.8 18.2 10 18.2 10S15.1 4.7 10 4.7c-.7 0-1.4.1-2 .3', | |||
alert: 'M10 6.6v4.2m0 2.6h.01M8.6 3.2 2.2 14.4A1.6 1.6 0 0 0 3.6 16.8h12.8a1.6 1.6 0 0 0 1.4-2.4L11.4 3.2a1.6 1.6 0 0 0-2.8 0Z' | |||
}; | |||
var root = null; | |||
var main = null; | |||
var body = null; | |||
var titleEl = null; | |||
var subEl = null; | |||
var alertEl = null; | |||
var lastFocus = null; | |||
var current = null; | |||
var api = null; | |||
var apiReady = null; | |||
var tokens = {}; | |||
var captcha = null; | |||
var submitting = false; | |||
function svg( d, cls, size ) { | |||
var s = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); | |||
s.setAttribute( 'viewBox', '0 0 20 20' ); | |||
s.setAttribute( 'width', size || '18' ); | |||
s.setAttribute( 'height', size || '18' ); | |||
s.setAttribute( 'aria-hidden', 'true' ); | |||
s.setAttribute( 'fill', 'none' ); | |||
s.setAttribute( 'stroke', 'currentColor' ); | |||
s.setAttribute( 'stroke-width', '1.6' ); | |||
s.setAttribute( 'stroke-linecap', 'round' ); | |||
s.setAttribute( 'stroke-linejoin', 'round' ); | |||
if ( cls ) { | |||
s.setAttribute( 'class', cls ); | |||
} | |||
var p = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); | |||
p.setAttribute( 'd', d ); | |||
s.appendChild( p ); | |||
return s; | |||
} | |||
function el( tag, cls, text ) { | |||
var n = document.createElement( tag ); | |||
if ( cls ) { | |||
n.className = cls; | |||
} | |||
if ( text ) { | |||
n.textContent = text; | |||
} | |||
return n; | |||
} | |||
function returnTarget() { | |||
var page = mw.config.get( 'wgPageName' ); | |||
if ( /^Special:(UserLogin|CreateAccount|UserLogout)$/i.test( page ) ) { | |||
return mw.config.get( 'wgMainPageTitle' ) || '首页'; | |||
} | |||
return page; | |||
} | |||
function returnUrl() { | |||
return location.origin + mw.util.getUrl( returnTarget() ); | |||
} | |||
/* ---------- API ---------- */ | |||
function ready() { | |||
if ( !apiReady ) { | |||
apiReady = mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () { | |||
api = new mw.Api(); | |||
return api; | |||
} ); | |||
} | |||
return apiReady; | |||
} | |||
function token( type ) { | |||
if ( tokens[ type ] ) { | |||
return tokens[ type ]; | |||
} | |||
tokens[ type ] = ready().then( function () { | |||
return api.get( { | |||
action: 'query', | |||
meta: 'tokens', | |||
type: type, | |||
formatversion: 2 | |||
} ); | |||
} ).then( function ( d ) { | |||
return d.query.tokens[ type + 'token' ]; | |||
} ); | |||
return tokens[ type ]; | |||
} | |||
/* QuestyCaptcha 的题面与 id,每次进入注册态重新取(旧 id 会失效) */ | |||
function loadCaptcha() { | |||
return ready().then( function () { | |||
return api.get( { | |||
action: 'query', | |||
meta: 'authmanagerinfo', | |||
amirequestsfor: 'create', | |||
formatversion: 2 | |||
} ); | |||
} ).then( function ( d ) { | |||
var reqs = ( d.query && d.query.authmanagerinfo && d.query.authmanagerinfo.requests ) || []; | |||
var hit = null; | |||
reqs.forEach( function ( r ) { | |||
if ( r.id && r.id.indexOf( 'Captcha' ) !== -1 && r.fields && r.fields.captchaId ) { | |||
hit = r; | |||
} | |||
} ); | |||
if ( !hit ) { | |||
return null; | |||
} | |||
return { | |||
id: hit.fields.captchaId.value, | |||
question: hit.fields.captchaInfo ? hit.fields.captchaInfo.value : '', | |||
label: ( hit.fields.captchaWord && hit.fields.captchaWord.label ) || '验证码' | |||
}; | |||
} ).catch( function () { | |||
return null; | |||
} ); | |||
} | |||
/* ---------- 结构 ---------- */ | |||
function build() { | |||
root = el( 'div', 'kuke-auth' ); | |||
root.hidden = true; | |||
var backdrop = el( 'div', 'kuke-auth__backdrop' ); | |||
backdrop.addEventListener( 'click', close ); | |||
root.appendChild( backdrop ); | |||
var dialog = el( 'div', 'kuke-auth__dialog' ); | |||
dialog.setAttribute( 'role', 'dialog' ); | |||
dialog.setAttribute( 'aria-modal', 'true' ); | |||
dialog.setAttribute( 'aria-labelledby', 'kuke-auth-title' ); | |||
var closeBtn = el( 'button', 'kuke-auth__close' ); | |||
closeBtn.type = 'button'; | |||
closeBtn.setAttribute( 'aria-label', '关闭' ); | |||
closeBtn.appendChild( svg( ICONS.close ) ); | |||
closeBtn.addEventListener( 'click', close ); | |||
dialog.appendChild( closeBtn ); | |||
// 左:品牌栏 | |||
var aside = el( 'aside', 'kuke-auth__aside' ); | |||
var logo = el( 'img', 'kuke-auth__logo' ); | |||
logo.src = LOGO; | |||
logo.alt = ''; | |||
logo.width = 44; | |||
logo.height = 44; | |||
aside.appendChild( logo ); | |||
aside.appendChild( el( 'p', 'kuke-auth__brand', 'Kuke Wiki' ) ); | |||
aside.appendChild( el( 'p', 'kuke-auth__tagline', 'KukeMC 群组服官方百科' ) ); | |||
var ul = el( 'ul', 'kuke-auth__points' ); | |||
POINTS.forEach( function ( item ) { | |||
var li = el( 'li', 'kuke-auth__point' ); | |||
li.appendChild( svg( ICONS[ item.icon ], 'kuke-auth__point-icon' ) ); | |||
li.appendChild( el( 'span', null, item.text ) ); | |||
ul.appendChild( li ); | |||
} ); | |||
aside.appendChild( ul ); | |||
aside.appendChild( el( 'p', 'kuke-auth__note', '本站内容由社区共同维护,欢迎参与。' ) ); | |||
dialog.appendChild( aside ); | |||
// 右:表单栏 | |||
main = el( 'div', 'kuke-auth__main' ); | |||
var head = el( 'div', 'kuke-auth__head' ); | |||
var tabs = el( 'div', 'kuke-auth__tabs' ); | |||
tabs.setAttribute( 'role', 'tablist' ); | |||
Object.keys( MODES ).forEach( function ( key ) { | |||
var b = el( 'button', 'kuke-auth__tab', MODES[ key ].tab ); | |||
b.type = 'button'; | |||
b.dataset.mode = key; | |||
b.setAttribute( 'role', 'tab' ); | |||
b.addEventListener( 'click', function () { | |||
render( key ); | |||
} ); | |||
tabs.appendChild( b ); | |||
} ); | |||
head.appendChild( tabs ); | |||
titleEl = el( 'h2', 'kuke-auth__title' ); | |||
titleEl.id = 'kuke-auth-title'; | |||
head.appendChild( titleEl ); | |||
subEl = el( 'p', 'kuke-auth__sub' ); | |||
head.appendChild( subEl ); | |||
main.appendChild( head ); | |||
body = el( 'div', 'kuke-auth__body' ); | |||
main.appendChild( body ); | |||
dialog.appendChild( main ); | |||
root.appendChild( dialog ); | |||
document.body.appendChild( root ); | |||
} | |||
/* ---------- 表单 ---------- */ | |||
function passwordToggle( input ) { | |||
var btn = el( 'button', 'kuke-auth__peek' ); | |||
btn.type = 'button'; | |||
btn.setAttribute( 'aria-label', '显示密码' ); | |||
btn.appendChild( svg( ICONS.eye, null, '17' ) ); | |||
btn.addEventListener( 'click', function () { | |||
var show = input.type === 'password'; | |||
input.type = show ? 'text' : 'password'; | |||
btn.setAttribute( 'aria-label', show ? '隐藏密码' : '显示密码' ); | |||
btn.textContent = ''; | |||
btn.appendChild( svg( show ? ICONS.eyeOff : ICONS.eye, null, '17' ) ); | |||
input.focus(); | |||
} ); | |||
return btn; | |||
} | |||
function field( spec, form ) { | |||
if ( spec.type === 'checkbox' ) { | |||
var wrap = el( 'label', 'kuke-auth__check' ); | |||
var box = el( 'input' ); | |||
box.type = 'checkbox'; | |||
box.name = spec.name; | |||
wrap.appendChild( box ); | |||
wrap.appendChild( el( 'span', null, spec.label ) ); | |||
return wrap; | |||
} | |||
var id = 'kuke-auth-' + spec.name; | |||
var row = el( 'div', 'kuke-auth__field' ); | |||
var label = el( 'label', 'kuke-auth__label', spec.label ); | |||
label.htmlFor = id; | |||
if ( !spec.required ) { | |||
label.appendChild( el( 'span', 'kuke-auth__optional', '选填' ) ); | |||
} | |||
row.appendChild( label ); | |||
var control = el( 'div', 'kuke-auth__control' ); | |||
var input = el( 'input', 'kuke-auth__input' ); | |||
input.id = id; | |||
input.name = spec.name; | |||
input.type = spec.type === 'captcha' ? 'text' : spec.type; | |||
if ( spec.autocomplete ) { | |||
input.autocomplete = spec.autocomplete; | |||
} | |||
if ( spec.required ) { | |||
input.required = true; | |||
} | |||
if ( spec.type === 'captcha' ) { | |||
input.autocomplete = 'off'; | |||
input.inputMode = 'text'; | |||
row.classList.add( 'kuke-auth__field--captcha' ); | |||
} | |||
control.appendChild( input ); | |||
if ( spec.type === 'password' ) { | |||
control.classList.add( 'kuke-auth__control--peek' ); | |||
control.appendChild( passwordToggle( input ) ); | |||
} | |||
row.appendChild( control ); | |||
if ( spec.type === 'captcha' ) { | |||
var q = el( 'p', 'kuke-auth__captcha' ); | |||
q.appendChild( el( 'span', 'kuke-auth__captcha-q', '正在获取…' ) ); | |||
row.appendChild( q ); | |||
form.captchaQuestion = q.firstChild; | |||
loadCaptcha().then( function ( data ) { | |||
if ( !form.isConnected ) { | |||
return; | |||
} | |||
captcha = data; | |||
q.firstChild.textContent = data ? data.question : '(暂不可用,请稍后重试)'; | |||
} ); | |||
} | |||
if ( spec.hint ) { | |||
row.appendChild( el( 'p', 'kuke-auth__hint', spec.hint ) ); | |||
} | |||
return row; | |||
} | |||
function showAlert( text ) { | |||
if ( !alertEl ) { | |||
return; | |||
} | |||
alertEl.textContent = ''; | |||
if ( !text ) { | |||
alertEl.hidden = true; | |||
return; | |||
} | |||
alertEl.appendChild( svg( ICONS.alert, 'kuke-auth__alert-icon' ) ); | |||
alertEl.appendChild( el( 'span', null, text ) ); | |||
alertEl.hidden = false; | |||
} | |||
function setSubmitting( btn, mode, on ) { | |||
submitting = on; | |||
btn.disabled = on; | |||
btn.classList.toggle( 'kuke-auth__submit--busy', on ); | |||
btn.textContent = ''; | |||
if ( on ) { | |||
btn.appendChild( el( 'span', 'kuke-auth__spinner' ) ); | |||
btn.appendChild( el( 'span', null, MODES[ mode ].busy ) ); | |||
} else { | |||
btn.appendChild( el( 'span', null, MODES[ mode ].submit ) ); | |||
} | |||
} | |||
function values( form ) { | |||
var out = {}; | |||
Array.prototype.forEach.call( form.querySelectorAll( 'input' ), function ( i ) { | |||
out[ i.name ] = i.type === 'checkbox' ? ( i.checked ? '1' : '' ) : i.value; | |||
} ); | |||
return out; | |||
} | |||
function submit( mode, form, btn ) { | |||
if ( submitting ) { | |||
return; | |||
} | |||
var v = values( form ); | |||
if ( mode === 'signup' && v.password !== v.retype ) { | |||
showAlert( '两次输入的密码不一致。' ); | |||
return; | |||
} | |||
showAlert( '' ); | |||
setSubmitting( btn, mode, true ); | |||
var type = mode === 'login' ? 'login' : 'createaccount'; | |||
ready().then( function () { | |||
return token( type ); | |||
} ).then( function ( tk ) { | |||
var params; | |||
if ( mode === 'login' ) { | |||
params = { | |||
action: 'clientlogin', | |||
loginreturnurl: returnUrl(), | |||
logintoken: tk, | |||
username: v.username, | |||
password: v.password, | |||
formatversion: 2, | |||
errorformat: 'html', | |||
errorlang: 'zh' | |||
}; | |||
if ( v.rememberMe ) { | |||
params.rememberMe = '1'; | |||
} | |||
} else { | |||
params = { | |||
action: 'createaccount', | |||
createreturnurl: returnUrl(), | |||
createtoken: tk, | |||
username: v.username, | |||
password: v.password, | |||
retype: v.retype, | |||
formatversion: 2, | |||
errorformat: 'html', | |||
errorlang: 'zh' | |||
}; | |||
if ( v.email ) { | |||
params.email = v.email; | |||
} | |||
if ( captcha ) { | |||
params.captchaId = captcha.id; | |||
params.captchaWord = v.captchaWord; | |||
} | |||
} | |||
return api.post( params ); | |||
} ).then( function ( d ) { | |||
var res = d.clientlogin || d.createaccount || {}; | |||
if ( res.status === 'PASS' ) { | |||
location.href = returnUrl(); | |||
return; | |||
} | |||
// 需要额外步骤(如二次验证)时交给完整页面处理 | |||
if ( res.status === 'UI' || res.status === 'REDIRECT' ) { | |||
location.href = mw.util.getUrl( MODES[ mode ].page, { returnto: returnTarget() } ); | |||
return; | |||
} | |||
setSubmitting( btn, mode, false ); | |||
showAlert( res.message || '提交失败,请稍后重试。' ); | |||
// token 与验证码一次性,失败后重取 | |||
delete tokens[ type ]; | |||
if ( mode === 'signup' ) { | |||
refreshCaptcha( form ); | |||
} | |||
} ).catch( function ( code, data ) { | |||
setSubmitting( btn, mode, false ); | |||
var msg = ( data && data.error && data.error.info ) || '网络异常,请稍后重试。'; | |||
showAlert( msg ); | |||
delete tokens[ type ]; | |||
} ); | |||
} | |||
function refreshCaptcha( form ) { | |||
var q = form.captchaQuestion; | |||
if ( !q ) { | |||
return; | |||
} | |||
q.textContent = '正在获取…'; | |||
loadCaptcha().then( function ( data ) { | |||
captcha = data; | |||
if ( q.isConnected ) { | |||
q.textContent = data ? data.question : '(暂不可用,请稍后重试)'; | |||
} | |||
} ); | |||
} | |||
function render( mode ) { | |||
if ( current === mode ) { | |||
return; | |||
} | |||
current = mode; | |||
captcha = null; | |||
titleEl.textContent = MODES[ mode ].title; | |||
subEl.textContent = MODES[ mode ].sub; | |||
Array.prototype.forEach.call( root.querySelectorAll( '.kuke-auth__tab' ), function ( b ) { | |||
var on = b.dataset.mode === mode; | |||
b.classList.toggle( 'kuke-auth__tab--active', on ); | |||
b.setAttribute( 'aria-selected', on ? 'true' : 'false' ); | |||
} ); | |||
body.textContent = ''; | |||
alertEl = el( 'div', 'kuke-auth__alert' ); | |||
alertEl.setAttribute( 'role', 'alert' ); | |||
alertEl.hidden = true; | |||
body.appendChild( alertEl ); | |||
var form = el( 'form', 'kuke-auth__form' ); | |||
form.setAttribute( 'novalidate', 'novalidate' ); | |||
FIELDS[ mode ].forEach( function ( spec ) { | |||
form.appendChild( field( spec, form ) ); | |||
} ); | |||
var btn = el( 'button', 'kuke-auth__submit' ); | |||
btn.type = 'submit'; | |||
btn.appendChild( el( 'span', null, MODES[ mode ].submit ) ); | |||
form.appendChild( btn ); | |||
form.addEventListener( 'submit', function ( e ) { | |||
e.preventDefault(); | |||
submit( mode, form, btn ); | |||
} ); | |||
body.appendChild( form ); | |||
var foot = el( 'p', 'kuke-auth__foot' ); | |||
if ( mode === 'login' ) { | |||
var reset = el( 'a', null, '忘记密码?' ); | |||
reset.href = mw.util.getUrl( 'Special:PasswordReset' ); | |||
foot.appendChild( reset ); | |||
foot.appendChild( el( 'span', 'kuke-auth__dot', '·' ) ); | |||
var toSignup = el( 'a', null, '还没有账号?立即注册' ); | |||
toSignup.href = '#'; | |||
toSignup.addEventListener( 'click', function ( e ) { | |||
e.preventDefault(); | |||
render( 'signup' ); | |||
} ); | |||
foot.appendChild( toSignup ); | |||
} else { | } else { | ||
foot.appendChild( el( 'span', null, '已有账号?' ) ); | |||
var toLogin = el( 'a', null, '直接登录' ); | |||
toLogin.href = '#'; | |||
toLogin.addEventListener( 'click', function ( e ) { | |||
e.preventDefault(); | |||
render( 'login' ); | |||
} ); | |||
foot.appendChild( toLogin ); | |||
} | |||
body.appendChild( foot ); | |||
main.scrollTop = 0; | |||
// 预热对应的 token | |||
token( mode === 'login' ? 'login' : 'createaccount' ); | |||
var first = form.querySelector( 'input' ); | |||
if ( first ) { | |||
first.focus(); | |||
} | |||
} | |||
/* ---------- 开关 ---------- */ | |||
function onKey( e ) { | |||
if ( e.key === 'Escape' ) { | |||
close(); | |||
return; | |||
} | |||
if ( e.key !== 'Tab' || root.hidden ) { | |||
return; | |||
} | |||
var items = root.querySelectorAll( | |||
'a[href], button:not([disabled]), input:not([type=hidden]), select, textarea' | |||
); | |||
if ( !items.length ) { | |||
return; | |||
} | |||
var first = items[ 0 ]; | |||
var last = items[ items.length - 1 ]; | |||
if ( e.shiftKey && document.activeElement === first ) { | |||
e.preventDefault(); | |||
last.focus(); | |||
} else if ( !e.shiftKey && document.activeElement === last ) { | |||
e.preventDefault(); | |||
first.focus(); | |||
} | |||
} | |||
function open( mode ) { | |||
if ( !root ) { | |||
build(); | |||
} | |||
lastFocus = document.activeElement; | |||
root.hidden = false; | |||
document.body.classList.add( 'kuke-auth-open' ); | |||
document.addEventListener( 'keydown', onKey ); | |||
render( mode ); | |||
} | |||
function close() { | |||
if ( !root || root.hidden ) { | |||
return; | |||
} | |||
root.hidden = true; | |||
current = null; | |||
document.body.classList.remove( 'kuke-auth-open' ); | |||
document.removeEventListener( 'keydown', onKey ); | |||
if ( lastFocus && lastFocus.focus ) { | |||
lastFocus.focus(); | |||
} | |||
} | |||
function modeOf( href ) { | |||
var target; | |||
try { | |||
target = new URL( href, location.href ); | |||
} catch ( e ) { | |||
return null; | |||
} | |||
if ( target.origin !== location.origin ) { | |||
return null; | |||
} | |||
var path = decodeURIComponent( target.pathname + '?' + target.search ); | |||
if ( /Special:UserLogin|特殊:用户登录|Special:登录/i.test( path ) ) { | |||
return 'login'; | |||
} | |||
if ( /Special:CreateAccount|特殊:创建账户|Special:创建账号/i.test( path ) ) { | |||
return 'signup'; | |||
} | |||
return null; | |||
} | |||
function onClick( e ) { | |||
if ( e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ) { | |||
return; | |||
} | |||
var a = e.target.closest ? e.target.closest( 'a[href]' ) : null; | |||
if ( !a || a.target === '_blank' ) { | |||
return; | |||
} | |||
var mode = modeOf( a.getAttribute( 'href' ) ); | |||
if ( !mode ) { | |||
return; | |||
} | |||
e.preventDefault(); | |||
open( mode ); | |||
} | |||
function init() { | |||
if ( mw.config.get( 'wgUserName' ) ) { | |||
return; | |||
} | |||
document.addEventListener( 'click', onClick ); | |||
// 后台预取登录 token,弹窗打开时表单已可直接提交 | |||
token( 'login' ); | |||
var direct = /[?&]kukeauth=(login|signup)/.exec( location.search ); | |||
if ( direct ) { | |||
open( direct[ 1 ] ); | |||
} | } | ||
} | } | ||
if ( document.readyState === 'loading' ) { | if ( document.readyState === 'loading' ) { | ||
document.addEventListener( 'DOMContentLoaded', | document.addEventListener( 'DOMContentLoaded', init ); | ||
} else { | } else { | ||
init(); | |||
} | } | ||
}() ); | }() ); | ||
2026年8月20日 (四) 13:20的版本
/* 在 Citizen 顶栏的官方预留槽位 .citizen-header__start 内注入横向主导航。
注意:不要插到 .citizen-header 根节点上——那里是 logo / 搜索 / 抽屉的 flex 行,
插进去会跟它们抢空间导致错位。__start 是皮肤留空的导航槽。 */
( function () {
'use strict';
var LINKS = [
{ text: '首页', page: '首页', match: /^(首页|Main_Page)$/ },
{ text: '子服一览', page: '子服一览', match: /^(子服一览|趣味生存|粘液空岛|纯净生存|机械动力|职业战争|起床战争|僵尸末日|避暑山庄)(\/|$)/ },
{ text: '总规章', page: 'KukeMC群组服总规章', match: /^KukeMC群组服总规章(\/|$)/ },
{ text: '常见问题', page: '服务器常见问题', match: /^服务器常见问题(\/|$)/ },
{ text: '历史资料', page: '历史资料展览馆', match: /^历史资料展览馆(\/|$)/ },
{ text: '全部条目', page: 'Special:AllPages', match: /^Special:AllPages$/i },
{ text: '分类', page: 'Special:Categories', match: /^Special:Categories$/i },
{ text: '最近更改', page: 'Special:RecentChanges', match: /^Special:RecentChanges$/i }
];
function build() {
var slot = document.querySelector( '.citizen-header__start' );
if ( !slot || slot.querySelector( '.kuke-nav' ) ) {
return;
}
var page = mw.config.get( 'wgPageName' );
var nav = document.createElement( 'nav' );
nav.className = 'kuke-nav';
nav.setAttribute( 'aria-label', '主导航' );
LINKS.forEach( function ( item ) {
var a = document.createElement( 'a' );
a.className = 'kuke-nav__link';
a.href = mw.util.getUrl( item.page );
a.textContent = item.text;
if ( item.match.test( page ) ) {
a.classList.add( 'kuke-nav__link--active' );
a.setAttribute( 'aria-current', 'page' );
}
nav.appendChild( a );
} );
slot.appendChild( nav );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', build );
} else {
build();
}
}() );
/* 横版登录 / 注册弹窗。
表单在本地直接渲染(不再抓取 Special 页面的 HTML),提交走 Action API
clientlogin / createaccount,token 在页面加载时就后台预取,
因此弹窗打开即可用,没有等待。
独立页面 Special:UserLogin / Special:CreateAccount 保留,无 JS 时仍可用。 */
( function () {
'use strict';
var LOGO = 'https://m.ccw.site/gandi_application/user_assets/2a6bb37880317d2bb5525ab560618e04.png';
var MODES = {
login: {
page: 'Special:UserLogin',
tab: '登录',
title: '登录到 Kuke Wiki',
sub: '登录后即可编辑条目、参与讨论并同步个人偏好设置。',
submit: '登录',
busy: '正在登录…'
},
signup: {
page: 'Special:CreateAccount',
tab: '注册',
title: '创建账号',
sub: '加入 KukeMC 官方百科的编辑与维护,只需一分钟。',
submit: '创建账号',
busy: '正在创建…'
}
};
var FIELDS = {
login: [
{ name: 'username', label: '用户名', type: 'text', autocomplete: 'username', required: true },
{ name: 'password', label: '密码', type: 'password', autocomplete: 'current-password', required: true },
{ name: 'rememberMe', label: '在此浏览器上保持登录', type: 'checkbox' }
],
signup: [
{ name: 'username', label: '用户名', type: 'text', autocomplete: 'username', required: true },
{ name: 'password', label: '密码', type: 'password', autocomplete: 'new-password', required: true },
{ name: 'retype', label: '确认密码', type: 'password', autocomplete: 'new-password', required: true },
{ name: 'email', label: '电子邮件', type: 'email', autocomplete: 'email', hint: '选填,用于找回密码' },
{ name: 'captchaWord', label: '验证码', type: 'captcha', required: true }
]
};
var POINTS = [
{ icon: 'pencil', text: '编辑与创建条目,完善服务器资料' },
{ icon: 'chat', text: '参与讨论页,与其他编辑者协作' },
{ icon: 'star', text: '保存个人偏好、监视列表与编辑历史' },
{ icon: 'clock', text: '追踪最近更改,第一时间了解更新' }
];
var ICONS = {
pencil: 'M13.5 2.5a2.12 2.12 0 0 1 3 3L7 15l-4 1 1-4Z',
chat: 'M3 5.5A1.5 1.5 0 0 1 4.5 4h11A1.5 1.5 0 0 1 17 5.5v7a1.5 1.5 0 0 1-1.5 1.5H8l-4 3v-3h-.5A1.5 1.5 0 0 1 3 12.5Z',
star: 'm10 2.6 2.3 4.66 5.15.75-3.72 3.63.88 5.12L10 14.34l-4.6 2.42.87-5.12L2.55 8l5.15-.75Z',
clock: 'M10 2.5a7.5 7.5 0 1 1 0 15 7.5 7.5 0 0 1 0-15Zm0 3.6v4.2l3 1.8',
close: 'M5 5l10 10M15 5L5 15',
eye: 'M1.8 10S4.9 4.7 10 4.7 18.2 10 18.2 10 15.1 15.3 10 15.3 1.8 10 1.8 10Zm8.2 2.2a2.2 2.2 0 1 0 0-4.4 2.2 2.2 0 0 0 0 4.4Z',
eyeOff: 'M3 3l14 14M8.2 8.3A2.2 2.2 0 0 0 10 12.2c.6 0 1.1-.2 1.6-.6M6.1 6.2C3.6 7.6 1.8 10 1.8 10s3.1 5.3 8.2 5.3c1.4 0 2.6-.4 3.7-1M15.5 13C17.3 11.8 18.2 10 18.2 10S15.1 4.7 10 4.7c-.7 0-1.4.1-2 .3',
alert: 'M10 6.6v4.2m0 2.6h.01M8.6 3.2 2.2 14.4A1.6 1.6 0 0 0 3.6 16.8h12.8a1.6 1.6 0 0 0 1.4-2.4L11.4 3.2a1.6 1.6 0 0 0-2.8 0Z'
};
var root = null;
var main = null;
var body = null;
var titleEl = null;
var subEl = null;
var alertEl = null;
var lastFocus = null;
var current = null;
var api = null;
var apiReady = null;
var tokens = {};
var captcha = null;
var submitting = false;
function svg( d, cls, size ) {
var s = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
s.setAttribute( 'viewBox', '0 0 20 20' );
s.setAttribute( 'width', size || '18' );
s.setAttribute( 'height', size || '18' );
s.setAttribute( 'aria-hidden', 'true' );
s.setAttribute( 'fill', 'none' );
s.setAttribute( 'stroke', 'currentColor' );
s.setAttribute( 'stroke-width', '1.6' );
s.setAttribute( 'stroke-linecap', 'round' );
s.setAttribute( 'stroke-linejoin', 'round' );
if ( cls ) {
s.setAttribute( 'class', cls );
}
var p = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
p.setAttribute( 'd', d );
s.appendChild( p );
return s;
}
function el( tag, cls, text ) {
var n = document.createElement( tag );
if ( cls ) {
n.className = cls;
}
if ( text ) {
n.textContent = text;
}
return n;
}
function returnTarget() {
var page = mw.config.get( 'wgPageName' );
if ( /^Special:(UserLogin|CreateAccount|UserLogout)$/i.test( page ) ) {
return mw.config.get( 'wgMainPageTitle' ) || '首页';
}
return page;
}
function returnUrl() {
return location.origin + mw.util.getUrl( returnTarget() );
}
/* ---------- API ---------- */
function ready() {
if ( !apiReady ) {
apiReady = mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () {
api = new mw.Api();
return api;
} );
}
return apiReady;
}
function token( type ) {
if ( tokens[ type ] ) {
return tokens[ type ];
}
tokens[ type ] = ready().then( function () {
return api.get( {
action: 'query',
meta: 'tokens',
type: type,
formatversion: 2
} );
} ).then( function ( d ) {
return d.query.tokens[ type + 'token' ];
} );
return tokens[ type ];
}
/* QuestyCaptcha 的题面与 id,每次进入注册态重新取(旧 id 会失效) */
function loadCaptcha() {
return ready().then( function () {
return api.get( {
action: 'query',
meta: 'authmanagerinfo',
amirequestsfor: 'create',
formatversion: 2
} );
} ).then( function ( d ) {
var reqs = ( d.query && d.query.authmanagerinfo && d.query.authmanagerinfo.requests ) || [];
var hit = null;
reqs.forEach( function ( r ) {
if ( r.id && r.id.indexOf( 'Captcha' ) !== -1 && r.fields && r.fields.captchaId ) {
hit = r;
}
} );
if ( !hit ) {
return null;
}
return {
id: hit.fields.captchaId.value,
question: hit.fields.captchaInfo ? hit.fields.captchaInfo.value : '',
label: ( hit.fields.captchaWord && hit.fields.captchaWord.label ) || '验证码'
};
} ).catch( function () {
return null;
} );
}
/* ---------- 结构 ---------- */
function build() {
root = el( 'div', 'kuke-auth' );
root.hidden = true;
var backdrop = el( 'div', 'kuke-auth__backdrop' );
backdrop.addEventListener( 'click', close );
root.appendChild( backdrop );
var dialog = el( 'div', 'kuke-auth__dialog' );
dialog.setAttribute( 'role', 'dialog' );
dialog.setAttribute( 'aria-modal', 'true' );
dialog.setAttribute( 'aria-labelledby', 'kuke-auth-title' );
var closeBtn = el( 'button', 'kuke-auth__close' );
closeBtn.type = 'button';
closeBtn.setAttribute( 'aria-label', '关闭' );
closeBtn.appendChild( svg( ICONS.close ) );
closeBtn.addEventListener( 'click', close );
dialog.appendChild( closeBtn );
// 左:品牌栏
var aside = el( 'aside', 'kuke-auth__aside' );
var logo = el( 'img', 'kuke-auth__logo' );
logo.src = LOGO;
logo.alt = '';
logo.width = 44;
logo.height = 44;
aside.appendChild( logo );
aside.appendChild( el( 'p', 'kuke-auth__brand', 'Kuke Wiki' ) );
aside.appendChild( el( 'p', 'kuke-auth__tagline', 'KukeMC 群组服官方百科' ) );
var ul = el( 'ul', 'kuke-auth__points' );
POINTS.forEach( function ( item ) {
var li = el( 'li', 'kuke-auth__point' );
li.appendChild( svg( ICONS[ item.icon ], 'kuke-auth__point-icon' ) );
li.appendChild( el( 'span', null, item.text ) );
ul.appendChild( li );
} );
aside.appendChild( ul );
aside.appendChild( el( 'p', 'kuke-auth__note', '本站内容由社区共同维护,欢迎参与。' ) );
dialog.appendChild( aside );
// 右:表单栏
main = el( 'div', 'kuke-auth__main' );
var head = el( 'div', 'kuke-auth__head' );
var tabs = el( 'div', 'kuke-auth__tabs' );
tabs.setAttribute( 'role', 'tablist' );
Object.keys( MODES ).forEach( function ( key ) {
var b = el( 'button', 'kuke-auth__tab', MODES[ key ].tab );
b.type = 'button';
b.dataset.mode = key;
b.setAttribute( 'role', 'tab' );
b.addEventListener( 'click', function () {
render( key );
} );
tabs.appendChild( b );
} );
head.appendChild( tabs );
titleEl = el( 'h2', 'kuke-auth__title' );
titleEl.id = 'kuke-auth-title';
head.appendChild( titleEl );
subEl = el( 'p', 'kuke-auth__sub' );
head.appendChild( subEl );
main.appendChild( head );
body = el( 'div', 'kuke-auth__body' );
main.appendChild( body );
dialog.appendChild( main );
root.appendChild( dialog );
document.body.appendChild( root );
}
/* ---------- 表单 ---------- */
function passwordToggle( input ) {
var btn = el( 'button', 'kuke-auth__peek' );
btn.type = 'button';
btn.setAttribute( 'aria-label', '显示密码' );
btn.appendChild( svg( ICONS.eye, null, '17' ) );
btn.addEventListener( 'click', function () {
var show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.setAttribute( 'aria-label', show ? '隐藏密码' : '显示密码' );
btn.textContent = '';
btn.appendChild( svg( show ? ICONS.eyeOff : ICONS.eye, null, '17' ) );
input.focus();
} );
return btn;
}
function field( spec, form ) {
if ( spec.type === 'checkbox' ) {
var wrap = el( 'label', 'kuke-auth__check' );
var box = el( 'input' );
box.type = 'checkbox';
box.name = spec.name;
wrap.appendChild( box );
wrap.appendChild( el( 'span', null, spec.label ) );
return wrap;
}
var id = 'kuke-auth-' + spec.name;
var row = el( 'div', 'kuke-auth__field' );
var label = el( 'label', 'kuke-auth__label', spec.label );
label.htmlFor = id;
if ( !spec.required ) {
label.appendChild( el( 'span', 'kuke-auth__optional', '选填' ) );
}
row.appendChild( label );
var control = el( 'div', 'kuke-auth__control' );
var input = el( 'input', 'kuke-auth__input' );
input.id = id;
input.name = spec.name;
input.type = spec.type === 'captcha' ? 'text' : spec.type;
if ( spec.autocomplete ) {
input.autocomplete = spec.autocomplete;
}
if ( spec.required ) {
input.required = true;
}
if ( spec.type === 'captcha' ) {
input.autocomplete = 'off';
input.inputMode = 'text';
row.classList.add( 'kuke-auth__field--captcha' );
}
control.appendChild( input );
if ( spec.type === 'password' ) {
control.classList.add( 'kuke-auth__control--peek' );
control.appendChild( passwordToggle( input ) );
}
row.appendChild( control );
if ( spec.type === 'captcha' ) {
var q = el( 'p', 'kuke-auth__captcha' );
q.appendChild( el( 'span', 'kuke-auth__captcha-q', '正在获取…' ) );
row.appendChild( q );
form.captchaQuestion = q.firstChild;
loadCaptcha().then( function ( data ) {
if ( !form.isConnected ) {
return;
}
captcha = data;
q.firstChild.textContent = data ? data.question : '(暂不可用,请稍后重试)';
} );
}
if ( spec.hint ) {
row.appendChild( el( 'p', 'kuke-auth__hint', spec.hint ) );
}
return row;
}
function showAlert( text ) {
if ( !alertEl ) {
return;
}
alertEl.textContent = '';
if ( !text ) {
alertEl.hidden = true;
return;
}
alertEl.appendChild( svg( ICONS.alert, 'kuke-auth__alert-icon' ) );
alertEl.appendChild( el( 'span', null, text ) );
alertEl.hidden = false;
}
function setSubmitting( btn, mode, on ) {
submitting = on;
btn.disabled = on;
btn.classList.toggle( 'kuke-auth__submit--busy', on );
btn.textContent = '';
if ( on ) {
btn.appendChild( el( 'span', 'kuke-auth__spinner' ) );
btn.appendChild( el( 'span', null, MODES[ mode ].busy ) );
} else {
btn.appendChild( el( 'span', null, MODES[ mode ].submit ) );
}
}
function values( form ) {
var out = {};
Array.prototype.forEach.call( form.querySelectorAll( 'input' ), function ( i ) {
out[ i.name ] = i.type === 'checkbox' ? ( i.checked ? '1' : '' ) : i.value;
} );
return out;
}
function submit( mode, form, btn ) {
if ( submitting ) {
return;
}
var v = values( form );
if ( mode === 'signup' && v.password !== v.retype ) {
showAlert( '两次输入的密码不一致。' );
return;
}
showAlert( '' );
setSubmitting( btn, mode, true );
var type = mode === 'login' ? 'login' : 'createaccount';
ready().then( function () {
return token( type );
} ).then( function ( tk ) {
var params;
if ( mode === 'login' ) {
params = {
action: 'clientlogin',
loginreturnurl: returnUrl(),
logintoken: tk,
username: v.username,
password: v.password,
formatversion: 2,
errorformat: 'html',
errorlang: 'zh'
};
if ( v.rememberMe ) {
params.rememberMe = '1';
}
} else {
params = {
action: 'createaccount',
createreturnurl: returnUrl(),
createtoken: tk,
username: v.username,
password: v.password,
retype: v.retype,
formatversion: 2,
errorformat: 'html',
errorlang: 'zh'
};
if ( v.email ) {
params.email = v.email;
}
if ( captcha ) {
params.captchaId = captcha.id;
params.captchaWord = v.captchaWord;
}
}
return api.post( params );
} ).then( function ( d ) {
var res = d.clientlogin || d.createaccount || {};
if ( res.status === 'PASS' ) {
location.href = returnUrl();
return;
}
// 需要额外步骤(如二次验证)时交给完整页面处理
if ( res.status === 'UI' || res.status === 'REDIRECT' ) {
location.href = mw.util.getUrl( MODES[ mode ].page, { returnto: returnTarget() } );
return;
}
setSubmitting( btn, mode, false );
showAlert( res.message || '提交失败,请稍后重试。' );
// token 与验证码一次性,失败后重取
delete tokens[ type ];
if ( mode === 'signup' ) {
refreshCaptcha( form );
}
} ).catch( function ( code, data ) {
setSubmitting( btn, mode, false );
var msg = ( data && data.error && data.error.info ) || '网络异常,请稍后重试。';
showAlert( msg );
delete tokens[ type ];
} );
}
function refreshCaptcha( form ) {
var q = form.captchaQuestion;
if ( !q ) {
return;
}
q.textContent = '正在获取…';
loadCaptcha().then( function ( data ) {
captcha = data;
if ( q.isConnected ) {
q.textContent = data ? data.question : '(暂不可用,请稍后重试)';
}
} );
}
function render( mode ) {
if ( current === mode ) {
return;
}
current = mode;
captcha = null;
titleEl.textContent = MODES[ mode ].title;
subEl.textContent = MODES[ mode ].sub;
Array.prototype.forEach.call( root.querySelectorAll( '.kuke-auth__tab' ), function ( b ) {
var on = b.dataset.mode === mode;
b.classList.toggle( 'kuke-auth__tab--active', on );
b.setAttribute( 'aria-selected', on ? 'true' : 'false' );
} );
body.textContent = '';
alertEl = el( 'div', 'kuke-auth__alert' );
alertEl.setAttribute( 'role', 'alert' );
alertEl.hidden = true;
body.appendChild( alertEl );
var form = el( 'form', 'kuke-auth__form' );
form.setAttribute( 'novalidate', 'novalidate' );
FIELDS[ mode ].forEach( function ( spec ) {
form.appendChild( field( spec, form ) );
} );
var btn = el( 'button', 'kuke-auth__submit' );
btn.type = 'submit';
btn.appendChild( el( 'span', null, MODES[ mode ].submit ) );
form.appendChild( btn );
form.addEventListener( 'submit', function ( e ) {
e.preventDefault();
submit( mode, form, btn );
} );
body.appendChild( form );
var foot = el( 'p', 'kuke-auth__foot' );
if ( mode === 'login' ) {
var reset = el( 'a', null, '忘记密码?' );
reset.href = mw.util.getUrl( 'Special:PasswordReset' );
foot.appendChild( reset );
foot.appendChild( el( 'span', 'kuke-auth__dot', '·' ) );
var toSignup = el( 'a', null, '还没有账号?立即注册' );
toSignup.href = '#';
toSignup.addEventListener( 'click', function ( e ) {
e.preventDefault();
render( 'signup' );
} );
foot.appendChild( toSignup );
} else {
foot.appendChild( el( 'span', null, '已有账号?' ) );
var toLogin = el( 'a', null, '直接登录' );
toLogin.href = '#';
toLogin.addEventListener( 'click', function ( e ) {
e.preventDefault();
render( 'login' );
} );
foot.appendChild( toLogin );
}
body.appendChild( foot );
main.scrollTop = 0;
// 预热对应的 token
token( mode === 'login' ? 'login' : 'createaccount' );
var first = form.querySelector( 'input' );
if ( first ) {
first.focus();
}
}
/* ---------- 开关 ---------- */
function onKey( e ) {
if ( e.key === 'Escape' ) {
close();
return;
}
if ( e.key !== 'Tab' || root.hidden ) {
return;
}
var items = root.querySelectorAll(
'a[href], button:not([disabled]), input:not([type=hidden]), select, textarea'
);
if ( !items.length ) {
return;
}
var first = items[ 0 ];
var last = items[ items.length - 1 ];
if ( e.shiftKey && document.activeElement === first ) {
e.preventDefault();
last.focus();
} else if ( !e.shiftKey && document.activeElement === last ) {
e.preventDefault();
first.focus();
}
}
function open( mode ) {
if ( !root ) {
build();
}
lastFocus = document.activeElement;
root.hidden = false;
document.body.classList.add( 'kuke-auth-open' );
document.addEventListener( 'keydown', onKey );
render( mode );
}
function close() {
if ( !root || root.hidden ) {
return;
}
root.hidden = true;
current = null;
document.body.classList.remove( 'kuke-auth-open' );
document.removeEventListener( 'keydown', onKey );
if ( lastFocus && lastFocus.focus ) {
lastFocus.focus();
}
}
function modeOf( href ) {
var target;
try {
target = new URL( href, location.href );
} catch ( e ) {
return null;
}
if ( target.origin !== location.origin ) {
return null;
}
var path = decodeURIComponent( target.pathname + '?' + target.search );
if ( /Special:UserLogin|特殊:用户登录|Special:登录/i.test( path ) ) {
return 'login';
}
if ( /Special:CreateAccount|特殊:创建账户|Special:创建账号/i.test( path ) ) {
return 'signup';
}
return null;
}
function onClick( e ) {
if ( e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ) {
return;
}
var a = e.target.closest ? e.target.closest( 'a[href]' ) : null;
if ( !a || a.target === '_blank' ) {
return;
}
var mode = modeOf( a.getAttribute( 'href' ) );
if ( !mode ) {
return;
}
e.preventDefault();
open( mode );
}
function init() {
if ( mw.config.get( 'wgUserName' ) ) {
return;
}
document.addEventListener( 'click', onClick );
// 后台预取登录 token,弹窗打开时表单已可直接提交
token( 'login' );
var direct = /[?&]kukeauth=(login|signup)/.exec( location.search );
if ( direct ) {
open( direct[ 1 ] );
}
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
}() );