User:SuperGrey/gadgets/Reaction/main.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/Reaction]]
var ReactionTools = {
/**
* 處理反應按鈕的點擊事件,轉發到相應的處理函式。
* @param button {HTMLElement} - 反應按鈕元素。
*/
handleReactionClick: function (button) {
// If it's a new reaction button, add a new reaction
if (button.classList.contains("reaction-new")) {
this.addNewReaction(button);
} else {
// Otherwise, toggle the reaction
this.toggleReaction(button);
}
},
/**
* 切換普通反應按鈕(非「新反應」)的反應狀態。
* @param button {HTMLElement} - 反應按鈕元素。
*/
toggleReaction: function (button) {
if (button.classList.contains("reaction-reacted")) {
if (!button.getAttribute("data-commentors").includes(this.userName)) {
mw.notify(this.convByVar({
hant: "[Reaction] 失敗!不能取消並未做出的反應。", hans: "[Reaction] 失败!不能取消并未做出的反应。",
}), {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
console.log("[Reaction] Should not happen! " + this.userName + " should be in " + button.getAttribute("data-commentors"));
return;
}
let buttonIcon = button.querySelector(".reaction-icon");
let buttonCounter = button.querySelector(".reaction-counter");
let count = parseInt(buttonCounter.innerText);
let mod;
if (count > 1) {
mod = {
timestamp: this.parseTimestamp(this._buttonTimestamps.get(button)),
downvote: buttonIcon.innerText.trim(),
};
} else {
mod = {
timestamp: this.parseTimestamp(this._buttonTimestamps.get(button)),
remove: buttonIcon.innerText.trim(),
};
}
this.modifyPage(mod).then((response) => {
if (response) {
// 外觀上取消反應
button.classList.remove("reaction-reacted");
if (count > 1) {
buttonCounter.innerText = (count - 1).toString();
// Update the data-commentors attribute
let dataCommentors = button.getAttribute("data-commentors") + "/"; // Add a trailing slash to make it easier to replace
dataCommentors = dataCommentors.replace(this.userName + "/", "");
dataCommentors = dataCommentors.slice(0, -1); // Remove the trailing slash
button.setAttribute("data-commentors", dataCommentors);
let buttonTitle = button.getAttribute("title");
if (buttonTitle) {
buttonTitle = buttonTitle.replace(new RegExp(this.userNameAtChineseUtcRegex(), "g"), "");
let trailingSemicolonRegex = new RegExp(";" + this.atChineseUtcRegex() + "回[應应]了[這这][條条]留言$", "g");
// console.log(trailingSemicolonRegex);
buttonTitle = buttonTitle.replace(trailingSemicolonRegex, "");
let trailingCommaRegex = new RegExp("、" + this.atChineseUtcRegex() + "(|、.+?)(回[應应]了[這这][條条]留言)$", "g");
// console.log(trailingCommaRegex);
buttonTitle = buttonTitle.replace(trailingCommaRegex, "$1$2");
buttonTitle = buttonTitle.replace(new RegExp("^" + this.atChineseUtcRegex() + "、"), ""); // Remove leading comma
button.setAttribute("title", buttonTitle);
}
} else {
button.parentNode.removeChild(button);
}
}
});
} else {
if (button.getAttribute("data-commentors").includes(this.userName)) {
mw.notify(this.convByVar({
hant: "[Reaction] 失敗!不能重複做出反應。", hans: "[Reaction] 失败!不能重复做出反应。",
}), {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
console.log("[Reaction] Should not happen! " + this.userName + " should not be in " + button.getAttribute("data-commentors"));
return;
}
let buttonIcon = button.querySelector(".reaction-icon");
let mod = {
timestamp: this.parseTimestamp(this._buttonTimestamps.get(button)), upvote: buttonIcon.innerText.trim(),
};
this.modifyPage(mod).then((response) => {
if (response) {
// 外觀上添加反應
button.classList.add("reaction-reacted");
let buttonCounter = button.querySelector(".reaction-counter");
let count = parseInt(buttonCounter.innerText);
buttonCounter.innerText = (count + 1).toString();
// Update the data-commentors attribute
let dataCommentors = button.getAttribute("data-commentors");
if (dataCommentors) {
dataCommentors += "/" + this.userName;
} else {
dataCommentors = this.userName;
}
button.setAttribute("data-commentors", dataCommentors);
let buttonTitle = button.getAttribute("title");
if (buttonTitle) {
buttonTitle += ";";
} else {
buttonTitle = "";
}
buttonTitle += this.userName + this.convByVar({
hant: "於", hans: "于",
}) + this.getCurrentChineseUtc() + this.convByVar({
hant: "回應了這條留言", hans: "回应了这条留言",
});
button.setAttribute("title", buttonTitle);
}
});
}
},
/**
* 取消新反應按鈕的編輯狀態。
* @param button {HTMLElement} - 「新反應」按鈕元素。
*/
cancelNewReaction: function (button) {
event.stopPropagation();
// Remove event handlers using the stored bound function reference.
let saveButton = button.querySelector(".reaction-save");
const saveButtonClickHandler = this._handlerRegistry.get(saveButton);
if (saveButtonClickHandler) {
saveButton.removeEventListener("click", saveButtonClickHandler);
// Remove the reference from the registry.
this._handlerRegistry.delete(saveButton);
}
let cancelButton = button.querySelector(".reaction-cancel");
const cancelButtonClickHandler = this._handlerRegistry.get(cancelButton);
if (cancelButtonClickHandler) {
cancelButton.removeEventListener("click", cancelButtonClickHandler);
// Remove the reference from the registry.
this._handlerRegistry.delete(cancelButton);
}
// Restore the add new reaction button to the original state
let buttonIcon = button.querySelector(".reaction-icon");
buttonIcon.textContent = "+";
let buttonCounter = button.querySelector(".reaction-counter");
buttonCounter.innerText = this.convByVar({hant: "反應", hans: "反应"});
// Restore the original event handler
// Create the bound function and store it in the WeakMap.
if (this._handlerRegistry.has(button)) {
console.error("[Reaction] Not possible! The event handler should not be registered yet.");
return;
}
const buttonClickHandler = this.handleReactionClick.bind(this, button);
this._handlerRegistry.set(button, buttonClickHandler);
button.addEventListener("click", buttonClickHandler);
},
/**
* 儲存新的反應,並更新按鈕的狀態。
* @param button {HTMLElement} - 「新反應」按鈕元素。
*/
saveNewReaction: function (button) {
event.stopPropagation();
let input = button.querySelector(".reaction-icon input");
if (!input.value.trim()) {
mw.notify(this.convByVar({
hant: "[Reaction] 反應內容不能為空!", hans: "[Reaction] 反应内容不能为空!",
}), {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
return;
}
// Save the new reaction
let timestamp = this.parseTimestamp(this._buttonTimestamps.get(button));
if (!timestamp) {
mw.notify(this.convByVar({
hant: "[Reaction] 失敗!無法獲取時間戳。", hans: "[Reaction] 失败!无法获取时间戳。",
}), {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
return;
}
let mod = {
timestamp: timestamp, append: input.value.trim(),
};
this.modifyPage(mod).then((response) => {
if (response) {
// Change the icon to the new reaction
button.classList.remove("reaction-new");
button.classList.add("reaction-reacted");
let buttonIcon = button.querySelector(".reaction-icon");
buttonIcon.textContent = input.value;
let buttonCounter = button.querySelector(".reaction-counter");
buttonCounter.textContent = "1";
button.setAttribute("title", this.userName + this.convByVar({
hant: "於", hans: "于",
}) + this.getCurrentChineseUtc() + this.convByVar({
hant: "回應了這條留言", hans: "回应了这条留言",
}));
button.setAttribute("data-commentors", this.userName);
// Remove event handlers using the stored bound function reference.
let saveButton = button.querySelector(".reaction-save");
const saveButtonClickHandler = this._handlerRegistry.get(saveButton);
if (saveButtonClickHandler) {
saveButton.removeEventListener("click", saveButtonClickHandler);
// Remove the reference from the registry.
this._handlerRegistry.delete(saveButton);
}
let cancelButton = button.querySelector(".reaction-cancel");
const cancelButtonClickHandler = this._handlerRegistry.get(cancelButton);
if (cancelButtonClickHandler) {
cancelButton.removeEventListener("click", cancelButtonClickHandler);
// Remove the reference from the registry.
this._handlerRegistry.delete(cancelButton);
}
// Add new reaction button after the old button
let newReactionButton = this.NewReactionButton();
button.parentNode.insertBefore(newReactionButton, button.nextSibling);
this._buttonTimestamps.set(newReactionButton, this._buttonTimestamps.get(button)); // Store the timestamp for the new button
// Restore the original event handler
// Create the bound function and store it in the WeakMap.
if (this._handlerRegistry.has(button)) {
console.error("Not possible! The event handler should not be registered yet.");
return;
}
const buttonClickHandler = this.handleReactionClick.bind(this, button);
this._handlerRegistry.set(button, buttonClickHandler);
button.addEventListener("click", buttonClickHandler);
}
});
},
/**
* 解析14位數字格式的UTC日期字串,並返回對應的Date物件。
* @param utc14 {string} - 14位數字格式的UTC日期字串,例如「20231015123456」。
* @returns {Date} - 對應的Date物件。
*/
parseUtc14: function (utc14) {
// Extract year, month, day, hour, minute, and second from the string
const year = Number(utc14.slice(0, 4));
const month = Number(utc14.slice(4, 6)) - 1; // JavaScript months are 0-indexed
const day = Number(utc14.slice(6, 8));
const hour = Number(utc14.slice(8, 10));
const minute = Number(utc14.slice(10, 12));
const second = Number(utc14.slice(12, 14));
// Create a Date object from UTC values
return new Date(Date.UTC(year, month, day, hour, minute, second));
},
/**
* 生成中文格式的UTC日期字串。
* @param utc14 {string} - 14位數字格式的UTC日期字串,例如「20231015123456」。
* @returns {string} - 中文格式的UTC日期字串,例如「2023年10月15日 (日) 12:34 (UTC)」。
*/
utc14ToChineseUtc: function (utc14) {
const date = this.parseUtc14(utc14);
return this.dateToChineseUtc(date);
},
/**
* 解析中文格式的UTC日期字串,並返回對應的Date物件。
* @param chineseUtcDate {string} - 中文格式的UTC日期字串,例如「2023年10月15日 (日) 12:34 (UTC)」。
* @returns {null|Date} - 對應的Date物件,或null(如果無法解析)。
*/
parseChineseUtc: function (chineseUtcDate) {
const match = chineseUtcDate.match(new RegExp('^' + this.chineseUtcCaptureRegex + '$'));
if (match) {
const year = parseInt(match[1]);
const month = parseInt(match[2]) - 1; // JavaScript months are 0-indexed
const day = parseInt(match[3]);
const hour = parseInt(match[5]);
const minute = parseInt(match[6]);
return new Date(Date.UTC(year, month, day, hour, minute));
} else {
console.error("[Reaction] Unable to parse Chinese UTC date: " + chineseUtcDate);
return null;
}
},
/**
* 將Date物件轉換為中文格式的UTC日期字串。
* @param date {Date} - Date物件。
* @returns {string} - 中文格式的UTC日期字串,例如「2023年10月15日 (日) 12:34 (UTC)」。
*/
dateToChineseUtc: function (date) {
return date.getUTCFullYear() + "年" + (date.getUTCMonth() + 1) + "月" + date.getUTCDate() + "日 (" + [
"日", "一", "二", "三", "四", "五", "六",
][date.getUTCDay()] + ") " + date.getUTCHours().toString().padStart(2, "0") + ":" + date.getUTCMinutes().toString().padStart(2, "0") + " (UTC)";
},
/**
* 解析時間戳,並返回對應的UTC日期字串。
* @param timestamp {HTMLElement} - 時間戳元素。
* @returns {null|string} - 對應的UTC日期字串,或null(如果無法解析)。
*/
parseTimestamp: function (timestamp) {
let utcTimestamp = timestamp.querySelector(".localcomments");
if (utcTimestamp) {
return utcTimestamp.getAttribute("title");
} else {
let href = timestamp.getAttribute("href");
let ts_s = (href.split('#')[1] || '');
let ts = ts_s.startsWith('c-') ? (ts_s.match(/-(\d{14})-/) || [])[1] : undefined;
if (ts) {
return this.utc14ToChineseUtc(ts);
} else {
console.error("[Reaction] Unable to parse timestamp in: " + href);
return null;
}
}
},
/**
* 獲取完整的wikitext。
* @returns {Promise<string>} 包含完整wikitext的Promise。
*/
retrieveFullText: async function () {
let api = new mw.Api({userAgent: 'Reaction/1.0.0'});
let response = await api.get({
action: 'query', titles: this.pageName, prop: 'revisions', rvslots: '*', rvprop: 'content', indexpageids: 1,
});
let fulltext = response.query.pages[response.query.pageids[0]].revisions[0].slots.main['*'];
return fulltext + "\n";
},
/**
* 儲存完整的wikitext。
* @param fulltext {string} - 完整的wikitext。
* @param summary {string} - 編輯摘要。
* @returns {Promise<boolean>} - 操作成功與否的Promise。
*/
saveFullText: async function (fulltext, summary) {
try {
let api = new mw.Api({userAgent: 'Reaction/1.0.0'});
await api.postWithToken('edit', {
action: 'edit',
title: this.pageName,
text: fulltext,
summary: summary + " ([[User:SuperGrey/gadgets/Reaction|Reaction]])",
});
mw.notify(this.convByVar({hant: "[Reaction] 儲存成功!", hans: "[Reaction] 保存成功!"}), {
title: "成功", type: "success",
});
return true;
} catch (e) {
console.error(e);
mw.notify(this.convByVar({
hant: "[Reaction] 失敗!無法儲存頁面。", hans: "[Reaction] 失败!无法保存页面。",
}), {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
return false;
}
},
/**
* 將字串中的特殊字符轉義。
* @param string {String} - 字串
* @returns {String} - 轉義後的字串
*/
escapeRegex: function (string) {
return mw.util.escapeRegExp(string);
},
/**
* 修改頁面內容。
* @param mod {Object} - 修改內容的物件,包含時間戳(timestamp)、要添加或刪除的反應等(upvote、downvote、append、remove)。
* @returns {Promise<boolean>} - 操作成功與否的Promise。
*/
modifyPage: async function (mod) {
let fulltext;
try {
fulltext = await this.retrieveFullText();
} catch (e) {
console.error(e);
mw.notify(this.convByVar({
hant: "[Reaction] 失敗!無法獲取頁面內容。", hans: "[Reaction] 失败!无法获取页面内容。",
}), {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
return false;
}
let newFulltext;
let summary = "";
try {
let timestampRegex = new RegExp(`${this.escapeRegex(mod.timestamp)}`, "g");
let timestampMatch = fulltext.match(timestampRegex);
// If the timestamp is not found, throw an error
if (!timestampMatch || timestampMatch.length === 0) {
console.log("[Reaction] Unable to find timestamp " + mod.timestamp + " in: " + fulltext);
throw new Error("[Reaction] " + this.convByVar({
hant: "原文中找不到時間戳:", hans: "原文中找不到时间戳:",
}) + mod.timestamp);
}
// Check if more than one match is found.
if (timestampMatch.length > 1) {
console.log("[Reaction] More than one timestamp found: " + timestampMatch);
throw new Error("[Reaction] " + this.convByVar({
hant: "原文中找到多個相同的時間戳,小工具無法處理:",
hans: "原文中找到多个相同的时间戳,小工具无法处理:",
}) + mod.timestamp);
}
let pos = fulltext.search(timestampRegex);
console.log("[Reaction] Found timestamp " + mod.timestamp + " at position " + pos);
if (mod.remove) {
let regex = new RegExp(` *\\{\\{ *[Rr]eact(?:ion|) *\\| *${this.escapeRegex(mod.remove)} *\\| *${this.userNameAtChineseUtcRegex()} *}}`, "g");
// console.log(regex);
// Find this after the timestamp, but before the next newline
let lineEnd = fulltext.indexOf("\n", pos);
let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
let newTimestamp2LineEnd = timestamp2LineEnd.replace(regex, "");
newFulltext = fulltext.slice(0, pos) + newTimestamp2LineEnd + fulltext.slice(lineEnd);
summary = "− " + mod.remove;
} else if (mod.downvote) {
let regex = new RegExp(`\\{\\{ *[Rr]eact(?:ion|) *\\| *${this.escapeRegex(mod.downvote)} *(|\\|[^}]*?)\\| *${this.userNameAtChineseUtcRegex()} *(|\\|[^}]*?)}}`, "g");
// console.log(regex);
// Find this after the timestamp, but before the next newline
let lineEnd = fulltext.indexOf("\n", pos);
let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
let newTimestamp2LineEnd = timestamp2LineEnd.replace(regex, `{{Reaction|${mod.downvote}$1$2}}`);
newFulltext = fulltext.slice(0, pos) + newTimestamp2LineEnd + fulltext.slice(lineEnd);
summary = "− " + mod.downvote;
} else if (mod.upvote) {
let regex = new RegExp(`\\{\\{ *[Rr]eact(?:ion|) *\\| *${this.escapeRegex(mod.upvote)}([^}]*?)}}`, "g");
// console.log(regex);
// Find this after the timestamp, but before the next newline
let lineEnd = fulltext.indexOf("\n", pos);
let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
let newTimestamp2LineEnd = timestamp2LineEnd.replace(regex, `{{Reaction|${mod.upvote}$1|${this.userName}於${this.getCurrentChineseUtc()}}}`);
newFulltext = fulltext.slice(0, pos) + newTimestamp2LineEnd + fulltext.slice(lineEnd);
summary = "+ " + mod.upvote;
} else if (mod.append) {
let regex = new RegExp(`\\{\\{ *[Rr]eact(?:ion|) *\\| *${this.escapeRegex(mod.append)}([^}]*?)}}`, "g");
// console.log(regex);
let lineEnd = fulltext.indexOf("\n", pos);
let timestamp2LineEnd = fulltext.slice(pos, lineEnd);
// If the reaction already exists, then error
if (regex.test(timestamp2LineEnd)) {
console.log("[Reaction] Reaction of " + mod.append + " already exists in: " + timestamp2LineEnd);
throw new Error("[Reaction] " + this.convByVar({
hant: "原文中已經有這個反應!", hans: "原文中已经有这个反应!",
}));
}
// Add text at the end of that line
let newText = "{{Reaction|" + mod.append + "|" + this.userName + "於" + this.getCurrentChineseUtc() + "}}";
newFulltext = fulltext.slice(0, lineEnd) + " " + newText + fulltext.slice(lineEnd);
summary = "+ " + mod.append;
}
if (newFulltext === fulltext) {
console.log("[Reaction] Nothing is modified. This should not happen.");
throw new Error("[Reaction] " + this.convByVar({
hant: "因為未知原因,原文未被修改!", hans: "因为未知原因,原文未被修改!",
}));
}
// 儲存全文。錯誤資訊已在函式內處理。
return await this.saveFullText(newFulltext, summary);
} catch (e) {
console.error(e);
mw.notify(e.message, {title: this.convByVar({hant: "錯誤", hans: "错误"}), type: "error"});
return false;
}
},
/**
* 創建一個可調整大小的輸入框。
* @param text {string} - 預設文字。
* @param parent {HTMLElement} - 父元素。輸入框(以及隱藏的寬度計算器)將被添加到這個元素中。
* @returns {HTMLInputElement} - 可調整大小的輸入框。
* @constructor
*/
ResizableInput: function (text = "", parent = document.body) {
let input = document.createElement("input");
input.value = text;
input.style.width = "1em";
input.style.background = "transparent";
input.style.border = "0";
input.style.boxSizing = "content-box";
parent.appendChild(input);
// Hidden width calculator
let hiddenInput = document.createElement("span");
hiddenInput.style.position = "absolute";
hiddenInput.style.top = "0";
hiddenInput.style.left = "0";
hiddenInput.style.visibility = "hidden";
hiddenInput.style.height = "0";
hiddenInput.style.overflow = "scroll";
hiddenInput.style.whiteSpace = "pre";
parent.appendChild(hiddenInput);
const inputStyles = window.getComputedStyle(input);
[
"fontFamily", "fontSize", "fontWeight", "fontStyle", "letterSpacing", "textTransform",
].forEach(prop => {
hiddenInput.style[prop] = inputStyles[prop];
});
function inputResize() {
hiddenInput.innerText = input.value || input.placeholder || text;
const width = hiddenInput.scrollWidth;
input.style.width = (width + 2) + "px";
}
input.addEventListener("input", inputResize);
inputResize();
return input;
},
/**
* 將「新反應」按鈕轉換為可編輯狀態,並加入「儲存」和「取消」選單。
* @param button {HTMLElement} - 「新反應」按鈕元素。
*/
addNewReaction: function (button) {
// Remove event handlers using the stored bound function reference.
// Retrieve the handler reference from the WeakMap.
const buttonClickHandler = this._handlerRegistry.get(button);
if (buttonClickHandler) {
button.removeEventListener("click", buttonClickHandler);
// Remove the reference from the registry.
this._handlerRegistry.delete(button);
}
// Change the icon to a textbox
let buttonIcon = button.querySelector(".reaction-icon");
buttonIcon.textContent = ""; // Clear the icon
let input = this.ResizableInput("👍", buttonIcon);
input.focus();
input.select();
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
ReactionTools.saveNewReaction(button, false);
} else if (event.key === "Escape") {
ReactionTools.cancelNewReaction(button, false);
}
});
let buttonCounter = button.querySelector(".reaction-counter");
let saveButton = document.createElement("span");
saveButton.className = "reaction-save";
saveButton.innerText = this.convByVar({hant: "儲存", hans: "保存"});
if (this._handlerRegistry.has(saveButton)) {
return;
}
const saveButtonClickHandler = this.saveNewReaction.bind(this, button); // Create bound functions and store them in the WeakMap.
this._handlerRegistry.set(saveButton, saveButtonClickHandler);
saveButton.addEventListener("click", saveButtonClickHandler);
let cancelButton = document.createElement("span");
cancelButton.className = "reaction-cancel";
cancelButton.innerText = this.convByVar({hant: "取消", hans: "取消"});
if (this._handlerRegistry.has(cancelButton)) {
return;
}
const cancelButtonClickHandler = this.cancelNewReaction.bind(this, button); // Create bound functions and store them in the WeakMap.
this._handlerRegistry.set(cancelButton, cancelButtonClickHandler);
cancelButton.addEventListener("click", cancelButtonClickHandler);
buttonCounter.innerText = "";
buttonCounter.appendChild(saveButton);
buttonCounter.appendChild(document.createTextNode(" | "));
buttonCounter.appendChild(cancelButton);
},
/**
* 創建一個「新反應」按鈕。
* @returns {HTMLSpanElement} - 「新反應」按鈕元素。
* @constructor
*/
NewReactionButton: function () {
let button = document.createElement("span");
button.className = "reaction reaction-new";
let buttonContent = document.createElement("span");
buttonContent.className = "reaction-content";
let buttonIconContainer = document.createElement("span");
buttonIconContainer.className = "reaction-icon-container";
let buttonIcon = document.createElement("span");
buttonIcon.className = "reaction-icon";
buttonIcon.innerText = "+";
buttonIconContainer.appendChild(buttonIcon);
let buttonCounterContainer = document.createElement("span");
buttonCounterContainer.className = "reaction-counter-container";
let buttonCounter = document.createElement("span");
buttonCounter.className = "reaction-counter";
buttonCounter.innerText = this.convByVar({hant: "反應", hans: "反应"});
buttonCounterContainer.appendChild(buttonCounter);
buttonContent.appendChild(buttonIconContainer);
buttonContent.appendChild(buttonCounterContainer);
button.appendChild(buttonContent);
// Create the bound function and store it in the WeakMap.
let buttonClickHandler = this.handleReactionClick.bind(this, button);
this._handlerRegistry.set(button, buttonClickHandler);
button.addEventListener("click", buttonClickHandler);
return button;
},
/**
* 綁定事件到普通反應按鈕(非「新反應」)。
* @param button {HTMLElement} - 反應按鈕元素。
*/
bindEvent2ReactionButton: function (button) {
// Create the bound function and store it in the WeakMap.
if (this._handlerRegistry.has(button)) {
return;
}
let buttonClickHandler = this.handleReactionClick.bind(this, button);
this._handlerRegistry.set(button, buttonClickHandler);
button.addEventListener("click", buttonClickHandler);
// Check if the user has reacted to this
let reacted = false;
for (const commentor of button.getAttribute("data-commentors").split("/")) {
// Either username or username於chineseUtc
let regex = new RegExp('^' + this.userNameAtChineseUtcRegex() + '$');
// console.log(regex);
if (regex.test(commentor)) {
reacted = true;
break;
}
}
if (reacted) {
button.classList.add("reaction-reacted");
}
},
/**
* 在每個回覆按鈕前添加「新反應」按鈕。
*/
addNewReactionButtons: function () {
// Add a "New Reaction" button before each reply button
for (let i = 0; i < this.replyButtons.length; i++) {
let reactionButton = this.NewReactionButton();
let timestamp = this.timestamps[i];
this._buttonTimestamps.set(reactionButton, timestamp); // Store the timestamp for the new button
// Insert the button before the reply button
let replyButton = this.replyButtons[i];
replyButton.parentNode.insertBefore(reactionButton, replyButton);
}
},
/**
* 載入所需的CSS和HanAssist模組。
* @returns {Promise<void>} - 載入完成的Promise。
*/
importDependencies: async function () {
mw.loader.load('/w/index.php?title=Template:Reaction/styles.css&action=raw&ctype=text/css', 'text/css');
await mw.loader.using('ext.gadget.HanAssist', function (require) {
const {convByVar} = require('ext.gadget.HanAssist');
ReactionTools.convByVar = convByVar;
});
},
/**
* 初始化函式,載入所需的模組和事件綁定。
*/
init: function () {
this.importDependencies().then(() => {
this.timestamps = document.querySelectorAll("a.ext-discussiontools-init-timestamplink");
this.replyButtons = document.querySelectorAll("span.ext-discussiontools-init-replylink-buttons");
// 尋找時間戳與回覆按鈕之間的所有反應按鈕
for (let i = 0; i < this.timestamps.length; i++) {
let timestamp = this.timestamps[i];
let replyButton = this.replyButtons[i];
let button = timestamp.nextElementSibling;
while (button && button !== replyButton) {
if (button.classList.contains("reaction")) {
this._buttonTimestamps.set(button, timestamp);
this.bindEvent2ReactionButton(button);
}
button = button.nextElementSibling;
}
}
this.addNewReactionButtons();
});
},
/**
* 事件處理函式註冊表。WeakMap用於儲存事件處理函式的引用,以便在需要時可以移除它們。
* @type {WeakMap<HTMLElement, Function>}
* @private
*/
_handlerRegistry: new WeakMap(),
/**
* 按鈕對應的時間戳。WeakMap用於儲存按鈕與時間戳之間的關聯。
* @type {WeakMap<HTMLElement, HTMLElement>}
* @private
*/
_buttonTimestamps: new WeakMap(),
/**
* 時間戳列表,包含所有的時間戳元素。
* @type {HTMLElement[]}
*/
timestamps: [],
/**
* 回覆按鈕列表,包含所有的回覆按鈕元素(與時間戳一一對應)。
* @type {HTMLElement[]}
*/
replyButtons: [],
/**
* 使用者名稱,從MediaWiki配置中獲取。
* @type {string}
* @constant
*/
userName: mw.config.get('wgUserName'),
/**
* 頁面名稱,從MediaWiki配置中獲取。
* @type {string}
* @constant
*/
pageName: mw.config.get('wgPageName'),
/**
* 簡繁轉換函式,從HanAssist模組中獲取。
* @type {function}
*/
convByVar: null,
/**
* 正則表達式,用於匹配中文格式的UTC時間戳。
* @type {string}
* @constant
*/
chineseUtcRegex: `\\d{4}年\\d{1,2}月\\d{1,2}日 \\([日一二三四五六]\\) \\d{1,2}:\\d{2} \\(UTC\\)`,
/**
* 正則表達式,用於匹配並捕獲中文格式的UTC時間戳。
* @type {string}
* @constant
*/
chineseUtcCaptureRegex: `(\\d{4})年(\\d{1,2})月(\\d{1,2})日 \\(([日一二三四五六])\\) (\\d{1,2}):(\\d{2}) \\(UTC\\)`,
/**
* 正則表達式,用於匹配「於」或「于」後的UTC時間戳。
* @returns {string}
* @constant
*/
atChineseUtcRegex: function () {
return "(?:|[於于]" + this.chineseUtcRegex + ")";
},
/**
* 正則表達式,用於匹配使用者名稱和時間戳。
* 格式為「使用者名稱於2023年10月15日 (日) 12:34 (UTC)」。
* @returns {string}
* @constant
*/
userNameAtChineseUtcRegex: function () {
return this.escapeRegex(this.userName) + this.atChineseUtcRegex();
},
/**
* 獲取當前的中文格式UTC時間字串。
* @returns {string} - 當前的中文格式UTC時間字串,例如「2023年10月15日 (日) 12:34 (UTC)」。
*/
getCurrentChineseUtc: function () {
const date = new Date();
return this.dateToChineseUtc(date);
},
};
ReactionTools.init();