From 9e2b4c583dbce6fffe3548a0d0004ceb715170dc Mon Sep 17 00:00:00 2001
From: windingwind <33902321+windingwind@users.noreply.github.com>
Date: Sat, 6 Apr 2024 11:23:24 +0800
Subject: [PATCH] add: linkNote dialog
---
addon/chrome/content/linkNote.xhtml | 98 +++
addon/chrome/content/styles/linkNote.css | 33 +
addon/chrome/content/styles/notePicker.css | 40 ++
addon/chrome/content/styles/toolbar.css | 48 ++
addon/locale/en-US/addon.ftl | 6 +-
addon/locale/it-IT/addon.ftl | 6 +-
addon/locale/ru-RU/addon.ftl | 6 +-
addon/locale/tr-TR/addon.ftl | 6 +-
addon/prefs.js | 2 +
src/api.ts | 2 +
src/elements/base.ts | 10 +-
src/elements/notePicker.ts | 304 +++++++++
src/elements/outlinePane.ts | 14 +-
.../{workspace.ts => customElements.ts} | 2 +
src/extras/linkNote.ts | 325 ++++++++++
src/hooks.ts | 7 +-
src/modules/convert/api.ts | 4 +-
src/modules/editor/toolbar.ts | 613 ++++++------------
src/modules/export/markdown.ts | 2 +-
src/modules/export/pdf.ts | 1 -
src/modules/relatedNotes.ts | 31 +
src/modules/sync/managerWindow.ts | 1 -
src/modules/template/api.ts | 4 +-
src/utils/config.ts | 20 +-
src/utils/linkNote.ts | 42 ++
src/utils/note.ts | 111 ++--
26 files changed, 1237 insertions(+), 501 deletions(-)
create mode 100644 addon/chrome/content/linkNote.xhtml
create mode 100644 addon/chrome/content/styles/linkNote.css
create mode 100644 addon/chrome/content/styles/notePicker.css
create mode 100644 addon/chrome/content/styles/toolbar.css
create mode 100644 src/elements/notePicker.ts
rename src/extras/{workspace.ts => customElements.ts} (86%)
create mode 100644 src/extras/linkNote.ts
create mode 100644 src/modules/relatedNotes.ts
create mode 100644 src/utils/linkNote.ts
diff --git a/addon/chrome/content/linkNote.xhtml b/addon/chrome/content/linkNote.xhtml
new file mode 100644
index 0000000..034ecb7
--- /dev/null
+++ b/addon/chrome/content/linkNote.xhtml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/chrome/content/styles/linkNote.css b/addon/chrome/content/styles/linkNote.css
new file mode 100644
index 0000000..1403699
--- /dev/null
+++ b/addon/chrome/content/styles/linkNote.css
@@ -0,0 +1,33 @@
+.container {
+ height: 100%;
+ margin: 0;
+}
+
+#top-container {
+ gap: 16px;
+ overflow: auto;
+ width: 800px;
+ padding: 2em;
+}
+
+bn-note-picker {
+ border: var(--material-border);
+ min-width: 600px;
+ height: 500px;
+}
+
+#bn-select-note-outline-container {
+ border: var(--material-border);
+ min-width: 300px;
+ height: 500px;
+}
+
+#bn-note-preview-container {
+ border: var(--material-border);
+ min-width: 450px;
+ height: 500px;
+}
+
+#bn-link-insert-position-container {
+ align-items: center;
+}
diff --git a/addon/chrome/content/styles/notePicker.css b/addon/chrome/content/styles/notePicker.css
new file mode 100644
index 0000000..e3347e1
--- /dev/null
+++ b/addon/chrome/content/styles/notePicker.css
@@ -0,0 +1,40 @@
+bn-note-picker {
+ flex-direction: column;
+}
+
+.container {
+ height: 100%;
+ margin: 0;
+}
+
+#select-items-dialog #zotero-select-items-container {
+ gap: 0;
+}
+
+#collections-items-container {
+ display: flex;
+ height: 100%;
+ border-bottom: var(--material-border);
+ user-select: none;
+}
+
+#zotero-collections-tree-container {
+ border-right: var(--material-border);
+}
+
+#zotero-collections-tree {
+ background: var(--material-sidepane);
+}
+
+#select-items-dialog {
+ display: flex;
+ padding: 0;
+}
+
+#select-items-dialog #collections-items-container {
+ margin-bottom: 0;
+}
+
+#bn-select-opened-notes-container {
+ min-width: 200px;
+}
diff --git a/addon/chrome/content/styles/toolbar.css b/addon/chrome/content/styles/toolbar.css
new file mode 100644
index 0000000..aa3f5af
--- /dev/null
+++ b/addon/chrome/content/styles/toolbar.css
@@ -0,0 +1,48 @@
+.toolbar {
+ background: var(--material-toolbar);
+ border-bottom: var(--material-border);
+ padding: 6px;
+ align-items: center;
+ justify-content: space-between !important;
+}
+
+.toolbar-start,
+.toolbar-middle,
+.toolbar-end {
+ align-items: center;
+}
+
+.toolbar-header {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+ overflow: hidden;
+ color: var(--fill-secondary);
+ text-overflow: ellipsis;
+ overflow-wrap: anywhere;
+ line-break: anywhere;
+ font-size: calc(var(--zotero-font-size) * 1.2);
+}
+
+.toolbar-header.content {
+ flex-shrink: 0;
+}
+
+.toolbar-header.highlight {
+ font-size: var(--zotero-font-size);
+ padding: 4px;
+ border: var(--material-border);
+ box-shadow: 0 2px 5px
+ color-mix(in srgb, var(--material-background) 15%, transparent);
+ border-radius: 4px;
+ background: var(--material-background);
+ transition: all 0.3s ease;
+}
+.toolbar-header.highlight:hover {
+ box-shadow: 0 5px 15px
+ color-mix(in srgb, var(--material-background) 20%, transparent);
+ background: var(--color-background50);
+}
+.toolbar-header.highlight:empty {
+ display: none;
+}
diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl
index d532e07..663c97d 100644
--- a/addon/locale/en-US/addon.ftl
+++ b/addon/locale/en-US/addon.ftl
@@ -78,9 +78,9 @@ editor-toolbar-settings-openWorkspace = Open Note Workspace
editor-toolbar-settings-setWorkspace = Set as Workspace Note
editor-toolbar-settings-previewInWorkspace = Preview in Workspace
editor-toolbar-settings-showInLibrary = Show in Library
-editor-toolbar-settings-insertTemplate = Insert Template to Cursor Line
-editor-toolbar-settings-copyLink = Copy Note Link at Line ({ $line })
-editor-toolbar-settings-copyLinkAtSection = Copy Note Link at Section ({ $section })
+editor-toolbar-settings-insertTemplate = Insert template
+editor-toolbar-settings-copyLink = Copy link (L{ $line })
+editor-toolbar-settings-copyLinkAtSection = Copy link (Sec. { $section })
editor-toolbar-settings-openParent = Open Attachment
editor-toolbar-settings-export = Export Current Note...
editor-toolbar-settings-refreshSyncing = Sync Now
diff --git a/addon/locale/it-IT/addon.ftl b/addon/locale/it-IT/addon.ftl
index 8fb74dc..f8b9091 100644
--- a/addon/locale/it-IT/addon.ftl
+++ b/addon/locale/it-IT/addon.ftl
@@ -74,9 +74,9 @@ editor-toolbar-settings-openWorkspace = Apri nota di lavoro
editor-toolbar-settings-setWorkspace = Imposta come nota di lavoro
editor-toolbar-settings-previewInWorkspace = Anteprima nello spazio di lavoro
editor-toolbar-settings-showInLibrary = Show in Library
-editor-toolbar-settings-insertTemplate = Inserisci template nella posizione del cursore
-editor-toolbar-settings-copyLink = Copia link della nota alla riga ({ $line })
-editor-toolbar-settings-copyLinkAtSection = Copia link della nota alla sezione ({ $section })
+editor-toolbar-settings-insertTemplate = Inserisci template
+editor-toolbar-settings-copyLink = Copia link (L{ $line })
+editor-toolbar-settings-copyLinkAtSection = Copia link (Sec. { $section })
editor-toolbar-settings-openParent = Apri allegato
editor-toolbar-settings-export = Esporta nota corrente...
editor-toolbar-settings-refreshSyncing = Sincronizza ora
diff --git a/addon/locale/ru-RU/addon.ftl b/addon/locale/ru-RU/addon.ftl
index 695aab1..7f509c6 100644
--- a/addon/locale/ru-RU/addon.ftl
+++ b/addon/locale/ru-RU/addon.ftl
@@ -78,9 +78,9 @@ editor-toolbar-settings-openWorkspace=Открыть пространство з
editor-toolbar-settings-setWorkspace=Установить как Заметку раб. пространства
editor-toolbar-settings-previewInWorkspace=Предпросмотр в рабочем пространстве
editor-toolbar-settings-showInLibrary = Show in Library
-editor-toolbar-settings-insertTemplate=Вставить шаблон в строку курсора
-editor-toolbar-settings-copyLink = Копировать Ссылку на Заметку на строке ({ $line })
-editor-toolbar-settings-copyLinkAtSection = Копировать Ссылку на Заметку в секции ({ $section })
+editor-toolbar-settings-insertTemplate=Вставить шаблон
+editor-toolbar-settings-copyLink = Копировать Ссылку (L{ $line })
+editor-toolbar-settings-copyLinkAtSection = Копировать Ссылку (Sec. { $section })
editor-toolbar-settings-openParent=Открыть вложение
editor-toolbar-settings-export=Экспортировать текущую заметку...
editor-toolbar-settings-refreshSyncing=Синхронизировать сейчас
diff --git a/addon/locale/tr-TR/addon.ftl b/addon/locale/tr-TR/addon.ftl
index 9764fa2..0bd255b 100644
--- a/addon/locale/tr-TR/addon.ftl
+++ b/addon/locale/tr-TR/addon.ftl
@@ -78,9 +78,9 @@ editor-toolbar-settings-openWorkspace = Not Çalışma Alanı Açın
editor-toolbar-settings-setWorkspace = Çalışma Alanı Notu Olarak Ayarla
editor-toolbar-settings-previewInWorkspace = Çalışma Alanında Ön İzle Preview in Workspace
editor-toolbar-settings-showInLibrary = Show in Library
-editor-toolbar-settings-insertTemplate = İşaretçi Satırına Şablon Ekle
-editor-toolbar-settings-copyLink = Satıra Not Linki Kopyala ({ $line })
-editor-toolbar-settings-copyLinkAtSection = Bölüme Not Linki Kopyala ({ $section })
+editor-toolbar-settings-insertTemplate = Insert template
+editor-toolbar-settings-copyLink = Copy link (L{ $line })
+editor-toolbar-settings-copyLinkAtSection =Copy link (Sec. { $section })
editor-toolbar-settings-openParent = Eki Aç
editor-toolbar-settings-export = Bu Notu Dışa Aktar...
editor-toolbar-settings-refreshSyncing = Senkronize Et
diff --git a/addon/prefs.js b/addon/prefs.js
index 95b1609..34e1fdb 100644
--- a/addon/prefs.js
+++ b/addon/prefs.js
@@ -6,6 +6,8 @@ pref("__prefsPrefix__.syncAttachmentFolder", "attachments");
pref("__prefsPrefix__.autoAnnotation", false);
+pref("__prefsPrefix__.insertLinkPosition", "end");
+
pref("__prefsPrefix__.embedLink", true);
pref("__prefsPrefix__.standaloneLink", false);
pref("__prefsPrefix__.keepLink", true);
diff --git a/src/api.ts b/src/api.ts
index 5bbabaf..9feb17b 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -65,6 +65,7 @@ import {
updateRelatedNotes,
getRelatedNoteIds,
getNoteTreeFlattened,
+ getLinesInNote,
} from "./utils/note";
const workspace = {};
@@ -144,6 +145,7 @@ const editor = {
const note = {
insert: addLineToNote,
+ getLinesInNote,
updateRelatedNotes,
getRelatedNoteIds,
getNoteTreeFlattened,
diff --git a/src/elements/base.ts b/src/elements/base.ts
index 76e10b7..65627d8 100644
--- a/src/elements/base.ts
+++ b/src/elements/base.ts
@@ -11,14 +11,14 @@ export class PluginCEBase extends XULElementBase {
_wrapID(key: string) {
if (key.startsWith(config.addonRef)) {
- return key;
+ return key;
}
return `${config.addonRef}-${key}`;
}
_unwrapID(id: string) {
if (id.startsWith(config.addonRef)) {
- return id.slice(config.addonRef.length + 1);
+ return id.slice(config.addonRef.length + 1);
}
return id;
}
@@ -28,9 +28,9 @@ export class PluginCEBase extends XULElementBase {
}
_parseContentID(dom: DocumentFragment) {
- dom.querySelectorAll("*[id]").forEach(elem => {
- elem.id = this._wrapID(elem.id);
- })
+ dom.querySelectorAll("*[id]").forEach((elem) => {
+ elem.id = this._wrapID(elem.id);
+ });
return dom;
}
diff --git a/src/elements/notePicker.ts b/src/elements/notePicker.ts
new file mode 100644
index 0000000..bd092c6d
--- /dev/null
+++ b/src/elements/notePicker.ts
@@ -0,0 +1,304 @@
+import { config } from "../../package.json";
+import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
+import { PluginCEBase } from "./base";
+
+const _require = window.require;
+const CollectionTree = _require("chrome://zotero/content/collectionTree.js");
+const ItemTree = _require("chrome://zotero/content/itemTree.js");
+const { getCSSItemTypeIcon } = _require("components/icons");
+
+export class NotePicker extends PluginCEBase {
+ itemsView!: _ZoteroTypes.ItemTree;
+ collectionsView!: _ZoteroTypes.CollectionTree;
+ openedNotesView!: VirtualizedTableHelper;
+
+ openedNotes: Zotero.Item[] = [];
+
+ activeSelectionType: "library" | "tabs" | "none" = "none";
+
+ get content() {
+ return MozXULElement.parseXULToFragment(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`);
+ }
+
+ set openedNoteIDs(ids: number[]) {
+ this.openedNotes = Zotero.Items.get(ids).filter((item) => item.isNote());
+ if (this.openedNotesView) {
+ this.openedNotesView.render();
+ return;
+ }
+ this.loadOpenedNotes();
+ }
+
+ async init() {
+ await this.loadLibraryNotes();
+ this.loadQuickSearch();
+ await this.loadOpenedNotes();
+
+ window.addEventListener("unload", () => {
+ this.destroy();
+ });
+ }
+
+ destroy(): void {
+ this.collectionsView.unregister();
+ if (this.itemsView) this.itemsView.unregister();
+ }
+
+ async loadLibraryNotes() {
+ this.itemsView = await ItemTree.init(
+ this.querySelector("#zotero-items-tree"),
+ {
+ onSelectionChange: () => {
+ this.onItemSelected();
+ },
+ id: "select-items-dialog",
+ dragAndDrop: false,
+ persistColumns: true,
+ columnPicker: true,
+ emptyMessage: Zotero.getString("pane.items.loading"),
+ },
+ );
+ this.itemsView.isSelectable = (index: number, selectAll = false) => {
+ const row = this.itemsView.getRow(index);
+ if (!row) {
+ return false;
+ }
+ // @ts-ignore
+ if (!row.ref.isNote()) return false;
+ if (this.itemsView.collectionTreeRow.isTrash()) {
+ // @ts-ignore
+ return row.ref.deleted;
+ } else {
+ // @ts-ignore
+ return this.itemsView._searchItemIDs.has(row.id);
+ }
+ };
+ this.itemsView.setItemsPaneMessage(Zotero.getString("pane.items.loading"));
+
+ // Wait otherwise the collection tree will not be initialized
+ await Zotero.Promise.delay(10);
+ this.collectionsView = await CollectionTree.init(
+ this.querySelector("#zotero-collections-tree"),
+ {
+ onSelectionChange: Zotero.Utilities.debounce(
+ () => this.onCollectionSelected(),
+ 100,
+ ),
+ },
+ );
+ this.collectionsView.hideSources = ["duplicates", "trash", "feeds"];
+
+ await this.collectionsView.makeVisible();
+ }
+
+ loadQuickSearch() {
+ // @ts-ignore
+ const searchBox = document.createXULElement("quick-search-textbox");
+ searchBox.id = "zotero-tb-search";
+ searchBox.setAttribute("timeout", "250");
+ searchBox.setAttribute("dir", "reverse");
+ searchBox.addEventListener("command", this.onSearch);
+ this.querySelector("#search-toolbar > .toolbar-end")?.appendChild(
+ searchBox,
+ );
+
+ Zotero.updateQuickSearchBox(document);
+ }
+
+ async loadOpenedNotes() {
+ const renderLock = Zotero.Promise.defer();
+ this.openedNotesView = new VirtualizedTableHelper(window)
+ .setContainerId("bn-select-opened-notes-tree")
+ .setProp({
+ id: `bn-select-opened-notes-table`,
+ columns: [
+ {
+ dataKey: "title",
+ label: "Opened Notes",
+ flex: 1,
+ },
+ ],
+ showHeader: true,
+ multiSelect: false,
+ staticColumns: true,
+ disableFontSizeScaling: true,
+ })
+ .setProp("getRowCount", () => this.openedNotes.length || 0)
+ .setProp("getRowData", (index) => {
+ const note = this.openedNotes[index];
+ return {
+ title: note.getNoteTitle(),
+ };
+ })
+ .setProp("onSelectionChange", (selection) => {
+ this.onOpenedNoteSelected();
+ })
+ // For find-as-you-type
+ .setProp(
+ "getRowString",
+ (index) => this.openedNotes[index].getNoteTitle() || "",
+ )
+ .setProp("renderItem", (index, selection, oldElem, columns) => {
+ let div;
+ if (oldElem) {
+ div = oldElem;
+ div.innerHTML = "";
+ } else {
+ div = document.createElement("div");
+ div.className = "row";
+ }
+
+ div.classList.toggle("selected", selection.isSelected(index));
+ div.classList.toggle("focused", selection.focused == index);
+ const rowData = this.openedNotes[index];
+
+ for (const column of columns) {
+ const span = document.createElement("span");
+ // @ts-ignore
+ span.className = `cell ${column?.className}`;
+ span.textContent = rowData.getNoteTitle();
+ const icon = getCSSItemTypeIcon("note");
+ icon.classList.add("cell-icon");
+ span.prepend(icon);
+ div.append(span);
+ }
+ return div;
+ })
+ .render(-1, () => {
+ renderLock.resolve();
+ });
+ await renderLock.promise;
+
+ if (this.openedNotes.length === 1) {
+ this.openedNotesView.treeInstance.selection.select(0);
+ }
+ }
+
+ onSearch() {
+ if (this.itemsView) {
+ const searchVal = (
+ this.querySelector("#zotero-tb-search-textbox") as HTMLInputElement
+ )?.value;
+ this.itemsView.setFilter("search", searchVal);
+ }
+ }
+
+ async onCollectionSelected() {
+ const collectionTreeRow = this.collectionsView.getRow(
+ this.collectionsView.selection.focused,
+ );
+ if (!this.collectionsView.selection.count) return;
+ // Collection not changed
+ if (
+ this.itemsView &&
+ this.itemsView.collectionTreeRow &&
+ this.itemsView.collectionTreeRow.id == collectionTreeRow.id
+ ) {
+ return;
+ }
+ // @ts-ignore
+ if (!collectionTreeRow._bnPatched) {
+ // @ts-ignore
+ collectionTreeRow._bnPatched = true;
+ const getItems = collectionTreeRow.getItems.bind(collectionTreeRow);
+ // @ts-ignore
+ collectionTreeRow.getItems = async function () {
+ const items = (await getItems()) as Zotero.Item[];
+ return items.filter((item) => item.isNote()) as unknown[];
+ };
+ }
+ collectionTreeRow.setSearch("");
+ Zotero.Prefs.set("lastViewedFolder", collectionTreeRow.id);
+
+ this.itemsView.setItemsPaneMessage(Zotero.getString("pane.items.loading"));
+
+ // Load library data if necessary
+ const library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
+ if (library) {
+ if (!library.getDataLoaded("item")) {
+ Zotero.debug(
+ "Waiting for items to load for library " + library.libraryID,
+ );
+ await library.waitForDataLoad("item");
+ }
+ }
+
+ await this.itemsView.changeCollectionTreeRow(collectionTreeRow);
+
+ this.itemsView.clearItemsPaneMessage();
+
+ this.collectionsView.runListeners("select");
+ }
+
+ onItemSelected() {
+ this.activeSelectionType = "library";
+ this.dispatchSelectionChange();
+ }
+
+ onOpenedNoteSelected() {
+ this.activeSelectionType = "tabs";
+ this.dispatchSelectionChange();
+ }
+
+ dispatchSelectionChange() {
+ this.dispatchEvent(
+ new CustomEvent("selectionChange", {
+ detail: {
+ selectedNote: this.getSelectedNotes()[0],
+ },
+ }),
+ );
+ }
+
+ getSelectedNotes(): Zotero.Item[] {
+ if (this.activeSelectionType == "none") {
+ return [];
+ } else if (this.activeSelectionType == "library") {
+ return this.itemsView.getSelectedItems();
+ }
+ return Array.from(this.openedNotesView.treeInstance.selection.selected).map(
+ (index) => this.openedNotes[index],
+ );
+ }
+}
diff --git a/src/elements/outlinePane.ts b/src/elements/outlinePane.ts
index 81d816e..43f6785 100644
--- a/src/elements/outlinePane.ts
+++ b/src/elements/outlinePane.ts
@@ -132,7 +132,9 @@ export class OutlinePane extends PluginCEBase {
init(): void {
MozXULElement.insertFTLIfNeeded(`${config.addonRef}-outline.ftl`);
- this._outlineContainer = this._queryID("outline") as unknown as HTMLIFrameElement;
+ this._outlineContainer = this._queryID(
+ "outline",
+ ) as unknown as HTMLIFrameElement;
this._queryID("left-toolbar")?.addEventListener(
"command",
@@ -163,6 +165,7 @@ export class OutlinePane extends PluginCEBase {
extraData: { [key: string]: any },
) {
if (!this.item) return;
+ if (extraData.skipBN) return;
if (event === "modify" && type === "item") {
if ((ids as number[]).includes(this.item.id)) {
this.updateOutline();
@@ -188,10 +191,15 @@ export class OutlinePane extends PluginCEBase {
this.messageHandler,
);
- this._outlineContainer.setAttribute("src", OutlinePane.outlineSources[this.outlineType]);
+ this._outlineContainer.setAttribute(
+ "src",
+ OutlinePane.outlineSources[this.outlineType],
+ );
await waitUtilAsync(
- () => this._outlineContainer.contentWindow?.document.readyState === "complete",
+ () =>
+ this._outlineContainer.contentWindow?.document.readyState ===
+ "complete",
);
this._outlineContainer.contentWindow?.addEventListener(
"message",
diff --git a/src/extras/workspace.ts b/src/extras/customElements.ts
similarity index 86%
rename from src/extras/workspace.ts
rename to src/extras/customElements.ts
index d629838..69e9fa4 100644
--- a/src/extras/workspace.ts
+++ b/src/extras/customElements.ts
@@ -1,5 +1,6 @@
import { ContextPane } from "../elements/context";
import { NoteDetails } from "../elements/detailsPane";
+import { NotePicker } from "../elements/notePicker";
import { OutlinePane } from "../elements/outlinePane";
import { Workspace } from "../elements/workspace";
@@ -8,6 +9,7 @@ const elements = {
"bn-outline": OutlinePane,
"bn-details": NoteDetails as unknown as CustomElementConstructor,
"bn-workspace": Workspace,
+ "bn-note-picker": NotePicker,
};
for (const [key, constructor] of Object.entries(elements)) {
diff --git a/src/extras/linkNote.ts b/src/extras/linkNote.ts
new file mode 100644
index 0000000..93c1615
--- /dev/null
+++ b/src/extras/linkNote.ts
@@ -0,0 +1,325 @@
+import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
+import { config } from "../../package.json";
+import Addon from "../addon";
+import { waitUtilAsync } from "../utils/wait";
+import { getPref, setPref } from "../utils/prefs";
+import { NotePicker } from "../elements/notePicker";
+
+let initialized = false;
+
+let notePicker: NotePicker;
+
+let noteOutlineView: VirtualizedTableHelper;
+let currentNote: Zotero.Item;
+let targetNote: Zotero.Item | undefined;
+let noteOutline: ReturnType = [];
+
+let positionData: NoteNodeData | undefined;
+
+// @ts-ignore
+window.addon = Zotero[config.addonRef];
+
+let io: {
+ currentNoteID: number;
+ openedNoteIDs?: number[];
+ deferred: _ZoteroTypes.DeferredPromise;
+
+ targetNoteID?: number;
+ content?: string;
+ lineIndex?: number;
+};
+
+window.onload = async function () {
+ // Set font size from pref
+ const sbc = document.getElementById("top-container");
+ Zotero.UIProperties.registerRoot(sbc);
+
+ // @ts-ignore
+ io = window.arguments[0];
+
+ loadNotePicker();
+
+ loadInsertPosition();
+
+ loadNoteOutline();
+
+ document.addEventListener("dialogaccept", doAccept);
+
+ currentNote = Zotero.Items.get(io.currentNoteID);
+
+ initialized = true;
+
+ scrollToSection("picker");
+};
+
+window.onunload = function () {
+ io.deferred && io.deferred.resolve();
+};
+
+function loadNotePicker() {
+ notePicker = document.querySelector("bn-note-picker") as NotePicker;
+ notePicker.openedNoteIDs = io.openedNoteIDs || [];
+ const content = document.createElement("span");
+ content.innerHTML = "Step 1. Choose target note:";
+ content.classList.add("toolbar-header", "content");
+ const title = document.createElement("span");
+ title.id = "selected-note-title";
+ title.classList.add("toolbar-header", "highlight");
+ notePicker
+ .querySelector("#search-toolbar .toolbar-start")
+ ?.append(content, title);
+ notePicker.addEventListener("selectionChange", (event: any) => {
+ updateSelectedNotesTitle(event.detail.selectedNote);
+ updateNoteOutline(event.detail.selectedNote);
+ });
+}
+
+function loadInsertPosition() {
+ const insertPosition = document.getElementById(
+ "bn-link-insert-position",
+ ) as HTMLSelectElement;
+ insertPosition.value = getPref("insertLinkPosition") as string;
+ insertPosition.addEventListener("command", () => {
+ setPref("insertLinkPosition", insertPosition.value);
+ updateNotePreview();
+ });
+}
+
+async function loadNoteOutline() {
+ const renderLock = Zotero.Promise.defer();
+ noteOutlineView = new VirtualizedTableHelper(window)
+ .setContainerId("bn-select-note-outline-tree")
+ .setProp({
+ id: `bn-select-note-outline-table`,
+ columns: [
+ {
+ dataKey: "level",
+ label: "Level",
+ width: 50,
+ staticWidth: true,
+ },
+ {
+ dataKey: "name",
+ label: "Table of Contents",
+ flex: 1,
+ },
+ ],
+ showHeader: true,
+ multiSelect: false,
+ staticColumns: true,
+ disableFontSizeScaling: true,
+ })
+ .setProp("getRowCount", () => noteOutline.length || 0)
+ .setProp("getRowData", (index) => {
+ const model = noteOutline[index]?.model;
+ if (!model) return { level: 0, name: "**Unknown**" };
+ return {
+ level: model.level,
+ name: "··".repeat(model.level - 1) + model.name,
+ };
+ })
+ .setProp("onSelectionChange", (selection) => {
+ onOutlineSelected(selection);
+ })
+ // For find-as-you-type
+ .setProp("getRowString", (index) => noteOutline[index]?.model.name || "")
+ .render(-1, () => {
+ renderLock.resolve();
+ });
+ await renderLock.promise;
+
+ // if (openedNotes.length === 1) {
+ // openedNotesView.treeInstance.selection.select(0);
+ // }
+}
+
+function onOutlineSelected(selection: { selected: Set }) {
+ positionData = noteOutline[selection.selected.values().next().value]?.model;
+ updateNotePreview();
+ updateSelectedOutlineTitle();
+}
+
+function updateSelectedNotesTitle(noteItem?: Zotero.Item) {
+ const title = noteItem ? noteItem.getNoteTitle() : "";
+ document.querySelector("#selected-note-title")!.textContent = title;
+}
+
+function updateSelectedOutlineTitle() {
+ const selectedOutline =
+ noteOutline[
+ noteOutlineView.treeInstance.selection.selected.values().next().value
+ ];
+ const title = selectedOutline ? selectedOutline.model.name : "";
+ document.querySelector("#selected-outline-title")!.textContent = title;
+}
+
+function updatePreviewTitle() {
+ document.querySelector("#preview-note-from-title")!.textContent =
+ currentNote.getNoteTitle() || "No title";
+ document.querySelector("#preview-note-middle-title")!.textContent = "to";
+ document.querySelector("#preview-note-to-title")!.textContent =
+ targetNote?.getNoteTitle() || "No title";
+}
+
+async function updateNoteOutline(noteItem?: Zotero.Item) {
+ if (!noteItem) {
+ targetNote = undefined;
+ noteOutline = [];
+ } else {
+ targetNote = noteItem;
+ noteOutline = addon.api.note.getNoteTreeFlattened(targetNote);
+ }
+ noteOutlineView?.render(undefined);
+ // Set default line index to the end of the note
+ positionData = undefined;
+ if (targetNote) scrollToSection("outline");
+}
+
+async function updateNotePreview() {
+ if (!initialized || !targetNote) return;
+ const lines = await addon.api.note.getLinesInNote(targetNote, {
+ convertToHTML: true,
+ });
+ let index = getIndexToInsert();
+ if (index < 0) {
+ index = lines.length;
+ } else {
+ scrollToSection("preview");
+ }
+ const before = lines.slice(0, index).join("\n");
+ const after = lines.slice(index).join("\n");
+
+ // TODO: use index or section
+ const content = await getContentToInsert();
+
+ const iframe = document.querySelector(
+ "#bn-note-preview",
+ ) as HTMLIFrameElement;
+
+ const activeElement = document.activeElement as HTMLElement; // 保存当前活动元素
+
+ iframe!.contentDocument!.documentElement.innerHTML = `
+
+
+
+
+
+
+
+
+ ${before}
+ ${content}
+ ${after}
+
+
+`;
+ activeElement?.focus();
+ await waitUtilAsync(() => iframe.contentDocument?.readyState === "complete");
+
+ // Scroll the inserted section into the center of the iframe
+ const inserted = iframe.contentDocument?.getElementById("inserted");
+ if (inserted) {
+ const rect = inserted.getBoundingClientRect();
+ const container = inserted.parentElement!;
+ container.scrollTo({
+ top:
+ container.scrollTop +
+ rect.top -
+ container.clientHeight / 2 +
+ rect.height,
+ behavior: "smooth",
+ });
+ }
+
+ updatePreviewTitle();
+}
+
+function scrollToSection(type: "picker" | "outline" | "preview") {
+ if (!initialized) return;
+ const querier = {
+ picker: "#zotero-select-items-container",
+ outline: "#bn-select-note-outline-container",
+ preview: "#bn-note-preview-container",
+ };
+ const container = document.querySelector(querier[type]);
+ if (!container) return;
+ container.scrollIntoView({
+ behavior: "smooth",
+ inline: "center",
+ });
+}
+
+async function getContentToInsert() {
+ const forwardLink = addon.api.convert.note2link(currentNote, {});
+ const content = await addon.api.template.runTemplate(
+ "[QuickInsertV2]",
+ "link, linkText, subNoteItem, noteItem",
+ [
+ forwardLink,
+ currentNote.getNoteTitle().trim() || forwardLink,
+ currentNote,
+ targetNote,
+ ],
+ {
+ dryRun: true,
+ },
+ );
+ return content;
+}
+
+function getIndexToInsert() {
+ if (!positionData) return -1;
+ let position = getPref("insertLinkPosition") as string;
+ if (!["start", "end"].includes(position)) {
+ position = "end";
+ }
+ let index = {
+ start: positionData.lineIndex + 1,
+ end: positionData.endIndex + 1,
+ }[position];
+ if (index === undefined) {
+ index = -1;
+ }
+ return index;
+}
+
+async function doAccept() {
+ if (!targetNote) return;
+ const content = await getContentToInsert();
+
+ io.targetNoteID = targetNote.id;
+ io.content = content;
+ io.lineIndex = getIndexToInsert();
+}
diff --git a/src/hooks.ts b/src/hooks.ts
index 7c52a59..493bccc 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -35,6 +35,7 @@ import { waitUtilAsync } from "./utils/wait";
import { initSyncList } from "./modules/sync/api";
import { getPref } from "./utils/prefs";
import { patchViewItems } from "./modules/viewItems";
+import { onUpdateRelated } from "./modules/relatedNotes";
async function onStartup() {
await Promise.all([
@@ -67,7 +68,7 @@ async function onMainWindowLoad(win: Window): Promise {
await waitUtilAsync(() => document.readyState === "complete");
Services.scriptloader.loadSubScript(
- `chrome://${config.addonRef}/content/scripts/workspace.js`,
+ `chrome://${config.addonRef}/content/scripts/customElements.js`,
win,
);
// Create ztoolkit for every window
@@ -121,6 +122,7 @@ function onNotify(
skipActive: true,
reason: "item-modify",
});
+ addon.hooks.onUpdateRelated(modifiedNotes, { skipActive: true });
}
} else {
return;
@@ -164,7 +166,7 @@ function onOpenNote(
// addon.hooks.onSetWorkspaceNote(noteId, "preview", options);
break;
case "workspace":
- // addon.hooks.onSetWorkspaceNote(noteId, "main", options);
+ addon.hooks.onOpenWorkspace(noteItem, "tab");
break;
case "standalone":
ZoteroPane.openNoteWindow(noteId);
@@ -247,6 +249,7 @@ export default {
onOpenWorkspace,
onToggleWorkspacePane,
onSyncing,
+ onUpdateRelated,
onShowTemplatePicker,
onUpdateTemplatePicker,
onImportTemplateFromClipboard,
diff --git a/src/modules/convert/api.ts b/src/modules/convert/api.ts
index 013e26e..39b8792 100644
--- a/src/modules/convert/api.ts
+++ b/src/modules/convert/api.ts
@@ -175,7 +175,7 @@ async function note2noteDiff(noteItem: Zotero.Item) {
function note2link(
noteItem: Zotero.Item,
- options: Parameters[1],
+ options: Parameters[1] = {},
) {
return getNoteLink(noteItem, options);
}
@@ -236,7 +236,7 @@ function annotations2html(
async function note2html(
noteItems: Zotero.Item | Zotero.Item[],
- options: { targetNoteItem?: Zotero.Item; html?: string },
+ options: { targetNoteItem?: Zotero.Item; html?: string } = {},
) {
if (!Array.isArray(noteItems)) {
noteItems = [noteItems];
diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts
index 89c0bec..525ab5e 100644
--- a/src/modules/editor/toolbar.ts
+++ b/src/modules/editor/toolbar.ts
@@ -2,431 +2,212 @@ import { config } from "../../../package.json";
import { ICONS } from "../../utils/config";
import { getLineAtCursor, getSectionAtCursor } from "../../utils/editor";
import { showHint } from "../../utils/hint";
-import { getNoteLink, getNoteLinkParams } from "../../utils/link";
+import { getNoteLink } from "../../utils/link";
import { getString } from "../../utils/locale";
-import { addLineToNote, getNoteTreeFlattened } from "../../utils/note";
-import { getPref } from "../../utils/prefs";
+import { openLinkNoteDialog } from "../../utils/linkNote";
import { slice } from "../../utils/str";
export async function initEditorToolbar(editor: Zotero.EditorInstance) {
const noteItem = editor._item;
- const toolbar = await registerEditorToolbar(editor, makeId("toolbar"));
- // Settings
- const settingsButton = await registerEditorToolbarDropdown(
+ const _document = editor._iframeWindow.document;
+ registerEditorToolbarElement(
editor,
- toolbar,
- makeId("settings"),
- ICONS.settings,
- getString("editor.toolbar.settings.title"),
- "end",
- (e) => {},
+ _document.querySelector(".toolbar") as HTMLDivElement,
+ "start",
+ ztoolkit.UI.createElement(_document, "button", {
+ classList: ["toolbar-button"],
+ properties: {
+ innerHTML: ICONS.addon,
+ title: "Link current note to another note",
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ openLinkNoteDialog(noteItem);
+ },
+ },
+ ],
+ }) as HTMLButtonElement,
);
- settingsButton.addEventListener("click", async (ev) => {
- ev.stopPropagation();
- function removePopup() {
- const popup = editor._iframeWindow.document.querySelector(
- `#${makeId("settings-popup")}`,
- );
- if (popup) {
- popup.remove();
- settingsButton
- .querySelector(".toolbar-button")
- ?.classList.remove("active");
- editor._iframeWindow.document.removeEventListener("click", removePopup);
- return true;
+ const settingsButton = editor._iframeWindow.document.querySelector(
+ ".toolbar .end .dropdown .toolbar-button",
+ ) as HTMLDivElement;
+
+ const MutationObserver = // @ts-ignore
+ editor._iframeWindow.MutationObserver as typeof window.MutationObserver;
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach(async (mutation) => {
+ if (
+ mutation.type === "attributes" &&
+ mutation.attributeName === "class" &&
+ mutation.target === settingsButton
+ ) {
+ if (settingsButton.classList.contains("active")) {
+ const dropdown = settingsButton.parentElement!;
+ const popup = dropdown.querySelector(".popup") as HTMLDivElement;
+ ztoolkit.log(popup);
+ registerEditorToolbarPopup(editor, popup, await getMenuData(editor));
+ }
}
- return false;
- }
-
- if (removePopup()) {
- return;
- }
-
- const currentLine = getLineAtCursor(editor);
- const currentSection = getSectionAtCursor(editor);
- const settingsMenuData: PopupData[] = [
- {
- id: makeId("settings-openWorkspace"),
- text: getString("editor.toolbar.settings.openWorkspace"),
- callback: (e) => {
- addon.hooks.onOpenWorkspace(noteItem, "tab");
- },
- },
- {
- id: makeId("settings-showInLibrary"),
- text: getString("editor.toolbar.settings.showInLibrary"),
- callback: (e) => {
- ZoteroPane.selectItems([e.editor._item.id]);
- },
- },
- ];
-
- if (currentLine >= 0) {
- settingsMenuData.push(
- ...([
- {
- type: "splitter",
- },
- {
- id: makeId("settings-export"),
- text: getString("editor.toolbar.settings.export"),
- callback: (e) => {
- if (addon.api.sync.isSyncNote(noteItem.id)) {
- addon.hooks.onShowSyncInfo(noteItem.id);
- } else {
- addon.hooks.onShowExportNoteOptions([noteItem.id]);
- }
- },
- },
- {
- type: "splitter",
- },
- {
- id: makeId("settings-insertTemplate"),
- text: getString("editor.toolbar.settings.insertTemplate"),
- callback: (e) => {
- addon.hooks.onShowTemplatePicker("insert", {
- noteId: e.editor._item.id,
- lineIndex: currentLine,
- });
- },
- },
- {
- type: "splitter",
- },
- {
- id: makeId("settings-copyLink"),
- text: getString("editor.toolbar.settings.copyLink", {
- args: {
- line: currentLine,
- },
- }),
- callback: (e) => {
- const link =
- getNoteLink(e.editor._item, {
- lineIndex: currentLine,
- }) || "";
- new ztoolkit.Clipboard()
- .addText(link, "text/unicode")
- .addText(
- `${
- e.editor._item.getNoteTitle().trim() || link
- }`,
- "text/html",
- )
- .copy();
- showHint(`Link ${link} copied`);
- },
- },
- {
- id: makeId("settings-copyLinkAtSection"),
- text: getString("editor.toolbar.settings.copyLinkAtSection", {
- args: {
- section: currentSection,
- },
- }),
- callback: (e) => {
- const link =
- getNoteLink(e.editor._item, {
- sectionName: currentSection,
- }) || "";
- new ztoolkit.Clipboard()
- .addText(link, "text/unicode")
- .addText(
- `${
- e.editor._item.getNoteTitle().trim() || link
- }`,
- "text/html",
- )
- .copy();
- showHint(`Link ${link} copied`);
- },
- },
- {
- id: makeId("settings-updateRelatedNotes"),
- text: getString("editor-toolbar-settings-updateRelatedNotes"),
- callback: (e) => {
- addon.api.note.updateRelatedNotes(e.editor._item.id);
- },
- },
- ]),
- );
- }
-
- const parentAttachment = await noteItem.parentItem?.getBestAttachment();
- if (parentAttachment) {
- settingsMenuData.push(
- ...([
- {
- type: "splitter",
- },
- {
- id: makeId("settings-openParent"),
- text: getString("editor.toolbar.settings.openParent"),
- callback: (e) => {
- ZoteroPane.viewAttachment([parentAttachment.id]);
- Zotero.Notifier.trigger("open", "file", parentAttachment.id);
- },
- },
- ]),
- );
- }
-
- if (addon.api.sync.isSyncNote(noteItem.id)) {
- settingsMenuData.splice(5, 0, {
- id: makeId("settings-refreshSyncing"),
- text: getString("editor.toolbar.settings.refreshSyncing"),
- callback: (e) => {
- addon.hooks.onSyncing(undefined, {
- quiet: false,
- skipActive: false,
- reason: "manual-editor",
- });
- },
- });
- }
-
- registerEditorToolbarPopup(
- editor,
- settingsButton,
- `${config.addonRef}-settings-popup`,
- "right",
- settingsMenuData,
- ).then((popup) => {
- settingsButton.querySelector(".toolbar-button")?.classList.add("active");
- editor._iframeWindow.document.addEventListener("click", removePopup);
});
});
-
- // Center button
-
- const onTriggerMenu = (ev: MouseEvent) => {
- editor._iframeWindow.focus();
- const linkMenu: PopupData[] = getLinkMenuData(editor);
- editor._iframeWindow.document
- .querySelector(`#${makeId("link")}`)!
- .querySelector(".toolbar-button")!.innerHTML = ICONS.linkAfter;
-
- const popup = registerEditorToolbarPopup(
- editor,
- linkButton,
- `${config.addonRef}-link-popup`,
- "middle",
- linkMenu,
- );
- };
-
- const onExitMenu = (ev: MouseEvent) => {
- editor._iframeWindow.document
- .querySelector(`#${makeId("link-popup")}`)
- ?.remove();
- editor._iframeWindow.document
- .querySelector(`#${makeId("link")}`)!
- .querySelector(".toolbar-button")!.innerHTML = ICONS.addon;
- };
-
- const onClickMenu = async (ev: MouseEvent) => {
- // TODO: fix link
- return;
- // const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null;
- // if (!mainNote?.isNote()) {
- // return;
- // }
- // const lineIndex = parseInt(
- // (ev.target as HTMLDivElement).id.split("-").pop() || "-1",
- // );
- // const forwardLink = getNoteLink(noteItem);
- // const backLink = getNoteLink(mainNote, { ignore: true, lineIndex });
- // addLineToNote(
- // mainNote,
- // await addon.api.template.runTemplate(
- // "[QuickInsertV2]",
- // "link, linkText, subNoteItem, noteItem",
- // [
- // forwardLink,
- // noteItem.getNoteTitle().trim() || forwardLink,
- // noteItem,
- // mainNote,
- // ],
- // ),
- // lineIndex,
- // );
- // addLineToNote(
- // noteItem,
- // await addon.api.template.runTemplate(
- // "[QuickBackLinkV2]",
- // "link, linkText, subNoteItem, noteItem",
- // [
- // backLink,
- // mainNote.getNoteTitle().trim() || "Workspace Note",
- // noteItem,
- // mainNote,
- // "",
- // ],
- // ),
- // );
- // onExitMenu(ev);
- // ev.stopPropagation();
- };
-
- const linkButton = await registerEditorToolbarDropdown(
- editor,
- toolbar,
- makeId("link"),
- ICONS.addon,
- getString("editor.toolbar.link.title"),
- "middle",
- onClickMenu,
- );
-
- linkButton.addEventListener("mouseenter", onTriggerMenu);
- linkButton.addEventListener("mouseleave", onExitMenu);
- linkButton.addEventListener("mouseleave", onExitMenu);
- linkButton.addEventListener("click", (ev) => {
- if ((ev.target as HTMLElement).classList.contains("option")) {
- onClickMenu(ev);
- }
+ observer.observe(settingsButton, {
+ attributes: true,
+ attributeFilter: ["class"],
});
- editor._iframeWindow.document.addEventListener("click", onExitMenu);
-
- // Export
- // const exportButton = await registerEditorToolbarDropdown(
- // editor,
- // toolbar,
- // makeId("export"),
- // ICONS.export,
- // getString("editor.toolbar.export.title"),
- // "end",
- // (e) => {
- // if (addon.api.sync.isSyncNote(noteItem.id)) {
- // addon.hooks.onShowSyncInfo(noteItem.id);
- // } else {
- // addon.hooks.onShowExportNoteOptions([noteItem.id]);
- // }
- // }
- // );
}
-function getLinkMenuData(editor: Zotero.EditorInstance): PopupData[] {
- return [];
- // const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId) || null;
- // const currentNote = editor._item;
- // if (!workspaceNote?.isNote()) {
- // return [
- // {
- // id: makeId("link-popup-nodata"),
- // text: getString("editor.toolbar.link.popup.nodata"),
- // },
- // ];
- // }
- // const nodes = getNoteTreeFlattened(workspaceNote, {
- // keepLink: true,
- // });
- // const menuData: PopupData[] = [];
- // for (const node of nodes) {
- // if (node.model.level === 7) {
- // const lastMenu =
- // menuData.length > 0 ? menuData[menuData.length - 1] : null;
- // const linkNote = getNoteLinkParams(node.model.link).noteItem;
- // if (linkNote && linkNote.id === currentNote.id && lastMenu) {
- // lastMenu.suffix = "🔗";
- // }
- // continue;
- // }
- // menuData.push({
- // id: makeId(
- // `link-popup-${
- // getPref("editor.link.insertPosition")
- // ? node.model.lineIndex - 1
- // : node.model.endIndex
- // }`,
- // ),
- // text: node.model.name,
- // prefix: "·".repeat(node.model.level - 1),
- // });
- // }
- // return menuData;
-}
+async function getMenuData(editor: Zotero.EditorInstance) {
+ const noteItem = editor._item;
-async function registerEditorToolbar(
- editor: Zotero.EditorInstance,
- id: string,
-) {
- await editor._initPromise;
- const _document = editor._iframeWindow.document;
- const toolbar = ztoolkit.UI.createElement(_document, "div", {
- attributes: {
- id,
+ const currentLine = getLineAtCursor(editor);
+ const currentSection = slice(getSectionAtCursor(editor) || "", 10);
+ const settingsMenuData: PopupData[] = [
+ {
+ id: makeId("settings-openWorkspace"),
+ text: getString("editor.toolbar.settings.openWorkspace"),
+ callback: (e) => {
+ addon.hooks.onOpenWorkspace(noteItem, "tab");
+ },
},
- classList: ["toolbar"],
- children: [
- {
- tag: "div",
- classList: ["start"],
+ {
+ id: makeId("settings-showInLibrary"),
+ text: getString("editor.toolbar.settings.showInLibrary"),
+ callback: (e) => {
+ ZoteroPane.selectItems([e.editor._item.id]);
},
- {
- tag: "div",
- classList: ["middle"],
- },
- {
- tag: "div",
- classList: ["end"],
- },
- ],
- ignoreIfExists: true,
- }) as HTMLDivElement;
- _document.querySelector(".editor")?.childNodes[0].before(toolbar);
- return toolbar;
-}
-
-async function registerEditorToolbarDropdown(
- editor: Zotero.EditorInstance,
- toolbar: HTMLDivElement,
- id: string,
- icon: string,
- title: string,
- position: "start" | "middle" | "end",
- callback: (e: MouseEvent & { editor: Zotero.EditorInstance }) => any,
-) {
- await editor._initPromise;
- const _document = editor._iframeWindow.document;
- const dropdown = ztoolkit.UI.createElement(_document, "div", {
- attributes: {
- id,
- title,
},
- classList: ["dropdown", "more-dropdown"],
- children: [
- {
- tag: "button",
- attributes: {
- title,
+ ];
+
+ if (currentLine >= 0) {
+ settingsMenuData.push(
+ ...([
+ {
+ type: "splitter",
},
- properties: {
- innerHTML: icon,
- },
- classList: ["toolbar-button"],
- listeners: [
- {
- type: "click",
- listener: (e) => {
- Object.assign(e, { editor });
- if (callback) {
- callback(
- e as any as MouseEvent & { editor: Zotero.EditorInstance },
- );
- }
- },
+ {
+ id: makeId("settings-export"),
+ text: getString("editor.toolbar.settings.export"),
+ callback: (e) => {
+ if (addon.api.sync.isSyncNote(noteItem.id)) {
+ addon.hooks.onShowSyncInfo(noteItem.id);
+ } else {
+ addon.hooks.onShowExportNoteOptions([noteItem.id]);
+ }
},
- ],
+ },
+ {
+ type: "splitter",
+ },
+ {
+ id: makeId("settings-insertTemplate"),
+ text: getString("editor.toolbar.settings.insertTemplate"),
+ callback: (e) => {
+ addon.hooks.onShowTemplatePicker("insert", {
+ noteId: e.editor._item.id,
+ lineIndex: currentLine,
+ });
+ },
+ },
+ {
+ type: "splitter",
+ },
+ {
+ id: makeId("settings-copyLink"),
+ text: getString("editor.toolbar.settings.copyLink", {
+ args: {
+ line: currentLine,
+ },
+ }),
+ callback: (e) => {
+ const link =
+ getNoteLink(e.editor._item, {
+ lineIndex: currentLine,
+ }) || "";
+ new ztoolkit.Clipboard()
+ .addText(link, "text/unicode")
+ .addText(
+ `${
+ e.editor._item.getNoteTitle().trim() || link
+ }`,
+ "text/html",
+ )
+ .copy();
+ showHint(`Link ${link} copied`);
+ },
+ },
+ {
+ id: makeId("settings-copyLinkAtSection"),
+ text: getString("editor.toolbar.settings.copyLinkAtSection", {
+ args: {
+ section: currentSection,
+ },
+ }),
+ callback: (e) => {
+ const link =
+ getNoteLink(e.editor._item, {
+ sectionName: currentSection,
+ }) || "";
+ new ztoolkit.Clipboard()
+ .addText(link, "text/unicode")
+ .addText(
+ `${
+ e.editor._item.getNoteTitle().trim() || link
+ }`,
+ "text/html",
+ )
+ .copy();
+ showHint(`Link ${link} copied`);
+ },
+ },
+ {
+ id: makeId("settings-updateRelatedNotes"),
+ text: getString("editor-toolbar-settings-updateRelatedNotes"),
+ callback: (e) => {
+ addon.api.note.updateRelatedNotes(e.editor._item.id);
+ },
+ },
+ ]),
+ );
+ }
+
+ const parentAttachment = await noteItem.parentItem?.getBestAttachment();
+ if (parentAttachment) {
+ settingsMenuData.push(
+ ...([
+ {
+ type: "splitter",
+ },
+ {
+ id: makeId("settings-openParent"),
+ text: getString("editor.toolbar.settings.openParent"),
+ callback: (e) => {
+ ZoteroPane.viewAttachment([parentAttachment.id]);
+ Zotero.Notifier.trigger("open", "file", parentAttachment.id);
+ },
+ },
+ ]),
+ );
+ }
+
+ if (addon.api.sync.isSyncNote(noteItem.id)) {
+ settingsMenuData.splice(5, 0, {
+ id: makeId("settings-refreshSyncing"),
+ text: getString("editor.toolbar.settings.refreshSyncing"),
+ callback: (e) => {
+ addon.hooks.onSyncing(undefined, {
+ quiet: false,
+ skipActive: false,
+ reason: "manual-editor",
+ });
},
- ],
- skipIfExists: true,
- });
- toolbar.querySelector(`.${position}`)?.append(dropdown);
- return dropdown;
+ });
+ }
+
+ return settingsMenuData;
}
declare interface PopupData {
@@ -440,21 +221,18 @@ declare interface PopupData {
async function registerEditorToolbarPopup(
editor: Zotero.EditorInstance,
- dropdown: HTMLDivElement,
- id: string,
- align: "middle" | "left" | "right",
+ popup: HTMLDivElement,
popupLines: PopupData[],
) {
await editor._initPromise;
- const popup = ztoolkit.UI.appendElement(
+ ztoolkit.UI.appendElement(
{
- tag: "div",
- classList: ["popup"],
- id,
+ tag: "fragment",
children: popupLines.map((props) => {
return props.type === "splitter"
? {
- tag: "hr",
+ tag: "div",
+ classList: ["separator"],
properties: {
id: props.id,
},
@@ -485,20 +263,9 @@ async function registerEditorToolbarPopup(
],
};
}),
- removeIfExists: true,
},
- dropdown,
+ popup,
) as HTMLDivElement;
- let style: string = "";
- if (align === "middle") {
- style = `right: -${popup.offsetWidth / 2 - 15}px;`;
- } else if (align === "left") {
- style = "left: 0; right: auto;";
- } else if (align === "right") {
- style = "right: 0;";
- }
- popup.setAttribute("style", style);
- return popup;
}
async function registerEditorToolbarElement(
diff --git a/src/modules/export/markdown.ts b/src/modules/export/markdown.ts
index 3b33186..7f5a1da 100644
--- a/src/modules/export/markdown.ts
+++ b/src/modules/export/markdown.ts
@@ -8,7 +8,7 @@ export async function saveMD(
options: {
keepNoteLink?: boolean;
withYAMLHeader?: boolean;
- },
+ } = {},
) {
const noteItem = Zotero.Items.get(noteId);
const dir = jointPath(...PathUtils.split(formatPath(filename)).slice(0, -1));
diff --git a/src/modules/export/pdf.ts b/src/modules/export/pdf.ts
index 23962c4..4f49e16 100644
--- a/src/modules/export/pdf.ts
+++ b/src/modules/export/pdf.ts
@@ -23,7 +23,6 @@ export async function savePDF(noteId: number) {
}
function disablePrintFooterHeader() {
- // @ts-ignore
Zotero.Prefs.resetBranch([], "print");
Zotero.Prefs.set("print.print_footercenter", "", true);
Zotero.Prefs.set("print.print_footerleft", "", true);
diff --git a/src/modules/relatedNotes.ts b/src/modules/relatedNotes.ts
new file mode 100644
index 0000000..8777fa2
--- /dev/null
+++ b/src/modules/relatedNotes.ts
@@ -0,0 +1,31 @@
+import { getPref } from "../utils/prefs";
+
+export { onUpdateRelated };
+
+function onUpdateRelated(
+ items: Zotero.Item[] = [],
+ { skipActive } = {
+ skipActive: true,
+ },
+) {
+ if (!getPref("workspace.autoUpdateRelatedNotes")) {
+ return;
+ }
+ if (skipActive) {
+ // Skip active note editors' targets
+ const activeNoteIds = Zotero.Notes._editorInstances
+ .filter(
+ (editor) =>
+ !Components.utils.isDeadWrapper(editor._iframeWindow) &&
+ editor._iframeWindow.document.hasFocus(),
+ )
+ .map((editor) => editor._item.id);
+ const filteredItems = items.filter(
+ (item) => !activeNoteIds.includes(item.id),
+ );
+ items = filteredItems;
+ }
+ for (const item of items) {
+ addon.api.note.updateRelatedNotes(item.id);
+ }
+}
diff --git a/src/modules/sync/managerWindow.ts b/src/modules/sync/managerWindow.ts
index 493b783..4bfa6c2 100644
--- a/src/modules/sync/managerWindow.ts
+++ b/src/modules/sync/managerWindow.ts
@@ -99,7 +99,6 @@ export async function showSyncManager() {
"getRowString",
(index) => addon.data.prefs?.rows[index].title || "",
)
- // @ts-ignore TODO: Fix type in zotero-plugin-toolkit
.setProp("onColumnSort", (columnIndex, ascending) => {
addon.data.sync.manager.columnIndex = columnIndex;
addon.data.sync.manager.columnAscending = ascending > 0;
diff --git a/src/modules/template/api.ts b/src/modules/template/api.ts
index 98fdd40..4b48f30 100644
--- a/src/modules/template/api.ts
+++ b/src/modules/template/api.ts
@@ -109,7 +109,7 @@ async function runTextTemplate(
options: {
targetNoteId?: number;
dryRun?: boolean;
- },
+ } = {},
) {
const { targetNoteId, dryRun } = options;
const targetNoteItem = Zotero.Items.get(targetNoteId || -1);
@@ -130,7 +130,7 @@ async function runItemTemplate(
itemIds?: number[];
targetNoteId?: number;
dryRun?: boolean;
- },
+ } = {},
): Promise {
/**
* args:
diff --git a/src/utils/config.ts b/src/utils/config.ts
index 9d230b3..5bd67ff 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -1,6 +1,6 @@
export const ICONS = {
- settings: ``,
- addon: ``,
+ settings: ``,
+ addon: ``,
linkAfter: `
`,
- previewImage: ``,
- resizeImage: ``,
- imageViewerPin: ``,
- imageViewerPined: ``,
+ previewImage: ``,
+ resizeImage: ``,
+ imageViewerPin: ``,
+ imageViewerPined: ``,
switchOutline: ``,
workspace_notes_collapsed: ``,
workspace_notes_open: ``,
- readerQuickNote: ``,
- embedLinkContent: ``,
- updateLinkText: ``,
- openInNewWindow: ``,
+ readerQuickNote: ``,
+ embedLinkContent: ``,
+ updateLinkText: ``,
+ openInNewWindow: ``,
};
export const PROGRESS_TITLE = "Better Notes";
diff --git a/src/utils/linkNote.ts b/src/utils/linkNote.ts
new file mode 100644
index 0000000..0a64da3
--- /dev/null
+++ b/src/utils/linkNote.ts
@@ -0,0 +1,42 @@
+import { config } from "../../package.json";
+
+export { openLinkNoteDialog };
+
+async function openLinkNoteDialog(currentNote: Zotero.Item) {
+ const io = {
+ openedNoteIDs: Zotero_Tabs._tabs
+ .map((tab) => tab.data?.itemID)
+ .filter((id) => id),
+ currentNoteID: currentNote.id,
+ deferred: Zotero.Promise.defer(),
+ } as any;
+ window.openDialog(
+ `chrome://${config.addonRef}/content/linkNote.xhtml`,
+ "_blank",
+ "chrome,modal,centerscreen,resizable=no",
+ io,
+ );
+ await io.deferred.promise;
+
+ const targetNote = Zotero.Items.get(io.targetNoteID);
+ const content = io.content;
+ const lineIndex = io.lineIndex;
+
+ if (!targetNote || !content) return;
+
+ await addon.api.note.insert(targetNote, content, lineIndex);
+
+ await Zotero.DB.executeTransaction(async () => {
+ const saveParams = {
+ skipDateModifiedUpdate: true,
+ skipSelect: true,
+ notifierData: {
+ skipBN: true,
+ },
+ };
+ targetNote.addRelatedItem(currentNote);
+ currentNote.addRelatedItem(targetNote);
+ targetNote.save(saveParams);
+ currentNote.save(saveParams);
+ });
+}
diff --git a/src/utils/note.ts b/src/utils/note.ts
index b15a549..ac85f97 100644
--- a/src/utils/note.ts
+++ b/src/utils/note.ts
@@ -4,7 +4,7 @@ import { getEditorInstance, getPositionAtLine, insert } from "./editor";
import { formatPath, getItemDataURL } from "./str";
import { showHint } from "./hint";
import { config } from "../../package.json";
-import { getNoteLinkParams } from "./link";
+import { getNoteLink, getNoteLinkParams } from "./link";
export {
renderNoteHTML,
@@ -111,11 +111,32 @@ function parseHTMLLines(html: string): string[] {
return parsedLines;
}
-function getLinesInNote(note: Zotero.Item): string[] {
+function getLinesInNote(note: Zotero.Item): string[];
+
+async function getLinesInNote(
+ note: Zotero.Item,
+ options: {
+ convertToHTML?: true;
+ },
+): Promise;
+
+function getLinesInNote(
+ note: Zotero.Item,
+ options?: {
+ convertToHTML?: boolean;
+ },
+): string[] | Promise {
if (!note) {
return [];
}
const noteText: string = note.getNote();
+ if (options?.convertToHTML) {
+ return new Promise((resolve) => {
+ addon.api.convert.note2html(note).then((html) => {
+ resolve(parseHTMLLines(html));
+ });
+ });
+ }
return parseHTMLLines(noteText);
}
@@ -162,7 +183,7 @@ async function addLineToNote(
const editor = getEditorInstance(note.id);
if (editor && !forceMetadata) {
// The note is opened. Add line via note editor
- const pos = getPositionAtLine(editor, lineIndex, "end");
+ const pos = getPositionAtLine(editor, lineIndex, "start");
ztoolkit.log("Add note line via note editor", pos);
insert(editor, html, pos);
// The selection is automatically moved to the next line
@@ -524,24 +545,8 @@ async function updateRelatedNotes(noteID: number) {
ztoolkit.log(`updateRelatedNotes: ${noteID} is not a note.`);
return;
}
- const relatedNoteIDs = await getRelatedNoteIds(noteID);
- const relatedNotes = Zotero.Items.get(relatedNoteIDs);
- const currentRelatedNotes = {} as Record;
+ const { detectedIDSet, currentIDSet } = await getRelatedNoteIds(noteID);
- // Get current related items
- for (const relItemKey of noteItem.relatedItems) {
- try {
- const relItem = (await Zotero.Items.getByLibraryAndKeyAsync(
- noteItem.libraryID,
- relItemKey,
- )) as Zotero.Item;
- if (relItem.isNote()) {
- currentRelatedNotes[relItem.id] = relItem;
- }
- } catch (e) {
- ztoolkit.log(e);
- }
- }
await Zotero.DB.executeTransaction(async () => {
const saveParams = {
skipDateModifiedUpdate: true,
@@ -550,18 +555,18 @@ async function updateRelatedNotes(noteID: number) {
skipBN: true,
},
};
- for (const toAddNote of relatedNotes) {
- if (toAddNote.id in currentRelatedNotes) {
+ for (const toAddNote of Zotero.Items.get(Array.from(detectedIDSet))) {
+ if (currentIDSet.has(toAddNote.id)) {
// Remove existing notes from current dict for later process
- delete currentRelatedNotes[toAddNote.id];
+ currentIDSet.delete(toAddNote.id);
continue;
}
toAddNote.addRelatedItem(noteItem);
noteItem.addRelatedItem(toAddNote);
toAddNote.save(saveParams);
- delete currentRelatedNotes[toAddNote.id];
+ currentIDSet.delete(toAddNote.id);
}
- for (const toRemoveNote of Object.values(currentRelatedNotes)) {
+ for (const toRemoveNote of Zotero.Items.get(Array.from(currentIDSet))) {
// Remove related notes that are not in the new list
toRemoveNote.removeRelatedItem(noteItem);
noteItem.removeRelatedItem(toRemoveNote);
@@ -571,21 +576,49 @@ async function updateRelatedNotes(noteID: number) {
});
}
-async function getRelatedNoteIds(noteId: number): Promise {
- let allNoteIds: number[] = [noteId];
+async function getRelatedNoteIds(noteId: number) {
+ let detectedIDs: number[] = [];
const note = Zotero.Items.get(noteId);
const linkMatches = note.getNote().match(/zotero:\/\/note\/\w+\/\w+\//g);
- if (!linkMatches) {
- return allNoteIds;
- }
- const subNoteIds = (
- await Promise.all(
- linkMatches.map(async (link) => getNoteLinkParams(link).noteItem),
+ const currentIDs: number[] = [];
+
+ if (linkMatches) {
+ const subNoteIds = (
+ await Promise.all(
+ linkMatches.map(async (link) => getNoteLinkParams(link).noteItem),
+ )
)
- )
- .filter((item) => item && item.isNote())
- .map((item) => (item as Zotero.Item).id);
- allNoteIds = allNoteIds.concat(subNoteIds);
- allNoteIds = new Array(...new Set(allNoteIds));
- return allNoteIds;
+ .filter((item) => item && item.isNote())
+ .map((item) => (item as Zotero.Item).id);
+ detectedIDs = detectedIDs.concat(subNoteIds);
+ }
+
+ const currentNoteLink = getNoteLink(note);
+ if (currentNoteLink) {
+ // Get current related items
+ for (const relItemKey of note.relatedItems) {
+ try {
+ const relItem = (await Zotero.Items.getByLibraryAndKeyAsync(
+ note.libraryID,
+ relItemKey,
+ )) as Zotero.Item;
+
+ // If the related item is a note and contains the current note link
+ // Add it to the related note list
+ if (relItem.isNote()) {
+ if (relItem.getNote().includes(currentNoteLink)) {
+ detectedIDs.push(relItem.id);
+ }
+ currentIDs.push(relItem.id);
+ }
+ } catch (e) {
+ ztoolkit.log(e);
+ }
+ }
+ }
+
+ const detectedIDSet = new Set(detectedIDs);
+ detectedIDSet.delete(noteId);
+ const currentIDSet = new Set(currentIDs);
+ return { detectedIDSet, currentIDSet };
}