add: inlink and outlink section

add: link relation graph direction
This commit is contained in:
windingwind 2024-04-12 20:38:08 +08:00
parent b16d0e13c3
commit a22dad3250
17 changed files with 546 additions and 99 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4670" width="16" height="16">
<path
d="m4.23,2.66c0,.52-.43.94-.95.93-.52,0-.94-.42-.94-.94s.42-.94.94-.94c.52,0,.94.42.95.93v.02h0Zm4.11-.11c1.14.51,2.18,1.29,2.66,2.4,1.27-.33,2.61.17,3.36,1.25.75,1.08.75,2.51,0,3.59-.75,1.08-2.09,1.58-3.36,1.25-.47,1.11-1.52,1.9-2.66,2.4-.94.42-2,.68-3.04.78-.44,1-1.54,1.52-2.59,1.24-1.05-.29-1.74-1.3-1.61-2.38.13-1.08,1.03-1.91,2.11-1.94,1.09-.04,2.04.73,2.24,1.8.82-.11,1.65-.33,2.37-.65,1.03-.46,1.75-1.08,2.03-1.81-.6-.46-1.01-1.12-1.17-1.87h-3.3c-.31,1.05-1.35,1.71-2.43,1.55-1.08-.16-1.88-1.09-1.88-2.18,0-1.09.8-2.02,1.88-2.17,1.08-.16,2.11.5,2.43,1.54h3.3c.15-.74.57-1.4,1.17-1.87-.29-.73-1-1.35-2.03-1.81-.72-.32-1.55-.54-2.37-.65-.2,1.07-1.15,1.84-2.24,1.8-1.09-.03-1.99-.86-2.12-1.94-.13-1.08.56-2.09,1.61-2.38,1.05-.29,2.15.24,2.59,1.24,1.03.11,2.1.37,3.04.78h0Zm-4.11,10.79c0-.52-.43-.94-.95-.93-.52,0-.94.42-.94.94,0,.52.42.94.94.94.52,0,.94-.42.95-.93v-.02Zm9.43-5.34c0-1.04-.84-1.89-1.89-1.89s-1.89.84-1.89,1.89.84,1.89,1.89,1.89,1.89-.84,1.89-1.89h0Zm-9.43,0c0-.52-.42-.94-.94-.94s-.94.42-.94.94.42.94.94.94.94-.42.94-.94h0Zm0,0"
fill="#e8af59" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4670" width="20" height="20">
<path
d="m5.27,3.31c0,.65-.54,1.17-1.19,1.17-.65,0-1.18-.53-1.18-1.18s.53-1.18,1.18-1.18c.65,0,1.18.52,1.19,1.17v.02h0Zm5.15-.14c1.43.63,2.74,1.62,3.33,3.01,1.6-.41,3.28.22,4.22,1.57.94,1.35.94,3.15,0,4.5-.94,1.35-2.62,1.98-4.22,1.57-.6,1.4-1.9,2.38-3.33,3.01-1.18.52-2.51.85-3.81.98-.55,1.25-1.93,1.91-3.25,1.55-1.32-.36-2.18-1.63-2.02-2.98.16-1.36,1.29-2.39,2.65-2.44,1.37-.04,2.56.92,2.8,2.26,1.03-.14,2.07-.41,2.98-.81,1.29-.57,2.19-1.35,2.55-2.26-.75-.58-1.27-1.41-1.46-2.34h-4.14c-.39,1.31-1.69,2.14-3.04,1.94-1.36-.2-2.36-1.36-2.36-2.73,0-1.37,1.01-2.53,2.36-2.72,1.35-.2,2.65.63,3.04,1.94h4.14c.19-.93.71-1.76,1.46-2.34-.36-.91-1.26-1.69-2.55-2.26-.91-.4-1.95-.68-2.98-.81-.25,1.34-1.44,2.31-2.8,2.26-1.37-.04-2.49-1.08-2.65-2.44-.16-1.36.7-2.63,2.02-2.98,1.32-.36,2.7.3,3.25,1.55,1.29.13,2.63.46,3.81.98h0Zm-5.15,13.52c0-.65-.54-1.17-1.19-1.17-.65,0-1.18.53-1.18,1.18s.53,1.18,1.18,1.18c.65,0,1.18-.52,1.19-1.17v-.02Zm11.83-6.69c0-1.31-1.06-2.37-2.37-2.37s-2.37,1.06-2.37,2.37c0,1.31,1.06,2.37,2.37,2.37s2.37-1.06,2.37-2.37h0Zm-11.83,0c0-.65-.53-1.18-1.18-1.18s-1.18.53-1.18,1.18.53,1.18,1.18,1.18,1.18-.53,1.18-1.18h0Zm0,0"
fill="#e8af59" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4670" width="16" height="16">
<path
d="m11.77,2.66v-.02c0-.52.43-.94.95-.93.52,0,.94.42.94.94s-.42.94-.94.94c-.52,0-.94-.42-.95-.93h0Zm-4.11-.11c.94-.42,2-.68,3.04-.78.44-1,1.54-1.52,2.59-1.24,1.05.29,1.73,1.3,1.61,2.38-.13,1.08-1.03,1.91-2.12,1.94-1.09.03-2.04-.73-2.24-1.8-.82.11-1.65.33-2.37.65-1.03.46-1.75,1.08-2.03,1.81.6.46,1.01,1.12,1.17,1.87h3.3c.31-1.04,1.35-1.7,2.43-1.54,1.08.16,1.88,1.08,1.88,2.17,0,1.09-.8,2.02-1.88,2.18-1.08.16-2.12-.5-2.43-1.55h-3.3c-.15.74-.57,1.4-1.17,1.87.29.73,1,1.35,2.03,1.81.72.32,1.55.54,2.37.65.2-1.07,1.15-1.84,2.24-1.8,1.09.04,1.99.86,2.11,1.94.13,1.08-.56,2.09-1.61,2.38-1.05.29-2.15-.24-2.59-1.24-1.03-.11-2.1-.37-3.04-.78-1.14-.51-2.18-1.29-2.66-2.4-1.27.33-2.61-.17-3.36-1.25-.75-1.08-.75-2.51,0-3.59.75-1.08,2.09-1.58,3.36-1.25.47-1.11,1.52-1.9,2.66-2.4h0Zm4.11,10.8c0,.52.43.94.95.93.52,0,.94-.42.94-.94s-.42-.94-.94-.94c-.52,0-.94.42-.95.93v.02ZM2.34,8c0,1.04.84,1.89,1.89,1.89s1.89-.84,1.89-1.89c0-1.04-.84-1.89-1.89-1.89s-1.89.84-1.89,1.89h0Zm9.43,0c0,.52.42.94.94.94s.94-.42.94-.94-.42-.94-.94-.94-.94.42-.94.94h0Zm0,0"
fill="#e8af59" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4670" width="20" height="20">
<path
d="m14.71,3.28v-.02c0-.65.54-1.17,1.19-1.17.65,0,1.18.53,1.18,1.18s-.53,1.18-1.18,1.18c-.65,0-1.18-.52-1.19-1.17h0Zm-5.15-.14c1.18-.52,2.51-.85,3.81-.98.55-1.25,1.93-1.91,3.25-1.55,1.32.36,2.17,1.63,2.02,2.98-.16,1.36-1.29,2.39-2.65,2.44-1.37.04-2.56-.92-2.8-2.26-1.03.14-2.07.41-2.98.81-1.29.57-2.19,1.35-2.55,2.26.75.58,1.27,1.41,1.46,2.34h4.14c.39-1.31,1.69-2.13,3.04-1.94,1.35.2,2.36,1.36,2.36,2.72,0,1.37-1,2.54-2.36,2.73-1.36.2-2.65-.63-3.04-1.94h-4.14c-.19.93-.71,1.76-1.46,2.34.36.91,1.26,1.69,2.55,2.26.91.4,1.95.68,2.98.81.25-1.34,1.44-2.3,2.8-2.26,1.37.04,2.49,1.08,2.65,2.44.16,1.36-.7,2.63-2.02,2.98-1.32.36-2.7-.3-3.25-1.55-1.29-.13-2.63-.46-3.81-.98-1.43-.63-2.74-1.62-3.33-3.01-1.6.41-3.28-.22-4.22-1.57-.94-1.35-.94-3.15,0-4.5.94-1.35,2.62-1.98,4.22-1.57.6-1.4,1.9-2.38,3.33-3.01h0Zm5.15,13.55c0,.65.54,1.17,1.19,1.17.65,0,1.18-.53,1.18-1.18s-.53-1.18-1.18-1.18c-.65,0-1.18.52-1.19,1.17v.02ZM2.88,9.97c0,1.31,1.06,2.37,2.37,2.37s2.37-1.06,2.37-2.37c0-1.31-1.06-2.37-2.37-2.37s-2.37,1.06-2.37,2.37h0Zm11.83,0c0,.65.53,1.18,1.18,1.18s1.18-.53,1.18-1.18-.53-1.18-1.18-1.18-1.18.53-1.18,1.18h0Zm0,0"
fill="#e8af59" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = 刷新

View File

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

View File

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

View File

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

View File

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

View File

@ -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<number> = new Set();
const linkModels: Record<number, NoteLinkModal> = {};
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";
}

View File

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