From 415705a3bf7a30235fb991d84490797ff223518d Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:20:25 +0800 Subject: [PATCH 01/23] add: note tabs --- addon/chrome/content/bubbleMap.html | 19 +- addon/chrome/content/icons/outline-20.svg | 12 + addon/chrome/content/icons/save-20.svg | 9 + addon/chrome/content/mindMap.html | 21 +- addon/chrome/content/styles/context.css | 3 + addon/chrome/content/styles/details.css | 3 + addon/chrome/content/styles/editor.css | 49 +++ addon/chrome/content/styles/outline.css | 39 ++ addon/chrome/content/styles/workspace.css | 11 + addon/chrome/content/treeView.html | 23 +- addon/locale/en-US/outline.ftl | 20 + package.json | 2 +- scripts/build-extras.mjs | 14 +- scripts/build.mjs | 1 + src/addon.ts | 48 --- src/api.ts | 7 +- src/elements/base.ts | 60 +++ src/elements/context.ts | 47 +++ src/elements/detailsPane.ts | 30 ++ src/elements/outlinePane.ts | 379 +++++++++++++++++ src/elements/workspace.ts | 116 +++++ src/extras/workspace.ts | 17 + src/hooks.ts | 148 ++----- src/modules/annotationTagAction.ts | 42 -- src/modules/createNote.ts | 40 +- src/modules/editor/inject.ts | 38 +- src/modules/editor/toolbar.ts | 196 ++++----- src/modules/export/api.ts | 2 +- src/modules/menu.ts | 171 +++----- src/modules/viewItems.ts | 28 ++ src/modules/workspace/content.ts | 489 +--------------------- src/modules/workspace/message.ts | 84 ---- src/modules/workspace/tab.ts | 367 ++-------------- src/modules/workspace/window.ts | 21 +- src/utils/note.ts | 17 +- src/utils/wait.ts | 12 +- typings/global.d.ts | 19 + 37 files changed, 1116 insertions(+), 1488 deletions(-) create mode 100644 addon/chrome/content/icons/outline-20.svg create mode 100644 addon/chrome/content/icons/save-20.svg create mode 100644 addon/chrome/content/styles/context.css create mode 100644 addon/chrome/content/styles/details.css create mode 100644 addon/chrome/content/styles/editor.css create mode 100644 addon/chrome/content/styles/outline.css create mode 100644 addon/chrome/content/styles/workspace.css create mode 100644 addon/locale/en-US/outline.ftl create mode 100644 src/elements/base.ts create mode 100644 src/elements/context.ts create mode 100644 src/elements/detailsPane.ts create mode 100644 src/elements/outlinePane.ts create mode 100644 src/elements/workspace.ts create mode 100644 src/extras/workspace.ts delete mode 100644 src/modules/annotationTagAction.ts create mode 100644 src/modules/viewItems.ts delete mode 100644 src/modules/workspace/message.ts diff --git a/addon/chrome/content/bubbleMap.html b/addon/chrome/content/bubbleMap.html index d194737..74f350e 100644 --- a/addon/chrome/content/bubbleMap.html +++ b/addon/chrome/content/bubbleMap.html @@ -172,12 +172,12 @@ ), $(go.Panel, "Auto"), ); - window.parent.postMessage({ type: "ready" }, "*"); + window.postMessage({ type: "ready" }, "*"); getData(); } function getData() { - window.parent.postMessage({ type: "getMindMapData" }, "*"); + window.postMessage({ type: "getMindMapData" }, "*"); } function setData(nodes) { @@ -219,17 +219,16 @@ var oldnode = adorn.adornedPart; var olddata = oldnode.data; if (olddata.noteLink) { - window.parent.postMessage( + window.postMessage( { type: "openNote", link: olddata.noteLink, id: olddata.key }, "*", ); } else { - window.parent.postMessage( + window.postMessage( { type: "jumpNode", lineIndex: olddata.lineIndex, id: olddata.key, - workspaceType: window.workspaceType || "tab", }, "*", ); @@ -243,12 +242,11 @@ alert("Link cannot be edited in mind map"); return false; } - window.parent.postMessage( + window.postMessage( { type: "editNode", lineIndex: data.lineIndex, text: data.text, - workspaceType: window.workspaceType || "tab", }, "*", ); @@ -259,30 +257,31 @@ switch (e.data.type) { case "setMindMapData": setData(e.data.nodes); - window.workspaceType = e.data.workspaceType; break; case "saveImage": const imgString = Diagram.makeImageData({ scale: 1, }); - window.parent.postMessage( + window.postMessage( { type: "saveImageReturn", image: imgString, }, "*", ); + break; case "saveSVG": const svgElement = Diagram.makeSvg({ scale: 1, }); - window.parent.postMessage( + window.postMessage( { type: "saveSVGReturn", image: svgElement.outerHTML, }, "*", ); + break; default: break; } diff --git a/addon/chrome/content/icons/outline-20.svg b/addon/chrome/content/icons/outline-20.svg new file mode 100644 index 0000000..99e7236 --- /dev/null +++ b/addon/chrome/content/icons/outline-20.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/addon/chrome/content/icons/save-20.svg b/addon/chrome/content/icons/save-20.svg new file mode 100644 index 0000000..69edd92 --- /dev/null +++ b/addon/chrome/content/icons/save-20.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/addon/chrome/content/mindMap.html b/addon/chrome/content/mindMap.html index 3a65026..833de88 100644 --- a/addon/chrome/content/mindMap.html +++ b/addon/chrome/content/mindMap.html @@ -146,7 +146,7 @@ }); // read in the predefined graph using the JSON format data held in the "mySavedModel" textarea - window.parent.postMessage({ type: "ready" }, "*"); + window.postMessage({ type: "ready" }, "*"); getData(); } @@ -240,7 +240,7 @@ } function getData() { - window.parent.postMessage({ type: "getMindMapData" }, "*"); + window.postMessage({ type: "getMindMapData" }, "*"); } function setData(nodes) { @@ -281,17 +281,16 @@ var oldnode = adorn.adornedPart; var olddata = oldnode.data; if (olddata.noteLink) { - window.parent.postMessage( + window.postMessage( { type: "openNote", link: olddata.noteLink, id: olddata.key }, "*", ); } else { - window.parent.postMessage( + window.postMessage( { type: "jumpNode", lineIndex: olddata.lineIndex, id: olddata.key, - workspaceType: window.workspaceType || "tab", }, "*", ); @@ -305,12 +304,11 @@ alert("Link cannot be edited in mind map"); return false; } - window.parent.postMessage( + window.postMessage( { type: "editNode", lineIndex: data.lineIndex, text: data.text, - workspaceType: window.workspaceType || "tab", }, "*", ); @@ -321,30 +319,31 @@ switch (e.data.type) { case "setMindMapData": setData(e.data.nodes); - window.workspaceType = e.data.workspaceType; break; case "saveImage": const imgString = Diagram.makeImageData({ scale: 1, }); - window.parent.postMessage( + window.postMessage( { type: "saveImageReturn", image: imgString, }, "*", ); + break; case "saveSVG": const svgElement = Diagram.makeSvg({ scale: 1, }); - window.parent.postMessage( + window.postMessage( { type: "saveSVGReturn", image: svgElement.outerHTML, }, "*", ); + break; default: break; } @@ -354,7 +353,7 @@ try { init(); } catch (e) { - window.parent.postMessage({ type: "error", event: e }, "*"); + window.postMessage({ type: "error", event: e }, "*"); } }); diff --git a/addon/chrome/content/styles/context.css b/addon/chrome/content/styles/context.css new file mode 100644 index 0000000..253e369 --- /dev/null +++ b/addon/chrome/content/styles/context.css @@ -0,0 +1,3 @@ +bn-context { + min-width: 182px; +} \ No newline at end of file diff --git a/addon/chrome/content/styles/details.css b/addon/chrome/content/styles/details.css new file mode 100644 index 0000000..25f9fdb --- /dev/null +++ b/addon/chrome/content/styles/details.css @@ -0,0 +1,3 @@ +bn-details pane-header { + display: none; +} \ No newline at end of file diff --git a/addon/chrome/content/styles/editor.css b/addon/chrome/content/styles/editor.css new file mode 100644 index 0000000..797a4a0 --- /dev/null +++ b/addon/chrome/content/styles/editor.css @@ -0,0 +1,49 @@ +.primary-editor > h1::before { + margin-left: -64px !important; + padding-left: 40px !important; + content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3E%E6%9C%AA%E6%A0%87%E9%A2%98-1%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12.29%2C16.8H11.14V12.33H6.07V16.8H4.92V7H6.07v4.3h5.07V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M18.05%2C16.8H16.93V8.41a4%2C4%2C0%2C0%2C1-.9.53%2C6.52%2C6.52%2C0%2C0%2C1-1.14.44l-.32-1a8.2%2C8.2%2C0%2C0%2C0%2C1.67-.67%2C6.31%2C6.31%2C0%2C0%2C0%2C1.39-1h.42Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; +} +.primary-editor > h2::before { + margin-left: -64px !important; + padding-left: 40px !important; + content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22a%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M14.14%2C16.8v-.48a4.1%2C4.1%2C0%2C0%2C1%2C.14-1.11%2C2.86%2C2.86%2C0%2C0%2C1%2C.45-.91%2C5.49%2C5.49%2C0%2C0%2C1%2C.83-.86c.33-.29.75-.61%2C1.24-1a7.43%2C7.43%2C0%2C0%2C0%2C.9-.73%2C3.9%2C3.9%2C0%2C0%2C0%2C.57-.7%2C2.22%2C2.22%2C0%2C0%2C0%2C.3-.66%2C2.87%2C2.87%2C0%2C0%2C0%2C.11-.77%2C1.89%2C1.89%2C0%2C0%2C0-.47-1.32%2C1.66%2C1.66%2C0%2C0%2C0-1.28-.5A3.17%2C3.17%2C0%2C0%2C0%2C15.7%2C8a3.49%2C3.49%2C0%2C0%2C0-1.08.76l-.68-.65a4.26%2C4.26%2C0%2C0%2C1%2C1.39-1A4%2C4%2C0%2C0%2C1%2C17%2C6.84a2.62%2C2.62%2C0%2C0%2C1%2C2.83%2C2.67%2C3.58%2C3.58%2C0%2C0%2C1-.15%2C1%2C3.09%2C3.09%2C0%2C0%2C1-.41.9%2C5.53%2C5.53%2C0%2C0%2C1-.67.81%2C9%2C9%2C0%2C0%2C1-.95.79c-.46.32-.84.59-1.13.82a4.68%2C4.68%2C0%2C0%2C0-.71.64%2C2%2C2%2C0%2C0%2C0-.38.6%2C2.08%2C2.08%2C0%2C0%2C0-.11.69h4.88v1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; +} +.primary-editor > h3::before { + margin-left: -64px !important; + padding-left: 40px !important; + content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16.14l.51-.8a4.75%2C4.75%2C0%2C0%2C0%2C1.1.52%2C4.27%2C4.27%2C0%2C0%2C0%2C1.12.16%2C2.29%2C2.29%2C0%2C0%2C0%2C1.64-.52A1.77%2C1.77%2C0%2C0%2C0%2C19%2C14.17a1.7%2C1.7%2C0%2C0%2C0-.68-1.48%2C3.6%2C3.6%2C0%2C0%2C0-2.06-.48H15.4v-1h.77A3%2C3%2C0%2C0%2C0%2C18%2C10.81a1.65%2C1.65%2C0%2C0%2C0%2C.6-1.41%2C1.47%2C1.47%2C0%2C0%2C0-.47-1.19A1.67%2C1.67%2C0%2C0%2C0%2C17%2C7.79a3.33%2C3.33%2C0%2C0%2C0-2.08.73l-.59-.75a4.4%2C4.4%2C0%2C0%2C1%2C1.28-.71A4.35%2C4.35%2C0%2C0%2C1%2C17%2C6.84a2.84%2C2.84%2C0%2C0%2C1%2C2%2C.65%2C2.21%2C2.21%2C0%2C0%2C1%2C.74%2C1.78%2C2.35%2C2.35%2C0%2C0%2C1-.49%2C1.5%2C2.7%2C2.7%2C0%2C0%2C1-1.46.89v0a2.74%2C2.74%2C0%2C0%2C1%2C1.65.74%2C2.15%2C2.15%2C0%2C0%2C1%2C.66%2C1.65%2C2.64%2C2.64%2C0%2C0%2C1-.9%2C2.12%2C3.44%2C3.44%2C0%2C0%2C1-2.34.78%2C5.3%2C5.3%2C0%2C0%2C1-1.48-.2A5%2C5%2C0%2C0%2C1%2C14%2C16.14Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; +} +.primary-editor > h4::before { + margin-left: -64px !important; + padding-left: 40px !important; + content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M19.43%2C6.92v6.59h1.05v1.05H19.43V16.9H18.31V14.56H13.66v-1c.43-.49.87-1%2C1.31-1.57s.87-1.13%2C1.27-1.7S17%2C9.14%2C17.36%2C8.57a16.51%2C16.51%2C0%2C0%2C0%2C.86-1.65Zm-4.49%2C6.59h3.37V8.63c-.34.61-.67%2C1.15-1%2C1.63s-.6.91-.87%2C1.3-.56.74-.81%2C1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; +} +.primary-editor > h5::before { + margin-left: -64px !important; + padding-left: 40px !important; + content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16l.58-.76a3.67%2C3.67%2C0%2C0%2C0%2C1%2C.58A3.44%2C3.44%2C0%2C0%2C0%2C16.8%2C16a2.17%2C2.17%2C0%2C0%2C0%2C1.58-.6A2%2C2%2C0%2C0%2C0%2C19%2C13.88a1.85%2C1.85%2C0%2C0%2C0-.64-1.5%2C2.83%2C2.83%2C0%2C0%2C0-1.86-.54c-.27%2C0-.55%2C0-.86%2C0s-.58%2C0-.81.06L15.17%2C7H19.7V8H16.14l-.2%2C2.88.47%2C0h.43a3.5%2C3.5%2C0%2C0%2C1%2C2.43.79%2C2.74%2C2.74%2C0%2C0%2C1%2C.88%2C2.16%2C3%2C3%2C0%2C0%2C1-.94%2C2.3%2C3.41%2C3.41%2C0%2C0%2C1-2.4.87%2C4.45%2C4.45%2C0%2C0%2C1-1.5-.24A4.81%2C4.81%2C0%2C0%2C1%2C14%2C16Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; +} +.primary-editor > h6::before { + margin-left: -64px !important; + padding-left: 40px !important; + content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M20.18%2C13.7a3.24%2C3.24%2C0%2C0%2C1-.88%2C2.38%2C2.94%2C2.94%2C0%2C0%2C1-2.2.9%2C2.69%2C2.69%2C0%2C0%2C1-2.31-1.17A5.59%2C5.59%2C0%2C0%2C1%2C14%2C12.49a12.18%2C12.18%2C0%2C0%2C1%2C.2-2.14%2C5.16%2C5.16%2C0%2C0%2C1%2C.84-2A3.65%2C3.65%2C0%2C0%2C1%2C16.27%2C7.2%2C3.71%2C3.71%2C0%2C0%2C1%2C18%2C6.84%2C3.14%2C3.14%2C0%2C0%2C1%2C19%2C7a3.59%2C3.59%2C0%2C0%2C1%2C1%2C.5l-.56.77a2.3%2C2.3%2C0%2C0%2C0-1.49-.48A2.3%2C2.3%2C0%2C0%2C0%2C16.79%2C8a3%2C3%2C0%2C0%2C0-.92.85%2C3.79%2C3.79%2C0%2C0%2C0-.56%2C1.25%2C6.56%2C6.56%2C0%2C0%2C0-.19%2C1.65h0a2.61%2C2.61%2C0%2C0%2C1%2C1-.84%2C2.91%2C2.91%2C0%2C0%2C1%2C1.23-.28%2C2.63%2C2.63%2C0%2C0%2C1%2C2%2C.85A3.09%2C3.09%2C0%2C0%2C1%2C20.18%2C13.7ZM19%2C13.78a2.28%2C2.28%2C0%2C0%2C0-.5-1.62%2C1.67%2C1.67%2C0%2C0%2C0-1.29-.54%2C2%2C2%2C0%2C0%2C0-1.5.58%2C2%2C2%2C0%2C0%2C0-.56%2C1.4%2C2.65%2C2.65%2C0%2C0%2C0%2C.55%2C1.74%2C1.85%2C1.85%2C0%2C0%2C0%2C2.78.1A2.38%2C2.38%2C0%2C0%2C0%2C19%2C13.78Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; +} +.primary-editor > p, +.primary-editor h1, +.primary-editor h2, +.primary-editor h3, +.primary-editor h4, +.primary-editor h5, +.primary-editor h6, +.primary-editor pre, +.primary-editor blockquote, +.primary-editor table, +.primary-editor ul, +.primary-editor ol, +.primary-editor hr { + max-width: unset; +} + +body[no-bn-toolbar] #BetterNotes-toolbar { + display: none; +} diff --git a/addon/chrome/content/styles/outline.css b/addon/chrome/content/styles/outline.css new file mode 100644 index 0000000..4598945 --- /dev/null +++ b/addon/chrome/content/styles/outline.css @@ -0,0 +1,39 @@ +bn-outline, +.container { + width: 100%; + height: 100%; + background: var(--material-sidepane); +} + +bn-outline { + min-width: 100px; + flex-direction: column; +} + +@media (-moz-platform: macos) { + #__addonRef__-left-toolbar { + -moz-window-dragging: drag; + } +} + +#__addonRef__-left-toolbar { + background: var(--material-toolbar); + border-bottom: var(--material-panedivider); + padding: 6px 8px; +} + +bn-outline .zotero-tb-button { + width: 28px; + height: 28px; + margin: 0px 4px 0px 4px; + fill: currentColor; + -moz-context-properties: fill, fill-opacity; +} + +#__addonRef__-setOutline { + list-style-image: url("chrome://__addonRef__/content/icons/outline-20.svg"); +} + +#__addonRef__-saveOutline { + list-style-image: url("chrome://__addonRef__/content/icons/save-20.svg"); +} diff --git a/addon/chrome/content/styles/workspace.css b/addon/chrome/content/styles/workspace.css new file mode 100644 index 0000000..5ad9b5a --- /dev/null +++ b/addon/chrome/content/styles/workspace.css @@ -0,0 +1,11 @@ +bn-workspace, +.container { + width: 100%; + height: 100%; + background: var(--material-sidepane); + flex-grow: 1; +} + +bn-workspace #__addonRef__-editor-main #links-container { + display: none; +} diff --git a/addon/chrome/content/treeView.html b/addon/chrome/content/treeView.html index 3476800..e9b1cbe 100644 --- a/addon/chrome/content/treeView.html +++ b/addon/chrome/content/treeView.html @@ -43,7 +43,7 @@ } #treeview { - padding: 20px; + padding: 8px; } .drive-header { @@ -72,13 +72,18 @@ color: var(--fill-primary); } + .dx-treeview-item { + border-radius: 5px; + cursor: auto; + } + .dx-treeview-toggle-item-visibility { color: var(--fill-primary); } :not(.dx-state-focused) > .dx-treeview-item.dx-state-hover { color: var(--fill-primary); - background-color: var(--fill-quinary); + background-color: transparent; } .dx-state-focused > .dx-treeview-item { @@ -94,7 +99,7 @@ var cachedNodes = null; function init() { window.addEventListener("message", handler, false); - window.parent.postMessage({ type: "ready" }, "*"); + window.postMessage({ type: "ready" }, "*"); getData(); } @@ -151,7 +156,7 @@ return; } - window.parent.postMessage( + window.postMessage( { type: "moveNode", fromID: parseInt(fromNode.itemData.id), @@ -295,7 +300,7 @@ function jumpNode(e) { var itemData = e.itemData; if (itemData.noteLink) { - window.parent.postMessage( + window.postMessage( { type: "openNote", link: itemData.noteLink, @@ -304,12 +309,11 @@ "*", ); } else { - window.parent.postMessage( + window.postMessage( { type: "jumpNode", lineIndex: itemData.lineIndex, id: parseInt(itemData.id), - workspaceType: window.workspaceType || "tab", }, "*", ); @@ -319,7 +323,7 @@ const noteIcon = ``; function getData() { - window.parent.postMessage({ type: "getMindMapData" }, "*"); + window.postMessage({ type: "getMindMapData" }, "*"); } function setData(nodes, expandLevel) { @@ -384,7 +388,6 @@ console.log(e); if (e.data.type === "setMindMapData") { setData(e.data.nodes, e.data.expandLevel); - window.workspaceType = e.data.workspaceType; } } @@ -392,7 +395,7 @@ try { init(); } catch (e) { - window.parent.postMessage({ type: "error", event: e }, "*"); + window.postMessage({ type: "error", event: e }, "*"); } }); diff --git a/addon/locale/en-US/outline.ftl b/addon/locale/en-US/outline.ftl new file mode 100644 index 0000000..aa31812 --- /dev/null +++ b/addon/locale/en-US/outline.ftl @@ -0,0 +1,20 @@ +setOutline = + .tooltiptext = Change outline mode +useTreeView = + .label = Tree View +useMindMap = + .label = Mind Map +useBubbleMap = + .label = Bubble Map +saveOutline = + .tooltiptext = Save as... +saveOutlineImage = + .label = Outline image + .tooltiptext = Only in mind map/bubble map mode +saveOutlineSVG = + .label = Outline SVG + .tooltiptext = Only in mind map/bubble map mode +saveOutlineFreeMind = + .label = Outline FreeMind +saveMore = + .label = MarkDown, Docx, PDF... diff --git a/package.json b/package.json index 66f3ca5..d99b862 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-toolkit": "^2.3.23" + "zotero-plugin-toolkit": "^2.3.26" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", diff --git a/scripts/build-extras.mjs b/scripts/build-extras.mjs index cefdd93..d00cda6 100644 --- a/scripts/build-extras.mjs +++ b/scripts/build-extras.mjs @@ -5,19 +5,9 @@ const buildDir = "build"; export async function main() { await build({ - entryPoints: ["src/extras/editorScript.ts"], + entryPoints: ["./src/extras/*.ts"], + outdir: path.join(buildDir, "addon/chrome/content/scripts"), bundle: true, - outfile: path.join( - buildDir, - "addon/chrome/content/scripts/editorScript.js", - ), - target: ["firefox102"], - }).catch(() => exit(1)); - - await build({ - entryPoints: ["src/extras/docxWorker.ts"], - bundle: true, - outfile: path.join(buildDir, "addon/chrome/content/scripts/docxWorker.js"), target: ["firefox102"], }).catch(() => exit(1)); } diff --git a/scripts/build.mjs b/scripts/build.mjs index 96bd4a1..495ef39 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -49,6 +49,7 @@ function replaceString(buildTime) { `${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.html`, `${buildDir}/addon/**/*.json`, + `${buildDir}/addon/**/*.css`, `${buildDir}/addon/prefs.js`, `${buildDir}/addon/manifest.json`, `${buildDir}/addon/bootstrap.js`, diff --git a/src/addon.ts b/src/addon.ts index c378645..fe9fbbe 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -7,7 +7,6 @@ import { LargePrefHelper } from "zotero-plugin-toolkit/dist/helpers/largePref"; import ToolkitGlobal from "zotero-plugin-toolkit/dist/managers/toolkitGlobal"; import { getPref, setPref } from "./utils/prefs"; -import { OutlineType } from "./utils/workspace"; import { SyncDataType } from "./modules/sync/managerWindow"; import hooks from "./hooks"; import api from "./api"; @@ -46,21 +45,6 @@ class Addon { }; }; notify: Array>; - workspace: { - mainId: number; - previewId: number; - tab: { - active: boolean; - id?: string; - container?: XUL.Box; - }; - window: { - active: boolean; - window?: Window; - container?: XUL.Box; - }; - outline: OutlineType; - }; imageViewer: { window?: Window; srcList: string[]; @@ -105,38 +89,6 @@ class Addon { diff: {}, }, notify: [], - workspace: { - get mainId(): number { - return parseInt(getPref("mainKnowledgeID") as string); - }, - set mainId(id: number) { - setPref("mainKnowledgeID", id); - const recentMainNoteIds = getPref("recentMainNoteIds") as string; - const recentMainNoteIdsArr = recentMainNoteIds - ? recentMainNoteIds.split(",").map((id) => parseInt(id)) - : []; - const idx = recentMainNoteIdsArr.indexOf(id); - if (idx !== -1) { - recentMainNoteIdsArr.splice(idx, 1); - } - recentMainNoteIdsArr.unshift(id); - setPref( - "recentMainNoteIds", - recentMainNoteIdsArr - .slice(0, 10) - .filter((id) => Zotero.Items.get(id).isNote()) - .join(","), - ); - }, - previewId: -1, - tab: { - active: false, - }, - window: { - active: false, - }, - outline: OutlineType.treeView, - }, imageViewer: { window: undefined, srcList: [], diff --git a/src/api.ts b/src/api.ts index 4922069..5bbabaf 100644 --- a/src/api.ts +++ b/src/api.ts @@ -44,7 +44,6 @@ import { DEFAULT_TEMPLATES, } from "./modules/template/data"; import { renderTemplatePreview } from "./modules/template/preview"; -import { getWorkspaceEditor } from "./modules/workspace/content"; import { parseCitationHTML } from "./utils/citation"; import { getEditorInstance, @@ -65,11 +64,10 @@ import { addLineToNote, updateRelatedNotes, getRelatedNoteIds, + getNoteTreeFlattened, } from "./utils/note"; -const workspace = { - getWorkspaceEditor, -}; +const workspace = {}; const sync = { isSyncNote, @@ -148,6 +146,7 @@ const note = { insert: addLineToNote, updateRelatedNotes, getRelatedNoteIds, + getNoteTreeFlattened, }; export default { diff --git a/src/elements/base.ts b/src/elements/base.ts new file mode 100644 index 0000000..76e10b7 --- /dev/null +++ b/src/elements/base.ts @@ -0,0 +1,60 @@ +import { config } from "../../package.json"; + +export class PluginCEBase extends XULElementBase { + _addon!: typeof addon; + + connectedCallback(): void { + this._addon = Zotero[config.addonInstance]; + Zotero.UIProperties.registerRoot(this); + super.connectedCallback(); + } + + _wrapID(key: string) { + if (key.startsWith(config.addonRef)) { + return key; + } + return `${config.addonRef}-${key}`; + } + + _unwrapID(id: string) { + if (id.startsWith(config.addonRef)) { + return id.slice(config.addonRef.length + 1); + } + return id; + } + + _queryID(key: string) { + return this.querySelector(`#${this._wrapID(key)}`) as XUL.Element | null; + } + + _parseContentID(dom: DocumentFragment) { + dom.querySelectorAll("*[id]").forEach(elem => { + elem.id = this._wrapID(elem.id); + }) + return dom; + } + + _loadPersist() { + const persistValues = Zotero.Prefs.get("pane.persist") as string; + if (!persistValues) return; + const serializedValues = JSON.parse(persistValues) as Record< + string, + Record + >; + + for (const id in serializedValues) { + const el = this.querySelector(`#${id}`) as HTMLElement; + if (!el) { + continue; + } + + const elValues = serializedValues[id]; + for (const attr in elValues) { + el.setAttribute(attr, elValues[attr]); + if (["width", "height"].includes(attr)) { + el.style[attr as any] = `${elValues[attr]}px`; + } + } + } + } +} diff --git a/src/elements/context.ts b/src/elements/context.ts new file mode 100644 index 0000000..febba36 --- /dev/null +++ b/src/elements/context.ts @@ -0,0 +1,47 @@ +import { config } from "../../package.json"; +import { PluginCEBase } from "./base"; + +export class ContextPane extends PluginCEBase { + _item?: Zotero.Item; + + _details!: any; + _sidenav: any; + + get item() { + return this._item; + } + + set item(val) { + this._item = val; + } + + get content() { + return this._parseContentID( + MozXULElement.parseXULToFragment(` + + + + + +`), + ); + } + + init(): void { + this._details = this._queryID("container"); + this._sidenav = this._queryID("sidenav"); + } + + render() { + if (!this.item) return; + this._details.mode = this.item.isEditable() ? null : "view"; + this._details.item = this.item; + this._details.parentID = this.item.parentID; + this._details.sidenav = this._sidenav; + this._details.render(); + this._sidenav.toggleDefaultStatus(); + } +} diff --git a/src/elements/detailsPane.ts b/src/elements/detailsPane.ts new file mode 100644 index 0000000..197197b --- /dev/null +++ b/src/elements/detailsPane.ts @@ -0,0 +1,30 @@ +import { config } from "../../package.json"; +const ItemDetails = customElements.get("item-details")! as any; + +export class NoteDetails extends ItemDetails { + content = MozXULElement.parseXULToFragment(` + + + + + + + + + + + + +`); + + forceUpdateSideNav() { + this._sidenav + .querySelectorAll("toolbarbutton") + .forEach((elem: HTMLElement) => (elem.parentElement!.hidden = true)); + super.forceUpdateSideNav(); + } +} diff --git a/src/elements/outlinePane.ts b/src/elements/outlinePane.ts new file mode 100644 index 0000000..81d816e --- /dev/null +++ b/src/elements/outlinePane.ts @@ -0,0 +1,379 @@ +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 { + getEditorInstance, + moveHeading, + updateHeadingTextAtLine, +} 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; + _item?: Zotero.Item; + _editorElement!: EditorElement; + + _outlineContainer!: HTMLIFrameElement; + _notifierID!: string; + + static outlineSources = [ + "", + `chrome://${config.addonRef}/content/treeView.html`, + `chrome://${config.addonRef}/content/mindMap.html`, + `chrome://${config.addonRef}/content/bubbleMap.html`, + ]; + + static outlineMenuIDs = { + "": OutlineType.empty, + useTreeView: OutlineType.treeView, + useMindMap: OutlineType.mindMap, + useBubbleMap: OutlineType.bubbleMap, + }; + + get content() { + return this._parseContentID( + MozXULElement.parseXULToFragment(` + + + + + + + + + + + + + + + + + + + + +`), + ); + } + + get outlineType() { + return this._outlineType; + } + + set outlineType(newType) { + if (newType === OutlineType.empty) { + newType = OutlineType.treeView; + } + if (newType > OutlineType.bubbleMap) { + newType = OutlineType.treeView; + } + + this._outlineType = newType; + } + + get item() { + return this._item; + } + + set item(val) { + this._item = val; + } + + get editor() { + return this._editorElement._editorInstance; + } + + init(): void { + MozXULElement.insertFTLIfNeeded(`${config.addonRef}-outline.ftl`); + + this._outlineContainer = this._queryID("outline") as unknown as HTMLIFrameElement; + + this._queryID("left-toolbar")?.addEventListener( + "command", + this.toolbarButtonCommandHandler, + ); + + this._notifierID = Zotero.Notifier.registerObserver( + this, + ["item"], + "attachmentsBox", + ); + + this._loadPersist(); + } + + destroy(): void { + Zotero.Notifier.unregisterObserver(this._notifierID); + this._outlineContainer.contentWindow?.removeEventListener( + "message", + this.messageHandler, + ); + } + + notify( + event: string, + type: string, + ids: number[] | string[], + extraData: { [key: string]: any }, + ) { + if (!this.item) return; + if (event === "modify" && type === "item") { + if ((ids as number[]).includes(this.item.id)) { + this.updateOutline(); + if (getPref("workspace.autoUpdateRelatedNotes")) { + this._addon.api.note.updateRelatedNotes(this.item.id); + } + } + } + } + + async render() { + if (this.outlineType === OutlineType.empty) { + this.outlineType = OutlineType.treeView; + } + await this.updateOutline(); + } + + async updateOutline() { + if (!this.item) return; + + this._outlineContainer.contentWindow?.removeEventListener( + "message", + this.messageHandler, + ); + + this._outlineContainer.setAttribute("src", OutlinePane.outlineSources[this.outlineType]); + + await waitUtilAsync( + () => this._outlineContainer.contentWindow?.document.readyState === "complete", + ); + this._outlineContainer.contentWindow?.addEventListener( + "message", + this.messageHandler, + ); + this._outlineContainer.contentWindow?.postMessage( + { + type: "setMindMapData", + nodes: this._addon.api.note.getNoteTreeFlattened(this.item, { + keepLink: !!getPref("workspace.outline.keepLinks"), + }), + expandLevel: getPref("workspace.outline.expandLevel"), + }, + "*", + ); + + // Update button hidden + const isTreeView = this.outlineType === OutlineType.treeView; + for (const key of ["saveImage", "saveSVG"]) { + const elem = this._queryID(key); + if (isTreeView) { + elem?.setAttribute("disabled", "true"); + } else { + elem?.removeAttribute("disabled"); + } + } + + // Update set outline menu + this._queryID("setOutlinePopup")?.childNodes.forEach((elem) => + (elem as XUL.MenuItem).removeAttribute("checked"), + ); + this._queryID( + Object.keys(OutlinePane.outlineMenuIDs)[this.outlineType], + )?.setAttribute("checked", "true"); + } + + saveImage(type: "saveSVG" | "saveImage") { + this._outlineContainer.contentWindow?.postMessage( + { + type, + }, + "*", + ); + } + + async saveFreeMind() { + if (!this.item?.id) return; + // TODO: uncouple this part + const filename = await new FilePickerHelper( + `${Zotero.getString("fileInterface.export")} FreeMind XML`, + "save", + [["FreeMind XML File(*.mm)", "*.mm"]], + `${this.item.getNoteTitle()}.mm`, + ).open(); + if (filename) { + await addon.api.$export.saveFreeMind(filename, this.item.id); + } + } + + toolbarButtonCommandHandler = async (ev: Event) => { + if (!this.item) return; + const type = this._unwrapID((ev.target as XUL.ToolBarButton).id); + switch (type) { + case "useTreeView": + case "useMindMap": + case "useBubbleMap": { + this.outlineType = OutlinePane.outlineMenuIDs[type]; + await this.updateOutline(); + break; + } + case "saveImage": + case "saveSVG": { + this.saveImage(type); + break; + } + case "saveFreeMind": { + this.saveFreeMind(); + break; + } + case "saveMore": { + this._addon.hooks.onShowExportNoteOptions([this.item.id]); + break; + } + default: { + break; + } + } + }; + + messageHandler = async (ev: MessageEvent) => { + switch (ev.data.type) { + case "jumpNode": { + if (!this.editor) { + return; + } + this._addon.api.editor.scroll(this.editor, ev.data.lineIndex); + return; + } + case "openNote": { + const linkParams = getNoteLinkParams(ev.data.link); + if (!linkParams.noteItem) { + return; + } + this._addon.hooks.onOpenNote(linkParams.noteItem.id, "preview", { + lineIndex: linkParams.lineIndex || undefined, + }); + return; + } + case "moveNode": { + if (!this.item) return; + const tree = getNoteTree(this.item); + const fromNode = getNoteTreeNodeById(this.item, ev.data.fromID, tree); + const toNode = getNoteTreeNodeById(this.item, ev.data.toID, tree); + moveHeading( + getEditorInstance(this.item.id), + fromNode!, + toNode!, + ev.data.moveType, + ); + return; + } + case "editNode": { + if (!this.editor) { + return; + } + updateHeadingTextAtLine( + this.editor, + ev.data.lineIndex, + ev.data.text.replace(/[\r\n]/g, ""), + ); + return; + } + case "saveSVGReturn": { + const filename = await new FilePickerHelper( + `${Zotero.getString("fileInterface.export")} SVG Image`, + "save", + [["SVG File(*.svg)", "*.svg"]], + `${this.item?.getNoteTitle()}.svg`, + ).open(); + if (filename) { + await Zotero.File.putContentsAsync( + formatPath(filename), + ev.data.image, + ); + showHintWithLink( + `Image Saved to ${filename}`, + "Show in Folder", + (ev) => { + Zotero.File.reveal(filename); + }, + ); + } + return; + } + case "saveImageReturn": { + const filename = await new FilePickerHelper( + `${Zotero.getString("fileInterface.export")} PNG Image`, + "save", + [["PNG File(*.png)", "*.png"]], + `${this.item?.getNoteTitle()}.png`, + ).open(); + if (filename) { + const parts = ev.data.image.split(","); + const bstr = atob(parts[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + await IOUtils.write(formatPath(filename), u8arr); + showHintWithLink( + `Image Saved to ${filename}`, + "Show in Folder", + (ev) => { + Zotero.File.reveal(filename); + }, + ); + } + return; + } + default: + return; + } + }; +} diff --git a/src/elements/workspace.ts b/src/elements/workspace.ts new file mode 100644 index 0000000..235b02e --- /dev/null +++ b/src/elements/workspace.ts @@ -0,0 +1,116 @@ +import { config } from "../../package.json"; +import { waitUtilAsync } from "../utils/wait"; +import { PluginCEBase } from "./base"; +import { ContextPane } from "./context"; +import { OutlinePane } from "./outlinePane"; + +export class Workspace extends PluginCEBase { + _item?: Zotero.Item; + + _editorElement!: EditorElement; + _outline!: OutlinePane; + _context!: ContextPane; + + get content() { + return this._parseContentID( + MozXULElement.parseXULToFragment(` + + + + + + + + + + + + + +`), + ); + } + + get containerType() { + return this.getAttribute("container-type") || ""; + } + + set containerType(val: string) { + this.setAttribute("container-type", val); + } + + get item() { + return this._item; + } + + set item(val) { + this._item = val; + this._outline.item = val; + this._context.item = val; + } + + get editor() { + return this._editorElement._editorInstance; + } + + init(): void { + // MozXULElement.insertFTLIfNeeded(`${config.addonRef}-workspace.ftl`); + + this._outline = this._queryID("left-container") as unknown as OutlinePane; + this._editorElement = this._queryID("editor-main") as EditorElement; + this._outline._editorElement = this._editorElement; + + this._context = this._queryID("right-container") as unknown as ContextPane; + + this._loadPersist(); + } + + destroy(): void {} + + async render() { + await this._outline.render(); + await this.updateEditor(); + await this._context.render(); + } + + async updateEditor() { + const editorElem = this._queryID("editor-main") as EditorElement; + await waitUtilAsync(() => Boolean(editorElem._initialized)); + if (!editorElem._initialized) { + throw new Error("initNoteEditor: waiting initialization failed"); + } + editorElem.mode = "edit"; + editorElem.viewMode = "library"; + editorElem.parent = this.item?.parentItem; + editorElem.item = this.item; + await waitUtilAsync(() => Boolean(editorElem._editorInstance)); + await editorElem._editorInstance._initPromise; + // Hide BN toolbar + editorElem._editorInstance._iframeWindow.document.body.setAttribute( + "no-bn-toolbar", + "true", + ); + // TODO: implement jump to + // if (typeof options.lineIndex === "number") { + // addon.api.editor.scroll(editorElem._editorInstance, options.lineIndex); + // } + // if (typeof options.sectionName === "string") { + // addon.api.editor.scrollToSection( + // editorElem._editorInstance, + // options.sectionName, + // ); + // } + return; + } +} diff --git a/src/extras/workspace.ts b/src/extras/workspace.ts new file mode 100644 index 0000000..2b134de --- /dev/null +++ b/src/extras/workspace.ts @@ -0,0 +1,17 @@ +import { ContextPane } from "../elements/context"; +import { NoteDetails } from "../elements/detailsPane"; +import { OutlinePane } from "../elements/outlinePane"; +import { Workspace } from "../elements/workspace"; + +const elements = { + "bn-context": ContextPane, + "bn-outline": OutlinePane, + "bn-details": NoteDetails, + "bn-workspace": Workspace, +}; + +for (const [key, constructor] of Object.entries(elements)) { + if (!customElements.get(key)) { + customElements.define(key, constructor); + } +} diff --git a/src/hooks.ts b/src/hooks.ts index a687c68..88fdcb7 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -11,15 +11,9 @@ import { registerMenus } from "./modules/menu"; import { registerWorkspaceTab, openWorkspaceTab, + onTabSelect, } from "./modules/workspace/tab"; -import { - initWorkspace, - initWorkspaceEditor, - toggleNotesPane, - toggleOutlinePane, - togglePreviewPane, - updateOutline, -} from "./modules/workspace/content"; +import { initWorkspace } from "./modules/workspace/content"; import { registerNotify } from "./modules/notify"; import { openWorkspaceWindow } from "./modules/workspace/window"; import { registerReaderAnnotationButton } from "./modules/reader"; @@ -34,16 +28,12 @@ import { showSyncDiff } from "./modules/sync/diffWindow"; import { showSyncInfo } from "./modules/sync/infoWindow"; import { showSyncManager } from "./modules/sync/managerWindow"; import { showTemplateEditor } from "./modules/template/editorWindow"; -import { - createNoteFromTemplate, - createWorkspaceNote, - createNoteFromMD, -} from "./modules/createNote"; -import { annotationTagAction } from "./modules/annotationTagAction"; +import { createNoteFromTemplate, createNoteFromMD } from "./modules/createNote"; import { createZToolkit } from "./utils/ztoolkit"; import { waitUtilAsync } from "./utils/wait"; import { initSyncList } from "./modules/sync/api"; import { getPref } from "./utils/prefs"; +import { patchViewItems } from "./modules/viewItems"; async function onStartup() { await Promise.all([ @@ -74,6 +64,11 @@ async function onStartup() { async function onMainWindowLoad(win: Window): Promise { await waitUtilAsync(() => document.readyState === "complete"); + + Services.scriptloader.loadSubScript( + `chrome://${config.addonRef}/content/scripts/workspace.js`, + win, + ); // Create ztoolkit for every window addon.data.ztoolkit = createZToolkit(); @@ -84,6 +79,8 @@ async function onMainWindowLoad(win: Window): Promise { registerWorkspaceTab(win); initTemplates(); + + patchViewItems(win); } async function onMainWindowUnload(win: Window): Promise { @@ -102,25 +99,16 @@ function onShutdown(): void { * Any operations should be placed in a function to keep this funcion clear. */ function onNotify( - event: string, - type: string, - ids: number[] | string[], - extraData: { [key: string]: any }, + event: Parameters<_ZoteroTypes.Notifier.Notify>["0"], + type: Parameters<_ZoteroTypes.Notifier.Notify>["1"], + ids: Parameters<_ZoteroTypes.Notifier.Notify>["2"], + extraData: Parameters<_ZoteroTypes.Notifier.Notify>["3"], ) { if (extraData.skipBN) { return; } - // Workspace main note update - if (event === "modify" && type === "item") { - if ((ids as number[]).includes(addon.data.workspace.mainId)) { - addon.data.workspace.tab.active && - updateOutline(addon.data.workspace.tab.container!); - addon.data.workspace.window.active && - updateOutline(addon.data.workspace.window.container!); - if (getPref("workspace.autoUpdateRelatedNotes")) { - addon.api.note.updateRelatedNotes(addon.data.workspace.mainId); - } - } + if (event === "select" && type === "tab") { + onTabSelect(extraData[ids[0]].type); } if (event === "modify" && type === "item") { const modifiedNotes = Zotero.Items.get(ids).filter((item) => item.isNote()); @@ -131,10 +119,6 @@ function onNotify( reason: "item-modify", }); } - } - // Insert annotation when assigning tag starts with @ - if (event === "add" && type === "item-tag") { - annotationTagAction(ids as number[], extraData); } else { return; } @@ -170,23 +154,14 @@ function onOpenNote( return; } if (mode === "auto") { - if (noteId === addon.data.workspace.mainId) { - mode = "workspace"; - } else if ( - addon.data.workspace.tab.active || - addon.data.workspace.window.active - ) { - mode = "preview"; - } else { - mode = "standalone"; - } + mode = "workspace"; } switch (mode) { case "preview": - addon.hooks.onSetWorkspaceNote(noteId, "preview", options); + // addon.hooks.onSetWorkspaceNote(noteId, "preview", options); break; case "workspace": - addon.hooks.onSetWorkspaceNote(noteId, "main", options); + // addon.hooks.onSetWorkspaceNote(noteId, "main", options); break; case "standalone": ZoteroPane.openNoteWindow(noteId); @@ -196,60 +171,13 @@ function onOpenNote( } } -function onSetWorkspaceNote( - noteId: number, - type: "main" | "preview" = "main", - options: { - lineIndex?: number; - sectionName?: string; - } = {}, -) { - if (type === "main") { - addon.data.workspace.mainId = noteId; - addon.data.workspace.tab.active && - updateOutline(addon.data.workspace.tab.container!); - addon.data.workspace.window.active && - updateOutline(addon.data.workspace.window.container!); - } - if (addon.data.workspace.window.active) { - initWorkspaceEditor( - addon.data.workspace.window.container!, - type, - noteId, - options, - ); - type === "preview" && - addon.hooks.onToggleWorkspacePane( - "preview", - true, - addon.data.workspace.window.container, - ); - addon.data.workspace.window.window?.focus(); - } - if (addon.data.workspace.tab.active) { - initWorkspaceEditor( - addon.data.workspace.tab.container!, - type, - noteId, - options, - ); - type === "preview" && - addon.hooks.onToggleWorkspacePane( - "preview", - true, - addon.data.workspace.tab.container, - ); - Zotero_Tabs.select(addon.data.workspace.tab.id!); - } -} - -function onOpenWorkspace(type: "tab" | "window" = "tab") { +function onOpenWorkspace(item: Zotero.Item, type: "tab" | "window" = "tab") { if (type === "window") { - openWorkspaceWindow(); + openWorkspaceWindow(item); return; } if (type === "tab") { - openWorkspaceTab(); + openWorkspaceTab(item); return; } } @@ -261,19 +189,19 @@ function onToggleWorkspacePane( visibility?: boolean, container?: XUL.Box, ) { - switch (type) { - case "outline": - toggleOutlinePane(visibility, container); - break; - case "preview": - togglePreviewPane(visibility, container); - break; - case "notes": - toggleNotesPane(visibility); - break; - default: - break; - } + // switch (type) { + // case "outline": + // toggleOutlinePane(visibility, container); + // break; + // case "preview": + // togglePreviewPane(visibility, container); + // break; + // case "notes": + // toggleNotesPane(visibility); + // break; + // default: + // break; + // } } const onSyncing = callSyncing; @@ -296,8 +224,6 @@ const onShowSyncDiff = showSyncDiff; const onShowTemplateEditor = showTemplateEditor; -const onCreateWorkspaceNote = createWorkspaceNote; - const onCreateNoteFromTemplate = createNoteFromTemplate; const onCreateNoteFromMD = createNoteFromMD; @@ -315,7 +241,6 @@ export default { onPrefsEvent, onOpenNote, onInitWorkspace, - onSetWorkspaceNote, onOpenWorkspace, onToggleWorkspacePane, onSyncing, @@ -328,7 +253,6 @@ export default { onShowSyncInfo, onShowSyncManager, onShowTemplateEditor, - onCreateWorkspaceNote, onCreateNoteFromTemplate, onCreateNoteFromMD, }; diff --git a/src/modules/annotationTagAction.ts b/src/modules/annotationTagAction.ts deleted file mode 100644 index 0293ba3..0000000 --- a/src/modules/annotationTagAction.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { addLineToNote, getNoteTreeFlattened } from "../utils/note"; - -export { annotationTagAction }; - -async function annotationTagAction( - ids: Array, - extraData: Record, -) { - const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId); - if (!workspaceNote || !workspaceNote.isNote()) { - return; - } - const nodes = getNoteTreeFlattened(workspaceNote); - const headings: string[] = nodes.map((node) => node.model.name); - - for (const tagId of ids.filter((t) => - (extraData[t].tag as string).startsWith("@"), - )) { - const tagName = (extraData[tagId].tag as string).slice(1).trim(); - if (headings.includes(tagName) || tagName === "@") { - let lineIndex: number; - if (tagName === "@") { - lineIndex = -1; - } else { - const targetNode = nodes.find((node) => node.model.name === tagName); - lineIndex = targetNode?.model.endIndex; - } - - const annotationItem = Zotero.Items.get((tagId as string).split("-")[0]); - if (!annotationItem.isAnnotation()) { - continue; - } - await addLineToNote( - workspaceNote, - await addon.api.convert.annotations2html([annotationItem], { - noteItem: workspaceNote, - }), - lineIndex, - ); - } - } -} diff --git a/src/modules/createNote.ts b/src/modules/createNote.ts index 702db0c..5740bbe 100644 --- a/src/modules/createNote.ts +++ b/src/modules/createNote.ts @@ -1,45 +1,7 @@ import { getString } from "../utils/locale"; -import { config } from "../../package.json"; import { formatPath } from "../utils/str"; -export { createWorkspaceNote, createNoteFromTemplate, createNoteFromMD }; - -async function createWorkspaceNote() { - const currentCollection = ZoteroPane.getSelectedCollection(); - if (!currentCollection) { - window.alert(getString("alert.notValidCollectionError")); - return; - } - const confirmOperation = window.confirm( - `${getString( - "menuAddNote.newMainNote.confirmHead", - // @ts-ignore - )} '${currentCollection.getName()}' ${getString( - "menuAddNote.newMainNote.confirmTail", - )}`, - ); - if (!confirmOperation) { - return; - } - const header = window.prompt( - getString("menuAddNote.newMainNote.enterNoteTitle"), - `New Note ${new Date().toLocaleString()}`, - ); - const noteID = await ZoteroPane.newNote(); - const noteItem = Zotero.Items.get(noteID); - noteItem.setNote( - `

${header}

\n
`, - ); - await noteItem.saveTx(); - addon.hooks.onSetWorkspaceNote(noteID, "main"); - if ( - !addon.data.workspace.tab.active && - !addon.data.workspace.window.active && - window.confirm(getString("menuAddNote.newMainNote.openWorkspaceTab")) - ) { - addon.hooks.onOpenWorkspace("tab"); - } -} +export { createNoteFromTemplate, createNoteFromMD }; function getLibraryParentId() { return ZoteroPane.getSelectedItems().filter((item) => item.isRegularItem())[0] diff --git a/src/modules/editor/inject.ts b/src/modules/editor/inject.ts index eb4afea..379e061 100644 --- a/src/modules/editor/inject.ts +++ b/src/modules/editor/inject.ts @@ -16,47 +16,13 @@ export async function injectEditorScripts(win: Window) { ); } -export function injectEditorCSS(win: Window) { +export async function injectEditorCSS(win: Window) { ztoolkit.UI.appendElement( { tag: "style", id: "betternotes-style", properties: { - innerHTML: ` - .primary-editor > h1::before { - margin-left: -64px !important; - padding-left: 40px !important; - content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3E%E6%9C%AA%E6%A0%87%E9%A2%98-1%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12.29%2C16.8H11.14V12.33H6.07V16.8H4.92V7H6.07v4.3h5.07V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M18.05%2C16.8H16.93V8.41a4%2C4%2C0%2C0%2C1-.9.53%2C6.52%2C6.52%2C0%2C0%2C1-1.14.44l-.32-1a8.2%2C8.2%2C0%2C0%2C0%2C1.67-.67%2C6.31%2C6.31%2C0%2C0%2C0%2C1.39-1h.42Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; - } - .primary-editor > h2::before { - margin-left: -64px !important; - padding-left: 40px !important; - content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22a%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M14.14%2C16.8v-.48a4.1%2C4.1%2C0%2C0%2C1%2C.14-1.11%2C2.86%2C2.86%2C0%2C0%2C1%2C.45-.91%2C5.49%2C5.49%2C0%2C0%2C1%2C.83-.86c.33-.29.75-.61%2C1.24-1a7.43%2C7.43%2C0%2C0%2C0%2C.9-.73%2C3.9%2C3.9%2C0%2C0%2C0%2C.57-.7%2C2.22%2C2.22%2C0%2C0%2C0%2C.3-.66%2C2.87%2C2.87%2C0%2C0%2C0%2C.11-.77%2C1.89%2C1.89%2C0%2C0%2C0-.47-1.32%2C1.66%2C1.66%2C0%2C0%2C0-1.28-.5A3.17%2C3.17%2C0%2C0%2C0%2C15.7%2C8a3.49%2C3.49%2C0%2C0%2C0-1.08.76l-.68-.65a4.26%2C4.26%2C0%2C0%2C1%2C1.39-1A4%2C4%2C0%2C0%2C1%2C17%2C6.84a2.62%2C2.62%2C0%2C0%2C1%2C2.83%2C2.67%2C3.58%2C3.58%2C0%2C0%2C1-.15%2C1%2C3.09%2C3.09%2C0%2C0%2C1-.41.9%2C5.53%2C5.53%2C0%2C0%2C1-.67.81%2C9%2C9%2C0%2C0%2C1-.95.79c-.46.32-.84.59-1.13.82a4.68%2C4.68%2C0%2C0%2C0-.71.64%2C2%2C2%2C0%2C0%2C0-.38.6%2C2.08%2C2.08%2C0%2C0%2C0-.11.69h4.88v1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; - } - .primary-editor > h3::before { - margin-left: -64px !important; - padding-left: 40px !important; - content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16.14l.51-.8a4.75%2C4.75%2C0%2C0%2C0%2C1.1.52%2C4.27%2C4.27%2C0%2C0%2C0%2C1.12.16%2C2.29%2C2.29%2C0%2C0%2C0%2C1.64-.52A1.77%2C1.77%2C0%2C0%2C0%2C19%2C14.17a1.7%2C1.7%2C0%2C0%2C0-.68-1.48%2C3.6%2C3.6%2C0%2C0%2C0-2.06-.48H15.4v-1h.77A3%2C3%2C0%2C0%2C0%2C18%2C10.81a1.65%2C1.65%2C0%2C0%2C0%2C.6-1.41%2C1.47%2C1.47%2C0%2C0%2C0-.47-1.19A1.67%2C1.67%2C0%2C0%2C0%2C17%2C7.79a3.33%2C3.33%2C0%2C0%2C0-2.08.73l-.59-.75a4.4%2C4.4%2C0%2C0%2C1%2C1.28-.71A4.35%2C4.35%2C0%2C0%2C1%2C17%2C6.84a2.84%2C2.84%2C0%2C0%2C1%2C2%2C.65%2C2.21%2C2.21%2C0%2C0%2C1%2C.74%2C1.78%2C2.35%2C2.35%2C0%2C0%2C1-.49%2C1.5%2C2.7%2C2.7%2C0%2C0%2C1-1.46.89v0a2.74%2C2.74%2C0%2C0%2C1%2C1.65.74%2C2.15%2C2.15%2C0%2C0%2C1%2C.66%2C1.65%2C2.64%2C2.64%2C0%2C0%2C1-.9%2C2.12%2C3.44%2C3.44%2C0%2C0%2C1-2.34.78%2C5.3%2C5.3%2C0%2C0%2C1-1.48-.2A5%2C5%2C0%2C0%2C1%2C14%2C16.14Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; - } - .primary-editor > h4::before { - margin-left: -64px !important; - padding-left: 40px !important; - content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M19.43%2C6.92v6.59h1.05v1.05H19.43V16.9H18.31V14.56H13.66v-1c.43-.49.87-1%2C1.31-1.57s.87-1.13%2C1.27-1.7S17%2C9.14%2C17.36%2C8.57a16.51%2C16.51%2C0%2C0%2C0%2C.86-1.65Zm-4.49%2C6.59h3.37V8.63c-.34.61-.67%2C1.15-1%2C1.63s-.6.91-.87%2C1.3-.56.74-.81%2C1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; - } - .primary-editor > h5::before { - margin-left: -64px !important; - padding-left: 40px !important; - content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16l.58-.76a3.67%2C3.67%2C0%2C0%2C0%2C1%2C.58A3.44%2C3.44%2C0%2C0%2C0%2C16.8%2C16a2.17%2C2.17%2C0%2C0%2C0%2C1.58-.6A2%2C2%2C0%2C0%2C0%2C19%2C13.88a1.85%2C1.85%2C0%2C0%2C0-.64-1.5%2C2.83%2C2.83%2C0%2C0%2C0-1.86-.54c-.27%2C0-.55%2C0-.86%2C0s-.58%2C0-.81.06L15.17%2C7H19.7V8H16.14l-.2%2C2.88.47%2C0h.43a3.5%2C3.5%2C0%2C0%2C1%2C2.43.79%2C2.74%2C2.74%2C0%2C0%2C1%2C.88%2C2.16%2C3%2C3%2C0%2C0%2C1-.94%2C2.3%2C3.41%2C3.41%2C0%2C0%2C1-2.4.87%2C4.45%2C4.45%2C0%2C0%2C1-1.5-.24A4.81%2C4.81%2C0%2C0%2C1%2C14%2C16Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; - } - .primary-editor > h6::before { - margin-left: -64px !important; - padding-left: 40px !important; - content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M20.18%2C13.7a3.24%2C3.24%2C0%2C0%2C1-.88%2C2.38%2C2.94%2C2.94%2C0%2C0%2C1-2.2.9%2C2.69%2C2.69%2C0%2C0%2C1-2.31-1.17A5.59%2C5.59%2C0%2C0%2C1%2C14%2C12.49a12.18%2C12.18%2C0%2C0%2C1%2C.2-2.14%2C5.16%2C5.16%2C0%2C0%2C1%2C.84-2A3.65%2C3.65%2C0%2C0%2C1%2C16.27%2C7.2%2C3.71%2C3.71%2C0%2C0%2C1%2C18%2C6.84%2C3.14%2C3.14%2C0%2C0%2C1%2C19%2C7a3.59%2C3.59%2C0%2C0%2C1%2C1%2C.5l-.56.77a2.3%2C2.3%2C0%2C0%2C0-1.49-.48A2.3%2C2.3%2C0%2C0%2C0%2C16.79%2C8a3%2C3%2C0%2C0%2C0-.92.85%2C3.79%2C3.79%2C0%2C0%2C0-.56%2C1.25%2C6.56%2C6.56%2C0%2C0%2C0-.19%2C1.65h0a2.61%2C2.61%2C0%2C0%2C1%2C1-.84%2C2.91%2C2.91%2C0%2C0%2C1%2C1.23-.28%2C2.63%2C2.63%2C0%2C0%2C1%2C2%2C.85A3.09%2C3.09%2C0%2C0%2C1%2C20.18%2C13.7ZM19%2C13.78a2.28%2C2.28%2C0%2C0%2C0-.5-1.62%2C1.67%2C1.67%2C0%2C0%2C0-1.29-.54%2C2%2C2%2C0%2C0%2C0-1.5.58%2C2%2C2%2C0%2C0%2C0-.56%2C1.4%2C2.65%2C2.65%2C0%2C0%2C0%2C.55%2C1.74%2C1.85%2C1.85%2C0%2C0%2C0%2C2.78.1A2.38%2C2.38%2C0%2C0%2C0%2C19%2C13.78Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important; - } - .primary-editor > p, .primary-editor h1, .primary-editor h2, .primary-editor h3, .primary-editor h4, .primary-editor h5, .primary-editor h6, .primary-editor pre, .primary-editor blockquote, .primary-editor table, .primary-editor ul, .primary-editor ol, .primary-editor hr{ - max-width: unset - } - `, + innerHTML: await getFileContent(rootURI + "chrome/content/styles/editor.css"), }, ignoreIfExists: true, }, diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts index 373f959..a680b62 100644 --- a/src/modules/editor/toolbar.ts +++ b/src/modules/editor/toolbar.ts @@ -4,17 +4,12 @@ import { getLineAtCursor, getSectionAtCursor } from "../../utils/editor"; import { showHint } from "../../utils/hint"; import { getNoteLink, getNoteLinkParams } from "../../utils/link"; import { getString } from "../../utils/locale"; -import { - addLineToNote, - getNoteTreeFlattened, - getNoteType, -} from "../../utils/note"; +import { addLineToNote, getNoteTreeFlattened } from "../../utils/note"; import { getPref } from "../../utils/prefs"; import { slice } from "../../utils/str"; export async function initEditorToolbar(editor: Zotero.EditorInstance) { const noteItem = editor._item; - const noteType = getNoteType(noteItem.id); const toolbar = await registerEditorToolbar(editor, makeId("toolbar")); // Settings @@ -56,22 +51,7 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { id: makeId("settings-openWorkspace"), text: getString("editor.toolbar.settings.openWorkspace"), callback: (e) => { - addon.hooks.onOpenWorkspace("tab"); - }, - }, - { - id: makeId("settings-setWorkspace"), - text: getString("editor.toolbar.settings.setWorkspace"), - callback: (e) => { - addon.hooks.onSetWorkspaceNote(e.editor._item.id, "main"); - }, - }, - { - id: makeId("settings-previewInWorkspace"), - text: getString("editor.toolbar.settings.previewInWorkspace"), - callback: (e) => { - addon.hooks.onOpenWorkspace("tab"); - addon.hooks.onSetWorkspaceNote(e.editor._item.id, "preview"); + addon.hooks.onOpenWorkspace(noteItem, "tab"); }, }, { @@ -221,103 +201,95 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { }); // Center button - if (noteType === "main") { - registerEditorToolbarElement( + + const onTriggerMenu = (ev: MouseEvent) => { + editor._iframeWindow.focus(); + const linkMenu: PopupData[] = getLinkMenuData(editor); + editor._iframeWindow.document + .querySelector(`#${makeId("link")}`)! + .querySelector(".toolbar-button")!.innerHTML = ICONS.linkAfter; + + const popup = registerEditorToolbarPopup( editor, - toolbar, + linkButton, + `${config.addonRef}-link-popup`, "middle", - ztoolkit.UI.createElement(editor._iframeWindow.document, "div", { - properties: { innerHTML: getString("editor.toolbar.main") }, - }), + linkMenu, ); - } else { - const onTriggerMenu = (ev: MouseEvent) => { - editor._iframeWindow.focus(); - const linkMenu: PopupData[] = getLinkMenuData(editor); - editor._iframeWindow.document - .querySelector(`#${makeId("link")}`)! - .querySelector(".toolbar-button")!.innerHTML = ICONS.linkAfter; + }; - const popup = registerEditorToolbarPopup( - editor, - linkButton, - `${config.addonRef}-link-popup`, - "middle", - linkMenu, - ); - }; + const onExitMenu = (ev: MouseEvent) => { + editor._iframeWindow.document + .querySelector(`#${makeId("link-popup")}`) + ?.remove(); + editor._iframeWindow.document + .querySelector(`#${makeId("link")}`)! + .querySelector(".toolbar-button")!.innerHTML = ICONS.addon; + }; - const onExitMenu = (ev: MouseEvent) => { - editor._iframeWindow.document - .querySelector(`#${makeId("link-popup")}`) - ?.remove(); - editor._iframeWindow.document - .querySelector(`#${makeId("link")}`)! - .querySelector(".toolbar-button")!.innerHTML = ICONS.addon; - }; - - const onClickMenu = async (ev: MouseEvent) => { - const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null; - if (!mainNote?.isNote()) { - return; - } - const lineIndex = parseInt( - (ev.target as HTMLDivElement).id.split("-").pop() || "-1", - ); - const forwardLink = getNoteLink(noteItem); - const backLink = getNoteLink(mainNote, { ignore: true, lineIndex }); - addLineToNote( - mainNote, - await addon.api.template.runTemplate( - "[QuickInsertV2]", - "link, linkText, subNoteItem, noteItem", - [ - forwardLink, - noteItem.getNoteTitle().trim() || forwardLink, - noteItem, - mainNote, - ], - ), - lineIndex, - ); - addLineToNote( - noteItem, - await addon.api.template.runTemplate( - "[QuickBackLinkV2]", - "link, linkText, subNoteItem, noteItem", - [ - backLink, - mainNote.getNoteTitle().trim() || "Workspace Note", - noteItem, - mainNote, - "", - ], - ), - ); - onExitMenu(ev); - ev.stopPropagation(); - }; - - const linkButton = await registerEditorToolbarDropdown( - editor, - toolbar, - makeId("link"), - ICONS.addon, - getString("editor.toolbar.link.title"), - "middle", - onClickMenu, + const onClickMenu = async (ev: MouseEvent) => { + // TODO: fix link + return; + const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null; + if (!mainNote?.isNote()) { + return; + } + const lineIndex = parseInt( + (ev.target as HTMLDivElement).id.split("-").pop() || "-1", ); + const forwardLink = getNoteLink(noteItem); + const backLink = getNoteLink(mainNote, { ignore: true, lineIndex }); + addLineToNote( + mainNote, + await addon.api.template.runTemplate( + "[QuickInsertV2]", + "link, linkText, subNoteItem, noteItem", + [ + forwardLink, + noteItem.getNoteTitle().trim() || forwardLink, + noteItem, + mainNote, + ], + ), + lineIndex, + ); + addLineToNote( + noteItem, + await addon.api.template.runTemplate( + "[QuickBackLinkV2]", + "link, linkText, subNoteItem, noteItem", + [ + backLink, + mainNote.getNoteTitle().trim() || "Workspace Note", + noteItem, + mainNote, + "", + ], + ), + ); + onExitMenu(ev); + ev.stopPropagation(); + }; - linkButton.addEventListener("mouseenter", onTriggerMenu); - linkButton.addEventListener("mouseleave", onExitMenu); - linkButton.addEventListener("mouseleave", onExitMenu); - linkButton.addEventListener("click", (ev) => { - if ((ev.target as HTMLElement).classList.contains("option")) { - onClickMenu(ev); - } - }); - editor._iframeWindow.document.addEventListener("click", onExitMenu); - } + const linkButton = await registerEditorToolbarDropdown( + editor, + toolbar, + makeId("link"), + ICONS.addon, + getString("editor.toolbar.link.title"), + "middle", + onClickMenu, + ); + + linkButton.addEventListener("mouseenter", onTriggerMenu); + linkButton.addEventListener("mouseleave", onExitMenu); + linkButton.addEventListener("mouseleave", onExitMenu); + linkButton.addEventListener("click", (ev) => { + if ((ev.target as HTMLElement).classList.contains("option")) { + onClickMenu(ev); + } + }); + editor._iframeWindow.document.addEventListener("click", onExitMenu); // Export // const exportButton = await registerEditorToolbarDropdown( diff --git a/src/modules/export/api.ts b/src/modules/export/api.ts index 19455c8..91c4c18 100644 --- a/src/modules/export/api.ts +++ b/src/modules/export/api.ts @@ -212,7 +212,7 @@ async function toFreeMind(noteItem: Zotero.Item) { } async function embedLinkedNotes(noteItem: Zotero.Item): Promise { - const parser = ztoolkit.getDOMParser(); + const parser = new DOMParser(); const globalCitationData = getNoteCitationData(noteItem as Zotero.Item); diff --git a/src/modules/menu.ts b/src/modules/menu.ts index aa8d78d..1c688a5 100644 --- a/src/modules/menu.ts +++ b/src/modules/menu.ts @@ -17,22 +17,6 @@ export function registerMenus() { ); }, }); - ztoolkit.Menu.register("item", { - tag: "menuitem", - label: getString("menuItem.setMainNote"), - icon: `chrome://${config.addonRef}/content/icons/favicon.png`, - commandListener: (ev) => { - addon.hooks.onSetWorkspaceNote(ZoteroPane.getSelectedItems()[0].id); - }, - getVisibility: (elem, ev) => { - const items = ZoteroPane.getSelectedItems(); - return ( - items.length == 1 && - items[0].isNote() && - items[0].id !== addon.data.workspace.mainId - ); - }, - }); // menuEdit const menuEditAnchor = document.querySelector( @@ -133,88 +117,66 @@ export function registerMenus() { menuFileAnchor, ); - document - .querySelector(`#${recentMainNotesMenuId}`) - ?.addEventListener("popupshowing", () => { - const popup = document.querySelector(`#${recentMainNotesMenuPopupId}`)!; - removeChildren(popup); - const children = Zotero.Items.get( - ((getPref("recentMainNoteIds") as string) || "") - .split(",") - .map((id) => parseInt(id)) - .filter((id) => id !== addon.data.workspace.mainId), - ) - .filter((item) => item.isNote()) - .map((item) => ({ - tag: "menuitem", - attributes: { - label: slice( - `${slice(item.getNoteTitle().trim() || item.key, 30)} - ${ - item.parentItem - ? "📄" + item.parentItem.getField("title") - : "📁" + - Zotero.Collections.get(item.getCollections()) - .map( - (collection) => (collection as Zotero.Collection).name, - ) - .join(", ") - }`, - 200, - ), - }, - listeners: [ - { - type: "command", - listener: () => { - addon.hooks.onSetWorkspaceNote(item.id, "main"); - addon.hooks.onOpenWorkspace(); - }, - }, - ], - })); - const defaultChildren = [ - { - tag: "menuitem", - attributes: { - label: getString("menuFile-openRecent-empty"), - disabled: true, - }, - }, - ]; + // document + // .querySelector(`#${recentMainNotesMenuId}`) + // ?.addEventListener("popupshowing", () => { + // const popup = document.querySelector(`#${recentMainNotesMenuPopupId}`)!; + // removeChildren(popup); + // const children = Zotero.Items.get( + // ((getPref("recentMainNoteIds") as string) || "") + // .split(",") + // .map((id) => parseInt(id)) + // .filter((id) => id !== addon.data.workspace.mainId), + // ) + // .filter((item) => item.isNote()) + // .map((item) => ({ + // tag: "menuitem", + // attributes: { + // label: slice( + // `${slice(item.getNoteTitle().trim() || item.key, 30)} - ${ + // item.parentItem + // ? "📄" + item.parentItem.getField("title") + // : "📁" + + // Zotero.Collections.get(item.getCollections()) + // .map( + // (collection) => (collection as Zotero.Collection).name, + // ) + // .join(", ") + // }`, + // 200, + // ), + // }, + // listeners: [ + // { + // type: "command", + // listener: () => { + // addon.hooks.onSetWorkspaceNote(item.id, "main"); + // addon.hooks.onOpenWorkspace(); + // }, + // }, + // ], + // })); + // const defaultChildren = [ + // { + // tag: "menuitem", + // attributes: { + // label: getString("menuFile-openRecent-empty"), + // disabled: true, + // }, + // }, + // ]; - ztoolkit.UI.appendElement( - { - tag: "fragment", - children: children.length === 0 ? defaultChildren : children, - enableElementRecord: false, - }, - popup, - ); - return true; - }); + // ztoolkit.UI.appendElement( + // { + // tag: "fragment", + // children: children.length === 0 ? defaultChildren : children, + // enableElementRecord: false, + // }, + // popup, + // ); + // return true; + // }); - ztoolkit.Menu.register( - "menuFile", - { - tag: "menuitem", - label: getString("menuFile-openMainNote"), - icon: `chrome://${config.addonRef}/content/icons/favicon.png`, - commandListener: async (ev) => { - const selectedIds = await itemPicker(); - if ( - selectedIds?.length === 1 && - Zotero.Items.get(selectedIds[0]).isNote() - ) { - addon.hooks.onSetWorkspaceNote(selectedIds[0], "main"); - addon.hooks.onOpenWorkspace(); - } else { - window.alert(getString("menuFile-openMainNote-error")); - } - }, - }, - "after", - menuFileAnchor, - ); ztoolkit.Menu.register( "menuFile", { tag: "menuseparator" }, @@ -256,28 +218,11 @@ export function registerMenus() { "after", menuFileAnchor, ); - ztoolkit.Menu.register( - "menuFile", - { - tag: "menuitem", - label: getString("menuAddNote.newMainNote"), - icon: `chrome://${config.addonRef}/content/icons/favicon.png`, - commandListener: addon.hooks.onCreateWorkspaceNote, - }, - "after", - menuFileAnchor, - ); // create note menu in library const newNoteMenu = document .querySelector("#zotero-tb-note-add") ?.querySelector("menupopup") as XUL.MenuPopup; - ztoolkit.Menu.register(newNoteMenu, { - tag: "menuitem", - label: getString("menuAddNote.newMainNote"), - icon: `chrome://${config.addonRef}/content/icons/favicon.png`, - commandListener: addon.hooks.onCreateWorkspaceNote, - }); ztoolkit.Menu.register(newNoteMenu, { tag: "menuitem", label: getString("menuAddNote.newTemplateStandaloneNote"), diff --git a/src/modules/viewItems.ts b/src/modules/viewItems.ts new file mode 100644 index 0000000..43e76f1 --- /dev/null +++ b/src/modules/viewItems.ts @@ -0,0 +1,28 @@ +import { PatchHelper } from "zotero-plugin-toolkit/dist/helpers/patch"; + +export function patchViewItems(win: Window) { + // @ts-ignore + const ZoteroPane = win.ZoteroPane; + new PatchHelper().setData({ + target: ZoteroPane, + funcSign: "viewItems", + patcher: (origin) => + function (items: Zotero.Item[], event?: KeyboardEvent) { + if (!addon.data.alive || event?.shiftKey) { + // @ts-ignore + return origin.apply(this, [items, event]); + } + const otherItems = []; + for (const item of items) { + if (item.isNote()) { + addon.hooks.onOpenWorkspace(item, "tab"); + continue; + } + otherItems.push(item); + } + // @ts-ignore + return origin.apply(this, [otherItems, event]); + }, + enabled: true, + }); +} diff --git a/src/modules/workspace/content.ts b/src/modules/workspace/content.ts index 9ca1ec9..97fb6f2 100644 --- a/src/modules/workspace/content.ts +++ b/src/modules/workspace/content.ts @@ -1,489 +1,20 @@ -import { config } from "../../../package.json"; -import { ICONS } from "../../utils/config"; -import { itemPicker } from "../../utils/itemPicker"; -import { getString } from "../../utils/locale"; -import { getNoteTreeFlattened } from "../../utils/note"; -import { getPref } from "../../utils/prefs"; -import { waitUtilAsync } from "../../utils/wait"; -import { OutlineType } from "../../utils/workspace"; import { saveFreeMind as _saveFreeMind } from "../export/freemind"; -function makeId(key: string) { - return `betternotes-workspace-${key}`; -} - -export function initWorkspace(container: XUL.Box | undefined) { +export function initWorkspace(container: XUL.Box, item: Zotero.Item) { if (!container) { return; } - function makeTooltipProp( - id: string, - content: string, - title: string, - callback: (ev: Event) => void, - ) { - return { - id, - tag: "button", - namespace: "html", - classList: ["tool-button"], - properties: { - innerHTML: content, - title, - }, - listeners: [ - { - type: "click", - listener: callback, - }, - ], - }; - } - ztoolkit.UI.appendElement( - { - tag: "hbox", - id: makeId("top-container"), - styles: { width: "100%", height: "100%" }, - properties: {}, - attributes: { - flex: "1", - }, - children: [ - { - tag: "vbox", - id: makeId("outline-container"), - styles: { - overflow: "hidden", - display: "flex", - flexDirection: "column", - }, - attributes: { - width: "330", - minwidth: "300", - flex: "1", - }, - children: [ - { - tag: "link", - properties: { - rel: "stylesheet", - href: `chrome://${config.addonRef}/content/toolbutton.css`, - }, - }, - { - tag: "div", - id: makeId("outline-content"), - styles: { - height: "100%", - }, - children: [ - { - tag: "iframe", - id: makeId("outline-iframe"), - properties: { - width: "100%", - }, - styles: { - border: "0", - height: "100%", - }, - }, - ], - }, - { - tag: "div", - id: makeId("outline-buttons"), - styles: { - height: "50px", - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - margin: "10px 20px 10px 20px", - }, - children: [ - makeTooltipProp( - makeId("switchOutline"), - ICONS.switchOutline, - getString("workspace.switchOutline"), - (ev) => { - setOutline(container); - }, - ), - makeTooltipProp( - makeId("saveOutlineImage"), - ICONS.saveOutlineImage, - getString("workspace.saveOutlineImage"), - (ev) => { - saveImage(container); - }, - ), - makeTooltipProp( - makeId("saveOutlineFreeMind"), - ICONS.saveOutlineFreeMind, - getString("workspace.saveOutlineFreeMind"), - (ev) => { - saveFreeMind(); - }, - ), - ], - }, - ], - }, - { - tag: "splitter", - id: makeId("outline-splitter"), - attributes: { collapse: "before" }, - children: [ - { - tag: "grippy", - }, - ], - }, - { - tag: "vbox", - id: makeId("editor-main-container"), - attributes: { - flex: "1", - width: "700", - }, - styles: { - background: "var(--material-background50)", - }, - }, - { - tag: "splitter", - id: makeId("preview-splitter"), - attributes: { collapse: "after", state: "collapsed" }, - children: [ - { - tag: "grippy", - }, - ], - }, - { - tag: "vbox", - id: makeId("editor-preview-container"), - attributes: { - flex: "1", - width: "500", - }, - }, - ], - }, - container, - ); - // Manually add custom editor items in Zotero 7 + + container.style.minWidth = "0px"; + container.style.minHeight = "0px"; + // @ts-ignore const customElements = container.ownerGlobal .customElements as CustomElementRegistry; - const mainEditorContainer = container.querySelector( - `#${makeId("editor-main-container")}`, - ); - const previewEditorContainer = container.querySelector( - `#${makeId("editor-preview-container")}`, - ); - const mainEditor = new (customElements.get("note-editor")!)(); - mainEditor.id = makeId("editor-main"); - mainEditor.setAttribute("flex", "1"); - const previewEditor = new (customElements.get("note-editor")!)(); - previewEditor.id = makeId("editor-preview"); - previewEditor.setAttribute("flex", "1"); - mainEditorContainer?.append(mainEditor); - previewEditorContainer?.append(previewEditor); - const outlineContainer = container.querySelector( - `#${makeId("outline-container")}`, - ) as XUL.Box; - outlineContainer.style.background = "var(--material-sidepane)"; - const outlineMut = new (ztoolkit.getGlobal("MutationObserver"))( - (mutations) => { - if (outlineContainer.getAttribute("collapsed") === "true") { - outlineContainer.style.removeProperty("display"); - } else { - outlineContainer.style.display = "flex"; - } - }, - ); - outlineMut.observe(outlineContainer, { - attributes: true, - attributeFilter: ["collapsed"], - }); - - setOutline(container, OutlineType.treeView); - initWorkspaceEditor(container, "main", addon.data.workspace.mainId); -} - -export async function initWorkspaceEditor( - container: XUL.Box, - type: "main" | "preview", - noteId: number, - options: { - lineIndex?: number; - sectionName?: string; - } = {}, -) { - const noteItem = Zotero.Items.get(noteId); - if (!noteItem || !noteItem.isNote()) { - ztoolkit.UI.appendElement( - { - tag: "div", - id: makeId("emptyWorkspaceGuide"), - styles: { - position: "absolute", - top: "0", - left: "0", - width: "100%", - height: "100%", - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - textAlign: "center", - backgroundColor: "rgba(255,255,255,0.8)", - zIndex: "100", - }, - children: [ - { - tag: "div", - properties: { - innerHTML: getString("workspace-emptyWorkspaceGuideInfo"), - }, - styles: { - fontSize: "20px", - fontWeight: "bold", - color: "gray", - }, - }, - { - tag: "button", - namespace: "xul", - properties: { - label: getString("workspace-emptyWorkspaceGuideOpen"), - }, - styles: { - fontSize: "16px", - }, - listeners: [ - { - type: "command", - listener: async (ev) => { - const selectedIds = await itemPicker(); - if ( - selectedIds?.length === 1 && - Zotero.Items.get(selectedIds[0]).isNote() - ) { - addon.hooks.onSetWorkspaceNote(selectedIds[0], "main"); - addon.hooks.onOpenWorkspace(); - } else { - window.alert(getString("menuFile-openMainNote-error")); - } - }, - }, - ], - }, - { - tag: "div", - properties: { - innerHTML: getString("workspace-emptyWorkspaceGuideOr"), - }, - styles: { - fontSize: "16px", - color: "gray", - }, - }, - { - tag: "button", - namespace: "xul", - properties: { - label: getString("workspace-emptyWorkspaceGuideCreate"), - }, - styles: { - fontSize: "16px", - }, - listeners: [ - { - type: "command", - listener: () => { - addon.hooks.onCreateWorkspaceNote(); - }, - }, - ], - }, - ], - }, - container, - ); - return; - } else { - container.querySelector(`#${makeId("emptyWorkspaceGuide")}`)?.remove(); - } - const editorElem = container.querySelector( - `#${makeId("editor-" + type)}`, - ) as EditorElement; - await waitUtilAsync(() => Boolean(editorElem._initialized)) - .then(() => ztoolkit.log("ok")) - .catch(() => ztoolkit.log("fail")); - if (!editorElem._initialized) { - throw new Error("initNoteEditor: waiting initialization failed"); - } - editorElem.mode = "edit"; - editorElem.viewMode = "library"; - editorElem.parent = undefined; - editorElem.item = noteItem; - await waitUtilAsync(() => Boolean(editorElem._editorInstance)); - await editorElem._editorInstance._initPromise; - if (typeof options.lineIndex === "number") { - addon.api.editor.scroll(editorElem._editorInstance, options.lineIndex); - } - if (typeof options.sectionName === "string") { - addon.api.editor.scrollToSection( - editorElem._editorInstance, - options.sectionName, - ); - } - return; -} - -function getContainerType( - container: XUL.Box | undefined, -): "tab" | "window" | "unknown" { - if (!container) { - return "unknown"; - } - return (container.getAttribute("workspace-type") || "unknown") as - | "tab" - | "window" - | "unknown"; -} - -export function toggleOutlinePane(visibility?: boolean, container?: XUL.Box) { - const splitter = container?.querySelector(`#${makeId("outline-splitter")}`); - if (typeof visibility === "undefined") { - visibility = splitter?.getAttribute("state") === "collapsed"; - } - splitter?.setAttribute("state", visibility ? "open" : "collapsed"); -} - -export function togglePreviewPane(visibility?: boolean, container?: XUL.Box) { - const splitter = container?.querySelector(`#${makeId("preview-splitter")}`); - if (typeof visibility === "undefined") { - visibility = splitter?.getAttribute("state") === "collapsed"; - } - splitter?.setAttribute("state", visibility ? "open" : "collapsed"); -} - -export function toggleNotesPane(visibility?: boolean) { - const splitter = document?.querySelector("#zotero-context-splitter"); - if (typeof visibility === "undefined") { - visibility = splitter?.getAttribute("state") === "collapsed"; - } - splitter?.setAttribute("state", visibility ? "open" : "collapsed"); -} - -export function getWorkspaceEditor( - workspaceType: "tab" | "window", - editorType: "main" | "preview" = "main", -) { - const container = - workspaceType === "tab" - ? addon.data.workspace.tab.container - : addon.data.workspace.window.container; - return ( - container?.querySelector(`#${makeId(`editor-${editorType}`)}`) as - | EditorElement - | undefined - )?._editorInstance; -} - -const SRC_LIST = [ - "", - `chrome://${config.addonRef}/content/treeView.html`, - `chrome://${config.addonRef}/content/mindMap.html`, - `chrome://${config.addonRef}/content/bubbleMap.html`, -]; - -function setOutline( - container: XUL.Box, - newType: OutlineType = OutlineType.empty, -) { - if (newType === OutlineType.empty) { - newType = addon.data.workspace.outline + 1; - } - if (newType > OutlineType.bubbleMap) { - newType = OutlineType.treeView; - } - addon.data.workspace.outline = newType; - ( - container.querySelector(`#${makeId("saveOutlineImage")}`) as HTMLDivElement - ).hidden = newType === OutlineType.treeView; - ( - container.querySelector( - `#${makeId("saveOutlineFreeMind")}`, - ) as HTMLDivElement - ).hidden = newType === OutlineType.treeView; - const iframe = container.querySelector( - `#${makeId("outline-iframe")}`, - ) as HTMLIFrameElement; - iframe.setAttribute("src", SRC_LIST[addon.data.workspace.outline]); - updateOutline(container); - updateOutlineButtons(container); -} - -export async function updateOutline(container: XUL.Box) { - const iframe = container.querySelector( - `#${makeId("outline-iframe")}`, - ) as HTMLIFrameElement; - await waitUtilAsync( - () => iframe.contentWindow?.document.readyState === "complete", - ); - iframe.contentWindow?.postMessage( - { - type: "setMindMapData", - nodes: getNoteTreeFlattened( - Zotero.Items.get(addon.data.workspace.mainId), - { keepLink: !!getPref("workspace.outline.keepLinks") }, - ), - workspaceType: getContainerType(container), - expandLevel: getPref("workspace.outline.expandLevel"), - }, - "*", - ); -} - -function updateOutlineButtons(container: XUL.Box) { - const outlineType = addon.data.workspace.outline; - const isTreeView = outlineType === OutlineType.treeView; - ( - container.querySelector(`#${makeId("saveOutlineImage")}`) as HTMLDivElement - ).style.visibility = isTreeView ? "hidden" : "visible"; - ( - container.querySelector( - `#${makeId("saveOutlineFreeMind")}`, - ) as HTMLDivElement - ).style.visibility = isTreeView ? "hidden" : "visible"; -} - -function saveImage(container: XUL.Box) { - const iframe = container.querySelector( - `#${makeId("outline-iframe")}`, - ) as HTMLIFrameElement; - iframe.contentWindow?.postMessage( - { - type: "saveSVG", - }, - "*", - ); -} - -async function saveFreeMind() { - // TODO: uncouple this part - const filename = await new ztoolkit.FilePicker( - `${Zotero.getString("fileInterface.export")} FreeMind XML`, - "save", - [["FreeMind XML File(*.mm)", "*.mm"]], - `${Zotero.Items.get(addon.data.workspace.mainId).getNoteTitle()}.mm`, - ).open(); - if (filename) { - await _saveFreeMind(filename, addon.data.workspace.mainId); - } + const workspace = new (customElements.get("bn-workspace")!)() as any; + container.append(workspace); + workspace.item = item; + workspace.containerType = "tab"; + workspace.render(); } diff --git a/src/modules/workspace/message.ts b/src/modules/workspace/message.ts deleted file mode 100644 index 61ae324..0000000 --- a/src/modules/workspace/message.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - getEditorInstance, - moveHeading, - updateHeadingTextAtLine, -} from "../../utils/editor"; -import { showHintWithLink } from "../../utils/hint"; -import { getNoteLinkParams } from "../../utils/link"; -import { getNoteTree, getNoteTreeNodeById } from "../../utils/note"; -import { formatPath } from "../../utils/str"; - -export async function messageHandler(ev: MessageEvent) { - switch (ev.data.type) { - case "jumpNode": { - const editor = addon.api.workspace.getWorkspaceEditor( - ev.data.workspaceType, - "main", - ); - if (!editor) { - return; - } - addon.api.editor.scroll(editor, ev.data.lineIndex); - return; - } - case "openNote": { - const linkParams = getNoteLinkParams(ev.data.link); - if (!linkParams.noteItem) { - return; - } - addon.hooks.onOpenNote(linkParams.noteItem.id, "preview", { - lineIndex: linkParams.lineIndex || undefined, - }); - return; - } - case "moveNode": { - const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - const tree = getNoteTree(noteItem); - const fromNode = getNoteTreeNodeById(noteItem, ev.data.fromID, tree); - const toNode = getNoteTreeNodeById(noteItem, ev.data.toID, tree); - moveHeading( - getEditorInstance(noteItem.id), - fromNode!, - toNode!, - ev.data.moveType, - ); - return; - } - case "editNode": { - const editor = addon.api.workspace.getWorkspaceEditor( - ev.data.workspaceType, - "main", - ); - if (!editor) { - return; - } - updateHeadingTextAtLine( - editor, - ev.data.lineIndex, - ev.data.text.replace(/[\r\n]/g, ""), - ); - return; - } - case "saveSVGReturn": { - const filename = await new ztoolkit.FilePicker( - `${Zotero.getString("fileInterface.export")} SVG Image`, - "save", - [["SVG File(*.svg)", "*.svg"]], - `${Zotero.Items.get(addon.data.workspace.mainId).getNoteTitle()}.svg`, - ).open(); - if (filename) { - await Zotero.File.putContentsAsync(formatPath(filename), ev.data.image); - showHintWithLink( - `Image Saved to ${filename}`, - "Show in Folder", - (ev) => { - Zotero.File.reveal(filename); - }, - ); - } - return; - } - default: - return; - } -} diff --git a/src/modules/workspace/tab.ts b/src/modules/workspace/tab.ts index 72e3004..d443f83 100644 --- a/src/modules/workspace/tab.ts +++ b/src/modules/workspace/tab.ts @@ -1,13 +1,7 @@ import { config } from "../../../package.json"; -import { ICONS } from "../../utils/config"; -import { showHint } from "../../utils/hint"; -import { getString } from "../../utils/locale"; -import { getPref, setPref } from "../../utils/prefs"; -import { waitUtilAsync } from "../../utils/wait"; -// TODO: uncouple these imports -import { messageHandler } from "./message"; +import { initWorkspace } from "./content"; -export const TAB_TYPE = "betternotes"; +export const TAB_TYPE = "note"; export function registerWorkspaceTab(win: Window) { const doc = win.document; @@ -29,18 +23,17 @@ export function registerWorkspaceTab(win: Window) { { type: "command", listener: (ev) => { - if ((ev as MouseEvent).shiftKey) { - addon.hooks.onOpenWorkspace("window"); - } else { - addon.hooks.onOpenWorkspace("tab"); - } + // if ((ev as MouseEvent).shiftKey) { + // addon.hooks.onOpenWorkspace("window"); + // } else { + // addon.hooks.onOpenWorkspace("tab"); + // } }, }, ], }, spacer, ) as XUL.ToolBarButton; - win.addEventListener("message", messageHandler, false); const collectionSearch = doc.querySelector("#zotero-collections-search")!; const ob = new (ztoolkit.getGlobal("MutationObserver"))((muts) => { tabButton.hidden = !!collectionSearch?.classList.contains("visible"); @@ -54,347 +47,47 @@ export function registerWorkspaceTab(win: Window) { "unload", () => { ob.disconnect(); - win.removeEventListener("message", messageHandler); }, { once: true }, ); } -export async function openWorkspaceTab() { - if (addon.data.workspace.tab.active) { - Zotero_Tabs.select(addon.data.workspace.tab.id!); +export async function openWorkspaceTab(item: Zotero.Item) { + const currentTab = Zotero_Tabs._tabs.find( + (tab) => tab.data?.itemID == item.id, + ); + if (currentTab) { + Zotero_Tabs.select(currentTab.id); return; } const { id, container } = Zotero_Tabs.add({ type: TAB_TYPE, - title: getString("tab.name"), + title: item.getNoteTitle(), index: 1, data: { - itemID: addon.data.workspace.mainId, + itemID: item.id, }, select: false, - onClose: () => { - deActivateWorkspaceTab(); - }, + onClose: () => {}, }); - addon.data.workspace.tab.id = id; - container.setAttribute("workspace-type", "tab"); - addon.data.workspace.tab.container = container; - - await activateWorkspaceTab(); + initWorkspace(container, item); Zotero_Tabs.select(id); } -function hoverWorkspaceTab(hovered: boolean) { - Array.from(document.querySelectorAll(".tab-toggle")).forEach((elem) => { - (elem as HTMLDivElement).style.visibility = hovered ? "visible" : "hidden"; - }); - const tabElem = document.querySelector( - `.tabs-wrapper .tab[data-id=${addon.data.workspace.tab.id}]`, - ) as HTMLDivElement; - const content = tabElem.querySelector(".tab-name") as HTMLDivElement; - content.removeAttribute("style"); - if (hovered) { - content.style["-moz-box-pack" as any] = "start"; - } -} +let contextPaneOpen: boolean | undefined = undefined; -function updateWorkspaceTabToggleButton( - type: "outline" | "preview" | "notes", - state: "open" | "collapsed", -) { - const elem = document.querySelector( - `#betternotes-tab-toggle-${type}`, - ) as HTMLDivElement; - if (!elem) { +export function onTabSelect(tabType: string) { + const ZoteroContextPane = ztoolkit.getGlobal("ZoteroContextPane"); + const splitter = ZoteroContextPane.getSplitter(); + + if (tabType === TAB_TYPE) { + contextPaneOpen = splitter.getAttribute("state") != "collapsed"; + splitter.setAttribute("state", "collapsed"); + } else if (typeof contextPaneOpen !== "undefined") { + splitter.setAttribute("state", contextPaneOpen ? "open" : "collapsed"); + contextPaneOpen = undefined; + } else { return; } - if (state !== "collapsed") { - state = "open"; - } - elem.innerHTML = ICONS[`workspace_${type}_${state}`]; -} - -function registerWorkspaceTabPaneObserver() { - const outlineSplitter = document.querySelector( - "#betternotes-workspace-outline-splitter", - ); - const outlineMut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => { - updateWorkspaceTabToggleButton( - "outline", - outlineSplitter!.getAttribute("state")! as "open" | "collapsed", - ); - }); - outlineMut.observe(outlineSplitter!, { - attributes: true, - attributeFilter: ["state"], - }); - const previewSplitter = document.querySelector( - "#betternotes-workspace-preview-splitter", - ); - const previeweMut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => { - updateWorkspaceTabToggleButton( - "preview", - previewSplitter!.getAttribute("state")! as "open" | "collapsed", - ); - }); - previeweMut.observe(previewSplitter!, { - attributes: true, - attributeFilter: ["state"], - }); - const notesSplitter = document.querySelector("#zotero-context-splitter"); - const notesMut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => { - updateWorkspaceTabToggleButton( - "notes", - notesSplitter!.getAttribute("state")! as "open" | "collapsed", - ); - }); - notesMut.observe(notesSplitter!, { - attributes: true, - attributeFilter: ["state"], - }); -} - -function isContextPaneInitialized() { - return ( - (document.querySelector(".notes-pane-deck")?.childElementCount || 0) > 0 - ); -} - -export async function activateWorkspaceTab() { - if (Zotero_Tabs.selectedType === TAB_TYPE && isContextPaneInitialized()) { - const tabToolbar = document.querySelector("#zotero-tab-toolbar") as XUL.Box; - tabToolbar && (tabToolbar.style.visibility = "collapse"); - const toolbar = document.querySelector( - "#zotero-context-toolbar-extension", - ) as XUL.Box; - if (toolbar) { - toolbar.style.visibility = "collapse"; - toolbar.nextElementSibling?.setAttribute("selectedIndex", "1"); - } - } - - if (addon.data.workspace.tab.active) { - ztoolkit.log("workspace tab is already active"); - return; - } - setWorkspaceTabStatus(true); - // reset tab style - await waitUtilAsync(() => - Boolean( - document.querySelector( - `.tabs-wrapper .tab[data-id=${addon.data.workspace.tab.id}]`, - ), - ), - ); - const tabElem = document.querySelector( - `.tabs-wrapper .tab[data-id=${addon.data.workspace.tab.id}]`, - ) as HTMLDivElement; - tabElem.removeAttribute("style"); - const content = tabElem.querySelector(".tab-name") as HTMLDivElement; - const close = tabElem.querySelector(".tab-close") as HTMLDivElement; - content.removeAttribute("style"); - content.append(document.createTextNode(getString("tab.name"))); - close.style.removeProperty("visibility"); - ztoolkit.UI.insertElementBefore( - { - tag: "fragment", - children: [ - { - tag: "div", - id: "betternotes-tab-toggle-outline", - classList: ["tab-close", "tab-toggle"], - styles: { - right: "56px", - }, - properties: { - innerHTML: ICONS.workspace_outline_open, - }, - listeners: [ - { - type: "click", - listener: (ev) => { - addon.hooks.onToggleWorkspacePane( - "outline", - undefined, - addon.data.workspace.tab.container, - ); - }, - }, - ], - }, - { - tag: "div", - id: "betternotes-tab-toggle-preview", - classList: ["tab-close", "tab-toggle"], - styles: { - right: "40px", - }, - properties: { - innerHTML: ICONS.workspace_preview_collapsed, - }, - listeners: [ - { - type: "click", - listener: (ev) => { - addon.hooks.onToggleWorkspacePane( - "preview", - undefined, - addon.data.workspace.tab.container, - ); - }, - }, - ], - }, - { - tag: "div", - id: "betternotes-tab-toggle-notes", - classList: ["tab-close", "tab-toggle"], - styles: { - right: "24px", - }, - properties: { - innerHTML: - document - .querySelector("#zotero-context-splitter") - ?.getAttribute("state") === "open" - ? ICONS.workspace_notes_open - : ICONS.workspace_notes_collapsed, - }, - listeners: [ - { - type: "click", - listener: (ev) => { - if (isContextPaneInitialized()) { - addon.hooks.onToggleWorkspacePane("notes"); - return; - } - showHint(getString("workspace.notesPane.hint")); - }, - }, - ], - }, - ], - }, - close, - ); - hoverWorkspaceTab(false); - tabElem.addEventListener("mouseenter", () => { - if (Zotero_Tabs.selectedType !== "betternotes") { - return; - } - hoverWorkspaceTab(true); - }); - tabElem.addEventListener("mousedown", () => hoverWorkspaceTab(true)); - tabElem.addEventListener("mouseleave", () => hoverWorkspaceTab(false)); - tabElem.addEventListener("mousedown", async (ev) => { - if (ev.button !== 2) { - return; - } - await Zotero.Promise.delay(300); - const menu = document - .querySelector("#zotero-itemmenu") - ?.parentElement?.lastElementChild?.querySelector("menu") - ?.querySelector("menupopup")?.lastElementChild; - menu?.addEventListener("click", () => { - addon.hooks.onOpenWorkspace("window"); - }); - }); - // load workspace content - const container = addon.data.workspace.tab.container; - initWorkspaceTabDragDrop(container, tabElem); - addon.hooks.onInitWorkspace(container); - registerWorkspaceTabPaneObserver(); - setWorkspaceTabStatus(true); -} - -export function deActivateWorkspaceTab() { - const tabToolbar = document.querySelector("#zotero-tab-toolbar") as XUL.Box; - tabToolbar && tabToolbar.style.removeProperty("visibility"); - const toolbar = document.querySelector( - "#zotero-context-toolbar-extension", - ) as XUL.Box; - toolbar?.style.removeProperty("visibility"); - setWorkspaceTabStatus(false); -} - -function setWorkspaceTabStatus(status: boolean) { - addon.data.workspace.tab.active = status; - setPref("workspace.tab.active", status); -} - -function initWorkspaceTabDragDrop( - container?: XUL.Box, - tabElem?: HTMLDivElement, -) { - if (!container) { - return; - } - const rect = tabElem?.getBoundingClientRect(); - ztoolkit.UI.appendElement( - { - tag: "div", - id: "bn-workspace-tab-drop", - styles: { - background: "#252526", - opacity: "0.6", - width: "100%", - height: "100px", - position: "fixed", - left: "0px", - top: `${rect?.bottom}px`, - textAlign: "center", - display: "flex", - visibility: "hidden", - zIndex: "65535", - }, - properties: { - hidden: true, - ondrop: (ev: DragEvent) => { - addon.hooks.onOpenWorkspace("window"); - }, - ondragenter: (ev: DragEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - dropElem.style.opacity = "0.9"; - if (ev.dataTransfer) { - ev.dataTransfer.dropEffect = "move"; - } - }, - ondragover: (ev: DragEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - }, - ondragleave: (ev: DragEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - dropElem.style.opacity = "0.6"; - }, - }, - children: [ - { - tag: "div", - styles: { - margin: "auto", - textAlign: "center", - color: "#fff", - }, - properties: { - innerHTML: getString("tab.openInWindow"), - }, - }, - ], - enableElementRecord: false, - }, - container, - ); - const dropElem = container.querySelector( - "#bn-workspace-tab-drop", - ) as HTMLDivElement; - tabElem?.addEventListener("dragstart", (ev) => { - dropElem.style.visibility = "visible"; - }); - tabElem?.addEventListener("dragend", (ev) => { - dropElem.style.visibility = "hidden"; - }); + ZoteroContextPane.update(); } diff --git a/src/modules/workspace/window.ts b/src/modules/workspace/window.ts index 855702b..382c83a 100644 --- a/src/modules/workspace/window.ts +++ b/src/modules/workspace/window.ts @@ -1,12 +1,6 @@ import { config } from "../../../package.json"; -import { isWindowAlive } from "../../utils/window"; -import { messageHandler } from "./message"; -export async function openWorkspaceWindow() { - if (isWindowAlive(addon.data.workspace.window.window)) { - addon.data.workspace.window.window?.focus(); - return; - } +export async function openWorkspaceWindow(item: Zotero.Item) { const windowArgs = { _initPromise: Zotero.Promise.defer(), }; @@ -17,16 +11,9 @@ export async function openWorkspaceWindow() { windowArgs, )!; await windowArgs._initPromise.promise; - addon.data.workspace.window.active = true; - addon.data.workspace.window.window = win; - addon.data.workspace.window.container = win.document.querySelector( + + const container = win.document.querySelector( "#workspace-container", ) as XUL.Box; - addon.hooks.onInitWorkspace(addon.data.workspace.window.container); - win.addEventListener("message", messageHandler, false); - win.addEventListener("unload", function onWindowUnload(ev) { - addon.data.workspace.window.active = false; - this.window.removeEventListener("unload", onWindowUnload, false); - this.window.removeEventListener("message", messageHandler, false); - }); + addon.hooks.onInitWorkspace(container, item); } diff --git a/src/utils/note.ts b/src/utils/note.ts index 4a7ad7c..b15a549 100644 --- a/src/utils/note.ts +++ b/src/utils/note.ts @@ -11,7 +11,6 @@ export { parseHTMLLines, getLinesInNote, addLineToNote, - getNoteType, getNoteTree, getNoteTreeFlattened, getNoteTreeNodeById, @@ -197,7 +196,7 @@ async function renderNoteHTML( refNotes = [noteItem]; } - const parser = ztoolkit.getDOMParser(); + const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const imageAttachments = refNotes.reduce((acc, note) => { acc.push(...Zotero.Items.get(note.getAttachments())); @@ -269,22 +268,12 @@ async function renderNoteHTML( return doc.body.innerHTML; } -function getNoteType(id: number) { - if (id === addon.data.workspace.mainId) { - return "main"; - } else if (id === addon.data.workspace.previewId) { - return "preview"; - } else { - return "default"; - } -} - function getNoteTree( note: Zotero.Item, parseLink: boolean = true, ): TreeModel.Node { const noteLines = getLinesInNote(note); - const parser = ztoolkit.getDOMParser(); + const parser = new DOMParser(); const tree = new TreeModel(); const root = tree.parse({ id: -1, @@ -422,7 +411,7 @@ async function copyEmbeddedImagesInHTML( ztoolkit.log(attachments); - const doc = ztoolkit.getDOMParser().parseFromString(html, "text/html"); + const doc = new DOMParser().parseFromString(html, "text/html"); // Copy note image attachments and replace keys in the new note for (const attachment of attachments) { diff --git a/src/utils/wait.ts b/src/utils/wait.ts index 4183d14..f03b3f6 100644 --- a/src/utils/wait.ts +++ b/src/utils/wait.ts @@ -5,12 +5,12 @@ export function waitUntil( timeout = 10000, ) { const start = Date.now(); - const intervalId = ztoolkit.getGlobal("setInterval")(() => { + const intervalId = setInterval(() => { if (condition()) { - ztoolkit.getGlobal("clearInterval")(intervalId); + clearInterval(intervalId); callback(); } else if (Date.now() - start > timeout) { - ztoolkit.getGlobal("clearInterval")(intervalId); + clearInterval(intervalId); } }, interval); } @@ -22,12 +22,12 @@ export function waitUtilAsync( ) { return new Promise((resolve, reject) => { const start = Date.now(); - const intervalId = ztoolkit.getGlobal("setInterval")(() => { + const intervalId = setInterval(() => { if (condition()) { - ztoolkit.getGlobal("clearInterval")(intervalId); + clearInterval(intervalId); resolve(); } else if (Date.now() - start > timeout) { - ztoolkit.getGlobal("clearInterval")(intervalId); + clearInterval(intervalId); reject(); } }, interval); diff --git a/typings/global.d.ts b/typings/global.d.ts index 0cae16c..a659262 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -33,3 +33,22 @@ declare const addon: import("../src/addon").default; declare const __env__: "production" | "development"; declare class Localization {} + +declare class XULElementBase extends HTMLElement { + get content(): DocumentFragment; + init(): void; + destroy(): void; + connectedCallback(): void; + disconnectedCallback(): void; + attributeChangedCallback( + name: string, + oldValue: string, + newValue: string, + ): void; + static get observedAttributes(): string[]; +} + +declare class MozXULElement { + static parseXULToFragment(xul: string): Fragment; + static insertFTLIfNeeded(ftl: string): void; +} From 33dda555b61fc1771a9873e21f8131548b2d6e07 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:37:38 +0800 Subject: [PATCH 02/23] fix: note tab button style --- addon/chrome/content/styles/outline.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addon/chrome/content/styles/outline.css b/addon/chrome/content/styles/outline.css index 4598945..8e0b09a 100644 --- a/addon/chrome/content/styles/outline.css +++ b/addon/chrome/content/styles/outline.css @@ -26,10 +26,15 @@ 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"); } From 86c77b095912eb1e16cbd1875852b41a0e520554 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:38:29 +0800 Subject: [PATCH 03/23] fix: disable workspace related code --- src/extras/workspace.ts | 2 +- src/modules/editor/toolbar.ts | 153 +++++++++++++++--------------- src/modules/sync/managerWindow.ts | 2 +- src/modules/template/picker.ts | 2 +- src/modules/template/preview.ts | 65 +++++++------ 5 files changed, 114 insertions(+), 110 deletions(-) diff --git a/src/extras/workspace.ts b/src/extras/workspace.ts index 2b134de..d629838 100644 --- a/src/extras/workspace.ts +++ b/src/extras/workspace.ts @@ -6,7 +6,7 @@ import { Workspace } from "../elements/workspace"; const elements = { "bn-context": ContextPane, "bn-outline": OutlinePane, - "bn-details": NoteDetails, + "bn-details": NoteDetails as unknown as CustomElementConstructor, "bn-workspace": Workspace, }; diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts index a680b62..89c0bec 100644 --- a/src/modules/editor/toolbar.ts +++ b/src/modules/editor/toolbar.ts @@ -230,45 +230,45 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { const onClickMenu = async (ev: MouseEvent) => { // TODO: fix link return; - const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null; - if (!mainNote?.isNote()) { - return; - } - const lineIndex = parseInt( - (ev.target as HTMLDivElement).id.split("-").pop() || "-1", - ); - const forwardLink = getNoteLink(noteItem); - const backLink = getNoteLink(mainNote, { ignore: true, lineIndex }); - addLineToNote( - mainNote, - await addon.api.template.runTemplate( - "[QuickInsertV2]", - "link, linkText, subNoteItem, noteItem", - [ - forwardLink, - noteItem.getNoteTitle().trim() || forwardLink, - noteItem, - mainNote, - ], - ), - lineIndex, - ); - addLineToNote( - noteItem, - await addon.api.template.runTemplate( - "[QuickBackLinkV2]", - "link, linkText, subNoteItem, noteItem", - [ - backLink, - mainNote.getNoteTitle().trim() || "Workspace Note", - noteItem, - mainNote, - "", - ], - ), - ); - onExitMenu(ev); - ev.stopPropagation(); + // const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null; + // if (!mainNote?.isNote()) { + // return; + // } + // const lineIndex = parseInt( + // (ev.target as HTMLDivElement).id.split("-").pop() || "-1", + // ); + // const forwardLink = getNoteLink(noteItem); + // const backLink = getNoteLink(mainNote, { ignore: true, lineIndex }); + // addLineToNote( + // mainNote, + // await addon.api.template.runTemplate( + // "[QuickInsertV2]", + // "link, linkText, subNoteItem, noteItem", + // [ + // forwardLink, + // noteItem.getNoteTitle().trim() || forwardLink, + // noteItem, + // mainNote, + // ], + // ), + // lineIndex, + // ); + // addLineToNote( + // noteItem, + // await addon.api.template.runTemplate( + // "[QuickBackLinkV2]", + // "link, linkText, subNoteItem, noteItem", + // [ + // backLink, + // mainNote.getNoteTitle().trim() || "Workspace Note", + // noteItem, + // mainNote, + // "", + // ], + // ), + // ); + // onExitMenu(ev); + // ev.stopPropagation(); }; const linkButton = await registerEditorToolbarDropdown( @@ -310,43 +310,44 @@ export async function initEditorToolbar(editor: Zotero.EditorInstance) { } function getLinkMenuData(editor: Zotero.EditorInstance): PopupData[] { - const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId) || null; - const currentNote = editor._item; - if (!workspaceNote?.isNote()) { - return [ - { - id: makeId("link-popup-nodata"), - text: getString("editor.toolbar.link.popup.nodata"), - }, - ]; - } - const nodes = getNoteTreeFlattened(workspaceNote, { - keepLink: true, - }); - const menuData: PopupData[] = []; - for (const node of nodes) { - if (node.model.level === 7) { - const lastMenu = - menuData.length > 0 ? menuData[menuData.length - 1] : null; - const linkNote = getNoteLinkParams(node.model.link).noteItem; - if (linkNote && linkNote.id === currentNote.id && lastMenu) { - lastMenu.suffix = "🔗"; - } - continue; - } - menuData.push({ - id: makeId( - `link-popup-${ - getPref("editor.link.insertPosition") - ? node.model.lineIndex - 1 - : node.model.endIndex - }`, - ), - text: node.model.name, - prefix: "·".repeat(node.model.level - 1), - }); - } - return menuData; + return []; + // const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId) || null; + // const currentNote = editor._item; + // if (!workspaceNote?.isNote()) { + // return [ + // { + // id: makeId("link-popup-nodata"), + // text: getString("editor.toolbar.link.popup.nodata"), + // }, + // ]; + // } + // const nodes = getNoteTreeFlattened(workspaceNote, { + // keepLink: true, + // }); + // const menuData: PopupData[] = []; + // for (const node of nodes) { + // if (node.model.level === 7) { + // const lastMenu = + // menuData.length > 0 ? menuData[menuData.length - 1] : null; + // const linkNote = getNoteLinkParams(node.model.link).noteItem; + // if (linkNote && linkNote.id === currentNote.id && lastMenu) { + // lastMenu.suffix = "🔗"; + // } + // continue; + // } + // menuData.push({ + // id: makeId( + // `link-popup-${ + // getPref("editor.link.insertPosition") + // ? node.model.lineIndex - 1 + // : node.model.endIndex + // }`, + // ), + // text: node.model.name, + // prefix: "·".repeat(node.model.level - 1), + // }); + // } + // return menuData; } async function registerEditorToolbar( diff --git a/src/modules/sync/managerWindow.ts b/src/modules/sync/managerWindow.ts index 493b783..ee17f5f 100644 --- a/src/modules/sync/managerWindow.ts +++ b/src/modules/sync/managerWindow.ts @@ -102,7 +102,7 @@ export async function showSyncManager() { // @ts-ignore TODO: Fix type in zotero-plugin-toolkit .setProp("onColumnSort", (columnIndex, ascending) => { addon.data.sync.manager.columnIndex = columnIndex; - addon.data.sync.manager.columnAscending = ascending > 0; + addon.data.sync.manager.columnAscending = ascending; refresh(); }) .render(); diff --git a/src/modules/template/picker.ts b/src/modules/template/picker.ts index 063b031..ccb2383 100644 --- a/src/modules/template/picker.ts +++ b/src/modules/template/picker.ts @@ -77,7 +77,7 @@ function getTemplatePromptHandler(name: string) { async function insertTemplateCallback(name: string) { const targetNoteItem = Zotero.Items.get( - addon.data.template.picker.data.noteId || addon.data.workspace.mainId, + addon.data.template.picker.data.noteId, ); let html = ""; if (name.toLowerCase().startsWith("[item]")) { diff --git a/src/modules/template/preview.ts b/src/modules/template/preview.ts index b9631b5..8f76010 100644 --- a/src/modules/template/preview.ts +++ b/src/modules/template/preview.ts @@ -78,15 +78,16 @@ async function renderTemplatePreview( const link = getNoteLink(data); const linkText = data.getNoteTitle().trim() || link; const subNoteItem = data; - const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - html = await addon.api.template.runTemplate( - templateName, - "link, linkText, subNoteItem, noteItem", - [link, linkText, subNoteItem, noteItem], - { - dryRun: true, - }, - ); + return ""; + // const noteItem = Zotero.Items.get(addon.data.workspace.mainId); + // html = await addon.api.template.runTemplate( + // templateName, + // "link, linkText, subNoteItem, noteItem", + // [link, linkText, subNoteItem, noteItem], + // { + // dryRun: true, + // }, + // ); } } else if (templateName.includes("QuickBackLink")) { // link, linkText, subNoteItem, noteItem @@ -94,18 +95,19 @@ async function renderTemplatePreview( if (!data) { html = "

No note item selected

"; } else { - const link = getNoteLink(data); - const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - const linkText = noteItem.getNoteTitle().trim() || "Workspace Note"; - const subNoteItem = data; - html = await addon.api.template.runTemplate( - templateName, - "link, linkText, subNoteItem, noteItem", - [link, linkText, subNoteItem, noteItem], - { - dryRun: true, - }, - ); + return ""; + // const link = getNoteLink(data); + // const noteItem = Zotero.Items.get(addon.data.workspace.mainId); + // const linkText = noteItem.getNoteTitle().trim() || "Workspace Note"; + // const subNoteItem = data; + // html = await addon.api.template.runTemplate( + // templateName, + // "link, linkText, subNoteItem, noteItem", + // [link, linkText, subNoteItem, noteItem], + // { + // dryRun: true, + // }, + // ); } } else if (templateName.includes("QuickImport")) { // link, noteItem @@ -113,16 +115,17 @@ async function renderTemplatePreview( if (!data) { html = "

No note item selected

"; } else { - const link = getNoteLink(data); - const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - html = await addon.api.template.runTemplate( - templateName, - "link, noteItem", - [link, noteItem], - { - dryRun: true, - }, - ); + return ""; + // const link = getNoteLink(data); + // const noteItem = Zotero.Items.get(addon.data.workspace.mainId); + // html = await addon.api.template.runTemplate( + // templateName, + // "link, noteItem", + // [link, noteItem], + // { + // dryRun: true, + // }, + // ); } } else if (templateName.includes("QuickNote")) { // annotationItem, topItem, noteItem From 75ca707edcf6245ecf3726d8eccf9cf6ef64f6b4 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:38:43 +0800 Subject: [PATCH 04/23] add: restore note tabs --- src/hooks.ts | 4 ++++ src/modules/workspace/tab.ts | 29 ++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index 88fdcb7..7c52a59 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -12,6 +12,7 @@ import { registerWorkspaceTab, openWorkspaceTab, onTabSelect, + restoreNoteTabs, } from "./modules/workspace/tab"; import { initWorkspace } from "./modules/workspace/content"; import { registerNotify } from "./modules/notify"; @@ -81,6 +82,8 @@ async function onMainWindowLoad(win: Window): Promise { initTemplates(); patchViewItems(win); + + restoreNoteTabs(); } async function onMainWindowUnload(win: Window): Promise { @@ -255,4 +258,5 @@ export default { onShowTemplateEditor, onCreateNoteFromTemplate, onCreateNoteFromMD, + restoreNoteTabs, }; diff --git a/src/modules/workspace/tab.ts b/src/modules/workspace/tab.ts index d443f83..214be70 100644 --- a/src/modules/workspace/tab.ts +++ b/src/modules/workspace/tab.ts @@ -52,26 +52,32 @@ export function registerWorkspaceTab(win: Window) { ); } -export async function openWorkspaceTab(item: Zotero.Item) { +export async function openWorkspaceTab( + item: Zotero.Item, + options: { select?: boolean; index?: number } = { + select: true, + }, +) { + const { select, index } = options; + if (!item) return; const currentTab = Zotero_Tabs._tabs.find( (tab) => tab.data?.itemID == item.id, ); if (currentTab) { - Zotero_Tabs.select(currentTab.id); + if (select) Zotero_Tabs.select(currentTab.id); return; } const { id, container } = Zotero_Tabs.add({ type: TAB_TYPE, title: item.getNoteTitle(), - index: 1, + index, data: { itemID: item.id, }, - select: false, + select, onClose: () => {}, }); initWorkspace(container, item); - Zotero_Tabs.select(id); } let contextPaneOpen: boolean | undefined = undefined; @@ -91,3 +97,16 @@ export function onTabSelect(tabType: string) { } ZoteroContextPane.update(); } + +export function restoreNoteTabs() { + const tabsCache: _ZoteroTypes.TabInstance[] = + Zotero.Session.state.windows.find((x: any) => x.type == "pane")?.tabs; + for (const i in tabsCache) { + const tab = tabsCache[i]; + if (tab.type !== TAB_TYPE) continue; + openWorkspaceTab(Zotero.Items.get(tab.data.itemID), { + select: tab.selected, + index: Number(i), + }); + } +} From acaef3d50731dd29772484f22425dbf9ce1f16e5 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:58:09 +0800 Subject: [PATCH 05/23] fix: sync manager list sorting --- package.json | 2 +- src/modules/sync/managerWindow.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d99b862..5639361 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-toolkit": "^2.3.26" + "zotero-plugin-toolkit": "^2.3.28" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", diff --git a/src/modules/sync/managerWindow.ts b/src/modules/sync/managerWindow.ts index ee17f5f..493b783 100644 --- a/src/modules/sync/managerWindow.ts +++ b/src/modules/sync/managerWindow.ts @@ -102,7 +102,7 @@ export async function showSyncManager() { // @ts-ignore TODO: Fix type in zotero-plugin-toolkit .setProp("onColumnSort", (columnIndex, ascending) => { addon.data.sync.manager.columnIndex = columnIndex; - addon.data.sync.manager.columnAscending = ascending; + addon.data.sync.manager.columnAscending = ascending > 0; refresh(); }) .render(); From 3579a5b583e3d6bb69104c30a6f8d24f7f8b7bc2 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:55:36 +0800 Subject: [PATCH 06/23] fix: workspace context pane editable --- src/elements/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/context.ts b/src/elements/context.ts index febba36..dee1d8d 100644 --- a/src/elements/context.ts +++ b/src/elements/context.ts @@ -37,7 +37,7 @@ export class ContextPane extends PluginCEBase { render() { if (!this.item) return; - this._details.mode = this.item.isEditable() ? null : "view"; + this._details.editable = this.item.isEditable(); this._details.item = this.item; this._details.parentID = this.item.parentID; this._details.sidenav = this._sidenav; From 1287ccc9002b468edd7089a7f20ffb0a0d826503 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Thu, 28 Mar 2024 21:01:11 +0800 Subject: [PATCH 07/23] fix: fx115 context pane --- src/modules/workspace/tab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/workspace/tab.ts b/src/modules/workspace/tab.ts index 214be70..0c53408 100644 --- a/src/modules/workspace/tab.ts +++ b/src/modules/workspace/tab.ts @@ -84,7 +84,7 @@ let contextPaneOpen: boolean | undefined = undefined; export function onTabSelect(tabType: string) { const ZoteroContextPane = ztoolkit.getGlobal("ZoteroContextPane"); - const splitter = ZoteroContextPane.getSplitter(); + const splitter = ZoteroContextPane.splitter; if (tabType === TAB_TYPE) { contextPaneOpen = splitter.getAttribute("state") != "collapsed"; From d8e29cedc8deffa5649db9cee11483601c895659 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Thu, 28 Mar 2024 21:01:22 +0800 Subject: [PATCH 08/23] fix: template preview --- src/modules/template/preview.ts | 65 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/src/modules/template/preview.ts b/src/modules/template/preview.ts index 8f76010..d5a0440 100644 --- a/src/modules/template/preview.ts +++ b/src/modules/template/preview.ts @@ -78,16 +78,15 @@ async function renderTemplatePreview( const link = getNoteLink(data); const linkText = data.getNoteTitle().trim() || link; const subNoteItem = data; - return ""; - // const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - // html = await addon.api.template.runTemplate( - // templateName, - // "link, linkText, subNoteItem, noteItem", - // [link, linkText, subNoteItem, noteItem], - // { - // dryRun: true, - // }, - // ); + const noteItem = new Zotero.Item("note"); + html = await addon.api.template.runTemplate( + templateName, + "link, linkText, subNoteItem, noteItem", + [link, linkText, subNoteItem, noteItem], + { + dryRun: true, + }, + ); } } else if (templateName.includes("QuickBackLink")) { // link, linkText, subNoteItem, noteItem @@ -95,19 +94,18 @@ async function renderTemplatePreview( if (!data) { html = "

No note item selected

"; } else { - return ""; - // const link = getNoteLink(data); - // const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - // const linkText = noteItem.getNoteTitle().trim() || "Workspace Note"; - // const subNoteItem = data; - // html = await addon.api.template.runTemplate( - // templateName, - // "link, linkText, subNoteItem, noteItem", - // [link, linkText, subNoteItem, noteItem], - // { - // dryRun: true, - // }, - // ); + const link = getNoteLink(data); + const noteItem = new Zotero.Item("note"); + const linkText = noteItem.getNoteTitle().trim() || "Workspace Note"; + const subNoteItem = data; + html = await addon.api.template.runTemplate( + templateName, + "link, linkText, subNoteItem, noteItem", + [link, linkText, subNoteItem, noteItem], + { + dryRun: true, + }, + ); } } else if (templateName.includes("QuickImport")) { // link, noteItem @@ -115,17 +113,16 @@ async function renderTemplatePreview( if (!data) { html = "

No note item selected

"; } else { - return ""; - // const link = getNoteLink(data); - // const noteItem = Zotero.Items.get(addon.data.workspace.mainId); - // html = await addon.api.template.runTemplate( - // templateName, - // "link, noteItem", - // [link, noteItem], - // { - // dryRun: true, - // }, - // ); + const link = getNoteLink(data); + const noteItem = new Zotero.Item("note"); + html = await addon.api.template.runTemplate( + templateName, + "link, noteItem", + [link, noteItem], + { + dryRun: true, + }, + ); } } else if (templateName.includes("QuickNote")) { // annotationItem, topItem, noteItem From 9e2b4c583dbce6fffe3548a0d0004ceb715170dc Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sat, 6 Apr 2024 11:23:24 +0800 Subject: [PATCH 09/23] add: linkNote dialog --- addon/chrome/content/linkNote.xhtml | 98 +++ addon/chrome/content/styles/linkNote.css | 33 + addon/chrome/content/styles/notePicker.css | 40 ++ addon/chrome/content/styles/toolbar.css | 48 ++ addon/locale/en-US/addon.ftl | 6 +- addon/locale/it-IT/addon.ftl | 6 +- addon/locale/ru-RU/addon.ftl | 6 +- addon/locale/tr-TR/addon.ftl | 6 +- addon/prefs.js | 2 + src/api.ts | 2 + src/elements/base.ts | 10 +- src/elements/notePicker.ts | 304 +++++++++ src/elements/outlinePane.ts | 14 +- .../{workspace.ts => customElements.ts} | 2 + src/extras/linkNote.ts | 325 ++++++++++ src/hooks.ts | 7 +- src/modules/convert/api.ts | 4 +- src/modules/editor/toolbar.ts | 613 ++++++------------ src/modules/export/markdown.ts | 2 +- src/modules/export/pdf.ts | 1 - src/modules/relatedNotes.ts | 31 + src/modules/sync/managerWindow.ts | 1 - src/modules/template/api.ts | 4 +- src/utils/config.ts | 20 +- src/utils/linkNote.ts | 42 ++ src/utils/note.ts | 111 ++-- 26 files changed, 1237 insertions(+), 501 deletions(-) create mode 100644 addon/chrome/content/linkNote.xhtml create mode 100644 addon/chrome/content/styles/linkNote.css create mode 100644 addon/chrome/content/styles/notePicker.css create mode 100644 addon/chrome/content/styles/toolbar.css create mode 100644 src/elements/notePicker.ts rename src/extras/{workspace.ts => customElements.ts} (86%) create mode 100644 src/extras/linkNote.ts create mode 100644 src/modules/relatedNotes.ts create mode 100644 src/utils/linkNote.ts diff --git a/addon/chrome/content/linkNote.xhtml b/addon/chrome/content/linkNote.xhtml new file mode 100644 index 0000000..034ecb7 --- /dev/null +++ b/addon/chrome/content/linkNote.xhtml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Step 2. Insert to: + + + + + + + + + + + + + + + + + + + + + Step 3. Preview: + + + + + + + + + + + + + + + diff --git a/addon/chrome/content/styles/linkNote.css b/addon/chrome/content/styles/linkNote.css new file mode 100644 index 0000000..1403699 --- /dev/null +++ b/addon/chrome/content/styles/linkNote.css @@ -0,0 +1,33 @@ +.container { + height: 100%; + margin: 0; +} + +#top-container { + gap: 16px; + overflow: auto; + width: 800px; + padding: 2em; +} + +bn-note-picker { + border: var(--material-border); + min-width: 600px; + height: 500px; +} + +#bn-select-note-outline-container { + border: var(--material-border); + min-width: 300px; + height: 500px; +} + +#bn-note-preview-container { + border: var(--material-border); + min-width: 450px; + height: 500px; +} + +#bn-link-insert-position-container { + align-items: center; +} diff --git a/addon/chrome/content/styles/notePicker.css b/addon/chrome/content/styles/notePicker.css new file mode 100644 index 0000000..e3347e1 --- /dev/null +++ b/addon/chrome/content/styles/notePicker.css @@ -0,0 +1,40 @@ +bn-note-picker { + flex-direction: column; +} + +.container { + height: 100%; + margin: 0; +} + +#select-items-dialog #zotero-select-items-container { + gap: 0; +} + +#collections-items-container { + display: flex; + height: 100%; + 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; +} diff --git a/addon/chrome/content/styles/toolbar.css b/addon/chrome/content/styles/toolbar.css new file mode 100644 index 0000000..aa3f5af --- /dev/null +++ b/addon/chrome/content/styles/toolbar.css @@ -0,0 +1,48 @@ +.toolbar { + background: var(--material-toolbar); + border-bottom: var(--material-border); + padding: 6px; + align-items: center; + justify-content: space-between !important; +} + +.toolbar-start, +.toolbar-middle, +.toolbar-end { + align-items: center; +} + +.toolbar-header { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; + color: var(--fill-secondary); + text-overflow: ellipsis; + overflow-wrap: anywhere; + line-break: anywhere; + font-size: calc(var(--zotero-font-size) * 1.2); +} + +.toolbar-header.content { + flex-shrink: 0; +} + +.toolbar-header.highlight { + font-size: var(--zotero-font-size); + padding: 4px; + 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); + transition: all 0.3s ease; +} +.toolbar-header.highlight:hover { + box-shadow: 0 5px 15px + color-mix(in srgb, var(--material-background) 20%, transparent); + background: var(--color-background50); +} +.toolbar-header.highlight:empty { + display: none; +} diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl index d532e07..663c97d 100644 --- a/addon/locale/en-US/addon.ftl +++ b/addon/locale/en-US/addon.ftl @@ -78,9 +78,9 @@ editor-toolbar-settings-openWorkspace = Open Note Workspace editor-toolbar-settings-setWorkspace = Set as Workspace Note editor-toolbar-settings-previewInWorkspace = Preview in Workspace editor-toolbar-settings-showInLibrary = Show in Library -editor-toolbar-settings-insertTemplate = Insert Template to Cursor Line -editor-toolbar-settings-copyLink = Copy Note Link at Line ({ $line }) -editor-toolbar-settings-copyLinkAtSection = Copy Note Link at Section ({ $section }) +editor-toolbar-settings-insertTemplate = Insert template +editor-toolbar-settings-copyLink = Copy link (L{ $line }) +editor-toolbar-settings-copyLinkAtSection = Copy link (Sec. { $section }) editor-toolbar-settings-openParent = Open Attachment editor-toolbar-settings-export = Export Current Note... editor-toolbar-settings-refreshSyncing = Sync Now diff --git a/addon/locale/it-IT/addon.ftl b/addon/locale/it-IT/addon.ftl index 8fb74dc..f8b9091 100644 --- a/addon/locale/it-IT/addon.ftl +++ b/addon/locale/it-IT/addon.ftl @@ -74,9 +74,9 @@ editor-toolbar-settings-openWorkspace = Apri nota di lavoro editor-toolbar-settings-setWorkspace = Imposta come nota di lavoro editor-toolbar-settings-previewInWorkspace = Anteprima nello spazio di lavoro editor-toolbar-settings-showInLibrary = Show in Library -editor-toolbar-settings-insertTemplate = Inserisci template nella posizione del cursore -editor-toolbar-settings-copyLink = Copia link della nota alla riga ({ $line }) -editor-toolbar-settings-copyLinkAtSection = Copia link della nota alla sezione ({ $section }) +editor-toolbar-settings-insertTemplate = Inserisci template +editor-toolbar-settings-copyLink = Copia link (L{ $line }) +editor-toolbar-settings-copyLinkAtSection = Copia link (Sec. { $section }) editor-toolbar-settings-openParent = Apri allegato editor-toolbar-settings-export = Esporta nota corrente... editor-toolbar-settings-refreshSyncing = Sincronizza ora diff --git a/addon/locale/ru-RU/addon.ftl b/addon/locale/ru-RU/addon.ftl index 695aab1..7f509c6 100644 --- a/addon/locale/ru-RU/addon.ftl +++ b/addon/locale/ru-RU/addon.ftl @@ -78,9 +78,9 @@ editor-toolbar-settings-openWorkspace=Открыть пространство з editor-toolbar-settings-setWorkspace=Установить как Заметку раб. пространства editor-toolbar-settings-previewInWorkspace=Предпросмотр в рабочем пространстве editor-toolbar-settings-showInLibrary = Show in Library -editor-toolbar-settings-insertTemplate=Вставить шаблон в строку курсора -editor-toolbar-settings-copyLink = Копировать Ссылку на Заметку на строке ({ $line }) -editor-toolbar-settings-copyLinkAtSection = Копировать Ссылку на Заметку в секции ({ $section }) +editor-toolbar-settings-insertTemplate=Вставить шаблон +editor-toolbar-settings-copyLink = Копировать Ссылку (L{ $line }) +editor-toolbar-settings-copyLinkAtSection = Копировать Ссылку (Sec. { $section }) editor-toolbar-settings-openParent=Открыть вложение editor-toolbar-settings-export=Экспортировать текущую заметку... editor-toolbar-settings-refreshSyncing=Синхронизировать сейчас diff --git a/addon/locale/tr-TR/addon.ftl b/addon/locale/tr-TR/addon.ftl index 9764fa2..0bd255b 100644 --- a/addon/locale/tr-TR/addon.ftl +++ b/addon/locale/tr-TR/addon.ftl @@ -78,9 +78,9 @@ editor-toolbar-settings-openWorkspace = Not Çalışma Alanı Açın editor-toolbar-settings-setWorkspace = Çalışma Alanı Notu Olarak Ayarla editor-toolbar-settings-previewInWorkspace = Çalışma Alanında Ön İzle Preview in Workspace editor-toolbar-settings-showInLibrary = Show in Library -editor-toolbar-settings-insertTemplate = İşaretçi Satırına Şablon Ekle -editor-toolbar-settings-copyLink = Satıra Not Linki Kopyala ({ $line }) -editor-toolbar-settings-copyLinkAtSection = Bölüme Not Linki Kopyala ({ $section }) +editor-toolbar-settings-insertTemplate = Insert template +editor-toolbar-settings-copyLink = Copy link (L{ $line }) +editor-toolbar-settings-copyLinkAtSection =Copy link (Sec. { $section }) editor-toolbar-settings-openParent = Eki Aç editor-toolbar-settings-export = Bu Notu Dışa Aktar... editor-toolbar-settings-refreshSyncing = Senkronize Et diff --git a/addon/prefs.js b/addon/prefs.js index 95b1609..34e1fdb 100644 --- a/addon/prefs.js +++ b/addon/prefs.js @@ -6,6 +6,8 @@ pref("__prefsPrefix__.syncAttachmentFolder", "attachments"); pref("__prefsPrefix__.autoAnnotation", false); +pref("__prefsPrefix__.insertLinkPosition", "end"); + pref("__prefsPrefix__.embedLink", true); pref("__prefsPrefix__.standaloneLink", false); pref("__prefsPrefix__.keepLink", true); diff --git a/src/api.ts b/src/api.ts index 5bbabaf..9feb17b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -65,6 +65,7 @@ import { updateRelatedNotes, getRelatedNoteIds, getNoteTreeFlattened, + getLinesInNote, } from "./utils/note"; const workspace = {}; @@ -144,6 +145,7 @@ const editor = { const note = { insert: addLineToNote, + getLinesInNote, updateRelatedNotes, getRelatedNoteIds, getNoteTreeFlattened, diff --git a/src/elements/base.ts b/src/elements/base.ts index 76e10b7..65627d8 100644 --- a/src/elements/base.ts +++ b/src/elements/base.ts @@ -11,14 +11,14 @@ export class PluginCEBase extends XULElementBase { _wrapID(key: string) { if (key.startsWith(config.addonRef)) { - return key; + return key; } return `${config.addonRef}-${key}`; } _unwrapID(id: string) { if (id.startsWith(config.addonRef)) { - return id.slice(config.addonRef.length + 1); + return id.slice(config.addonRef.length + 1); } return id; } @@ -28,9 +28,9 @@ export class PluginCEBase extends XULElementBase { } _parseContentID(dom: DocumentFragment) { - dom.querySelectorAll("*[id]").forEach(elem => { - elem.id = this._wrapID(elem.id); - }) + dom.querySelectorAll("*[id]").forEach((elem) => { + elem.id = this._wrapID(elem.id); + }); return dom; } diff --git a/src/elements/notePicker.ts b/src/elements/notePicker.ts new file mode 100644 index 0000000..bd092c6d --- /dev/null +++ b/src/elements/notePicker.ts @@ -0,0 +1,304 @@ +import { config } from "../../package.json"; +import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable"; +import { PluginCEBase } from "./base"; + +const _require = window.require; +const CollectionTree = _require("chrome://zotero/content/collectionTree.js"); +const ItemTree = _require("chrome://zotero/content/itemTree.js"); +const { getCSSItemTypeIcon } = _require("components/icons"); + +export class NotePicker extends PluginCEBase { + itemsView!: _ZoteroTypes.ItemTree; + collectionsView!: _ZoteroTypes.CollectionTree; + openedNotesView!: VirtualizedTableHelper; + + openedNotes: Zotero.Item[] = []; + + activeSelectionType: "library" | "tabs" | "none" = "none"; + + get content() { + return MozXULElement.parseXULToFragment(` + + + + + + + + + + + + + + + + + + + + + + + + + + + +`); + } + + set openedNoteIDs(ids: number[]) { + this.openedNotes = Zotero.Items.get(ids).filter((item) => item.isNote()); + if (this.openedNotesView) { + this.openedNotesView.render(); + return; + } + this.loadOpenedNotes(); + } + + async init() { + await this.loadLibraryNotes(); + this.loadQuickSearch(); + await this.loadOpenedNotes(); + + window.addEventListener("unload", () => { + this.destroy(); + }); + } + + destroy(): void { + this.collectionsView.unregister(); + if (this.itemsView) this.itemsView.unregister(); + } + + async loadLibraryNotes() { + this.itemsView = await ItemTree.init( + this.querySelector("#zotero-items-tree"), + { + onSelectionChange: () => { + this.onItemSelected(); + }, + id: "select-items-dialog", + dragAndDrop: false, + persistColumns: true, + columnPicker: true, + emptyMessage: Zotero.getString("pane.items.loading"), + }, + ); + this.itemsView.isSelectable = (index: number, selectAll = false) => { + const row = this.itemsView.getRow(index); + if (!row) { + return false; + } + // @ts-ignore + if (!row.ref.isNote()) return false; + if (this.itemsView.collectionTreeRow.isTrash()) { + // @ts-ignore + return row.ref.deleted; + } else { + // @ts-ignore + return this.itemsView._searchItemIDs.has(row.id); + } + }; + this.itemsView.setItemsPaneMessage(Zotero.getString("pane.items.loading")); + + // Wait otherwise the collection tree will not be initialized + await Zotero.Promise.delay(10); + this.collectionsView = await CollectionTree.init( + this.querySelector("#zotero-collections-tree"), + { + onSelectionChange: Zotero.Utilities.debounce( + () => this.onCollectionSelected(), + 100, + ), + }, + ); + this.collectionsView.hideSources = ["duplicates", "trash", "feeds"]; + + await this.collectionsView.makeVisible(); + } + + loadQuickSearch() { + // @ts-ignore + const searchBox = document.createXULElement("quick-search-textbox"); + searchBox.id = "zotero-tb-search"; + searchBox.setAttribute("timeout", "250"); + searchBox.setAttribute("dir", "reverse"); + searchBox.addEventListener("command", this.onSearch); + this.querySelector("#search-toolbar > .toolbar-end")?.appendChild( + searchBox, + ); + + Zotero.updateQuickSearchBox(document); + } + + async loadOpenedNotes() { + const renderLock = Zotero.Promise.defer(); + this.openedNotesView = new VirtualizedTableHelper(window) + .setContainerId("bn-select-opened-notes-tree") + .setProp({ + id: `bn-select-opened-notes-table`, + columns: [ + { + dataKey: "title", + label: "Opened Notes", + flex: 1, + }, + ], + showHeader: true, + multiSelect: false, + staticColumns: true, + disableFontSizeScaling: true, + }) + .setProp("getRowCount", () => this.openedNotes.length || 0) + .setProp("getRowData", (index) => { + const note = this.openedNotes[index]; + return { + title: note.getNoteTitle(), + }; + }) + .setProp("onSelectionChange", (selection) => { + this.onOpenedNoteSelected(); + }) + // For find-as-you-type + .setProp( + "getRowString", + (index) => this.openedNotes[index].getNoteTitle() || "", + ) + .setProp("renderItem", (index, selection, oldElem, columns) => { + let div; + if (oldElem) { + div = oldElem; + div.innerHTML = ""; + } else { + div = document.createElement("div"); + div.className = "row"; + } + + div.classList.toggle("selected", selection.isSelected(index)); + div.classList.toggle("focused", selection.focused == index); + const rowData = this.openedNotes[index]; + + for (const column of columns) { + const span = document.createElement("span"); + // @ts-ignore + span.className = `cell ${column?.className}`; + span.textContent = rowData.getNoteTitle(); + const icon = getCSSItemTypeIcon("note"); + icon.classList.add("cell-icon"); + span.prepend(icon); + div.append(span); + } + return div; + }) + .render(-1, () => { + renderLock.resolve(); + }); + await renderLock.promise; + + if (this.openedNotes.length === 1) { + this.openedNotesView.treeInstance.selection.select(0); + } + } + + onSearch() { + if (this.itemsView) { + const searchVal = ( + this.querySelector("#zotero-tb-search-textbox") as HTMLInputElement + )?.value; + this.itemsView.setFilter("search", searchVal); + } + } + + async onCollectionSelected() { + const collectionTreeRow = this.collectionsView.getRow( + this.collectionsView.selection.focused, + ); + if (!this.collectionsView.selection.count) return; + // Collection not changed + if ( + this.itemsView && + this.itemsView.collectionTreeRow && + this.itemsView.collectionTreeRow.id == collectionTreeRow.id + ) { + return; + } + // @ts-ignore + if (!collectionTreeRow._bnPatched) { + // @ts-ignore + collectionTreeRow._bnPatched = true; + const getItems = collectionTreeRow.getItems.bind(collectionTreeRow); + // @ts-ignore + collectionTreeRow.getItems = async function () { + const items = (await getItems()) as Zotero.Item[]; + return items.filter((item) => item.isNote()) as unknown[]; + }; + } + collectionTreeRow.setSearch(""); + Zotero.Prefs.set("lastViewedFolder", collectionTreeRow.id); + + this.itemsView.setItemsPaneMessage(Zotero.getString("pane.items.loading")); + + // Load library data if necessary + const library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID); + if (library) { + if (!library.getDataLoaded("item")) { + Zotero.debug( + "Waiting for items to load for library " + library.libraryID, + ); + await library.waitForDataLoad("item"); + } + } + + await this.itemsView.changeCollectionTreeRow(collectionTreeRow); + + this.itemsView.clearItemsPaneMessage(); + + this.collectionsView.runListeners("select"); + } + + onItemSelected() { + this.activeSelectionType = "library"; + this.dispatchSelectionChange(); + } + + onOpenedNoteSelected() { + this.activeSelectionType = "tabs"; + this.dispatchSelectionChange(); + } + + dispatchSelectionChange() { + this.dispatchEvent( + new CustomEvent("selectionChange", { + detail: { + selectedNote: this.getSelectedNotes()[0], + }, + }), + ); + } + + getSelectedNotes(): 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], + ); + } +} diff --git a/src/elements/outlinePane.ts b/src/elements/outlinePane.ts index 81d816e..43f6785 100644 --- a/src/elements/outlinePane.ts +++ b/src/elements/outlinePane.ts @@ -132,7 +132,9 @@ export class OutlinePane extends PluginCEBase { init(): void { MozXULElement.insertFTLIfNeeded(`${config.addonRef}-outline.ftl`); - this._outlineContainer = this._queryID("outline") as unknown as HTMLIFrameElement; + this._outlineContainer = this._queryID( + "outline", + ) as unknown as HTMLIFrameElement; this._queryID("left-toolbar")?.addEventListener( "command", @@ -163,6 +165,7 @@ export class OutlinePane extends PluginCEBase { extraData: { [key: string]: any }, ) { if (!this.item) return; + if (extraData.skipBN) return; if (event === "modify" && type === "item") { if ((ids as number[]).includes(this.item.id)) { this.updateOutline(); @@ -188,10 +191,15 @@ export class OutlinePane extends PluginCEBase { this.messageHandler, ); - this._outlineContainer.setAttribute("src", OutlinePane.outlineSources[this.outlineType]); + this._outlineContainer.setAttribute( + "src", + OutlinePane.outlineSources[this.outlineType], + ); await waitUtilAsync( - () => this._outlineContainer.contentWindow?.document.readyState === "complete", + () => + this._outlineContainer.contentWindow?.document.readyState === + "complete", ); this._outlineContainer.contentWindow?.addEventListener( "message", diff --git a/src/extras/workspace.ts b/src/extras/customElements.ts similarity index 86% rename from src/extras/workspace.ts rename to src/extras/customElements.ts index d629838..69e9fa4 100644 --- a/src/extras/workspace.ts +++ b/src/extras/customElements.ts @@ -1,5 +1,6 @@ import { ContextPane } from "../elements/context"; import { NoteDetails } from "../elements/detailsPane"; +import { NotePicker } from "../elements/notePicker"; import { OutlinePane } from "../elements/outlinePane"; import { Workspace } from "../elements/workspace"; @@ -8,6 +9,7 @@ const elements = { "bn-outline": OutlinePane, "bn-details": NoteDetails as unknown as CustomElementConstructor, "bn-workspace": Workspace, + "bn-note-picker": NotePicker, }; for (const [key, constructor] of Object.entries(elements)) { diff --git a/src/extras/linkNote.ts b/src/extras/linkNote.ts new file mode 100644 index 0000000..93c1615 --- /dev/null +++ b/src/extras/linkNote.ts @@ -0,0 +1,325 @@ +import { VirtualizedTableHelper } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable"; +import { config } from "../../package.json"; +import Addon from "../addon"; +import { waitUtilAsync } from "../utils/wait"; +import { getPref, setPref } from "../utils/prefs"; +import { NotePicker } from "../elements/notePicker"; + +let initialized = false; + +let notePicker: NotePicker; + +let noteOutlineView: VirtualizedTableHelper; +let currentNote: Zotero.Item; +let targetNote: Zotero.Item | undefined; +let noteOutline: ReturnType = []; + +let positionData: NoteNodeData | undefined; + +// @ts-ignore +window.addon = Zotero[config.addonRef]; + +let io: { + currentNoteID: number; + openedNoteIDs?: number[]; + deferred: _ZoteroTypes.DeferredPromise; + + targetNoteID?: number; + content?: string; + lineIndex?: number; +}; + +window.onload = async function () { + // Set font size from pref + const sbc = document.getElementById("top-container"); + Zotero.UIProperties.registerRoot(sbc); + + // @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(); +}; + +function loadNotePicker() { + notePicker = document.querySelector("bn-note-picker") as NotePicker; + notePicker.openedNoteIDs = io.openedNoteIDs || []; + const content = document.createElement("span"); + content.innerHTML = "Step 1. Choose target note:"; + content.classList.add("toolbar-header", "content"); + const title = document.createElement("span"); + title.id = "selected-note-title"; + title.classList.add("toolbar-header", "highlight"); + notePicker + .querySelector("#search-toolbar .toolbar-start") + ?.append(content, title); + notePicker.addEventListener("selectionChange", (event: any) => { + updateSelectedNotesTitle(event.detail.selectedNote); + updateNoteOutline(event.detail.selectedNote); + }); +} + +function loadInsertPosition() { + const insertPosition = document.getElementById( + "bn-link-insert-position", + ) as HTMLSelectElement; + insertPosition.value = getPref("insertLinkPosition") as string; + insertPosition.addEventListener("command", () => { + setPref("insertLinkPosition", insertPosition.value); + updateNotePreview(); + }); +} + +async function loadNoteOutline() { + const renderLock = Zotero.Promise.defer(); + noteOutlineView = new VirtualizedTableHelper(window) + .setContainerId("bn-select-note-outline-tree") + .setProp({ + id: `bn-select-note-outline-table`, + columns: [ + { + dataKey: "level", + label: "Level", + width: 50, + staticWidth: true, + }, + { + dataKey: "name", + label: "Table of Contents", + flex: 1, + }, + ], + showHeader: true, + multiSelect: false, + staticColumns: true, + disableFontSizeScaling: true, + }) + .setProp("getRowCount", () => noteOutline.length || 0) + .setProp("getRowData", (index) => { + const model = noteOutline[index]?.model; + if (!model) return { level: 0, name: "**Unknown**" }; + return { + level: model.level, + name: "··".repeat(model.level - 1) + model.name, + }; + }) + .setProp("onSelectionChange", (selection) => { + onOutlineSelected(selection); + }) + // For find-as-you-type + .setProp("getRowString", (index) => noteOutline[index]?.model.name || "") + .render(-1, () => { + renderLock.resolve(); + }); + await renderLock.promise; + + // if (openedNotes.length === 1) { + // openedNotesView.treeInstance.selection.select(0); + // } +} + +function onOutlineSelected(selection: { selected: Set }) { + positionData = noteOutline[selection.selected.values().next().value]?.model; + updateNotePreview(); + updateSelectedOutlineTitle(); +} + +function updateSelectedNotesTitle(noteItem?: Zotero.Item) { + const title = noteItem ? noteItem.getNoteTitle() : ""; + document.querySelector("#selected-note-title")!.textContent = title; +} + +function updateSelectedOutlineTitle() { + const selectedOutline = + noteOutline[ + noteOutlineView.treeInstance.selection.selected.values().next().value + ]; + const title = selectedOutline ? selectedOutline.model.name : ""; + document.querySelector("#selected-outline-title")!.textContent = title; +} + +function updatePreviewTitle() { + document.querySelector("#preview-note-from-title")!.textContent = + currentNote.getNoteTitle() || "No title"; + document.querySelector("#preview-note-middle-title")!.textContent = "to"; + document.querySelector("#preview-note-to-title")!.textContent = + targetNote?.getNoteTitle() || "No title"; +} + +async function updateNoteOutline(noteItem?: Zotero.Item) { + if (!noteItem) { + targetNote = undefined; + noteOutline = []; + } else { + targetNote = noteItem; + noteOutline = addon.api.note.getNoteTreeFlattened(targetNote); + } + noteOutlineView?.render(undefined); + // Set default line index to the end of the note + positionData = undefined; + if (targetNote) scrollToSection("outline"); +} + +async function updateNotePreview() { + if (!initialized || !targetNote) return; + const lines = await addon.api.note.getLinesInNote(targetNote, { + convertToHTML: true, + }); + let index = getIndexToInsert(); + if (index < 0) { + index = lines.length; + } else { + scrollToSection("preview"); + } + const before = lines.slice(0, index).join("\n"); + const after = lines.slice(index).join("\n"); + + // TODO: use index or section + const content = await getContentToInsert(); + + const iframe = document.querySelector( + "#bn-note-preview", + ) as HTMLIFrameElement; + + const activeElement = document.activeElement as HTMLElement; // 保存当前活动元素 + + iframe!.contentDocument!.documentElement.innerHTML = ` + + + + + + + + +
${before}
+
${content}
+
${after}
+ + +`; + activeElement?.focus(); + await waitUtilAsync(() => iframe.contentDocument?.readyState === "complete"); + + // Scroll the inserted section into the center of the iframe + const inserted = iframe.contentDocument?.getElementById("inserted"); + if (inserted) { + const rect = inserted.getBoundingClientRect(); + const container = inserted.parentElement!; + container.scrollTo({ + top: + container.scrollTop + + rect.top - + container.clientHeight / 2 + + rect.height, + behavior: "smooth", + }); + } + + updatePreviewTitle(); +} + +function scrollToSection(type: "picker" | "outline" | "preview") { + if (!initialized) return; + const querier = { + picker: "#zotero-select-items-container", + outline: "#bn-select-note-outline-container", + preview: "#bn-note-preview-container", + }; + const container = document.querySelector(querier[type]); + if (!container) return; + container.scrollIntoView({ + behavior: "smooth", + inline: "center", + }); +} + +async function getContentToInsert() { + const forwardLink = addon.api.convert.note2link(currentNote, {}); + const content = await addon.api.template.runTemplate( + "[QuickInsertV2]", + "link, linkText, subNoteItem, noteItem", + [ + forwardLink, + currentNote.getNoteTitle().trim() || forwardLink, + currentNote, + targetNote, + ], + { + dryRun: true, + }, + ); + return content; +} + +function getIndexToInsert() { + if (!positionData) return -1; + let position = getPref("insertLinkPosition") as string; + if (!["start", "end"].includes(position)) { + position = "end"; + } + let index = { + start: positionData.lineIndex + 1, + end: positionData.endIndex + 1, + }[position]; + if (index === undefined) { + index = -1; + } + return index; +} + +async function doAccept() { + if (!targetNote) return; + const content = await getContentToInsert(); + + io.targetNoteID = targetNote.id; + io.content = content; + io.lineIndex = getIndexToInsert(); +} diff --git a/src/hooks.ts b/src/hooks.ts index 7c52a59..493bccc 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -35,6 +35,7 @@ import { waitUtilAsync } from "./utils/wait"; import { initSyncList } from "./modules/sync/api"; import { getPref } from "./utils/prefs"; import { patchViewItems } from "./modules/viewItems"; +import { onUpdateRelated } from "./modules/relatedNotes"; async function onStartup() { await Promise.all([ @@ -67,7 +68,7 @@ async function onMainWindowLoad(win: Window): Promise { await waitUtilAsync(() => document.readyState === "complete"); Services.scriptloader.loadSubScript( - `chrome://${config.addonRef}/content/scripts/workspace.js`, + `chrome://${config.addonRef}/content/scripts/customElements.js`, win, ); // Create ztoolkit for every window @@ -121,6 +122,7 @@ function onNotify( skipActive: true, reason: "item-modify", }); + addon.hooks.onUpdateRelated(modifiedNotes, { skipActive: true }); } } else { return; @@ -164,7 +166,7 @@ function onOpenNote( // addon.hooks.onSetWorkspaceNote(noteId, "preview", options); break; case "workspace": - // addon.hooks.onSetWorkspaceNote(noteId, "main", options); + addon.hooks.onOpenWorkspace(noteItem, "tab"); break; case "standalone": ZoteroPane.openNoteWindow(noteId); @@ -247,6 +249,7 @@ export default { onOpenWorkspace, onToggleWorkspacePane, onSyncing, + onUpdateRelated, onShowTemplatePicker, onUpdateTemplatePicker, onImportTemplateFromClipboard, diff --git a/src/modules/convert/api.ts b/src/modules/convert/api.ts index 013e26e..39b8792 100644 --- a/src/modules/convert/api.ts +++ b/src/modules/convert/api.ts @@ -175,7 +175,7 @@ async function note2noteDiff(noteItem: Zotero.Item) { function note2link( noteItem: Zotero.Item, - options: Parameters[1], + options: Parameters[1] = {}, ) { return getNoteLink(noteItem, options); } @@ -236,7 +236,7 @@ function annotations2html( async function note2html( noteItems: Zotero.Item | Zotero.Item[], - options: { targetNoteItem?: Zotero.Item; html?: string }, + options: { targetNoteItem?: Zotero.Item; html?: string } = {}, ) { if (!Array.isArray(noteItems)) { noteItems = [noteItems]; diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts index 89c0bec..525ab5e 100644 --- a/src/modules/editor/toolbar.ts +++ b/src/modules/editor/toolbar.ts @@ -2,431 +2,212 @@ import { config } from "../../../package.json"; import { ICONS } from "../../utils/config"; import { getLineAtCursor, getSectionAtCursor } from "../../utils/editor"; import { showHint } from "../../utils/hint"; -import { getNoteLink, getNoteLinkParams } from "../../utils/link"; +import { getNoteLink } from "../../utils/link"; import { getString } from "../../utils/locale"; -import { addLineToNote, getNoteTreeFlattened } from "../../utils/note"; -import { getPref } from "../../utils/prefs"; +import { openLinkNoteDialog } from "../../utils/linkNote"; import { slice } from "../../utils/str"; export async function initEditorToolbar(editor: Zotero.EditorInstance) { const noteItem = editor._item; - const toolbar = await registerEditorToolbar(editor, makeId("toolbar")); - // Settings - const settingsButton = await registerEditorToolbarDropdown( + const _document = editor._iframeWindow.document; + registerEditorToolbarElement( editor, - toolbar, - makeId("settings"), - ICONS.settings, - getString("editor.toolbar.settings.title"), - "end", - (e) => {}, + _document.querySelector(".toolbar") as HTMLDivElement, + "start", + ztoolkit.UI.createElement(_document, "button", { + classList: ["toolbar-button"], + properties: { + innerHTML: ICONS.addon, + title: "Link current note to another note", + }, + listeners: [ + { + type: "click", + listener: (e) => { + openLinkNoteDialog(noteItem); + }, + }, + ], + }) as HTMLButtonElement, ); - settingsButton.addEventListener("click", async (ev) => { - ev.stopPropagation(); - function removePopup() { - const popup = editor._iframeWindow.document.querySelector( - `#${makeId("settings-popup")}`, - ); - if (popup) { - popup.remove(); - settingsButton - .querySelector(".toolbar-button") - ?.classList.remove("active"); - editor._iframeWindow.document.removeEventListener("click", removePopup); - return true; + const settingsButton = editor._iframeWindow.document.querySelector( + ".toolbar .end .dropdown .toolbar-button", + ) as HTMLDivElement; + + const MutationObserver = // @ts-ignore + editor._iframeWindow.MutationObserver as typeof window.MutationObserver; + const observer = new MutationObserver((mutations) => { + mutations.forEach(async (mutation) => { + if ( + mutation.type === "attributes" && + mutation.attributeName === "class" && + mutation.target === settingsButton + ) { + if (settingsButton.classList.contains("active")) { + const dropdown = settingsButton.parentElement!; + const popup = dropdown.querySelector(".popup") as HTMLDivElement; + ztoolkit.log(popup); + registerEditorToolbarPopup(editor, popup, await getMenuData(editor)); + } } - return false; - } - - if (removePopup()) { - return; - } - - const currentLine = getLineAtCursor(editor); - const currentSection = getSectionAtCursor(editor); - const settingsMenuData: PopupData[] = [ - { - id: makeId("settings-openWorkspace"), - text: getString("editor.toolbar.settings.openWorkspace"), - callback: (e) => { - addon.hooks.onOpenWorkspace(noteItem, "tab"); - }, - }, - { - id: makeId("settings-showInLibrary"), - text: getString("editor.toolbar.settings.showInLibrary"), - callback: (e) => { - ZoteroPane.selectItems([e.editor._item.id]); - }, - }, - ]; - - if (currentLine >= 0) { - settingsMenuData.push( - ...([ - { - type: "splitter", - }, - { - id: makeId("settings-export"), - text: getString("editor.toolbar.settings.export"), - callback: (e) => { - if (addon.api.sync.isSyncNote(noteItem.id)) { - addon.hooks.onShowSyncInfo(noteItem.id); - } else { - addon.hooks.onShowExportNoteOptions([noteItem.id]); - } - }, - }, - { - type: "splitter", - }, - { - id: makeId("settings-insertTemplate"), - text: getString("editor.toolbar.settings.insertTemplate"), - callback: (e) => { - addon.hooks.onShowTemplatePicker("insert", { - noteId: e.editor._item.id, - lineIndex: currentLine, - }); - }, - }, - { - type: "splitter", - }, - { - id: makeId("settings-copyLink"), - text: getString("editor.toolbar.settings.copyLink", { - args: { - line: currentLine, - }, - }), - callback: (e) => { - const link = - getNoteLink(e.editor._item, { - lineIndex: currentLine, - }) || ""; - new ztoolkit.Clipboard() - .addText(link, "text/unicode") - .addText( - `${ - e.editor._item.getNoteTitle().trim() || link - }`, - "text/html", - ) - .copy(); - showHint(`Link ${link} copied`); - }, - }, - { - id: makeId("settings-copyLinkAtSection"), - text: getString("editor.toolbar.settings.copyLinkAtSection", { - args: { - section: currentSection, - }, - }), - callback: (e) => { - const link = - getNoteLink(e.editor._item, { - sectionName: currentSection, - }) || ""; - new ztoolkit.Clipboard() - .addText(link, "text/unicode") - .addText( - `${ - e.editor._item.getNoteTitle().trim() || link - }`, - "text/html", - ) - .copy(); - showHint(`Link ${link} copied`); - }, - }, - { - id: makeId("settings-updateRelatedNotes"), - text: getString("editor-toolbar-settings-updateRelatedNotes"), - callback: (e) => { - addon.api.note.updateRelatedNotes(e.editor._item.id); - }, - }, - ]), - ); - } - - const parentAttachment = await noteItem.parentItem?.getBestAttachment(); - if (parentAttachment) { - settingsMenuData.push( - ...([ - { - type: "splitter", - }, - { - id: makeId("settings-openParent"), - text: getString("editor.toolbar.settings.openParent"), - callback: (e) => { - ZoteroPane.viewAttachment([parentAttachment.id]); - Zotero.Notifier.trigger("open", "file", parentAttachment.id); - }, - }, - ]), - ); - } - - if (addon.api.sync.isSyncNote(noteItem.id)) { - settingsMenuData.splice(5, 0, { - id: makeId("settings-refreshSyncing"), - text: getString("editor.toolbar.settings.refreshSyncing"), - callback: (e) => { - addon.hooks.onSyncing(undefined, { - quiet: false, - skipActive: false, - reason: "manual-editor", - }); - }, - }); - } - - registerEditorToolbarPopup( - editor, - settingsButton, - `${config.addonRef}-settings-popup`, - "right", - settingsMenuData, - ).then((popup) => { - settingsButton.querySelector(".toolbar-button")?.classList.add("active"); - editor._iframeWindow.document.addEventListener("click", removePopup); }); }); - - // Center button - - const onTriggerMenu = (ev: MouseEvent) => { - editor._iframeWindow.focus(); - const linkMenu: PopupData[] = getLinkMenuData(editor); - editor._iframeWindow.document - .querySelector(`#${makeId("link")}`)! - .querySelector(".toolbar-button")!.innerHTML = ICONS.linkAfter; - - const popup = registerEditorToolbarPopup( - editor, - linkButton, - `${config.addonRef}-link-popup`, - "middle", - linkMenu, - ); - }; - - const onExitMenu = (ev: MouseEvent) => { - editor._iframeWindow.document - .querySelector(`#${makeId("link-popup")}`) - ?.remove(); - editor._iframeWindow.document - .querySelector(`#${makeId("link")}`)! - .querySelector(".toolbar-button")!.innerHTML = ICONS.addon; - }; - - const onClickMenu = async (ev: MouseEvent) => { - // TODO: fix link - return; - // const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null; - // if (!mainNote?.isNote()) { - // return; - // } - // const lineIndex = parseInt( - // (ev.target as HTMLDivElement).id.split("-").pop() || "-1", - // ); - // const forwardLink = getNoteLink(noteItem); - // const backLink = getNoteLink(mainNote, { ignore: true, lineIndex }); - // addLineToNote( - // mainNote, - // await addon.api.template.runTemplate( - // "[QuickInsertV2]", - // "link, linkText, subNoteItem, noteItem", - // [ - // forwardLink, - // noteItem.getNoteTitle().trim() || forwardLink, - // noteItem, - // mainNote, - // ], - // ), - // lineIndex, - // ); - // addLineToNote( - // noteItem, - // await addon.api.template.runTemplate( - // "[QuickBackLinkV2]", - // "link, linkText, subNoteItem, noteItem", - // [ - // backLink, - // mainNote.getNoteTitle().trim() || "Workspace Note", - // noteItem, - // mainNote, - // "", - // ], - // ), - // ); - // onExitMenu(ev); - // ev.stopPropagation(); - }; - - const linkButton = await registerEditorToolbarDropdown( - editor, - toolbar, - makeId("link"), - ICONS.addon, - getString("editor.toolbar.link.title"), - "middle", - onClickMenu, - ); - - linkButton.addEventListener("mouseenter", onTriggerMenu); - linkButton.addEventListener("mouseleave", onExitMenu); - linkButton.addEventListener("mouseleave", onExitMenu); - linkButton.addEventListener("click", (ev) => { - if ((ev.target as HTMLElement).classList.contains("option")) { - onClickMenu(ev); - } + observer.observe(settingsButton, { + attributes: true, + attributeFilter: ["class"], }); - editor._iframeWindow.document.addEventListener("click", onExitMenu); - - // Export - // const exportButton = await registerEditorToolbarDropdown( - // editor, - // toolbar, - // makeId("export"), - // ICONS.export, - // getString("editor.toolbar.export.title"), - // "end", - // (e) => { - // if (addon.api.sync.isSyncNote(noteItem.id)) { - // addon.hooks.onShowSyncInfo(noteItem.id); - // } else { - // addon.hooks.onShowExportNoteOptions([noteItem.id]); - // } - // } - // ); } -function getLinkMenuData(editor: Zotero.EditorInstance): PopupData[] { - return []; - // const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId) || null; - // const currentNote = editor._item; - // if (!workspaceNote?.isNote()) { - // return [ - // { - // id: makeId("link-popup-nodata"), - // text: getString("editor.toolbar.link.popup.nodata"), - // }, - // ]; - // } - // const nodes = getNoteTreeFlattened(workspaceNote, { - // keepLink: true, - // }); - // const menuData: PopupData[] = []; - // for (const node of nodes) { - // if (node.model.level === 7) { - // const lastMenu = - // menuData.length > 0 ? menuData[menuData.length - 1] : null; - // const linkNote = getNoteLinkParams(node.model.link).noteItem; - // if (linkNote && linkNote.id === currentNote.id && lastMenu) { - // lastMenu.suffix = "🔗"; - // } - // continue; - // } - // menuData.push({ - // id: makeId( - // `link-popup-${ - // getPref("editor.link.insertPosition") - // ? node.model.lineIndex - 1 - // : node.model.endIndex - // }`, - // ), - // text: node.model.name, - // prefix: "·".repeat(node.model.level - 1), - // }); - // } - // return menuData; -} +async function getMenuData(editor: Zotero.EditorInstance) { + const noteItem = editor._item; -async function registerEditorToolbar( - editor: Zotero.EditorInstance, - id: string, -) { - await editor._initPromise; - const _document = editor._iframeWindow.document; - const toolbar = ztoolkit.UI.createElement(_document, "div", { - attributes: { - id, + const currentLine = getLineAtCursor(editor); + const currentSection = slice(getSectionAtCursor(editor) || "", 10); + const settingsMenuData: PopupData[] = [ + { + id: makeId("settings-openWorkspace"), + text: getString("editor.toolbar.settings.openWorkspace"), + callback: (e) => { + addon.hooks.onOpenWorkspace(noteItem, "tab"); + }, }, - classList: ["toolbar"], - children: [ - { - tag: "div", - classList: ["start"], + { + id: makeId("settings-showInLibrary"), + text: getString("editor.toolbar.settings.showInLibrary"), + callback: (e) => { + ZoteroPane.selectItems([e.editor._item.id]); }, - { - tag: "div", - classList: ["middle"], - }, - { - tag: "div", - classList: ["end"], - }, - ], - ignoreIfExists: true, - }) as HTMLDivElement; - _document.querySelector(".editor")?.childNodes[0].before(toolbar); - return toolbar; -} - -async function registerEditorToolbarDropdown( - editor: Zotero.EditorInstance, - toolbar: HTMLDivElement, - id: string, - icon: string, - title: string, - position: "start" | "middle" | "end", - callback: (e: MouseEvent & { editor: Zotero.EditorInstance }) => any, -) { - await editor._initPromise; - const _document = editor._iframeWindow.document; - const dropdown = ztoolkit.UI.createElement(_document, "div", { - attributes: { - id, - title, }, - classList: ["dropdown", "more-dropdown"], - children: [ - { - tag: "button", - attributes: { - title, + ]; + + if (currentLine >= 0) { + settingsMenuData.push( + ...([ + { + type: "splitter", }, - properties: { - innerHTML: icon, - }, - classList: ["toolbar-button"], - listeners: [ - { - type: "click", - listener: (e) => { - Object.assign(e, { editor }); - if (callback) { - callback( - e as any as MouseEvent & { editor: Zotero.EditorInstance }, - ); - } - }, + { + id: makeId("settings-export"), + text: getString("editor.toolbar.settings.export"), + callback: (e) => { + if (addon.api.sync.isSyncNote(noteItem.id)) { + addon.hooks.onShowSyncInfo(noteItem.id); + } else { + addon.hooks.onShowExportNoteOptions([noteItem.id]); + } }, - ], + }, + { + type: "splitter", + }, + { + id: makeId("settings-insertTemplate"), + text: getString("editor.toolbar.settings.insertTemplate"), + callback: (e) => { + addon.hooks.onShowTemplatePicker("insert", { + noteId: e.editor._item.id, + lineIndex: currentLine, + }); + }, + }, + { + type: "splitter", + }, + { + id: makeId("settings-copyLink"), + text: getString("editor.toolbar.settings.copyLink", { + args: { + line: currentLine, + }, + }), + callback: (e) => { + const link = + getNoteLink(e.editor._item, { + lineIndex: currentLine, + }) || ""; + new ztoolkit.Clipboard() + .addText(link, "text/unicode") + .addText( + `${ + e.editor._item.getNoteTitle().trim() || link + }`, + "text/html", + ) + .copy(); + showHint(`Link ${link} copied`); + }, + }, + { + id: makeId("settings-copyLinkAtSection"), + text: getString("editor.toolbar.settings.copyLinkAtSection", { + args: { + section: currentSection, + }, + }), + callback: (e) => { + const link = + getNoteLink(e.editor._item, { + sectionName: currentSection, + }) || ""; + new ztoolkit.Clipboard() + .addText(link, "text/unicode") + .addText( + `${ + e.editor._item.getNoteTitle().trim() || link + }`, + "text/html", + ) + .copy(); + showHint(`Link ${link} copied`); + }, + }, + { + id: makeId("settings-updateRelatedNotes"), + text: getString("editor-toolbar-settings-updateRelatedNotes"), + callback: (e) => { + addon.api.note.updateRelatedNotes(e.editor._item.id); + }, + }, + ]), + ); + } + + const parentAttachment = await noteItem.parentItem?.getBestAttachment(); + if (parentAttachment) { + settingsMenuData.push( + ...([ + { + type: "splitter", + }, + { + id: makeId("settings-openParent"), + text: getString("editor.toolbar.settings.openParent"), + callback: (e) => { + ZoteroPane.viewAttachment([parentAttachment.id]); + Zotero.Notifier.trigger("open", "file", parentAttachment.id); + }, + }, + ]), + ); + } + + if (addon.api.sync.isSyncNote(noteItem.id)) { + settingsMenuData.splice(5, 0, { + id: makeId("settings-refreshSyncing"), + text: getString("editor.toolbar.settings.refreshSyncing"), + callback: (e) => { + addon.hooks.onSyncing(undefined, { + quiet: false, + skipActive: false, + reason: "manual-editor", + }); }, - ], - skipIfExists: true, - }); - toolbar.querySelector(`.${position}`)?.append(dropdown); - return dropdown; + }); + } + + return settingsMenuData; } declare interface PopupData { @@ -440,21 +221,18 @@ declare interface PopupData { async function registerEditorToolbarPopup( editor: Zotero.EditorInstance, - dropdown: HTMLDivElement, - id: string, - align: "middle" | "left" | "right", + popup: HTMLDivElement, popupLines: PopupData[], ) { await editor._initPromise; - const popup = ztoolkit.UI.appendElement( + ztoolkit.UI.appendElement( { - tag: "div", - classList: ["popup"], - id, + tag: "fragment", children: popupLines.map((props) => { return props.type === "splitter" ? { - tag: "hr", + tag: "div", + classList: ["separator"], properties: { id: props.id, }, @@ -485,20 +263,9 @@ async function registerEditorToolbarPopup( ], }; }), - removeIfExists: true, }, - dropdown, + popup, ) as HTMLDivElement; - let style: string = ""; - if (align === "middle") { - style = `right: -${popup.offsetWidth / 2 - 15}px;`; - } else if (align === "left") { - style = "left: 0; right: auto;"; - } else if (align === "right") { - style = "right: 0;"; - } - popup.setAttribute("style", style); - return popup; } async function registerEditorToolbarElement( diff --git a/src/modules/export/markdown.ts b/src/modules/export/markdown.ts index 3b33186..7f5a1da 100644 --- a/src/modules/export/markdown.ts +++ b/src/modules/export/markdown.ts @@ -8,7 +8,7 @@ export async function saveMD( options: { keepNoteLink?: boolean; withYAMLHeader?: boolean; - }, + } = {}, ) { const noteItem = Zotero.Items.get(noteId); const dir = jointPath(...PathUtils.split(formatPath(filename)).slice(0, -1)); diff --git a/src/modules/export/pdf.ts b/src/modules/export/pdf.ts index 23962c4..4f49e16 100644 --- a/src/modules/export/pdf.ts +++ b/src/modules/export/pdf.ts @@ -23,7 +23,6 @@ export async function savePDF(noteId: number) { } function disablePrintFooterHeader() { - // @ts-ignore Zotero.Prefs.resetBranch([], "print"); Zotero.Prefs.set("print.print_footercenter", "", true); Zotero.Prefs.set("print.print_footerleft", "", true); diff --git a/src/modules/relatedNotes.ts b/src/modules/relatedNotes.ts new file mode 100644 index 0000000..8777fa2 --- /dev/null +++ b/src/modules/relatedNotes.ts @@ -0,0 +1,31 @@ +import { getPref } from "../utils/prefs"; + +export { onUpdateRelated }; + +function onUpdateRelated( + items: Zotero.Item[] = [], + { skipActive } = { + skipActive: true, + }, +) { + if (!getPref("workspace.autoUpdateRelatedNotes")) { + return; + } + if (skipActive) { + // Skip active note editors' targets + const activeNoteIds = Zotero.Notes._editorInstances + .filter( + (editor) => + !Components.utils.isDeadWrapper(editor._iframeWindow) && + editor._iframeWindow.document.hasFocus(), + ) + .map((editor) => editor._item.id); + const filteredItems = items.filter( + (item) => !activeNoteIds.includes(item.id), + ); + items = filteredItems; + } + for (const item of items) { + addon.api.note.updateRelatedNotes(item.id); + } +} diff --git a/src/modules/sync/managerWindow.ts b/src/modules/sync/managerWindow.ts index 493b783..4bfa6c2 100644 --- a/src/modules/sync/managerWindow.ts +++ b/src/modules/sync/managerWindow.ts @@ -99,7 +99,6 @@ export async function showSyncManager() { "getRowString", (index) => addon.data.prefs?.rows[index].title || "", ) - // @ts-ignore TODO: Fix type in zotero-plugin-toolkit .setProp("onColumnSort", (columnIndex, ascending) => { addon.data.sync.manager.columnIndex = columnIndex; addon.data.sync.manager.columnAscending = ascending > 0; diff --git a/src/modules/template/api.ts b/src/modules/template/api.ts index 98fdd40..4b48f30 100644 --- a/src/modules/template/api.ts +++ b/src/modules/template/api.ts @@ -109,7 +109,7 @@ async function runTextTemplate( options: { targetNoteId?: number; dryRun?: boolean; - }, + } = {}, ) { const { targetNoteId, dryRun } = options; const targetNoteItem = Zotero.Items.get(targetNoteId || -1); @@ -130,7 +130,7 @@ async function runItemTemplate( itemIds?: number[]; targetNoteId?: number; dryRun?: boolean; - }, + } = {}, ): Promise { /** * args: diff --git a/src/utils/config.ts b/src/utils/config.ts index 9d230b3..5bd67ff 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,6 +1,6 @@ export const ICONS = { - settings: ``, - addon: ``, + settings: ``, + addon: ``, linkAfter: ` ... `, - previewImage: ``, - resizeImage: ``, - imageViewerPin: ``, - imageViewerPined: ``, + previewImage: ``, + resizeImage: ``, + imageViewerPin: ``, + imageViewerPined: ``, switchOutline: ``, workspace_notes_collapsed: ``, workspace_notes_open: ``, - readerQuickNote: ``, - embedLinkContent: ``, - updateLinkText: ``, - openInNewWindow: ``, + readerQuickNote: ``, + embedLinkContent: ``, + updateLinkText: ``, + openInNewWindow: ``, }; export const PROGRESS_TITLE = "Better Notes"; diff --git a/src/utils/linkNote.ts b/src/utils/linkNote.ts new file mode 100644 index 0000000..0a64da3 --- /dev/null +++ b/src/utils/linkNote.ts @@ -0,0 +1,42 @@ +import { config } from "../../package.json"; + +export { openLinkNoteDialog }; + +async function openLinkNoteDialog(currentNote: Zotero.Item) { + const io = { + openedNoteIDs: Zotero_Tabs._tabs + .map((tab) => tab.data?.itemID) + .filter((id) => id), + currentNoteID: currentNote.id, + deferred: Zotero.Promise.defer(), + } as any; + window.openDialog( + `chrome://${config.addonRef}/content/linkNote.xhtml`, + "_blank", + "chrome,modal,centerscreen,resizable=no", + io, + ); + await io.deferred.promise; + + const targetNote = Zotero.Items.get(io.targetNoteID); + const content = io.content; + const lineIndex = io.lineIndex; + + if (!targetNote || !content) return; + + await addon.api.note.insert(targetNote, content, lineIndex); + + await Zotero.DB.executeTransaction(async () => { + const saveParams = { + skipDateModifiedUpdate: true, + skipSelect: true, + notifierData: { + skipBN: true, + }, + }; + targetNote.addRelatedItem(currentNote); + currentNote.addRelatedItem(targetNote); + targetNote.save(saveParams); + currentNote.save(saveParams); + }); +} diff --git a/src/utils/note.ts b/src/utils/note.ts index b15a549..ac85f97 100644 --- a/src/utils/note.ts +++ b/src/utils/note.ts @@ -4,7 +4,7 @@ import { getEditorInstance, getPositionAtLine, insert } from "./editor"; import { formatPath, getItemDataURL } from "./str"; import { showHint } from "./hint"; import { config } from "../../package.json"; -import { getNoteLinkParams } from "./link"; +import { getNoteLink, getNoteLinkParams } from "./link"; export { renderNoteHTML, @@ -111,11 +111,32 @@ function parseHTMLLines(html: string): string[] { return parsedLines; } -function getLinesInNote(note: Zotero.Item): string[] { +function getLinesInNote(note: Zotero.Item): string[]; + +async function getLinesInNote( + note: Zotero.Item, + options: { + convertToHTML?: true; + }, +): Promise; + +function getLinesInNote( + note: Zotero.Item, + options?: { + convertToHTML?: boolean; + }, +): string[] | Promise { if (!note) { return []; } const noteText: string = note.getNote(); + if (options?.convertToHTML) { + return new Promise((resolve) => { + addon.api.convert.note2html(note).then((html) => { + resolve(parseHTMLLines(html)); + }); + }); + } return parseHTMLLines(noteText); } @@ -162,7 +183,7 @@ async function addLineToNote( const editor = getEditorInstance(note.id); if (editor && !forceMetadata) { // The note is opened. Add line via note editor - const pos = getPositionAtLine(editor, lineIndex, "end"); + const pos = getPositionAtLine(editor, lineIndex, "start"); ztoolkit.log("Add note line via note editor", pos); insert(editor, html, pos); // The selection is automatically moved to the next line @@ -524,24 +545,8 @@ async function updateRelatedNotes(noteID: number) { ztoolkit.log(`updateRelatedNotes: ${noteID} is not a note.`); return; } - const relatedNoteIDs = await getRelatedNoteIds(noteID); - const relatedNotes = Zotero.Items.get(relatedNoteIDs); - const currentRelatedNotes = {} as Record; + const { detectedIDSet, currentIDSet } = await getRelatedNoteIds(noteID); - // Get current related items - for (const relItemKey of noteItem.relatedItems) { - try { - const relItem = (await Zotero.Items.getByLibraryAndKeyAsync( - noteItem.libraryID, - relItemKey, - )) as Zotero.Item; - if (relItem.isNote()) { - currentRelatedNotes[relItem.id] = relItem; - } - } catch (e) { - ztoolkit.log(e); - } - } await Zotero.DB.executeTransaction(async () => { const saveParams = { skipDateModifiedUpdate: true, @@ -550,18 +555,18 @@ async function updateRelatedNotes(noteID: number) { skipBN: true, }, }; - for (const toAddNote of relatedNotes) { - if (toAddNote.id in currentRelatedNotes) { + for (const toAddNote of Zotero.Items.get(Array.from(detectedIDSet))) { + if (currentIDSet.has(toAddNote.id)) { // Remove existing notes from current dict for later process - delete currentRelatedNotes[toAddNote.id]; + currentIDSet.delete(toAddNote.id); continue; } toAddNote.addRelatedItem(noteItem); noteItem.addRelatedItem(toAddNote); toAddNote.save(saveParams); - delete currentRelatedNotes[toAddNote.id]; + currentIDSet.delete(toAddNote.id); } - for (const toRemoveNote of Object.values(currentRelatedNotes)) { + for (const toRemoveNote of Zotero.Items.get(Array.from(currentIDSet))) { // Remove related notes that are not in the new list toRemoveNote.removeRelatedItem(noteItem); noteItem.removeRelatedItem(toRemoveNote); @@ -571,21 +576,49 @@ async function updateRelatedNotes(noteID: number) { }); } -async function getRelatedNoteIds(noteId: number): Promise { - let allNoteIds: number[] = [noteId]; +async function getRelatedNoteIds(noteId: number) { + let detectedIDs: number[] = []; const note = Zotero.Items.get(noteId); const linkMatches = note.getNote().match(/zotero:\/\/note\/\w+\/\w+\//g); - if (!linkMatches) { - return allNoteIds; - } - const subNoteIds = ( - await Promise.all( - linkMatches.map(async (link) => getNoteLinkParams(link).noteItem), + const currentIDs: number[] = []; + + if (linkMatches) { + const subNoteIds = ( + await Promise.all( + linkMatches.map(async (link) => getNoteLinkParams(link).noteItem), + ) ) - ) - .filter((item) => item && item.isNote()) - .map((item) => (item as Zotero.Item).id); - allNoteIds = allNoteIds.concat(subNoteIds); - allNoteIds = new Array(...new Set(allNoteIds)); - return allNoteIds; + .filter((item) => item && item.isNote()) + .map((item) => (item as Zotero.Item).id); + detectedIDs = detectedIDs.concat(subNoteIds); + } + + const currentNoteLink = getNoteLink(note); + if (currentNoteLink) { + // Get current related items + for (const relItemKey of note.relatedItems) { + try { + const relItem = (await Zotero.Items.getByLibraryAndKeyAsync( + note.libraryID, + relItemKey, + )) as Zotero.Item; + + // If the related item is a note and contains the current note link + // Add it to the related note list + if (relItem.isNote()) { + if (relItem.getNote().includes(currentNoteLink)) { + detectedIDs.push(relItem.id); + } + currentIDs.push(relItem.id); + } + } catch (e) { + ztoolkit.log(e); + } + } + } + + const detectedIDSet = new Set(detectedIDs); + detectedIDSet.delete(noteId); + const currentIDSet = new Set(currentIDs); + return { detectedIDSet, currentIDSet }; } From 1977c16a09852455df9b5a4f76463d064999e683 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sat, 6 Apr 2024 11:43:45 +0800 Subject: [PATCH 10/23] fix: tab title update --- src/hooks.ts | 5 ++- src/modules/workspace/tab.ts | 63 ++++++++---------------------------- 2 files changed, 16 insertions(+), 52 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index 493bccc..8bb3262 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -9,10 +9,10 @@ import { } from "./modules/template/controller"; import { registerMenus } from "./modules/menu"; import { - registerWorkspaceTab, openWorkspaceTab, onTabSelect, restoreNoteTabs, + onUpdateNoteTabsTitle, } from "./modules/workspace/tab"; import { initWorkspace } from "./modules/workspace/content"; import { registerNotify } from "./modules/notify"; @@ -78,8 +78,6 @@ async function onMainWindowLoad(win: Window): Promise { registerMenus(); - registerWorkspaceTab(win); - initTemplates(); patchViewItems(win); @@ -123,6 +121,7 @@ function onNotify( reason: "item-modify", }); addon.hooks.onUpdateRelated(modifiedNotes, { skipActive: true }); + onUpdateNoteTabsTitle(modifiedNotes); } } else { return; diff --git a/src/modules/workspace/tab.ts b/src/modules/workspace/tab.ts index 0c53408..5f7f66f 100644 --- a/src/modules/workspace/tab.ts +++ b/src/modules/workspace/tab.ts @@ -3,55 +3,6 @@ import { initWorkspace } from "./content"; export const TAB_TYPE = "note"; -export function registerWorkspaceTab(win: Window) { - const doc = win.document; - const spacer = doc.querySelector("#zotero-collections-toolbar > spacer"); - if (!spacer) { - return; - } - const tabButton = ztoolkit.UI.insertElementBefore( - { - tag: "toolbarbutton", - classList: ["zotero-tb-button"], - styles: { - listStyleImage: `url("chrome://${config.addonRef}/content/icons/icon-linear-20.svg")`, - }, - attributes: { - tooltiptext: "Open workspace", - }, - listeners: [ - { - type: "command", - listener: (ev) => { - // if ((ev as MouseEvent).shiftKey) { - // addon.hooks.onOpenWorkspace("window"); - // } else { - // addon.hooks.onOpenWorkspace("tab"); - // } - }, - }, - ], - }, - spacer, - ) as XUL.ToolBarButton; - const collectionSearch = doc.querySelector("#zotero-collections-search")!; - const ob = new (ztoolkit.getGlobal("MutationObserver"))((muts) => { - tabButton.hidden = !!collectionSearch?.classList.contains("visible"); - }); - ob.observe(collectionSearch, { - attributes: true, - attributeFilter: ["class"], - }); - - win.addEventListener( - "unload", - () => { - ob.disconnect(); - }, - { once: true }, - ); -} - export async function openWorkspaceTab( item: Zotero.Item, options: { select?: boolean; index?: number } = { @@ -110,3 +61,17 @@ export function restoreNoteTabs() { }); } } + +export function onUpdateNoteTabsTitle(noteItems: Zotero.Item[]) { + const ids = noteItems.map((item) => item.id); + for (const tab of Zotero_Tabs._tabs) { + if (tab.type !== TAB_TYPE) continue; + if (ids.includes(tab.data.itemID)) { + const newTitle = Zotero.Items.get(tab.data.itemID).getNoteTitle(); + if (tab.title === newTitle) { + continue; + } + Zotero_Tabs.rename(tab.id, newTitle); + } + } +} From 30fc3c8e86770200db36d1cae244bd6d998dd678 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sat, 6 Apr 2024 11:52:56 +0800 Subject: [PATCH 11/23] fix: linkNote layout --- addon/chrome/content/styles/linkNote.css | 1 + addon/chrome/content/styles/notePicker.css | 1 + 2 files changed, 2 insertions(+) diff --git a/addon/chrome/content/styles/linkNote.css b/addon/chrome/content/styles/linkNote.css index 1403699..61204aa 100644 --- a/addon/chrome/content/styles/linkNote.css +++ b/addon/chrome/content/styles/linkNote.css @@ -1,4 +1,5 @@ .container { + min-height: 0; height: 100%; margin: 0; } diff --git a/addon/chrome/content/styles/notePicker.css b/addon/chrome/content/styles/notePicker.css index e3347e1..8f68793 100644 --- a/addon/chrome/content/styles/notePicker.css +++ b/addon/chrome/content/styles/notePicker.css @@ -3,6 +3,7 @@ bn-note-picker { } .container { + min-height: 0; height: 100%; margin: 0; } From 3a42791729c55e3368d4160e34adaa2404b80e23 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:42:09 +0800 Subject: [PATCH 12/23] fix: workspace window --- addon/chrome/content/styles/related.css | 61 ++++++++++++ addon/chrome/content/workspaceWindow.xhtml | 59 +++++------- addon/locale/en-US/addon.ftl | 3 +- addon/locale/it-IT/addon.ftl | 3 +- addon/locale/ru-RU/addon.ftl | 3 +- addon/locale/tr-TR/addon.ftl | 3 +- addon/locale/zh-CN/addon.ftl | 3 +- src/elements/detailsPane.ts | 2 +- src/elements/related.ts | 103 +++++++++++++++++++++ src/extras/customElements.ts | 2 + src/extras/workspaceWindow.ts | 37 ++++++++ src/hooks.ts | 5 +- src/modules/editor/toolbar.ts | 11 ++- src/modules/workspace/content.ts | 2 - src/modules/workspace/window.ts | 4 + 15 files changed, 254 insertions(+), 47 deletions(-) create mode 100644 addon/chrome/content/styles/related.css create mode 100644 src/elements/related.ts create mode 100644 src/extras/workspaceWindow.ts diff --git a/addon/chrome/content/styles/related.css b/addon/chrome/content/styles/related.css new file mode 100644 index 0000000..ee3abdf --- /dev/null +++ b/addon/chrome/content/styles/related.css @@ -0,0 +1,61 @@ +bn-related-box { + display: flex; + flex-direction: column; + gap: 2px; +} +bn-related-box[hidden] { + display: none; +} +bn-related-box[readonly] .add { + display: none; +} +bn-related-box .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; +} diff --git a/addon/chrome/content/workspaceWindow.xhtml b/addon/chrome/content/workspaceWindow.xhtml index 8b9909d..c856b79 100644 --- a/addon/chrome/content/workspaceWindow.xhtml +++ b/addon/chrome/content/workspaceWindow.xhtml @@ -4,7 +4,7 @@ - + - + + + + + + + + + + + + + + diff --git a/addon/chrome/content/styles/relation.css b/addon/chrome/content/styles/relation.css new file mode 100644 index 0000000..ac41ee9 --- /dev/null +++ b/addon/chrome/content/styles/relation.css @@ -0,0 +1,5 @@ +#bn-relation-graph { + width: 100%; + height: 250px; + border-radius: 8px; +} diff --git a/addon/locale/en-US/noteRelation.ftl b/addon/locale/en-US/noteRelation.ftl new file mode 100644 index 0000000..8efeb8f --- /dev/null +++ b/addon/locale/en-US/noteRelation.ftl @@ -0,0 +1,6 @@ +note-relation-header = + .label = Relation Graph +note-relation-sidenav = + .tooltiptext = Relation Graph +note-relation-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/it-IT/noteRelation.ftl b/addon/locale/it-IT/noteRelation.ftl new file mode 100644 index 0000000..8efeb8f --- /dev/null +++ b/addon/locale/it-IT/noteRelation.ftl @@ -0,0 +1,6 @@ +note-relation-header = + .label = Relation Graph +note-relation-sidenav = + .tooltiptext = Relation Graph +note-relation-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/ru-RU/noteRelation.ftl b/addon/locale/ru-RU/noteRelation.ftl new file mode 100644 index 0000000..8efeb8f --- /dev/null +++ b/addon/locale/ru-RU/noteRelation.ftl @@ -0,0 +1,6 @@ +note-relation-header = + .label = Relation Graph +note-relation-sidenav = + .tooltiptext = Relation Graph +note-relation-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/tr-TR/noteRelation.ftl b/addon/locale/tr-TR/noteRelation.ftl new file mode 100644 index 0000000..8efeb8f --- /dev/null +++ b/addon/locale/tr-TR/noteRelation.ftl @@ -0,0 +1,6 @@ +note-relation-header = + .label = Relation Graph +note-relation-sidenav = + .tooltiptext = Relation Graph +note-relation-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/zh-CN/noteRelation.ftl b/addon/locale/zh-CN/noteRelation.ftl new file mode 100644 index 0000000..cd201ad --- /dev/null +++ b/addon/locale/zh-CN/noteRelation.ftl @@ -0,0 +1,6 @@ +note-relation-header = + .label = 关系图 +note-relation-sidenav = + .tooltiptext = 关系图 +note-relation-refresh = + .tooltiptext = 刷新 diff --git a/scripts/build-extras.mjs b/scripts/build-extras.mjs index d00cda6..39f4f40 100644 --- a/scripts/build-extras.mjs +++ b/scripts/build-extras.mjs @@ -5,9 +5,9 @@ const buildDir = "build"; export async function main() { await build({ - entryPoints: ["./src/extras/*.ts"], + entryPoints: ["./src/extras/*.*"], outdir: path.join(buildDir, "addon/chrome/content/scripts"), bundle: true, - target: ["firefox102"], + target: ["firefox115"], }).catch(() => exit(1)); } diff --git a/scripts/build.mjs b/scripts/build.mjs index 495ef39..f485705 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -194,7 +194,7 @@ export const esbuildOptions = { __env__: `"${env.NODE_ENV}"`, }, bundle: true, - target: "firefox102", + target: "firefox115", outfile: path.join( buildDir, `addon/chrome/content/scripts/${config.addonRef}.js`, diff --git a/src/elements/detailsPane.ts b/src/elements/detailsPane.ts index b1090ee..6104a7f 100644 --- a/src/elements/detailsPane.ts +++ b/src/elements/detailsPane.ts @@ -23,6 +23,7 @@ export class NoteDetails extends ItemDetails { init() { MozXULElement.insertFTLIfNeeded(`${config.addonRef}-notePreview.ftl`); + MozXULElement.insertFTLIfNeeded(`${config.addonRef}-noteRelation.ftl`); super.init(); } diff --git a/src/hooks.ts b/src/hooks.ts index 97015c0..6434917 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -37,6 +37,7 @@ import { initSyncList } from "./modules/sync/api"; import { patchViewItems } from "./modules/viewItems"; import { onUpdateRelated } from "./modules/relatedNotes"; import { getFocusedWindow } from "./utils/window"; +import { registerNoteRelation } from "./modules/workspace/relation"; async function onStartup() { await Promise.all([ @@ -58,6 +59,8 @@ async function onStartup() { registerReaderAnnotationButton(); + registerNoteRelation(); + initSyncList(); setSyncing(); diff --git a/src/modules/workspace/relation.ts b/src/modules/workspace/relation.ts new file mode 100644 index 0000000..4020764 --- /dev/null +++ b/src/modules/workspace/relation.ts @@ -0,0 +1,139 @@ +import { config } from "../../../package.json"; +import { slice } from "../../utils/str"; +import { waitUtilAsync } from "../../utils/wait"; + +export function registerNoteRelation() { + const key = Zotero.ItemPaneManager.registerSection({ + paneID: `bn-note-relation`, + pluginID: config.addonID, + header: { + icon: `chrome://${config.addonRef}/content/icons/relation-16.svg`, + l10nID: `${config.addonRef}-note-relation-header`, + }, + sidenav: { + icon: `chrome://${config.addonRef}/content/icons/relation-20.svg`, + l10nID: `${config.addonRef}-note-relation-sidenav`, + }, + bodyXHTML: ` + + + +`, + sectionButtons: [ + { + type: "refreshGraph", + icon: "chrome://zotero/skin/16/universal/sync.svg", + l10nID: `${config.addonRef}-note-relation-refresh`, + onClick: ({ body, item }) => { + refresh(body, item); + }, + }, + ], + onInit({ body }) { + body + .querySelector("iframe")! + .contentWindow?.addEventListener("message", (ev) => { + if (ev.data.type === "openNote") { + addon.hooks.onOpenNote( + ev.data.id, + ev.data.isShift ? "window" : "tab", + ); + } + }); + }, + onItemChange: ({ body, setEnabled }) => { + if (body.closest("bn-workspace") as HTMLElement | undefined) { + setEnabled(true); + return; + } + setEnabled(false); + }, + onRender: () => {}, + onAsyncRender: async ({ body, item }) => { + await refresh(body, item); + }, + }); +} + +async function refresh(body: HTMLElement, item: Zotero.Item) { + const data = await getRelationData(item); + await waitUtilAsync( + () => + body.querySelector("iframe")!.contentDocument?.readyState === "complete", + ); + body.querySelector("iframe")!.contentWindow?.postMessage( + { + type: "render", + graph: data, + }, + "*", + ); +} + +async function getRelationData(note: Zotero.Item) { + if (!note) return; + const currentContent = note.getNote(); + const currentLink = addon.api.convert.note2link(note); + const currentTitle = slice(note.getNoteTitle(), 15); + const { detectedIDSet, currentIDSet } = + await addon.api.related.getRelatedNoteIds(note.id); + if (!areSetsEqual(detectedIDSet, currentIDSet)) { + await addon.api.related.updateRelatedNotes(note.id); + } + const items = Zotero.Items.get(Array.from(detectedIDSet)); + + const nodes = []; + const links = []; + for (const item of items) { + const compareContent = item.getNote(); + const compareLink = addon.api.convert.note2link(item); + const compareTitle = slice(item.getNoteTitle(), 15); + + if (currentLink && compareContent.includes(currentLink)) { + links.push({ + source: item.id, + target: note.id, + value: 1, + }); + } + if (compareLink && currentContent.includes(compareLink)) { + links.push({ + source: note.id, + target: item.id, + value: 1, + }); + } + + nodes.push({ + id: item.id, + title: compareTitle, + group: 2, + }); + } + + nodes.push({ + id: note.id, + title: currentTitle, + group: 1, + }); + + return { nodes, links }; +} + +function areSetsEqual(set1: Set, set2: Set): boolean { + if (set1.size !== set2.size) { + return false; + } + for (const item of set1) { + if (!set2.has(item)) { + return false; + } + } + return true; +} From 2fe1d0453d450b9b7a7102d269aaac890c391ec8 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:41:07 +0800 Subject: [PATCH 19/23] update: note preview style --- addon/chrome/content/styles/workspace.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addon/chrome/content/styles/workspace.css b/addon/chrome/content/styles/workspace.css index 5fd744b..869bd6b 100644 --- a/addon/chrome/content/styles/workspace.css +++ b/addon/chrome/content/styles/workspace.css @@ -14,3 +14,7 @@ bn-workspace #__addonRef__-editor-main #links-container, .bn-note-preview { height: 450px; } + +.bn-note-preview iframe { + border-radius: 8px; +} From 65cdddf792cd2393e036929b41445fa43498a9a3 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sun, 7 Apr 2024 23:17:33 +0800 Subject: [PATCH 20/23] update: prefs --- addon/chrome/content/preferences.xhtml | 38 ++++++++++++++++---------- addon/locale/en-US/addon.ftl | 2 +- addon/locale/en-US/preferences.ftl | 15 ++++++---- addon/locale/it-IT/addon.ftl | 2 +- addon/locale/it-IT/preferences.ftl | 15 ++++++---- addon/locale/ru-RU/addon.ftl | 2 +- addon/locale/ru-RU/preferences.ftl | 15 ++++++---- addon/locale/tr-TR/addon.ftl | 2 +- addon/locale/tr-TR/preferences.ftl | 15 ++++++---- addon/locale/zh-CN/addon.ftl | 8 +++--- addon/locale/zh-CN/preferences.ftl | 15 ++++++---- addon/prefs.js | 4 ++- src/elements/outlinePane.ts | 2 +- src/elements/related.ts | 2 +- src/hooks.ts | 5 ++++ src/modules/relatedNotes.ts | 2 +- src/modules/sync/infoWindow.ts | 2 +- 17 files changed, 93 insertions(+), 53 deletions(-) diff --git a/addon/chrome/content/preferences.xhtml b/addon/chrome/content/preferences.xhtml index f6bc1b8..abb0f1e 100644 --- a/addon/chrome/content/preferences.xhtml +++ b/addon/chrome/content/preferences.xhtml @@ -6,35 +6,43 @@ onload="Zotero.__addonInstance__.hooks.onPrefsEvent('load', {window})" > - - + + + + + + + - - + - + - +