Files
outline/shared/editor/plugins/Suggestions.ts

115 lines
3.2 KiB
TypeScript

import { action } from "mobx";
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
const MAX_MATCH = 500;
type Options = {
openRegex: RegExp;
closeRegex: RegExp;
enabledInCode: true;
};
type ExtensionState = {
open: boolean;
query: string;
};
export class SuggestionsMenuPlugin extends Plugin {
constructor(options: Options, extensionState: ExtensionState) {
super({
props: {
handleKeyDown: (view, event) => {
// Prosemirror input rules are not triggered on backspace, however
// we need them to be evaluted for the filter trigger to work
// correctly. This additional handler adds inputrules-like handling.
if (event.key === "Backspace") {
// timeout ensures that the delete has been handled by prosemirror
// and any characters removed, before we evaluate the rule.
setTimeout(() => {
const { pos: fromPos } = view.state.selection.$from;
return this.execute(
view,
fromPos,
fromPos,
options.openRegex,
action((_, match) => {
if (match) {
extensionState.open = true;
extensionState.query = match[1];
} else {
extensionState.open = false;
extensionState.query = "";
}
return null;
})
);
});
}
const { pos } = view.state.selection.$from;
// If the query is active and we're navigating the block menu then
// just ignore the key events in the editor itself until we're done
if (
event.key === "Enter" ||
event.key === "ArrowUp" ||
event.key === "ArrowDown" ||
event.key === "Tab"
) {
return this.execute(
view,
pos,
pos,
options.openRegex,
(state, match) =>
// just tell Prosemirror we handled it and not to do anything
match ? true : null
);
}
return false;
},
},
});
}
// based on the input rules code in Prosemirror, here:
// https://github.com/ProseMirror/prosemirror-inputrules/blob/master/src/inputrules.js
private execute(
view: EditorView,
from: number,
to: number,
regex: RegExp,
handler: (
state: EditorState,
match: RegExpExecArray | null,
from?: number,
to?: number
) => boolean | null
) {
if (view.composing) {
return false;
}
const state = view.state;
const $from = state.doc.resolve(from);
if ($from.parent.type.spec.code) {
return false;
}
const textBefore = $from.parent.textBetween(
Math.max(0, $from.parentOffset - MAX_MATCH),
$from.parentOffset,
undefined,
"\ufffc"
);
const match = regex.exec(textBefore);
const tr = handler(state, match, match ? from - match[0].length : from, to);
if (!tr) {
return false;
}
return true;
}
}