feat: Inline editor menu (#12611)

* wip

* Mobile support

* Address review feedback on inline menu

- Mark selection-restore transaction as not added to history
- Only open desktop inline menu when an anchor is available

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-06-07 07:57:34 -04:00
committed by GitHub
parent 492af6683b
commit ea665b80ee
17 changed files with 693 additions and 268 deletions
+12
View File
@@ -273,7 +273,19 @@ export default class ExtensionManager {
return;
}
if (extension.focusAfterExecution) {
// Focusing a blurred editor (e.g. when the command is run from a
// menu that holds focus) can collapse a non-text selection such as
// a table cell selection. Restore it so selection-based commands
// operate on the intended selection.
const { selection } = view.state;
view.focus();
if (!view.state.selection.eq(selection)) {
view.dispatch(
view.state.tr
.setSelection(selection)
.setMeta("addToHistory", false)
);
}
}
return callback(attrs)?.(view.state, view.dispatch, view);
};
+4 -1
View File
@@ -6,6 +6,7 @@ import type { EditorView } from "prosemirror-view";
import { DecorationSet, Decoration } from "prosemirror-view";
import { isInTable, moveTableColumn, TableMap } from "prosemirror-tables";
import { addColumnBefore, selectColumn } from "../commands/table";
import { isMobile } from "../../utils/browser";
import {
getCellAttrs,
isValidCellAlignment,
@@ -326,7 +327,9 @@ export default class TableHeader extends Node {
)
);
if (!isDragging) {
// The add-column affordance is too small to tap on mobile, where
// columns can be added via the inline menu instead.
if (!isDragging && !isMobile()) {
if (index === 0) {
decorations.push(buildAddColumnDecoration(pos, index));
}
+4 -1
View File
@@ -8,6 +8,7 @@ import { Decoration, DecorationSet } from "prosemirror-view";
import type { EditorView } from "prosemirror-view";
import { Plugin } from "prosemirror-state";
import { addRowBefore, selectRow, selectTable } from "../commands/table";
import { isMobile } from "../../utils/browser";
import {
getCellsInRow,
getRowsInTable,
@@ -339,7 +340,9 @@ export default class TableRow extends Node {
)
);
if (!isDragging) {
// The add-row affordance is too small to tap on mobile, where
// rows can be added via the inline menu instead.
if (!isDragging && !isMobile()) {
if (index === 0) {
decorations.push(buildAddRowDecoration(pos, index));
}
+14
View File
@@ -17,6 +17,14 @@ export enum TableLayout {
fullWidth = "full-width",
}
/** How a selection toolbar menu is presented. */
export enum MenuType {
/** A horizontal strip of buttons; nested options open behind a trigger. */
toolbar = "toolbar",
/** A vertical menu rendered directly, anchored to the selection. */
inline = "inline",
}
type Section = ({ t }: { t: TFunction }) => string;
export type MenuItem = {
@@ -140,6 +148,12 @@ export interface SelectionToolbarMenuDescriptor {
priority: number;
/** Toolbar alignment when this menu is active. Defaults to "center". */
align?: "center" | "start" | "end";
/**
* How the menu is presented. "toolbar" (default) renders a horizontal strip
* of buttons; "inline" renders a vertical menu anchored to the selection
* without requiring a trigger button.
*/
variant?: MenuType;
/**
* Returns the menu items to display for the current selection.
*
+5 -1
View File
@@ -567,6 +567,7 @@
"Replacement": "Replacement",
"Replace": "Replace",
"Replace all": "Replace all",
"Options": "Options",
"Go to link": "Go to link",
"Open link": "Open link",
"Remove link": "Remove link",
@@ -647,10 +648,13 @@
"Edit image URL": "Edit image URL",
"Default width": "Default width",
"Distribute columns": "Distribute columns",
"Delete table": "Delete table",
"Export as CSV": "Export as CSV",
"Delete table": "Delete table",
"Align": "Align",
"Sort": "Sort",
"Sort ascending": "Sort ascending",
"Sort descending": "Sort descending",
"Background": "Background",
"Toggle header": "Toggle header",
"Insert after": "Insert after",
"Insert before": "Insert before",