From fa143e3c72d0770e7d3896b2907c6eb75ac60896 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Wed, 30 Aug 2023 22:12:23 +0800 Subject: [PATCH] add: support note link with section --- addon/locale/en-US/addon.ftl | 3 +- addon/locale/ru-RU/addon.ftl | 3 +- addon/locale/zh-CN/addon.ftl | 3 +- src/api.ts | 4 + src/hooks.ts | 2 + src/modules/editor/toolbar.ts | 165 +++++++++++++++++++------------ src/modules/noteLink.ts | 3 +- src/modules/workspace/content.ts | 10 +- src/modules/workspace/message.ts | 3 +- src/utils/editor.ts | 43 +++++++- src/utils/link.ts | 21 ++-- 11 files changed, 183 insertions(+), 77 deletions(-) diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl index 3ddc5c0..10b3c22 100644 --- a/addon/locale/en-US/addon.ftl +++ b/addon/locale/en-US/addon.ftl @@ -91,7 +91,8 @@ 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-insertTemplate = Insert Template to Cursor Line -editor-toolbar-settings-copyLink = Copy Note Link +editor-toolbar-settings-copyLink = Copy Note Link at Line ({ $line }) +editor-toolbar-settings-copyLinkAtSection = Copy Note Link at Section ({ $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/ru-RU/addon.ftl b/addon/locale/ru-RU/addon.ftl index 3e1556b..2116512 100644 --- a/addon/locale/ru-RU/addon.ftl +++ b/addon/locale/ru-RU/addon.ftl @@ -91,7 +91,8 @@ editor-toolbar-settings-openWorkspace=Открыть пространство з editor-toolbar-settings-setWorkspace=Установить как заметку раб. пространства editor-toolbar-settings-previewInWorkspace=Предпросмотр в рабочем пространстве editor-toolbar-settings-insertTemplate=Вставить шаблон в строку курсора -editor-toolbar-settings-copyLink=Скопировать ссылку заметки +editor-toolbar-settings-copyLink = Copy Note Link at Line ({ $line }) +editor-toolbar-settings-copyLinkAtSection = Copy Note Link at Section ({ $section }) editor-toolbar-settings-openParent=Открыть вложение editor-toolbar-settings-export=Экспортировать текущую заметку... editor-toolbar-settings-refreshSyncing=Синхронизировать сейчас diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl index 35cd80e..8231687 100644 --- a/addon/locale/zh-CN/addon.ftl +++ b/addon/locale/zh-CN/addon.ftl @@ -91,7 +91,8 @@ editor-toolbar-settings-openWorkspace=打开笔记工作区 editor-toolbar-settings-setWorkspace=设为工作区笔记 editor-toolbar-settings-previewInWorkspace=在工作区预览 editor-toolbar-settings-insertTemplate=插入模板到光标行 -editor-toolbar-settings-copyLink=复制笔记链接 +editor-toolbar-settings-copyLink=复制当前行({ $line })笔记链接 +editor-toolbar-settings-copyLinkAtSection=复制当前节({ $section })笔记链接 editor-toolbar-settings-openParent=打开附件 editor-toolbar-settings-export=导出当前笔记... editor-toolbar-settings-refreshSyncing=立即同步 diff --git a/src/api.ts b/src/api.ts index f85b259..7800710 100644 --- a/src/api.ts +++ b/src/api.ts @@ -51,8 +51,10 @@ import { insert, del, scroll, + scrollToSection, getTextBetweenLines, getLineAtCursor, + getSectionAtCursor, getPositionAtLine, getTextBetween, getRangeAtCursor, @@ -124,8 +126,10 @@ const editor = { move, replace, scroll, + scrollToSection, getRangeAtCursor, getLineAtCursor, + getSectionAtCursor, getPositionAtLine, getTextBetween, getTextBetweenLines, diff --git a/src/hooks.ts b/src/hooks.ts index b20d945..4ac2ead 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -178,6 +178,7 @@ function onOpenNote( mode: "auto" | "preview" | "workspace" | "standalone" = "auto", options: { lineIndex?: number; + sectionName?: string; } = {}, ) { const noteItem = Zotero.Items.get(noteId); @@ -217,6 +218,7 @@ function onSetWorkspaceNote( type: "main" | "preview" = "main", options: { lineIndex?: number; + sectionName?: string; } = {}, ) { if (type === "main") { diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts index 439b21d..229b16c 100644 --- a/src/modules/editor/toolbar.ts +++ b/src/modules/editor/toolbar.ts @@ -1,6 +1,6 @@ import { config } from "../../../package.json"; import { ICONS } from "../../utils/config"; -import { getLineAtCursor } from "../../utils/editor"; +import { getLineAtCursor, getSectionAtCursor } from "../../utils/editor"; import { showHint } from "../../utils/hint"; import { getNoteLink, getNoteLinkParams } from "../../utils/link"; import { getString } from "../../utils/locale"; @@ -49,6 +49,8 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { return; } + const currentLine = getLineAtCursor(editor); + const currentSection = getSectionAtCursor(editor); const settingsMenuData: PopupData[] = [ { id: makeId("settings-openWorkspace"), @@ -72,71 +74,110 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { addon.hooks.onSetWorkspaceNote(e.editor._item.id, "preview"); }, }, - { - 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: getLineAtCursor(e.editor), - }); - }, - }, - { - type: "splitter", - }, - { - id: makeId("settings-copyLink"), - text: getString("editor.toolbar.settings.copyLink"), - callback: (e) => { - const link = - getNoteLink(e.editor._item, { - lineIndex: getLineAtCursor(e.editor), - }) || ""; - new ztoolkit.Clipboard() - .addText(link, "text/unicode") - .addText( - `${ - e.editor._item.getNoteTitle().trim() || link - }`, - "text/html", - ) - .copy(); - showHint(`Link ${link} copied`); - }, - }, ]; + 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`); + }, + }, + ]), + ); + } + const parentAttachment = await noteItem.parentItem?.getBestAttachment(); if (parentAttachment) { - settingsMenuData.push({ - type: "splitter", - }); - settingsMenuData.push({ - id: makeId("settings-openParent"), - text: getString("editor.toolbar.settings.openParent"), - callback: (e) => { - ZoteroPane.viewAttachment([parentAttachment.id]); - Zotero.Notifier.trigger("open", "file", parentAttachment.id); - }, - }); + 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)) { diff --git a/src/modules/noteLink.ts b/src/modules/noteLink.ts index 653e9f3..368e724 100644 --- a/src/modules/noteLink.ts +++ b/src/modules/noteLink.ts @@ -7,7 +7,8 @@ export function registerNoteLinkProxyHandler() { const linkParams = getNoteLinkParams(uri.spec); if (linkParams.noteItem) { addon.hooks.onOpenNote(linkParams.noteItem.id, "auto", { - lineIndex: linkParams.lineIndex || undefined, + lineIndex: linkParams.lineIndex, + sectionName: linkParams.sectionName, }); } }, diff --git a/src/modules/workspace/content.ts b/src/modules/workspace/content.ts index 1f2a863..d7ab9e0 100644 --- a/src/modules/workspace/content.ts +++ b/src/modules/workspace/content.ts @@ -1,6 +1,5 @@ import { config } from "../../../package.json"; import { ICONS } from "../../utils/config"; -import { scroll } from "../../utils/editor"; import { getString } from "../../utils/locale"; import { getNoteTreeFlattened } from "../../utils/note"; import { getPref } from "../../utils/prefs"; @@ -227,6 +226,7 @@ export async function initWorkspaceEditor( noteId: number, options: { lineIndex?: number; + sectionName?: string; } = {}, ) { const noteItem = Zotero.Items.get(noteId); @@ -252,7 +252,13 @@ export async function initWorkspaceEditor( await waitUtilAsync(() => Boolean(editorElem._editorInstance)); await editorElem._editorInstance._initPromise; if (typeof options.lineIndex === "number") { - scroll(editorElem._editorInstance, options.lineIndex); + addon.api.editor.scroll(editorElem._editorInstance, options.lineIndex); + } + if (typeof options.sectionName === "string") { + addon.api.editor.scrollToSection( + editorElem._editorInstance, + options.sectionName, + ); } return; } diff --git a/src/modules/workspace/message.ts b/src/modules/workspace/message.ts index f24c5ae..61ae324 100644 --- a/src/modules/workspace/message.ts +++ b/src/modules/workspace/message.ts @@ -1,7 +1,6 @@ import { getEditorInstance, moveHeading, - scroll, updateHeadingTextAtLine, } from "../../utils/editor"; import { showHintWithLink } from "../../utils/hint"; @@ -19,7 +18,7 @@ export async function messageHandler(ev: MessageEvent) { if (!editor) { return; } - scroll(editor, ev.data.lineIndex); + addon.api.editor.scroll(editor, ev.data.lineIndex); return; } case "openNote": { diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 2e357fe..83852f1 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -1,5 +1,6 @@ import TreeModel = require("tree-model"); import { TextSelection } from "prosemirror-state"; +import { getNoteTreeFlattened } from "./note"; export { insert, @@ -7,12 +8,14 @@ export { move, replace, scroll, + scrollToSection, getEditorInstance, moveHeading, updateHeadingTextAtLine, getEditorCore, getRangeAtCursor, getLineAtCursor, + getSectionAtCursor, getPositionAtLine, getPositionAtCursor, getURLAtCursor, @@ -130,6 +133,16 @@ function scroll(editor: Zotero.EditorInstance, lineIndex: number) { core.view.dom.parentElement?.scrollTo(0, offset); } +function scrollToSection(editor: Zotero.EditorInstance, sectionName: string) { + const item = editor._item; + const sectionTree = getNoteTreeFlattened(item); + const sectionNode = sectionTree.find( + (node) => node.model.name.trim() === sectionName.trim(), + ); + if (!sectionNode) return; + scroll(editor, sectionNode.model.lineIndex); +} + function getEditorInstance(noteId: number) { const editor = Zotero.Notes._editorInstances.find( (e) => @@ -150,7 +163,11 @@ function getEditorAPI(editor: Zotero.EditorInstance) { function getPositionAtCursor(editor: Zotero.EditorInstance) { const selection = getEditorCore(editor).view.state.selection; - return selection.$anchor.after(selection.$anchor.depth); + try { + return selection.$anchor.after(selection.$anchor.depth); + } catch (e) { + return -1; + } } function getRangeAtCursor(editor: Zotero.EditorInstance) { @@ -163,6 +180,9 @@ function getRangeAtCursor(editor: Zotero.EditorInstance) { function getLineAtCursor(editor: Zotero.EditorInstance) { const position = getPositionAtCursor(editor); + if (position < 0) { + return -1; + } const lastPos = getEditorCore(editor).view.state.tr.doc.content.size; let i = 0; let currentPos = getPositionAtLine(editor, 0); @@ -176,6 +196,27 @@ function getLineAtCursor(editor: Zotero.EditorInstance) { return i; } +function getSectionAtCursor(editor: Zotero.EditorInstance): string | undefined { + const lineIndex = getLineAtCursor(editor); + if (lineIndex < 0) return undefined; + const item = editor._item; + const sectionTree = getNoteTreeFlattened(item); + let sectionNode; + for (let i = 0; i < sectionTree.length; i++) { + if ( + // Is before cursor + sectionTree[i].model.lineIndex <= lineIndex && + // Is last node, or next node is after cursor + (i === sectionTree.length - 1 || + sectionTree[i + 1].model.lineIndex > lineIndex) + ) { + sectionNode = sectionTree[i]; + break; + } + } + return sectionNode?.model.name; +} + function getDOMAtLine( editor: Zotero.EditorInstance, lineIndex: number, diff --git a/src/utils/link.ts b/src/utils/link.ts index 71f9c71..7474828 100644 --- a/src/utils/link.ts +++ b/src/utils/link.ts @@ -18,8 +18,9 @@ export function getNoteLinkParams(link: string) { noteItem: Zotero.Items.getByLibraryAndKey(libraryID, noteKey || "") as | Zotero.Item | false, - ignore: Boolean(url.searchParams.get("ignore")), - lineIndex: typeof line === "string" ? parseInt(line) : null, + ignore: Boolean(url.searchParams.get("ignore")) || undefined, + lineIndex: typeof line === "string" ? parseInt(line) : undefined, + sectionName: url.searchParams.get("section") || undefined, }; } catch (e: unknown) { return { @@ -27,8 +28,9 @@ export function getNoteLinkParams(link: string) { libraryID: -1, noteKey: undefined, noteItem: false as const, - ignore: null, - lineIndex: null, + ignore: undefined, + lineIndex: undefined, + sectionName: undefined, }; } } @@ -36,8 +38,9 @@ export function getNoteLinkParams(link: string) { export function getNoteLink( noteItem: Zotero.Item, options: { - ignore?: boolean | null; - lineIndex?: number | null; + ignore?: boolean; + lineIndex?: number; + sectionName?: string; } = {}, ) { const libraryID = noteItem.libraryID; @@ -71,6 +74,12 @@ export function getNoteLink( if (options.lineIndex) { link = addParam(link, `line=${options.lineIndex}`); } + if (options.sectionName) { + link = addParam( + link, + `section=${encodeURIComponent(options.sectionName)}`, + ); + } } return link; }