;
+ if (items[index]?.hidden) {
+ // Will find the first visible item
+ index = items.length;
+ }
+ if (index >= items.length) {
+ // Find the first visible item with :first-of-type
+ const item = this.popup!.container.querySelector(
+ ".popup-item:not([hidden])",
+ ) as HTMLElement;
+ index = parseInt(item?.dataset.commandId || "-1", 10);
+ } else if (index < 0) {
+ // Find the last visible item with :last-of-type
+ const visibleItems = this.popup!.container.querySelectorAll(
+ ".popup-item:not([hidden])",
+ );
+ const item = visibleItems[visibleItems.length - 1] as HTMLElement;
+ index = parseInt(item?.dataset.commandId || "-1", 10);
+ }
+
+ if (index < 0) {
+ this.selectedCommandIndex = -1;
+ return;
+ }
+ this.selectedCommandIndex = index;
+ items[index].classList.add("selected");
+ // Record the scroll position of the top document
+ const scrollTop = document.querySelector(".editor-core")!.scrollTop;
+ items[index].scrollIntoView({
+ block: "center",
+ });
+ // Restore the scroll position
+ document.querySelector(".editor-core")!.scrollTop = scrollTop;
+ }
+
+ _executeCommand(index: number, state: EditorState) {
+ const command = this.commands[index];
+ if (!command) {
+ return;
+ }
+ // Remove the current input `/`
+ this.removeInputSlash(state);
+
+ const newState = _currentEditorInstance._editorCore.view.state;
+
+ // Apply the command
+ try {
+ const mightBeTr = command.command(newState);
+ if (mightBeTr) {
+ _currentEditorInstance._editorCore.view.dispatch(mightBeTr);
+ }
+ } catch (error) {
+ console.error("Error applying command", error);
+ }
+
+ this._closePopup();
+ }
+
+ removeInputSlash(state: EditorState) {
+ const { $from } = state.selection;
+ const { pos } = $from;
+ const tr = state.tr.delete(pos - 1, pos);
+ _currentEditorInstance._editorCore.view.dispatch(tr);
+ }
+}
+
+function initMagicKeyPlugin(
+ plugins: readonly Plugin[],
+ options: MagicKeyOptions,
+) {
+ console.log("Init BN Magic Key Plugin");
+ const key = new PluginKey("linkPreviewPlugin");
+ return [
+ ...plugins,
+ new Plugin({
+ key,
+ state: {
+ init(config, state) {
+ return new PluginState(state, options);
+ },
+ apply: (tr, pluginState, oldState, newState) => {
+ pluginState.update(newState, oldState);
+ return pluginState;
+ },
+ },
+ props: {
+ handleDOMEvents: {
+ keydown: (view, event) => {
+ const pluginState = key.getState(view.state) as PluginState;
+ pluginState.handleKeydown(event);
+ },
+ },
+ },
+ view: (editorView) => {
+ return {
+ update(view, prevState) {
+ const pluginState = key.getState(view.state) as PluginState;
+ pluginState.update(view.state, prevState);
+ },
+ destroy() {
+ const pluginState = key.getState(editorView.state) as PluginState;
+ pluginState.destroy();
+ },
+ };
+ },
+ }),
+ ];
+}
+
+function getPlugin(key = "menu") {
+ return _currentEditorInstance._editorCore.pluginState[key] as any;
+}
diff --git a/src/extras/editor/pasteMarkdown.ts b/src/extras/editor/markdownPaste.ts
similarity index 94%
rename from src/extras/editor/pasteMarkdown.ts
rename to src/extras/editor/markdownPaste.ts
index bd7e9ee..66040ea 100644
--- a/src/extras/editor/pasteMarkdown.ts
+++ b/src/extras/editor/markdownPaste.ts
@@ -1,15 +1,19 @@
import { Plugin, PluginKey } from "prosemirror-state";
import { md2html } from "../../utils/convert";
-export { initPasteMarkdownPlugin };
+export { initMarkdownPastePlugin, MarkdownPasteOptions };
declare const _currentEditorInstance: {
_editorCore: EditorCore;
};
-function initPasteMarkdownPlugin(plugins: readonly Plugin[]) {
+interface MarkdownPasteOptions {
+ enable: boolean;
+}
+
+function initMarkdownPastePlugin(plugins: readonly Plugin[]) {
const core = _currentEditorInstance._editorCore;
- console.log("Init BN Paste Markdown Plugin");
+ console.log("Init BN Markdown Paste Plugin");
const key = new PluginKey("pasteDropPlugin");
const oldPastePluginIndex = plugins.findIndex(
(plugin) => plugin.props.handlePaste && plugin.props.handleDrop,
diff --git a/src/extras/editor/plugins.ts b/src/extras/editor/plugins.ts
index 51d5d72..9e86b69 100644
--- a/src/extras/editor/plugins.ts
+++ b/src/extras/editor/plugins.ts
@@ -1,5 +1,6 @@
import { initLinkPreviewPlugin, LinkPreviewOptions } from "./linkPreview";
-import { initPasteMarkdownPlugin } from "./pasteMarkdown";
+import { initMagicKeyPlugin, MagicKeyOptions } from "./magicKey";
+import { initMarkdownPastePlugin, MarkdownPasteOptions } from "./markdownPaste";
export { initPlugins };
@@ -7,11 +8,18 @@ declare const _currentEditorInstance: {
_editorCore: EditorCore;
};
-function initPlugins(options: LinkPreviewOptions) {
+function initPlugins(options: {
+ linkPreview: LinkPreviewOptions;
+ magicKey: MagicKeyOptions;
+ markdownPaste: MarkdownPasteOptions;
+}) {
const core = _currentEditorInstance._editorCore;
let plugins = core.view.state.plugins;
- plugins = initLinkPreviewPlugin(plugins, options);
- plugins = initPasteMarkdownPlugin(plugins);
+ if (options.linkPreview.previewType !== "disable")
+ plugins = initLinkPreviewPlugin(plugins, options.linkPreview);
+ if (options.markdownPaste.enable) plugins = initMarkdownPastePlugin(plugins);
+ if (options.magicKey.enable)
+ plugins = initMagicKeyPlugin(plugins, options.magicKey);
// Collect all plugins and reconfigure the state only once
const newState = core.view.state.reconfigure({
plugins,
diff --git a/src/extras/editor/popup.ts b/src/extras/editor/popup.ts
index c172d19..e770362 100644
--- a/src/extras/editor/popup.ts
+++ b/src/extras/editor/popup.ts
@@ -5,6 +5,8 @@ class Popup {
hasHover = false;
+ className: string;
+
get container() {
return this._popup;
}
@@ -18,6 +20,7 @@ class Popup {
className?: string,
children: (HTMLElement | DocumentFragment)[] = [],
) {
+ this.className = className || "";
this._popup = doc.createElement("div");
this._popup.className = `popup-container ${className}`;
this._popup.innerHTML = `
@@ -58,7 +61,7 @@ class Popup {
// Bottom
const otherPopupHeight = Array.from(
popupParent.querySelectorAll(
- ".popup-container:not(.link-preview) > .popup.popup-bottom",
+ `.popup-container:not(.${this.className}) > .popup.popup-bottom`,
),
).reduce((acc, el) => acc + (el as HTMLElement).offsetHeight, 0);
top =
@@ -72,7 +75,7 @@ class Popup {
// Top
const otherPopupHeight = Array.from(
popupParent.querySelectorAll(
- ".popup-container:not(.link-preview) > .popup.popup-top",
+ `.popup-container:not(.${this.className}) > .popup.popup-top`,
),
).reduce((acc, el) => acc + (el as HTMLElement).offsetHeight, 0);
top =
diff --git a/src/extras/linkCreator.ts b/src/extras/linkCreator.ts
index 3141491..6d8b81e 100644
--- a/src/extras/linkCreator.ts
+++ b/src/extras/linkCreator.ts
@@ -15,6 +15,7 @@ let io: {
targetNoteID?: number;
content?: string;
lineIndex?: number;
+ mode?: "inbound" | "outbound";
};
window.onload = async function () {
@@ -50,9 +51,20 @@ function init() {
io = window.arguments[0];
+ if (!io.deferred) {
+ // @ts-ignore
+ io = io.wrappedJSObject;
+ }
+
tabbox = document.querySelector("#top-container")!;
- tabbox.selectedIndex =
- (getPref("windows.linkCreator.tabIndex") as number) || 0;
+
+ if (io.mode) {
+ tabbox.selectedIndex = io.mode === "inbound" ? 0 : 1;
+ } else {
+ tabbox.selectedIndex =
+ (getPref("windows.linkCreator.tabIndex") as number) || 0;
+ }
+
tabbox.addEventListener("select", loadSelectedPanel);
inboundCreator = document.querySelector(
diff --git a/src/extras/relationWorker.ts b/src/extras/relationWorker.ts
index e87adf5..fd2bc26 100644
--- a/src/extras/relationWorker.ts
+++ b/src/extras/relationWorker.ts
@@ -38,10 +38,12 @@ messageServer.start();
async function addLink(model: LinkModel) {
await db.link.add(model);
+ log("addLink", model);
}
async function bulkAddLink(models: LinkModel[]) {
await db.link.bulkAdd(models);
+ log("bulkAddLink", models);
}
async function rebuildLinkForNote(
@@ -51,15 +53,17 @@ async function rebuildLinkForNote(
) {
log("rebuildLinkForNote", fromLibID, fromKey, links);
- const collection = db.link.where({ fromLibID, fromKey });
- const oldOutboundLinks = await collection.toArray();
- await collection.delete().then((deleteCount) => {
- log("Deleted " + deleteCount + " objects");
- return bulkAddLink(links);
+ return db.transaction("rw", db.link, async () => {
+ const collection = db.link.where({ fromLibID, fromKey });
+ const oldOutboundLinks = await collection.toArray();
+ await collection.delete().then((deleteCount) => {
+ log("Deleted " + deleteCount + " objects");
+ return bulkAddLink(links);
+ });
+ return {
+ oldOutboundLinks,
+ };
});
- return {
- oldOutboundLinks,
- };
}
async function getOutboundLinks(fromLibID: number, fromKey: string) {
diff --git a/src/extras/templatePicker.ts b/src/extras/templatePicker.ts
index f0afc3e..10b5d33 100644
--- a/src/extras/templatePicker.ts
+++ b/src/extras/templatePicker.ts
@@ -7,7 +7,10 @@ document.addEventListener("DOMContentLoaded", (ev) => {
document.addEventListener("dialogaccept", () => accept());
-const args = window.arguments[0] as any;
+let args = window.arguments[0] as any;
+if (!args._initPromise) {
+ args = args.wrappedJSObject;
+}
const templateData = args.templates;
const multiSelect = args.multiSelect;
let tableHelper: VirtualizedTableHelper;
diff --git a/src/modules/editor/initalize.ts b/src/modules/editor/initalize.ts
index 677671a..88db6ae 100644
--- a/src/modules/editor/initalize.ts
+++ b/src/modules/editor/initalize.ts
@@ -1,6 +1,6 @@
import { initEditorImagePreviewer } from "./image";
import { injectEditorCSS, injectEditorScripts } from "./inject";
-import { initEditorLinkPreview } from "./linkPreview";
+import { initEditorPlugins } from "./plugins";
import { initEditorMenu } from "./menu";
import { initEditorPopup } from "./popup";
import { initEditorToolbar } from "./toolbar";
@@ -38,5 +38,5 @@ async function onEditorInstanceCreated(editor: Zotero.EditorInstance) {
await initEditorToolbar(editor);
initEditorPopup(editor);
initEditorMenu(editor);
- initEditorLinkPreview(editor);
+ initEditorPlugins(editor);
}
diff --git a/src/modules/editor/linkPreview.ts b/src/modules/editor/linkPreview.ts
deleted file mode 100644
index 6b8aa48..0000000
--- a/src/modules/editor/linkPreview.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { initLinkPreview } from "../../utils/editor";
-
-export function initEditorLinkPreview(editor: Zotero.EditorInstance) {
- initLinkPreview(editor);
-}
diff --git a/src/modules/editor/plugins.ts b/src/modules/editor/plugins.ts
new file mode 100644
index 0000000..af0e437
--- /dev/null
+++ b/src/modules/editor/plugins.ts
@@ -0,0 +1,5 @@
+import { initEditorPlugins as initEditorPluginsIntl } from "../../utils/editor";
+
+export function initEditorPlugins(editor: Zotero.EditorInstance) {
+ initEditorPluginsIntl(editor);
+}
diff --git a/src/modules/editor/popup.ts b/src/modules/editor/popup.ts
index e4c1637..5e37743 100644
--- a/src/modules/editor/popup.ts
+++ b/src/modules/editor/popup.ts
@@ -19,14 +19,16 @@ export function initEditorPopup(editor: Zotero.EditorInstance) {
ztoolkit.log(mut);
if (
(mut.addedNodes.length &&
- (mut.addedNodes[0] as HTMLElement).querySelector(".link-popup")) ||
+ mut.addedNodes[0]?.hasChildNodes() &&
+ (mut.addedNodes[0] as HTMLElement)?.querySelector(".link-popup")) ||
(mut.attributeName === "href" &&
mut.target.parentElement?.classList.contains("link"))
) {
updateEditorLinkPopup(editor);
} else if (
mut.addedNodes.length &&
- (mut.addedNodes[0] as HTMLElement).querySelector(".image-popup")
+ mut.addedNodes[0]?.hasChildNodes() &&
+ (mut.addedNodes[0] as HTMLElement)?.querySelector(".image-popup")
) {
updateEditorImagePopup(editor);
}
diff --git a/src/utils/editor.ts b/src/utils/editor.ts
index 1da2689..188d8ca 100644
--- a/src/utils/editor.ts
+++ b/src/utils/editor.ts
@@ -2,6 +2,7 @@ import TreeModel = require("tree-model");
import { TextSelection } from "prosemirror-state";
import { getNoteTreeFlattened } from "./note";
import { getPref } from "./prefs";
+import { openLinkCreator } from "./linkCreator";
export {
insert,
@@ -26,7 +27,7 @@ export {
getTextBetween,
getTextBetweenLines,
isImageAtCursor,
- initLinkPreview,
+ initEditorPlugins,
};
function insert(
@@ -446,7 +447,7 @@ function getTextBetweenLines(
return core.view.state.doc.textBetween(from, to);
}
-function initLinkPreview(editor: Zotero.EditorInstance) {
+function initEditorPlugins(editor: Zotero.EditorInstance) {
const previewType = getPref("editor.noteLinkPreviewType") as string;
if (!["hover", "ctrl"].includes(previewType)) {
return;
@@ -456,29 +457,49 @@ function initLinkPreview(editor: Zotero.EditorInstance) {
EditorAPI.initPlugins(
Components.utils.cloneInto(
{
- setPreviewContent: (
- link: string,
- setContent: (content: string) => void,
- ) => {
- const note = addon.api.convert.link2note(link);
- if (!note) {
- setContent(
- `Invalid note link: ${link}
`,
- );
- return;
- }
- addon.api.convert
- .link2html(link, {
- noteItem: note,
- dryRun: true,
- usePosition: true,
- })
- .then((content) => setContent(content));
+ linkPreview: {
+ setPreviewContent: (
+ link: string,
+ setContent: (content: string) => void,
+ ) => {
+ const note = addon.api.convert.link2note(link);
+ if (!note) {
+ setContent(
+ `Invalid note link: ${link}
`,
+ );
+ return;
+ }
+ addon.api.convert
+ .link2html(link, {
+ noteItem: note,
+ dryRun: true,
+ usePosition: true,
+ })
+ .then((content) => setContent(content));
+ },
+ openURL: (url: string) => {
+ Zotero.getActiveZoteroPane().loadURI(url);
+ },
+ previewType,
},
- openURL: (url: string) => {
- Zotero.getActiveZoteroPane().loadURI(url);
+ magicKey: {
+ insertTemplate: () => {
+ addon.hooks.onShowTemplatePicker("insert", {
+ noteId: editor._item.id,
+ lineIndex: getLineAtCursor(editor),
+ });
+ },
+ insertLink: (mode: "inbound" | "outbound") => {
+ openLinkCreator(editor._item, {
+ lineIndex: getLineAtCursor(editor),
+ mode,
+ });
+ },
+ enable: getPref("editor.useMagicKey") as boolean,
+ },
+ markdownPaste: {
+ enable: getPref("editor.useMarkdownPaste") as boolean,
},
- requireCtrl: previewType === "ctrl",
},
editor._iframeWindow,
{ wrapReflectors: true, cloneFunctions: true },
diff --git a/src/utils/linkCreator.ts b/src/utils/linkCreator.ts
index da88108..268337a 100644
--- a/src/utils/linkCreator.ts
+++ b/src/utils/linkCreator.ts
@@ -8,7 +8,8 @@ export { openLinkCreator };
async function openLinkCreator(
currentNote: Zotero.Item,
options?: {
- lineIndex: number;
+ mode?: "inbound" | "outbound";
+ lineIndex?: number;
},
) {
if (!currentNote.id) {
@@ -25,9 +26,13 @@ async function openLinkCreator(
),
currentNoteID: currentNote.id,
currentLineIndex: options?.lineIndex,
+ mode: options?.mode,
deferred: Zotero.Promise.defer(),
} as any;
- Zotero.getMainWindow().openDialog(
+
+ Services.ww.openWindow(
+ // @ts-ignore
+ null,
`chrome://${config.addonRef}/content/linkCreator.xhtml`,
`${config.addonRef}-linkCreator`,
"chrome,modal,centerscreen,resizable=yes",
@@ -42,6 +47,4 @@ async function openLinkCreator(
if (!targetNote || !content) return;
await addLineToNote(targetNote, content, lineIndex);
-
- await addon.api.relation.updateNoteLinkRelation(targetNote.id);
}
diff --git a/src/utils/relation.ts b/src/utils/relation.ts
index 66c8613..e5acb4d 100644
--- a/src/utils/relation.ts
+++ b/src/utils/relation.ts
@@ -43,6 +43,7 @@ export {
};
async function updateNoteLinkRelation(noteID: number) {
+ ztoolkit.log("updateNoteLinkRelation", noteID);
const note = Zotero.Items.get(noteID);
const affectedNoteIDs = new Set([noteID]);
const fromLibID = note.libraryID;
diff --git a/src/utils/templatePicker.ts b/src/utils/templatePicker.ts
index ab38f9d..2647cca 100644
--- a/src/utils/templatePicker.ts
+++ b/src/utils/templatePicker.ts
@@ -18,7 +18,11 @@ export async function openTemplatePicker(
selected: [] as string[],
_initPromise: Zotero.Promise.defer(),
};
- Zotero.getMainWindow().openDialog(
+ // @ts-ignore
+ // args.wrappedJSObject = args;
+ Services.ww.openWindow(
+ // @ts-ignore
+ null,
`chrome://${config.addonRef}/content/templatePicker.xhtml`,
"_blank",
"chrome,modal,centerscreen,resizable=yes",