add: magic key command palette

This commit is contained in:
windingwind 2024-11-11 20:19:57 +01:00
parent 968eb0ef40
commit e44e07eefa
27 changed files with 803 additions and 89 deletions

View File

@ -59,6 +59,16 @@
<radio data-l10n-id="editor-noteLinkPreview-disable" value="disable" />
</radiogroup>
</hbox>
<checkbox
data-l10n-id="editor-useMagicKey"
native="true"
preference="__prefsPrefix__.editor.useMagicKey"
/>
<checkbox
data-l10n-id="editor-useMarkdownPaste"
native="true"
preference="__prefsPrefix__.editor.useMarkdownPaste"
/>
</groupbox>
<groupbox>
<label><html:h2 data-l10n-id="sync-title"></html:h2></label>

View File

@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
}
editor-noteLinkPreview-disable =
.label = Never
editor-useMagicKey =
.label = Use magic key "/" to show command palette
editor-useMarkdownPaste =
.label = Use enhanced markdown paste
sync-title = Sync
sync-period-label = Auto-sync period (seconds)

View File

@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
}
editor-noteLinkPreview-disable =
.label = Never
editor-useMagicKey =
.label = Usa il tasto magico "/" per mostrare il pannello dei comandi
editor-useMarkdownPaste =
.label = Usa l'incolla markdown avanzato
sync-title = Sincronizzazione
sync-period-label = Intervallo della sincronizzazione automatica (secondi)

View File

@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
}
editor-noteLinkPreview-disable =
.label = Never
editor-useMagicKey =
.label = Использовать магическую клавишу "/" для отображения панели команд
editor-useMarkdownPaste =
.label = Использовать расширенное вставление Markdown
sync-title = Синк
sync-period-label = Авто-синк период (сек)

View File

@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
}
editor-noteLinkPreview-disable =
.label = Never
editor-useMagicKey =
.label = Komut panelini göstermek için sihirli tuş "/" kullan
editor-useMarkdownPaste =
.label = Gelişmiş markdown yapıştırma kullan
sync-title = Eşitle
sync-period-label = Otomatik Eşitleme Sıklığı (saniye)

View File

@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
}
editor-noteLinkPreview-disable =
.label = 从不
editor-useMagicKey =
.label = 使用魔法键 "/" 显示命令面板
editor-useMarkdownPaste =
.label = 使用增强的Markdown粘贴
sync-title = 同步
sync-period-label = 自动同步周期 (秒)

View File

@ -6,22 +6,12 @@ pref("__prefsPrefix__.autoAnnotation", false);
pref("__prefsPrefix__.insertLinkPosition", "end");
pref("__prefsPrefix__.embedLink", true);
pref("__prefsPrefix__.standaloneLink", false);
pref("__prefsPrefix__.keepLink", true);
pref("__prefsPrefix__.exportMD", true);
pref("__prefsPrefix__.setAutoSync", false);
pref("__prefsPrefix__.withYAMLHeader", false);
pref("__prefsPrefix__.autoMDFileName", false);
pref("__prefsPrefix__.exportDocx", false);
pref("__prefsPrefix__.exportPDF", false);
pref("__prefsPrefix__.exportFreeMind", false);
pref("__prefsPrefix__.exportNote", false);
pref("__prefsPrefix__.workspace.outline.expandLevel", 2);
pref("__prefsPrefix__.workspace.outline.keepLinks", true);
pref("__prefsPrefix__.editor.noteLinkPreviewType", "hover");
pref("__prefsPrefix__.editor.useMagicKey", true);
pref("__prefsPrefix__.editor.useMarkdownPaste", true);
pref("__prefsPrefix__.openNote.takeover", true);
pref("__prefsPrefix__.openNote.defaultAsWindow", false);

14
package-lock.json generated
View File

@ -10042,9 +10042,9 @@
}
},
"node_modules/prosemirror-model": {
"version": "1.19.4",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.4.tgz",
"integrity": "sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==",
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
"integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
"dev": true,
"dependencies": {
"orderedmap": "^2.0.0"
@ -10062,12 +10062,12 @@
}
},
"node_modules/prosemirror-transform": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz",
"integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
"dev": true,
"dependencies": {
"prosemirror-model": "^1.0.0"
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {

View File

@ -34,6 +34,8 @@ export class NotePicker extends PluginCEBase {
_prefObserverID!: symbol;
_cachedLibraryIDs: number[] = [];
get content() {
return MozXULElement.parseXULToFragment(`
<linkset>
@ -394,6 +396,12 @@ export class NotePicker extends PluginCEBase {
onItemSelected() {
this.activeSelectionType = "library";
const selectedIDs = this.itemsView.getSelectedItems(true) as number[];
// Compare the selected IDs with the cached IDs
// Since the library selection change can be triggered multiple times or with no change
if (arraysEqual(this._cachedLibraryIDs, selectedIDs)) {
return;
}
this.dispatchSelectionChange();
}
@ -483,3 +491,18 @@ export class NotePicker extends PluginCEBase {
}
}
}
function arraysEqual(arr1: number[], arr2: number[]): boolean {
if (arr1.length !== arr2.length) return false;
const set1 = new Set(arr1);
const set2 = new Set(arr2);
if (set1.size !== set2.size) return false;
for (const item of set1) {
if (!set2.has(item)) return false;
}
return true;
}

View File

@ -83,7 +83,7 @@ export class OutboundCreator extends PluginCEBase {
await this.notePicker.load();
this.notePicker.addEventListener("selectionchange", (event: any) => {
this.targetNotes = event.detail.selectedNotes;
this.targetNotes = this.notePicker.getSelectedNotes();
this.updatePickerTitle(this.targetNotes);
this.updateNotePreview();
if (this.targetNotes) this.scrollToSection("outline");

View File

@ -0,0 +1,75 @@
import { insert } from "../../utils/editor";
export { formatMessage };
function formatMessage(message: string, locale: string) {
const stringObj = editorStrings[message as keyof typeof editorStrings];
if (!stringObj) {
return message;
}
return stringObj[locale as "en-US" | "zh-CN"] || message;
}
const editorStrings = {
insertTemplate: {
"en-US": "Insert Template",
"zh-CN": "插入模板",
},
outboundLink: {
"en-US": "Insert Outbound Link (Link to another note)",
"zh-CN": "插入出链 (链接到另一个笔记)",
},
inboundLink: {
"en-US": "Insert Inbound Link in another note (Link to this note)",
"zh-CN": "插入入链到另一笔记 (链接到本笔记)",
},
insertCitation: {
"en-US": "Insert Citation",
"zh-CN": "插入引用",
},
heading1: {
"en-US": "Heading 1",
"zh-CN": "一级标题",
},
heading2: {
"en-US": "Heading 2",
"zh-CN": "二级标题",
},
heading3: {
"en-US": "Heading 3",
"zh-CN": "三级标题",
},
paragraph: {
"en-US": "Paragraph",
"zh-CN": "段落",
},
monospaced: {
"en-US": "Monospaced",
"zh-CN": "等宽",
},
bulletList: {
"en-US": "Bullet List",
"zh-CN": "无序列表",
},
orderedList: {
"en-US": "Ordered List",
"zh-CN": "有序列表",
},
blockquote: {
"en-US": "Blockquote",
"zh-CN": "引用",
},
mathBlock: {
"en-US": "Math Block",
"zh-CN": "数学",
},
clearFormatting: {
"en-US": "Clear Format",
"zh-CN": "清除格式",
},
table: {
"en-US": "Table",
"zh-CN": "表格",
},
};

View File

@ -15,10 +15,10 @@ interface LinkPreviewOptions {
openURL: (url: string) => void;
requireCtrl: boolean;
previewType: "hover" | "ctrl" | "disable";
}
class LinkPreviewState {
class PluginState {
state: EditorState;
options: LinkPreviewOptions;
@ -40,6 +40,10 @@ class LinkPreviewState {
update(state: EditorState, prevState?: EditorState) {
this.state = state;
if (this.options.previewType === "disable") {
return;
}
if (
prevState &&
prevState.doc.eq(state.doc) &&
@ -58,6 +62,10 @@ class LinkPreviewState {
}
handleMouseMove = async (event: MouseEvent) => {
if (this.options.previewType === "disable") {
return;
}
const { target } = event;
let isValid = false;
@ -82,9 +90,10 @@ class LinkPreviewState {
};
handleKeydown = async (event: KeyboardEvent) => {
if (!this.options.requireCtrl) {
if (this.options.previewType !== "ctrl") {
return;
}
if (!this.hasHover || !this.currentLink) {
return;
}
@ -96,9 +105,10 @@ class LinkPreviewState {
};
tryOpenPopupByHover() {
if (this.options.requireCtrl) {
if (this.options.previewType !== "hover") {
return;
}
const href = this.currentLink!;
setTimeout(() => {
if (this.currentLink === href) {
@ -204,7 +214,7 @@ function initLinkPreviewPlugin(
key,
state: {
init(config, state) {
return new LinkPreviewState(state, options);
return new PluginState(state, options);
},
apply: (tr, pluginState, oldState, newState) => {
pluginState.update(newState, oldState);
@ -214,16 +224,16 @@ function initLinkPreviewPlugin(
props: {
handleDOMEvents: {
mousemove: (view, event) => {
const pluginState = key.getState(view.state) as LinkPreviewState;
const pluginState = key.getState(view.state) as PluginState;
pluginState.update(view.state);
pluginState.handleMouseMove(event);
},
keydown: (view, event) => {
const pluginState = key.getState(view.state) as LinkPreviewState;
const pluginState = key.getState(view.state) as PluginState;
pluginState.handleKeydown(event);
},
wheel: (view, event) => {
const pluginState = key.getState(view.state) as LinkPreviewState;
const pluginState = key.getState(view.state) as PluginState;
pluginState.popup?.layoutPopup(pluginState);
},
},
@ -231,13 +241,11 @@ function initLinkPreviewPlugin(
view: (editorView) => {
return {
update(view, prevState) {
const pluginState = key.getState(view.state) as LinkPreviewState;
const pluginState = key.getState(view.state) as PluginState;
pluginState.update(view.state, prevState);
},
destroy() {
const pluginState = key.getState(
editorView.state,
) as LinkPreviewState;
const pluginState = key.getState(editorView.state) as PluginState;
pluginState.destroy();
},
};

View File

@ -0,0 +1,523 @@
import { EditorState, Plugin, PluginKey, Transaction } from "prosemirror-state";
import { Popup } from "./popup";
import { formatMessage } from "./editorStrings";
export { initMagicKeyPlugin, MagicKeyOptions };
declare const _currentEditorInstance: {
_editorCore: EditorCore;
};
interface MagicKeyOptions {
insertTemplate?: () => void;
insertLink?: (type: "inbound" | "outbound") => void;
enable?: boolean;
}
interface MagicCommand {
messageId?: string;
title?: string;
icon?: string;
command: (state: EditorState) => void | Transaction;
}
class PluginState {
state: EditorState;
options: MagicKeyOptions;
commands: MagicCommand[] = [
{
messageId: "insertTemplate",
command: (state) => {
this.options.insertTemplate?.();
},
},
{
messageId: "outboundLink",
command: (state) => {
this.options.insertLink?.("outbound");
},
},
{
messageId: "inboundLink",
command: (state) => {
this.options.insertLink?.("inbound");
},
},
{
messageId: "insertCitation",
command: (state) => {
getPlugin("citation")?.insertCitation();
},
},
{
messageId: "table",
command: (state) => {
const input = prompt(
"Enter the number of rows and columns, separated by a comma (e.g., 3,3)",
);
if (!input) {
return state.tr;
}
const splitter = input.includes("x")
? "x"
: input.includes(",")
? ","
: " ";
const [rows, cols] = input.split(splitter).map((n) => parseInt(n, 10));
if (isNaN(rows) || isNaN(cols)) {
return state.tr;
}
const { tr, selection } = state;
const { $from, $to } = selection;
const { pos } = $from;
const table = state.schema.nodes.table.createAndFill(
{},
Array.from(
{ length: rows },
() =>
state.schema.nodes.table_row.createAndFill(
{},
Array.from(
{ length: cols },
() => state.schema.nodes.table_cell.createAndFill()!,
),
)!,
),
)!;
tr.replaceWith(pos, pos, table);
_currentEditorInstance._editorCore.view.dispatch(tr);
},
},
{
messageId: "heading1",
command: (state) => {
getPlugin()?.heading1.run();
},
},
{
messageId: "heading2",
command: (state) => {
getPlugin()?.heading2.run();
},
},
{
messageId: "heading3",
command: (state) => {
getPlugin()?.heading3.run();
},
},
{
messageId: "paragraph",
command: (state) => {
getPlugin()?.paragraph.run();
},
},
{
messageId: "monospaced",
command: (state) => {
getPlugin()?.codeBlock.run();
},
},
{
messageId: "bulletList",
command: (state) => {
getPlugin()?.bulletList.run();
},
},
{
messageId: "orderedList",
command: (state) => {
getPlugin()?.orderedList.run();
},
},
{
messageId: "blockquote",
command: (state) => {
getPlugin()?.blockquote.run();
},
},
{
messageId: "mathBlock",
command: (state) => {
getPlugin()?.math_display.run();
},
},
{
messageId: "clearFormatting",
command: (state) => {
getPlugin()?.clearFormatting.run();
},
},
];
popup: Popup | null = null;
selectedCommandIndex = 0;
get node() {
const node =
// @ts-ignore - private API
_currentEditorInstance._editorCore.view.domSelection().anchorNode;
if (node.nodeType === Node.TEXT_NODE) {
return node.parentElement;
}
return node;
}
popupClass = "command-palette";
constructor(state: EditorState, options: MagicKeyOptions) {
this.state = state;
this.options = options;
const locale = window.navigator.language || "en-US";
for (const key in this.commands) {
const command = this.commands[key];
if (command.messageId) {
command.title = formatMessage(command.messageId, locale);
}
}
this.update(state);
}
update(state: EditorState, prevState?: EditorState) {
this.state = state;
if (!prevState || prevState.doc.eq(state.doc)) {
return;
}
// When `/` is pressed, we should open the command palette
const selectionText = state.doc.textBetween(
state.selection.from,
state.selection.to,
);
if (!selectionText) {
const { $from } = this.state.selection;
const { parent } = $from;
// Don't open the popup if we are in the document root
if (parent.type.name === "doc") {
return;
}
const text = parent.textContent;
if (text.endsWith("/") && !text.endsWith("//")) {
this._openPopup(state);
} else {
this._closePopup();
}
}
}
destroy() {
this.popup?.remove();
}
handleKeydown = async (event: KeyboardEvent) => {
if (!this._hasPopup()) {
return;
}
if (event.key === "Escape") {
this._closePopup();
}
};
_openPopup(state: EditorState) {
if (this._hasPopup()) {
return;
}
this.popup = new Popup(document, this.popupClass, [
document.createRange().createContextualFragment(`
<style>
.${this.popupClass} > .popup {
max-width: 360px;
max-height: 360px;
overflow: hidden;
}
.${this.popupClass} > .popup input {
padding: 0 7px;
background: var(--material-background);
border-radius: 5px;
border: var(--material-border-quinary);
width: 100%;
outline: none;
height: 28px;
flex-shrink: 0;
}
.${this.popupClass} > .popup input:focus {
outline: none;
border-color: rgba(0, 0, 0, 0);
box-shadow: 0 0 0 var(--width-focus-border) var(--color-focus-search);
}
.${this.popupClass} .popup-content {
display: flex;
flex-direction: column;
gap: 6px;
padding: 6px;
}
.${this.popupClass} .popup-list {
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden auto;
}
.${this.popupClass} .popup-item {
padding: 6px;
cursor: pointer;
border-radius: 5px;
}
.${this.popupClass} .popup-item:hover {
background-color: var(--fill-senary);
}
.${this.popupClass} .popup-item.selected {
background-color: var(--color-accent);
color: #fff;
}
</style>
<div class="popup-content">
<input type="text" class="popup-input" placeholder="Search commands" />
<div class="popup-list">
${Object.entries(this.commands)
.map(
([id, command]) => `
<div class="popup-item" data-command-id="${id}">
<div class="popup-item-icon">${command.icon || ""}</div>
<div class="popup-item-title">${command.title}</div>
</div>`,
)
.join("")}
</div>
</div>`),
]);
this.popup.layoutPopup(this);
this.popup.container.scrollIntoView({
block: "nearest",
});
// Focus the input
const input = this.popup.container.querySelector(
".popup-input",
) as HTMLInputElement;
input.focus();
// Handle input
input.addEventListener("input", (event) => {
const target = event.target as HTMLInputElement;
const value = target.value;
for (const [id, command] of Object.entries(this.commands)) {
const item = this.popup!.container.querySelector(
`.popup-item[data-command-id="${id}"]`,
) as HTMLElement;
const matchedIndex = command
.title!.toLowerCase()
.indexOf(value.toLowerCase());
if (matchedIndex >= 0) {
// Change the matched part to bold
const title = command.title!;
item.querySelector(".popup-item-title")!.innerHTML =
title.slice(0, matchedIndex) +
`<b>${title.slice(matchedIndex, matchedIndex + value.length)}</b>` +
title.slice(matchedIndex + value.length);
item.hidden = false;
} else {
item.hidden = true;
}
}
this._selectCommand();
});
input.addEventListener("blur", () => {
if (__env__ === "development") {
return;
}
this._closePopup();
});
input.addEventListener("keydown", (event) => {
if (event.key === "ArrowUp") {
this._selectCommand(this.selectedCommandIndex - 1);
event.preventDefault();
} else if (event.key === "ArrowDown") {
this._selectCommand(this.selectedCommandIndex + 1);
event.preventDefault();
} else if (event.key === "Enter") {
event.preventDefault();
const command = this.commands[this.selectedCommandIndex];
if (!command) {
this._closePopup();
return;
}
this._executeCommand(this.selectedCommandIndex, state);
} else if (event.key === "Escape") {
this._closePopup();
} else if (event.key === "z" && (event.ctrlKey || event.metaKey)) {
this._closePopup();
this.removeInputSlash(state);
}
});
this.popup.container.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const target = event.target as HTMLElement;
// Find the command
const item = target.closest(".popup-item");
if (!item) {
return;
}
const index = Array.from(item.parentElement!.children).indexOf(item);
this._executeCommand(index, state);
});
this._selectCommand(0);
}
_closePopup() {
if (!this._hasPopup()) {
return;
}
document
.querySelectorAll(`.${this.popupClass}`)
.forEach((el) => el.remove());
this.popup = null;
window.BetterNotesEditorAPI.refocusEditor();
}
_hasPopup() {
return !!document.querySelector(`.${this.popupClass}`);
}
_selectCommand(index?: number) {
if (typeof index === "undefined") {
index = this.selectedCommandIndex;
}
// Unselect the previous command
this.popup!.container.querySelectorAll(".popup-item.selected").forEach(
(el) => {
el.classList.remove("selected");
},
);
if (!this._hasPopup()) {
return;
}
const items = this.popup!.container.querySelectorAll(
".popup-item",
) as NodeListOf<HTMLElement>;
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import { initLinkPreview } from "../../utils/editor";
export function initEditorLinkPreview(editor: Zotero.EditorInstance) {
initLinkPreview(editor);
}

View File

@ -0,0 +1,5 @@
import { initEditorPlugins as initEditorPluginsIntl } from "../../utils/editor";
export function initEditorPlugins(editor: Zotero.EditorInstance) {
initEditorPluginsIntl(editor);
}

View File

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

View File

@ -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(
`<p style="color: red;">Invalid note link: ${link}</p>`,
);
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(
`<p style="color: red;">Invalid note link: ${link}</p>`,
);
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 },

View File

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

View File

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

View File

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