diff --git a/addon/bootstrap.js b/addon/bootstrap.js index d3c07b0..419c7bf 100644 --- a/addon/bootstrap.js +++ b/addon/bootstrap.js @@ -69,7 +69,15 @@ async function startup({ id, version, resourceURI, rootURI }, reason) { rootURI = resourceURI.spec; } - const ctx = { Zotero, rootURI }; + const window = Zotero.getMainWindow(); + // Global variables for plugin code + const ctx = { + Zotero, + rootURI, + window, + document: window.document, + ZoteroPane: Zotero.getActiveZoteroPane(), + }; Services.scriptloader.loadSubScript( `${rootURI}/chrome/content/scripts/index.js`, @@ -86,13 +94,6 @@ async function startup({ id, version, resourceURI, rootURI }, reason) { ["locale", "__addonRef__", "en-US", rootURI + "chrome/locale/en-US/"], ["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, - // }); } } @@ -113,8 +114,10 @@ function shutdown({ id, version, resourceURI, rootURI }, reason) { Cu.unload(`${rootURI}/chrome/content/scripts/index.js`); - chromeHandle.destruct(); - chromeHandle = null; + if (chromeHandle) { + chromeHandle.destruct(); + chromeHandle = null; + } } function uninstall(data, reason) {} diff --git a/src/events.ts b/src/events.ts index 52fd8eb..11f0563 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,6 +1,6 @@ import Addon from "./addon"; import AddonModule from "./module"; -import { addonName } from "../package.json"; +import { addonName, addonID, addonRef } from "../package.json"; class AddonEvents extends AddonModule { private notifierCallback: any; @@ -28,15 +28,12 @@ class AddonEvents extends AddonModule { }; } - public async onInit(_Zotero: _ZoteroConstructable, rootURI) { - this._Addon.Zotero = _Zotero; + public async onInit() { + this._Addon.Zotero = Zotero; + // @ts-ignore this._Addon.rootURI = rootURI; // This function is the setup code of the addon this._Addon.Utils.Tool.log(`${addonName}: init called`); - // alert(112233); - - // Reset prefs - this.resetState(); // Register the callback in Zotero as an item observer let notifierID = Zotero.Notifier.registerObserver(this.notifierCallback, [ @@ -54,26 +51,41 @@ class AddonEvents extends AddonModule { false ); + // Initialize preference window + this.initPrefs(); this._Addon.views.initViews(); - this._Addon.views.initPrefs(); } - private resetState(): void { - /* - For prefs that could be simply set to a static default value, - Please use addon/defaults/preferences/defaults.js - Reset other preferrences here. - Uncomment to use the example code. - */ - // let testPref = Zotero.Prefs.get("addonTemplate.testPref"); - // if (typeof testPref === "undefined") { - // Zotero.Prefs.set("addonTemplate.testPref", true); - // } + public initPrefs() { + 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); + } + } + + private unInitPrefs() { + if (!this._Addon.Utils.Compat.isZotero7()) { + this._Addon.Utils.Compat.unregisterPrefPane(); + } } public onUnInit(): void { const Zotero = this._Addon.Zotero; this._Addon.Utils.Tool.log(`${addonName}: uninit called`); + this.unInitPrefs(); // Remove elements and do clean up this._Addon.views.unInitViews(); // Remove addon object diff --git a/src/index.ts b/src/index.ts index a58cf92..6c92462 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,17 @@ import Addon from "./addon"; +/** + * Globals: bootstrap.js > ctx + * const ctx = { + Zotero, + rootURI, + window, + document: window.document, + ZoteroPane: Zotero.getActiveZoteroPane(), + }; + */ if (!Zotero.AddonTemplate) { Zotero.AddonTemplate = new Addon(); // @ts-ignore - Zotero.AddonTemplate.events.onInit(Zotero, rootURI); + Zotero.AddonTemplate.events.onInit(); } diff --git a/src/utils.ts b/src/utils.ts index 19bc114..d4b5149 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,6 +18,9 @@ class AddonUtils extends AddonModule { } return Zotero; }, + getWindow: () => { + return this.Compat.getZotero().getMainWindow() as Window; + }, // Check if it's running on Zotero 7 (Firefox 102) isZotero7: () => Zotero.platformMajorVersion >= 102, // Firefox 102 support DOMParser natively @@ -33,7 +36,12 @@ class AddonUtils extends AddonModule { ].createInstance(Components.interfaces.nsIDOMParser); } }, - + isXULElement: (elem: Element) => { + return ( + elem.namespaceURI === + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ); + }, // create XUL element createXULElement: (doc: Document, type: string) => { if (this.Compat.isZotero7()) { @@ -93,6 +101,145 @@ class AddonUtils extends AddonModule { }, prefPaneCache: { win: undefined, listeners: [], ids: [] }, registerPrefPane: (options: PrefPaneOptions) => { + const _initImportedNodesPostInsert = (container) => { + const _observerSymbols = new Map(); + const Zotero = this.Compat.getZotero(); + const window = container.ownerGlobal; + let useChecked = (elem) => + (elem instanceof window.HTMLInputElement && + elem.type == "checkbox") || + elem.tagName == "checkbox"; + + let syncFromPref = (elem, preference) => { + let value = Zotero.Prefs.get(preference, true); + if (useChecked(elem)) { + elem.checked = value; + } else { + elem.value = value; + } + elem.dispatchEvent(new window.Event("syncfrompreference")); + }; + + // We use a single listener function shared between all elements so we can easily detach it later + let syncToPrefOnModify = (event) => { + if (event.currentTarget.getAttribute("preference")) { + let value = useChecked(event.currentTarget) + ? event.currentTarget.checked + : event.currentTarget.value; + Zotero.Prefs.set( + event.currentTarget.getAttribute("preference"), + value, + true + ); + event.currentTarget.dispatchEvent( + new window.Event("synctopreference") + ); + } + }; + + let attachToPreference = (elem, preference) => { + Zotero.debug( + `Attaching <${elem.tagName}> element to ${preference}` + ); + // @ts-ignore + let symbol = Zotero.Prefs.registerObserver( + preference, + () => syncFromPref(elem, preference), + true + ); + _observerSymbols.set(elem, symbol); + }; + + let detachFromPreference = (elem) => { + if (_observerSymbols.has(elem)) { + Zotero.debug( + `Detaching <${elem.tagName}> element from preference` + ); + // @ts-ignore + Zotero.Prefs.unregisterObserver(this._observerSymbols.get(elem)); + _observerSymbols.delete(elem); + } + }; + + // Activate `preference` attributes + for (let elem of container.querySelectorAll("[preference]")) { + let preference = elem.getAttribute("preference"); + if ( + container.querySelector("preferences > preference#" + preference) + ) { + Zotero.warn( + " is deprecated -- `preference` attribute values " + + "should be full preference keys, not IDs" + ); + preference = container + .querySelector("preferences > preference#" + preference) + .getAttribute("name"); + } + + attachToPreference(elem, preference); + + elem.addEventListener( + this.Compat.isXULElement(elem) ? "command" : "input", + syncToPrefOnModify + ); + + // Set timeout before populating the value so the pane can add listeners first + window.setTimeout(() => { + syncFromPref(elem, preference); + }); + } + + new window.MutationObserver((mutations) => { + for (let mutation of mutations) { + if (mutation.type == "attributes") { + let target = mutation.target as Element; + detachFromPreference(target); + if (target.hasAttribute("preference")) { + attachToPreference(target, target.getAttribute("preference")); + target.addEventListener( + this.Compat.isXULElement(target) ? "command" : "input", + syncToPrefOnModify + ); + } + } else if (mutation.type == "childList") { + for (let node of mutation.removedNodes) { + detachFromPreference(node); + } + for (let node of mutation.addedNodes) { + if ( + node.nodeType == Node.ELEMENT_NODE && + (node as Element).hasAttribute("preference") + ) { + attachToPreference( + node, + (node as Element).getAttribute("preference") + ); + node.addEventListener( + this.Compat.isXULElement(node as Element) + ? "command" + : "input", + syncToPrefOnModify + ); + } + } + } + } + }).observe(container, { + childList: true, + subtree: true, + attributeFilter: ["preference"], + }); + + // parseXULToFragment() doesn't convert oncommand attributes into actual + // listeners, so we'll do it here + for (let elem of container.querySelectorAll("[oncommand]")) { + elem.oncommand = elem.getAttribute("oncommand"); + } + + for (let child of container.children) { + child.dispatchEvent(new window.Event("load")); + } + }; const windowListener = { onOpenWindow: (xulWindow) => { const win: Window = xulWindow @@ -107,7 +254,8 @@ class AddonUtils extends AddonModule { ) { this.Tool.log("registerPrefPane:detected", options); const Zotero = this.Compat.getZotero(); - options.id || (options.id = `plugin-${new Date().getTime()}`); + options.id || + (options.id = `plugin-${Zotero.Utilities.randomString()}-${new Date().getTime()}`); const contenrOrXHR = await Zotero.File.getContentsAsync( options.src ); @@ -115,7 +263,7 @@ class AddonUtils extends AddonModule { typeof contenrOrXHR === "string" ? contenrOrXHR : (contenrOrXHR as any as XMLHttpRequest).response; - const src = ` { - 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; 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 75c2131..704e44e 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -1,7 +1,9 @@ declare interface ZoteroCompat { getZotero: () => _ZoteroConstructable; + getWindow: () => Window; isZotero7: () => boolean; getDOMParser: () => DOMParser; + isXULElement: (elem: Element) => boolean; createXULElement: (doc: Document, type: string) => XUL.Element; parseXHTMLToFragment: ( str: string, @@ -29,7 +31,7 @@ declare interface ZoteroUI { createElement: ( doc: Document, tagName: string, - namespace: "html" | "svg" | "xul" + namespace?: "html" | "svg" | "xul" ) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement; removeAddonElements: () => void; creatElementsFromJSON: (