diff --git a/README.md b/README.md index 60b28bf..fa20636 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/). - Release to GitHub automatically(using [release-it](https://github.com/release-it/release-it)); - Extensive skeleton; - Some sample code of UI and lifecycle. -- ⭐Compatibilities for Zotero 6 & Zotero 7. +- ⭐Compatibilities for Zotero 6 & Zotero 7.(using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit)) ## Quick Start Guide @@ -26,6 +26,7 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/). - Modify the settings in `./package.json`, including: ``` + version, author, description, homepage, @@ -38,7 +39,7 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/). > Be careful to set the addonID and addonRef to avoid confliction. -- Run `npm install` to setup the plugin and install dependencies. If you don't have NodeJS installed, please download it [here](https://nodejs.org/en/); +- Run `npm install` to set up the plugin and install dependencies. If you don't have NodeJS installed, please download it [here](https://nodejs.org/en/); - Run `npm run build` to build the plugin. The xpi for installation and the built code is under builds folder. ### Plugin Life Cycle @@ -56,6 +57,8 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/). ### Examples +See https://github.com/windingwind/zotero-plugin-toolkit for more detailed API documentations. + #### Menu (file, edit, view, ...) & Right-click Menu (item, collection/library) **File Menu** @@ -70,61 +73,15 @@ https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062d https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062db51cf16e99dda35288/src/views.ts#L23-L51 -`Utils.UI.insertMenuItem` resolved the input object and inject the menu items. - -Available types `menuFile`, `menuEdit`, ...: - -```ts -defaultMenuPopupSelectors: { - menuFile: "#menu_FilePopup", - menuEdit: "#menu_EditPopup", - menuView: "#menu_viewPopup", - menuGo: "#menu_goPopup", - menuTools: "#menu_ToolsPopup", - menuHelp: "#menu_HelpPopup", - collection: "#zotero-collectionmenu", - item: "#zotero-itemmenu", -}, -``` - -You can choose an anchor element and insert before/after it using `insertPosition` and `anchorElement`. Default the insert position is the end of menu. +`insertMenuItem` resolved the input object and inject the menu items. -```ts -insertMenuItem: ( - menuPopup: XUL.Menupopup | string, - options: MenuitemOptions, - insertPosition?: "before" | "after", - anchorElement?: XUL.Element -) => boolean; -``` - -Full options you can use: - -```ts -declare interface MenuitemOptions { - tag: "menuitem" | "menu" | "menuseparator"; - id?: string; - label?: string; - // data url (chrome://xxx.png) or base64 url (data:image/png;base64,xxx) - icon?: string; - class?: string; - styles?: { [key: string]: string }; - hidden?: boolean; - disabled?: boolean; - oncommand?: string; - commandListener?: EventListenerOrEventListenerObject; - // Attributes below are used when type === "menu" - popupId?: string; - onpopupshowing?: string; - subElementOptions?: Array; -} -``` +You can choose an anchor element and insert before/after it using `insertPosition` and `anchorElement`. Default the insert position is the end of the menu. #### Preference, for both Zotero 6 and Zotero 7 (all in bootstrap) Zotero 6 doesn't support preference pane injection in bootstrap mode, thus I write a register for Zotero 6 or lower. -You only need to maintain one `preferences.xhtml` which runs natively on Zotero 7 and let the plugin template handle when it is running on Zotero 6. +You only need to maintain one `preferences.xhtml` which runs natively on Zotero 7 and let the plugin template handle it when it is running on Zotero 6. @@ -141,9 +98,9 @@ You only need to maintain one `preferences.xhtml` which runs natively on Zotero https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062db51cf16e99dda35288/src/views.ts#L63-L82 -Call `Utils.Compat.registerPrefPane` when it's on Zotero 6. +Call `registerPrefPane` when it's on Zotero 6. -Note that `` element is deprecated. Please use the full pref-key in elements' `preference` attribute. Like: +Note that `` element is deprecated. Please use the full pref-key in the elements' `preference` attribute. Like: ```xml ` element is deprecated. Please use the full pref-key in The elements with `preference` attributes will bind to Zotero preferences. -Remember to call `Utils.Compat.unregisterPrefPane()` on plugin unload. +Remember to call `unregisterPrefPane()` on plugin unload. https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062db51cf16e99dda35288/src/views.ts#L88-L90 @@ -160,55 +117,10 @@ https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062d The plugin template provides new APIs for bootstrap plugins. We have two reasons to use these APIs, instead of the `createElement/createElementNS`: -- In bootstrap mode, plugins have to clean up all UI elements on exit (disable or uninstall), which is very annoying. Using the `Utils.UI.createElement`, the plugin template will maintain these elements. Just `Utils.UI.removeAddonElements` on exit. -- Zotero 7 requires createElement()/createElementNS() → createXULElement() for remaining XUL elements, while on Zotero 6 doesn't support `createXULElement`. Using `Utils.UI.createElement`, it switches API depending on the current platform automatically. - -Definition: +- In bootstrap mode, plugins have to clean up all UI elements on exit (disable or uninstall), which is very annoying. Using the `createElement`, the plugin template will maintain these elements. Just `removeAddonElements` on exit. +- Zotero 7 requires createElement()/createElementNS() → createXULElement() for remaining XUL elements, while Zotero 6 doesn't support `createXULElement`. Using `createElement`, it switches API depending on the current platform automatically. -```ts -function createElement ( - doc: Document, - tagName: string, - namespace: "html" | "svg" | "xul" -) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement; -``` - -There are more advanced APIs for creating elements in batch: `Utils.UI.creatElementsFromJSON`. Input an element tree in JSON and return a fragment/element. These elements are also maintained by this plugin template. - -Definition: - -```ts -function creatElementsFromJSON ( - doc: Document, - options: ElementOptions -) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement; -``` - -Available options: - -```ts -declare interface ElementOptions { - tag: string; - id?: string; - namespace?: "html" | "svg" | "xul"; - styles?: { [key: string]: string }; - directAttributes?: { [key: string]: string | boolean | number }; - attributes?: { [key: string]: string | boolean | number }; - listeners?: Array< - | [ - string, - EventListenerOrEventListenerObject, - boolean | AddEventListenerOptions - ] - | [string, EventListenerOrEventListenerObject] - >; - checkExistanceParent?: HTMLElement; - ignoreIfExists?: boolean; - removeIfExists?: boolean; - customCheck?: () => boolean; - subElementOptions?: Array; -} -``` +There are more advanced APIs for creating elements in batch: `creatElementsFromJSON`. Input an element tree in JSON and return a fragment/element. These elements are also maintained by this plugin template. ### Directory Structure @@ -263,7 +175,6 @@ This section shows the directory structure of a template. │ module.ts # module class │ addon.ts # base class │ events.ts # events class - │ utils.ts # Utils class │ views.ts # UI class └─ prefs.ts # preferences class diff --git a/package.json b/package.json index 3148f07..50f092a 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,14 @@ "releasepage": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi", "updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/master/update.json", "dependencies": { - "compressing": "^1.5.1", - "esbuild": "^0.15.16", - "replace-in-file": "^6.3.2" + "zotero-plugin-toolkit": "^0.0.1" }, "devDependencies": { "@types/node": "^18.7.20", + "compressing": "^1.5.1", + "esbuild": "^0.15.16", + "replace-in-file": "^6.3.2", "release-it": "^14.14.0", "zotero-types": "^0.0.8" } -} +} \ No newline at end of file diff --git a/src/addon.ts b/src/addon.ts index bcc0ff3..3e04f60 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -1,22 +1,23 @@ import AddonEvents from "./events"; import AddonPrefs from "./prefs"; -import AddonUtils from "./utils"; import AddonViews from "./views"; +import ZoteroToolkit from "zotero-plugin-toolkit"; + class Addon { - public Zotero: _ZoteroConstructable; + public Zotero!: _ZoteroConstructable; public events: AddonEvents; public views: AddonViews; public prefs: AddonPrefs; - public Utils: AddonUtils; + public toolkit: ZoteroToolkit; // root path to access the resources - public rootURI: string; + public rootURI!: string; constructor() { this.events = new AddonEvents(this); this.views = new AddonViews(this); this.prefs = new AddonPrefs(this); - this.Utils = new AddonUtils(this); + this.toolkit = new ZoteroToolkit(); } } diff --git a/src/events.ts b/src/events.ts index 11f0563..4e5abf5 100644 --- a/src/events.ts +++ b/src/events.ts @@ -11,7 +11,7 @@ class AddonEvents extends AddonModule { event: string, type: string, ids: Array, - extraData: object + extraData: { [key: string]: any } ) => { // You can add your code to the corresponding notify type if ( @@ -33,7 +33,7 @@ class AddonEvents extends AddonModule { // @ts-ignore this._Addon.rootURI = rootURI; // This function is the setup code of the addon - this._Addon.Utils.Tool.log(`${addonName}: init called`); + this._Addon.toolkit.Tool.log(`${addonName}: init called`); // Register the callback in Zotero as an item observer let notifierID = Zotero.Notifier.registerObserver(this.notifierCallback, [ @@ -45,7 +45,7 @@ class AddonEvents extends AddonModule { // Unregister callback when the window closes (important to avoid a memory leak) Zotero.getMainWindow().addEventListener( "unload", - function (e) { + function (e: Event) { Zotero.Notifier.unregisterObserver(notifierID); }, false @@ -57,7 +57,7 @@ class AddonEvents extends AddonModule { } public initPrefs() { - this._Addon.Utils.Tool.log(this._Addon.rootURI); + this._Addon.toolkit.Tool.log(this._Addon.rootURI); const prefOptions = { pluginID: addonID, src: this._Addon.rootURI + "chrome/content/preferences.xhtml", @@ -69,22 +69,22 @@ class AddonEvents extends AddonModule { this._Addon.prefs.initPreferences(win); }, }; - if (this._Addon.Utils.Compat.isZotero7()) { + if (this._Addon.toolkit.Compat.isZotero7()) { Zotero.PreferencePanes.register(prefOptions); } else { - this._Addon.Utils.Compat.registerPrefPane(prefOptions); + this._Addon.toolkit.Compat.registerPrefPane(prefOptions); } } private unInitPrefs() { - if (!this._Addon.Utils.Compat.isZotero7()) { - this._Addon.Utils.Compat.unregisterPrefPane(); + if (!this._Addon.toolkit.Compat.isZotero7()) { + this._Addon.toolkit.Compat.unregisterPrefPane(); } } public onUnInit(): void { const Zotero = this._Addon.Zotero; - this._Addon.Utils.Tool.log(`${addonName}: uninit called`); + this._Addon.toolkit.Tool.log(`${addonName}: uninit called`); this.unInitPrefs(); // Remove elements and do clean up this._Addon.views.unInitViews(); diff --git a/src/prefs.ts b/src/prefs.ts index 82ad941..19a53d3 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -3,7 +3,7 @@ import AddonModule from "./module"; import { addonName, addonRef } from "../package.json"; class AddonPrefs extends AddonModule { - private _window: Window; + private _window!: Window; constructor(parent: Addon) { super(parent); } @@ -11,7 +11,7 @@ class AddonPrefs extends AddonModule { // This function is called when the prefs window is opened // See addon/chrome/content/preferences.xul onpaneload this._window = _window; - this._Addon.Utils.Tool.log(`${addonName}: init preferences`); + this._Addon.toolkit.Tool.log(`${addonName}: init preferences`); this.updatePrefsUI(); this.bindPrefEvents(); } @@ -20,14 +20,14 @@ class AddonPrefs extends AddonModule { // You can initialize some UI elements on prefs window // with this._window.document // Or bind some events to the elements - this._Addon.Utils.Tool.log(`${addonName}: init preferences UI`); + this._Addon.toolkit.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._Addon.toolkit.Tool.log(e); this._window.alert( `Successfully changed to ${(e.target as XUL.Checkbox).checked}!` ); @@ -36,7 +36,7 @@ class AddonPrefs extends AddonModule { this._window.document .querySelector(`#zotero-prefpane-${addonRef}-input`) ?.addEventListener("change", (e) => { - this._Addon.Utils.Tool.log(e); + this._Addon.toolkit.Tool.log(e); this._window.alert( `Successfully changed to ${(e.target as HTMLInputElement).value}!` ); diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index d4b5149..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,619 +0,0 @@ -import Addon from "./addon"; -import AddonModule from "./module"; - -class AddonUtils extends AddonModule { - public Compat: ZoteroCompat; - public Tool: ZoteroTool; - public UI: ZoteroUI; - - constructor(parent: Addon) { - super(parent); - this.Compat = { - // Get Zotero instance - getZotero: () => { - if (typeof Zotero === "undefined") { - return Components.classes["@zotero.org/Zotero;1"].getService( - Components.interfaces.nsISupports - ).wrappedJSObject; - } - 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 - 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); - } - }, - 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()) { - // @ts-ignore - return doc.createXULElement(type); - } else { - return doc.createElementNS( - "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", - type - ) 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 _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 - .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-${Zotero.Utilities.randomString()}-${new Date().getTime()}`); - const contenrOrXHR = await Zotero.File.getContentsAsync( - options.src - ); - const content = - typeof contenrOrXHR === "string" - ? contenrOrXHR - : (contenrOrXHR as any as XMLHttpRequest).response; - const src = ` - ${content} - `; - 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); - // Binding preferences - _initImportedNodesPostInsert(prefPane); - if (options.onload) { - options.onload(win); - } - } - }, - false - ); - }, - }; - Services.wm.addListener(windowListener); - }, - unregisterPrefPane: () => { - this.Compat.prefPaneCache.listeners.forEach((l) => { - Services.wm.removeListener(l); - l.onOpenWindow = undefined; - }); - 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(), - openFilePicker: ( - title: string, - mode: "open" | "save" | "folder", - filters?: [string, string][], - suggestion?: string - ) => { - const fp = Components.classes[ - "@mozilla.org/filepicker;1" - ].createInstance(Components.interfaces.nsIFilePicker); - - if (suggestion) fp.defaultString = suggestion; - - mode = { - open: Components.interfaces.nsIFilePicker.modeOpen, - save: Components.interfaces.nsIFilePicker.modeSave, - folder: Components.interfaces.nsIFilePicker.modeGetFolder, - }[mode]; - - fp.init(window, title, mode); - - for (const [label, ext] of filters || []) { - fp.appendFilter(label, ext); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return new Promise((resolve) => { - fp.open((userChoice) => { - switch (userChoice) { - case Components.interfaces.nsIFilePicker.returnOK: - case Components.interfaces.nsIFilePicker.returnReplace: - resolve(fp.file.path); - break; - - default: // aka returnCancel - resolve(""); - break; - } - }); - }); - }, - log: (...data: any[]) => { - try { - this._Addon.Zotero.getMainWindow().console.log(...data); - for (const d of data) { - this._Addon.Zotero.debug(d); - } - } catch (e) { - this._Addon.Zotero.debug(e); - } - }, - }; - this.UI = { - addonElements: [], - createElement: ( - doc: Document, - tagName: string, - namespace: "html" | "svg" | "xul" = "html" - ) => { - namespace = namespace || "html"; - const namespaces = { - html: "http://www.w3.org/1999/xhtml", - svg: "http://www.w3.org/2000/svg", - }; - if (tagName === "fragment") { - return doc.createDocumentFragment(); - } else if (namespace === "xul") { - const e = this.Compat.createXULElement(doc, tagName); - this.UI.addonElements.push(e); - return e; - } else { - const e = doc.createElementNS(namespaces[namespace], tagName) as - | HTMLElement - | SVGAElement; - this.UI.addonElements.push(e); - return e; - } - }, - removeAddonElements: () => { - this.UI.addonElements.forEach((e) => { - try { - e?.remove(); - } catch (e) { - this._Addon.Utils.Tool.log(e); - } - }); - }, - creatElementsFromJSON: (doc: Document, options: ElementOptions) => { - this.Tool.log(options); - if ( - options.id && - (options.checkExistanceParent - ? options.checkExistanceParent - : doc - ).querySelector(`#${options.id}`) - ) { - if (options.ignoreIfExists) { - return undefined; - } - if (options.removeIfExists) { - doc.querySelector(`#${options.id}`).remove(); - } - } - if (options.customCheck && !options.customCheck()) { - return undefined; - } - const element = this.UI.createElement( - doc, - options.tag, - options.namespace - ); - - let _DocumentFragment: typeof DocumentFragment; - if (typeof DocumentFragment === "undefined") { - _DocumentFragment = (doc as any).ownerGlobal.DocumentFragment; - } else { - _DocumentFragment = DocumentFragment; - } - if (!(element instanceof _DocumentFragment)) { - if (options.id) { - element.id = options.id; - } - if (options.styles && Object.keys(options.styles).length) { - Object.keys(options.styles).forEach((k) => { - const v = options.styles[k]; - typeof v !== "undefined" && (element.style[k] = v); - }); - } - if ( - options.directAttributes && - Object.keys(options.directAttributes).length - ) { - Object.keys(options.directAttributes).forEach((k) => { - const v = options.directAttributes[k]; - typeof v !== "undefined" && (element[k] = v); - }); - } - if (options.attributes && Object.keys(options.attributes).length) { - Object.keys(options.attributes).forEach((k) => { - const v = options.attributes[k]; - typeof v !== "undefined" && element.setAttribute(k, String(v)); - }); - } - if (options.listeners?.length) { - options.listeners.forEach(([type, cbk, option]) => { - typeof cbk !== "undefined" && - element.addEventListener(type, cbk, option); - }); - } - } - - if (options.subElementOptions?.length) { - const subElements = options.subElementOptions - .map((_options) => this.UI.creatElementsFromJSON(doc, _options)) - .filter((e) => e); - element.append(...subElements); - } - return element; - }, - defaultMenuPopupSelectors: { - menuFile: "#menu_FilePopup", - menuEdit: "#menu_EditPopup", - menuView: "#menu_viewPopup", - menuGo: "#menu_goPopup", - menuTools: "#menu_ToolsPopup", - menuHelp: "#menu_HelpPopup", - collection: "#zotero-collectionmenu", - item: "#zotero-itemmenu", - }, - insertMenuItem: ( - menuPopup: XUL.Menupopup | string, - options: MenuitemOptions, - insertPosition: "before" | "after" = "after", - anchorElement: XUL.Element = undefined - ) => { - const Zotero = this.Compat.getZotero(); - let popup: XUL.Menupopup; - if (typeof menuPopup === "string") { - if ( - !Object.keys(this.UI.defaultMenuPopupSelectors).includes(menuPopup) - ) { - return false; - } else { - popup = (Zotero.getMainWindow() as Window).document.querySelector( - this.UI.defaultMenuPopupSelectors[menuPopup] - ); - } - } else { - popup = menuPopup; - } - if (!popup) { - return false; - } - const doc: Document = popup.ownerDocument; - const generateElementOptions = ( - menuitemOption: MenuitemOptions - ): ElementOptions => { - let elementOption: ElementOptions = { - tag: menuitemOption.tag, - id: menuitemOption.id, - namespace: "xul", - attributes: { - label: menuitemOption.label, - hidden: Boolean(menuitemOption.hidden), - disaled: Boolean(menuitemOption.disabled), - class: menuitemOption.class || "", - oncommand: menuitemOption.oncommand, - }, - styles: menuitemOption.styles || {}, - listeners: [["command", menuitemOption.commandListener]], - subElementOptions: [], - }; - if (menuitemOption.icon) { - elementOption.attributes["class"] += " menuitem-iconic"; - elementOption.styles[ - "list-style-image" - ] = `url(${menuitemOption.icon})`; - } - if (menuitemOption.tag === "menu") { - elementOption.subElementOptions.push({ - tag: "menupopup", - id: menuitemOption.popupId, - namespace: "xul", - attributes: { onpopupshowing: menuitemOption.onpopupshowing }, - subElementOptions: menuitemOption.subElementOptions.map( - generateElementOptions - ), - }); - } - return elementOption; - }; - const menuItem = this.UI.creatElementsFromJSON( - doc, - generateElementOptions(options) - ); - if (!anchorElement) { - anchorElement = ( - insertPosition === "after" - ? popup.lastElementChild - : popup.firstElementChild - ) as XUL.Element; - } - anchorElement[insertPosition](menuItem); - }, - }; - } -} - -class CopyHelper { - private transferable: any; - private clipboardService: any; - - constructor() { - this.transferable = Components.classes[ - "@mozilla.org/widget/transferable;1" - ].createInstance(Components.interfaces.nsITransferable); - this.clipboardService = Components.classes[ - "@mozilla.org/widget/clipboard;1" - ].getService(Components.interfaces.nsIClipboard); - this.transferable.init(null); - } - - public addText(source: string, type: "text/html" | "text/unicode") { - const str = Components.classes[ - "@mozilla.org/supports-string;1" - ].createInstance(Components.interfaces.nsISupportsString); - str.data = source; - this.transferable.addDataFlavor(type); - this.transferable.setTransferData(type, str, source.length * 2); - return this; - } - - public addImage(source: string) { - let parts = source.split(","); - if (!parts[0].includes("base64")) { - return; - } - let mime = parts[0].match(/:(.*?);/)[1]; - let bstr = atob(parts[1]); - let n = bstr.length; - let u8arr = new Uint8Array(n); - while (n--) { - u8arr[n] = bstr.charCodeAt(n); - } - let imgTools = Components.classes["@mozilla.org/image/tools;1"].getService( - Components.interfaces.imgITools - ); - let imgPtr = Components.classes[ - "@mozilla.org/supports-interface-pointer;1" - ].createInstance(Components.interfaces.nsISupportsInterfacePointer); - imgPtr.data = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mime); - this.transferable.addDataFlavor(mime); - this.transferable.setTransferData(mime, imgPtr, 0); - return this; - } - - public copy() { - this.clipboardService.setData( - this.transferable, - null, - Components.interfaces.nsIClipboard.kGlobalClipboard - ); - } -} - -export default AddonUtils; diff --git a/src/views.ts b/src/views.ts index 8fac5d6..7c2f546 100644 --- a/src/views.ts +++ b/src/views.ts @@ -4,7 +4,7 @@ const { addonRef } = require("../package.json"); class AddonViews extends AddonModule { // You can store some element in the object attributes - private progressWindowIcon: object; + private progressWindowIcon: { [key: string]: string }; constructor(parent: Addon) { super(parent); @@ -18,10 +18,10 @@ class AddonViews extends AddonModule { public initViews() { // You can init the UI elements that // cannot be initialized with overlay.xul - this._Addon.Utils.Tool.log("Initializing UI"); + this._Addon.toolkit.Tool.log("Initializing UI"); const menuIcon = "chrome://addontemplate/content/icons/favicon@0.5x.png"; // item menuitem with icon - this._Addon.Utils.UI.insertMenuItem("item", { + this._Addon.toolkit.UI.insertMenuItem("item", { tag: "menuitem", id: "zotero-itemmenu-addontemplate-test", label: "Addon Template: Menuitem", @@ -29,7 +29,7 @@ class AddonViews extends AddonModule { icon: menuIcon, }); // item menupopup with sub-menuitems - this._Addon.Utils.UI.insertMenuItem( + this._Addon.toolkit.UI.insertMenuItem( "item", { tag: "menu", @@ -47,11 +47,11 @@ class AddonViews extends AddonModule { "#zotero-itemmenu-addontemplate-test" ) ); - this._Addon.Utils.UI.insertMenuItem("menuFile", { + this._Addon.toolkit.UI.insertMenuItem("menuFile", { tag: "menuseparator", }); // menu->File menuitem - this._Addon.Utils.UI.insertMenuItem("menuFile", { + this._Addon.toolkit.UI.insertMenuItem("menuFile", { tag: "menuitem", label: "Addon Template: File Menuitem", oncommand: "alert('Hello World! File Menuitem.')", @@ -59,8 +59,8 @@ class AddonViews extends AddonModule { } public unInitViews() { - this._Addon.Utils.Tool.log("Uninitializing UI"); - this._Addon.Utils.UI.removeAddonElements(); + this._Addon.toolkit.Tool.log("Uninitializing UI"); + this._Addon.toolkit.UI.removeAddonElements(); } public showProgressWindow( diff --git a/tsconfig.json b/tsconfig.json index cf12fa8..46480e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,9 @@ { "compilerOptions": { "module": "commonjs", - "target": "es6", - "resolveJsonModule": true + "target": "ES2016", + "resolveJsonModule": true, + "strict": true }, "include": [ "src", diff --git a/typing/global.d.ts b/typing/global.d.ts index 704e44e..e69de29 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -1,110 +0,0 @@ -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, - entities: string[], - defaultXUL?: boolean - ) => DocumentFragment; - prefPaneCache: { win: Window; listeners: any[]; ids: string[] }; - registerPrefPane: (options: PrefPaneOptions) => void; - unregisterPrefPane: () => void; -} - -declare interface ZoteroTool { - getCopyHelper: () => CopyHelper; - openFilePicker: ( - title: string, - mode: "open" | "save" | "folder", - filters?: [string, string][], - suggestion?: string - ) => Promise; - log: (...data: any[]) => void; -} - -declare interface ZoteroUI { - addonElements: Element[]; - createElement: ( - doc: Document, - tagName: string, - namespace?: "html" | "svg" | "xul" - ) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement; - removeAddonElements: () => void; - creatElementsFromJSON: ( - doc: Document, - options: ElementOptions - ) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement; - defaultMenuPopupSelectors: { - [key: string]: string; - }; - insertMenuItem: ( - menuPopup: XUL.Menupopup | string, - options: MenuitemOptions, - insertPosition?: "before" | "after", - anchorElement?: XUL.Element - ) => boolean; -} - -declare interface ElementOptions { - tag: string; - id?: string; - namespace?: "html" | "svg" | "xul"; - styles?: { [key: string]: string }; - directAttributes?: { [key: string]: string | boolean | number }; - attributes?: { [key: string]: string | boolean | number }; - listeners?: Array< - | [ - string, - EventListenerOrEventListenerObject, - boolean | AddEventListenerOptions - ] - | [string, EventListenerOrEventListenerObject] - >; - checkExistanceParent?: HTMLElement; - ignoreIfExists?: boolean; - removeIfExists?: boolean; - customCheck?: () => boolean; - subElementOptions?: Array; -} - -declare interface MenuitemOptions { - tag: "menuitem" | "menu" | "menuseparator"; - id?: string; - label?: string; - // data url (chrome://xxx.png) or base64 url (data:image/png;base64,xxx) - icon?: string; - class?: string; - styles?: { [key: string]: string }; - hidden?: boolean; - disabled?: boolean; - oncommand?: string; - commandListener?: EventListenerOrEventListenerObject; - // Attributes below are used when type === "menu" - popupId?: string; - onpopupshowing?: string; - 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; - copy: () => void; -}