diff --git a/addon/bootstrap.js b/addon/bootstrap.js
index a22fe5b..d3c07b0 100644
--- a/addon/bootstrap.js
+++ b/addon/bootstrap.js
@@ -3,69 +3,118 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-/* global Components, Services */
-/* global addon, APP_SHUTDOWN */
-const { classes: Cc, utils: Cu } = Components;
+if (typeof Zotero == "undefined") {
+ var Zotero;
+}
-Cu.import("resource://gre/modules/Services.jsm");
+var chromeHandle;
+
+// In Zotero 6, bootstrap methods are called before Zotero is initialized, and using include.js
+// to get the Zotero XPCOM service would risk breaking Zotero startup. Instead, wait for the main
+// Zotero window to open and get the Zotero object from there.
+//
+// In Zotero 7, bootstrap methods are not called until Zotero is initialized, and the 'Zotero' is
+// automatically made available.
+async function waitForZotero() {
+ if (typeof Zotero != "undefined") {
+ await Zotero.initializationPromise;
+ }
+
+ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ var windows = Services.wm.getEnumerator("navigator:browser");
+ var found = false;
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ if (win.Zotero) {
+ Zotero = win.Zotero;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ await new Promise((resolve) => {
+ var listener = {
+ onOpenWindow: function (aWindow) {
+ // Wait for the window to finish loading
+ let domWindow = aWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
+ domWindow.addEventListener(
+ "load",
+ function () {
+ domWindow.removeEventListener("load", arguments.callee, false);
+ if (domWindow.Zotero) {
+ Services.wm.removeListener(listener);
+ Zotero = domWindow.Zotero;
+ resolve();
+ }
+ },
+ false
+ );
+ },
+ };
+ Services.wm.addListener(listener);
+ });
+ }
+ await Zotero.initializationPromise;
+}
function install(data, reason) {}
-function startup(data, reason) {
- // Load the addon to Zotero if window is ready
- const loadAddon = (window) => {
- console.log(window);
- if (window.document.readyState === "complete" && window.Zotero) {
- Services.scriptloader.loadSubScript(
- "chrome://__addonRef__/content/scripts/index.js"
- );
- } else {
- window.addEventListener("load", (e) => {
- if (window.Zotero) {
- Services.scriptloader.loadSubScript(
- "chrome://__addonRef__/content/scripts/index.js"
- );
- }
- });
- }
- };
+async function startup({ id, version, resourceURI, rootURI }, reason) {
+ await waitForZotero();
- // Listen to windows
- var WindowListener = {
- onOpenWindow: function (xulWindow) {
- loadAddon(
- xulWindow
- .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
- .getInterface(Components.interfaces.nsIDOMWindow)
- );
- },
- };
- Services.wm.addListener(WindowListener);
+ // String 'rootURI' introduced in Zotero 7
+ if (!rootURI) {
+ rootURI = resourceURI.spec;
+ }
- // Scan current windows
- const windows = Services.wm.getEnumerator("navigator:browser");
- while (windows.hasMoreElements()) {
- loadAddon(
- windows.getNext().QueryInterface(Components.interfaces.nsIDOMWindow)
- );
+ const ctx = { Zotero, rootURI };
+
+ Services.scriptloader.loadSubScript(
+ `${rootURI}/chrome/content/scripts/index.js`,
+ ctx
+ );
+
+ if (Zotero.platformMajorVersion >= 102) {
+ var aomStartup = Components.classes[
+ "@mozilla.org/addons/addon-manager-startup;1"
+ ].getService(Components.interfaces.amIAddonManagerStartup);
+ var manifestURI = Services.io.newURI(rootURI + "manifest.json");
+ chromeHandle = aomStartup.registerChrome(manifestURI, [
+ ["content", "__addonRef__", rootURI + "chrome/content/"],
+ ["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,
+ // });
}
}
-function shutdown(data, reason) {
+function shutdown({ id, version, resourceURI, rootURI }, reason) {
if (reason === APP_SHUTDOWN) {
return;
}
- var _Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
- Components.interfaces.nsISupports
- ).wrappedJSObject;
- _Zotero.AddonTemplate.events.onUnInit(_Zotero);
+ if (typeof Zotero === "undefined") {
+ Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
+ Components.interfaces.nsISupports
+ ).wrappedJSObject;
+ }
+ Zotero.AddonTemplate.events.onUnInit(Zotero);
Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Components.interfaces.nsIStringBundleService)
.flushBundles();
- Cu.unload("chrome://_addonRef__/scripts/index.js");
+ Cu.unload(`${rootURI}/chrome/content/scripts/index.js`);
+
+ chromeHandle.destruct();
+ chromeHandle = null;
}
function uninstall(data, reason) {}
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
new file mode 100644
index 0000000..ad73f90
--- /dev/null
+++ b/addon/chrome/content/preferences.xhtml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ &zotero.__addonRef__.pref.input.label;
+
+
+
diff --git a/addon/chrome/content/preferences.xul b/addon/chrome/content/preferences.xul
deleted file mode 100644
index 7f75ee3..0000000
--- a/addon/chrome/content/preferences.xul
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
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
new file mode 100644
index 0000000..029bf11
--- /dev/null
+++ b/addon/manifest.json
@@ -0,0 +1,19 @@
+{
+ "manifest_version": 2,
+ "name": "__addonName__",
+ "version": "__buildVersion__",
+ "description": "__description__",
+ "author": "__author__",
+ "icons": {
+ "48": "chrome/content/icons/favicon@0.5x.png",
+ "96": "chrome/content/icons/favicon.png"
+ },
+ "applications": {
+ "zotero": {
+ "id": "__addonID__",
+ "update_url": "__updaterdf__",
+ "strict_min_version": "6.999",
+ "strict_max_version": "7.0.*"
+ }
+ }
+}
diff --git a/build.js b/build.js
index 71578ae..ecb7f6f 100644
--- a/build.js
+++ b/build.js
@@ -96,6 +96,9 @@ async function main() {
copyFolderRecursiveSync("addon", buildDir);
+ copyFileSync("update-template.json", "update.json");
+ copyFileSync("update-template.rdf", "update.rdf");
+
await esbuild
.build({
entryPoints: ["src/index.ts"],
@@ -113,9 +116,13 @@ async function main() {
path.join(buildDir, "**/*.rdf"),
path.join(buildDir, "**/*.dtd"),
path.join(buildDir, "**/*.xul"),
- path.join(buildDir, "**/*.manifest"),
+ path.join(buildDir, "**/*.xhtml"),
+ path.join(buildDir, "**/*.json"),
path.join(buildDir, "addon/defaults", "**/*.js"),
+ path.join(buildDir, "addon/chrome.manifest"),
+ path.join(buildDir, "addon/manifest.json"),
path.join(buildDir, "addon/bootstrap.js"),
+ "update.json",
"update.rdf",
],
from: [
@@ -129,7 +136,6 @@ async function main() {
/__addonRef__/g,
/__buildVersion__/g,
/__buildTime__/g,
- /\S*<\/em:version>/g,
],
to: [
author,
@@ -142,7 +148,6 @@ async function main() {
addonRef,
version,
buildTime,
- `${version}`,
],
countMatches: true,
};
diff --git a/package.json b/package.json
index 5621feb..1fe5c78 100644
--- a/package.json
+++ b/package.json
@@ -26,14 +26,15 @@
},
"homepage": "https://github.com/windingwind/zotero-addon-template#readme",
"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.rdf",
+ "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"
},
"devDependencies": {
- "release-it": "^15.5.0",
- "zotero-types": "^0.0.6"
+ "@types/node": "^18.7.20",
+ "release-it": "^14.14.0",
+ "zotero-types": "^0.0.8"
}
}
diff --git a/src/addon.ts b/src/addon.ts
index 3945f91..bcc0ff3 100644
--- a/src/addon.ts
+++ b/src/addon.ts
@@ -1,19 +1,23 @@
import AddonEvents from "./events";
import AddonPrefs from "./prefs";
+import AddonUtils from "./utils";
import AddonViews from "./views";
-const { addonName } = require("../package.json");
-
class Addon {
+ public Zotero: _ZoteroConstructable;
public events: AddonEvents;
public views: AddonViews;
public prefs: AddonPrefs;
+ public Utils: AddonUtils;
+ // root path to access the resources
+ public rootURI: string;
constructor() {
this.events = new AddonEvents(this);
this.views = new AddonViews(this);
this.prefs = new AddonPrefs(this);
+ this.Utils = new AddonUtils(this);
}
}
-export { addonName, Addon };
+export default Addon;
diff --git a/src/events.ts b/src/events.ts
index 322b174..52fd8eb 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -1,5 +1,6 @@
-import { Addon, addonName } from "./addon";
+import Addon from "./addon";
import AddonModule from "./module";
+import { addonName } from "../package.json";
class AddonEvents extends AddonModule {
private notifierCallback: any;
@@ -27,32 +28,34 @@ class AddonEvents extends AddonModule {
};
}
- public async onInit(_Zotero) {
+ public async onInit(_Zotero: _ZoteroConstructable, rootURI) {
+ this._Addon.Zotero = _Zotero;
+ this._Addon.rootURI = rootURI;
// This function is the setup code of the addon
- console.log(`${addonName}: init called`);
- _Zotero.debug(`${addonName}: init called`);
+ 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, [
+ let notifierID = Zotero.Notifier.registerObserver(this.notifierCallback, [
"tab",
"item",
"file",
]);
// Unregister callback when the window closes (important to avoid a memory leak)
- _Zotero.getMainWindow().addEventListener(
+ Zotero.getMainWindow().addEventListener(
"unload",
function (e) {
- _Zotero.Notifier.unregisterObserver(notifierID);
+ Zotero.Notifier.unregisterObserver(notifierID);
},
false
);
- this._Addon.views.initViews(_Zotero);
+ this._Addon.views.initViews();
+ this._Addon.views.initPrefs();
}
private resetState(): void {
@@ -68,13 +71,13 @@ class AddonEvents extends AddonModule {
// }
}
- public onUnInit(_Zotero): void {
- console.log(`${addonName}: uninit called`);
- _Zotero.debug(`${addonName}: uninit called`);
+ public onUnInit(): void {
+ const Zotero = this._Addon.Zotero;
+ this._Addon.Utils.Tool.log(`${addonName}: uninit called`);
// Remove elements and do clean up
- this._Addon.views.unInitViews(_Zotero);
+ this._Addon.views.unInitViews();
// Remove addon object
- _Zotero.AddonTemplate = undefined;
+ Zotero.AddonTemplate = undefined;
}
}
diff --git a/src/index.ts b/src/index.ts
index 87f6363..a58cf92 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,7 @@
-import { Addon } from "./addon";
+import Addon from "./addon";
-var _Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
- Components.interfaces.nsISupports
-).wrappedJSObject;
-if (!_Zotero.AddonTemplate) {
- _Zotero.AddonTemplate = new Addon();
- _Zotero.AddonTemplate.events.onInit(_Zotero);
+if (!Zotero.AddonTemplate) {
+ Zotero.AddonTemplate = new Addon();
+ // @ts-ignore
+ Zotero.AddonTemplate.events.onInit(Zotero, rootURI);
}
diff --git a/src/module.ts b/src/module.ts
index e286abb..3b01dca 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -1,6 +1,8 @@
+import Addon from "./addon";
+
class AddonModule {
- protected _Addon: any;
- constructor(parent: any) {
+ protected _Addon: Addon;
+ constructor(parent: Addon) {
this._Addon = parent;
}
}
diff --git a/src/prefs.ts b/src/prefs.ts
index ac2b0d7..82ad941 100644
--- a/src/prefs.ts
+++ b/src/prefs.ts
@@ -1,5 +1,6 @@
-import { Addon, addonName } from "./addon";
+import Addon from "./addon";
import AddonModule from "./module";
+import { addonName, addonRef } from "../package.json";
class AddonPrefs extends AddonModule {
private _window: Window;
@@ -10,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
new file mode 100644
index 0000000..e7304c0
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,461 @@
+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;
+ },
+ // 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);
+ }
+ },
+
+ // 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 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(),
+ 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 df36171..d8299d8 100644
--- a/src/views.ts
+++ b/src/views.ts
@@ -1,10 +1,9 @@
-import { Addon } from "./addon";
+import Addon from "./addon";
import AddonModule from "./module";
-const { addonRef } = require("../package.json");
+const { addonRef, addonID } = require("../package.json");
class AddonViews extends AddonModule {
// You can store some element in the object attributes
- private testButton: XUL.Button;
private progressWindowIcon: object;
constructor(parent: Addon) {
@@ -12,31 +11,83 @@ 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`,
};
}
- public initViews(_Zotero) {
+ public initViews() {
+ const Zotero = this._Addon.Zotero;
// You can init the UI elements that
// cannot be initialized with overlay.xul
- console.log("Initializing UI");
- const _window: Window = _Zotero.getMainWindow();
- const menuitem = _window.document.createElement("menuitem");
- menuitem.id = "zotero-itemmenu-addontemplate-test";
- menuitem.setAttribute("label", "Addon Template");
- menuitem.setAttribute("oncommand", "alert('Hello World!')");
- menuitem.className = "menuitem-iconic";
- menuitem.style["list-style-image"] =
- "url('chrome://addontemplate/skin/favicon@0.5x.png')";
- _window.document.querySelector("#zotero-itemmenu").appendChild(menuitem);
+ this._Addon.Utils.Tool.log("Initializing UI");
+ const menuIcon =
+ 'url("chrome://addontemplate/content/icons/favicon@0.5x.png")';
+ // item menuitem with icon
+ this._Addon.Utils.UI.insertMenuItem("item", {
+ tag: "menuitem",
+ id: "zotero-itemmenu-addontemplate-test",
+ label: "Addon Template: Menuitem",
+ oncommand: "alert('Hello World! Default Menuitem.')",
+ icon: menuIcon,
+ });
+ // item menupopup with sub-menuitems
+ this._Addon.Utils.UI.insertMenuItem(
+ "item",
+ {
+ tag: "menu",
+ label: "Addon Template: Menupopup",
+ subElementOptions: [
+ {
+ tag: "menuitem",
+ label: "Addon Template",
+ oncommand: "alert('Hello World! Sub Menuitem.')",
+ },
+ ],
+ },
+ "before",
+ this._Addon.Zotero.getMainWindow().document.querySelector(
+ "#zotero-itemmenu-addontemplate-test"
+ )
+ );
+ this._Addon.Utils.UI.insertMenuItem("menuFile", {
+ tag: "menuseparator",
+ });
+ // menu->File menuitem
+ this._Addon.Utils.UI.insertMenuItem("menuFile", {
+ tag: "menuitem",
+ label: "Addon Template: File Menuitem",
+ oncommand: "alert('Hello World! File Menuitem.')",
+ });
}
- public unInitViews(_Zotero) {
- console.log("Uninitializing UI");
- const _window: Window = _Zotero.getMainWindow();
- _window.document
- .querySelector("#zotero-itemmenu-addontemplate-test")
- ?.remove();
+ 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;
+ 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/tsconfig.json b/tsconfig.json
index d5b9388..cf12fa8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "es6",
+ "resolveJsonModule": true
},
"include": [
"src",
diff --git a/typing/global.d.ts b/typing/global.d.ts
new file mode 100644
index 0000000..75c2131
--- /dev/null
+++ b/typing/global.d.ts
@@ -0,0 +1,108 @@
+declare interface ZoteroCompat {
+ getZotero: () => _ZoteroConstructable;
+ 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 {
+ 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;
+}
diff --git a/update-template.json b/update-template.json
new file mode 100644
index 0000000..7dca98e
--- /dev/null
+++ b/update-template.json
@@ -0,0 +1,26 @@
+{
+ "addons": {
+ "__addonID__": {
+ "updates": [
+ {
+ "version": "__buildVersion__",
+ "update_link": "__releasepage__",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "60.0"
+ }
+ }
+ },
+ {
+ "version": "__buildVersion__",
+ "update_link": "__releasepage__",
+ "applications": {
+ "zotero": {
+ "strict_min_version": "6.999"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/update-template.rdf b/update-template.rdf
new file mode 100644
index 0000000..b82d92f
--- /dev/null
+++ b/update-template.rdf
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ __buildVersion__
+
+
+ zotero@chnm.gmu.edu
+ 6.999
+ *
+ __releasepage__
+
+
+
+
+ juris-m@juris-m.github.io
+ 6.999
+ *
+ __releasepage__
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/update.json b/update.json
new file mode 100644
index 0000000..8613ac6
--- /dev/null
+++ b/update.json
@@ -0,0 +1,26 @@
+{
+ "addons": {
+ "addontemplate@euclpts.com": {
+ "updates": [
+ {
+ "version": "0.0.0",
+ "update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "60.0"
+ }
+ }
+ },
+ {
+ "version": "0.0.0",
+ "update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
+ "applications": {
+ "zotero": {
+ "strict_min_version": "6.999"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/update.rdf b/update.rdf
index c237fc1..379d578 100644
--- a/update.rdf
+++ b/update.rdf
@@ -1,6 +1,6 @@
-
+
@@ -9,17 +9,17 @@
zotero@chnm.gmu.edu
- 5.0
+ 6.999
*
- __releasepage__
+ https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi
juris-m@juris-m.github.io
- 5.0
+ 6.999
*
- __releasepage__
+ https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi