add: link creator

refactor: link note -> link creator
add: link to mode in link creator
fix: locale
This commit is contained in:
windingwind 2024-04-16 00:41:51 +08:00
parent b6c0415267
commit 357e9a77bd
45 changed files with 1348 additions and 587 deletions

View File

@ -0,0 +1,54 @@
<?xml version="1.0"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://__addonRef__/content/styles/linkCreator/toolbar.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://__addonRef__/content/styles/linkCreator/linkCreator.css" type="text/css"?>
<!-- prettier-ignore -->
<!DOCTYPE window>
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
id="bn-note-picker"
data-l10n-id="__addonRef__-title"
windowtype="__addonRef__-link-note"
persist="screenX screenY width height sizemode"
style="min-width: 40em"
>
<linkset>
<html:link rel="localization" href="__addonRef__-linkCreator.ftl" />
</linkset>
<script src="chrome://zotero/content/include.js"></script>
<script src="chrome://zotero/content/customElements.js"></script>
<script src="chrome://__addonRef__/content/scripts/customElements.js"></script>
<script src="chrome://__addonRef__/content/scripts/linkCreator.js"></script>
<dialog buttons="accept, cancel">
<tabbox id="top-container" class="container">
<tabs>
<tab data-l10n-id="__addonRef__-tab-inbound"></tab>
<tab data-l10n-id="__addonRef__-tab-outbound"></tab>
</tabs>
<tabpanels class="container">
<tabpanel class="content-container">
<bn-inbound-creator
id="bn-inbound-creator"
data-bn-type="content"
></bn-inbound-creator>
</tabpanel>
<tabpanel class="content-container">
<bn-outbound-creator
id="bn-outbound-creator"
data-bn-type="content"
></bn-outbound-creator>
</tabpanel>
</tabpanels>
</tabbox>
</dialog>
</window>

View File

@ -1,98 +0,0 @@
<?xml version="1.0"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://__addonRef__/content/styles/toolbar.css" type="text/css"?>
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://__addonRef__/content/styles/linkNote.css" type="text/css"?>
<!-- prettier-ignore -->
<!DOCTYPE window>
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
id="bn-note-picker"
windowtype="__addonRef__-link-note"
persist="screenX screenY width height sizemode"
style="min-width: 40em"
>
<script src="chrome://zotero/content/include.js"></script>
<script src="chrome://zotero/content/customElements.js"></script>
<script src="chrome://__addonRef__/content/scripts/customElements.js"></script>
<script src="chrome://__addonRef__/content/scripts/linkNote.js"></script>
<dialog buttons="accept, cancel">
<hbox id="top-container" class="container">
<bn-note-picker></bn-note-picker>
<vbox id="bn-select-note-outline-container" class="container">
<hbox class="toolbar">
<hbox class="toolbar-start">
<html:span class="toolbar-header content"
>Step 2. Insert to:
</html:span>
<html:span
id="selected-outline-title"
class="toolbar-header highlight"
></html:span>
</hbox>
<hbox class="toolbar-middle"></hbox>
<hbox class="toolbar-end"></hbox>
</hbox>
<vbox
id="bn-select-note-outline-content"
class="container virtualized-table-container"
>
<html:div id="bn-select-note-outline-tree"></html:div>
</vbox>
<hbox id="bn-link-insert-position-container">
<label>At section</label>
<radiogroup id="bn-link-insert-position" orient="horizontal">
<radio
id="bn-link-insert-position-top"
label="Start"
value="start"
></radio>
<radio
id="bn-link-insert-position-bottom"
label="End"
value="end"
></radio>
</radiogroup>
</hbox>
</vbox>
<vbox id="bn-note-preview-container" class="container">
<hbox class="toolbar">
<hbox class="toolbar-start">
<html:span class="toolbar-header content"
>Step 3. Preview:
</html:span>
<html:span
id="preview-note-from-title"
class="toolbar-header highlight"
></html:span>
<html:span
id="preview-note-middle-title"
class="toolbar-header content"
></html:span>
<html:span
id="preview-note-to-title"
class="toolbar-header highlight"
></html:span>
</hbox>
<hbox class="toolbar-middle"></hbox>
<hbox class="toolbar-end"></hbox>
</hbox>
<vbox id="bn-note-preview-content" class="container">
<iframe
id="bn-note-preview"
class="container"
type="content"
></iframe>
</vbox>
</vbox>
</hbox>
</dialog>
</window>

View File

@ -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;
}
}

View File

@ -0,0 +1,14 @@
.container {
min-height: 0;
height: 100%;
margin: 0;
}
.content-container {
overflow: auto;
}
/* TODO: remove fx115 workaround */
tab {
color: unset !important;
}

View File

@ -0,0 +1,7 @@
bn-note-outline {
flex-direction: column;
}
#bn-link-insert-position-container {
align-items: center;
}

View File

@ -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%;
}
}

View File

@ -0,0 +1,3 @@
bn-note-preview {
flex-direction: column;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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%;
}

View File

@ -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");
}

View File

@ -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] { "" }
}

View File

@ -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] { "" }
}

View File

@ -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...

View File

@ -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] { "" }
}

View File

@ -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...

View File

@ -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] { "" }
}

View File

@ -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...

View File

@ -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] { "" }
}

View File

@ -1,7 +1,7 @@
note-relation-header =
.label = 关系图
note-relation-sidenav =
.tooltiptext = 关系
.tooltiptext = 关系
note-relation-refresh =
.tooltiptext = 刷新

View File

@ -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...

View File

@ -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) {

View File

@ -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(`
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/linkCreator/inboundCreator.css"
></html:link>
</linkset>
<bn-note-picker></bn-note-picker>
<bn-note-outline></bn-note-outline>
<bn-note-preview></bn-note-preview>
`);
}
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;
}
}

View File

@ -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(`
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/notePicker.css"
href="chrome://${config.addonRef}/content/styles/linkCreator/notePicker.css"
></html:link>
</linkset>
<vbox id="select-items-dialog" class="container">
@ -52,7 +54,7 @@ export class NotePicker extends PluginCEBase {
id="bn-select-opened-notes-content"
class="container virtualized-table-container"
>
<html:div id="bn-select-opened-notes-tree"></html:div>
<html:div id="bn-select-opened-notes-tree-${this.uid}"></html:div>
</vbox>
</vbox>
</vbox>
@ -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<number> }) {
this.activeSelectionType = "tabs";
this.dispatchSelectionChange();
this.dispatchSelectionChange(selection);
}
dispatchSelectionChange() {
dispatchSelectionChange(selection?: { selected: Set<number> }) {
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<number> }): 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]);
}
}

View File

@ -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<NoteNodeData>[] = [];
get content() {
return MozXULElement.parseXULToFragment(`
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/linkCreator/notePreview.css"
></html:link>
</linkset>
<hbox class="toolbar">
<hbox class="toolbar-start"></hbox>
<hbox class="toolbar-middle"></hbox>
<hbox class="toolbar-end"></hbox>
</hbox>
<vbox id="bn-note-preview-content" class="container">
<iframe
id="bn-note-preview"
class="container"
type="content"
></iframe>
</vbox>
`);
}
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 = `<html>
<head>
<title></title>
<link
rel="stylesheet"
type="text/css"
href="chrome://zotero-platform/content/zotero.css"
/>
<link
rel="stylesheet"
type="text/css"
href="chrome://${config.addonRef}/content/lib/css/github-markdown.css"
/>
<link
rel="stylesheet"
href="chrome://${config.addonRef}/content/lib/css/katex.min.css"
crossorigin="anonymous"
/>
<style>
html {
color-scheme: light dark;
background: var(--material-sidepane);
}
body {
overflow-x: clip;
}
#inserted {
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);
padding: 10px;
transition: all 0.3s ease;
}
#inserted:hover {
box-shadow: 0 5px 15px color-mix(in srgb, var(--material-background) 20%, transparent);
background: var(--color-background50);
}
</style>
</head>
<body>
<div>${options.before}</div>
<div id="inserted">${options.middle}</div>
<div>${options.after}</div>
</body>
</html>
`;
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",
});
}
}
}

View File

@ -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(`
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/linkCreator/outboundCreator.css"
></html:link>
</linkset>
<bn-note-picker></bn-note-picker>
<bn-note-outline></bn-note-outline>
<bn-note-preview></bn-note-preview>
`);
}
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;
}
}

View File

@ -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<NoteNodeData>[] = [];
uid = Zotero.Utilities.randomString(8);
get content() {
return MozXULElement.parseXULToFragment(`
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/linkCreator/noteOutline.css"
></html:link>
</linkset>
<hbox class="toolbar">
<hbox class="toolbar-start"></hbox>
<hbox class="toolbar-middle"></hbox>
<hbox class="toolbar-end"></hbox>
</hbox>
<vbox
id="bn-select-note-outline-content"
class="container virtualized-table-container"
>
<html:div id="bn-select-note-outline-tree-${this.uid}"></html:div>
</vbox>
<hbox id="bn-link-insert-position-container">
<label>At section</label>
<radiogroup id="bn-link-insert-position" orient="horizontal">
<radio
id="bn-link-insert-position-top"
label="Start"
value="start"
></radio>
<radio
id="bn-link-insert-position-bottom"
label="End"
value="end"
></radio>
</radiogroup>
</hbox>
`);
}
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<number> }) {
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<number> }) {
this.dispatchEvent(
new CustomEvent("selectionchange", {
detail: {
selectedSection: this.getSelectedSection(selection),
},
}),
);
}
getSelectedSection(selection?: { selected: Set<number> }): NoteNodeData {
return this.noteOutline[
(selection || this.noteOutlineView.treeInstance.selection).selected
.values()
.next().value
]?.model;
}
}

View File

@ -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 {
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/context.css"
href="chrome://${config.addonRef}/content/styles/workspace/context.css"
></html:link>
</linkset>
<bn-details id="container" class="container"></bn-details>

View File

@ -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(`
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/details.css"
href="chrome://${config.addonRef}/content/styles/workspace/details.css"
></html:link>
</linkset>
<hbox id="zotero-view-item-container" class="zotero-view-item-container" flex="1">

View File

@ -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 {
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/outline.css"
href="chrome://${config.addonRef}/content/styles/workspace/outline.css"
></html:link>
</linkset>
<hbox id="left-toolbar">

View File

@ -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 {
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/related.css"
href="chrome://${config.addonRef}/content/styles/workspace/related.css"
></html:link>
</linkset>
<collapsible-section

View File

@ -1,7 +1,7 @@
import { config } from "../../package.json";
import { waitUtilAsync } from "../utils/wait";
import { PluginCEBase } from "./base";
import { ContextPane } from "./context";
import { config } from "../../../package.json";
import { waitUtilAsync } from "../../utils/wait";
import { PluginCEBase } from "../base";
import { ContextPane } from "./contextPane";
import { OutlinePane } from "./outlinePane";
export class Workspace extends PluginCEBase {
@ -20,7 +20,7 @@ export class Workspace extends PluginCEBase {
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/workspace.css"
href="chrome://${config.addonRef}/content/styles/workspace/workspace.css"
></html:link>
</linkset>
<hbox id="top-container" class="container">

View File

@ -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,
};

85
src/extras/linkCreator.ts Normal file
View File

@ -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<void>;
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();
}

View File

@ -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<Addon["api"]["note"]["getNoteTreeFlattened"]> = [];
let positionData: NoteNodeData | undefined;
// @ts-ignore
window.addon = Zotero[config.addonRef];
let io: {
currentNoteID: number;
openedNoteIDs?: number[];
deferred: _ZoteroTypes.DeferredPromise<void>;
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<number> }) {
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 = `<html>
<head>
<title></title>
<link
rel="stylesheet"
type="text/css"
href="chrome://zotero-platform/content/zotero.css"
/>
<link
rel="stylesheet"
type="text/css"
href="chrome://${config.addonRef}/content/lib/css/github-markdown.css"
/>
<link
rel="stylesheet"
href="chrome://${config.addonRef}/content/lib/css/katex.min.css"
crossorigin="anonymous"
/>
<style>
html {
color-scheme: light dark;
background: var(--material-sidepane);
}
body {
overflow-x: clip;
}
#inserted {
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);
padding: 10px;
transition: all 0.3s ease;
}
#inserted:hover {
box-shadow: 0 5px 15px color-mix(in srgb, var(--material-background) 20%, transparent);
background: var(--color-background50);
}
</style>
</head>
<body>
<div>${before}</div>
<div id="inserted">${content}</div>
<div>${after}</div>
</body>
</html>
`;
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();
}

View File

@ -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),
});
},
},
],

View File

@ -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({

View File

@ -18,7 +18,7 @@ export function registerNoteRelation() {
<linkset>
<html:link
rel="stylesheet"
href="chrome://${config.addonRef}/content/styles/relation.css"
href="chrome://${config.addonRef}/content/styles/workspace/relation.css"
></html:link>
</linkset>
<iframe

View File

@ -1,18 +1,24 @@
import { config } from "../../package.json";
import { addLineToNote } from "./note";
export { openLinkNoteDialog };
export { openLinkCreator };
async function openLinkNoteDialog(currentNote: Zotero.Item) {
async function openLinkCreator(
currentNote: Zotero.Item,
options?: {
lineIndex: number;
},
) {
const io = {
openedNoteIDs: Zotero_Tabs._tabs
.map((tab) => tab.data?.itemID)
.filter((id) => id && id != currentNote.id),
currentNoteID: currentNote.id,
currentLineIndex: options?.lineIndex,
deferred: Zotero.Promise.defer(),
} as any;
window.openDialog(
`chrome://${config.addonRef}/content/linkNote.xhtml`,
`chrome://${config.addonRef}/content/linkCreator.xhtml`,
"_blank",
"chrome,modal,centerscreen,resizable=yes",
io,