change: use zotero-plugin-toolkit
change: use strict ts mode change: dependencies
This commit is contained in:
parent
c3db612775
commit
969a5274c6
117
README.md
117
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));
|
- Release to GitHub automatically(using [release-it](https://github.com/release-it/release-it));
|
||||||
- Extensive skeleton;
|
- Extensive skeleton;
|
||||||
- Some sample code of UI and lifecycle.
|
- 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
|
## 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:
|
- Modify the settings in `./package.json`, including:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
version,
|
||||||
author,
|
author,
|
||||||
description,
|
description,
|
||||||
homepage,
|
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.
|
> 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.
|
- Run `npm run build` to build the plugin. The xpi for installation and the built code is under builds folder.
|
||||||
|
|
||||||
### Plugin Life Cycle
|
### Plugin Life Cycle
|
||||||
@ -56,6 +57,8 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/).
|
|||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
See https://github.com/windingwind/zotero-plugin-toolkit for more detailed API documentations.
|
||||||
|
|
||||||
#### Menu (file, edit, view, ...) & Right-click Menu (item, collection/library)
|
#### Menu (file, edit, view, ...) & Right-click Menu (item, collection/library)
|
||||||
|
|
||||||
**File Menu**
|
**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
|
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.
|
`insertMenuItem` resolved the input object and inject the menu items.
|
||||||
|
|
||||||
Available types `menuFile`, `menuEdit`, ...:
|
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.
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```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<MenuitemOptions>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Preference, for both Zotero 6 and Zotero 7 (all in bootstrap)
|
#### 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.
|
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.
|
||||||
|
|
||||||
<table style="margin-left: auto; margin-right: auto;">
|
<table style="margin-left: auto; margin-right: auto;">
|
||||||
<tr>
|
<tr>
|
||||||
@ -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
|
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 `<preferences>` element is deprecated. Please use the full pref-key in elements' `preference` attribute. Like:
|
Note that `<preferences>` element is deprecated. Please use the full pref-key in the elements' `preference` attribute. Like:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<checkbox label="&zotero.__addonRef__.pref.enable.label;" preference="extensions.zotero.__addonRef__.enable"
|
<checkbox label="&zotero.__addonRef__.pref.enable.label;" preference="extensions.zotero.__addonRef__.enable"
|
||||||
@ -152,7 +109,7 @@ Note that `<preferences>` element is deprecated. Please use the full pref-key in
|
|||||||
|
|
||||||
The elements with `preference` attributes will bind to Zotero preferences.
|
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
|
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`:
|
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.
|
- 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 on Zotero 6 doesn't support `createXULElement`. Using `Utils.UI.createElement`, it switches API depending on the current platform automatically.
|
- 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.
|
||||||
|
|
||||||
Definition:
|
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.
|
||||||
|
|
||||||
```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<ElementOptions>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Directory Structure
|
### Directory Structure
|
||||||
|
|
||||||
@ -263,7 +175,6 @@ This section shows the directory structure of a template.
|
|||||||
│ module.ts # module class
|
│ module.ts # module class
|
||||||
│ addon.ts # base class
|
│ addon.ts # base class
|
||||||
│ events.ts # events class
|
│ events.ts # events class
|
||||||
│ utils.ts # Utils class
|
|
||||||
│ views.ts # UI class
|
│ views.ts # UI class
|
||||||
└─ prefs.ts # preferences class
|
└─ prefs.ts # preferences class
|
||||||
|
|
||||||
|
@ -28,13 +28,14 @@
|
|||||||
"releasepage": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
|
"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",
|
"updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/master/update.json",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"compressing": "^1.5.1",
|
"zotero-plugin-toolkit": "^0.0.1"
|
||||||
"esbuild": "^0.15.16",
|
|
||||||
"replace-in-file": "^6.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.7.20",
|
"@types/node": "^18.7.20",
|
||||||
|
"compressing": "^1.5.1",
|
||||||
|
"esbuild": "^0.15.16",
|
||||||
|
"replace-in-file": "^6.3.2",
|
||||||
"release-it": "^14.14.0",
|
"release-it": "^14.14.0",
|
||||||
"zotero-types": "^0.0.8"
|
"zotero-types": "^0.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
11
src/addon.ts
11
src/addon.ts
@ -1,22 +1,23 @@
|
|||||||
import AddonEvents from "./events";
|
import AddonEvents from "./events";
|
||||||
import AddonPrefs from "./prefs";
|
import AddonPrefs from "./prefs";
|
||||||
import AddonUtils from "./utils";
|
|
||||||
import AddonViews from "./views";
|
import AddonViews from "./views";
|
||||||
|
|
||||||
|
import ZoteroToolkit from "zotero-plugin-toolkit";
|
||||||
|
|
||||||
class Addon {
|
class Addon {
|
||||||
public Zotero: _ZoteroConstructable;
|
public Zotero!: _ZoteroConstructable;
|
||||||
public events: AddonEvents;
|
public events: AddonEvents;
|
||||||
public views: AddonViews;
|
public views: AddonViews;
|
||||||
public prefs: AddonPrefs;
|
public prefs: AddonPrefs;
|
||||||
public Utils: AddonUtils;
|
public toolkit: ZoteroToolkit;
|
||||||
// root path to access the resources
|
// root path to access the resources
|
||||||
public rootURI: string;
|
public rootURI!: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.events = new AddonEvents(this);
|
this.events = new AddonEvents(this);
|
||||||
this.views = new AddonViews(this);
|
this.views = new AddonViews(this);
|
||||||
this.prefs = new AddonPrefs(this);
|
this.prefs = new AddonPrefs(this);
|
||||||
this.Utils = new AddonUtils(this);
|
this.toolkit = new ZoteroToolkit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class AddonEvents extends AddonModule {
|
|||||||
event: string,
|
event: string,
|
||||||
type: string,
|
type: string,
|
||||||
ids: Array<string>,
|
ids: Array<string>,
|
||||||
extraData: object
|
extraData: { [key: string]: any }
|
||||||
) => {
|
) => {
|
||||||
// You can add your code to the corresponding notify type
|
// You can add your code to the corresponding notify type
|
||||||
if (
|
if (
|
||||||
@ -33,7 +33,7 @@ class AddonEvents extends AddonModule {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this._Addon.rootURI = rootURI;
|
this._Addon.rootURI = rootURI;
|
||||||
// This function is the setup code of the addon
|
// 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
|
// Register the callback in Zotero as an item observer
|
||||||
let notifierID = Zotero.Notifier.registerObserver(this.notifierCallback, [
|
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)
|
// Unregister callback when the window closes (important to avoid a memory leak)
|
||||||
Zotero.getMainWindow().addEventListener(
|
Zotero.getMainWindow().addEventListener(
|
||||||
"unload",
|
"unload",
|
||||||
function (e) {
|
function (e: Event) {
|
||||||
Zotero.Notifier.unregisterObserver(notifierID);
|
Zotero.Notifier.unregisterObserver(notifierID);
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
@ -57,7 +57,7 @@ class AddonEvents extends AddonModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initPrefs() {
|
public initPrefs() {
|
||||||
this._Addon.Utils.Tool.log(this._Addon.rootURI);
|
this._Addon.toolkit.Tool.log(this._Addon.rootURI);
|
||||||
const prefOptions = {
|
const prefOptions = {
|
||||||
pluginID: addonID,
|
pluginID: addonID,
|
||||||
src: this._Addon.rootURI + "chrome/content/preferences.xhtml",
|
src: this._Addon.rootURI + "chrome/content/preferences.xhtml",
|
||||||
@ -69,22 +69,22 @@ class AddonEvents extends AddonModule {
|
|||||||
this._Addon.prefs.initPreferences(win);
|
this._Addon.prefs.initPreferences(win);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (this._Addon.Utils.Compat.isZotero7()) {
|
if (this._Addon.toolkit.Compat.isZotero7()) {
|
||||||
Zotero.PreferencePanes.register(prefOptions);
|
Zotero.PreferencePanes.register(prefOptions);
|
||||||
} else {
|
} else {
|
||||||
this._Addon.Utils.Compat.registerPrefPane(prefOptions);
|
this._Addon.toolkit.Compat.registerPrefPane(prefOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unInitPrefs() {
|
private unInitPrefs() {
|
||||||
if (!this._Addon.Utils.Compat.isZotero7()) {
|
if (!this._Addon.toolkit.Compat.isZotero7()) {
|
||||||
this._Addon.Utils.Compat.unregisterPrefPane();
|
this._Addon.toolkit.Compat.unregisterPrefPane();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUnInit(): void {
|
public onUnInit(): void {
|
||||||
const Zotero = this._Addon.Zotero;
|
const Zotero = this._Addon.Zotero;
|
||||||
this._Addon.Utils.Tool.log(`${addonName}: uninit called`);
|
this._Addon.toolkit.Tool.log(`${addonName}: uninit called`);
|
||||||
this.unInitPrefs();
|
this.unInitPrefs();
|
||||||
// Remove elements and do clean up
|
// Remove elements and do clean up
|
||||||
this._Addon.views.unInitViews();
|
this._Addon.views.unInitViews();
|
||||||
|
10
src/prefs.ts
10
src/prefs.ts
@ -3,7 +3,7 @@ import AddonModule from "./module";
|
|||||||
import { addonName, addonRef } from "../package.json";
|
import { addonName, addonRef } from "../package.json";
|
||||||
|
|
||||||
class AddonPrefs extends AddonModule {
|
class AddonPrefs extends AddonModule {
|
||||||
private _window: Window;
|
private _window!: Window;
|
||||||
constructor(parent: Addon) {
|
constructor(parent: Addon) {
|
||||||
super(parent);
|
super(parent);
|
||||||
}
|
}
|
||||||
@ -11,7 +11,7 @@ class AddonPrefs extends AddonModule {
|
|||||||
// This function is called when the prefs window is opened
|
// This function is called when the prefs window is opened
|
||||||
// See addon/chrome/content/preferences.xul onpaneload
|
// See addon/chrome/content/preferences.xul onpaneload
|
||||||
this._window = _window;
|
this._window = _window;
|
||||||
this._Addon.Utils.Tool.log(`${addonName}: init preferences`);
|
this._Addon.toolkit.Tool.log(`${addonName}: init preferences`);
|
||||||
this.updatePrefsUI();
|
this.updatePrefsUI();
|
||||||
this.bindPrefEvents();
|
this.bindPrefEvents();
|
||||||
}
|
}
|
||||||
@ -20,14 +20,14 @@ class AddonPrefs extends AddonModule {
|
|||||||
// You can initialize some UI elements on prefs window
|
// You can initialize some UI elements on prefs window
|
||||||
// with this._window.document
|
// with this._window.document
|
||||||
// Or bind some events to the elements
|
// 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() {
|
private bindPrefEvents() {
|
||||||
this._window.document
|
this._window.document
|
||||||
.querySelector(`#zotero-prefpane-${addonRef}-enable`)
|
.querySelector(`#zotero-prefpane-${addonRef}-enable`)
|
||||||
?.addEventListener("command", (e) => {
|
?.addEventListener("command", (e) => {
|
||||||
this._Addon.Utils.Tool.log(e);
|
this._Addon.toolkit.Tool.log(e);
|
||||||
this._window.alert(
|
this._window.alert(
|
||||||
`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`
|
`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`
|
||||||
);
|
);
|
||||||
@ -36,7 +36,7 @@ class AddonPrefs extends AddonModule {
|
|||||||
this._window.document
|
this._window.document
|
||||||
.querySelector(`#zotero-prefpane-${addonRef}-input`)
|
.querySelector(`#zotero-prefpane-${addonRef}-input`)
|
||||||
?.addEventListener("change", (e) => {
|
?.addEventListener("change", (e) => {
|
||||||
this._Addon.Utils.Tool.log(e);
|
this._Addon.toolkit.Tool.log(e);
|
||||||
this._window.alert(
|
this._window.alert(
|
||||||
`Successfully changed to ${(e.target as HTMLInputElement).value}!`
|
`Successfully changed to ${(e.target as HTMLInputElement).value}!`
|
||||||
);
|
);
|
||||||
|
619
src/utils.ts
619
src/utils.ts
@ -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
|
|
||||||
? `<!DOCTYPE bindings [ ${entities.reduce(
|
|
||||||
(preamble, url, index) => {
|
|
||||||
return (
|
|
||||||
preamble +
|
|
||||||
`<!ENTITY % _dtd-${index} SYSTEM "${url}"> %_dtd-${index}; `
|
|
||||||
);
|
|
||||||
},
|
|
||||||
""
|
|
||||||
)}]>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
<html:div xmlns="${defaultXUL ? xulns : htmlns}"
|
|
||||||
xmlns:xul="${xulns}" xmlns:html="${htmlns}">
|
|
||||||
${str}
|
|
||||||
</html:div>`;
|
|
||||||
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(
|
|
||||||
"<preference> is deprecated -- `preference` attribute values " +
|
|
||||||
"should be full preference keys, not <preference> 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 = `<prefpane xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="${
|
|
||||||
options.id
|
|
||||||
}" insertafter="zotero-prefpane-advanced" label="${
|
|
||||||
options.label || options.pluginID
|
|
||||||
}" image="${options.image || ""}">
|
|
||||||
${content}
|
|
||||||
</prefpane>`;
|
|
||||||
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;
|
|
16
src/views.ts
16
src/views.ts
@ -4,7 +4,7 @@ const { addonRef } = require("../package.json");
|
|||||||
|
|
||||||
class AddonViews extends AddonModule {
|
class AddonViews extends AddonModule {
|
||||||
// You can store some element in the object attributes
|
// You can store some element in the object attributes
|
||||||
private progressWindowIcon: object;
|
private progressWindowIcon: { [key: string]: string };
|
||||||
|
|
||||||
constructor(parent: Addon) {
|
constructor(parent: Addon) {
|
||||||
super(parent);
|
super(parent);
|
||||||
@ -18,10 +18,10 @@ class AddonViews extends AddonModule {
|
|||||||
public initViews() {
|
public initViews() {
|
||||||
// You can init the UI elements that
|
// You can init the UI elements that
|
||||||
// cannot be initialized with overlay.xul
|
// 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";
|
const menuIcon = "chrome://addontemplate/content/icons/favicon@0.5x.png";
|
||||||
// item menuitem with icon
|
// item menuitem with icon
|
||||||
this._Addon.Utils.UI.insertMenuItem("item", {
|
this._Addon.toolkit.UI.insertMenuItem("item", {
|
||||||
tag: "menuitem",
|
tag: "menuitem",
|
||||||
id: "zotero-itemmenu-addontemplate-test",
|
id: "zotero-itemmenu-addontemplate-test",
|
||||||
label: "Addon Template: Menuitem",
|
label: "Addon Template: Menuitem",
|
||||||
@ -29,7 +29,7 @@ class AddonViews extends AddonModule {
|
|||||||
icon: menuIcon,
|
icon: menuIcon,
|
||||||
});
|
});
|
||||||
// item menupopup with sub-menuitems
|
// item menupopup with sub-menuitems
|
||||||
this._Addon.Utils.UI.insertMenuItem(
|
this._Addon.toolkit.UI.insertMenuItem(
|
||||||
"item",
|
"item",
|
||||||
{
|
{
|
||||||
tag: "menu",
|
tag: "menu",
|
||||||
@ -47,11 +47,11 @@ class AddonViews extends AddonModule {
|
|||||||
"#zotero-itemmenu-addontemplate-test"
|
"#zotero-itemmenu-addontemplate-test"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this._Addon.Utils.UI.insertMenuItem("menuFile", {
|
this._Addon.toolkit.UI.insertMenuItem("menuFile", {
|
||||||
tag: "menuseparator",
|
tag: "menuseparator",
|
||||||
});
|
});
|
||||||
// menu->File menuitem
|
// menu->File menuitem
|
||||||
this._Addon.Utils.UI.insertMenuItem("menuFile", {
|
this._Addon.toolkit.UI.insertMenuItem("menuFile", {
|
||||||
tag: "menuitem",
|
tag: "menuitem",
|
||||||
label: "Addon Template: File Menuitem",
|
label: "Addon Template: File Menuitem",
|
||||||
oncommand: "alert('Hello World! File Menuitem.')",
|
oncommand: "alert('Hello World! File Menuitem.')",
|
||||||
@ -59,8 +59,8 @@ class AddonViews extends AddonModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public unInitViews() {
|
public unInitViews() {
|
||||||
this._Addon.Utils.Tool.log("Uninitializing UI");
|
this._Addon.toolkit.Tool.log("Uninitializing UI");
|
||||||
this._Addon.Utils.UI.removeAddonElements();
|
this._Addon.toolkit.UI.removeAddonElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
public showProgressWindow(
|
public showProgressWindow(
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es6",
|
"target": "ES2016",
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"strict": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
|
110
typing/global.d.ts
vendored
110
typing/global.d.ts
vendored
@ -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<string>;
|
|
||||||
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<ElementOptions>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<MenuitemOptions>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user