import { config } from "../../package.json"; import { getLocaleID, getString } from "../utils/locale"; function example( target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor, ) { const original = descriptor.value; descriptor.value = function (...args: any) { try { ztoolkit.log(`Calling example ${target.name}.${String(propertyKey)}`); return original.apply(this, args); } catch (e) { ztoolkit.log(`Error in example ${target.name}.${String(propertyKey)}`, e); throw e; } }; return descriptor; } export class BasicExampleFactory { @example static registerNotifier() { const callback = { notify: async ( event: string, type: string, ids: number[] | string[], extraData: { [key: string]: any }, ) => { if (!addon?.data.alive) { this.unregisterNotifier(notifierID); return; } addon.hooks.onNotify(event, type, ids, extraData); }, }; // Register the callback in Zotero as an item observer const notifierID = Zotero.Notifier.registerObserver(callback, [ "tab", "item", "file", ]); // Unregister callback when the window closes (important to avoid a memory leak) window.addEventListener( "unload", (e: Event) => { this.unregisterNotifier(notifierID); }, false, ); } @example static exampleNotifierCallback() { new ztoolkit.ProgressWindow(config.addonName) .createLine({ text: "Open Tab Detected!", type: "success", progress: 100, }) .show(); } @example private static unregisterNotifier(notifierID: string) { Zotero.Notifier.unregisterObserver(notifierID); } @example static registerPrefs() { Zotero.PreferencePanes.register({ pluginID: config.addonID, src: rootURI + "chrome/content/preferences.xhtml", label: getString("prefs-title"), image: `chrome://${config.addonRef}/content/icons/favicon.png`, }); } } export class KeyExampleFactory { @example static registerShortcuts() { // Register an event key for Alt+L ztoolkit.Keyboard.register((ev, keyOptions) => { ztoolkit.log(ev, keyOptions.keyboard); if (keyOptions.keyboard?.equals("shift,l")) { addon.hooks.onShortcuts("larger"); } if (ev.shiftKey && ev.key === "S") { addon.hooks.onShortcuts("smaller"); } }); new ztoolkit.ProgressWindow(config.addonName) .createLine({ text: "Example Shortcuts: Alt+L/S/C", type: "success", }) .show(); } @example static exampleShortcutLargerCallback() { new ztoolkit.ProgressWindow(config.addonName) .createLine({ text: "Larger!", type: "default", }) .show(); } @example static exampleShortcutSmallerCallback() { new ztoolkit.ProgressWindow(config.addonName) .createLine({ text: "Smaller!", type: "default", }) .show(); } } export class UIExampleFactory { @example static registerStyleSheet() { const styles = ztoolkit.UI.createElement(document, "link", { properties: { type: "text/css", rel: "stylesheet", href: `chrome://${config.addonRef}/content/zoteroPane.css`, }, }); document.documentElement.appendChild(styles); document .getElementById("zotero-item-pane-content") ?.classList.add("makeItRed"); } @example static registerRightClickMenuItem() { const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`; // item menuitem with icon ztoolkit.Menu.register("item", { tag: "menuitem", id: "zotero-itemmenu-addontemplate-test", label: getString("menuitem-label"), commandListener: (ev) => addon.hooks.onDialogEvents("dialogExample"), icon: menuIcon, }); } @example static registerRightClickMenuPopup() { ztoolkit.Menu.register( "item", { tag: "menu", label: getString("menupopup-label"), children: [ { tag: "menuitem", label: getString("menuitem-submenulabel"), oncommand: "alert('Hello World! Sub Menuitem.')", }, ], }, "before", document.querySelector( "#zotero-itemmenu-addontemplate-test", ) as XUL.MenuItem, ); } @example static registerWindowMenuWithSeparator() { ztoolkit.Menu.register("menuFile", { tag: "menuseparator", }); // menu->File menuitem ztoolkit.Menu.register("menuFile", { tag: "menuitem", label: getString("menuitem-filemenulabel"), oncommand: "alert('Hello World! File Menuitem.')", }); } @example static async registerExtraColumn() { const field = "test1"; await Zotero.ItemTreeManager.registerColumns({ pluginID: config.addonID, dataKey: field, label: "text column", dataProvider: (item: Zotero.Item, dataKey: string) => { return field + String(item.id); }, iconPath: "chrome://zotero/skin/cross.png", }); } @example static async registerExtraColumnWithCustomCell() { const field = "test2"; await Zotero.ItemTreeManager.registerColumns({ pluginID: config.addonID, dataKey: field, label: "custom column", dataProvider: (item: Zotero.Item, dataKey: string) => { return field + String(item.id); }, renderCell(index, data, column) { ztoolkit.log("Custom column cell is rendered!"); const span = document.createElementNS( "http://www.w3.org/1999/xhtml", "span", ); span.className = `cell ${column.className}`; span.style.background = "#0dd068"; span.innerText = "⭐" + data; return span; }, }); } @example static registerItemPaneSection() { Zotero.ItemPaneManager.registerSection({ paneID: "example", pluginID: config.addonID, header: { l10nID: getLocaleID("item-section-example1-head-text"), icon: "chrome://zotero/skin/16/universal/book.svg", }, sidenav: { l10nID: getLocaleID("item-section-example1-sidenav-tooltip"), icon: "chrome://zotero/skin/20/universal/save.svg", }, onRender: ({ body, item, editable, tabType }) => { body.textContent = JSON.stringify({ id: item?.id, editable, tabType, }); }, }); } @example static async registerReaderItemPaneSection() { Zotero.ItemPaneManager.registerSection({ paneID: "reader-example", pluginID: config.addonID, header: { l10nID: getLocaleID("item-section-example2-head-text"), // Optional l10nArgs: `{"status": "Initialized"}`, // Can also have a optional dark icon icon: "chrome://zotero/skin/16/universal/book.svg", }, sidenav: { l10nID: getLocaleID("item-section-example2-sidenav-tooltip"), icon: "chrome://zotero/skin/20/universal/save.svg", }, // Optional bodyXHTML: 'THIS IS TEST', // Optional, Called when the section is first created, must be synchronous onInit: ({ item }) => { ztoolkit.log("Section init!", item?.id); }, // Optional, Called when the section is destroyed, must be synchronous onDestroy: (props) => { ztoolkit.log("Section destroy!"); }, // Optional, Called when the section data changes (setting item/mode/tabType/inTrash), must be synchronous. return false to cancel the change onItemChange: ({ item, setEnabled, tabType }) => { ztoolkit.log(`Section item data changed to ${item?.id}`); setEnabled(tabType === "reader"); return true; }, // Called when the section is asked to render, must be synchronous. onRender: ({ body, item, setL10nArgs, setSectionSummary, setSectionButtonStatus, }) => { ztoolkit.log("Section rendered!", item?.id); const title = body.querySelector("#test") as HTMLElement; title.style.color = "red"; title.textContent = "LOADING"; setL10nArgs(`{ "status": "Loading" }`); setSectionSummary("loading!"); setSectionButtonStatus("test", { hidden: true }); }, // Optional, can be asynchronous. onAsyncRender: async ({ body, item, setL10nArgs, setSectionSummary, setSectionButtonStatus, }) => { ztoolkit.log("Section secondary render start!", item?.id); await Zotero.Promise.delay(1000); ztoolkit.log("Section secondary render finish!", item?.id); const title = body.querySelector("#test") as HTMLElement; title.style.color = "green"; title.textContent = item.getField("title"); setL10nArgs(`{ "status": "Loaded" }`); setSectionSummary("rendered!"); setSectionButtonStatus("test", { hidden: false }); }, // Optional, Called when the section is toggled. Can happen anytime even if the section is not visible or not rendered onToggle: ({ item }) => { ztoolkit.log("Section toggled!", item?.id); }, // Optional, Buttons to be shown in the section header sectionButtons: [ { type: "test", icon: "chrome://zotero/skin/16/universal/empty-trash.svg", l10nID: getLocaleID("item-section-example2-button-tooltip"), onClick: ({ item, paneID }) => { ztoolkit.log("Section clicked!", item?.id); Zotero.ItemPaneManager.unregisterSection(paneID); }, }, ], }); } } export class PromptExampleFactory { @example static registerNormalCommandExample() { ztoolkit.Prompt.register([ { name: "Normal Command Test", label: "Plugin Template", callback(prompt) { ztoolkit.getGlobal("alert")("Command triggered!"); }, }, ]); } @example static registerAnonymousCommandExample() { ztoolkit.Prompt.register([ { id: "search", callback: async (prompt) => { // https://github.com/zotero/zotero/blob/7262465109c21919b56a7ab214f7c7a8e1e63909/chrome/content/zotero/integration/quickFormat.js#L589 function getItemDescription(item: Zotero.Item) { const nodes = []; let str = ""; let author, authorDate = ""; if (item.firstCreator) { author = authorDate = item.firstCreator; } let date = item.getField("date", true, true) as string; if (date && (date = date.substr(0, 4)) !== "0000") { authorDate += " (" + parseInt(date) + ")"; } authorDate = authorDate.trim(); if (authorDate) nodes.push(authorDate); const publicationTitle = item.getField( "publicationTitle", false, true, ); if (publicationTitle) { nodes.push(`${publicationTitle}`); } let volumeIssue = item.getField("volume"); const issue = item.getField("issue"); if (issue) volumeIssue += "(" + issue + ")"; if (volumeIssue) nodes.push(volumeIssue); const publisherPlace = []; let field; if ((field = item.getField("publisher"))) publisherPlace.push(field); if ((field = item.getField("place"))) publisherPlace.push(field); if (publisherPlace.length) nodes.push(publisherPlace.join(": ")); const pages = item.getField("pages"); if (pages) nodes.push(pages); if (!nodes.length) { const url = item.getField("url"); if (url) nodes.push(url); } // compile everything together for (let i = 0, n = nodes.length; i < n; i++) { const node = nodes[i]; if (i != 0) str += ", "; if (typeof node === "object") { const label = document.createElement("label"); label.setAttribute("value", str); label.setAttribute("crop", "end"); str = ""; } else { str += node; } } str.length && (str += "."); return str; } function filter(ids: number[]) { ids = ids.filter(async (id) => { const item = (await Zotero.Items.getAsync(id)) as Zotero.Item; return item.isRegularItem() && !(item as any).isFeedItem; }); return ids; } const text = prompt.inputNode.value; prompt.showTip("Searching..."); const s = new Zotero.Search(); s.addCondition("quicksearch-titleCreatorYear", "contains", text); s.addCondition("itemType", "isNot", "attachment"); let ids = await s.search(); // prompt.exit will remove current container element. // @ts-ignore ignore prompt.exit(); const container = prompt.createCommandsContainer(); container.classList.add("suggestions"); ids = filter(ids); console.log(ids.length); if (ids.length == 0) { const s = new Zotero.Search(); const operators = [ "is", "isNot", "true", "false", "isInTheLast", "isBefore", "isAfter", "contains", "doesNotContain", "beginsWith", ]; let hasValidCondition = false; let joinMode = "all"; if (/\s*\|\|\s*/.test(text)) { joinMode = "any"; } text.split(/\s*(&&|\|\|)\s*/g).forEach((conditinString: string) => { const conditions = conditinString.split(/\s+/g); if ( conditions.length == 3 && operators.indexOf(conditions[1]) != -1 ) { hasValidCondition = true; s.addCondition( "joinMode", joinMode as Zotero.Search.Operator, "", ); s.addCondition( conditions[0] as string, conditions[1] as Zotero.Search.Operator, conditions[2] as string, ); } }); if (hasValidCondition) { ids = await s.search(); } } ids = filter(ids); console.log(ids.length); if (ids.length > 0) { ids.forEach((id: number) => { const item = Zotero.Items.get(id); const title = item.getField("title"); const ele = ztoolkit.UI.createElement(document, "div", { namespace: "html", classList: ["command"], listeners: [ { type: "mousemove", listener: function () { // @ts-ignore ignore prompt.selectItem(this); }, }, { type: "click", listener: () => { prompt.promptNode.style.display = "none"; Zotero_Tabs.select("zotero-pane"); ZoteroPane.selectItem(item.id); }, }, ], styles: { display: "flex", flexDirection: "column", justifyContent: "start", }, children: [ { tag: "span", styles: { fontWeight: "bold", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, properties: { innerText: title, }, }, { tag: "span", styles: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, properties: { innerHTML: getItemDescription(item), }, }, ], }); container.appendChild(ele); }); } else { // @ts-ignore ignore prompt.exit(); prompt.showTip("Not Found."); } }, }, ]); } @example static registerConditionalCommandExample() { ztoolkit.Prompt.register([ { name: "Conditional Command Test", label: "Plugin Template", // The when function is executed when Prompt UI is woken up by `Shift + P`, and this command does not display when false is returned. when: () => { const items = ZoteroPane.getSelectedItems(); return items.length > 0; }, callback(prompt) { prompt.inputNode.placeholder = "Hello World!"; const items = ZoteroPane.getSelectedItems(); ztoolkit.getGlobal("alert")( `You select ${items.length} items!\n\n${items .map( (item, index) => String(index + 1) + ". " + item.getDisplayTitle(), ) .join("\n")}`, ); }, }, ]); } } export class HelperExampleFactory { @example static async dialogExample() { const dialogData: { [key: string | number]: any } = { inputValue: "test", checkboxValue: true, loadCallback: () => { ztoolkit.log(dialogData, "Dialog Opened!"); }, unloadCallback: () => { ztoolkit.log(dialogData, "Dialog closed!"); }, }; const dialogHelper = new ztoolkit.Dialog(10, 2) .addCell(0, 0, { tag: "h1", properties: { innerHTML: "Helper Examples" }, }) .addCell(1, 0, { tag: "h2", properties: { innerHTML: "Dialog Data Binding" }, }) .addCell(2, 0, { tag: "p", properties: { innerHTML: "Elements with attribute 'data-bind' are binded to the prop under 'dialogData' with the same name.", }, styles: { width: "200px", }, }) .addCell(3, 0, { tag: "label", namespace: "html", attributes: { for: "dialog-checkbox", }, properties: { innerHTML: "bind:checkbox" }, }) .addCell( 3, 1, { tag: "input", namespace: "html", id: "dialog-checkbox", attributes: { "data-bind": "checkboxValue", "data-prop": "checked", type: "checkbox", }, properties: { label: "Cell 1,0" }, }, false, ) .addCell(4, 0, { tag: "label", namespace: "html", attributes: { for: "dialog-input", }, properties: { innerHTML: "bind:input" }, }) .addCell( 4, 1, { tag: "input", namespace: "html", id: "dialog-input", attributes: { "data-bind": "inputValue", "data-prop": "value", type: "text", }, }, false, ) .addCell(5, 0, { tag: "h2", properties: { innerHTML: "Toolkit Helper Examples" }, }) .addCell( 6, 0, { tag: "button", namespace: "html", attributes: { type: "button", }, listeners: [ { type: "click", listener: (e: Event) => { addon.hooks.onDialogEvents("clipboardExample"); }, }, ], children: [ { tag: "div", styles: { padding: "2.5px 15px", }, properties: { innerHTML: "example:clipboard", }, }, ], }, false, ) .addCell( 7, 0, { tag: "button", namespace: "html", attributes: { type: "button", }, listeners: [ { type: "click", listener: (e: Event) => { addon.hooks.onDialogEvents("filePickerExample"); }, }, ], children: [ { tag: "div", styles: { padding: "2.5px 15px", }, properties: { innerHTML: "example:filepicker", }, }, ], }, false, ) .addCell( 8, 0, { tag: "button", namespace: "html", attributes: { type: "button", }, listeners: [ { type: "click", listener: (e: Event) => { addon.hooks.onDialogEvents("progressWindowExample"); }, }, ], children: [ { tag: "div", styles: { padding: "2.5px 15px", }, properties: { innerHTML: "example:progressWindow", }, }, ], }, false, ) .addCell( 9, 0, { tag: "button", namespace: "html", attributes: { type: "button", }, listeners: [ { type: "click", listener: (e: Event) => { addon.hooks.onDialogEvents("vtableExample"); }, }, ], children: [ { tag: "div", styles: { padding: "2.5px 15px", }, properties: { innerHTML: "example:virtualized-table", }, }, ], }, false, ) .addButton("Confirm", "confirm") .addButton("Cancel", "cancel") .addButton("Help", "help", { noClose: true, callback: (e) => { dialogHelper.window?.alert( "Help Clicked! Dialog will not be closed.", ); }, }) .setDialogData(dialogData) .open("Dialog Example"); addon.data.dialog = dialogHelper; await dialogData.unloadLock.promise; addon.data.dialog = undefined; addon.data.alive && ztoolkit.getGlobal("alert")( `Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`, ); ztoolkit.log(dialogData); } @example static clipboardExample() { new ztoolkit.Clipboard() .addText( "![Plugin Template](https://github.com/windingwind/zotero-plugin-template)", "text/unicode", ) .addText( 'Plugin Template', "text/html", ) .copy(); ztoolkit.getGlobal("alert")("Copied!"); } @example static async filePickerExample() { const path = await new ztoolkit.FilePicker( "Import File", "open", [ ["PNG File(*.png)", "*.png"], ["Any", "*.*"], ], "image.png", ).open(); ztoolkit.getGlobal("alert")(`Selected ${path}`); } @example static progressWindowExample() { new ztoolkit.ProgressWindow(config.addonName) .createLine({ text: "ProgressWindow Example!", type: "success", progress: 100, }) .show(); } @example static vtableExample() { ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts"); } }