From 357e9a77bd70c16cfba4a50c3ff079c773271274 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Tue, 16 Apr 2024 00:41:51 +0800 Subject: [PATCH] add: link creator refactor: link note -> link creator add: link to mode in link creator fix: locale --- addon/chrome/content/linkCreator.xhtml | 54 +++ addon/chrome/content/linkNote.xhtml | 98 ----- .../styles/linkCreator/inboundCreator.css | 19 + .../styles/linkCreator/linkCreator.css | 14 + .../styles/linkCreator/noteOutline.css | 7 + .../content/styles/linkCreator/notePicker.css | 37 ++ .../styles/linkCreator/notePreview.css | 3 + .../styles/linkCreator/outboundCreator.css | 19 + .../styles/{ => linkCreator}/toolbar.css | 0 addon/chrome/content/styles/linkNote.css | 30 -- addon/chrome/content/styles/notePicker.css | 43 --- .../styles/{ => workspace}/context.css | 0 .../styles/{ => workspace}/details.css | 0 .../styles/{ => workspace}/outline.css | 26 +- .../styles/{ => workspace}/related.css | 0 .../styles/{ => workspace}/relation.css | 0 .../styles/{ => workspace}/workspace.css | 0 addon/locale/en-US/linkCreator.ftl | 24 ++ addon/locale/it-IT/linkCreator.ftl | 24 ++ addon/locale/it-IT/outline.ftl | 20 ++ addon/locale/ru-RU/linkCreator.ftl | 24 ++ addon/locale/ru-RU/outline.ftl | 20 ++ addon/locale/tr-TR/linkCreator.ftl | 24 ++ addon/locale/tr-TR/outline.ftl | 20 ++ addon/locale/zh-CN/linkCreator.ftl | 24 ++ addon/locale/zh-CN/noteRelation.ftl | 2 +- addon/locale/zh-CN/outline.ftl | 20 ++ src/elements/base.ts | 35 +- src/elements/linkCreator/inboundCreator.ts | 250 +++++++++++++ src/elements/{ => linkCreator}/notePicker.ts | 47 +-- src/elements/linkCreator/notePreview.ts | 109 ++++++ src/elements/linkCreator/outboundCreator.ts | 259 +++++++++++++ src/elements/linkCreator/outlinePicker.ts | 165 +++++++++ .../{context.ts => workspace/contextPane.ts} | 6 +- src/elements/{ => workspace}/detailsPane.ts | 6 +- src/elements/{ => workspace}/outlinePane.ts | 22 +- src/elements/{ => workspace}/related.ts | 4 +- src/elements/{ => workspace}/workspace.ts | 10 +- src/extras/customElements.ts | 22 +- src/extras/linkCreator.ts | 85 +++++ src/extras/linkNote.ts | 339 ------------------ src/modules/editor/toolbar.ts | 8 +- src/modules/workspace/link.ts | 2 +- src/modules/workspace/relation.ts | 2 +- src/utils/{linkNote.ts => linkCreator.ts} | 12 +- 45 files changed, 1348 insertions(+), 587 deletions(-) create mode 100644 addon/chrome/content/linkCreator.xhtml delete mode 100644 addon/chrome/content/linkNote.xhtml create mode 100644 addon/chrome/content/styles/linkCreator/inboundCreator.css create mode 100644 addon/chrome/content/styles/linkCreator/linkCreator.css create mode 100644 addon/chrome/content/styles/linkCreator/noteOutline.css create mode 100644 addon/chrome/content/styles/linkCreator/notePicker.css create mode 100644 addon/chrome/content/styles/linkCreator/notePreview.css create mode 100644 addon/chrome/content/styles/linkCreator/outboundCreator.css rename addon/chrome/content/styles/{ => linkCreator}/toolbar.css (100%) delete mode 100644 addon/chrome/content/styles/linkNote.css delete mode 100644 addon/chrome/content/styles/notePicker.css rename addon/chrome/content/styles/{ => workspace}/context.css (100%) rename addon/chrome/content/styles/{ => workspace}/details.css (100%) rename addon/chrome/content/styles/{ => workspace}/outline.css (71%) rename addon/chrome/content/styles/{ => workspace}/related.css (100%) rename addon/chrome/content/styles/{ => workspace}/relation.css (100%) rename addon/chrome/content/styles/{ => workspace}/workspace.css (100%) create mode 100644 addon/locale/en-US/linkCreator.ftl create mode 100644 addon/locale/it-IT/linkCreator.ftl create mode 100644 addon/locale/it-IT/outline.ftl create mode 100644 addon/locale/ru-RU/linkCreator.ftl create mode 100644 addon/locale/ru-RU/outline.ftl create mode 100644 addon/locale/tr-TR/linkCreator.ftl create mode 100644 addon/locale/tr-TR/outline.ftl create mode 100644 addon/locale/zh-CN/linkCreator.ftl create mode 100644 addon/locale/zh-CN/outline.ftl create mode 100644 src/elements/linkCreator/inboundCreator.ts rename src/elements/{ => linkCreator}/notePicker.ts (89%) create mode 100644 src/elements/linkCreator/notePreview.ts create mode 100644 src/elements/linkCreator/outboundCreator.ts create mode 100644 src/elements/linkCreator/outlinePicker.ts rename src/elements/{context.ts => workspace/contextPane.ts} (84%) rename src/elements/{ => workspace}/detailsPane.ts (86%) rename src/elements/{ => workspace}/outlinePane.ts (94%) rename src/elements/{ => workspace}/related.ts (96%) rename src/elements/{ => workspace}/workspace.ts (93%) create mode 100644 src/extras/linkCreator.ts delete mode 100644 src/extras/linkNote.ts rename src/utils/{linkNote.ts => linkCreator.ts} (75%) diff --git a/addon/chrome/content/linkCreator.xhtml b/addon/chrome/content/linkCreator.xhtml new file mode 100644 index 0000000..c1ced93 --- /dev/null +++ b/addon/chrome/content/linkCreator.xhtml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon/chrome/content/linkNote.xhtml b/addon/chrome/content/linkNote.xhtml deleted file mode 100644 index 74c85e6..0000000 --- a/addon/chrome/content/linkNote.xhtml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - Step 2. Insert to: - - - - - - - - - - - - - - - - - - - - - Step 3. Preview: - - - - - - - - - - - - - - - diff --git a/addon/chrome/content/styles/linkCreator/inboundCreator.css b/addon/chrome/content/styles/linkCreator/inboundCreator.css new file mode 100644 index 0000000..fbd1643 --- /dev/null +++ b/addon/chrome/content/styles/linkCreator/inboundCreator.css @@ -0,0 +1,19 @@ +bn-inbound-creator { + gap: 16px; + overflow: auto; + + bn-note-picker { + border: var(--material-border); + min-width: 600px; + } + + bn-note-outline { + border: var(--material-border); + min-width: 300px; + } + + bn-note-preview { + border: var(--material-border); + min-width: 450px; + } +} diff --git a/addon/chrome/content/styles/linkCreator/linkCreator.css b/addon/chrome/content/styles/linkCreator/linkCreator.css new file mode 100644 index 0000000..9763f9c --- /dev/null +++ b/addon/chrome/content/styles/linkCreator/linkCreator.css @@ -0,0 +1,14 @@ +.container { + min-height: 0; + height: 100%; + margin: 0; +} + +.content-container { + overflow: auto; +} + +/* TODO: remove fx115 workaround */ +tab { + color: unset !important; +} diff --git a/addon/chrome/content/styles/linkCreator/noteOutline.css b/addon/chrome/content/styles/linkCreator/noteOutline.css new file mode 100644 index 0000000..89617de --- /dev/null +++ b/addon/chrome/content/styles/linkCreator/noteOutline.css @@ -0,0 +1,7 @@ +bn-note-outline { + flex-direction: column; +} + +#bn-link-insert-position-container { + align-items: center; +} diff --git a/addon/chrome/content/styles/linkCreator/notePicker.css b/addon/chrome/content/styles/linkCreator/notePicker.css new file mode 100644 index 0000000..d9bee6d --- /dev/null +++ b/addon/chrome/content/styles/linkCreator/notePicker.css @@ -0,0 +1,37 @@ +bn-note-picker { + flex-direction: column; + + #collections-items-container { + display: flex; + height: 100%; + max-height: 50%; + 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; + + #zotero-select-items-container { + gap: 0; + } + + #collections-items-container { + margin-bottom: 0; + } + } + + #bn-select-opened-notes-container { + min-width: 200px; + max-height: 50%; + } +} diff --git a/addon/chrome/content/styles/linkCreator/notePreview.css b/addon/chrome/content/styles/linkCreator/notePreview.css new file mode 100644 index 0000000..417b34a --- /dev/null +++ b/addon/chrome/content/styles/linkCreator/notePreview.css @@ -0,0 +1,3 @@ +bn-note-preview { + flex-direction: column; +} diff --git a/addon/chrome/content/styles/linkCreator/outboundCreator.css b/addon/chrome/content/styles/linkCreator/outboundCreator.css new file mode 100644 index 0000000..fc71339 --- /dev/null +++ b/addon/chrome/content/styles/linkCreator/outboundCreator.css @@ -0,0 +1,19 @@ +bn-outbound-creator { + gap: 16px; + overflow: auto; + + bn-note-picker { + border: var(--material-border); + min-width: 600px; + } + + bn-note-outline { + border: var(--material-border); + min-width: 300px; + } + + bn-note-preview { + border: var(--material-border); + min-width: 450px; + } +} diff --git a/addon/chrome/content/styles/toolbar.css b/addon/chrome/content/styles/linkCreator/toolbar.css similarity index 100% rename from addon/chrome/content/styles/toolbar.css rename to addon/chrome/content/styles/linkCreator/toolbar.css diff --git a/addon/chrome/content/styles/linkNote.css b/addon/chrome/content/styles/linkNote.css deleted file mode 100644 index fbe5ddd..0000000 --- a/addon/chrome/content/styles/linkNote.css +++ /dev/null @@ -1,30 +0,0 @@ -.container { - min-height: 0; - height: 100%; - margin: 0; -} - -#top-container { - gap: 16px; - overflow: auto; - padding: 2em; -} - -bn-note-picker { - border: var(--material-border); - min-width: 600px; -} - -#bn-select-note-outline-container { - border: var(--material-border); - min-width: 300px; -} - -#bn-note-preview-container { - border: var(--material-border); - min-width: 450px; -} - -#bn-link-insert-position-container { - align-items: center; -} diff --git a/addon/chrome/content/styles/notePicker.css b/addon/chrome/content/styles/notePicker.css deleted file mode 100644 index 3be12c3..0000000 --- a/addon/chrome/content/styles/notePicker.css +++ /dev/null @@ -1,43 +0,0 @@ -bn-note-picker { - flex-direction: column; -} - -.container { - min-height: 0; - height: 100%; - margin: 0; -} - -#select-items-dialog #zotero-select-items-container { - gap: 0; -} - -#collections-items-container { - display: flex; - height: 100%; - max-height: 50%; - 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; - max-height: 50%; -} diff --git a/addon/chrome/content/styles/context.css b/addon/chrome/content/styles/workspace/context.css similarity index 100% rename from addon/chrome/content/styles/context.css rename to addon/chrome/content/styles/workspace/context.css diff --git a/addon/chrome/content/styles/details.css b/addon/chrome/content/styles/workspace/details.css similarity index 100% rename from addon/chrome/content/styles/details.css rename to addon/chrome/content/styles/workspace/details.css diff --git a/addon/chrome/content/styles/outline.css b/addon/chrome/content/styles/workspace/outline.css similarity index 71% rename from addon/chrome/content/styles/outline.css rename to addon/chrome/content/styles/workspace/outline.css index c79e561..6e6f4c9 100644 --- a/addon/chrome/content/styles/outline.css +++ b/addon/chrome/content/styles/workspace/outline.css @@ -8,6 +8,19 @@ bn-outline, bn-outline { min-width: 100px; flex-direction: column; + + .zotero-tb-button { + width: 28px; + height: 28px; + margin: 0px 4px 0px 4px; + padding: 0px 4px 0px 4px; + fill: currentColor; + -moz-context-properties: fill, fill-opacity; + } + + .zotero-tb-button[type="menu"] { + width: 40px; + } } @media (-moz-platform: macos) { @@ -22,19 +35,6 @@ bn-outline { padding: 6px 8px; } -bn-outline .zotero-tb-button { - width: 28px; - height: 28px; - margin: 0px 4px 0px 4px; - padding: 0px 4px 0px 4px; - fill: currentColor; - -moz-context-properties: fill, fill-opacity; -} - -bn-outline .zotero-tb-button[type="menu"] { - width: 40px; -} - #__addonRef__-setOutline { list-style-image: url("chrome://__addonRef__/content/icons/outline-20.svg"); } diff --git a/addon/chrome/content/styles/related.css b/addon/chrome/content/styles/workspace/related.css similarity index 100% rename from addon/chrome/content/styles/related.css rename to addon/chrome/content/styles/workspace/related.css diff --git a/addon/chrome/content/styles/relation.css b/addon/chrome/content/styles/workspace/relation.css similarity index 100% rename from addon/chrome/content/styles/relation.css rename to addon/chrome/content/styles/workspace/relation.css diff --git a/addon/chrome/content/styles/workspace.css b/addon/chrome/content/styles/workspace/workspace.css similarity index 100% rename from addon/chrome/content/styles/workspace.css rename to addon/chrome/content/styles/workspace/workspace.css diff --git a/addon/locale/en-US/linkCreator.ftl b/addon/locale/en-US/linkCreator.ftl new file mode 100644 index 0000000..abd790c --- /dev/null +++ b/addon/locale/en-US/linkCreator.ftl @@ -0,0 +1,24 @@ +title = + .title = Link Creator +tab-inbound = + .label = Mention in +tab-outbound = + .label = Link to + +inbound-step1-content = Step 1. Mention in note: +inbound-step2-content = Step 2. Insert to: +inbound-step3-content = Step 3. Preview: +inbound-step3-middle = + { $show -> + [true] mentions + *[other] { "" } + } + +outbound-step1-content = Step 1. Link to note: +outbound-step2-content = Step 2. Insert to: +outbound-step3-content = Step 3. Preview: +outbound-step3-middle = + { $show -> + [true] links to + *[other] { "" } + } diff --git a/addon/locale/it-IT/linkCreator.ftl b/addon/locale/it-IT/linkCreator.ftl new file mode 100644 index 0000000..34a00d9 --- /dev/null +++ b/addon/locale/it-IT/linkCreator.ftl @@ -0,0 +1,24 @@ +title = + .title = Link Creator +tab-inbound = + .label = Mention in +tab-outbound = + .label = Link to + +inbound-step1-content = Step 1. Mention in note: +inbound-step2-content = Step 2. Insert link to: +inbound-step3-content = Step 3. Preview: +inbound-step3-middle = + { $show -> + [true] mentions + *[other] { "" } + } + +outbound-step1-content = Step 1. Link to note: +outbound-step2-content = Step 2. Insert link to: +outbound-step3-content = Step 3. Preview: +outbound-step3-middle = + { $show -> + [true] links to + *[other] { "" } + } diff --git a/addon/locale/it-IT/outline.ftl b/addon/locale/it-IT/outline.ftl new file mode 100644 index 0000000..aa31812 --- /dev/null +++ b/addon/locale/it-IT/outline.ftl @@ -0,0 +1,20 @@ +setOutline = + .tooltiptext = Change outline mode +useTreeView = + .label = Tree View +useMindMap = + .label = Mind Map +useBubbleMap = + .label = Bubble Map +saveOutline = + .tooltiptext = Save as... +saveOutlineImage = + .label = Outline image + .tooltiptext = Only in mind map/bubble map mode +saveOutlineSVG = + .label = Outline SVG + .tooltiptext = Only in mind map/bubble map mode +saveOutlineFreeMind = + .label = Outline FreeMind +saveMore = + .label = MarkDown, Docx, PDF... diff --git a/addon/locale/ru-RU/linkCreator.ftl b/addon/locale/ru-RU/linkCreator.ftl new file mode 100644 index 0000000..34a00d9 --- /dev/null +++ b/addon/locale/ru-RU/linkCreator.ftl @@ -0,0 +1,24 @@ +title = + .title = Link Creator +tab-inbound = + .label = Mention in +tab-outbound = + .label = Link to + +inbound-step1-content = Step 1. Mention in note: +inbound-step2-content = Step 2. Insert link to: +inbound-step3-content = Step 3. Preview: +inbound-step3-middle = + { $show -> + [true] mentions + *[other] { "" } + } + +outbound-step1-content = Step 1. Link to note: +outbound-step2-content = Step 2. Insert link to: +outbound-step3-content = Step 3. Preview: +outbound-step3-middle = + { $show -> + [true] links to + *[other] { "" } + } diff --git a/addon/locale/ru-RU/outline.ftl b/addon/locale/ru-RU/outline.ftl new file mode 100644 index 0000000..aa31812 --- /dev/null +++ b/addon/locale/ru-RU/outline.ftl @@ -0,0 +1,20 @@ +setOutline = + .tooltiptext = Change outline mode +useTreeView = + .label = Tree View +useMindMap = + .label = Mind Map +useBubbleMap = + .label = Bubble Map +saveOutline = + .tooltiptext = Save as... +saveOutlineImage = + .label = Outline image + .tooltiptext = Only in mind map/bubble map mode +saveOutlineSVG = + .label = Outline SVG + .tooltiptext = Only in mind map/bubble map mode +saveOutlineFreeMind = + .label = Outline FreeMind +saveMore = + .label = MarkDown, Docx, PDF... diff --git a/addon/locale/tr-TR/linkCreator.ftl b/addon/locale/tr-TR/linkCreator.ftl new file mode 100644 index 0000000..34a00d9 --- /dev/null +++ b/addon/locale/tr-TR/linkCreator.ftl @@ -0,0 +1,24 @@ +title = + .title = Link Creator +tab-inbound = + .label = Mention in +tab-outbound = + .label = Link to + +inbound-step1-content = Step 1. Mention in note: +inbound-step2-content = Step 2. Insert link to: +inbound-step3-content = Step 3. Preview: +inbound-step3-middle = + { $show -> + [true] mentions + *[other] { "" } + } + +outbound-step1-content = Step 1. Link to note: +outbound-step2-content = Step 2. Insert link to: +outbound-step3-content = Step 3. Preview: +outbound-step3-middle = + { $show -> + [true] links to + *[other] { "" } + } diff --git a/addon/locale/tr-TR/outline.ftl b/addon/locale/tr-TR/outline.ftl new file mode 100644 index 0000000..aa31812 --- /dev/null +++ b/addon/locale/tr-TR/outline.ftl @@ -0,0 +1,20 @@ +setOutline = + .tooltiptext = Change outline mode +useTreeView = + .label = Tree View +useMindMap = + .label = Mind Map +useBubbleMap = + .label = Bubble Map +saveOutline = + .tooltiptext = Save as... +saveOutlineImage = + .label = Outline image + .tooltiptext = Only in mind map/bubble map mode +saveOutlineSVG = + .label = Outline SVG + .tooltiptext = Only in mind map/bubble map mode +saveOutlineFreeMind = + .label = Outline FreeMind +saveMore = + .label = MarkDown, Docx, PDF... diff --git a/addon/locale/zh-CN/linkCreator.ftl b/addon/locale/zh-CN/linkCreator.ftl new file mode 100644 index 0000000..880ca79 --- /dev/null +++ b/addon/locale/zh-CN/linkCreator.ftl @@ -0,0 +1,24 @@ +title = + .title = 链接精灵 +tab-inbound = + .label = 提及 +tab-outbound = + .label = 指向 + +inbound-step1-content = 第一步-在此处提及: +inbound-step2-content = 第二步-插入链接到: +inbound-step3-content = 第三步-预览: +inbound-step3-middle = + { $show -> + [true] 提及了 + *[other] { "" } + } + +outbound-step1-content = 第一步-链接指向: +outbound-step2-content = 第二步-插入链接到: +outbound-step3-content = 第三步-预览: +outbound-step3-middle = + { $show -> + [true] 指向了 + *[other] { "" } + } \ No newline at end of file diff --git a/addon/locale/zh-CN/noteRelation.ftl b/addon/locale/zh-CN/noteRelation.ftl index ae98c1d..902c871 100644 --- a/addon/locale/zh-CN/noteRelation.ftl +++ b/addon/locale/zh-CN/noteRelation.ftl @@ -1,7 +1,7 @@ note-relation-header = .label = 关系图 note-relation-sidenav = - .tooltiptext = 关系图 + .tooltiptext = 关系网 note-relation-refresh = .tooltiptext = 刷新 diff --git a/addon/locale/zh-CN/outline.ftl b/addon/locale/zh-CN/outline.ftl new file mode 100644 index 0000000..b404257 --- /dev/null +++ b/addon/locale/zh-CN/outline.ftl @@ -0,0 +1,20 @@ +setOutline = + .tooltiptext = 切换大纲模式 +useTreeView = + .label = 目录树 +useMindMap = + .label = 思维导图 +useBubbleMap = + .label = 气泡关系图 +saveOutline = + .tooltiptext = 另存为... +saveOutlineImage = + .label = 大纲图片 + .tooltiptext = 仅适用于思维导图/气泡关系图模式 +saveOutlineSVG = + .label = 大纲SVG + .tooltiptext = 仅适用于思维导图/气泡关系图模式 +saveOutlineFreeMind = + .label = 大纲FreeMind思维导图 +saveMore = + .label = MarkDown, Docx, PDF... diff --git a/src/elements/base.ts b/src/elements/base.ts index 65627d8..881a510 100644 --- a/src/elements/base.ts +++ b/src/elements/base.ts @@ -2,11 +2,37 @@ import { config } from "../../package.json"; export class PluginCEBase extends XULElementBase { _addon!: typeof addon; + useShadowRoot = false; connectedCallback(): void { this._addon = Zotero[config.addonInstance]; Zotero.UIProperties.registerRoot(this); - super.connectedCallback(); + if (!this.useShadowRoot) { + super.connectedCallback(); + return; + } + this.attachShadow({ mode: "open" }); + // Following the connectedCallback from XULElementBase + let content = this.content; + if (content) { + content = document.importNode(content, true); + this.shadowRoot?.append(content); + } + + MozXULElement.insertFTLIfNeeded("branding/brand.ftl"); + MozXULElement.insertFTLIfNeeded("zotero.ftl"); + // @ts-ignore + if (document.l10n && this.shadowRoot) { + // @ts-ignore + document.l10n.connectRoot(this.shadowRoot); + } + + // @ts-ignore + window.addEventListener("unload", this._handleWindowUnload); + + // @ts-ignore + this.initialized = true; + this.init(); } _wrapID(key: string) { @@ -24,7 +50,12 @@ export class PluginCEBase extends XULElementBase { } _queryID(key: string) { - return this.querySelector(`#${this._wrapID(key)}`) as XUL.Element | null; + const selector = `#${this._wrapID(key)}`; + return (this.querySelector(selector) || + this.shadowRoot?.querySelector(selector)) as + | XUL.Element + | HTMLElement + | null; } _parseContentID(dom: DocumentFragment) { diff --git a/src/elements/linkCreator/inboundCreator.ts b/src/elements/linkCreator/inboundCreator.ts new file mode 100644 index 0000000..320c56b --- /dev/null +++ b/src/elements/linkCreator/inboundCreator.ts @@ -0,0 +1,250 @@ +import { config } from "../../../package.json"; +import { PluginCEBase } from "../base"; +import { NotePicker } from "./notePicker"; +import { NotePreview } from "./notePreview"; +import { OutlinePicker } from "./outlinePicker"; +import { getPref, setPref } from "../../utils/prefs"; + +export class InboundCreator extends PluginCEBase { + notePicker!: NotePicker; + noteOutline!: OutlinePicker; + notePreview!: NotePreview; + + // Where the link is generated from + currentNote: Zotero.Item | undefined; + // Where the link is inserted to + targetNote: Zotero.Item | undefined; + + positionData: NoteNodeData | undefined; + + _openedNoteIDs: number[] = []; + + loaded: boolean = false; + + get content() { + return MozXULElement.parseXULToFragment(` + + + + + + +`); + } + + get openedNoteIDs() { + return this._openedNoteIDs; + } + + set openedNoteIDs(val) { + this._openedNoteIDs = val; + } + + get item() { + return this.currentNote; + } + + set item(val) { + this.currentNote = val; + } + + async load(io: any) { + if (this.loaded) return; + this.openedNoteIDs = io.openedNoteIDs || []; + this.item = Zotero.Items.get(io.currentNoteID); + this.loadNotePicker(); + this.loadNoteOutline(); + this.loadNotePreview(); + this.loadInsertPosition(); + this.loaded = true; + + this.scrollToSection("picker"); + } + + async accept(io: any) { + if (!this.targetNote) return; + const content = await this.getContentToInsert(); + + io.targetNoteID = this.targetNote.id; + io.content = content; + io.lineIndex = this.getIndexToInsert(); + } + + async loadNotePicker() { + this.notePicker = this.querySelector("bn-note-picker") as NotePicker; + this.notePicker.openedNoteIDs = this.openedNoteIDs; + await this.notePicker.load(); + + this.notePicker.addEventListener("selectionchange", (event: any) => { + this.targetNote = event.detail.selectedNote; + this.updatePickerTitle(this.targetNote); + this.noteOutline.item = this.targetNote; + this.noteOutline.render(); + this.positionData = undefined; + if (this.targetNote) this.scrollToSection("outline"); + }); + + const content = document.createElement("span"); + content.dataset.l10nId = `${config.addonRef}-inbound-step1-content`; + content.classList.add("toolbar-header", "content"); + const title = document.createElement("span"); + title.id = "selected-note-title"; + title.classList.add("toolbar-header", "highlight"); + this.notePicker + .querySelector("#search-toolbar .toolbar-start") + ?.append(content, title); + } + + loadNoteOutline() { + this.noteOutline = this.querySelector("bn-note-outline") as OutlinePicker; + + this.noteOutline.load(); + + this.noteOutline.addEventListener("selectionchange", (event: any) => { + this.positionData = event.detail.selectedSection; + this.updateNotePreview(); + this.updateOutlineTitle(); + }); + + const content = document.createElement("span"); + content.dataset.l10nId = `${config.addonRef}-inbound-step2-content`; + content.classList.add("toolbar-header", "content"); + const title = document.createElement("span"); + title.id = "selected-outline-title"; + title.classList.add("toolbar-header", "highlight"); + this.noteOutline + .querySelector(".toolbar .toolbar-start") + ?.append(content, title); + } + + loadInsertPosition() { + const insertPosition = this.querySelector( + "#bn-link-insert-position", + ) as HTMLSelectElement; + insertPosition.value = getPref("insertLinkPosition") as string; + + insertPosition.addEventListener("command", () => { + setPref("insertLinkPosition", insertPosition.value); + this.updateNotePreview(); + }); + } + + loadNotePreview() { + this.notePreview = this.querySelector("bn-note-preview") as NotePreview; + + const content = document.createElement("span"); + content.dataset.l10nId = `${config.addonRef}-inbound-step3-content`; + content.classList.add("toolbar-header", "content"); + + const fromTitle = document.createElement("span"); + fromTitle.id = "preview-note-from-title"; + fromTitle.classList.add("toolbar-header", "highlight"); + + const middleTitle = document.createElement("span"); + middleTitle.id = "preview-note-middle-title"; + middleTitle.dataset.l10nId = `${config.addonRef}-inbound-step3-middle`; + middleTitle.classList.add("toolbar-header", "content"); + + const toTitle = document.createElement("span"); + toTitle.id = "preview-note-to-title"; + toTitle.classList.add("toolbar-header", "highlight"); + this.notePreview + .querySelector(".toolbar .toolbar-start") + ?.append(content, fromTitle, middleTitle, toTitle); + } + + updatePickerTitle(noteItem?: Zotero.Item) { + const title = noteItem ? noteItem.getNoteTitle() : ""; + this.querySelector("#selected-note-title")!.textContent = title; + } + + updateOutlineTitle() { + const title = this.positionData?.name || ""; + this.querySelector("#selected-outline-title")!.textContent = title; + } + + updatePreviewTitle() { + this.querySelector("#preview-note-from-title")!.textContent = + this.targetNote?.getNoteTitle() || "No title"; + ( + this.querySelector("#preview-note-middle-title") as HTMLElement + ).dataset.l10nArgs = `{"show": "true"}`; + this.querySelector("#preview-note-to-title")!.textContent = + this.currentNote?.getNoteTitle() || "No title"; + } + + async updateNotePreview() { + if (!this.loaded || !this.targetNote) return; + + const lines = await this._addon.api.note.getLinesInNote(this.targetNote, { + convertToHTML: true, + }); + let index = this.getIndexToInsert(); + if (index < 0) { + index = lines.length; + } else { + this.scrollToSection("preview"); + } + const before = lines.slice(0, index).join("\n"); + const after = lines.slice(index).join("\n"); + + // TODO: use index or section + const middle = await this.getContentToInsert(); + + this.notePreview.render({ before, middle, after }); + this.updatePreviewTitle(); + } + + scrollToSection(type: "picker" | "outline" | "preview") { + if (!this.loaded) return; + const querier = { + picker: "bn-note-picker", + outline: "bn-note-outline", + preview: "bn-note-preview", + }; + const container = this.querySelector(querier[type]); + if (!container) return; + container.scrollIntoView({ + behavior: "smooth", + inline: "center", + }); + } + + async getContentToInsert() { + if (!this.currentNote || !this.targetNote) return ""; + const forwardLink = this._addon.api.convert.note2link(this.currentNote, {}); + const content = await this._addon.api.template.runTemplate( + "[QuickInsertV2]", + "link, linkText, subNoteItem, noteItem", + [ + forwardLink, + this.currentNote.getNoteTitle().trim() || forwardLink, + this.currentNote, + this.targetNote, + ], + { + dryRun: true, + }, + ); + return content; + } + + getIndexToInsert() { + if (!this.positionData) return -1; + let position = getPref("insertLinkPosition") as string; + if (!["start", "end"].includes(position)) { + position = "end"; + } + let index = { + start: this.positionData.lineIndex + 1, + end: this.positionData.endIndex + 1, + }[position]; + if (index === undefined) { + index = -1; + } + return index; + } +} diff --git a/src/elements/notePicker.ts b/src/elements/linkCreator/notePicker.ts similarity index 89% rename from src/elements/notePicker.ts rename to src/elements/linkCreator/notePicker.ts index 95f9eaf..3fcfe1e 100644 --- a/src/elements/notePicker.ts +++ b/src/elements/linkCreator/notePicker.ts @@ -1,6 +1,6 @@ -import { config } from "../../package.json"; +import { config } from "../../../package.json"; import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable"; -import { PluginCEBase } from "./base"; +import { PluginCEBase } from "../base"; const _require = window.require; const CollectionTree = _require("chrome://zotero/content/collectionTree.js"); @@ -16,12 +16,14 @@ export class NotePicker extends PluginCEBase { activeSelectionType: "library" | "tabs" | "none" = "none"; + uid = Zotero.Utilities.randomString(8); + get content() { return MozXULElement.parseXULToFragment(` @@ -52,7 +54,7 @@ export class NotePicker extends PluginCEBase { id="bn-select-opened-notes-content" class="container virtualized-table-container" > - + @@ -67,14 +69,9 @@ export class NotePicker extends PluginCEBase { this.openedNotesView.render(); return; } - this.loadOpenedNotes(); } async init() { - await this.loadLibraryNotes(); - this.loadQuickSearch(); - await this.loadOpenedNotes(); - window.addEventListener("unload", () => { this.destroy(); }); @@ -85,6 +82,12 @@ export class NotePicker extends PluginCEBase { if (this.itemsView) this.itemsView.unregister(); } + async load() { + await this.loadLibraryNotes(); + this.loadQuickSearch(); + await this.loadOpenedNotes(); + } + async loadLibraryNotes() { this.itemsView = await ItemTree.init( this.querySelector("#zotero-items-tree"), @@ -143,15 +146,15 @@ export class NotePicker extends PluginCEBase { searchBox, ); - Zotero.updateQuickSearchBox(document); + searchBox.updateMode(); } async loadOpenedNotes() { const renderLock = Zotero.Promise.defer(); this.openedNotesView = new VirtualizedTableHelper(window) - .setContainerId("bn-select-opened-notes-tree") + .setContainerId(`bn-select-opened-notes-tree-${this.uid}`) .setProp({ - id: `bn-select-opened-notes-table`, + id: `bn-select-opened-notes-table-${this.uid}`, columns: [ { dataKey: "title", @@ -172,7 +175,7 @@ export class NotePicker extends PluginCEBase { }; }) .setProp("onSelectionChange", (selection) => { - this.onOpenedNoteSelected(); + this.onOpenedNoteSelected(selection); }) // For find-as-you-type .setProp( @@ -276,29 +279,29 @@ export class NotePicker extends PluginCEBase { this.dispatchSelectionChange(); } - onOpenedNoteSelected() { + onOpenedNoteSelected(selection: { selected: Set }) { this.activeSelectionType = "tabs"; - this.dispatchSelectionChange(); + this.dispatchSelectionChange(selection); } - dispatchSelectionChange() { + dispatchSelectionChange(selection?: { selected: Set }) { this.dispatchEvent( - new CustomEvent("selectionChange", { + new CustomEvent("selectionchange", { detail: { - selectedNote: this.getSelectedNotes()[0], + selectedNote: this.getSelectedNotes(selection)[0], }, }), ); } - getSelectedNotes(): Zotero.Item[] { + getSelectedNotes(selection?: { selected: Set }): 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], - ); + return Array.from( + (selection || this.openedNotesView.treeInstance.selection).selected, + ).map((index) => this.openedNotes[index]); } } diff --git a/src/elements/linkCreator/notePreview.ts b/src/elements/linkCreator/notePreview.ts new file mode 100644 index 0000000..5236a05 --- /dev/null +++ b/src/elements/linkCreator/notePreview.ts @@ -0,0 +1,109 @@ +import { config } from "../../../package.json"; +import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable"; +import { PluginCEBase } from "../base"; +import TreeModel = require("tree-model"); +import { waitUtilAsync } from "../../utils/wait"; + +export class NotePreview extends PluginCEBase { + _item?: Zotero.Item; + noteOutlineView!: VirtualizedTableHelper; + noteOutline: TreeModel.Node[] = []; + + get content() { + return MozXULElement.parseXULToFragment(` + + + + + + + + + + + +`); + } + + async init() {} + + async render(options: { before: string; middle: string; after: string }) { + const iframe = this.querySelector("#bn-note-preview") as HTMLIFrameElement; + + const activeElement = document.activeElement as HTMLElement; + + iframe!.contentDocument!.documentElement.innerHTML = ` + + + + + + + + +
${options.before}
+
${options.middle}
+
${options.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", + }); + } + } +} diff --git a/src/elements/linkCreator/outboundCreator.ts b/src/elements/linkCreator/outboundCreator.ts new file mode 100644 index 0000000..5d38625 --- /dev/null +++ b/src/elements/linkCreator/outboundCreator.ts @@ -0,0 +1,259 @@ +import { config } from "../../../package.json"; +import { PluginCEBase } from "../base"; +import { NotePicker } from "./notePicker"; +import { NotePreview } from "./notePreview"; +import { OutlinePicker } from "./outlinePicker"; +import { getPref, setPref } from "../../utils/prefs"; + +export class OutboundCreator extends PluginCEBase { + notePicker!: NotePicker; + noteOutline!: OutlinePicker; + notePreview!: NotePreview; + + // Where the link is inserted to + currentNote: Zotero.Item | undefined; + // Where the link is generated from + targetNote: Zotero.Item | undefined; + + positionData: NoteNodeData | undefined; + + _openedNoteIDs: number[] = []; + + _currentLineIndex: number | undefined; + + loaded: boolean = false; + + get content() { + return MozXULElement.parseXULToFragment(` + + + + + + +`); + } + + get openedNoteIDs() { + return this._openedNoteIDs; + } + + set openedNoteIDs(val) { + this._openedNoteIDs = val; + } + + get item() { + return this.currentNote; + } + + set item(val) { + this.currentNote = val; + } + + async load(io: any) { + if (this.loaded) return; + this.openedNoteIDs = io.openedNoteIDs || []; + this._currentLineIndex = io.currentLineIndex; + this.item = Zotero.Items.get(io.currentNoteID); + this.loadNotePicker(); + this.loadNoteOutline(); + this.loadNotePreview(); + this.loadInsertPosition(); + this.loaded = true; + + this.scrollToSection("picker"); + } + + async accept(io: any) { + if (!this.targetNote) return; + const content = await this.getContentToInsert(); + + io.targetNoteID = this.currentNote!.id; + io.content = content; + io.lineIndex = this.getIndexToInsert(); + } + + async loadNotePicker() { + this.notePicker = this.querySelector("bn-note-picker") as NotePicker; + this.notePicker.openedNoteIDs = this.openedNoteIDs; + await this.notePicker.load(); + + this.notePicker.addEventListener("selectionchange", (event: any) => { + this.targetNote = event.detail.selectedNote; + this.updatePickerTitle(this.targetNote); + this.updateNotePreview(); + if (this.targetNote) this.scrollToSection("outline"); + }); + + const content = document.createElement("span"); + content.innerHTML = ""; + content.dataset.l10nId = `${config.addonRef}-outbound-step1-content`; + content.classList.add("toolbar-header", "content"); + const title = document.createElement("span"); + title.id = "selected-note-title"; + title.classList.add("toolbar-header", "highlight"); + this.notePicker + .querySelector("#search-toolbar .toolbar-start") + ?.append(content, title); + } + + loadNoteOutline() { + this.noteOutline = this.querySelector("bn-note-outline") as OutlinePicker; + + this.noteOutline.load(); + this.noteOutline.item = this.currentNote; + if (typeof this._currentLineIndex === "number") { + this.noteOutline.lineIndex = this._currentLineIndex; + } + this.noteOutline.render(); + this.positionData = undefined; + this.updateNotePreview(); + + this.noteOutline.addEventListener("selectionchange", (event: any) => { + this.positionData = event.detail.selectedSection; + this.updateNotePreview(); + this.updateOutlineTitle(); + }); + + const content = document.createElement("span"); + content.dataset.l10nId = `${config.addonRef}-outbound-step2-content`; + content.classList.add("toolbar-header", "content"); + const title = document.createElement("span"); + title.id = "selected-outline-title"; + title.classList.add("toolbar-header", "highlight"); + this.noteOutline + .querySelector(".toolbar .toolbar-start") + ?.append(content, title); + } + + loadInsertPosition() { + const insertPosition = this.querySelector( + "#bn-link-insert-position", + ) as HTMLSelectElement; + insertPosition.value = getPref("insertLinkPosition") as string; + + insertPosition.addEventListener("command", () => { + setPref("insertLinkPosition", insertPosition.value); + this.updateNotePreview(); + }); + } + + loadNotePreview() { + this.notePreview = this.querySelector("bn-note-preview") as NotePreview; + + const content = document.createElement("span"); + content.dataset.l10nId = `${config.addonRef}-outbound-step3-content`; + content.classList.add("toolbar-header", "content"); + + const fromTitle = document.createElement("span"); + fromTitle.id = "preview-note-from-title"; + fromTitle.classList.add("toolbar-header", "highlight"); + + const middleTitle = document.createElement("span"); + middleTitle.id = "preview-note-middle-title"; + middleTitle.dataset.l10nId = `${config.addonRef}-outbound-step3-middle`; + middleTitle.classList.add("toolbar-header", "content"); + + const toTitle = document.createElement("span"); + toTitle.id = "preview-note-to-title"; + toTitle.classList.add("toolbar-header", "highlight"); + this.notePreview + .querySelector(".toolbar .toolbar-start") + ?.append(content, fromTitle, middleTitle, toTitle); + } + + updatePickerTitle(noteItem?: Zotero.Item) { + const title = noteItem ? noteItem.getNoteTitle() : ""; + this.querySelector("#selected-note-title")!.textContent = title; + } + + updateOutlineTitle() { + const title = this.positionData?.name || ""; + this.querySelector("#selected-outline-title")!.textContent = title; + } + + updatePreviewTitle() { + this.querySelector("#preview-note-from-title")!.textContent = + this.currentNote?.getNoteTitle() || "No title"; + ( + this.querySelector("#preview-note-middle-title") as HTMLElement + ).dataset.l10nArgs = `{"show": "true"}`; + this.querySelector("#preview-note-to-title")!.textContent = + this.targetNote?.getNoteTitle() || "No title"; + } + + async updateNotePreview() { + if (!this.loaded || !this.currentNote) return; + + const lines = await this._addon.api.note.getLinesInNote(this.currentNote, { + convertToHTML: true, + }); + let index = this.getIndexToInsert(); + if (index < 0) { + index = lines.length; + } else { + this.scrollToSection("preview"); + } + const before = lines.slice(0, index).join("\n"); + const after = lines.slice(index).join("\n"); + + // TODO: use index or section + const middle = await this.getContentToInsert(); + + this.notePreview.render({ before, middle, after }); + this.updatePreviewTitle(); + } + + scrollToSection(type: "picker" | "outline" | "preview") { + if (!this.loaded) return; + const querier = { + picker: "bn-note-picker", + outline: "bn-note-outline", + preview: "bn-note-preview", + }; + const container = this.querySelector(querier[type]); + if (!container) return; + container.scrollIntoView({ + behavior: "smooth", + inline: "center", + }); + } + + async getContentToInsert() { + if (!this.currentNote || !this.targetNote) return ""; + const forwardLink = this._addon.api.convert.note2link(this.targetNote, {}); + const content = await this._addon.api.template.runTemplate( + "[QuickInsertV2]", + "link, linkText, subNoteItem, noteItem", + [ + forwardLink, + this.targetNote.getNoteTitle().trim() || forwardLink, + this.targetNote, + this.currentNote, + ], + { + dryRun: true, + }, + ); + return content; + } + + getIndexToInsert() { + if (!this.positionData) return -1; + let position = getPref("insertLinkPosition") as string; + if (!["start", "end"].includes(position)) { + position = "end"; + } + let index = { + start: this.positionData.lineIndex + 1, + end: this.positionData.endIndex + 1, + }[position]; + if (index === undefined) { + index = -1; + } + return index; + } +} diff --git a/src/elements/linkCreator/outlinePicker.ts b/src/elements/linkCreator/outlinePicker.ts new file mode 100644 index 0000000..7faf716 --- /dev/null +++ b/src/elements/linkCreator/outlinePicker.ts @@ -0,0 +1,165 @@ +import { config } from "../../../package.json"; +import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable"; +import { PluginCEBase } from "../base"; +import TreeModel = require("tree-model"); + +export class OutlinePicker extends PluginCEBase { + _item?: Zotero.Item; + _lineIndex?: number; + + noteOutlineView!: VirtualizedTableHelper; + noteOutline: TreeModel.Node[] = []; + + uid = Zotero.Utilities.randomString(8); + + get content() { + return MozXULElement.parseXULToFragment(` + + + + + + + + + + + + + + + + + + +`); + } + + get item() { + return this._item; + } + + set item(item: Zotero.Item | undefined) { + this._item = item; + } + + set lineIndex(index: number | undefined) { + this._lineIndex = index; + } + + get lineIndex() { + return this._lineIndex; + } + + async load() { + this.loadNoteOutline(); + } + + async loadNoteOutline() { + const renderLock = Zotero.Promise.defer(); + this.noteOutlineView = new VirtualizedTableHelper(window) + .setContainerId(`bn-select-note-outline-tree-${this.uid}`) + .setProp({ + id: `bn-select-note-outline-table-${this.uid}`, + 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", () => this.noteOutline.length || 0) + .setProp("getRowData", (index) => { + const model = this.noteOutline[index]?.model; + if (!model) return { level: 0, name: "**Unknown**" }; + return { + level: model.level, + name: + (model.level > 0 ? "··".repeat(model.level - 1) : "") + model.name, + }; + }) + .setProp("onSelectionChange", (selection) => { + this.onOutlineSelected(selection); + }) + // For find-as-you-type + .setProp( + "getRowString", + (index) => this.noteOutline[index]?.model.name || "", + ) + .render(-1, () => { + renderLock.resolve(); + }); + await renderLock.promise; + + // if (openedNotes.length === 1) { + // openedNotesView.treeInstance.selection.select(0); + // } + } + + onOutlineSelected(selection: { selected: Set }) { + this.dispatchSelectionChange(selection); + } + + async render() { + if (!this.item) { + return; + } + this.noteOutline = this._addon.api.note.getNoteTreeFlattened(this.item); + // Fake a cursor position + if (typeof this.lineIndex === "number") { + this.noteOutline.unshift({ + model: { + level: 0, + name: `🖋️Cursor (L${this._lineIndex})`, + lineIndex: this._lineIndex, + endIndex: this._lineIndex, + }, + } as any); + } + + this.noteOutlineView?.render(undefined); + } + + dispatchSelectionChange(selection: { selected: Set }) { + this.dispatchEvent( + new CustomEvent("selectionchange", { + detail: { + selectedSection: this.getSelectedSection(selection), + }, + }), + ); + } + + getSelectedSection(selection?: { selected: Set }): NoteNodeData { + return this.noteOutline[ + (selection || this.noteOutlineView.treeInstance.selection).selected + .values() + .next().value + ]?.model; + } +} diff --git a/src/elements/context.ts b/src/elements/workspace/contextPane.ts similarity index 84% rename from src/elements/context.ts rename to src/elements/workspace/contextPane.ts index dee1d8d..60cb271 100644 --- a/src/elements/context.ts +++ b/src/elements/workspace/contextPane.ts @@ -1,5 +1,5 @@ -import { config } from "../../package.json"; -import { PluginCEBase } from "./base"; +import { config } from "../../../package.json"; +import { PluginCEBase } from "../base"; export class ContextPane extends PluginCEBase { _item?: Zotero.Item; @@ -21,7 +21,7 @@ export class ContextPane extends PluginCEBase { diff --git a/src/elements/detailsPane.ts b/src/elements/workspace/detailsPane.ts similarity index 86% rename from src/elements/detailsPane.ts rename to src/elements/workspace/detailsPane.ts index 6104a7f..98e824b 100644 --- a/src/elements/detailsPane.ts +++ b/src/elements/workspace/detailsPane.ts @@ -1,12 +1,12 @@ -import { config } from "../../package.json"; +import { config } from "../../../package.json"; const ItemDetails = customElements.get("item-details")! as any; -export class NoteDetails extends ItemDetails { +export class DetailsPane extends ItemDetails { content = MozXULElement.parseXULToFragment(` diff --git a/src/elements/outlinePane.ts b/src/elements/workspace/outlinePane.ts similarity index 94% rename from src/elements/outlinePane.ts rename to src/elements/workspace/outlinePane.ts index 600721a..6fe3aa8 100644 --- a/src/elements/outlinePane.ts +++ b/src/elements/workspace/outlinePane.ts @@ -1,18 +1,18 @@ import { FilePickerHelper } from "zotero-plugin-toolkit/dist/helpers/filePicker"; -import { config } from "../../package.json"; -import { showHintWithLink } from "../utils/hint"; -import { formatPath } from "../utils/str"; -import { waitUtilAsync } from "../utils/wait"; -import { OutlineType } from "../utils/workspace"; -import { PluginCEBase } from "./base"; +import { config } from "../../../package.json"; +import { showHintWithLink } from "../../utils/hint"; +import { formatPath } from "../../utils/str"; +import { waitUtilAsync } from "../../utils/wait"; +import { OutlineType } from "../../utils/workspace"; +import { PluginCEBase } from "../base"; import { getEditorInstance, moveHeading, updateHeadingTextAtLine, -} from "../utils/editor"; -import { getNoteLinkParams } from "../utils/link"; -import { getNoteTree, getNoteTreeNodeById } from "../utils/note"; -import { getPref } from "../utils/prefs"; +} from "../../utils/editor"; +import { getNoteLinkParams } from "../../utils/link"; +import { getNoteTree, getNoteTreeNodeById } from "../../utils/note"; +import { getPref } from "../../utils/prefs"; export class OutlinePane extends PluginCEBase { _outlineType: OutlineType = OutlineType.empty; @@ -42,7 +42,7 @@ export class OutlinePane extends PluginCEBase { diff --git a/src/elements/related.ts b/src/elements/workspace/related.ts similarity index 96% rename from src/elements/related.ts rename to src/elements/workspace/related.ts index 64e32e4..7aacd07 100644 --- a/src/elements/related.ts +++ b/src/elements/workspace/related.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { config } from "../../package.json"; +import { config } from "../../../package.json"; const RelatedBox = customElements.get("related-box")! as typeof XULElementBase; @@ -11,7 +11,7 @@ export class NoteRelatedBox extends RelatedBox { diff --git a/src/extras/customElements.ts b/src/extras/customElements.ts index c0a649c..4ee11ea 100644 --- a/src/extras/customElements.ts +++ b/src/extras/customElements.ts @@ -1,16 +1,24 @@ -import { ContextPane } from "../elements/context"; -import { NoteDetails } from "../elements/detailsPane"; -import { NotePicker } from "../elements/notePicker"; -import { OutlinePane } from "../elements/outlinePane"; -import { NoteRelatedBox } from "../elements/related"; -import { Workspace } from "../elements/workspace"; +import { ContextPane } from "../elements/workspace/contextPane"; +import { DetailsPane } from "../elements/workspace/detailsPane"; +import { OutlinePicker } from "../elements/linkCreator/outlinePicker"; +import { NotePicker } from "../elements/linkCreator/notePicker"; +import { NotePreview } from "../elements/linkCreator/notePreview"; +import { OutlinePane } from "../elements/workspace/outlinePane"; +import { NoteRelatedBox } from "../elements/workspace/related"; +import { Workspace } from "../elements/workspace/workspace"; +import { InboundCreator } from "../elements/linkCreator/inboundCreator"; +import { OutboundCreator } from "../elements/linkCreator/outboundCreator"; const elements = { "bn-context": ContextPane, "bn-outline": OutlinePane, - "bn-details": NoteDetails as unknown as CustomElementConstructor, + "bn-details": DetailsPane as unknown as CustomElementConstructor, "bn-workspace": Workspace, "bn-note-picker": NotePicker, + "bn-note-outline": OutlinePicker, + "bn-note-preview": NotePreview, + "bn-inbound-creator": InboundCreator, + "bn-outbound-creator": OutboundCreator, "bn-related-box": NoteRelatedBox, }; diff --git a/src/extras/linkCreator.ts b/src/extras/linkCreator.ts new file mode 100644 index 0000000..7767394 --- /dev/null +++ b/src/extras/linkCreator.ts @@ -0,0 +1,85 @@ +import { getPref, setPref } from "../utils/prefs"; +import { InboundCreator } from "../elements/linkCreator/inboundCreator"; +import { OutboundCreator } from "../elements/linkCreator/outboundCreator"; + +let tabbox: XUL.TabBox; +let inboundCreator: InboundCreator; +let outboundCreator: OutboundCreator; + +let io: { + currentNoteID: number; + currentLineIndex?: number; + openedNoteIDs?: number[]; + deferred: _ZoteroTypes.DeferredPromise; + + targetNoteID?: number; + content?: string; + lineIndex?: number; +}; + +window.onload = async function () { + if (document.readyState === "complete") { + setTimeout(init, 0); + return; + } + document.addEventListener("DOMContentLoaded", init, { once: true }); +}; + +window.onunload = function () { + io.deferred && io.deferred.resolve(); + setPref( + "windows.linkCreator.size", + `${document.documentElement.getAttribute( + "width", + )},${document.documentElement.getAttribute("height")}`, + ); + setPref("windows.linkCreator.tabIndex", tabbox.selectedIndex); +}; + +function init() { + // Set font size from pref + const sbc = document.getElementById("top-container"); + Zotero.UIProperties.registerRoot(sbc); + + setTimeout(() => { + const size = ((getPref("windows.linkCreator.size") as string) || "").split( + ",", + ); + window.resizeTo(Number(size[0] || "800"), Number(size[1] || "600")); + }, 0); + + // @ts-ignore + io = window.arguments[0]; + + tabbox = document.querySelector("#top-container")!; + tabbox.selectedIndex = + (getPref("windows.linkCreator.tabIndex") as number) || 0; + tabbox.addEventListener("select", loadSelectedPanel); + + inboundCreator = document.querySelector( + "bn-inbound-creator", + ) as InboundCreator; + outboundCreator = document.querySelector( + "bn-outbound-creator", + ) as OutboundCreator; + loadSelectedPanel(); + + document.addEventListener("dialogaccept", doAccept); +} + +async function loadSelectedPanel() { + const content = getSelectedContent(); + await content.load(io); +} + +async function acceptSelectedPanel() { + await getSelectedContent().accept(io); +} + +function getSelectedContent() { + return tabbox.selectedPanel.querySelector("[data-bn-type=content]") as any; +} + +async function doAccept() { + await acceptSelectedPanel(); +} diff --git a/src/extras/linkNote.ts b/src/extras/linkNote.ts deleted file mode 100644 index f7f7a1f..0000000 --- a/src/extras/linkNote.ts +++ /dev/null @@ -1,339 +0,0 @@ -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); - - setTimeout(() => { - const size = ((getPref("windows.linkNote.size") as string) || "").split( - ",", - ); - window.resizeTo(Number(size[0] || "800"), Number(size[1] || "600")); - console.log(size); - }, 300); - - // @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(); - setPref( - "windows.linkNote.size", - `${document.documentElement.getAttribute( - "width", - )},${document.documentElement.getAttribute("height")}`, - ); -}; - -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/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts index 3c1f4df..5054839 100644 --- a/src/modules/editor/toolbar.ts +++ b/src/modules/editor/toolbar.ts @@ -4,7 +4,7 @@ import { getLineAtCursor, getSectionAtCursor } from "../../utils/editor"; import { showHint } from "../../utils/hint"; import { getNoteLink } from "../../utils/link"; import { getString } from "../../utils/locale"; -import { openLinkNoteDialog } from "../../utils/linkNote"; +import { openLinkCreator } from "../../utils/linkCreator"; import { slice } from "../../utils/str"; export async function initEditorToolbar(editor: Zotero.EditorInstance) { @@ -19,13 +19,15 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { classList: ["toolbar-button"], properties: { innerHTML: ICONS.addon, - title: "Link current note to another note", + title: "Link creator", }, listeners: [ { type: "click", listener: (e) => { - openLinkNoteDialog(noteItem); + openLinkCreator(noteItem, { + lineIndex: getLineAtCursor(editor), + }); }, }, ], diff --git a/src/modules/workspace/link.ts b/src/modules/workspace/link.ts index 2ad86be..9e9c66e 100644 --- a/src/modules/workspace/link.ts +++ b/src/modules/workspace/link.ts @@ -1,5 +1,5 @@ import { config } from "../../../package.json"; -import { Workspace } from "../../elements/workspace"; +import { Workspace } from "../../elements/workspace/workspace"; export function registerNoteLinkSection(type: "inbound" | "outbound") { const key = Zotero.ItemPaneManager.registerSection({ diff --git a/src/modules/workspace/relation.ts b/src/modules/workspace/relation.ts index 59c550c..17f0171 100644 --- a/src/modules/workspace/relation.ts +++ b/src/modules/workspace/relation.ts @@ -18,7 +18,7 @@ export function registerNoteRelation() {