fix: Prevent block menu trigger when marked (#12515)

* Prevent block menu trigger when marked

* PR feedback
This commit is contained in:
Tom Moor
2026-05-28 21:30:53 -04:00
committed by GitHub
parent 3f92e96006
commit 1eba87020c
3 changed files with 89 additions and 22 deletions
+3 -1
View File
@@ -17,6 +17,7 @@ export default class BlockMenuExtension extends Suggestion {
allowSpaces: false,
requireSearchTerm: false,
enabledInCode: false,
enabledInMarks: false,
};
}
@@ -87,7 +88,8 @@ export default class BlockMenuExtension extends Suggestion {
condition: ({ node, $start, state }) =>
$start.depth === 1 &&
state.selection.$from.pos === $start.pos + node.content.size &&
node.textContent === "/",
node.textContent === "/" &&
node.firstChild?.marks.length === 0,
text: ` ${t("Keep typing to filter")}`,
},
]),
+43 -15
View File
@@ -4,7 +4,10 @@ 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 {
isTriggerMarked,
SuggestionsMenuPlugin,
} from "@shared/editor/plugins/SuggestionsMenuPlugin";
import { isInCode } from "@shared/editor/queries/isInCode";
/**
@@ -14,6 +17,12 @@ import { isInCode } from "@shared/editor/queries/isInCode";
export type SuggestionOptions = {
/** Whether the suggestion menu is allowed to open inside code blocks or inline code. */
enabledInCode: boolean;
/**
* Whether the suggestion menu may open when the trigger character carries a
* mark (e.g. bold, italic, link). Defaults to true disable for menus where
* the trigger is only meaningful as plain text, such as the block menu.
*/
enabledInMarks?: boolean;
/** Character (or list of characters) that opens the suggestion menu. */
trigger: string | string[];
/** Whether spaces are allowed inside the search term. */
@@ -45,7 +54,18 @@ export default class Suggestion<
}
get plugins(): Plugin[] {
return [new SuggestionsMenuPlugin(this.state, this.openRegex)];
return [
new SuggestionsMenuPlugin(
this.state,
this.openRegex,
this.enabledInMarks
),
];
}
/** Whether the menu may open when the trigger character carries a mark. */
protected get enabledInMarks(): boolean {
return this.options.enabledInMarks ?? true;
}
keys() {
@@ -62,21 +82,29 @@ export default class Suggestion<
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;
action(
(
state: EditorState,
match: RegExpMatchArray,
_start: number,
end: number
) => {
const { parent } = state.selection.$from;
if (
match &&
(parent.type.name === "paragraph" ||
parent.type.name === "heading") &&
(!isInCode(state) || this.options.enabledInCode) &&
(this.enabledInMarks || !isTriggerMarked(state, end, match))
) {
if (match[0].length <= 2) {
this.state.open = true;
}
this.state.query = match[1];
}
this.state.query = match[1];
return null;
}
return null;
})
)
),
];
+43 -6
View File
@@ -2,6 +2,7 @@ import { action } from "mobx";
import type { EditorState } from "prosemirror-state";
import { Plugin } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import { getMarksBetween } from "@shared/editor/queries/getMarksBetween";
const MAX_MATCH = 500;
@@ -10,8 +11,35 @@ type ExtensionState = {
query: string;
};
/**
* Determine whether the trigger character of a suggestion match carries any
* marks (e.g. bold, code, link).
*
* @param state The editor state.
* @param cursorPos The document position of the cursor (end of the match).
* @param match The regex match where group 1 is the search term.
* @returns True if the trigger character has one or more marks applied.
*/
export function isTriggerMarked(
state: EditorState,
cursorPos: number,
match: RegExpMatchArray
): boolean {
const queryLength = match[1]?.length ?? 0;
const triggerEnd = cursorPos - queryLength;
const triggerStart = triggerEnd - 1;
if (triggerStart < 0) {
return false;
}
return getMarksBetween(triggerStart, triggerEnd, state).length > 0;
}
export class SuggestionsMenuPlugin extends Plugin {
constructor(extensionState: ExtensionState, openRegex: RegExp) {
constructor(
extensionState: ExtensionState,
openRegex: RegExp,
enabledInMarks: boolean
) {
super({
props: {
handleDOMEvents: {
@@ -34,7 +62,10 @@ export class SuggestionsMenuPlugin extends Plugin {
);
const match = openRegex.exec(textBefore);
action(() => {
if (match) {
if (
match &&
(enabledInMarks || !isTriggerMarked(state, fromPos, match))
) {
if (match[0].length <= 2) {
extensionState.open = true;
}
@@ -59,8 +90,11 @@ export class SuggestionsMenuPlugin extends Plugin {
fromPos,
fromPos,
openRegex,
action((_, match) => {
if (match) {
action((state, match) => {
if (
match &&
(enabledInMarks || !isTriggerMarked(state, fromPos, match))
) {
extensionState.query = match[1];
} else {
extensionState.open = false;
@@ -89,8 +123,11 @@ export class SuggestionsMenuPlugin extends Plugin {
fromPos,
fromPos,
openRegex,
action((_, match) => {
if (match) {
action((state, match) => {
if (
match &&
(enabledInMarks || !isTriggerMarked(state, fromPos, match))
) {
if (match[0].length <= 2) {
extensionState.open = true;
}