User:SuperGrey/gadgets/voter/main.js
外观
(重定向自User:SuperGrey/ voter.js)
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/voter]]
var Voter = {
/**
* 頁面名稱
* @type {string}
*/
pageName: null,
/**
* 頁面標題
* @type {string}
*/
sectionTitles: [],
/**
* 有效投票模板
* @type {string[]}
*/
validVoteTemplates: [],
/**
* 無效投票模板
* @type {string[]}
*/
invalidVoteTemplates: [],
/**
* 視窗管理器
* @type {OO.ui.WindowManager}
*/
windowManager: null,
/**
* 投票對話框
* @type {OO.ui.ProcessDialog}
*/
voteDialog: null,
/**
* 程式入口。
*/
init: function () {
this.pageName = mw.config.get('wgPageName');
if (!this.validatePage()) {
console.log("[Voter] 非目標頁面,停止執行。");
return;
}
console.log(`[Voter] 已載入,當前頁面為 ${this.pageName}。`);
this.addVoteButtons();
this.setupDialog();
this.windowManager = new OO.ui.WindowManager();
$(document.body).append(this.windowManager.$element);
this.voteDialog = new this.VoteDialog();
this.windowManager.addWindows([this.voteDialog]);
console.log(`[Voter] 已識別可投票事項共 ${this.sectionTitles.length} 項。`);
},
/**
* 初始化對話框類別。
*/
setupDialog: function () {
let VoteDialog = this.VoteDialog = function (config) {
Voter.VoteDialog.super.call(this, config);
};
OO.inheritClass(VoteDialog, OO.ui.ProcessDialog);
VoteDialog.static.name = 'voteDialog';
VoteDialog.static.title = '投票助手 (Voter v4.0.0)';
VoteDialog.static.actions = [
{flags: ['primary', 'progressive'], label: '儲存投票', action: 'save'},
{flags: 'safe', label: '取消'},
];
VoteDialog.prototype.initialize = function () {
VoteDialog.super.prototype.initialize.call(this);
this.panel = new OO.ui.PanelLayout({padded: true, expanded: false});
this.content = new OO.ui.FieldsetLayout();
this.entrySelection = new OO.ui.MenuTagMultiselectWidget({options: Voter.sectionTitles});
this.fieldEntrySelection = new OO.ui.FieldLayout(this.entrySelection, {
label: '投票條目', align: 'top',
});
this.templateSelection = new OO.ui.MenuTagMultiselectWidget({
options: Voter.validVoteTemplates.concat(Voter.invalidVoteTemplates),
});
this.fieldTemplateSelection = new OO.ui.FieldLayout(this.templateSelection, {
label: '投票模板', align: 'top',
});
this.voteMessageInput = new OO.ui.MultilineTextInputWidget({autosize: true, rows: 1, maxRows: 10});
this.fieldVoteMessageInput = new OO.ui.FieldLayout(this.voteMessageInput, {
label: '投票理由(可不填;無須簽名)', align: 'top',
});
this.content.addItems([
this.fieldEntrySelection, this.fieldTemplateSelection, this.fieldVoteMessageInput,
]);
this.panel.$element.append(this.content.$element);
this.$body.append(this.panel.$element);
this.voteMessageInput.connect(this, {resize: 'onVoteMessageInputResize'});
};
VoteDialog.prototype.onVoteMessageInputResize = function () {
this.updateSize();
};
VoteDialog.prototype.getBodyHeight = function () {
return this.panel.$element.outerHeight(true);
};
VoteDialog.prototype.getSetupProcess = function (data) {
return VoteDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
this.entrySelection.setValue([
Voter.sectionTitles.find(x => x.data === data.sectionID),
]);
this.templateSelection.setValue([Voter.validVoteTemplates[0]?.data]);
}, this);
};
VoteDialog.prototype.getActionProcess = function (action) {
if (action === 'save') {
if (this.entrySelection.getValue().length && this.templateSelection.getValue().length) {
let dialog = this;
return new OO.ui.Process(async function () {
console.log("[Voter] 投票:", {
entries: dialog.entrySelection.getValue(),
templates: dialog.templateSelection.getValue(),
reason: dialog.voteMessageInput.getValue(),
});
await Voter.vote(dialog.entrySelection.getValue(), dialog.templateSelection.getValue(), dialog.voteMessageInput.getValue());
});
}
}
return VoteDialog.super.prototype.getActionProcess.call(this, action);
};
VoteDialog.prototype.getTeardownProcess = function (data) {
return VoteDialog.super.prototype.getTeardownProcess.call(this, data).first(() => {
}, this);
};
},
/**
* 驗證是否為投票頁面,並設置投票模板。
* @returns {boolean} 是否為有效的投票頁面
*/
validatePage: function () {
let validPages = [
{
name: 'Wikipedia:新条目推荐/候选', templates: [
{data: '支持', label: '支持'},
{data: '反對', label: '反對'},
{data: '不合要求', label: '不合要求'},
{data: '問題不當', label: '問題不當'},
],
}, {
name: 'Wikipedia:優良條目評選', templates: [
{data: 'yesGA', label: '符合優良條目標準'},
{data: 'noGA', label: '不符合優良條目標準'},
],
}, {
name: 'Wikipedia:典范条目评选', templates: [
{data: 'yesFA', label: '符合典範條目標準'},
{data: 'noFA', label: '不符合典範條目標準'},
],
}, {
name: 'Wikipedia:特色列表评选', templates: [
{data: 'yesFL', label: '符合特色列表標準'},
{data: 'noFL', label: '不符合特色列表標準'},
],
},
];
for (let page of validPages) {
if (this.pageName === page.name || new RegExp(`^${page.name}/`, 'i').test(this.pageName)) {
this.validVoteTemplates = page.templates;
this.invalidVoteTemplates = ['中立', '意見', '建議', '疑問', '同上', '提醒'].map(template => ({
data: template,
label: template,
}));
return true;
}
}
return false;
},
/**
* 為每個投票區段添加投票按鈕。
*/
addVoteButtons: function () {
let headingSelector = this.pageName === 'Wikipedia:新条目推荐/候选' ? 'div.mw-heading.mw-heading4' : 'div.mw-heading.mw-heading3';
$(headingSelector).each((index, element) => {
let $element = $(element);
let anchor = this.pageName === 'Wikipedia:新条目推荐/候选' ? $element.nextUntil(headingSelector, 'ul').find('li .anchor').attr('id') : $element.find('h3').attr('id');
if (anchor) {
let sectionID = this.getSectionID(index + 1);
$('<span class="mw-editsection-bracket">|</span> <a onclick="Voter.showVoteDialog(' + sectionID + ')">投票</a>').insertAfter($element.find('span.mw-editsection > a'));
this.sectionTitles.push({data: sectionID, label: anchor.replace(/_/g, ' ')});
}
});
console.log("[Voter] 已添加投票按鈕:", this.sectionTitles);
},
/**
* 打開投票對話框。
* @param sectionID {number} 章節編號
*/
showVoteDialog: function (sectionID) {
event.preventDefault();
this.windowManager.openWindow(this.voteDialog, {sectionID});
},
/**
* 取得特定章節編輯編號(支援不同參數位置)。
* @param childid {number} 章節編號
* @returns {number} 編輯編號
*/
getSectionID: function (childid) {
try {
let $heading = this.pageName === 'Wikipedia:新条目推荐/候选' ? $('div.mw-heading.mw-heading4').eq(childid - 1) : $('div.mw-heading.mw-heading3').eq(childid - 1);
let $editlink = $heading.find('span.mw-editsection > a');
let href = $editlink.attr('href');
if (!href) throw new Error('No href found');
let match = href.match(/section=(\\d+)/);
if (match) return +match[1];
let parts = href.split('&');
for (let part of parts) {
if (part.startsWith('section=')) return +part.split('=')[1].replace(/^T-/, '');
}
} catch (e) {
console.log(`[Voter] Failed to get section ID for child ${childid}`);
throw e;
}
return 0;
},
/**
* 比對標題與文本內容。
* @param title {string} 標題
* @returns {string[]} 標題變體
*/
titleVariants: function (title) {
let us = title.replace(/ /g, '_');
let sp = title.replace(/_/g, ' ');
return [title, us, sp, us.charAt(0).toUpperCase() + us.slice(1), sp.charAt(0).toUpperCase() + sp.slice(1)];
},
/**
* 比對文本與標題變體。
* @param text {string} 文本內容
* @param title {string} 標題
* @returns {boolean} 是否包含標題變體
*/
textMatchTitleVariants: function (text, title) {
return this.titleVariants(title).some(variant => text.includes(variant));
},
/**
* 將文字加上縮排。
* @param text {string} 文字內容
* @param indent {string} 縮排字串
* @returns {string} 加上縮排的文字
*/
addIndent: function (text, indent) {
return text.replace(/^/gm, indent);
},
/**
* 重新載入頁面。
* @param entryName {string} 章節名稱
*/
refreshPage: function (entryName) {
location.href = mw.util.getUrl(this.pageName + '#' + entryName); // 先跳轉到投票章節,這樣重載後就不會跳到最上面了
location.reload();
},
/**
* 單次處理投票寫入並檢查衝突。
* @param tracePage {string} 追蹤頁面
* @param destPage {string} 目標頁面
* @param sectionID {number} 章節編號
* @param text {string} 投票內容
* @param summary {string} 編輯摘要
* @returns {Promise<boolean>} 是否發生衝突
*/
voteAPI: async function (tracePage, destPage, sectionID, text, summary) {
let api = new mw.Api({userAgent: 'Voter/4.0.0'});
let votedPageName = this.sectionTitles.find(x => x.data === sectionID)?.label || `section ${sectionID}`;
mw.notify(`正在為「${votedPageName}」投出一票⋯⋯`);
let res = await api.get({
action: 'query',
titles: tracePage,
prop: 'revisions|info',
rvslots: '*',
rvprop: 'content',
rvsection: sectionID,
indexpageids: 1,
});
let page = res.query.pages[res.query.pageids[0]];
let sectionText = page.revisions[0].slots.main['*'];
if (sectionText === undefined || sectionText === '') {
console.log(`[Voter] 無法取得「${votedPageName}」的投票區段內容。區段ID:${sectionID}。API 回傳:`, res);
mw.notify(`無法取得「${votedPageName}」的投票區段內容,請刷新後重試。`);
return true;
}
if (!this.textMatchTitleVariants(sectionText, votedPageName)) {
console.log(`[Voter] 在「${votedPageName}」的投票區段中找不到該條目。區段文本:`, sectionText);
mw.notify(`在該章節找不到名為「${votedPageName}」的提名,請刷新後重試。`);
return true;
}
let mod = {
action: 'edit',
title: destPage,
section: sectionID,
summary: summary,
token: mw.user.tokens.get('csrfToken'),
};
// 處理內部有小標題的情況(例如獨立的評審章節)。
let innerHeadings = tracePage === 'Wikipedia:新条目推荐/候选' ? sectionText.match(/=====.+?=====/g) : sectionText.match(/====.+?====/g);
if (innerHeadings) {
// 在下一個章節號(即小標題)前面插入投票內容。
mod.section += 1;
mod.prependtext = text + '\n';
} else {
// 內部沒有標題,則直接附在最後面。
mod.appendtext = '\n' + text;
}
await api.post(mod);
mw.notify(`「${votedPageName}」已完成投票。`);
return false;
},
/**
* 投票動作的完整實現。
* @param voteIDs {string[]} 投票ID
* @param templates {string[]} 投票模板
* @param message {string} 投票理由
* @returns {Promise<boolean>} 是否發生衝突
*/
vote: async function (voteIDs, templates, message) {
event.preventDefault();
let VTReason = templates.map(str => `{{${str}}}`).join(';');
VTReason += message ? ':' + message : '。';
VTReason += '--~~' + '~~';
for (const id of voteIDs) {
let votedPageName = this.sectionTitles.find(x => x.data === id)?.label || `section ${id}`;
let indent = '*';
let destPage = this.pageName;
if (this.pageName === 'Wikipedia:新条目推荐/候选') {
indent = '**';
} else if (this.pageName === 'Wikipedia:優良條目評選') {
destPage += '/提名區';
} else if (/^Wikipedia:(典范条目评选|特色列表评选)$/i.test(this.pageName)) {
destPage += '/提名区';
}
let text = this.addIndent(VTReason, indent);
let summary = `/* ${votedPageName} */ `;
summary += templates.join('、');
summary += ' ([[User:SuperGrey/gadgets/voter|Voter]])';
if (await this.voteAPI(this.pageName, destPage, id, text, summary)) return true;
}
// 投票完成,等待1秒鐘後刷新頁面。
setTimeout(() => this.refreshPage(this.sectionTitles.find(x => x.data === voteIDs[0])?.label), 1000);
return false;
},
};
// 等待2秒後執行初始化(避免頁面尚未完全載入)。
setTimeout(() => Voter.init(), 2000);