diff --git a/addon/prefs.js b/addon/prefs.js index 9b118ed..be8f47d 100644 --- a/addon/prefs.js +++ b/addon/prefs.js @@ -1,2 +1,3 @@ -pref("extensions.zotero.__addonRef__.enable", true); -pref("extensions.zotero.__addonRef__.input", "This is input"); +/* eslint-disable no-undef */ +pref("__prefsPrefix__.enable", true); +pref("__prefsPrefix__.input", "This is input"); diff --git a/package.json b/package.json index 1226fd6..b918871 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "addonID": "addontemplate@euclpts.com", "addonRef": "addontemplate", "addonInstance": "AddonTemplate", + "prefsPrefix": "extensions.zotero.addontemplate", "releasepage": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi", "updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/bootstrap/update.json" }, @@ -39,26 +40,26 @@ }, "homepage": "https://github.com/windingwind/zotero-addon-template#readme", "dependencies": { - "zotero-plugin-toolkit": "^2.0.1" + "zotero-plugin-toolkit": "^2.1.3" }, "devDependencies": { - "@types/node": "^18.11.17", - "@typescript-eslint/eslint-plugin": "^5.59.1", - "@typescript-eslint/parser": "^5.59.1", - "compressing": "^1.6.3", + "@types/node": "^18.16.5", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", + "compressing": "^1.9.0", "concurrently": "^8.0.1", "cross-env": "^7.0.3", - "esbuild": "^0.17.4", - "eslint": "^8.39.0", + "esbuild": "^0.17.18", + "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", - "minimist": "^1.2.7", + "minimist": "^1.2.8", "prettier": "2.8.8", - "release-it": "^15.6.0", + "release-it": "^15.10.3", "replace-in-file": "^6.3.5", "typescript": "^5.0.4", - "zotero-types": "^1.0.12" + "zotero-types": "^1.0.14" }, "prettier": { "tabWidth": 2 } -} \ No newline at end of file +} diff --git a/src/hooks.ts b/src/hooks.ts index 8e47462..30d642f 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -6,7 +6,7 @@ import { UIExampleFactory, } from "./modules/examples"; import { config } from "../package.json"; -import { getString, initLocale } from "./modules/locale"; +import { getString, initLocale } from "./utils/locale"; import { registerPrefsScripts } from "./modules/preferenceScript"; async function onStartup() { diff --git a/src/modules/examples.ts b/src/modules/examples.ts index 77dc765..c0e457b 100644 --- a/src/modules/examples.ts +++ b/src/modules/examples.ts @@ -1,5 +1,5 @@ import { config } from "../../package.json"; -import { getString } from "./locale"; +import { getString } from "../utils/locale"; function example( target: any, diff --git a/src/modules/preferenceScript.ts b/src/modules/preferenceScript.ts index 292ee77..709d45e 100644 --- a/src/modules/preferenceScript.ts +++ b/src/modules/preferenceScript.ts @@ -1,5 +1,5 @@ import { config } from "../../package.json"; -import { getString } from "./locale"; +import { getString } from "../utils/locale"; export function registerPrefsScripts(_window: Window) { // This function is called when the prefs window is opened diff --git a/src/modules/locale.ts b/src/utils/locale.ts similarity index 85% rename from src/modules/locale.ts rename to src/utils/locale.ts index 09034d8..2b83153 100644 --- a/src/modules/locale.ts +++ b/src/utils/locale.ts @@ -1,5 +1,8 @@ import { config } from "../../package.json"; +/** + * Initialize locale data + */ export function initLocale() { addon.data.locale = { stringBundle: Components.classes["@mozilla.org/intl/stringbundle;1"] @@ -8,6 +11,11 @@ export function initLocale() { }; } +/** + * Get locale string + * @param localString + * @param noReload + */ export function getString(localString: string, noReload = false): string { try { return addon.data.locale?.stringBundle.GetStringFromName(localString); diff --git a/src/utils/prefs.ts b/src/utils/prefs.ts new file mode 100644 index 0000000..0d5b035 --- /dev/null +++ b/src/utils/prefs.ts @@ -0,0 +1,29 @@ +import { config } from "../../package.json"; + +/** + * Get preference value. + * Wrapper of `Zotero.Prefs.get`. + * @param key + */ +export function getPref(key: string) { + return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true); +} + +/** + * Set preference value. + * Wrapper of `Zotero.Prefs.set`. + * @param key + * @param value + */ +export function setPref(key: string, value: string | number | boolean) { + return Zotero.Prefs.set(`${config.prefsPrefix}.${key}`, value, true); +} + +/** + * Clear preference value. + * Wrapper of `Zotero.Prefs.clear`. + * @param key + */ +export function clearPref(key: string) { + return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true); +} diff --git a/src/utils/wait.ts b/src/utils/wait.ts new file mode 100644 index 0000000..604237f --- /dev/null +++ b/src/utils/wait.ts @@ -0,0 +1,49 @@ +/** + * Wait until the condition is `true` or timeout. + * The callback is triggered if condition returns `true`. + * @param condition + * @param callback + * @param interval + * @param timeout + */ +export function waitUntil( + condition: () => boolean, + callback: () => void, + interval = 100, + timeout = 10000 +) { + const start = Date.now(); + const intervalId = ztoolkit.getGlobal("setInterval")(() => { + if (condition()) { + ztoolkit.getGlobal("clearInterval")(intervalId); + callback(); + } else if (Date.now() - start > timeout) { + ztoolkit.getGlobal("clearInterval")(intervalId); + } + }, interval); +} + +/** + * Wait async until the condition is `true` or timeout. + * @param condition + * @param interval + * @param timeout + */ +export function waitUtilAsync( + condition: () => boolean, + interval = 100, + timeout = 10000 +) { + return new Promise((resolve, reject) => { + const start = Date.now(); + const intervalId = ztoolkit.getGlobal("setInterval")(() => { + if (condition()) { + ztoolkit.getGlobal("clearInterval")(intervalId); + resolve(); + } else if (Date.now() - start > timeout) { + ztoolkit.getGlobal("clearInterval")(intervalId); + reject(); + } + }, interval); + }); +} diff --git a/src/utils/window.ts b/src/utils/window.ts new file mode 100644 index 0000000..4b072fa --- /dev/null +++ b/src/utils/window.ts @@ -0,0 +1,66 @@ +import { getString } from "./locale"; + +export { isWindowAlive, localeWindow }; + +/** + * Check if the window is alive. + * Useful to prevent opening duplicate windows. + * @param win + */ +function isWindowAlive(win?: Window) { + return win && !Components.utils.isDeadWrapper(win) && !win.closed; +} + +/** + * Locale the elements in window with the locale-target attribute. + * Useful when the window is created dynamically. + * @example + * In HTML: + * ```html + *
elem.text
+ * ``` + * In `addon/chrome/locale/en-US/addon.properties`: + * ```properties + * elem.text=Hello World + * elem.title=Locale example + * ``` + * In `addon/chrome/locale/zh-CN/addon.properties`: + * ```properties + * elem.text=你好世界 + * elem.title=多语言样例 + * ``` + * After locale: + * + * if locale is "en-US" + * ```html + *
Hello World
+ * ``` + * else if locale is "zh-CN" + * ```html + *
你好世界
+ * ``` + * @param win + */ +function localeWindow(win: Window) { + Array.from(win.document.querySelectorAll("*[locale-target]")).forEach( + (elem) => { + const errorInfo = "Locale Error"; + const locales = elem.getAttribute("locale-target")?.split(","); + locales?.forEach((key) => { + const isProp = key in elem; + try { + const localeString = getString( + (isProp ? (elem as any)[key] : elem.getAttribute(key)).trim() || "" + ); + isProp + ? ((elem as any)[key] = localeString) + : elem.setAttribute(key, localeString); + } catch (error) { + isProp + ? ((elem as any)[key] = errorInfo) + : elem.setAttribute(key, errorInfo); + } + }); + } + ); +}