add: utils

This commit is contained in:
xiangyu 2022-12-15 20:39:13 +08:00
parent 7de5e38d81
commit 6f752cd451
8 changed files with 440 additions and 38 deletions

View File

@ -35,6 +35,6 @@
"devDependencies": {
"@types/node": "^18.7.20",
"release-it": "^14.14.0",
"zotero-types": "^0.0.7"
"zotero-types": "^0.0.8"
}
}

View File

@ -1,13 +1,14 @@
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;
@ -15,32 +16,8 @@ class Addon {
this.events = new AddonEvents(this);
this.views = new AddonViews(this);
this.prefs = new AddonPrefs(this);
this.utils = new AddonUtils(this);
}
}
function getZotero(): _ZoteroConstructable {
if (typeof Zotero === "undefined") {
return Components.classes["@zotero.org/Zotero;1"].getService(
Components.interfaces.nsISupports
).wrappedJSObject;
}
return Zotero;
}
function isZotero7(): boolean {
return Zotero.platformMajorVersion >= 102;
}
function createXULElement(doc: Document, type: string): XUL.Element {
if (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;
}
}
export { addonName, Addon, getZotero, isZotero7, createXULElement };
export default Addon;

View File

@ -1,5 +1,6 @@
import { Addon, addonName, getZotero } from "./addon";
import Addon from "./addon";
import AddonModule from "./module";
import { addonName } from "../package.json";
class AddonEvents extends AddonModule {
private notifierCallback: any;
@ -27,8 +28,8 @@ class AddonEvents extends AddonModule {
};
}
public async onInit() {
const Zotero = getZotero();
public async onInit(_Zotero: _ZoteroConstructable) {
this._Addon.Zotero = _Zotero;
// This function is the setup code of the addon
Zotero.debug(`${addonName}: init called`);
// alert(112233);
@ -70,7 +71,7 @@ class AddonEvents extends AddonModule {
}
public onUnInit(): void {
const Zotero = getZotero();
const Zotero = this._Addon.Zotero;
Zotero.debug(`${addonName}: uninit called`);
// Remove elements and do clean up
this._Addon.views.unInitViews();

View File

@ -1,8 +1,10 @@
import { Addon, getZotero } from "./addon";
import Addon from "./addon";
const Zotero = getZotero();
const Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
Components.interfaces.nsISupports
).wrappedJSObject;
if (!Zotero.AddonTemplate) {
Zotero.AddonTemplate = new Addon();
Zotero.AddonTemplate.events.onInit();
Zotero.AddonTemplate.events.onInit(Zotero);
}

View File

@ -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;
}
}

335
src/utils.ts Normal file
View File

@ -0,0 +1,335 @@
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: () =>
this.Compat.isZotero7()
? new DOMParser()
: 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;
}
},
};
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 = {
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") {
return this.Compat.createXULElement(doc, tagName);
} else {
return doc.createElementNS(namespaces[namespace], tagName) as
| HTMLElement
| SVGAElement;
}
},
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;

View File

@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"resolveJsonModule": true
},
"include": [
"src",

84
typing/global.d.ts vendored Normal file
View File

@ -0,0 +1,84 @@
declare interface ZoteroCompat {
getZotero: () => _ZoteroConstructable;
isZotero7: () => boolean;
getDOMParser: () => DOMParser;
createXULElement: (doc: Document, type: string) => XUL.Element;
}
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 {
createElement: (
doc: Document,
tagName: string,
namespace: "html" | "svg" | "xul"
) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement;
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";
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 class CopyHelper {
addText: (source: string, type: "text/html" | "text/unicode") => CopyHelper;
addImage: (source: string) => CopyHelper;
copy: () => void;
}