add: magic key command palette
This commit is contained in:
parent
968eb0ef40
commit
e44e07eefa
|
|
@ -59,6 +59,16 @@
|
||||||
<radio data-l10n-id="editor-noteLinkPreview-disable" value="disable" />
|
<radio data-l10n-id="editor-noteLinkPreview-disable" value="disable" />
|
||||||
</radiogroup>
|
</radiogroup>
|
||||||
</hbox>
|
</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>
|
||||||
<groupbox>
|
<groupbox>
|
||||||
<label><html:h2 data-l10n-id="sync-title"></html:h2></label>
|
<label><html:h2 data-l10n-id="sync-title"></html:h2></label>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
|
||||||
}
|
}
|
||||||
editor-noteLinkPreview-disable =
|
editor-noteLinkPreview-disable =
|
||||||
.label = Never
|
.label = Never
|
||||||
|
editor-useMagicKey =
|
||||||
|
.label = Use magic key "/" to show command palette
|
||||||
|
editor-useMarkdownPaste =
|
||||||
|
.label = Use enhanced markdown paste
|
||||||
|
|
||||||
sync-title = Sync
|
sync-title = Sync
|
||||||
sync-period-label = Auto-sync period (seconds)
|
sync-period-label = Auto-sync period (seconds)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
|
||||||
}
|
}
|
||||||
editor-noteLinkPreview-disable =
|
editor-noteLinkPreview-disable =
|
||||||
.label = Never
|
.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-title = Sincronizzazione
|
||||||
sync-period-label = Intervallo della sincronizzazione automatica (secondi)
|
sync-period-label = Intervallo della sincronizzazione automatica (secondi)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
|
||||||
}
|
}
|
||||||
editor-noteLinkPreview-disable =
|
editor-noteLinkPreview-disable =
|
||||||
.label = Never
|
.label = Never
|
||||||
|
editor-useMagicKey =
|
||||||
|
.label = Использовать магическую клавишу "/" для отображения панели команд
|
||||||
|
editor-useMarkdownPaste =
|
||||||
|
.label = Использовать расширенное вставление Markdown
|
||||||
|
|
||||||
sync-title = Синк
|
sync-title = Синк
|
||||||
sync-period-label = Авто-синк период (сек)
|
sync-period-label = Авто-синк период (сек)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
|
||||||
}
|
}
|
||||||
editor-noteLinkPreview-disable =
|
editor-noteLinkPreview-disable =
|
||||||
.label = Never
|
.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-title = Eşitle
|
||||||
sync-period-label = Otomatik Eşitleme Sıklığı (saniye)
|
sync-period-label = Otomatik Eşitleme Sıklığı (saniye)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ editor-noteLinkPreview-ctrl =
|
||||||
}
|
}
|
||||||
editor-noteLinkPreview-disable =
|
editor-noteLinkPreview-disable =
|
||||||
.label = 从不
|
.label = 从不
|
||||||
|
editor-useMagicKey =
|
||||||
|
.label = 使用魔法键 "/" 显示命令面板
|
||||||
|
editor-useMarkdownPaste =
|
||||||
|
.label = 使用增强的Markdown粘贴
|
||||||
|
|
||||||
sync-title = 同步
|
sync-title = 同步
|
||||||
sync-period-label = 自动同步周期 (秒)
|
sync-period-label = 自动同步周期 (秒)
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,12 @@ pref("__prefsPrefix__.autoAnnotation", false);
|
||||||
|
|
||||||
pref("__prefsPrefix__.insertLinkPosition", "end");
|
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.expandLevel", 2);
|
||||||
pref("__prefsPrefix__.workspace.outline.keepLinks", true);
|
pref("__prefsPrefix__.workspace.outline.keepLinks", true);
|
||||||
|
|
||||||
pref("__prefsPrefix__.editor.noteLinkPreviewType", "hover");
|
pref("__prefsPrefix__.editor.noteLinkPreviewType", "hover");
|
||||||
|
pref("__prefsPrefix__.editor.useMagicKey", true);
|
||||||
|
pref("__prefsPrefix__.editor.useMarkdownPaste", true);
|
||||||
|
|
||||||
pref("__prefsPrefix__.openNote.takeover", true);
|
pref("__prefsPrefix__.openNote.takeover", true);
|
||||||
pref("__prefsPrefix__.openNote.defaultAsWindow", false);
|
pref("__prefsPrefix__.openNote.defaultAsWindow", false);
|
||||||
|
|
|
||||||
|
|
@ -10042,9 +10042,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-model": {
|
"node_modules/prosemirror-model": {
|
||||||
"version": "1.19.4",
|
"version": "1.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
|
||||||
"integrity": "sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==",
|
"integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
|
|
@ -10062,12 +10062,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-transform": {
|
"node_modules/prosemirror-transform": {
|
||||||
"version": "1.8.0",
|
"version": "1.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
|
||||||
"integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==",
|
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0"
|
"prosemirror-model": "^1.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-view": {
|
"node_modules/prosemirror-view": {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ export class NotePicker extends PluginCEBase {
|
||||||
|
|
||||||
_prefObserverID!: symbol;
|
_prefObserverID!: symbol;
|
||||||
|
|
||||||
|
_cachedLibraryIDs: number[] = [];
|
||||||
|
|
||||||
get content() {
|
get content() {
|
||||||
return MozXULElement.parseXULToFragment(`
|
return MozXULElement.parseXULToFragment(`
|
||||||
<linkset>
|
<linkset>
|
||||||
|
|
@ -394,6 +396,12 @@ export class NotePicker extends PluginCEBase {
|
||||||
|
|
||||||
onItemSelected() {
|
onItemSelected() {
|
||||||
this.activeSelectionType = "library";
|
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();
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export class OutboundCreator extends PluginCEBase {
|
||||||
await this.notePicker.load();
|
await this.notePicker.load();
|
||||||
|
|
||||||
this.notePicker.addEventListener("selectionchange", (event: any) => {
|
this.notePicker.addEventListener("selectionchange", (event: any) => {
|
||||||
this.targetNotes = event.detail.selectedNotes;
|
this.targetNotes = this.notePicker.getSelectedNotes();
|
||||||
this.updatePickerTitle(this.targetNotes);
|
this.updatePickerTitle(this.targetNotes);
|
||||||
this.updateNotePreview();
|
this.updateNotePreview();
|
||||||
if (this.targetNotes) this.scrollToSection("outline");
|
if (this.targetNotes) this.scrollToSection("outline");
|
||||||
|
|
|
||||||
|
|
@ -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": "表格",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -15,10 +15,10 @@ interface LinkPreviewOptions {
|
||||||
|
|
||||||
openURL: (url: string) => void;
|
openURL: (url: string) => void;
|
||||||
|
|
||||||
requireCtrl: boolean;
|
previewType: "hover" | "ctrl" | "disable";
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkPreviewState {
|
class PluginState {
|
||||||
state: EditorState;
|
state: EditorState;
|
||||||
|
|
||||||
options: LinkPreviewOptions;
|
options: LinkPreviewOptions;
|
||||||
|
|
@ -40,6 +40,10 @@ class LinkPreviewState {
|
||||||
update(state: EditorState, prevState?: EditorState) {
|
update(state: EditorState, prevState?: EditorState) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
|
if (this.options.previewType === "disable") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevState &&
|
prevState &&
|
||||||
prevState.doc.eq(state.doc) &&
|
prevState.doc.eq(state.doc) &&
|
||||||
|
|
@ -58,6 +62,10 @@ class LinkPreviewState {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove = async (event: MouseEvent) => {
|
handleMouseMove = async (event: MouseEvent) => {
|
||||||
|
if (this.options.previewType === "disable") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { target } = event;
|
const { target } = event;
|
||||||
|
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
|
|
@ -82,9 +90,10 @@ class LinkPreviewState {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleKeydown = async (event: KeyboardEvent) => {
|
handleKeydown = async (event: KeyboardEvent) => {
|
||||||
if (!this.options.requireCtrl) {
|
if (this.options.previewType !== "ctrl") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.hasHover || !this.currentLink) {
|
if (!this.hasHover || !this.currentLink) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -96,9 +105,10 @@ class LinkPreviewState {
|
||||||
};
|
};
|
||||||
|
|
||||||
tryOpenPopupByHover() {
|
tryOpenPopupByHover() {
|
||||||
if (this.options.requireCtrl) {
|
if (this.options.previewType !== "hover") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const href = this.currentLink!;
|
const href = this.currentLink!;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.currentLink === href) {
|
if (this.currentLink === href) {
|
||||||
|
|
@ -204,7 +214,7 @@ function initLinkPreviewPlugin(
|
||||||
key,
|
key,
|
||||||
state: {
|
state: {
|
||||||
init(config, state) {
|
init(config, state) {
|
||||||
return new LinkPreviewState(state, options);
|
return new PluginState(state, options);
|
||||||
},
|
},
|
||||||
apply: (tr, pluginState, oldState, newState) => {
|
apply: (tr, pluginState, oldState, newState) => {
|
||||||
pluginState.update(newState, oldState);
|
pluginState.update(newState, oldState);
|
||||||
|
|
@ -214,16 +224,16 @@ function initLinkPreviewPlugin(
|
||||||
props: {
|
props: {
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
mousemove: (view, event) => {
|
mousemove: (view, event) => {
|
||||||
const pluginState = key.getState(view.state) as LinkPreviewState;
|
const pluginState = key.getState(view.state) as PluginState;
|
||||||
pluginState.update(view.state);
|
pluginState.update(view.state);
|
||||||
pluginState.handleMouseMove(event);
|
pluginState.handleMouseMove(event);
|
||||||
},
|
},
|
||||||
keydown: (view, event) => {
|
keydown: (view, event) => {
|
||||||
const pluginState = key.getState(view.state) as LinkPreviewState;
|
const pluginState = key.getState(view.state) as PluginState;
|
||||||
pluginState.handleKeydown(event);
|
pluginState.handleKeydown(event);
|
||||||
},
|
},
|
||||||
wheel: (view, event) => {
|
wheel: (view, event) => {
|
||||||
const pluginState = key.getState(view.state) as LinkPreviewState;
|
const pluginState = key.getState(view.state) as PluginState;
|
||||||
pluginState.popup?.layoutPopup(pluginState);
|
pluginState.popup?.layoutPopup(pluginState);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -231,13 +241,11 @@ function initLinkPreviewPlugin(
|
||||||
view: (editorView) => {
|
view: (editorView) => {
|
||||||
return {
|
return {
|
||||||
update(view, prevState) {
|
update(view, prevState) {
|
||||||
const pluginState = key.getState(view.state) as LinkPreviewState;
|
const pluginState = key.getState(view.state) as PluginState;
|
||||||
pluginState.update(view.state, prevState);
|
pluginState.update(view.state, prevState);
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
const pluginState = key.getState(
|
const pluginState = key.getState(editorView.state) as PluginState;
|
||||||
editorView.state,
|
|
||||||
) as LinkPreviewState;
|
|
||||||
pluginState.destroy();
|
pluginState.destroy();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import { Plugin, PluginKey } from "prosemirror-state";
|
import { Plugin, PluginKey } from "prosemirror-state";
|
||||||
import { md2html } from "../../utils/convert";
|
import { md2html } from "../../utils/convert";
|
||||||
|
|
||||||
export { initPasteMarkdownPlugin };
|
export { initMarkdownPastePlugin, MarkdownPasteOptions };
|
||||||
|
|
||||||
declare const _currentEditorInstance: {
|
declare const _currentEditorInstance: {
|
||||||
_editorCore: EditorCore;
|
_editorCore: EditorCore;
|
||||||
};
|
};
|
||||||
|
|
||||||
function initPasteMarkdownPlugin(plugins: readonly Plugin[]) {
|
interface MarkdownPasteOptions {
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMarkdownPastePlugin(plugins: readonly Plugin[]) {
|
||||||
const core = _currentEditorInstance._editorCore;
|
const core = _currentEditorInstance._editorCore;
|
||||||
console.log("Init BN Paste Markdown Plugin");
|
console.log("Init BN Markdown Paste Plugin");
|
||||||
const key = new PluginKey("pasteDropPlugin");
|
const key = new PluginKey("pasteDropPlugin");
|
||||||
const oldPastePluginIndex = plugins.findIndex(
|
const oldPastePluginIndex = plugins.findIndex(
|
||||||
(plugin) => plugin.props.handlePaste && plugin.props.handleDrop,
|
(plugin) => plugin.props.handlePaste && plugin.props.handleDrop,
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { initLinkPreviewPlugin, LinkPreviewOptions } from "./linkPreview";
|
import { initLinkPreviewPlugin, LinkPreviewOptions } from "./linkPreview";
|
||||||
import { initPasteMarkdownPlugin } from "./pasteMarkdown";
|
import { initMagicKeyPlugin, MagicKeyOptions } from "./magicKey";
|
||||||
|
import { initMarkdownPastePlugin, MarkdownPasteOptions } from "./markdownPaste";
|
||||||
|
|
||||||
export { initPlugins };
|
export { initPlugins };
|
||||||
|
|
||||||
|
|
@ -7,11 +8,18 @@ declare const _currentEditorInstance: {
|
||||||
_editorCore: EditorCore;
|
_editorCore: EditorCore;
|
||||||
};
|
};
|
||||||
|
|
||||||
function initPlugins(options: LinkPreviewOptions) {
|
function initPlugins(options: {
|
||||||
|
linkPreview: LinkPreviewOptions;
|
||||||
|
magicKey: MagicKeyOptions;
|
||||||
|
markdownPaste: MarkdownPasteOptions;
|
||||||
|
}) {
|
||||||
const core = _currentEditorInstance._editorCore;
|
const core = _currentEditorInstance._editorCore;
|
||||||
let plugins = core.view.state.plugins;
|
let plugins = core.view.state.plugins;
|
||||||
plugins = initLinkPreviewPlugin(plugins, options);
|
if (options.linkPreview.previewType !== "disable")
|
||||||
plugins = initPasteMarkdownPlugin(plugins);
|
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
|
// Collect all plugins and reconfigure the state only once
|
||||||
const newState = core.view.state.reconfigure({
|
const newState = core.view.state.reconfigure({
|
||||||
plugins,
|
plugins,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ class Popup {
|
||||||
|
|
||||||
hasHover = false;
|
hasHover = false;
|
||||||
|
|
||||||
|
className: string;
|
||||||
|
|
||||||
get container() {
|
get container() {
|
||||||
return this._popup;
|
return this._popup;
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +20,7 @@ class Popup {
|
||||||
className?: string,
|
className?: string,
|
||||||
children: (HTMLElement | DocumentFragment)[] = [],
|
children: (HTMLElement | DocumentFragment)[] = [],
|
||||||
) {
|
) {
|
||||||
|
this.className = className || "";
|
||||||
this._popup = doc.createElement("div");
|
this._popup = doc.createElement("div");
|
||||||
this._popup.className = `popup-container ${className}`;
|
this._popup.className = `popup-container ${className}`;
|
||||||
this._popup.innerHTML = `
|
this._popup.innerHTML = `
|
||||||
|
|
@ -58,7 +61,7 @@ class Popup {
|
||||||
// Bottom
|
// Bottom
|
||||||
const otherPopupHeight = Array.from(
|
const otherPopupHeight = Array.from(
|
||||||
popupParent.querySelectorAll(
|
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);
|
).reduce((acc, el) => acc + (el as HTMLElement).offsetHeight, 0);
|
||||||
top =
|
top =
|
||||||
|
|
@ -72,7 +75,7 @@ class Popup {
|
||||||
// Top
|
// Top
|
||||||
const otherPopupHeight = Array.from(
|
const otherPopupHeight = Array.from(
|
||||||
popupParent.querySelectorAll(
|
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);
|
).reduce((acc, el) => acc + (el as HTMLElement).offsetHeight, 0);
|
||||||
top =
|
top =
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ let io: {
|
||||||
targetNoteID?: number;
|
targetNoteID?: number;
|
||||||
content?: string;
|
content?: string;
|
||||||
lineIndex?: number;
|
lineIndex?: number;
|
||||||
|
mode?: "inbound" | "outbound";
|
||||||
};
|
};
|
||||||
|
|
||||||
window.onload = async function () {
|
window.onload = async function () {
|
||||||
|
|
@ -50,9 +51,20 @@ function init() {
|
||||||
|
|
||||||
io = window.arguments[0];
|
io = window.arguments[0];
|
||||||
|
|
||||||
|
if (!io.deferred) {
|
||||||
|
// @ts-ignore
|
||||||
|
io = io.wrappedJSObject;
|
||||||
|
}
|
||||||
|
|
||||||
tabbox = document.querySelector("#top-container")!;
|
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);
|
tabbox.addEventListener("select", loadSelectedPanel);
|
||||||
|
|
||||||
inboundCreator = document.querySelector(
|
inboundCreator = document.querySelector(
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,12 @@ messageServer.start();
|
||||||
|
|
||||||
async function addLink(model: LinkModel) {
|
async function addLink(model: LinkModel) {
|
||||||
await db.link.add(model);
|
await db.link.add(model);
|
||||||
|
log("addLink", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkAddLink(models: LinkModel[]) {
|
async function bulkAddLink(models: LinkModel[]) {
|
||||||
await db.link.bulkAdd(models);
|
await db.link.bulkAdd(models);
|
||||||
|
log("bulkAddLink", models);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildLinkForNote(
|
async function rebuildLinkForNote(
|
||||||
|
|
@ -51,15 +53,17 @@ async function rebuildLinkForNote(
|
||||||
) {
|
) {
|
||||||
log("rebuildLinkForNote", fromLibID, fromKey, links);
|
log("rebuildLinkForNote", fromLibID, fromKey, links);
|
||||||
|
|
||||||
const collection = db.link.where({ fromLibID, fromKey });
|
return db.transaction("rw", db.link, async () => {
|
||||||
const oldOutboundLinks = await collection.toArray();
|
const collection = db.link.where({ fromLibID, fromKey });
|
||||||
await collection.delete().then((deleteCount) => {
|
const oldOutboundLinks = await collection.toArray();
|
||||||
log("Deleted " + deleteCount + " objects");
|
await collection.delete().then((deleteCount) => {
|
||||||
return bulkAddLink(links);
|
log("Deleted " + deleteCount + " objects");
|
||||||
|
return bulkAddLink(links);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
oldOutboundLinks,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
return {
|
|
||||||
oldOutboundLinks,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOutboundLinks(fromLibID: number, fromKey: string) {
|
async function getOutboundLinks(fromLibID: number, fromKey: string) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ document.addEventListener("DOMContentLoaded", (ev) => {
|
||||||
|
|
||||||
document.addEventListener("dialogaccept", () => accept());
|
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 templateData = args.templates;
|
||||||
const multiSelect = args.multiSelect;
|
const multiSelect = args.multiSelect;
|
||||||
let tableHelper: VirtualizedTableHelper;
|
let tableHelper: VirtualizedTableHelper;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { initEditorImagePreviewer } from "./image";
|
import { initEditorImagePreviewer } from "./image";
|
||||||
import { injectEditorCSS, injectEditorScripts } from "./inject";
|
import { injectEditorCSS, injectEditorScripts } from "./inject";
|
||||||
import { initEditorLinkPreview } from "./linkPreview";
|
import { initEditorPlugins } from "./plugins";
|
||||||
import { initEditorMenu } from "./menu";
|
import { initEditorMenu } from "./menu";
|
||||||
import { initEditorPopup } from "./popup";
|
import { initEditorPopup } from "./popup";
|
||||||
import { initEditorToolbar } from "./toolbar";
|
import { initEditorToolbar } from "./toolbar";
|
||||||
|
|
@ -38,5 +38,5 @@ async function onEditorInstanceCreated(editor: Zotero.EditorInstance) {
|
||||||
await initEditorToolbar(editor);
|
await initEditorToolbar(editor);
|
||||||
initEditorPopup(editor);
|
initEditorPopup(editor);
|
||||||
initEditorMenu(editor);
|
initEditorMenu(editor);
|
||||||
initEditorLinkPreview(editor);
|
initEditorPlugins(editor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { initLinkPreview } from "../../utils/editor";
|
|
||||||
|
|
||||||
export function initEditorLinkPreview(editor: Zotero.EditorInstance) {
|
|
||||||
initLinkPreview(editor);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { initEditorPlugins as initEditorPluginsIntl } from "../../utils/editor";
|
||||||
|
|
||||||
|
export function initEditorPlugins(editor: Zotero.EditorInstance) {
|
||||||
|
initEditorPluginsIntl(editor);
|
||||||
|
}
|
||||||
|
|
@ -19,14 +19,16 @@ export function initEditorPopup(editor: Zotero.EditorInstance) {
|
||||||
ztoolkit.log(mut);
|
ztoolkit.log(mut);
|
||||||
if (
|
if (
|
||||||
(mut.addedNodes.length &&
|
(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.attributeName === "href" &&
|
||||||
mut.target.parentElement?.classList.contains("link"))
|
mut.target.parentElement?.classList.contains("link"))
|
||||||
) {
|
) {
|
||||||
updateEditorLinkPopup(editor);
|
updateEditorLinkPopup(editor);
|
||||||
} else if (
|
} else if (
|
||||||
mut.addedNodes.length &&
|
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);
|
updateEditorImagePopup(editor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import TreeModel = require("tree-model");
|
||||||
import { TextSelection } from "prosemirror-state";
|
import { TextSelection } from "prosemirror-state";
|
||||||
import { getNoteTreeFlattened } from "./note";
|
import { getNoteTreeFlattened } from "./note";
|
||||||
import { getPref } from "./prefs";
|
import { getPref } from "./prefs";
|
||||||
|
import { openLinkCreator } from "./linkCreator";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
insert,
|
insert,
|
||||||
|
|
@ -26,7 +27,7 @@ export {
|
||||||
getTextBetween,
|
getTextBetween,
|
||||||
getTextBetweenLines,
|
getTextBetweenLines,
|
||||||
isImageAtCursor,
|
isImageAtCursor,
|
||||||
initLinkPreview,
|
initEditorPlugins,
|
||||||
};
|
};
|
||||||
|
|
||||||
function insert(
|
function insert(
|
||||||
|
|
@ -446,7 +447,7 @@ function getTextBetweenLines(
|
||||||
return core.view.state.doc.textBetween(from, to);
|
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;
|
const previewType = getPref("editor.noteLinkPreviewType") as string;
|
||||||
if (!["hover", "ctrl"].includes(previewType)) {
|
if (!["hover", "ctrl"].includes(previewType)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -456,29 +457,49 @@ function initLinkPreview(editor: Zotero.EditorInstance) {
|
||||||
EditorAPI.initPlugins(
|
EditorAPI.initPlugins(
|
||||||
Components.utils.cloneInto(
|
Components.utils.cloneInto(
|
||||||
{
|
{
|
||||||
setPreviewContent: (
|
linkPreview: {
|
||||||
link: string,
|
setPreviewContent: (
|
||||||
setContent: (content: string) => void,
|
link: string,
|
||||||
) => {
|
setContent: (content: string) => void,
|
||||||
const note = addon.api.convert.link2note(link);
|
) => {
|
||||||
if (!note) {
|
const note = addon.api.convert.link2note(link);
|
||||||
setContent(
|
if (!note) {
|
||||||
`<p style="color: red;">Invalid note link: ${link}</p>`,
|
setContent(
|
||||||
);
|
`<p style="color: red;">Invalid note link: ${link}</p>`,
|
||||||
return;
|
);
|
||||||
}
|
return;
|
||||||
addon.api.convert
|
}
|
||||||
.link2html(link, {
|
addon.api.convert
|
||||||
noteItem: note,
|
.link2html(link, {
|
||||||
dryRun: true,
|
noteItem: note,
|
||||||
usePosition: true,
|
dryRun: true,
|
||||||
})
|
usePosition: true,
|
||||||
.then((content) => setContent(content));
|
})
|
||||||
|
.then((content) => setContent(content));
|
||||||
|
},
|
||||||
|
openURL: (url: string) => {
|
||||||
|
Zotero.getActiveZoteroPane().loadURI(url);
|
||||||
|
},
|
||||||
|
previewType,
|
||||||
},
|
},
|
||||||
openURL: (url: string) => {
|
magicKey: {
|
||||||
Zotero.getActiveZoteroPane().loadURI(url);
|
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,
|
editor._iframeWindow,
|
||||||
{ wrapReflectors: true, cloneFunctions: true },
|
{ wrapReflectors: true, cloneFunctions: true },
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ export { openLinkCreator };
|
||||||
async function openLinkCreator(
|
async function openLinkCreator(
|
||||||
currentNote: Zotero.Item,
|
currentNote: Zotero.Item,
|
||||||
options?: {
|
options?: {
|
||||||
lineIndex: number;
|
mode?: "inbound" | "outbound";
|
||||||
|
lineIndex?: number;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (!currentNote.id) {
|
if (!currentNote.id) {
|
||||||
|
|
@ -25,9 +26,13 @@ async function openLinkCreator(
|
||||||
),
|
),
|
||||||
currentNoteID: currentNote.id,
|
currentNoteID: currentNote.id,
|
||||||
currentLineIndex: options?.lineIndex,
|
currentLineIndex: options?.lineIndex,
|
||||||
|
mode: options?.mode,
|
||||||
deferred: Zotero.Promise.defer(),
|
deferred: Zotero.Promise.defer(),
|
||||||
} as any;
|
} as any;
|
||||||
Zotero.getMainWindow().openDialog(
|
|
||||||
|
Services.ww.openWindow(
|
||||||
|
// @ts-ignore
|
||||||
|
null,
|
||||||
`chrome://${config.addonRef}/content/linkCreator.xhtml`,
|
`chrome://${config.addonRef}/content/linkCreator.xhtml`,
|
||||||
`${config.addonRef}-linkCreator`,
|
`${config.addonRef}-linkCreator`,
|
||||||
"chrome,modal,centerscreen,resizable=yes",
|
"chrome,modal,centerscreen,resizable=yes",
|
||||||
|
|
@ -42,6 +47,4 @@ async function openLinkCreator(
|
||||||
if (!targetNote || !content) return;
|
if (!targetNote || !content) return;
|
||||||
|
|
||||||
await addLineToNote(targetNote, content, lineIndex);
|
await addLineToNote(targetNote, content, lineIndex);
|
||||||
|
|
||||||
await addon.api.relation.updateNoteLinkRelation(targetNote.id);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ export {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function updateNoteLinkRelation(noteID: number) {
|
async function updateNoteLinkRelation(noteID: number) {
|
||||||
|
ztoolkit.log("updateNoteLinkRelation", noteID);
|
||||||
const note = Zotero.Items.get(noteID);
|
const note = Zotero.Items.get(noteID);
|
||||||
const affectedNoteIDs = new Set([noteID]);
|
const affectedNoteIDs = new Set([noteID]);
|
||||||
const fromLibID = note.libraryID;
|
const fromLibID = note.libraryID;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ export async function openTemplatePicker(
|
||||||
selected: [] as string[],
|
selected: [] as string[],
|
||||||
_initPromise: Zotero.Promise.defer(),
|
_initPromise: Zotero.Promise.defer(),
|
||||||
};
|
};
|
||||||
Zotero.getMainWindow().openDialog(
|
// @ts-ignore
|
||||||
|
// args.wrappedJSObject = args;
|
||||||
|
Services.ww.openWindow(
|
||||||
|
// @ts-ignore
|
||||||
|
null,
|
||||||
`chrome://${config.addonRef}/content/templatePicker.xhtml`,
|
`chrome://${config.addonRef}/content/templatePicker.xhtml`,
|
||||||
"_blank",
|
"_blank",
|
||||||
"chrome,modal,centerscreen,resizable=yes",
|
"chrome,modal,centerscreen,resizable=yes",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue