From 1ab8af4ad24257903b53019e24a2f0439d8facdf Mon Sep 17 00:00:00 2001 From: xiangyu <3170102889@zju.edu.cn> Date: Fri, 16 Dec 2022 18:11:47 +0800 Subject: [PATCH] add: preference compatibility for Zotero 6 & 7 --- addon/bootstrap.js | 21 +-- addon/chrome.manifest | 1 - .../icons}/favicon.png | Bin .../icons}/favicon@0.5x.png | Bin addon/chrome/content/preferences.xhtml | 10 +- addon/chrome/locale/en-US/overlay.dtd | 1 + addon/chrome/locale/zh-CN/overlay.dtd | 1 + addon/install.rdf | 2 +- addon/manifest.json | 4 +- src/events.ts | 7 +- src/index.ts | 7 +- src/prefs.ts | 27 +++- src/utils.ts | 126 +++++++++++++++++- src/views.ts | 28 +++- typing/global.d.ts | 22 +++ 15 files changed, 220 insertions(+), 37 deletions(-) rename addon/chrome/{skin/default/addontemplate => content/icons}/favicon.png (100%) rename addon/chrome/{skin/default/addontemplate => content/icons}/favicon@0.5x.png (100%) diff --git a/addon/bootstrap.js b/addon/bootstrap.js index 39826cf..d3c07b0 100644 --- a/addon/bootstrap.js +++ b/addon/bootstrap.js @@ -69,11 +69,12 @@ async function startup({ id, version, resourceURI, rootURI }, reason) { rootURI = resourceURI.spec; } - Services.scriptloader.loadSubScript( - `${rootURI}/chrome/content/scripts/index.js` - ); + const ctx = { Zotero, rootURI }; - Zotero.AddonTemplate.rootURI = rootURI; + Services.scriptloader.loadSubScript( + `${rootURI}/chrome/content/scripts/index.js`, + ctx + ); if (Zotero.platformMajorVersion >= 102) { var aomStartup = Components.classes[ @@ -86,12 +87,12 @@ async function startup({ id, version, resourceURI, rootURI }, reason) { ["locale", "__addonRef__", "zh-CN", rootURI + "chrome/locale/zh-CN/"], ]); - Zotero.PreferencePanes.register({ - pluginID: "__addonID__", - src: rootURI + "chrome/content/preferences.xhtml", - extraDTD: [`chrome://__addonRef__/locale/overlay.dtd`], - defaultXUL: true, - }); + // Zotero.PreferencePanes.register({ + // pluginID: "__addonID__", + // src: rootURI + "chrome/content/preferences.xhtml", + // extraDTD: ["chrome://__addonRef__/locale/overlay.dtd"], + // defaultXUL: true, + // }); } } diff --git a/addon/chrome.manifest b/addon/chrome.manifest index 6c33bcd..713e1b0 100644 --- a/addon/chrome.manifest +++ b/addon/chrome.manifest @@ -1,4 +1,3 @@ content __addonRef__ chrome/content/ -skin __addonRef__ default chrome/skin/default/__addonRef__/ locale __addonRef__ en-US chrome/locale/en-US/ locale __addonRef__ zh-CN chrome/locale/zh-CN/ diff --git a/addon/chrome/skin/default/addontemplate/favicon.png b/addon/chrome/content/icons/favicon.png similarity index 100% rename from addon/chrome/skin/default/addontemplate/favicon.png rename to addon/chrome/content/icons/favicon.png diff --git a/addon/chrome/skin/default/addontemplate/favicon@0.5x.png b/addon/chrome/content/icons/favicon@0.5x.png similarity index 100% rename from addon/chrome/skin/default/addontemplate/favicon@0.5x.png rename to addon/chrome/content/icons/favicon@0.5x.png diff --git a/addon/chrome/content/preferences.xhtml b/addon/chrome/content/preferences.xhtml index 60607f3..ad73f90 100644 --- a/addon/chrome/content/preferences.xhtml +++ b/addon/chrome/content/preferences.xhtml @@ -6,8 +6,16 @@ + + + &zotero.__addonRef__.pref.input.label; + diff --git a/addon/chrome/locale/en-US/overlay.dtd b/addon/chrome/locale/en-US/overlay.dtd index 319c425..beffe6c 100644 --- a/addon/chrome/locale/en-US/overlay.dtd +++ b/addon/chrome/locale/en-US/overlay.dtd @@ -1,5 +1,6 @@ + \ No newline at end of file diff --git a/addon/chrome/locale/zh-CN/overlay.dtd b/addon/chrome/locale/zh-CN/overlay.dtd index 8417667..8d0a76f 100644 --- a/addon/chrome/locale/zh-CN/overlay.dtd +++ b/addon/chrome/locale/zh-CN/overlay.dtd @@ -1,5 +1,6 @@ + \ No newline at end of file diff --git a/addon/install.rdf b/addon/install.rdf index 15fd8bf..77666f5 100644 --- a/addon/install.rdf +++ b/addon/install.rdf @@ -12,7 +12,7 @@ em:creator="__author__" em:description="__description__" em:homepageURL="__homepage__" - em:iconURL="chrome://__addonRef__/skin/favicon.png" + em:iconURL="chrome://__addonRef__/content/icons/favicon.png" em:optionsURL="chrome://__addonRef__/content/preferences.xul" em:updateURL="__updaterdf__" em:multiprocessCompatible="true" diff --git a/addon/manifest.json b/addon/manifest.json index c7bc69b..029bf11 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -5,8 +5,8 @@ "description": "__description__", "author": "__author__", "icons": { - "48": "chrome/skin/default/__addonRef__/favicon@0.5x.png", - "96": "chrome/skin/default/__addonRef__/favicon.png" + "48": "chrome/content/icons/favicon@0.5x.png", + "96": "chrome/content/icons/favicon.png" }, "applications": { "zotero": { diff --git a/src/events.ts b/src/events.ts index 441ad37..52fd8eb 100644 --- a/src/events.ts +++ b/src/events.ts @@ -28,10 +28,11 @@ class AddonEvents extends AddonModule { }; } - public async onInit(_Zotero: _ZoteroConstructable) { + public async onInit(_Zotero: _ZoteroConstructable, rootURI) { this._Addon.Zotero = _Zotero; + this._Addon.rootURI = rootURI; // This function is the setup code of the addon - Zotero.debug(`${addonName}: init called`); + this._Addon.Utils.Tool.log(`${addonName}: init called`); // alert(112233); // Reset prefs @@ -72,7 +73,7 @@ class AddonEvents extends AddonModule { public onUnInit(): void { const Zotero = this._Addon.Zotero; - Zotero.debug(`${addonName}: uninit called`); + this._Addon.Utils.Tool.log(`${addonName}: uninit called`); // Remove elements and do clean up this._Addon.views.unInitViews(); // Remove addon object diff --git a/src/index.ts b/src/index.ts index 845bb14..a58cf92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,7 @@ import Addon from "./addon"; -const Zotero = Components.classes["@zotero.org/Zotero;1"].getService( - Components.interfaces.nsISupports -).wrappedJSObject; - if (!Zotero.AddonTemplate) { Zotero.AddonTemplate = new Addon(); - Zotero.AddonTemplate.events.onInit(Zotero); + // @ts-ignore + Zotero.AddonTemplate.events.onInit(Zotero, rootURI); } diff --git a/src/prefs.ts b/src/prefs.ts index 4f10cfc..82ad941 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -1,6 +1,6 @@ import Addon from "./addon"; import AddonModule from "./module"; -import { addonName } from "../package.json"; +import { addonName, addonRef } from "../package.json"; class AddonPrefs extends AddonModule { private _window: Window; @@ -11,15 +11,36 @@ class AddonPrefs extends AddonModule { // This function is called when the prefs window is opened // See addon/chrome/content/preferences.xul onpaneload this._window = _window; - Zotero.debug(`${addonName}: init preferences`); + this._Addon.Utils.Tool.log(`${addonName}: init preferences`); this.updatePrefsUI(); + this.bindPrefEvents(); } private updatePrefsUI() { // You can initialize some UI elements on prefs window // with this._window.document // Or bind some events to the elements - Zotero.debug(`${addonName}: init preferences UI`); + this._Addon.Utils.Tool.log(`${addonName}: init preferences UI`); + } + + private bindPrefEvents() { + this._window.document + .querySelector(`#zotero-prefpane-${addonRef}-enable`) + ?.addEventListener("command", (e) => { + this._Addon.Utils.Tool.log(e); + this._window.alert( + `Successfully changed to ${(e.target as XUL.Checkbox).checked}!` + ); + }); + + this._window.document + .querySelector(`#zotero-prefpane-${addonRef}-input`) + ?.addEventListener("change", (e) => { + this._Addon.Utils.Tool.log(e); + this._window.alert( + `Successfully changed to ${(e.target as HTMLInputElement).value}!` + ); + }); } } diff --git a/src/utils.ts b/src/utils.ts index 9de9d8a..e7304c0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,12 +21,19 @@ class AddonUtils extends AddonModule { // Check if it's running on Zotero 7 (Firefox 102) isZotero7: () => Zotero.platformMajorVersion >= 102, // Firefox 102 support DOMParser natively - getDOMParser: () => - this.Compat.isZotero7() - ? new DOMParser() - : Components.classes[ - "@mozilla.org/xmlextras/domparser;1" - ].createInstance(Components.interfaces.nsIDOMParser), + getDOMParser: () => { + if (this.Compat.isZotero7()) { + return new DOMParser(); + } + try { + return new (this.Compat.getZotero().getMainWindow().DOMParser)(); + } catch (e) { + return Components.classes[ + "@mozilla.org/xmlextras/domparser;1" + ].createInstance(Components.interfaces.nsIDOMParser); + } + }, + // create XUL element createXULElement: (doc: Document, type: string) => { if (this.Compat.isZotero7()) { @@ -39,6 +46,111 @@ class AddonUtils extends AddonModule { ) as XUL.Element; } }, + parseXHTMLToFragment: ( + str: string, + entities: string[] = [], + defaultXUL = true + ) => { + // Adapted from MozXULElement.parseXULToFragment + + /* eslint-disable indent */ + let parser = this.Compat.getDOMParser(); + // parser.forceEnableXULXBL(); + const xulns = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + const htmlns = "http://www.w3.org/1999/xhtml"; + const wrappedStr = `${ + entities.length + ? ` { + return ( + preamble + + ` %_dtd-${index}; ` + ); + }, + "" + )}]>` + : "" + } + + ${str} + `; + this.Tool.log(wrappedStr, parser); + let doc = parser.parseFromString(wrappedStr, "text/xml"); + /* eslint-enable indent */ + console.log(doc); + + if (doc.documentElement.localName === "parsererror") { + throw new Error("not well-formed XHTML"); + } + + // We use a range here so that we don't access the inner DOM elements from + // JavaScript before they are imported and inserted into a document. + let range = doc.createRange(); + range.selectNodeContents(doc.querySelector("div")); + return range.extractContents(); + }, + prefPaneCache: { win: undefined, listeners: [], ids: [] }, + registerPrefPane: (options: PrefPaneOptions) => { + const windowListener = { + onOpenWindow: (xulWindow) => { + const win: Window = xulWindow + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow); + win.addEventListener( + "load", + async () => { + if ( + win.location.href === + "chrome://zotero/content/preferences/preferences.xul" + ) { + this.Tool.log("registerPrefPane:detected", options); + const Zotero = this.Compat.getZotero(); + options.id || (options.id = `plugin-${new Date().getTime()}`); + const src = ` + ${(await Zotero.File.getContentsAsync(options.src)) as string} + `; + const frag = this.Compat.parseXHTMLToFragment( + src, + options.extraDTD, + options.defaultXUL + ); + this.Tool.log(frag); + const prefWindow = win.document.querySelector("prefwindow"); + prefWindow.appendChild(frag); + const prefPane = win.document.querySelector(`#${options.id}`); + // @ts-ignore + prefWindow.addPane(prefPane); + this.Compat.prefPaneCache.win = win; + this.Compat.prefPaneCache.listeners.push(windowListener); + this.Compat.prefPaneCache.ids.push(options.id); + if (options.onload) { + options.onload(win); + } + } + }, + false + ); + }, + }; + Services.wm.addListener(windowListener); + }, + unregisterPrefPane: () => { + this.Compat.prefPaneCache.listeners.forEach((l) => + Services.wm.removeListener(l) + ); + const win = this.Compat.prefPaneCache.win; + if (win && !win.closed) { + this.Compat.prefPaneCache.ids.forEach((id) => + win.document.querySelector(id)?.remove() + ); + } + }, }; this.Tool = { getCopyHelper: () => new CopyHelper(), @@ -84,7 +196,7 @@ class AddonUtils extends AddonModule { }, log: (...data: any[]) => { try { - this._Addon.Zotero.getMainWindow().console.log(data); + this._Addon.Zotero.getMainWindow().console.log(...data); for (const d of data) { this._Addon.Zotero.debug(d); } diff --git a/src/views.ts b/src/views.ts index a1bc6bf..d8299d8 100644 --- a/src/views.ts +++ b/src/views.ts @@ -11,7 +11,7 @@ class AddonViews extends AddonModule { this.progressWindowIcon = { success: "chrome://zotero/skin/tick.png", fail: "chrome://zotero/skin/cross.png", - default: `chrome://${addonRef}/skin/favicon.png`, + default: `chrome://${addonRef}/content/icons/favicon.png`, }; } @@ -19,9 +19,9 @@ class AddonViews extends AddonModule { const Zotero = this._Addon.Zotero; // You can init the UI elements that // cannot be initialized with overlay.xul - Zotero.debug("Initializing UI"); + this._Addon.Utils.Tool.log("Initializing UI"); const menuIcon = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAAC9klEQVQ4jU1TzW/UZRB+Zub9fexXt7s0YG0RAlTAg6EqjfUkMYYDN2OiJngxXOBEBP0T9KY3LiQe1JgYP4BEw0laBSGgSSVIYhNLw5cuaaXdbnf39/G+73jo/hommcxhMk9m5nkeUlUU4RQkBJ25kwRHv358LmAcEUbKRNxJfRAb8kbIqip1Mw2mn4nOMJ4IVWUAuHE/e3m1748IAyt9bxZXbPDCWHiJGa37qzZcz9SUAsK1e+kJowBtAoAIAEJDVcNAKaCZYweHzjPRyORo8P1fS/apwGBqZiGpzN5JT9UiEkPFLAAa1FCos5Z4fdBG9cf5ZO7geGhOX+y+Hwq1XhwLPv234z8UhjgFTGLBACoFiir828+Xb/etTnVT3TpSZrPw2D7993L+7njdLN5q5Tfnl+3JekRwCpgPLvW/AXAYQD7YQgwTanHUowj2P4eSC+W3Stz7dXtdzjngQCSAV1gAht77obdJAxPQy9VZjywOUFKFEkBeCZNbcWjhUd9+Mde73CwzVKEAiIXghaABw/VyxfSYHD+8y7yqCpQMaSzkmYDMYWhXw7R3NsxnTMgGz1dWgBWAVUgtpKUddfllelwe1SOayz3YKWgo5uy7W739H8+uff7sSHCtHNAp9VAhwAzu9pmDbB+mC/90/Ru/t3R07xb58vpDNxkbqFOloZhzJuz4eTE5GzB1haGdVJmL2zOn2NOUq3dX/XPtxB/aVqE/AFUCGAoOGL3hEi+N1qS7pUwaCdLXJ+JPDAC1HjIc03okuLKS6EPrcX5nnf9slni+m+u+1cTLsanq4pv7GnvbqTaEYBVAJFhnAnzugW0VvpA5vLKW6sXU4dvL9+w7E03+yroNhkLZqNWQ1qohdWohdQwTmAjqVLGnwT/dXnavhUIsBLS6+tZIiW74jR9t6l0BKpIIMF5hYkO4+sCeth67zcBe7VRfmr1rPyobuJUEknv4Qu6F5AGAAVwhAJnHfgWiouEVgVUc8AqphHRzosnXC8aedPD/kPxathRorWcAAAAASUVORK5CYII="; + 'url("chrome://addontemplate/content/icons/favicon@0.5x.png")'; // item menuitem with icon this._Addon.Utils.UI.insertMenuItem("item", { tag: "menuitem", @@ -62,12 +62,32 @@ class AddonViews extends AddonModule { public initPrefs() { const Zotero = this._Addon.Zotero; + this._Addon.Utils.Tool.log(this._Addon.rootURI); + const prefOptions = { + pluginID: addonID, + src: this._Addon.rootURI + "chrome/content/preferences.xhtml", + label: "Template", + image: `chrome://${addonRef}/content/icons/favicon.png`, + extraDTD: [`chrome://${addonRef}/locale/overlay.dtd`], + defaultXUL: true, + onload: (win: Window) => { + this._Addon.prefs.initPreferences(win); + }, + }; + if (this._Addon.Utils.Compat.isZotero7()) { + Zotero.PreferencePanes.register(prefOptions); + } else { + this._Addon.Utils.Compat.registerPrefPane(prefOptions); + } } public unInitViews() { const Zotero = this._Addon.Zotero; - Zotero.debug("Uninitializing UI"); + this._Addon.Utils.Tool.log("Uninitializing UI"); this._Addon.Utils.UI.removeAddonElements(); + if (!this._Addon.Utils.Compat.isZotero7()) { + this._Addon.Utils.Compat.unregisterPrefPane(); + } } public showProgressWindow( diff --git a/typing/global.d.ts b/typing/global.d.ts index 415b7fe..75c2131 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -3,6 +3,14 @@ declare interface ZoteroCompat { isZotero7: () => boolean; getDOMParser: () => DOMParser; createXULElement: (doc: Document, type: string) => XUL.Element; + parseXHTMLToFragment: ( + str: string, + entities: string[], + defaultXUL?: boolean + ) => DocumentFragment; + prefPaneCache: { win: Window; listeners: any[]; ids: string[] }; + registerPrefPane: (options: PrefPaneOptions) => void; + unregisterPrefPane: () => void; } declare interface ZoteroTool { @@ -79,6 +87,20 @@ declare interface MenuitemOptions { subElementOptions?: Array; } +declare interface PrefPaneOptions { + pluginID: string; + src: string; + id?: string; + parent?: string; + label?: string; + image?: string; + extraDTD?: string[]; + scripts?: string[]; + defaultXUL?: boolean; + // Only for Zotero 6 + onload?: (win: Window) => any; +} + declare class CopyHelper { addText: (source: string, type: "text/html" | "text/unicode") => CopyHelper; addImage: (source: string) => CopyHelper;