Files
outline/shared/editor/lib/Extension.ts
T
Tom Moor c3ba14f069 chore: Refactor SelectionToolbar to menu registry (#12439)
* refactor: introduce declarative menu registry for selection toolbar

Replace the hard-coded if-else chain in SelectionToolbar with a
priority-based menu registry system. Extensions can now declare
selection toolbar menus via `selectionToolbarMenus()`, following the
same pattern as `commands()` and `keys()`.

Key changes:
- Add SelectionContext interface computed once per toolbar render
- Add SelectionToolbarMenuDescriptor for declarative menu registration
- Add selectionToolbarMenus() to Extension base class
- Add buildSelectionContext() utility to eliminate repeated state queries
- ExtensionManager collects and sorts menus from all extensions
- SelectionToolbarExtension registers all 10 existing menus
- All menu functions now accept SelectionContext instead of raw state
- SelectionToolbar uses registry lookup instead of if-else chain

https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU

* refactor: import t directly from i18next in menu functions

Remove the `t: TFunction` parameter from all menu functions and the
`SelectionToolbarMenuDescriptor.getItems` signature. Each menu file
now imports `t` directly from i18next, matching the pattern used
throughout the rest of the codebase (e.g. Image.tsx, Link.tsx).

https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU

* refactor: move divider menu into HorizontalRule node extension

The divider selection toolbar menu is now declared via
selectionToolbarMenus() on the HorizontalRule node class, co-locating
the menu with the node that owns it. Delete the standalone
app/editor/menus/divider.tsx file and remove the entry from
SelectionToolbarExtension.

This is the first menu migrated from the centralized toolbar extension
to an individual node extension, demonstrating the pattern for the
remaining menus.

https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU

* refactor: check readOnly in matches predicate for divider menu

https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-27 20:28:17 -04:00

124 lines
3.3 KiB
TypeScript

import type { PluginSimple } from "markdown-it";
import type { InputRule } from "prosemirror-inputrules";
import type { NodeType, MarkType, Schema } from "prosemirror-model";
import type { Command, Plugin, Selection } from "prosemirror-state";
import type { Editor } from "../../../app/editor";
import type { SelectionToolbarMenuDescriptor } from "../types";
export type CommandFactory = (attrs?: unknown, options?: unknown) => Command;
export type WidgetProps = {
rtl: boolean;
readOnly: boolean | undefined;
selection?: Selection;
};
export default class Extension<TOptions extends object = object> {
options: TOptions;
editor: Editor;
constructor(options: Partial<TOptions> = {}) {
this.options = {
...this.defaultOptions,
...options,
} as TOptions;
}
bindEditor(editor: Editor) {
this.editor = editor;
}
get type() {
return "extension";
}
get name() {
return "";
}
get plugins(): Plugin[] {
return [];
}
get rulePlugins(): PluginSimple[] {
return [];
}
get defaultOptions(): Partial<TOptions> {
return {};
}
/**
* Whether this extension is needed in read-only mode. When false (default), pure Extension types
* are not instantiated and their commands are blocked. Node and Mark extensions are always
* instantiated for the schema regardless of this setting.
*/
get allowInReadOnly(): boolean {
return false;
}
get focusAfterExecution(): boolean {
return true;
}
/**
* A widget is a React component to be rendered in the editor's context, independent of any
* specific node or mark. It can be used to render things like toolbars, menus, etc. Note that
* all widgets are observed automatically, so you can use observable values.
*
* @returns A React component
*/
widget(_props: WidgetProps): React.ReactElement | undefined {
return undefined;
}
/**
* A map of ProseMirror keymap bindings. It can be used to bind keyboard shortcuts to commands.
*
* @returns An object mapping key bindings to commands
*/
keys(_options: {
type?: NodeType | MarkType;
schema: Schema;
}): Record<string, Command | CommandFactory> {
return {};
}
/**
* A map of ProseMirror input rules. It can be used to automatically replace certain patterns
* while typing.
*
* @returns An array of input rules
*/
inputRules(_options: {
type?: NodeType | MarkType;
schema: Schema;
}): InputRule[] {
return [];
}
/**
* A map of ProseMirror commands. It can be used to expose commands to the editor. If a single
* command is returned, it will be available under the extension's name.
*
* @returns An object mapping command names to command factories, or a command factory
*/
commands(_options: {
type?: NodeType | MarkType;
schema: Schema;
}): Record<string, CommandFactory> | CommandFactory | undefined {
return {};
}
/**
* Declares selection toolbar menus contributed by this extension. When
* the user selects content or clicks a node, the toolbar evaluates all
* registered menus in priority order and displays the first match.
*
* @returns an array of menu descriptors, or an empty array if this extension does not contribute menus.
*/
selectionToolbarMenus(): SelectionToolbarMenuDescriptor[] {
return [];
}
}