chore: Cleanup editor menu handlers (#5174

* wip

* wip

* refactor
This commit is contained in:
Tom Moor
2023-04-10 18:50:21 -04:00
committed by GitHub
parent 70f3a998a4
commit 60dbad765a
15 changed files with 502 additions and 572 deletions

View File

@@ -0,0 +1,67 @@
import { Plugin, PluginKey } from "prosemirror-state";
import { findBlockNodes } from "prosemirror-utils";
import { Decoration, DecorationSet } from "prosemirror-view";
import Storage from "../../utils/Storage";
import { headingToPersistenceKey } from "../lib/headingToSlug";
import findCollapsedNodes from "../queries/findCollapsedNodes";
export class FoldingHeadersPlugin extends Plugin {
constructor(documentId: string | undefined) {
const plugin = new PluginKey("folding");
let loaded = false;
super({
key: plugin,
view: (view) => {
loaded = false;
view.dispatch(view.state.tr.setMeta("folding", { loaded: true }));
return {};
},
appendTransaction: (transactions, oldState, newState) => {
if (loaded) {
return;
}
if (
!transactions.some((transaction) => transaction.getMeta("folding"))
) {
return;
}
let modified = false;
const tr = newState.tr;
const blocks = findBlockNodes(newState.doc);
for (const block of blocks) {
if (block.node.type.name === "heading") {
const persistKey = headingToPersistenceKey(block.node, documentId);
const persistedState = Storage.get(persistKey);
if (persistedState === "collapsed") {
tr.setNodeMarkup(block.pos, undefined, {
...block.node.attrs,
collapsed: true,
});
modified = true;
}
}
}
loaded = true;
return modified ? tr : null;
},
props: {
decorations: (state) => {
const { doc } = state;
const decorations: Decoration[] = findCollapsedNodes(doc).map(
(block) =>
Decoration.node(block.pos, block.pos + block.node.nodeSize, {
class: "folded-content",
})
);
return DecorationSet.create(doc, decorations);
},
},
});
}
}

View File

@@ -0,0 +1,126 @@
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import type { Editor } from "../../../app/editor";
import { EventType } from "../types";
const MAX_MATCH = 500;
export enum SuggestionsMenuType {
Emoji = "emoji",
Block = "block",
Mention = "mention",
}
type Options = {
type: SuggestionsMenuType;
openRegex: RegExp;
closeRegex: RegExp;
enabledInCode: true;
enabledInTable: true;
};
export class SuggestionsMenuPlugin extends Plugin {
constructor(editor: Editor, options: Options) {
super({
props: {
handleClick: () => {
editor.events.emit(options.type);
return false;
},
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 } = view.state.selection.$from;
return this.execute(
view,
pos,
pos,
options.openRegex,
(state, match) => {
if (match) {
editor.events.emit(EventType.SuggestionsMenuOpen, {
type: options.type,
query: match[1],
});
} else {
editor.events.emit(
EventType.SuggestionsMenuClose,
options.type
);
}
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;
}
}