diff --git a/addon/chrome/content/icons/in-link-16.svg b/addon/chrome/content/icons/in-link-16.svg
new file mode 100644
index 0000000..2faf4cb
--- /dev/null
+++ b/addon/chrome/content/icons/in-link-16.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/addon/chrome/content/icons/in-link-20.svg b/addon/chrome/content/icons/in-link-20.svg
new file mode 100644
index 0000000..375aa39
--- /dev/null
+++ b/addon/chrome/content/icons/in-link-20.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/addon/chrome/content/icons/out-link-16.svg b/addon/chrome/content/icons/out-link-16.svg
new file mode 100644
index 0000000..81b1dff
--- /dev/null
+++ b/addon/chrome/content/icons/out-link-16.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/addon/chrome/content/icons/out-link-20.svg b/addon/chrome/content/icons/out-link-20.svg
new file mode 100644
index 0000000..d9a993c
--- /dev/null
+++ b/addon/chrome/content/icons/out-link-20.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/addon/chrome/content/relationGraph.html b/addon/chrome/content/relationGraph.html
index 84d2a81..1296dcc 100644
--- a/addon/chrome/content/relationGraph.html
+++ b/addon/chrome/content/relationGraph.html
@@ -12,13 +12,6 @@
background: var(--material-background);
}
- .tooltip {
- position: absolute;
- left: 0;
- top: 0;
- margin: 10px;
- }
-
.node:hover {
cursor: pointer;
}
@@ -54,13 +47,13 @@
}
// Specify the color scale.
- const color = d3.scaleOrdinal([1, 2], ["grey", "#e8af59"]);
+ const color = d3.scaleOrdinal([1, 2], ["grey", "#A88F6A"]);
// The force simulation mutates links and nodes, so create a copy
// so that re-evaluating this cell produces the same result.
const links = data.links.map((d) => ({ ...d }));
const nodes = data.nodes.map((d) => ({ ...d }));
-
+ const linkColor = "#e8af59";
// Create a simulation with several forces.
const simulation = d3
.forceSimulation(nodes)
@@ -83,15 +76,34 @@
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto;");
- // Add a line for each link, and a circle for each node.
+ svg
+ .append("defs")
+ .append("marker")
+ .attr("id", "arrowhead")
+ .attr("refX", 25)
+ .attr("refY", 6)
+ .attr("orient", "auto-start-reverse")
+ .attr("markerWidth", 20)
+ .attr("markerHeight", 12)
+ .attr("markerUnits", "userSpaceOnUse")
+ .append("path")
+ .attr("d", "M 1 1 L 18 6 L 1 11 Z")
+ .attr("fill", linkColor)
+ .attr("stroke", linkColor)
+ .attr("class", "arrow-head");
+
const link = svg
.append("g")
- .attr("stroke", "#999")
- .attr("stroke-opacity", 0.6)
- .selectAll("line")
+ .attr("fill", "none")
+ .selectAll("path")
.data(links)
.join("line")
- .attr("stroke-width", (d) => Math.sqrt(d.value));
+ .attr("stroke-width", (d) => Math.sqrt(d.value))
+ .attr("stroke", linkColor)
+ .attr("marker-start", (d) =>
+ d.type === "both" ? "url(#arrowhead)" : "",
+ )
+ .attr("marker-end", "url(#arrowhead)");
const node = svg
.append("g")
@@ -114,7 +126,7 @@
.attr("text-anchor", "middle") // Ensure the text is centered
.attr("fill", "var(--text-color)")
.attr("stroke", "none")
- .text((d) => d.title);
+ .text((d) => (d.group === 1 ? "" : d.shortTitle));
node.append("title").text((d) => d.title);
@@ -168,7 +180,9 @@
.transition()
.duration(200)
.attr("r", 10); // New, larger radius
- d3.select(".tooltip").style("display", "").html(d.title);
+ d3.select(this)
+ .select("text")
+ .text((d) => d.title);
})
.on("mouseout", function (event, d) {
// Shrink the node circle back to original size
@@ -177,7 +191,9 @@
.transition()
.duration(500)
.attr("r", 7); // Original radius
- d3.select(".tooltip").style("display", "none");
+ d3.select(this)
+ .select("text")
+ .text((d) => (d.group === 1 ? "" : d.shortTitle));
})
.on("click", function (event, d) {
window.postMessage(
@@ -186,14 +202,7 @@
);
});
- document.body.replaceChildren(
- svg.node(),
- d3
- .create("div")
- .attr("class", "tooltip")
- .style("display", "none")
- .node(),
- );
+ document.body.replaceChildren(svg.node());
}
d3.select(window).on("resize", function () {
diff --git a/addon/chrome/content/styles/related.css b/addon/chrome/content/styles/related.css
index ee3abdf..132b974 100644
--- a/addon/chrome/content/styles/related.css
+++ b/addon/chrome/content/styles/related.css
@@ -2,60 +2,76 @@ bn-related-box {
display: flex;
flex-direction: column;
gap: 2px;
+
+ &[hidden] {
+ display: none;
+ }
+
+ &[readonly] {
+ .add {
+ display: none;
+ }
+ }
}
-bn-related-box[hidden] {
- display: none;
-}
-bn-related-box[readonly] .add {
- display: none;
-}
-bn-related-box .body {
+
+bn-related-box .body,
+item-pane-custom-section .bn-link-body {
display: flex;
flex-direction: column;
padding-inline-start: 12px;
-}
-bn-related-box .body .row {
- display: flex;
- gap: 4px;
- align-items: flex-start;
-}
-[zoteroUIDensity="comfortable"] bn-related-box .body .row {
- padding-block: 2px;
-}
-bn-related-box .body .row .box {
- display: flex;
- align-items: flex-start;
- gap: 4px;
- padding-inline-start: 4px;
- overflow: hidden;
- border-radius: 5px;
- flex: 1;
-}
-bn-related-box .body .row .box:not([disabled]):hover {
- background-color: var(--fill-quinary);
-}
-bn-related-box .body .row .box:not([disabled]):active {
- background-color: var(--fill-quarternary);
-}
-bn-related-box .body .row .box .icon {
- height: calc(1.3333333333 * var(--zotero-font-size));
-}
-bn-related-box .body .row .box .label {
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 10;
- width: 0;
- flex: 1;
- overflow: hidden;
-}
-bn-related-box .body .row .box .icon,
-bn-related-box .body .row .box .label {
- padding-block: 2px;
-}
-bn-related-box .body .row toolbarbutton {
- margin-inline-start: auto;
- visibility: hidden;
-}
-bn-related-box .body .row:is(:hover, :focus-within) toolbarbutton {
- visibility: visible;
+
+ .row {
+ display: flex;
+ gap: 4px;
+ align-items: flex-start;
+
+ [zoteroUIDensity="comfortable"] & {
+ padding-block: 2px;
+ }
+
+ .box {
+ display: flex;
+ align-items: flex-start;
+ gap: 4px;
+ padding-inline-start: 4px;
+ overflow: hidden;
+ border-radius: 5px;
+
+ &:not([disabled]):hover {
+ background-color: var(--fill-quinary);
+ }
+
+ &:not([disabled]):active {
+ background-color: var(--fill-quarternary);
+ }
+
+ .icon {
+ height: calc(1.3333333333 * var(--zotero-font-size));
+ }
+
+ .label {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 10;
+ width: 0;
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .icon,
+ .label {
+ padding-block: 2px;
+ }
+ flex: 1;
+ }
+
+ toolbarbutton {
+ margin-inline-start: auto;
+ visibility: hidden;
+ }
+
+ &:is(:hover, :focus-within) toolbarbutton {
+ visibility: visible;
+ }
+ }
}
diff --git a/addon/locale/en-US/noteRelation.ftl b/addon/locale/en-US/noteRelation.ftl
index 8efeb8f..6b3705c 100644
--- a/addon/locale/en-US/noteRelation.ftl
+++ b/addon/locale/en-US/noteRelation.ftl
@@ -4,3 +4,25 @@ note-relation-sidenav =
.tooltiptext = Relation Graph
note-relation-refresh =
.tooltiptext = Refresh
+
+note-inlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Inbound Link
+ *[other] { $count } Inbound Links
+ }
+note-inlink-sidenav =
+ .tooltiptext = Inbound Links
+note-inlink-refresh =
+ .tooltiptext = Refresh
+
+note-outlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Outbound Link
+ *[other] { $count } Outbound Links
+ }
+note-outlink-sidenav =
+ .tooltiptext = Outbound Links
+note-outlink-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/it-IT/noteRelation.ftl b/addon/locale/it-IT/noteRelation.ftl
index 8efeb8f..6b3705c 100644
--- a/addon/locale/it-IT/noteRelation.ftl
+++ b/addon/locale/it-IT/noteRelation.ftl
@@ -4,3 +4,25 @@ note-relation-sidenav =
.tooltiptext = Relation Graph
note-relation-refresh =
.tooltiptext = Refresh
+
+note-inlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Inbound Link
+ *[other] { $count } Inbound Links
+ }
+note-inlink-sidenav =
+ .tooltiptext = Inbound Links
+note-inlink-refresh =
+ .tooltiptext = Refresh
+
+note-outlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Outbound Link
+ *[other] { $count } Outbound Links
+ }
+note-outlink-sidenav =
+ .tooltiptext = Outbound Links
+note-outlink-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/ru-RU/noteRelation.ftl b/addon/locale/ru-RU/noteRelation.ftl
index 8efeb8f..6b3705c 100644
--- a/addon/locale/ru-RU/noteRelation.ftl
+++ b/addon/locale/ru-RU/noteRelation.ftl
@@ -4,3 +4,25 @@ note-relation-sidenav =
.tooltiptext = Relation Graph
note-relation-refresh =
.tooltiptext = Refresh
+
+note-inlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Inbound Link
+ *[other] { $count } Inbound Links
+ }
+note-inlink-sidenav =
+ .tooltiptext = Inbound Links
+note-inlink-refresh =
+ .tooltiptext = Refresh
+
+note-outlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Outbound Link
+ *[other] { $count } Outbound Links
+ }
+note-outlink-sidenav =
+ .tooltiptext = Outbound Links
+note-outlink-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/tr-TR/noteRelation.ftl b/addon/locale/tr-TR/noteRelation.ftl
index 8efeb8f..6b3705c 100644
--- a/addon/locale/tr-TR/noteRelation.ftl
+++ b/addon/locale/tr-TR/noteRelation.ftl
@@ -4,3 +4,25 @@ note-relation-sidenav =
.tooltiptext = Relation Graph
note-relation-refresh =
.tooltiptext = Refresh
+
+note-inlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Inbound Link
+ *[other] { $count } Inbound Links
+ }
+note-inlink-sidenav =
+ .tooltiptext = Inbound Links
+note-inlink-refresh =
+ .tooltiptext = Refresh
+
+note-outlink-header =
+ .label =
+ { $count ->
+ [one] { $count } Outbound Link
+ *[other] { $count } Outbound Links
+ }
+note-outlink-sidenav =
+ .tooltiptext = Outbound Links
+note-outlink-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/zh-CN/noteRelation.ftl b/addon/locale/zh-CN/noteRelation.ftl
index cd201ad..534952e 100644
--- a/addon/locale/zh-CN/noteRelation.ftl
+++ b/addon/locale/zh-CN/noteRelation.ftl
@@ -4,3 +4,25 @@ note-relation-sidenav =
.tooltiptext = 关系图
note-relation-refresh =
.tooltiptext = 刷新
+
+note-inlink-header =
+ .label =
+ { $count ->
+ [one] { $count }条入链
+ *[other] { $count }条入链
+ }
+note-inlink-sidenav =
+ .tooltiptext = 入链
+note-inlink-refresh =
+ .tooltiptext = 刷新
+
+note-outlink-header =
+ .label =
+ { $count ->
+ [one] { $count }条出链
+ *[other] { $count }条出链
+ }
+note-outlink-sidenav =
+ .tooltiptext = 出链
+note-outlink-refresh =
+ .tooltiptext = 刷新
diff --git a/src/elements/related.ts b/src/elements/related.ts
index a0ca49c..64e32e4 100644
--- a/src/elements/related.ts
+++ b/src/elements/related.ts
@@ -1,6 +1,5 @@
// @ts-nocheck
import { config } from "../../package.json";
-import { getPref } from "../utils/prefs";
const RelatedBox = customElements.get("related-box")! as typeof XULElementBase;
@@ -68,14 +67,14 @@ export class NoteRelatedBox extends RelatedBox {
// Extra button to open note
if (relatedItem.isNote()) {
- const note = document.createXULElement("toolbarbutton");
- note.addEventListener("command", (event) => {
+ const openNote = document.createXULElement("toolbarbutton");
+ openNote.addEventListener("command", (event) => {
const position = event.shiftKey ? "window" : "tab";
Zotero[config.addonRef].hooks.onOpenNote(id, position);
});
- note.className = "zotero-clicky zotero-clicky-open-link";
- note.setAttribute("tabindex", "0");
- row.append(note);
+ openNote.className = "zotero-clicky zotero-clicky-open-link";
+ openNote.setAttribute("tabindex", "0");
+ row.append(openNote);
}
if (this.editable) {
diff --git a/src/hooks.ts b/src/hooks.ts
index 6f127a1..9ab87c3 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -37,8 +37,10 @@ import { initSyncList } from "./modules/sync/api";
import { patchViewItems } from "./modules/viewItems";
import { getFocusedWindow } from "./utils/window";
import { registerNoteRelation } from "./modules/workspace/relation";
-import { getPref } from "./utils/prefs";
+import { getPref, setPref } from "./utils/prefs";
import { closeRelationWorker } from "./utils/relation";
+import { registerNoteInboundLink } from "./modules/workspace/inLink";
+import { registerNoteOutboundLink } from "./modules/workspace/outLink";
async function onStartup() {
await Promise.all([
@@ -46,6 +48,7 @@ async function onStartup() {
Zotero.unlockPromise,
Zotero.uiReadyPromise,
]);
+ Zotero.Prefs.set("layout.css.nesting.enabled", true, true);
initLocale();
ztoolkit.ProgressWindow.setIconURI(
"default",
@@ -62,6 +65,10 @@ async function onStartup() {
registerNoteRelation();
+ registerNoteOutboundLink();
+
+ registerNoteInboundLink();
+
initSyncList();
setSyncing();
diff --git a/src/modules/workspace/inLink.ts b/src/modules/workspace/inLink.ts
new file mode 100644
index 0000000..c8c896c
--- /dev/null
+++ b/src/modules/workspace/inLink.ts
@@ -0,0 +1,125 @@
+import { config } from "../../../package.json";
+
+export function registerNoteInboundLink() {
+ const key = Zotero.ItemPaneManager.registerSection({
+ paneID: `bn-note-inbound-link`,
+ pluginID: config.addonID,
+ header: {
+ icon: `chrome://${config.addonRef}/content/icons/in-link-16.svg`,
+ l10nID: `${config.addonRef}-note-inlink-header`,
+ },
+ sidenav: {
+ icon: `chrome://${config.addonRef}/content/icons/in-link-20.svg`,
+ l10nID: `${config.addonRef}-note-inlink-sidenav`,
+ },
+ sectionButtons: [
+ {
+ type: "refreshGraph",
+ icon: "chrome://zotero/skin/16/universal/sync.svg",
+ l10nID: `${config.addonRef}-note-inlink-refresh`,
+ onClick: ({ body, item, setL10nArgs }) => {
+ renderSection(body, item, makeSetCount(setL10nArgs));
+ },
+ },
+ ],
+ onInit({ body, refresh }) {
+ const notifierKey = Zotero.Notifier.registerObserver(
+ {
+ notify: (event, type, ids, extraData) => {
+ const item = Zotero.Items.get(body.dataset.itemID || "");
+ if (
+ item &&
+ // @ts-ignore
+ event === "updateBNRelation" &&
+ type === "item" &&
+ (ids as number[]).includes(item.id)
+ ) {
+ ztoolkit.log("relation notify refresh", item.id);
+ refresh();
+ }
+ },
+ },
+ ["item"],
+ );
+ body.classList.add("bn-link-body");
+ body.dataset.notifierKey = notifierKey;
+ },
+ onDestroy({ body }) {
+ const notifierKey = body.dataset.notifierKey;
+ if (notifierKey) {
+ Zotero.Notifier.unregisterObserver(notifierKey);
+ }
+ },
+ onItemChange: ({ body, item, setEnabled }) => {
+ if (body.closest("bn-workspace") as HTMLElement | undefined) {
+ setEnabled(true);
+ body.dataset.itemID = String(item.id);
+ return;
+ }
+ setEnabled(false);
+ },
+ onRender: () => {},
+ onAsyncRender: async ({ body, item, setL10nArgs }) => {
+ await renderSection(body, item, makeSetCount(setL10nArgs));
+ },
+ });
+}
+
+async function renderSection(
+ body: HTMLElement,
+ item: Zotero.Item,
+ setCount: (count: number) => void,
+) {
+ body.replaceChildren();
+ const doc = body.ownerDocument;
+ const inLinks = await addon.api.relation.getNoteLinkInboundRelation(item.id);
+ for (const linkData of inLinks) {
+ const fromItem = (await Zotero.Items.getByLibraryAndKeyAsync(
+ linkData.fromLibID,
+ linkData.fromKey,
+ )) as Zotero.Item;
+
+ const row = doc.createElement("div");
+ row.className = "row";
+
+ const icon = ztoolkit
+ .getGlobal("require")("components/icons")
+ .getCSSItemTypeIcon("note");
+
+ const label = doc.createElement("span");
+ label.className = "label";
+ label.append(fromItem.getNoteTitle());
+
+ const box = doc.createElement("div");
+ box.addEventListener("click", () => handleShowItem(fromItem.id));
+ box.className = "box keyboard-clickable";
+ box.setAttribute("tabindex", "0");
+ box.append(icon, label);
+
+ row.append(box);
+
+ const note = (doc as any).createXULElement("toolbarbutton");
+ note.addEventListener("command", (event: MouseEvent) => {
+ const position = event.shiftKey ? "window" : "tab";
+ addon.hooks.onOpenNote(fromItem.id, position);
+ });
+ note.className = "zotero-clicky zotero-clicky-open-link";
+ note.setAttribute("tabindex", "0");
+ row.append(note);
+
+ body.append(row);
+ }
+
+ const count = inLinks.length;
+ setCount(count);
+}
+
+function handleShowItem(id: number) {
+ ZoteroPane.selectItem(id);
+}
+
+function makeSetCount(setL10nArgs: (str: string) => void) {
+ return (count: number) => {
+ setL10nArgs(`{"count": "${count}"}`);
+ };
+}
diff --git a/src/modules/workspace/outLink.ts b/src/modules/workspace/outLink.ts
new file mode 100644
index 0000000..69a9f73
--- /dev/null
+++ b/src/modules/workspace/outLink.ts
@@ -0,0 +1,125 @@
+import { config } from "../../../package.json";
+
+export function registerNoteOutboundLink() {
+ const key = Zotero.ItemPaneManager.registerSection({
+ paneID: `bn-note-outbound-link`,
+ pluginID: config.addonID,
+ header: {
+ icon: `chrome://${config.addonRef}/content/icons/out-link-16.svg`,
+ l10nID: `${config.addonRef}-note-outlink-header`,
+ },
+ sidenav: {
+ icon: `chrome://${config.addonRef}/content/icons/out-link-20.svg`,
+ l10nID: `${config.addonRef}-note-outlink-sidenav`,
+ },
+ sectionButtons: [
+ {
+ type: "refreshGraph",
+ icon: "chrome://zotero/skin/16/universal/sync.svg",
+ l10nID: `${config.addonRef}-note-outlink-refresh`,
+ onClick: ({ body, item, setL10nArgs }) => {
+ renderSection(body, item, makeSetCount(setL10nArgs));
+ },
+ },
+ ],
+ onInit({ body, refresh, getData }) {
+ const notifierKey = Zotero.Notifier.registerObserver(
+ {
+ notify: (event, type, ids, extraData) => {
+ const item = Zotero.Items.get(body.dataset.itemID || "");
+ if (
+ item &&
+ // @ts-ignore
+ event === "updateBNRelation" &&
+ type === "item" &&
+ (ids as number[]).includes(item.id)
+ ) {
+ ztoolkit.log("relation notify refresh", item.id);
+ refresh();
+ }
+ },
+ },
+ ["item"],
+ );
+ body.classList.add("bn-link-body");
+ body.dataset.notifierKey = notifierKey;
+ },
+ onDestroy({ body }) {
+ const notifierKey = body.dataset.notifierKey;
+ if (notifierKey) {
+ Zotero.Notifier.unregisterObserver(notifierKey);
+ }
+ },
+ onItemChange: ({ body, item, setEnabled }) => {
+ if (body.closest("bn-workspace") as HTMLElement | undefined) {
+ setEnabled(true);
+ body.dataset.itemID = String(item.id);
+ return;
+ }
+ setEnabled(false);
+ },
+ onRender: () => {},
+ onAsyncRender: async ({ body, item, setL10nArgs }) => {
+ await renderSection(body, item, makeSetCount(setL10nArgs));
+ },
+ });
+}
+
+async function renderSection(
+ body: HTMLElement,
+ item: Zotero.Item,
+ setCount: (count: number) => void,
+) {
+ body.replaceChildren();
+ const doc = body.ownerDocument;
+ const inLinks = await addon.api.relation.getNoteLinkOutboundRelation(item.id);
+ for (const linkData of inLinks) {
+ const toItem = (await Zotero.Items.getByLibraryAndKeyAsync(
+ linkData.toLibID,
+ linkData.toKey,
+ )) as Zotero.Item;
+
+ const row = doc.createElement("div");
+ row.className = "row";
+
+ const icon = ztoolkit
+ .getGlobal("require")("components/icons")
+ .getCSSItemTypeIcon("note");
+
+ const label = doc.createElement("span");
+ label.className = "label";
+ label.append(toItem.getNoteTitle());
+
+ const box = doc.createElement("div");
+ box.addEventListener("click", () => handleShowItem(toItem.id));
+ box.className = "box keyboard-clickable";
+ box.setAttribute("tabindex", "0");
+ box.append(icon, label);
+
+ row.append(box);
+
+ const note = (doc as any).createXULElement("toolbarbutton");
+ note.addEventListener("command", (event: MouseEvent) => {
+ const position = event.shiftKey ? "window" : "tab";
+ addon.hooks.onOpenNote(toItem.id, position);
+ });
+ note.className = "zotero-clicky zotero-clicky-open-link";
+ note.setAttribute("tabindex", "0");
+ row.append(note);
+
+ body.append(row);
+ }
+
+ const count = inLinks.length;
+ setCount(count);
+}
+
+function handleShowItem(id: number) {
+ ZoteroPane.selectItem(id);
+}
+
+function makeSetCount(setL10nArgs: (str: string) => void) {
+ return (count: number) => {
+ setL10nArgs(`{"count": "${count}"}`);
+ };
+}
diff --git a/src/modules/workspace/relation.ts b/src/modules/workspace/relation.ts
index 5fc4a44..b2d47af 100644
--- a/src/modules/workspace/relation.ts
+++ b/src/modules/workspace/relation.ts
@@ -106,9 +106,10 @@ async function getRelationData(note: Zotero.Item) {
const inLink = await addon.api.relation.getNoteLinkInboundRelation(note.id);
const outLink = await addon.api.relation.getNoteLinkOutboundRelation(note.id);
- const links = [];
const noteSet: Set = new Set();
+ const linkModels: Record = {};
+
for (const linkData of inLink) {
const noteItem = await Zotero.Items.getByLibraryAndKeyAsync(
linkData.fromLibID,
@@ -116,11 +117,18 @@ async function getRelationData(note: Zotero.Item) {
);
if (!noteItem) continue;
noteSet.add(noteItem.id);
- links.push({
- source: noteItem.id,
- target: note.id,
- value: 1,
- });
+ let noteLinks = linkModels[noteItem.id];
+ if (!noteLinks) {
+ noteLinks = {
+ source: noteItem.id,
+ target: note.id,
+ value: 1,
+ type: "in",
+ };
+ linkModels[noteItem.id] = noteLinks;
+ } else {
+ noteLinks.value++;
+ }
}
for (const linkData of outLink) {
@@ -130,28 +138,52 @@ async function getRelationData(note: Zotero.Item) {
);
if (!noteItem) continue;
noteSet.add(noteItem.id);
- links.push({
- source: note.id,
- target: noteItem.id,
- value: 1,
- });
+ let noteLinks = linkModels[noteItem.id];
+ if (!noteLinks) {
+ noteLinks = {
+ source: note.id,
+ target: noteItem.id,
+ value: 1,
+ type: "out",
+ };
+ linkModels[noteItem.id] = noteLinks;
+ } else {
+ noteLinks.value++;
+ if (noteLinks.type === "in") {
+ noteLinks.type = "both";
+ }
+ }
}
noteSet.delete(note.id);
const nodes = Array.from(noteSet).map((id) => {
const item = Zotero.Items.get(id);
+ const title = item.getNoteTitle();
return {
id: item.id,
- title: slice(item.getNoteTitle(), 15),
+ title,
+ shortTitle: slice(title, 15),
group: 2,
};
});
+ const title = note.getNoteTitle();
nodes.push({
id: note.id,
- title: slice(note.getNoteTitle(), 15),
+ title,
+ shortTitle: slice(title, 15),
group: 1,
});
- return { nodes, links };
+ return {
+ nodes,
+ links: Object.values(linkModels),
+ };
+}
+
+interface NoteLinkModal {
+ source: number;
+ target: number;
+ value: number;
+ type: "in" | "out" | "both";
}
diff --git a/src/utils/relation.ts b/src/utils/relation.ts
index df26bdf..6ee4188 100644
--- a/src/utils/relation.ts
+++ b/src/utils/relation.ts
@@ -80,7 +80,7 @@ async function updateNoteLinkRelation(noteID: number) {
for (const link of linkMatches) {
const { noteItem, libraryID, noteKey, lineIndex, sectionName } =
getNoteLinkParams(link);
- if (noteItem && noteItem.isNote()) {
+ if (noteItem && noteItem.isNote() && noteItem.id !== note.id) {
affectedNoteIDs.add(noteItem.id);
linkToData.push({
fromLibID,