Files
outline/app/editor/extensions/Suggestion.ts
T
Tom Moor 8c716b173a chore: Update editor generics (#12247)
* chore: Update editor generics

* fix: Address PR review on editor generics

- Restore null-guard on Link click handler so anchors aren't inert when no onClickLink is provided
- Mark onClickLink optional in LinkOptions and openLink command to match runtime
- Remove dead `collapsed` option from HeadingOptions
- Make ToggleBlock dictionary optional and restore optional-chained access for server-side schema instantiation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:54:27 -04:00

98 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import escapeRegExp from "lodash/escapeRegExp";
import { action, observable } from "mobx";
import { InputRule } from "prosemirror-inputrules";
import type { NodeType, Schema } from "prosemirror-model";
import type { EditorState, Plugin } from "prosemirror-state";
import Extension from "@shared/editor/lib/Extension";
import { SuggestionsMenuPlugin } from "@shared/editor/plugins/SuggestionsMenuPlugin";
import { isInCode } from "@shared/editor/queries/isInCode";
/**
* Options shared by all suggestion-style extensions (block menu, emoji menu,
* mention menu).
*/
export type SuggestionOptions = {
/** Whether the suggestion menu is allowed to open inside code blocks or inline code. */
enabledInCode: boolean;
/** Character (or list of characters) that opens the suggestion menu. */
trigger: string | string[];
/** Whether spaces are allowed inside the search term. */
allowSpaces: boolean;
/** Whether the menu only opens once at least one character has been typed after the trigger. */
requireSearchTerm: boolean;
};
export default class Suggestion<
TOptions extends SuggestionOptions = SuggestionOptions,
> extends Extension<TOptions> {
constructor(options: TOptions) {
super(options);
const triggers = Array.isArray(this.options.trigger)
? this.options.trigger
: [this.options.trigger];
const triggerPattern =
triggers.length === 1
? escapeRegExp(triggers[0])
: `(?:${triggers.map(escapeRegExp).join("|")})`;
this.openRegex = new RegExp(
`(?:^|\\s|\\(|[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}])${triggerPattern}(${`[\\p{L}/\\p{M}\\d${
this.options.allowSpaces ? "\\s{1}" : ""
}\\.\\-_]+`})${this.options.requireSearchTerm ? "" : "?"}$`,
"u"
);
}
get plugins(): Plugin[] {
return [new SuggestionsMenuPlugin(this.state, this.openRegex)];
}
keys() {
return {
Space: action(() => {
if (this.state.open && !this.options.allowSpaces) {
this.state.open = false;
}
return false;
}),
};
}
inputRules = (_options: { type: NodeType; schema: Schema }) => [
new InputRule(
this.openRegex,
action((state: EditorState, match: RegExpMatchArray) => {
const { parent } = state.selection.$from;
if (
match &&
(parent.type.name === "paragraph" ||
parent.type.name === "heading") &&
(!isInCode(state) || this.options.enabledInCode)
) {
if (match[0].length <= 2) {
this.state.open = true;
}
this.state.query = match[1];
}
return null;
})
),
];
protected openRegex: RegExp;
protected state: {
open: boolean;
query: string;
} = observable({
open: false,
query: "",
});
/** Whether the suggestion menu is currently open. */
get isOpen(): boolean {
return this.state.open;
}
}