User:SuperGrey/gadgets/VGTNTool/main.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/VGTNTool]]
// <nowiki>
(() => {
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/api.ts
var _api = null;
function getApi() {
if (!_api) {
_api = new mw.Api({ "User-Agent": "VGTNTool/1.0.2" });
}
return _api;
}
async function fetchModuleData() {
const res = await getApi().get({
action: "expandtemplates",
text: "{{#invoke:Vgtn/data|toJSON}}",
prop: "wikitext",
format: "json"
});
try {
return JSON.parse(res.expandtemplates.wikitext);
} catch (e) {
console.error("[VGTNTool] 解析 JSON 時出錯:", e);
alert("無法載入資料,請稍後再試。");
console.log("[VGTNTool]", res.expandtemplates.wikitext);
throw e;
}
}
async function fetchModuleText() {
const res = await getApi().get({
action: "query",
titles: "Module:Vgtn/data",
curtimestamp: 1,
prop: "revisions",
indexpageids: 1,
rvprop: ["timestamp", "content"],
rvslots: "main"
});
const rev = res.query.pages[res.query.pageids[0]].revisions[0];
return {
start: rev.curtimestamp,
base: rev.timestamp,
fulltext: rev.slots.main["*"]
};
}
async function fetchDiffHtml(oldText, newText) {
const res = await getApi().postWithToken("csrf", {
action: "compare",
fromslots: "main",
"fromtext-main": oldText,
fromtitle: "Module:Vgtn/data",
frompst: "true",
toslots: "main",
"totext-main": newText,
totitle: "Module:Vgtn/data",
topst: "true"
});
return res.compare["*"] ? $("<table>").addClass("diff").append($("<colgroup>").append($("<col>").addClass("diff-marker"), $("<col>").addClass("diff-content"), $("<col>").addClass("diff-marker"), $("<col>").addClass("diff-content"))).append(res.compare["*"]) : null;
}
async function saveModuleText(diffText, startTimestamp, baseTimestamp, summary) {
const trimmedSummary = (summary || "").trim();
const editSummary = trimmedSummary ? `[[User:SuperGrey/gadgets/VGTNTool|編輯資料]]:${trimmedSummary}` : "[[User:SuperGrey/gadgets/VGTNTool|編輯資料]]";
const res = await getApi().postWithToken("csrf", {
action: "edit",
title: "Module:Vgtn/data",
text: diffText,
summary: editSummary,
starttimestamp: startTimestamp,
basetimestamp: baseTimestamp
});
if (res.edit && res.edit.result === "Success") {
console.log("[VGTNTool] 資料已成功儲存。");
mw.notify("資料已成功儲存。", { type: "success", autoHide: true, autoHideSeconds: 3 });
refreshPage();
return false;
} else if (res.error && res.error.code === "editconflict") {
console.error("[VGTNTool] 儲存資料時發生編輯衝突:", res);
mw.notify("儲存資料時發生編輯衝突。請稍後再試。", { type: "error", autoHide: true, autoHideSeconds: 3 });
return true;
} else {
console.error("[VGTNTool] 儲存資料時出錯:", res, "擬儲存的資料:", diffText);
mw.notify("儲存資料時出錯。請稍後再試。", { type: "error", autoHide: true, autoHideSeconds: 3 });
return true;
}
}
function refreshPage() {
setTimeout(function() {
window.location.reload();
}, 2e3);
}
// src/state.ts
var State = class {
constructor() {
/**
* 使用者名稱,從MediaWiki配置中獲取。
* @type {string}
* @constant
*/
__publicField(this, "userName", mw.config.get("wgUserName"));
/**
* 頁面名稱,從MediaWiki配置中獲取。
* @type {string}
* @constant
*/
__publicField(this, "pageName", mw.config.get("wgPageName"));
// 簡繁轉換
__publicField(this, "convByVar", function(langDict) {
if (langDict && langDict.hant) {
return langDict.hant;
}
return "繁簡轉換未初始化,且 langDict 無效!";
});
}
async initHanAssist() {
let require2 = await mw.loader.using("ext.gadget.HanAssist");
const { convByVar } = require2("ext.gadget.HanAssist");
if (typeof convByVar === "function") {
this.convByVar = convByVar;
}
}
};
var state = new State();
var state_default = state;
// src/dialog.ts
var _mountedApp = null;
function loadCodexAndVue() {
return mw.loader.using("@wikimedia/codex").then((require2) => ({
Vue: require2("vue"),
Codex: require2("@wikimedia/codex")
}));
}
function createDialogMountIfNeeded() {
if (!document.getElementById("review-tool-dialog-mount")) {
const mountPoint = document.createElement("div");
mountPoint.id = "review-tool-dialog-mount";
document.body.appendChild(mountPoint);
}
return document.getElementById("review-tool-dialog-mount");
}
function mountApp(app) {
createDialogMountIfNeeded();
_mountedApp = app.mount("#review-tool-dialog-mount");
return _mountedApp;
}
function getMountedApp() {
return _mountedApp;
}
function removeDialogMount() {
const mountPoint = document.getElementById("review-tool-dialog-mount");
if (mountPoint) mountPoint.remove();
_mountedApp = null;
}
function registerCodexComponents(app, Codex) {
if (!app || !Codex) return;
try {
app.component("cdx-dialog", Codex.CdxDialog).component("cdx-text-input", Codex.CdxTextInput).component("cdx-text-area", Codex.CdxTextArea).component("cdx-checkbox", Codex.CdxCheckbox).component("cdx-select", Codex.CdxSelect).component("cdx-button", Codex.CdxButton).component("cdx-button-group", Codex.CdxButtonGroup);
} catch (e) {
}
}
// src/diff_dialog.ts
async function showDiffDialog(diffHtml, diffText, startTimestamp, baseTimestamp) {
if (getMountedApp()) removeDialogMount();
try {
const { Vue, Codex } = await loadCodexAndVue();
return await new Promise((resolve) => {
let resolved = false;
const finalize = (result_4) => {
if (resolved) return;
resolved = true;
resolve(result_4);
};
const htmlString = diffHtml ? diffHtml.prop("outerHTML") || diffHtml.html() || "" : "";
const app = Vue.createMwApp({
i18n: {
title: state_default.convByVar({ hant: "檢視差異", hans: "查看差异" }),
save: state_default.convByVar({ hant: "儲存", hans: "保存" }),
cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
noDiff: state_default.convByVar({ hant: "無差異。", hans: "无差异。" }),
summaryPlaceholder: state_default.convByVar({ hant: "編輯摘要(可留空)", hans: "编辑摘要(可留空)" })
},
data() {
return {
open: true,
diffHtml: htmlString || "",
diffText: diffText || "",
startTimestamp: startTimestamp || "",
baseTimestamp: baseTimestamp || "",
editSummary: "",
saving: false
};
},
computed: {
hasDiff() {
return Boolean(this.diffHtml && this.diffHtml !== "");
},
primaryAction() {
return {
label: this.$options.i18n.save,
actionType: "progressive",
disabled: !this.hasDiff || this.saving
};
},
defaultAction() {
return {
label: this.$options.i18n.cancel
};
}
},
methods: {
async onPrimaryAction() {
if (this.saving) return;
this.saving = true;
try {
const res = await saveModuleText(this.diffText, this.startTimestamp, this.baseTimestamp, this.editSummary);
this.saving = false;
if (res === false) {
finalize({ action: "save" });
this.closeDialog();
} else {
mw && mw.notify && mw.notify(state_default.convByVar({ hant: "儲存時發生錯誤,請稍後再試。", hans: "保存时发生错误,请稍后再试。" }), { type: "error" });
}
} catch (err) {
this.saving = false;
console.error("[VGTNTool] saveModuleText failed", err);
mw && mw.notify && mw.notify(state_default.convByVar({ hant: "儲存時發生錯誤,請稍後再試。", hans: "保存时发生错误,请稍后再试。" }), { type: "error" });
}
},
onCancelAction() {
finalize({ action: "cancel" });
this.closeDialog();
},
onUpdateOpen(newValue) {
if (!newValue) this.onCancelAction();
},
closeDialog() {
this.open = false;
setTimeout(() => removeDialogMount(), 200);
}
},
template: `
<cdx-dialog
v-model:open="open"
:title="$options.i18n.title"
:use-close-button="true"
@update:open="onUpdateOpen"
:primary-action="primaryAction"
:default-action="defaultAction"
@primary="onPrimaryAction"
@default="onCancelAction"
class="vgtn-diff-dialog review-tool-dialog"
>
<div class="review-tool-form-section vgtn-diff-dialog__content">
<div v-if="!diffHtml || diffHtml === ''" class="vgtn-diff-dialog__nodiff">
{{ $options.i18n.noDiff }}
</div>
<div v-else class="vgtn-diff-dialog__diff" v-html="diffHtml"></div>
<cdx-text-input
v-model="editSummary"
:placeholder="$options.i18n.summaryPlaceholder"
class="vgtn-diff-dialog__summary-input"
/>
</div>
</cdx-dialog>
`
});
registerCodexComponents(app, Codex);
mountApp(app);
});
} catch (error) {
console.error("[VGTNTool] Failed to open diff dialog", error);
mw && mw.notify && mw.notify(state_default.convByVar({
hant: "無法開啟差異對話框。",
hans: "无法开启差异对话框。"
}), { type: "error", title: "[VGTNTool]" });
throw error;
}
}
// src/dom.ts
var editorContainerId = "vgtn-editor";
var addedSections = [];
var $tableTemplate = null;
function buildTable() {
if (!$tableTemplate) {
$tableTemplate = $("<table>").addClass("wikitable vgtn-editable-table tablesorter").append($("<thead>").append($("<tr>").append($("<th>").attr("scope", "col").text("原名")).append($("<th>").attr("scope", "col").data("sorter", false).text("相關連結")).append($("<th>").attr("scope", "col").data("sorter", false).text("中文名")).append($("<th>").attr("scope", "col").data("sorter", false).text("備註")))).append($("<tbody>"));
}
const $table = $tableTemplate.clone(true);
setTableFooter($table);
return $table;
}
function setTableRow($tbody, record = {}) {
const $row = $("<tr>").addClass("vgtn-editable-row");
const $th = $("<th>").attr("scope", "row").css({
"font-weight": "normal",
"text-align": "left"
}).append(EntryNameField(record));
$row.append($th);
$row.append($("<td>").append(LinkVariableField(record)));
$row.append($("<td>").append(LocaleVariableField(record)));
$row.append($("<td>").append(CommentField(record)));
$tbody.append($row);
return $row;
}
function setTableFooter($table) {
const $tfoot = $("<tfoot>");
const $tfootRow = $("<tr>");
const $tfootCell = $("<td>").attr("colspan", 4).css({ "text-align": "center" }).addClass("vgtn-table-footer");
const $addRowButton = $("<a>").addClass("vgtn-add-row-btn").attr("href", "javascript:void(0)").text("新增行").on("click", (e) => {
e.preventDefault();
const $newRow = setTableRow($table.find("tbody"), {});
$table.trigger("addRows", [$newRow]);
});
$tfootCell.append($addRowButton);
$tfootRow.append($tfootCell);
$tfoot.append($tfootRow);
$table.append($tfoot);
}
function addSection($mwHeading2) {
const $table = buildTable();
setTableRow($table.find("tbody"));
const $newHeadingSpan = $("<span>").addClass("vgtn-editable-span vgtn-new-section-name").attr("contenteditable", "plaintext-only").attr("placeholder", "(新章節)");
const $h3 = $("<h3>").addClass("vgtn-new-section").append($newHeadingSpan);
const $mwHeading3 = $("<div>").addClass("mw-heading mw-heading3").append($h3);
setHeading3EditingButtons($mwHeading3, null, $table);
$mwHeading2.after($mwHeading3);
$mwHeading3.after($table);
addedSections.push({ $h2: $mwHeading2.find("h2"), $h3 });
setEditable($table);
}
function removeSection($mwHeading3) {
const $h3 = $mwHeading3.find("h3");
if ($h3.hasClass("vgtn-section-removed")) {
$h3.removeClass("vgtn-section-removed");
const $removeSectionButton = $mwHeading3.find(".mw-editsection .vgtn-remove-section-btn");
$removeSectionButton.text("刪除章節");
} else {
$h3.addClass("vgtn-section-removed");
const $removeSectionButton = $mwHeading3.find(".mw-editsection .vgtn-remove-section-btn");
$removeSectionButton.text("取消刪除章節");
}
}
async function editSection($mwHeading3) {
const sectionTitle = $mwHeading3.find("h3").attr("id");
const allData = await fetchModuleData();
const entries = {};
for (const mainIdx in allData) {
const group = allData[mainIdx];
if (typeof group !== "object" || !group.name) continue;
for (const key in group) {
if (key === "name") continue;
const sub = group[key];
const arr = [];
for (const idx in sub) {
if (idx === "name") continue;
arr.push(sub[idx]);
}
entries[sub.name.replace(/\s/g, "_")] = arr;
}
}
if (!entries[sectionTitle]) {
mw.notify(`無法找到段落「${sectionTitle}」。請確認段落名稱是否正確。`, {
type: "error",
autoHide: true,
autoHideSeconds: 3
});
return;
}
const $ogTable = $mwHeading3.next("table.wikitable");
const $table = buildTable();
$table.data("vgtn-section", sectionTitle);
$ogTable.hide();
$mwHeading3.after($table);
const $tbody = $table.find("tbody");
for (let i = 0; i < entries[sectionTitle].length; i++) {
let record = entries[sectionTitle][i];
if (Array.isArray(record)) record = { "1": record[0] };
setTableRow($tbody, record);
}
setHeading3EditingButtons($mwHeading3, $ogTable, $table);
setEditable($table);
}
function setEditable($table) {
$("[contenteditable]").on("paste", function(e) {
const $self = $(this);
setTimeout(function() {
$self.html($self.text());
}, 0);
}).on("keypress", function(e) {
return e.which !== 13;
});
$table.tablesorter({
sortReset: true,
sortStable: true,
emptyTo: "bottom",
textSorter: { 0: $.tablesorter.sortText },
textExtraction: {
0: (node, table, cellIndex) => {
return $(node).find(".vgtn-record-name").text().trim().replace(/[\s_]/g, "").toLowerCase();
}
}
});
}
async function saveChanges() {
const { start, base, fulltext: oldText } = await fetchModuleText();
const $editableTables = $(".vgtn-editable-table");
if ($editableTables.length === 0) {
console.error("[VGTNTool] 沒有找到編輯中的表格。");
mw.notify("沒有找到編輯中的表格。", { type: "error", autoHide: true, autoHideSeconds: 3 });
return;
}
let newText = oldText;
if (addedSections.length > 0) {
addedSections.forEach((addedSection) => {
const { $h2, $h3 } = addedSection;
const h2Title = $h2.attr("id");
const h3Title = $h3.find(".vgtn-new-section-name").text().trim();
if (!h3Title) {
return;
}
const escapedSectionName = h3Title.replace(/"/g, '\\"');
const escapedParentName = mw.util.escapeRegExp(h2Title).replace(/_/g, "[ _]");
const parentRegex = new RegExp(`^(data\\[\\d+\\]=\\{\\s*\\-*\\s*name=['"]${escapedParentName}['"],\\s*\\-*\\r?\\n[\\s\\S]*?)(^\\})$`, "gm");
if (parentRegex.test(newText)) {
const newSectionText = ` {
name="${escapedSectionName}",
--------------------------------------------------------------------------
},
`;
newText = newText.replace(parentRegex, `$1${newSectionText}$2`);
console.log(`[VGTNTool] 新增段落「${h3Title}」到「${h2Title}」。`);
} else {
console.error(`[VGTNTool] 無法找到父段落「${h2Title}」。無法新增段落「${h3Title}」。`);
mw.notify(`無法找到父段落「${h2Title}」。無法新增段落「${h3Title}」。`, {
type: "error",
autoHide: true,
autoHideSeconds: 3
});
}
});
}
const $deletedSections = $(".vgtn-section-removed");
if ($deletedSections.length > 0) {
$deletedSections.each(function() {
const $h3 = $(this);
let sectionName;
if ($h3.hasClass("vgtn-new-section")) {
sectionName = $h3.find(".vgtn-new-section-name").text().trim();
} else {
sectionName = $h3.attr("id");
}
const escapedSectionName = mw.util.escapeRegExp(sectionName).replace(/_/g, "[ _]");
const sectionRegex = new RegExp(`[\\r\\n]*^[ \\t]*\\{\\s*name=['"]${escapedSectionName}['"],\\s*\\-*\\r?\\n[\\s\\S]*?^[ \\t]*\\},$`, "gm");
if (sectionRegex.test(newText)) {
newText = newText.replace(sectionRegex, "");
console.log(`[VGTNTool] 刪除段落「${sectionName}」。`);
} else {
console.error(`[VGTNTool] 無法找到段落「${sectionName}」。無法刪除。`);
}
});
}
$editableTables.each(function() {
const $table = $(this);
const $rows = $table.find("tbody tr");
let sectionName = "";
if ($table.data("vgtn-section")) {
sectionName = $table.data("vgtn-section");
} else {
const $mwHeading3 = $table.prev(".mw-heading3");
const $h3 = $mwHeading3.find("h3");
if ($h3.hasClass("vgtn-section-removed")) {
return;
}
sectionName = $h3.find(".vgtn-new-section-name").text().trim();
if (!sectionName) {
console.error("[VGTNTool] 段落名稱為空,無法儲存。請確認段落名稱是否已填寫。");
mw.notify("新章節名稱為空,無法儲存。請確認新章節名稱是否已填寫。", {
type: "error",
autoHide: true,
autoHideSeconds: 3
});
return;
}
}
let sectionNewRows = "";
$rows.each(function() {
const $row = $(this);
if ($row.hasClass("vgtn-row-removed")) {
return;
}
const name = $row.find(".vgtn-record-name").text().trim();
if (!name) {
console.warn("[VGTNTool] 發現空名稱的行,將跳過此行。");
return;
}
const lang = $row.find(".vgtn-record-lang").text().trim();
const dab = $row.find(".vgtn-record-dab").text().trim();
const hanja = $row.find(".vgtn-record-hanja").is(":checked");
const rm = $row.find(".vgtn-record-rm").text().trim();
const aliases = $row.find(".vgtn-record-aliases").text().trim().split("|").map((a) => a.trim()).filter((a) => a);
const link = $row.find(".vgtn-record-link").text().trim();
const iw = $row.find(".vgtn-record-iw").text().trim();
const wd = $row.find(".vgtn-record-wd").text().trim();
const namu = $row.find(".vgtn-record-namu").text().trim();
const tw = $row.find(".vgtn-record-tw").text().trim();
const hk = $row.find(".vgtn-record-hk").text().trim();
const mo = $row.find(".vgtn-record-mo").text().trim();
const cn = $row.find(".vgtn-record-cn").text().trim();
const sg = $row.find(".vgtn-record-sg").text().trim();
const my = $row.find(".vgtn-record-my").text().trim();
const comment = $row.find(".vgtn-record-comment").text().trim();
sectionNewRows += ` { "${name.replace(/"/g, '\\"')}"`;
if (lang) sectionNewRows += `, lang="${lang}"`;
if (dab) sectionNewRows += `, dab="${dab}"`;
if (rm) sectionNewRows += `, rm="${rm}"`;
if (cn) sectionNewRows += `, cn="${cn}"`;
if (hk) sectionNewRows += `, hk="${hk}"`;
if (tw) sectionNewRows += `, tw="${tw}"`;
if (sg) sectionNewRows += `, sg="${sg}"`;
if (mo) sectionNewRows += `, mo="${mo}"`;
if (my) sectionNewRows += `, my="${my}"`;
if (hanja) sectionNewRows += `, hanja=true`;
if (link) sectionNewRows += `, link="${link}"`;
if (iw) sectionNewRows += `, iw="${iw}"`;
if (wd) sectionNewRows += `, wd="${wd}"`;
if (namu) sectionNewRows += `, namu="${namu}"`;
if (aliases.length > 0) sectionNewRows += `, aliases={ "${aliases.join('", "')}" }`;
if (comment) sectionNewRows += `, comment="${comment}"`;
sectionNewRows += " },\n";
});
const escapedSectionName = mw.util.escapeRegExp(sectionName).replace(/_/g, "[ _]");
console.log(`[VGTNTool] escapedSectionName: ${escapedSectionName}`);
const sectionRegex = new RegExp(`^([ \\t]*\\{\\s*name=['"]${escapedSectionName}['"],\\s*\\-*\\r?\\n)([\\s\\S]*?)(^[ \\t]*\\},)$`, "gm");
if (sectionRegex.test(newText)) {
newText = newText.replace(sectionRegex, `$1${sectionNewRows}$3`);
} else {
console.error(`[VGTNTool] 無法找到標題為「${sectionName}」的段落,可能被刪除了。sectionRegex:`, sectionRegex, `newText:`, newText);
}
});
const $mwHeading2s = $(".mw-heading2");
$mwHeading2s.each(function() {
const $mwHeading2 = $(this);
if ($mwHeading2.hasClass("vgtn-sort-asc") || $mwHeading2.hasClass("vgtn-sort-desc")) {
const sortOrder = $mwHeading2.hasClass("vgtn-sort-asc") ? "asc" : "desc";
const parentName = $mwHeading2.find("h2").attr("id");
const escapedParentName = mw.util.escapeRegExp(parentName).replace(/_/g, "[\\s_]");
const parentRegex = new RegExp(`^(data\\[\\d+\\]=\\{\\s*\\-*\\s*name=['"]${escapedParentName}['"],\\s*\\-*\\r?\\n)([\\s\\S]*?)(^\\})$`, "gm");
if (parentRegex.test(newText)) {
const parentContent = newText.match(parentRegex)[0];
const sectionRegex = new RegExp(`^[ \\t]*\\{\\s*name=['"]([^'"]+)['"],\\s*\\-*\\r?\\n[\\s\\S]*?^[ \\t]*\\},$`, "gm");
const matches = Array.from(parentContent.matchAll(sectionRegex));
matches.sort((a, b) => {
const nameA = a[1].toLowerCase().replace(/[\s_]/g, "");
const nameB = b[1].toLowerCase().replace(/[\s_]/g, "");
return sortOrder === "asc" ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
});
let sortedContent = "";
matches.forEach((match) => {
sortedContent += match[0] + "\n\n";
});
newText = newText.replace(parentRegex, `$1
${sortedContent}$3`);
console.log(`[VGTNTool] 已對段落「${parentName}」進行排序。排序方式:${sortOrder === "asc" ? "升序" : "降序"}。`);
} else {
console.error(`[VGTNTool] 無法找到段落「${parentName}」。無法進行排序。`);
}
}
});
const diffHtml = await fetchDiffHtml(oldText, newText);
showDiffDialog(diffHtml, newText, start, base);
}
function cancelEdit($ogTable, $table, $mwHeading3) {
$table.remove();
$ogTable.show();
setHeading3Buttons($mwHeading3);
}
function EntryNameField(record) {
var _a;
const $editableField = $("<div>").addClass("vgtn-editable-field");
const $nameSpan = $("<span>").data("vgtn-record", "name").addClass("vgtn-editable-span vgtn-record-name").css({ "white-space": "nowrap" }).attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record["1"] || "");
$nameSpan.on("blur", function() {
const $th = $(this).closest("th");
$th.closest("table").trigger("updateCell", [$th]);
});
const $langCode = $("<code>").data("vgtn-record", "lang").addClass("vgtn-editable-code vgtn-record-lang").attr("contenteditable", "plaintext-only").attr("placeholder", "(lang)").text(record.lang || "");
$langCode.on("input", function() {
const lang = $(this).text().trim();
const $hanjaBundle = $editableField.find(".vgtn-editable-hanja-bundle");
const $rmSpanBundle2 = $editableField.find(".vgtn-editable-rm-bundle");
const $namuSpanBundle = $editableField.closest("tr").find(".vgtn-editable-namu-bundle");
if (lang === "ko") {
$hanjaBundle.show();
$namuSpanBundle.show();
} else {
$hanjaBundle.hide();
$namuSpanBundle.hide();
}
if (["ja", "ko", "ru"].includes(lang)) $rmSpanBundle2.show();
else $rmSpanBundle2.hide();
});
const $dabCode = $("<code>").data("vgtn-record", "dab").addClass("vgtn-editable-code vgtn-record-dab").attr("contenteditable", "plaintext-only").attr("placeholder", "(dab)").text(record.dab || "");
const $removeRowButton = $("<a>").addClass("vgtn-remove-row-btn").attr("href", "javascript:void(0)").text("刪除行").on("click", (e) => {
e.preventDefault();
const $row = $editableField.closest("tr");
if ($row.hasClass("vgtn-row-removed")) {
$row.removeClass("vgtn-row-removed");
$removeRowButton.text("刪除行");
} else {
$row.addClass("vgtn-row-removed");
$removeRowButton.text("取消刪除行");
}
});
const $removeRowButtonBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-remove-row-bundle").append($("<span>").text("[").css({ "padding-left": "0.2em" })).append($removeRowButton).append($("<span>").text("]"));
$editableField.append($nameSpan).append(" ").append($langCode).append(" ").append($dabCode).append(" ").append($removeRowButtonBundle);
const $hanjaCheckbox = $("<input>").attr("type", "checkbox").data("vgtn-record", "hanja").addClass("vgtn-editable-checkbox vgtn-record-hanja");
const hanjaChecked = record.hanja === true;
$hanjaCheckbox.prop("checked", hanjaChecked);
if (hanjaChecked) {
$hanjaCheckbox.attr("checked", "checked");
} else {
$hanjaCheckbox.removeAttr("checked");
}
const $hanjaCheckboxBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-hanja-bundle").append($("<br>")).append($("<label>").attr("for", "vgtn-record-hanja").addClass("vgtn-record-label").append("確認正字(非音譯): ")).append($hanjaCheckbox);
$editableField.append($hanjaCheckboxBundle);
if (!record.lang || record.lang !== "ko") {
$hanjaCheckboxBundle.hide();
}
const $rmSpan = $("<span>").data("vgtn-record", "rm").addClass("vgtn-editable-span vgtn-record-rm").attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record.rm || "");
const $rmSpanBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-rm-bundle").append($("<br>")).append($("<label>").attr("for", "vgtn-record-rm").addClass("vgtn-record-label").append("羅馬字: ")).append($rmSpan);
$editableField.append($rmSpanBundle);
if (!record.lang || !["ja", "ko", "ru"].includes(record.lang)) {
$rmSpanBundle.hide();
}
const $aliasesSpan = $("<span>").data("vgtn-record", "aliases").addClass("vgtn-editable-span vgtn-record-aliases").attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(((_a = record.aliases) == null ? void 0 : _a.join("|")) || "");
$editableField.append($("<br>")).append($("<label>").attr("for", "vgtn-record-aliases").addClass("vgtn-record-label").append("別名(豎線隔開): ")).append($aliasesSpan);
return $editableField;
}
function LinkVariableField(record) {
const $editableField = $("<div>").addClass("vgtn-editable-field");
const linkNames = [
["link", "本站連結"],
["iw", "跨語言"],
["wd", "維基數據"],
["namu", "納木維基"]
];
linkNames.forEach(([key, label]) => {
const $editableSpan = $("<span>").data("vgtn-record", key).addClass("vgtn-editable-span vgtn-record-" + key).attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record[key] || "");
const $editableSpanBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-" + key + "-bundle");
if ($editableField.children().length > 0) $editableSpanBundle.append($("<br>"));
$editableSpanBundle.append($("<label>").attr("for", "vgtn-record-" + key).addClass("vgtn-record-label").append(label + ": ")).append($editableSpan);
$editableField.append($editableSpanBundle);
});
if (!record.lang || record.lang !== "ko") {
$editableField.find(".vgtn-editable-namu-bundle").hide();
}
return $editableField;
}
function LocaleVariableField(record) {
const $editableField = $("<div>").addClass("vgtn-editable-field");
const locales = ["tw", "hk", "mo", "cn", "sg", "my"];
const localeFallback = function(locale, record2) {
const localeFallbacks = {
"tw": ["hk", "mo", "cn", "sg", "my"],
"hk": ["mo", "tw", "cn", "sg", "my"],
"mo": ["hk", "tw", "cn", "sg", "my"],
"cn": ["sg", "my", "tw", "hk", "mo"],
"sg": ["my", "cn", "tw", "hk", "mo"],
"my": ["sg", "cn", "tw", "hk", "mo"]
};
for (const fallback of localeFallbacks[locale]) {
if (record2[fallback]) return record2[fallback];
}
return "(空)";
};
locales.forEach((locale) => {
const $editableSpan = $("<span>").data("vgtn-record", locale).addClass("vgtn-editable-span vgtn-record-" + locale).attr("lang", "zh-" + locale).attr("contenteditable", "plaintext-only").attr("placeholder", localeFallback(locale, record)).text(record[locale] || "");
$editableSpan.on("input", function() {
const $editableSpans = $editableField.find(".vgtn-editable-span");
const newRecord = {};
$editableSpans.each(function() {
newRecord[$(this).data("vgtn-record")] = $(this).text().trim();
});
$editableSpans.each(function() {
$(this).attr("placeholder", localeFallback($(this).data("vgtn-record"), newRecord));
});
});
if ($editableField.children().length > 0) $editableField.append($("<br>"));
$editableField.append($("<label>").attr("for", "vgtn-record-" + locale).addClass("vgtn-record-label").append(locale + ": ")).append($editableSpan);
});
return $editableField;
}
function CommentField(record) {
const $editableField = $("<div>").addClass("vgtn-editable-field");
const $commentSpan = $("<span>").data("vgtn-record", "comment").addClass("vgtn-editable-span vgtn-record-comment").attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record.comment || "");
$editableField.append($commentSpan);
return $editableField;
}
function addEditButtons($content) {
const $parser = $content.find(".mw-parser-output");
if (document.getElementById(editorContainerId)) return;
$("<div>").attr("id", editorContainerId).css("display", "none").appendTo($parser);
$parser.find(".mw-heading3").each((i, el) => {
const $mwHeading3 = $(el);
setHeading3Buttons($mwHeading3);
});
$parser.find(".mw-heading2").each((i, el) => {
const $mwHeading2 = $(el);
if ($mwHeading2.find("h2").attr("id") === "参见") return;
setHeading2Buttons($mwHeading2);
});
}
function setHeading3Buttons($mwHeading3) {
const $editButton = $("<a>").addClass("vgtn-edit-btn").attr("href", "javascript:void(0)").text("编辑").on("click", (e) => {
e.preventDefault();
editSection($mwHeading3);
});
if (!$mwHeading3.find(".mw-editsection").length) {
$mwHeading3.append($("<span>").addClass("mw-editsection"));
}
const $editSection = $mwHeading3.find(".mw-editsection");
$editSection.empty();
$editSection.append($("<span>").addClass("mw-editsection-bracket").text("["));
$editSection.append($editButton);
$editSection.append($("<span>").addClass("mw-editsection-bracket").text("]"));
}
function setHeading3EditingButtons($mwHeading3, $ogTable, $table) {
const $saveButton = $("<a>").addClass("vgtn-save-btn").attr("href", "javascript:void(0)").text("儲存").on("click", async (e) => {
e.preventDefault();
await saveChanges();
});
const $cancelButton = $("<a>").addClass("vgtn-cancel-btn").attr("href", "javascript:void(0)").text("取消").on("click", (e) => {
e.preventDefault();
cancelEdit($ogTable, $table, $mwHeading3);
});
const $removeSectionButton = $("<a>").addClass("vgtn-remove-section-btn").attr("href", "javascript:void(0)").text("删除章節").on("click", (e) => {
e.preventDefault();
removeSection($mwHeading3);
});
if (!$mwHeading3.find(".mw-editsection").length) {
$mwHeading3.append($("<span>").addClass("mw-editsection"));
}
const $editSection = $mwHeading3.find(".mw-editsection");
$editSection.empty();
$editSection.append($("<span>").addClass("mw-editsection-bracket").text("["));
$editSection.append($saveButton);
if (!$mwHeading3.find("h3").hasClass("vgtn-new-section")) {
$editSection.append($("<span>").addClass("mw-editsection-separator").text(" | "));
$editSection.append($cancelButton);
}
$editSection.append($("<span>").addClass("mw-editsection-separator").text(" | "));
$editSection.append($removeSectionButton);
$editSection.append($("<span>").addClass("mw-editsection-bracket").text("]"));
}
function setHeading2Buttons($mwHeading2) {
const $addButton = $("<a>").addClass("vgtn-add-btn").attr("href", "javascript:void(0)").text("新增章節").on("click", (e) => {
e.preventDefault();
addSection($mwHeading2);
});
const $sortButton = $("<a>").addClass("vgtn-sort-btn").attr("href", "javascript:void(0)").text("升序排序").on("click", (e) => {
e.preventDefault();
if ($mwHeading2.hasClass("vgtn-sort-asc")) {
$mwHeading2.removeClass("vgtn-sort-asc").addClass("vgtn-sort-desc");
$sortButton.text("原始排序");
} else if ($mwHeading2.hasClass("vgtn-sort-desc")) {
$mwHeading2.removeClass("vgtn-sort-desc");
$sortButton.text("升序排序");
} else {
$mwHeading2.addClass("vgtn-sort-asc");
$sortButton.text("降序排序");
}
});
if (!$mwHeading2.find(".mw-editsection").length) {
$mwHeading2.append($("<span>").addClass("mw-editsection"));
}
const $editSection = $mwHeading2.find(".mw-editsection");
$editSection.empty();
$editSection.append($("<span>").addClass("mw-editsection-bracket").text("["));
$editSection.append($addButton);
$editSection.append($("<span>").addClass("mw-editsection-separator").text(" | "));
$editSection.append($sortButton);
$editSection.append($("<span>").addClass("mw-editsection-bracket").text("]"));
}
// src/main.ts
function injectStylesheet() {
mw.util.addCSS(`
.vgtn-section-removed {
color: var(--lt-color-text-very-light, #8f96a3) !important;
text-decoration: line-through;
}
.vgtn-section-removed .mw-editsection {
color: var(--color-base, #202122) !important;
display: inline-block;
}
.vgtn-record-label {
font-size: small;
}
.vgtn-editable-remove-row-bundle {
font-size: smaller;
white-space: nowrap;
}
.vgtn-editable-span {
padding-left: 0.2em;
padding-right: 0.2em;
border-bottom: 2px solid var(--lt-color-border-dark, #c2c9d6);
}
.vgtn-editable-span:focus {
border-bottom: none;
}
.vgtn-editable-code {
font-size: smaller;
white-space: nowrap;
}
[contenteditable="plaintext-only"]:empty:before{
content: attr(placeholder);
pointer-events: none;
color: var(--lt-color-text-very-light, #8f96a3);
}
.wikitable tbody tr th,
.wikitable tbody tr td:nth-last-child(1) {
max-width: 15em;
}
.vgtn-row-removed th .vgtn-editable-field:before {
content: "✘";
color: red;
font-size: larger;
}
.vgtn-table-footer {
font-size: smaller;
}
.tablesorter-headerUnSorted:not(.sorter-false) {
background-image: url(/w/resources/src/jquery.tablesorter.styles/images/sort_both.svg);
cursor: pointer;
background-repeat: no-repeat;
background-position: center right;
padding-right: 21px;
}
.tablesorter-headerAsc:not(.vgtn-table-footer) {
background-image: url(/w/resources/src/jquery.tablesorter.styles/images/sort_up.svg);
cursor: pointer;
background-repeat: no-repeat;
background-position: center right;
padding-right: 21px;
}
.tablesorter-headerDesc:not(.vgtn-table-footer) {
background-image: url(/w/resources/src/jquery.tablesorter.styles/images/sort_down.svg);
cursor: pointer;
background-repeat: no-repeat;
background-position: center right;
padding-right: 21px;
}
.mw-heading2.vgtn-sort-asc .mw-editsection::after {
content: url(/w/resources/src/jquery.tablesorter.styles/images/sort_up.svg);
display: inline-block;
width: 16px;
position: relative;
top: -.2em;
}
.mw-heading2.vgtn-sort-desc .mw-editsection::after {
content: url(/w/resources/src/jquery.tablesorter.styles/images/sort_down.svg);
display: inline-block;
width: 16px;
position: relative;
top: -.2em;
}
.vgtn-diff-dialog-diff {
padding: 10px;
overflow-x: auto;
}
.vgtn-diff-dialog__summary-input {
margin-top: 20px;
}
`);
}
function initInjection() {
injectStylesheet();
mw.hook("wikipage.content").add(($content) => addEditButtons($content));
}
function init() {
if (mw.config.get("wgPageName") !== "WikiProject:电子游戏/译名表") return;
mw.loader.load("mediawiki.diff.styles");
mw.loader.getScript("https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.32.0/js/jquery.tablesorter.min.js").then(() => {
console.log("[VGTNTool] 小工具已載入。");
initInjection();
}).catch((e) => {
console.error("[VGTNTool] 無法載入 jQuery TableSorter 函式庫", e);
mw.notify("無法載入 jQuery TableSorter 函式庫,部分功能可能無法使用。", { type: "error", title: "VGTNTool 小工具" });
initInjection();
});
}
init();
})();
// </nowiki>