User:SuperGrey/gadgets/voter/main.js
外观
(重定向自User:SuperGrey/ voter.js)
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// [[User:SuperGrey/gadgets/voter]]
// Release: 4.2.2
// Timestamp: 2026-02-06T03:21:53.073Z
// <nowiki>
"use strict";
(() => {
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
// src/state.ts
var State = class {
constructor() {
// 簡繁轉換
this.convByVar = (langDict) => {
if (langDict && langDict.hant) {
return langDict.hant;
}
return "繁簡轉換未初始化,且 langDict 無效!";
};
// 用戶名
this.userName = mw.config.get("wgUserName") || "Example";
// 頁面名稱
this.pageName = mw.config.get("wgPageName");
/**
* 版本號
*/
this.version = "4.2.2";
// MediaWiki API 實例
this._api = null;
/**
* 頁面標題
* @type {{data: number; label: string;}[]}
*/
this.sectionTitles = [];
/**
* 有效投票模板
* @type {{data: string; label: string;}[]}
*/
this.validVoteTemplates = [];
/**
* 無效投票模板
* @type {{data: string; label: string;}[]}
*/
this.invalidVoteTemplates = [];
}
async initHanAssist() {
const requireModule = await mw.loader.using("ext.gadget.HanAssist");
const hanAssist = requireModule("ext.gadget.HanAssist");
if (hanAssist && typeof hanAssist.convByVar === "function") {
this.convByVar = hanAssist.convByVar;
}
}
getApi() {
if (!this._api) {
this._api = new mw.Api({
ajax: {
headers: {
"User-Agent": `Voter/${this.version}`
}
}
});
}
return this._api;
}
};
var state = new State();
var state_default = state;
// src/api.ts
async function getXToolsInfo(pageName) {
const safeToLocaleString = (value) => {
if (typeof value === "number" && !isNaN(value)) {
return value.toLocaleString();
}
return "0";
};
try {
const pageInfo = await $.get("https://xtools.wmcloud.org/api/page/pageinfo/" + mw.config.get("wgServerName") + "/" + pageName.replace(/["?%&+\\]/g, escape));
const project = pageInfo.project;
const pageEnc = encodeURIComponent(pageInfo.page);
const pageUrl = `https://${project}/wiki/${pageInfo.page}`;
const pageinfoUrl = `https://xtools.wmcloud.org/pageinfo/${project}/${pageEnc}`;
const permaLinkUrl = `https://${project}/wiki/Special:PermaLink%2F${pageInfo.created_rev_id}`;
const diffUrl = `https://${project}/wiki/Special:Diff%2F${pageInfo.modified_rev_id}`;
const pageviewsUrl = `https://pageviews.wmcloud.org/?project=${project}&pages=${pageEnc}&range=latest-${pageInfo.pageviews_offset}`;
const creatorLink = `https://${project}/wiki/User:${pageInfo.creator}`;
const creatorContribsUrl = `https://${project}/wiki/Special:Contributions/${pageInfo.creator}`;
const createdDate = new Date(pageInfo.created_at).toISOString().split("T")[0];
const revisionsText = safeToLocaleString(pageInfo.revisions);
const editorsText = safeToLocaleString(pageInfo.editors);
const watchersText = safeToLocaleString(pageInfo.watchers);
const pageviewsText = safeToLocaleString(pageInfo.pageviews);
const days = Math.round(pageInfo.secs_since_last_edit / 86400);
let creatorText = "";
if (pageInfo.creator_editcount) {
creatorText = `<bdi><a href="${creatorLink}" target="_blank">${pageInfo.creator}</a></bdi> (<a href="${creatorContribsUrl}" target="_blank">${safeToLocaleString(pageInfo.creator_editcount)}</a>)`;
} else {
creatorText = `<bdi><a href="${creatorContribsUrl}" target="_blank">${pageInfo.creator}</a></bdi>`;
}
let pageCreationText = `「<a target="_blank" title="評級: ${pageInfo.assessment.value}" href="${pageinfoUrl}"><img src="${pageInfo.assessment.badge}" style="height:16px !important; vertical-align:-4px; margin-right:3px"/></a><bdi><a target="_blank" href="${pageUrl}">${pageInfo.page}</a></bdi>」由 ${creatorText} 於 <bdi><a target='_blank' href='${permaLinkUrl}'>${createdDate}</a></bdi> 建立,共 ${revisionsText} 個修訂,最後修訂於 <a href="${diffUrl}">${days} 天</a>前。`;
let pageEditorsText = `共 ${editorsText} 編輯者` + (watchersText !== "0" ? `、${watchersText} 監視者` : "") + `,最近 ${pageInfo.pageviews_offset} 天共 <a target="_blank" href="${pageviewsUrl}">${pageviewsText} 瀏覽數</a>。`;
return `<span style="line-height:20px">${pageCreationText}${pageEditorsText}<a target="_blank" href="${pageinfoUrl}">檢視完整頁面統計</a>。</span>`.trim();
} catch (error) {
console.error("[Voter] Error fetching XTools data:", error);
return '<span style="color: red; font-weight: bold;">無法獲取 XTools 頁面資訊。</span>';
}
}
async function voteAPI(tracePage, destPage, sectionID, text, summary) {
var _a, _b;
const votedPageName = ((_a = state_default.sectionTitles.find((x) => x.data === sectionID)) == null ? void 0 : _a.label) || `section ${sectionID}`;
mw.notify(`正在為「${votedPageName}」投出一票⋯⋯`);
const res = await state_default.getApi().get({
action: "query",
titles: destPage,
prop: "revisions|info",
rvslots: "*",
rvprop: "content",
rvsection: sectionID,
indexpageids: 1
});
const firstPageId = res.query.pageids[0];
const page = res.query.pages[firstPageId];
const firstRevision = (_b = page == null ? void 0 : page.revisions) == null ? void 0 : _b[0];
const sectionText = firstRevision == null ? void 0 : firstRevision.slots.main["*"];
if (sectionText === void 0 || sectionText === "") {
console.log(`[Voter] 無法取得「${votedPageName}」的投票區段內容。區段ID:${sectionID}。API 回傳:`, res);
mw.notify(`無法取得「${votedPageName}」的投票區段內容,請刷新後重試。`);
return true;
}
if (!textMatchTitleVariants(sectionText, votedPageName)) {
console.log(`[Voter] 在「${votedPageName}」的投票區段中找不到該條目。區段文本:`, sectionText);
mw.notify(`在該章節找不到名為「${votedPageName}」的提名,請刷新後重試。`);
return true;
}
let innerHeadings;
if (tracePage === "Wikipedia:新条目推荐/候选") {
innerHeadings = sectionText.match(/=====.+?=====/g);
} else {
innerHeadings = sectionText.match(/===.+?===/g);
}
const targetSection = innerHeadings ? sectionID + 1 : sectionID;
const editParams = {
action: "edit",
title: destPage,
section: targetSection,
summary,
token: mw.user.tokens.get("csrfToken")
};
if (innerHeadings) {
editParams.prependtext = `${text}
`;
} else {
editParams.appendtext = `
${text}`;
}
await state_default.getApi().post(editParams);
mw.notify(`「${votedPageName}」已完成投票。`);
return false;
}
// src/build_comment.ts
function isListLineNode(node) {
return node.text !== void 0;
}
var SPACE_AFTER_INDENTATION_CHARS = true;
var PARAGRAPH_TEMPLATES = [];
var FILE_PREFIX_PATTERN = "(?:File|Image)";
var MASK_PREFIX = "<<VOTER_MASK_";
var MASK_SUFFIX = "_VOTER>>";
var MASK_ANY_REGEXP = /<<VOTER_MASK_(\d+)(?:_\w+(?:_\d+)?)?_VOTER>>/g;
var POPULAR_NOT_INLINE_ELEMENTS = [
"BLOCKQUOTE",
"CAPTION",
"CENTER",
"DD",
"DIV",
"DL",
"DT",
"FIGURE",
"FIGCAPTION",
"FORM",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"HR",
"INPUT",
"LI",
"LINK",
"OL",
"P",
"PRE",
"SECTION",
"STYLE",
"TABLE",
"TBODY",
"TD",
"TFOOT",
"TH",
"THEAD",
"TR",
"UL"
];
var PNIE_PATTERN = `(?:${POPULAR_NOT_INLINE_ELEMENTS.join("|")})`;
var FILE_PATTERN_END = `\\[\\[${FILE_PREFIX_PATTERN}:.+\\]\\]$`;
var GALLERY_REGEXP = /^<<VOTER_MASK_\d+_gallery_VOTER>>$/m;
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function generateTagsRegexp(tags) {
const tagsJoined = tags.join("|");
return new RegExp(`(<(${tagsJoined})(?: [\\w ]+(?:=[^<>]+?)?| *)>)([^]*?)(</\\2>)`, "ig");
}
var TextMasker = class {
constructor(text, maskedTexts) {
this.text = text;
this.maskedTexts = maskedTexts || [];
}
/**
* 使用指定的正則表達式和類型對文本進行遮罩處理,將匹配的文本替換為特殊標記並存儲原始文本。
* @param {RegExp} regexp 用於匹配要遮罩的文本的正則表達式
* @param {string} [type] 遮罩類型,用於生成不同的標記
* @param {boolean} [useGroups=false] 是否使用正則表達式的捕獲組來分割前綴文本和要遮罩的文本
* @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
*/
mask(regexp, type, useGroups = false) {
this.text = this.text.replace(regexp, (s, preText, textToMask) => {
if (!useGroups) {
preText = "";
textToMask = s;
}
const masked = textToMask || s;
const id = this.maskedTexts.push(masked);
const typeSuffix = type ? `_${type}` : "";
return `${preText || ""}${MASK_PREFIX}${id}${typeSuffix}${MASK_SUFFIX}`;
});
return this;
}
/**
* 根據指定的類型對文本進行解遮罩處理,將特殊標記替換回原始文本。
* @param {string} text 要解遮罩的文本
* @param {string} [type] 遮罩類型,用於匹配對應的標記
* @returns {string} 解遮罩後的文本
*/
unmaskText(text, type) {
const regexp = type ? new RegExp(
`${escapeRegExp(MASK_PREFIX)}(\\d+)(?:_${escapeRegExp(type)}(?:_\\d+)?)?${escapeRegExp(MASK_SUFFIX)}`,
"g"
) : MASK_ANY_REGEXP;
while (regexp.test(text)) {
text = text.replace(regexp, (_s, num) => this.maskedTexts[Number(num) - 1]);
}
return text;
}
/**
* 對文本進行解遮罩處理,將所有特殊標記替換回原始文本。
* @param {string} [type] 遮罩類型,用於匹配對應的標記
* @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
*/
unmask(type) {
this.text = this.unmaskText(this.text, type);
return this;
}
/**
* 遞歸地遮罩文本中的模板,將匹配的模板替換為特殊標記並存儲原始模板文本。
* @param {(templateCode: string) => string} [handler] 可選的處理函數,用於在遮罩前對模板文本進行修改
* @param {boolean} [addLengths=false] 是否在標記中添加原始模板文本的長度信息
* @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
*/
maskTemplatesRecursively(handler, addLengths = false) {
let pos = 0;
const stack = [];
while (true) {
let left = this.text.indexOf("{{", pos);
let right = this.text.indexOf("}}", pos);
if (left !== -1 && left < right) {
stack.push(left);
pos = left + 2;
} else {
if (!stack.length) break;
left = stack.pop();
if (typeof left === "undefined") {
if (right === -1) {
pos += 2;
continue;
} else {
left = 0;
}
}
if (right === -1) {
right = this.text.length;
}
right += 2;
let template = this.text.substring(left, right);
if (handler) {
template = handler(template);
}
const lengthOrNot = addLengths ? "_" + template.replace(
new RegExp(
`${escapeRegExp(MASK_PREFIX)}\\d+_template_(\\d+)${escapeRegExp(MASK_SUFFIX)}`,
"g"
),
(_m, n) => new Array(Number(n) + 1).join(" ")
).length : "";
this.text = this.text.substring(0, left) + MASK_PREFIX + this.maskedTexts.push(template) + "_template" + lengthOrNot + MASK_SUFFIX + this.text.slice(right);
pos = right - template.length;
}
}
return this;
}
/**
* 對文本中的指定標籤進行遮罩處理,將匹配的標籤替換為特殊標記並存儲原始標籤文本。
* @param {string[]} tags 標籤名稱列表
* @param {string} type 遮罩類型,用於生成不同的標記
* @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
*/
maskTags(tags, type) {
return this.mask(generateTagsRegexp(tags), type);
}
/**
* 對敏感代碼區塊進行遮罩處理(block/gallery/nowiki/template/table)。
* @param {(templateCode: string) => string} [templateHandler] 可選的處理函數,用於在遮罩前對模板文本進行修改
* @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
*/
maskSensitiveCode(templateHandler) {
return this.maskTags(["pre", "source", "syntaxhighlight"], "block").maskTags(["gallery", "poem"], "gallery").maskTags(["nowiki"], "inline").maskTemplatesRecursively(templateHandler).mask(/^(:* *)(\{\|[^]*?\n\|\})/gm, "table", true).mask(/^(:* *)(\{\|[^]*\n\|)/gm, "table", true);
}
};
function prependIndentationToLine(indentation, line) {
return indentation + (indentation && SPACE_AFTER_INDENTATION_CHARS && !/^[:*#;]/.test(line) ? " " : "") + line;
}
function linesToLists(lines, isNested = false) {
const listTags = { ":": "dl", ";": "dl", "*": "ul", "#": "ol" };
const itemTags = { ":": "dd", ";": "dt", "*": "li", "#": "li" };
let list = { items: [] };
for (let i = 0; i <= lines.length; i++) {
if (i === lines.length) {
if (list.type) {
lineToList(lines, i, list, isNested);
}
} else {
const node = lines[i];
const text = isListLineNode(node) ? node.text : "";
const firstChar = text[0] || "";
const listType = listTags[firstChar];
if (list.type && listType !== list.type) {
const itemsCount = list.items.length;
lineToList(lines, i, list, isNested);
i -= itemsCount - 1;
list = { items: [] };
}
if (listType) {
list.type = listType;
list.items.push({
type: itemTags[firstChar],
text: text.slice(1)
});
}
}
}
return lines;
}
function lineToList(lines, i, list, isNested = false) {
if (isNested) {
const previousItemIndex = i - list.items.length - 1;
if (previousItemIndex >= 0) {
const item = {
type: lines[previousItemIndex].type,
items: [lines[previousItemIndex], list]
};
lines.splice(previousItemIndex, list.items.length + 1, item);
} else {
const item = {
type: lines[0].type,
items: [list]
};
lines.splice(i - list.items.length, list.items.length, item);
}
} else {
lines.splice(i - list.items.length, list.items.length, list);
}
linesToLists(list.items, true);
}
function listToTags(lines, isNested = false) {
let text = "";
lines.forEach((line, i) => {
if (!isListLineNode(line)) {
const itemsText = (line.items || []).map((item) => {
const itemText = !isListLineNode(item) ? listToTags(item.items || [], true) : String(item.text).trim();
return item.type ? `<${item.type}>${itemText}</${item.type}>` : itemText;
}).join("");
text += `<${line.type}>${itemsText}</${line.type}>`;
} else {
text += isNested ? String(line.text).trim() : String(line.text);
}
if (i !== lines.length - 1) {
text += "\n";
}
});
return text;
}
function listMarkupToTags(code) {
const lineObjects = code.split("\n").map((line) => ({ type: "", text: line }));
return listToTags(linesToLists(lineObjects));
}
function findWrapperFlags(text, indentation) {
if (!indentation) {
return {
areThereTagsAroundMultipleLines: false,
areThereTagsAroundListMarkup: false
};
}
const tagMatches = text.match(generateTagsRegexp(["[a-z]+"])) || [];
const quoteMatches = text.match(
/(<(?:blockquote|q)(?: [\w ]+(?:=[^<>]+?)?| *)>)([^]*?)(<\/(?:blockquote|q)>)/ig
) || [];
const matches = tagMatches.concat(quoteMatches);
return {
areThereTagsAroundMultipleLines: matches.some((match) => match.indexOf("\n") !== -1),
areThereTagsAroundListMarkup: matches.some((match) => /\n[:*#;]/.test(match))
};
}
function handleIndentedComment(code, indentation, restLinesIndentation, isWrapped, isInTemplate, flags) {
if (!indentation) {
return code;
}
code = code.replace(/^ +/gm, "");
if (/^[:*#;]/m.test(code) && (isWrapped || restLinesIndentation === "#")) {
if (isInTemplate) {
code = code.replace(/\|(?:[^|=}]*=)?(?=[:*#;])/, "$&\n");
}
code = listMarkupToTags(code);
}
code = code.replace(
new RegExp(`(\\n+)([:*#;]|${escapeRegExp(MASK_PREFIX)}\\d+_table${escapeRegExp(MASK_SUFFIX)}|${FILE_PATTERN_END})`, "gmi"),
(_s, newlines, nextLine) => (newlines.length > 1 ? "\n\n\n" : "\n") + prependIndentationToLine(restLinesIndentation, nextLine)
);
code = code.replace(/(^|[^\n])(<<VOTER_MASK_\d+_gallery_VOTER>>)/g, (_s, before, marker) => `${before}
${marker}`).replace(/<<VOTER_MASK_\d+_gallery_VOTER>>(?=(?:$|[^\n]))/g, (marker) => `${marker}
`);
if (restLinesIndentation.indexOf("#") !== -1 && /<<VOTER_MASK_\d+_table_VOTER>>/.test(code)) {
throw new Error("numberedList-table");
}
if (restLinesIndentation === "#" && GALLERY_REGEXP.test(code)) {
throw new Error("numberedList");
}
code = code.replace(
/^((?:[:*#;].+|<<VOTER_MASK_\d+_(?:table|gallery)_VOTER>>))(\n+)(?![:#])/mg,
(_s, previousLine, newlines) => previousLine + "\n" + prependIndentationToLine(restLinesIndentation, newlines.length > 1 ? "\n\n" : "")
);
if (PARAGRAPH_TEMPLATES.length) {
code = code.replace(/^(.*)\n\n+(?!:)/gm, `$1{{${PARAGRAPH_TEMPLATES[0]}}}
`);
} else if (flags.areThereTagsAroundMultipleLines) {
code = code.replace(/^(.*)\n\n+(?!:)/gm, "$1<br> \n");
} else {
code = code.replace(
/^(.*)\n\n+(?!:)/gm,
(_s, m1) => `${m1}
${prependIndentationToLine(restLinesIndentation, "")}`
);
}
return code;
}
function processNewlines(code, indentation, isInTemplate = false) {
const entireLineRegexp = /^<<VOTER_MASK_\d+_(block|template)(?:_\d+)?_VOTER>> *$/;
const entireLineFromStartRegexp = /^(=+).*\1[ \t]*$|^----/;
const fileRegexp = new RegExp(`^${FILE_PATTERN_END}`, "i");
let currentLineInTemplates = "";
let nextLineInTemplates = "";
if (isInTemplate) {
currentLineInTemplates = "|=";
nextLineInTemplates = "|\\||}}";
}
const paragraphTemplatePattern = PARAGRAPH_TEMPLATES.length ? mw.util.escapeRegExp(`{{${PARAGRAPH_TEMPLATES[0]}}}`) : "(?!)";
const currentLineEndingRegexp = new RegExp(
`(?:<${PNIE_PATTERN}(?: [\\w ]+?=[^<>]+?| ?\\/?)>|<\\/${PNIE_PATTERN}>|${escapeRegExp(MASK_PREFIX)}\\d+_block${escapeRegExp(MASK_SUFFIX)}|<br[ \\n]*\\/?>|${paragraphTemplatePattern}${currentLineInTemplates}) *$`,
"i"
);
const nextLineBeginningRegexp = new RegExp(
`^(?:<\\/${PNIE_PATTERN}>|<${PNIE_PATTERN}${nextLineInTemplates})`,
"i"
);
const newlinesRegexp = indentation ? /^(.+)\n(?![:#])(?=(.*))/gm : new RegExp(
`^((?![:*#; ]).+)\\n(?![\\n:*#; ]|${escapeRegExp(MASK_PREFIX)}\\d+_table${escapeRegExp(MASK_SUFFIX)})(?=(.*))`,
"gm"
);
return code.replace(newlinesRegexp, (_s, currentLine, nextLine) => {
const lineBreakOrNot = entireLineRegexp.test(currentLine) || entireLineRegexp.test(nextLine) || !indentation && (entireLineFromStartRegexp.test(currentLine) || entireLineFromStartRegexp.test(nextLine)) || fileRegexp.test(currentLine) || fileRegexp.test(nextLine) || GALLERY_REGEXP.test(currentLine) || GALLERY_REGEXP.test(nextLine) || currentLineEndingRegexp.test(currentLine) || nextLineBeginningRegexp.test(nextLine) ? "" : `<br>${indentation ? " " : ""}`;
const newlineOrNot = indentation && !GALLERY_REGEXP.test(nextLine) ? "" : "\n";
return currentLine + lineBreakOrNot + newlineOrNot;
});
}
function processCode(code, indentation, restLinesIndentation, flags, isInTemplate) {
let result = handleIndentedComment(
code,
indentation,
restLinesIndentation,
isInTemplate || flags.areThereTagsAroundListMarkup,
isInTemplate,
flags
);
result = processNewlines(result, indentation, isInTemplate);
return result;
}
function buildWikitext(text, indent) {
const indentation = indent || "";
const restLinesIndentation = indentation ? indentation.replace(/\*/g, ":") : "";
const masker = new TextMasker((text || "").trim());
masker.maskSensitiveCode((templateCode) => processCode(
templateCode,
indentation,
restLinesIndentation,
{
areThereTagsAroundMultipleLines: false,
areThereTagsAroundListMarkup: false
},
true
));
const flags = findWrapperFlags(masker.text, indentation);
masker.text = processCode(masker.text, indentation, restLinesIndentation, flags, false);
masker.text += "\n";
let finalIndentation = indentation;
if (finalIndentation && /^[*#;\x03]/.test(masker.text)) {
finalIndentation = restLinesIndentation;
}
masker.text = prependIndentationToLine(finalIndentation, masker.text);
masker.unmask();
return masker.text;
}
// src/dialog.ts
var mountedApp = null;
var mountedRoot = null;
var MOUNT_ID = "voter-dialog-mount";
function loadCodex() {
return new Promise((resolve, reject) => {
mw.loader.using("@wikimedia/codex").then((requireFn) => {
resolve({
Vue: requireFn ? requireFn("vue") : null,
Codex: requireFn ? requireFn("@wikimedia/codex") : null
});
}).catch((err) => {
const reason = err instanceof Error ? err : new Error(
typeof err === "string" ? err : (() => {
try {
return JSON.stringify(err);
} catch (e) {
return "Unknown error";
}
})()
);
reject(reason);
});
});
}
async function loadCodexAndVue() {
const loaded = await loadCodex();
return loaded;
}
function ensureMount(id = MOUNT_ID) {
let mount = document.getElementById(id);
if (!mount) {
mount = document.createElement("div");
mount.id = id;
document.body.appendChild(mount);
}
return mount;
}
function createDialogMountIfNeeded() {
return ensureMount(MOUNT_ID);
}
function mountApp(app) {
createDialogMountIfNeeded();
mountedApp = app;
mountedRoot = mountedApp.mount(`#${MOUNT_ID}`);
return mountedApp;
}
function getMountedApp() {
return mountedApp;
}
function removeDialogMount() {
const mount = document.getElementById(MOUNT_ID);
if (mount) {
mount.remove();
}
mountedApp = null;
mountedRoot = null;
}
function registerCodexComponents(app, Codex) {
if (!app || !app.component || !Codex) return;
try {
if (Codex.CdxDialog) app.component("cdx-dialog", Codex.CdxDialog);
if (Codex.CdxButton) app.component("cdx-button", Codex.CdxButton);
if (Codex.CdxSelect) app.component("cdx-select", Codex.CdxSelect);
if (Codex.CdxTextInput) app.component("cdx-text-input", Codex.CdxTextInput);
if (Codex.CdxTextArea) app.component("cdx-text-area", Codex.CdxTextArea);
if (Codex.CdxCheckbox) app.component("cdx-checkbox", Codex.CdxCheckbox);
if (Codex.CdxField) app.component("cdx-field", Codex.CdxField);
if (Codex.CdxMultiselectLookup) app.component("cdx-multiselect-lookup", Codex.CdxMultiselectLookup);
} catch (e) {
}
}
// src/vote_dialog.ts
var entryInfoPromiseCache = /* @__PURE__ */ new Map();
var voteMessageCache = /* @__PURE__ */ new Map();
var codeMirrorRequirePromise = null;
function getVoteIndent(useBulleted) {
if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
return useBulleted ? "**" : "*:";
}
return useBulleted ? "*" : ":";
}
function buildFinalVoteWikitext(rawText, indent) {
let finalVoteText = rawText.trim();
if (!/--~{3,}/.test(finalVoteText)) {
finalVoteText += "--~~~~";
}
return buildWikitext(finalVoteText, indent);
}
function getCachedEntryInfo(entryName) {
const cached = entryInfoPromiseCache.get(entryName);
if (cached) return cached;
const pending = getXToolsInfo(entryName).catch((error) => {
entryInfoPromiseCache.delete(entryName);
throw error;
});
entryInfoPromiseCache.set(entryName, pending);
return pending;
}
function loadCodeMirrorModules() {
if (codeMirrorRequirePromise) {
return codeMirrorRequirePromise;
}
codeMirrorRequirePromise = new Promise((resolve, reject) => {
mw.loader.using(["ext.CodeMirror.v6", "ext.CodeMirror.v6.mode.mediawiki"]).then(
(requireFn) => resolve(requireFn),
(error) => {
const reason = error instanceof Error ? error : new Error(String(error));
reject(reason);
}
);
});
return codeMirrorRequirePromise;
}
function getEntryNameById(entryId) {
var _a;
return ((_a = state_default.sectionTitles.find((x) => x.data === entryId)) == null ? void 0 : _a.label) || "";
}
function getCachedVoteMessageById(entryId) {
const entryName = getEntryNameById(entryId);
if (!entryName) return void 0;
return voteMessageCache.get(entryName);
}
function setCachedVoteMessageById(entryId, message) {
const entryName = getEntryNameById(entryId);
if (!entryName) return;
voteMessageCache.set(entryName, message);
}
function createVoteDialog(sectionID) {
loadCodexAndVue().then(({ Vue, Codex }) => {
const app = Vue.createMwApp({
i18n: {
dialogTitle: state_default.convByVar({
hant: `投票助手 (Voter v${state_default.version})`,
hans: `投票助手 (Voter v${state_default.version})`
}),
submitting: state_default.convByVar({ hant: "儲存中…", hans: "保存中…" }),
submit: state_default.convByVar({ hant: "儲存投票", hans: "保存投票" }),
cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
next: state_default.convByVar({ hant: "下一步", hans: "下一步" }),
previous: state_default.convByVar({ hant: "上一步", hans: "上一步" }),
// Step 0: Entry Selection
selectEntriesHeading: state_default.convByVar({ hant: "投票條目", hans: "投票条目" }),
selectEntriesHint: state_default.convByVar({ hant: "建議在閱讀條目後再投票。", hans: "建议在阅读条目后再投票。" }),
// Step 1: Per-entry Vote Content
insertTemplateHeading: state_default.convByVar({ hant: "投票理由", hans: "投票理由" }),
insertTemplateHint: state_default.convByVar({ hant: "模板按鈕會插入到游標所在位置。", hans: "模板按钮会插入到光标所在位置。" }),
voteReasonPlaceholder: state_default.convByVar({ hant: "輸入投票內容…", hans: "输入投票内容…" }),
useBulleted: state_default.convByVar({ hant: "使用 * 縮排", hans: "使用 * 缩进" }),
// Step 2: Preview
previewHeading: state_default.convByVar({ hant: "預覽投票內容", hans: "预览投票内容" }),
previewInfo: state_default.convByVar({ hant: "以下是將要提交的投票內容。", hans: "以下是将要提交的投票内容。" }),
// Validation
noEntriesSelected: state_default.convByVar({ hant: "請選擇至少一個投票條目。", hans: "请选择至少一个投票条目。" }),
noVoteContent: state_default.convByVar({ hant: "請輸入投票內容,或先插入模板。", hans: "请输入投票内容,或先插入模板。" })
},
data() {
var _a;
const defaultVoteMessage = state_default.validVoteTemplates.length > 0 ? `{{${state_default.validVoteTemplates[0].data}}}。` : "";
const initialVoteMessage = (_a = getCachedVoteMessageById(sectionID)) != null ? _a : defaultVoteMessage;
return {
open: true,
isSubmitting: false,
currentStep: 0,
totalSteps: 3,
// Step 0: Entry selection
selectedEntries: [sectionID],
entryInfoHtml: "",
entryInfoById: {},
isLoadingInfo: false,
// Step 1: Per-entry vote content
voteMessages: {
[sectionID]: initialVoteMessage
},
codeMirrorByEntryId: {},
useBulleted: true
};
},
computed: {
entryOptions() {
return state_default.sectionTitles.map((item) => ({ value: item.data, label: item.label }));
},
validTemplateOptions() {
return (state_default.validVoteTemplates || []).map((item) => ({ value: item.data, label: item.label }));
},
invalidTemplateOptions() {
return (state_default.invalidVoteTemplates || []).map((item) => ({ value: item.data, label: item.label }));
},
allTemplateOptions() {
return [...this.validTemplateOptions, ...this.invalidTemplateOptions];
},
selectedEntryItems() {
return this.selectedEntries.map((id) => {
const entry = state_default.sectionTitles.find((x) => x.data === id);
return { id, name: entry ? entry.label : `Section ${id}` };
});
},
previewVoteItems() {
const indent = getVoteIndent(this.useBulleted);
return this.selectedEntryItems.map((item) => {
const message = this.voteMessages[item.id] || "";
return {
id: item.id,
name: item.name,
text: buildFinalVoteWikitext(message, indent)
};
});
},
primaryAction() {
if (this.currentStep < this.totalSteps - 1) {
return { label: this.$options.i18n.next, actionType: "primary", disabled: false };
}
return {
label: this.isSubmitting ? this.$options.i18n.submitting : this.$options.i18n.submit,
actionType: "progressive",
disabled: this.isSubmitting
};
},
defaultAction() {
if (this.currentStep > 0) {
return { label: this.$options.i18n.previous };
}
return { label: this.$options.i18n.cancel };
}
},
watch: {
selectedEntries: {
handler() {
if (this.currentStep === 1) {
this.syncVoteMessagesFromTextareas();
}
this.syncVoteMessages();
void this.loadEntryInfo();
if (this.currentStep === 1) {
setTimeout(() => {
void this.syncCodeMirrorInstances();
}, 0);
}
},
immediate: true
},
currentStep(step) {
if (step === 1) {
setTimeout(() => {
void this.syncCodeMirrorInstances();
}, 0);
} else {
this.destroyAllCodeMirror();
}
}
},
methods: {
getStepClass(step) {
return { "voter-multistep-dialog__stepper__step--active": step <= this.currentStep };
},
getDefaultVoteMessage() {
return this.validTemplateOptions.length > 0 ? `{{${this.validTemplateOptions[0].value}}}。` : "";
},
syncVoteMessages() {
var _a;
const nextMessages = {};
for (const id of this.selectedEntries) {
if (Object.prototype.hasOwnProperty.call(this.voteMessages, id)) {
nextMessages[id] = this.voteMessages[id];
} else {
nextMessages[id] = (_a = getCachedVoteMessageById(id)) != null ? _a : this.getDefaultVoteMessage();
}
setCachedVoteMessageById(id, nextMessages[id]);
}
this.voteMessages = nextMessages;
},
syncVoteMessagesFromTextareas() {
var _a, _b, _c, _d;
const nextMessages = __spreadValues({}, this.voteMessages);
for (const id of this.selectedEntries) {
const binding = this.codeMirrorByEntryId[id];
const codeMirrorText = (_d = (_c = (_b = (_a = binding == null ? void 0 : binding.cm) == null ? void 0 : _a.view) == null ? void 0 : _b.state) == null ? void 0 : _c.doc) == null ? void 0 : _d.toString();
if (typeof codeMirrorText === "string") {
nextMessages[id] = codeMirrorText;
continue;
}
const textarea = this.getVoteTextarea(id);
if (textarea) {
nextMessages[id] = textarea.value;
}
setCachedVoteMessageById(id, nextMessages[id]);
}
this.voteMessages = nextMessages;
},
async loadEntryInfo() {
if (!this.selectedEntries.length) {
this.entryInfoHtml = "";
this.entryInfoById = {};
return;
}
this.isLoadingInfo = true;
const infoPromises = this.selectedEntries.map(async (id) => {
var _a;
const entryName = ((_a = state_default.sectionTitles.find((x) => x.data === id)) == null ? void 0 : _a.label) || "";
if (!entryName) return { id, html: "" };
const html = await getCachedEntryInfo(entryName);
return { id, html };
});
const results = await Promise.all(infoPromises);
const nextInfoById = {};
for (const result of results) {
nextInfoById[result.id] = result.html || "";
}
this.entryInfoById = nextInfoById;
this.entryInfoHtml = this.selectedEntries.map((id) => this.entryInfoById[id]).filter(Boolean).map((html) => `<div style="margin-top:0.5em">${html}</div>`).join("");
this.isLoadingInfo = false;
},
validateStep0() {
if (!this.selectedEntries.length) {
mw.notify(this.$options.i18n.noEntriesSelected, { type: "error", title: "[Voter]" });
return false;
}
return true;
},
validateStep1() {
this.syncVoteMessagesFromTextareas();
for (const item of this.selectedEntryItems) {
if (!(this.voteMessages[item.id] || "").trim()) {
mw.notify(`${this.$options.i18n.noVoteContent} (${item.name})`, { type: "error", title: "[Voter]" });
return false;
}
}
return true;
},
getVoteTextarea(entryId) {
const container = document.querySelector(`.voter-entry-vote[data-entry-id="${entryId}"]`);
return container ? container.querySelector("textarea") : null;
},
destroyCodeMirrorForEntry(entryId) {
const binding = this.codeMirrorByEntryId[entryId];
if (!binding) return;
binding.textarea.removeEventListener("input", binding.onInput);
try {
if (typeof binding.cm.destroy === "function") {
binding.cm.destroy();
}
} catch (error) {
console.warn("[Voter] Failed to destroy CodeMirror:", error);
}
const nextBindings = __spreadValues({}, this.codeMirrorByEntryId);
delete nextBindings[entryId];
this.codeMirrorByEntryId = nextBindings;
},
destroyAllCodeMirror() {
const ids = Object.keys(this.codeMirrorByEntryId).map((id) => Number(id));
for (const id of ids) {
this.destroyCodeMirrorForEntry(id);
}
},
async initCodeMirrorForEntry(entryId) {
if (this.codeMirrorByEntryId[entryId]) return;
const textarea = this.getVoteTextarea(entryId);
if (!textarea) return;
try {
const requireFn = await loadCodeMirrorModules();
const CodeMirrorCtor = requireFn("ext.CodeMirror.v6");
const modeModule = requireFn("ext.CodeMirror.v6.mode.mediawiki");
const mode = typeof modeModule.mediawiki === "function" ? modeModule.mediawiki() : void 0;
if (!CodeMirrorCtor || !mode) return;
const cm = new CodeMirrorCtor(textarea, mode);
cm.initialize();
const onInput = () => {
this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
[entryId]: textarea.value
});
};
textarea.addEventListener("input", onInput);
this.codeMirrorByEntryId = __spreadProps(__spreadValues({}, this.codeMirrorByEntryId), {
[entryId]: { cm, textarea, onInput }
});
} catch (error) {
console.warn("[Voter] CodeMirror initialization failed, fallback to textarea.", error);
}
},
async syncCodeMirrorInstances() {
if (this.currentStep !== 1) return;
const selectedIdSet = new Set(this.selectedEntries);
for (const key of Object.keys(this.codeMirrorByEntryId)) {
const id = Number(key);
if (!selectedIdSet.has(id)) {
this.destroyCodeMirrorForEntry(id);
}
}
for (const id of this.selectedEntries) {
await this.initCodeMirrorForEntry(id);
}
},
insertTemplate(template, entryId) {
var _a, _b, _c, _d, _e, _f, _g;
const templateText = `{{${template}}}`;
const binding = this.codeMirrorByEntryId[entryId];
const view = (_a = binding == null ? void 0 : binding.cm) == null ? void 0 : _a.view;
const selection = (_c = (_b = view == null ? void 0 : view.state) == null ? void 0 : _b.selection) == null ? void 0 : _c.main;
if (view && selection && typeof view.dispatch === "function") {
view.dispatch({
changes: {
from: selection.from,
to: selection.to,
insert: templateText
},
selection: { anchor: selection.from + templateText.length }
});
const updated = ((_e = (_d = view.state) == null ? void 0 : _d.doc) == null ? void 0 : _e.toString()) || "";
this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
[entryId]: updated
});
setCachedVoteMessageById(entryId, updated);
if (typeof view.focus === "function") {
view.focus();
}
return;
}
const current = this.voteMessages[entryId] || "";
const textArea = this.getVoteTextarea(entryId);
if (!textArea) {
this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
[entryId]: `${current}${templateText}`
});
setCachedVoteMessageById(entryId, `${current}${templateText}`);
return;
}
const start = (_f = textArea.selectionStart) != null ? _f : current.length;
const end = (_g = textArea.selectionEnd) != null ? _g : start;
this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
[entryId]: `${current.slice(0, start)}${templateText}${current.slice(end)}`
});
setCachedVoteMessageById(entryId, `${current.slice(0, start)}${templateText}${current.slice(end)}`);
setTimeout(() => {
const focusedTextArea = this.getVoteTextarea(entryId);
if (!focusedTextArea) return;
const nextCursor = start + templateText.length;
focusedTextArea.focus();
focusedTextArea.setSelectionRange(nextCursor, nextCursor);
}, 0);
},
onPrimaryAction() {
if (this.currentStep === 0 && !this.validateStep0()) {
return;
}
if (this.currentStep === 1 && !this.validateStep1()) {
return;
}
if (this.currentStep < this.totalSteps - 1) {
this.currentStep++;
return;
}
void this.submitVote();
},
onDefaultAction() {
if (this.currentStep > 0) {
this.currentStep--;
return;
}
this.closeDialog();
},
onUpdateOpen(newValue) {
if (!newValue) {
this.closeDialog();
}
},
closeDialog() {
this.destroyAllCodeMirror();
this.open = false;
setTimeout(() => {
removeDialogMount();
}, 300);
},
async submitVote() {
this.isSubmitting = true;
this.syncVoteMessagesFromTextareas();
try {
const indent = getVoteIndent(this.useBulleted);
const builtVoteTexts = this.selectedEntries.reduce((acc, id) => {
acc[id] = buildFinalVoteWikitext(this.voteMessages[id] || "", indent);
return acc;
}, {});
const hasConflict = await vote(this.selectedEntries, builtVoteTexts, this.voteMessages);
if (hasConflict) {
this.isSubmitting = false;
return;
}
mw.notify(state_default.convByVar({ hant: "投票已成功提交。", hans: "投票已成功提交。" }), { tag: "voter" });
this.isSubmitting = false;
this.open = false;
setTimeout(() => {
removeDialogMount();
}, 300);
} catch (error) {
console.error("[Voter] submitVote failed:", error);
const msg = state_default.convByVar({ hant: "投票提交失敗,請稍後再試。", hans: "投票提交失败,请稍后再试。" });
mw.notify(msg, { type: "error", title: "[Voter]" });
this.isSubmitting = false;
}
}
},
template: `
<cdx-dialog
v-model:open="open"
:title="$options.i18n.dialogTitle"
:use-close-button="true"
:primary-action="primaryAction"
:default-action="defaultAction"
@primary="onPrimaryAction"
@default="onDefaultAction"
@update:open="onUpdateOpen"
class="voter-dialog voter-multistep-dialog"
>
<template #header>
<div class="voter-multistep-dialog__header-top">
<h2>{{ $options.i18n.dialogTitle }}</h2>
</div>
<div class="voter-multistep-dialog__stepper">
<div class="voter-multistep-dialog__stepper__label">{{ (currentStep + 1) + ' / ' + totalSteps }}</div>
<div class="voter-multistep-dialog__stepper__steps" aria-hidden="true">
<span
v-for="step of [0, 1, 2]"
:key="step"
class="voter-multistep-dialog__stepper__step"
:class="getStepClass(step)"
></span>
</div>
</div>
</template>
<div v-if="currentStep === 0" class="voter-form-section">
<h3>{{ $options.i18n.selectEntriesHeading }}</h3>
<div class="voter-template-hint">{{ $options.i18n.selectEntriesHint }}</div>
<div class="voter-checkbox-grid">
<cdx-checkbox
v-for="option in entryOptions"
:key="option.value"
v-model="selectedEntries"
:input-value="option.value"
>
{{ option.label }}
</cdx-checkbox>
</div>
<div
v-if="entryInfoHtml"
class="voter-entry-info"
v-html="entryInfoHtml"
></div>
<div v-else-if="isLoadingInfo" class="voter-entry-info voter-entry-info--loading">
載入中...
</div>
</div>
<div v-else-if="currentStep === 1" class="voter-form-section">
<h3>{{ $options.i18n.insertTemplateHeading }}</h3>
<div class="voter-template-hint">{{ $options.i18n.insertTemplateHint }}</div>
<div
v-for="item in selectedEntryItems"
:key="item.id"
class="voter-entry-vote"
:data-entry-id="item.id"
>
<div class="voter-entry-vote__title">{{ item.name }}</div>
<div class="voter-template-buttons">
<cdx-button
v-for="option in allTemplateOptions"
:key="option.value"
@click="insertTemplate(option.value, item.id)"
>
{{ option.label }}
</cdx-button>
</div>
<cdx-text-area
v-model="voteMessages[item.id]"
:placeholder="$options.i18n.voteReasonPlaceholder"
rows="3"
></cdx-text-area>
<div
v-if="entryInfoById[item.id]"
class="voter-entry-info voter-entry-info--inline"
v-html="entryInfoById[item.id]"
></div>
</div>
<div class="voter-form-section" style="padding-bottom: 0;">
<cdx-checkbox v-model="useBulleted">
{{ $options.i18n.useBulleted }}
</cdx-checkbox>
</div>
</div>
<div v-else-if="currentStep === 2" class="voter-preview-section">
<h3>{{ $options.i18n.previewHeading }}</h3>
<div class="voter-template-hint">{{ $options.i18n.previewInfo }}</div>
<div
v-for="item in previewVoteItems"
:key="item.id"
class="voter-preview-item"
>
<strong>{{ item.name }}</strong>
<pre class="voter-preview-code">{{ item.text }}</pre>
</div>
</div>
</cdx-dialog>
`
});
registerCodexComponents(app, Codex);
mountApp(app);
}).catch((error) => {
console.error("[Voter] 無法加載 Codex:", error);
mw.notify(state_default.convByVar({ hant: "無法加載對話框組件。", hans: "无法加载对话框组件。" }), {
type: "error",
title: "[Voter]"
});
});
}
function openVoteDialog(sectionID) {
const mountedApp2 = getMountedApp();
if (mountedApp2) removeDialogMount();
createVoteDialog(sectionID);
}
window.openVoteDialog = openVoteDialog;
// src/dom.ts
function addVoteButtons() {
var _a;
if (document.querySelector("#voter-finished-loading")) {
return;
}
state_default.sectionTitles = [];
let headingSelector;
if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
headingSelector = "div.mw-heading.mw-heading4";
} else {
headingSelector = "div.mw-heading.mw-heading2";
}
$(headingSelector).each((index, element) => {
let $element = $(element);
let anchor;
if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
anchor = $element.nextUntil(headingSelector, "ul").find("li .anchor").attr("id");
} else {
anchor = $element.find("h2").attr("id");
}
if (anchor) {
let sectionID = getSectionID(index + 1);
const $voteLink = $("<a>").text(state_default.convByVar({ hant: "投票", hans: "投票" })).css({ "cursor": "pointer", "margin-left": "0.25em" });
$voteLink.on("click", (e) => {
e.preventDefault();
openVoteDialog(sectionID);
});
$('<span class="mw-editsection-bracket">|</span> ').insertAfter($element.find("span.mw-editsection > a").first());
$voteLink.insertAfter($element.find("span.mw-editsection > a").first().next());
state_default.sectionTitles.push({ data: sectionID, label: anchor.replace(/_/g, " ") });
}
});
console.log(`[Voter] 已識別可投票事項共 ${state_default.sectionTitles.length} 項。`);
let finishedLoading = document.createElement("div");
finishedLoading.id = "voter-finished-loading";
finishedLoading.style.display = "none";
(_a = document.querySelector("#mw-content-text .mw-parser-output")) == null ? void 0 : _a.appendChild(finishedLoading);
}
function getSectionID(childid) {
try {
let $heading;
if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
$heading = $("div.mw-heading.mw-heading4").eq(childid - 1);
} else {
$heading = $("div.mw-heading.mw-heading2").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;
}
function titleVariants(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)];
}
function textMatchTitleVariants(text, title) {
return titleVariants(title).some((variant) => text.includes(variant));
}
function refreshPage(entryName) {
location.href = mw.util.getUrl(state_default.pageName + "#" + entryName);
location.reload();
}
async function vote(voteIDs, builtVoteTexts, rawVoteTexts) {
var _a;
for (const id of voteIDs) {
const rawVoteText = (rawVoteTexts[id] || "").trim();
const summaryVoteText = rawVoteText.length > 100 ? `${rawVoteText.slice(0, 100)}...` : rawVoteText;
let votedPageName = ((_a = state_default.sectionTitles.find((x) => x.data === id)) == null ? void 0 : _a.label) || `section ${id}`;
let destPage = state_default.pageName;
if (state_default.pageName === "Wikipedia:優良條目評選") {
destPage += "/提名區";
} else if (/^Wikipedia:(典范条目评选|特色列表评选)$/i.test(state_default.pageName)) {
destPage += "/提名区";
}
const text = builtVoteTexts[id] || "";
let summary = `/* ${votedPageName} */ `;
summary += summaryVoteText || state_default.convByVar({ hant: "投票", hans: "投票" });
summary += " ([[User:SuperGrey/gadgets/voter|Voter]])";
if (await voteAPI(state_default.pageName, destPage, id, text, summary)) return true;
}
setTimeout(() => {
var _a2;
return refreshPage((_a2 = state_default.sectionTitles.find((x) => x.data === voteIDs[0])) == null ? void 0 : _a2.label);
}, 1e3);
return false;
}
// src/styles.css
var styles_default = "/* Voter Gadget Styles */\r\n\r\n/* Multi-step Dialog */\r\n.voter-dialog {\r\n max-width: 600px;\r\n}\r\n\r\n.voter-multistep-dialog__header-top {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 16px 24px 0;\r\n}\r\n\r\n.voter-multistep-dialog__header-top h2 {\r\n margin: 0;\r\n font-size: 18px;\r\n font-weight: 600;\r\n}\r\n\r\n.voter-multistep-dialog__stepper {\r\n display: flex;\r\n align-items: center;\r\n gap: 12px;\r\n padding: 12px 24px;\r\n border-bottom: 1px solid rgba(0, 0, 0, 0.1);\r\n}\r\n\r\n.voter-multistep-dialog__stepper__label {\r\n font-size: 13px;\r\n color: #54595d;\r\n min-width: 36px;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__steps {\r\n display: flex;\r\n gap: 6px;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__step {\r\n display: block;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 999px;\r\n background-color: #c8ccd1;\r\n transition: background-color 0.2s ease;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__step--active {\r\n background-color: #36c;\r\n}\r\n\r\n.cdx-dialog__body h3 {\r\n margin: 0;\r\n font-size: 16px;\r\n font-weight: 600;\r\n}\r\n\r\n/* Form Sections */\r\n.voter-form-section {\r\n padding: 16px 0;\r\n}\r\n\r\n.voter-form-label {\r\n display: block;\r\n font-weight: 600;\r\n margin-bottom: 8px;\r\n font-size: 14px;\r\n}\r\n\r\n.voter-checkbox-grid {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\r\n gap: 0 8px;\r\n}\r\n\r\n.voter-template-hint {\r\n margin-bottom: 8px;\r\n font-size: 13px;\r\n color: #54595d;\r\n}\r\n\r\n.voter-template-buttons {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 5px;\r\n margin-bottom: 5px;\r\n}\r\n\r\n.voter-template-buttons .cdx-button {\r\n font-size: 13px;\r\n padding: 2px 5px;\r\n min-height: inherit;\r\n}\r\n\r\n.voter-entry-vote {\r\n /* border: 1px solid #eaecf0;\r\n border-radius: 4px;\r\n padding: 12px; */\r\n margin-top: 12px;\r\n}\r\n\r\n.voter-entry-vote__title {\r\n font-size: 14px;\r\n font-weight: 600;\r\n margin-bottom: 8px;\r\n}\r\n\r\n/* Entry Info */\r\n.voter-entry-info {\r\n margin-top: 12px;\r\n padding: 12px;\r\n background-color: #f8f9fa;\r\n border: 1px solid #eaecf0;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n line-height: 1.5;\r\n}\r\n\r\n.voter-entry-info--loading {\r\n color: #72777d;\r\n font-style: italic;\r\n}\r\n\r\n.voter-entry-info--inline {\r\n margin-top: 8px;\r\n margin-bottom: 0;\r\n}\r\n\r\n/* Preview Section */\r\n.voter-preview-item {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.voter-preview-item strong {\r\n display: block;\r\n margin-bottom: 8px;\r\n font-size: 14px;\r\n}\r\n\r\n.voter-preview-item ul {\r\n margin: 0;\r\n padding-left: 24px;\r\n}\r\n\r\n.voter-preview-item li {\r\n font-size: 14px;\r\n line-height: 1.6;\r\n}\r\n\r\n.voter-preview-code {\r\n padding: 12px;\r\n background-color: #f8f9fa;\r\n border: 1px solid #eaecf0;\r\n border-radius: 4px;\r\n font-family: monospace;\r\n font-size: 13px;\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n margin: 0;\r\n}\r\n\r\n/* Dialog Footer - override Codex defaults */\r\n.voter-dialog footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 12px;\r\n border-top: 1px solid rgba(0, 0, 0, 0.06);\r\n padding: 12px 24px;\r\n}\r\n";
// src/main.ts
function injectStyles(css) {
if (!css) return;
try {
const styleEl = document.createElement("style");
styleEl.appendChild(document.createTextNode(css));
document.head.appendChild(styleEl);
} catch (e) {
const div = document.createElement("div");
div.innerHTML = `<style>${css}</style>`;
const styleEl = div.firstElementChild;
if (styleEl) {
document.head.appendChild(styleEl);
}
}
}
function validatePage(pageName) {
const 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 (const page of validPages) {
if (pageName === page.name || new RegExp(`^${page.name}/`, "i").test(pageName)) {
state_default.validVoteTemplates = page.templates;
state_default.invalidVoteTemplates = [
"中立",
"意見",
"建議",
"疑問",
"同上",
"提醒"
].map((template) => ({
data: template,
label: template
}));
return true;
}
}
return false;
}
async function init() {
if (typeof document !== "undefined") {
injectStyles(styles_default);
}
if (!validatePage(state_default.pageName)) {
console.log("[Voter] 不是目標頁面,小工具終止。");
return;
}
await state_default.initHanAssist();
console.log(`[Voter] 已載入,當前頁面為 ${state_default.pageName}。`);
mw.hook("wikipage.content").add(() => {
setTimeout(() => addVoteButtons(), 200);
});
}
void init();
})();
// </nowiki>