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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Step 2. Insert to: + + + + + + + + + + + + + + + + + + + + + Step 3. Preview: + + + + + + + + + + + + + + + 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 }; }