MediaWiki:Gadget-unihan-helper.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**!
* _________________________________________________________________________________
* | |
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes on the talk page, [[WP:VPT]] or GitHub before editing. |
* |_________________________________________________________________________________|
*
* Built from GitHub repository (https://github.com/Suoerix/unihan-helper), you should not make
* changes directly here.
*
* See https://github.com/Suoerix/unihan-helper/blob/main/README.md for build instructions.
*/
// <nowiki>
"use strict";
var __defProp = Object.defineProperty;
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 __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
const API_BASE = "https://webfont-zh.toolforge.org";
const IS_TOUCHSCREEN = "ontouchstart" in document.documentElement;
const STORAGE_KEY = "unihan-settings";
const DEFAULT_FONT = "Plangothic";
const DEFAULT_SETTINGS = {
enabled: true,
useWebfont: false,
loadMode: "always",
selectedFont: DEFAULT_FONT
};
const CLASSES = {
OVERLAY: "unihan-overlay",
INLINE_UNIHAN: "inline-unihan"
};
const TIMINGS = {
HOVER_DELAY: IS_TOUCHSCREEN ? 0 : 200,
HIDE_DELAY: 200
};
const fontVersions = {
"Plangothic": "2.9.5787"
};
async function fetchFontList() {
const response = await fetch(`${API_BASE}/api/v1/list`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const list = await response.json();
list.forEach((font) => {
fontVersions[font.id] = font.version;
});
return list;
}
function buildFontApiUrl(fontId, codePoint, version) {
const v = fontVersions[fontId];
let url = `${API_BASE}/api/v1/font?id=${fontId}&char=${codePoint}`;
if (v) {
url += `&v=${v}`;
}
return url;
}
function getCodePoint(char) {
return char.codePointAt(0) || 0;
}
function getHexCodePoint(char) {
return getCodePoint(char).toString(16).toUpperCase();
}
function getSettings(key, defaults) {
try {
const stored = localStorage.getItem(key);
if (stored) {
return __spreadValues(__spreadValues({}, defaults), JSON.parse(stored));
}
return defaults;
} catch (e) {
return defaults;
}
}
function saveSettings(key, settings2) {
try {
localStorage.setItem(key, JSON.stringify(settings2));
} catch (e) {
console.error("Failed to save settings:", e);
}
}
const appliedChars = /* @__PURE__ */ new Set();
let styleElement = null;
function getStyleElement() {
if (!styleElement) {
styleElement = document.getElementById("unihan-webfont-styles");
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "unihan-webfont-styles";
document.head.appendChild(styleElement);
}
}
return styleElement;
}
function applyWebFont(char, fontId, loadMode = "always") {
const codePoint = getCodePoint(char);
const hexCodePoint = getHexCodePoint(char);
const key = `${fontId}-${codePoint}`;
if (appliedChars.has(key)) {
return;
}
appliedChars.add(key);
const fontFamily = `${fontId}-${codePoint}`;
const css = `
@font-face {
font-family: "${fontFamily}";
src: url("${buildFontApiUrl(fontId, codePoint)}") format("woff2");
unicode-range: U+${hexCodePoint};
}`;
const styleEl = getStyleElement();
styleEl.textContent += css + "\n";
document.querySelectorAll(`.${CLASSES.INLINE_UNIHAN}`).forEach((el) => {
const element = el;
const text = element.textContent || "";
if (text.includes(char)) {
const currentFamily = element.style.fontFamily || "serif";
if (loadMode === "fallback") {
element.style.fontFamily = `${currentFamily}, "${fontFamily}"`;
} else {
element.style.fontFamily = `"${fontFamily}", ${currentFamily}`;
}
}
});
}
function processUnihanChars(fontId, loadMode = "always") {
document.querySelectorAll(`.${CLASSES.INLINE_UNIHAN}`).forEach((el) => {
const text = (el.textContent || "").trim();
for (const char of text) {
applyWebFont(char, fontId, loadMode);
}
});
}
function clearAppliedFonts() {
appliedChars.clear();
const styleEl = getStyleElement();
styleEl.textContent = "";
}
class Tooltip {
constructor(element, text, onSettingsClick) {
__publicField(this, "element");
__publicField(this, "text");
__publicField(this, "$tooltip", null);
__publicField(this, "$content", null);
__publicField(this, "$tail", null);
__publicField(this, "isPresent", false);
__publicField(this, "disappearing", false);
__publicField(this, "hideTimer", null);
__publicField(this, "removeTimer", null);
__publicField(this, "onSettingsClick");
__publicField(this, "isSettingsLoading", false);
// 外部定时器清除回调
__publicField(this, "clearExternalTimers", null);
this.element = element;
this.text = text;
this.onSettingsClick = onSettingsClick;
}
show() {
this.clearTimers();
this.disappearing = false;
if (!this.$tooltip) {
this.create();
}
if (this.$tooltip) {
this.$tooltip.classList.remove("unihan-fade-out-up", "unihan-fade-out-down");
}
if (!this.isPresent && this.$tooltip) {
document.body.appendChild(this.$tooltip);
this.isPresent = true;
}
this.calculatePosition();
}
hide() {
if (!this.$tooltip || !this.isPresent) {
return;
}
this.disappearing = true;
if (this.$tooltip.classList.contains("unihan-tooltip-above")) {
this.$tooltip.classList.remove("unihan-fade-in-down");
this.$tooltip.classList.add("unihan-fade-out-up");
} else {
this.$tooltip.classList.remove("unihan-fade-in-up");
this.$tooltip.classList.add("unihan-fade-out-down");
}
this.removeTimer = window.setTimeout(() => {
if (this.$tooltip && this.isPresent) {
this.$tooltip.remove();
this.isPresent = false;
this.disappearing = false;
}
}, 200);
}
clearTimers() {
if (this.hideTimer !== null) {
clearTimeout(this.hideTimer);
this.hideTimer = null;
}
if (this.removeTimer !== null) {
clearTimeout(this.removeTimer);
this.removeTimer = null;
}
}
isVisible() {
return this.isPresent && !this.disappearing;
}
destroy() {
this.clearTimers();
if (this.$tooltip) {
this.$tooltip.remove();
this.$tooltip = null;
this.$content = null;
this.$tail = null;
}
this.isPresent = false;
this.disappearing = false;
}
create() {
this.$tooltip = document.createElement("div");
this.$tooltip.className = "unihan-tooltip";
this.$tooltip.setAttribute("role", "tooltip");
this.$content = document.createElement("div");
this.$content.className = "unihan-tooltip-content";
const settingsContainer = document.createElement("div");
settingsContainer.className = "unihan-settings-btn-container";
const settingsButton = document.createElement("button");
settingsButton.className = "unihan-settings-button";
const settingsLabel = mw.msg("unihan-settings");
settingsButton.setAttribute("aria-label", settingsLabel);
settingsButton.setAttribute("title", settingsLabel);
const settingsIcon = document.createElement("span");
settingsIcon.className = "unihan-settings-icon";
settingsButton.appendChild(settingsIcon);
settingsButton.addEventListener("click", async () => {
if (this.isSettingsLoading) {
return;
}
this.isSettingsLoading = true;
settingsButton.disabled = true;
try {
await this.onSettingsClick();
} finally {
setTimeout(() => {
this.isSettingsLoading = false;
settingsButton.disabled = false;
}, 300);
}
this.hide();
});
settingsContainer.appendChild(settingsButton);
const textDiv = document.createElement("div");
textDiv.className = "unihan-tooltip-text";
textDiv.textContent = this.text;
this.$content.appendChild(settingsContainer);
this.$content.appendChild(textDiv);
this.$tail = document.createElement("div");
this.$tail.className = "unihan-tooltip-tail";
this.$tooltip.appendChild(this.$tail);
this.$tooltip.appendChild(this.$content);
if (!IS_TOUCHSCREEN) {
this.$tooltip.addEventListener("mouseenter", () => {
this.clearTimers();
if (this.clearExternalTimers) {
this.clearExternalTimers();
}
this.disappearing = false;
});
this.$tooltip.addEventListener("mouseleave", () => {
this.hideTimer = window.setTimeout(() => {
this.hide();
}, TIMINGS.HIDE_DELAY);
});
}
}
calculatePosition() {
if (!this.$tooltip || !this.$tail) {
return;
}
const anchorRect = this.element.getBoundingClientRect();
const tooltipRect = this.$tooltip.getBoundingClientRect();
const windowWidth = window.innerWidth;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
const anchorTop = anchorRect.top + scrollTop;
const anchorBottom = anchorRect.bottom + scrollTop;
let tooltipTop = anchorTop - tooltipRect.height - 7;
let tooltipLeft = anchorCenterX - 20;
let isAbove = true;
if (anchorTop < tooltipRect.height + scrollTop + 6) {
tooltipTop = anchorBottom + 9;
isAbove = false;
}
if (tooltipLeft + tooltipRect.width > windowWidth + scrollLeft - 1) {
tooltipLeft = windowWidth + scrollLeft - tooltipRect.width;
}
if (tooltipLeft < scrollLeft) {
tooltipLeft = scrollLeft;
}
this.$tooltip.style.top = `${tooltipTop}px`;
this.$tooltip.style.left = `${tooltipLeft}px`;
const tailLeft = anchorCenterX - tooltipLeft - 6;
this.$tail.style.left = `${tailLeft}px`;
if (isAbove) {
this.$tooltip.classList.remove("unihan-tooltip-below", "unihan-fade-in-up");
this.$tooltip.classList.add("unihan-tooltip-above", "unihan-fade-in-down");
} else {
this.$tooltip.classList.remove("unihan-tooltip-above", "unihan-fade-in-down");
this.$tooltip.classList.add("unihan-tooltip-below", "unihan-fade-in-up");
}
}
}
const { batchConv } = require("ext.gadget.HanAssist");
mw.messages.set(
batchConv({
"unihan-settings": { hans: "设置", hant: "設定" }
})
);
let availableFonts = null;
let settings = getSettings(STORAGE_KEY, DEFAULT_SETTINGS);
const tooltips = /* @__PURE__ */ new Map();
function checkDisabled() {
const ep = mw.util.getParamValue("UTdontload");
if (ep && !isNaN(Number(ep))) {
$.cookie("UTdontload", "1", { path: "/", expires: parseInt(ep) });
}
return $.cookie("UTdontload") === "1";
}
function initOverlay() {
const overlay = document.createElement("div");
overlay.className = CLASSES.OVERLAY;
document.body.appendChild(overlay);
}
async function openSettings() {
try {
await mw.loader.using("ext.gadget.unihan-helper-settings");
const settingsModule = require("ext.gadget.unihan-helper-settings");
if (settingsModule && typeof settingsModule.openDialog === "function") {
let fontLoadError = false;
if (settings.useWebfont && availableFonts === null) {
try {
availableFonts = await fetchFontList();
} catch (error) {
console.error("Failed to fetch font list:", error);
fontLoadError = true;
}
}
settingsModule.openDialog(
availableFonts,
settings,
(newSettings) => {
settings = newSettings;
saveSettings(STORAGE_KEY, settings);
if (settings.enabled && settings.useWebfont) {
clearAppliedFonts();
processUnihanChars(settings.selectedFont, settings.loadMode);
} else {
clearAppliedFonts();
}
},
// 提供字体加载函数
async () => {
if (availableFonts === null) {
availableFonts = await fetchFontList();
}
return availableFonts;
},
// 传递初始加载错误状态
fontLoadError
);
}
} catch (error) {
console.error("Failed to load settings module:", error);
mw.notify("无法加载设置模块", { type: "error" });
}
}
function bindInteractions() {
if (!settings.enabled) {
return;
}
const elements = document.querySelectorAll(`.${CLASSES.INLINE_UNIHAN}`);
elements.forEach((el) => {
const element = el;
const tooltipText = element.getAttribute("title");
if (!tooltipText) return;
element.removeAttribute("title");
const tooltip = new Tooltip(element, tooltipText, openSettings);
tooltips.set(element, tooltip);
let showTimer = null;
let hideTimer = null;
tooltip.clearExternalTimers = () => {
if (showTimer !== null) {
clearTimeout(showTimer);
showTimer = null;
}
if (hideTimer !== null) {
clearTimeout(hideTimer);
hideTimer = null;
}
};
const show = () => {
if (showTimer !== null) {
clearTimeout(showTimer);
showTimer = null;
}
if (hideTimer !== null) {
clearTimeout(hideTimer);
hideTimer = null;
}
tooltip.show();
};
const hide = () => {
if (showTimer !== null) {
clearTimeout(showTimer);
showTimer = null;
}
if (hideTimer !== null) {
clearTimeout(hideTimer);
}
hideTimer = window.setTimeout(() => {
tooltip.hide();
hideTimer = null;
}, TIMINGS.HIDE_DELAY);
};
if (IS_TOUCHSCREEN) {
element.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
tooltips.forEach((t, el2) => {
if (el2 !== element) {
t.hide();
}
});
if (tooltip.isVisible()) {
tooltip.hide();
} else {
show();
}
});
} else {
element.addEventListener("mouseenter", () => {
showTimer = window.setTimeout(() => show(), TIMINGS.HOVER_DELAY);
});
element.addEventListener("mouseleave", hide);
}
});
if (IS_TOUCHSCREEN) {
document.addEventListener("click", (e) => {
const clickedTooltip = e.target.closest(".unihan-tooltip");
const clickedTrigger = e.target.closest(`.${CLASSES.INLINE_UNIHAN}`);
if (!clickedTooltip && !clickedTrigger) {
tooltips.forEach((tooltip) => {
tooltip.hide();
});
}
});
}
}
async function init() {
if (checkDisabled()) {
return;
}
initOverlay();
if (settings.enabled && settings.useWebfont) {
processUnihanChars(settings.selectedFont, settings.loadMode);
}
bindInteractions();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
void init();
}
// </nowiki>