@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
233 lines • 10.3 kB
text/typescript
import { type Position } from "./useEditableUtils.mjs";
import type { EditingEngineLoader } from "./editingEngineCache.mjs";
type History = [Position, string];
export interface State {
disconnected: boolean;
onChange(text: string, position: Position, preParseResult?: unknown): void;
pendingContent: string | null;
queue: MutationRecord[];
history: History[];
historyAt: number;
/**
* The text most recently reported via `onChange` (i.e. last seen by the
* controlled host), independent of the undo stack. Lets the
* external-swap detector recover edits that the 500ms dedup kept out
* of `history`: when the host swaps the editable's content, anything
* the user typed since the last history checkpoint is still reachable
* here and gets pushed onto the stack just before the swap is
* recorded. Cleared on every undo/redo so we don't double-record after
* navigating the existing history.
*/
lastCommittedContent: string | null;
/**
* Set whenever the MutationObserver sees DOM changes between renders,
* cleared after the snapshot block consumes them. Lets the per-render
* layout effect skip the O(N) `toString` walk on idle re-renders
* (parent updates, async state syncs, variant switches that don't
* actually touch the editable's DOM). React's reconciliation of an
* unchanged highlighted subtree produces zero mutation records, so
* `domDirty` stays false and the snapshot is a no-op.
*/
domDirty: boolean;
position: Position | null;
/** setTimeout id used to debounce flushChanges() calls during key-repeat */
repeatFlushId: ReturnType<typeof setTimeout> | null;
/**
* AbortController for the in-flight `preParse` callback (if any). Reset
* on every new flush so a rapidly-typed sequence aborts stale parses
* before posting a fresh request.
*/
preParseAbort: AbortController | null;
/**
* Set when an arrow-key handler invokes `onBoundary` (which typically
* triggers a host re-render to expand a collapsed region). The native
* arrow-key default action moves the caret AFTER our keydown handler
* returns, but the host's re-render commits BEFORE the resulting
* `selectionchange` updates `state.position`. Without this flag, the
* unconditional restore effect would snap the caret back to the stale
* pre-arrow `state.position` on that intermediate render. The flag is
* cleared after one skipped restore.
*/
skipNextRestore: boolean;
}
export interface Options<TPreParseResult = unknown> {
disabled?: boolean;
indentation?: number;
/**
* Minimum column the cursor is allowed to occupy on indented lines.
*
* When set, horizontal arrow navigation skips over the leading whitespace
* up to `minColumn` so the caret never lands inside a clipped/hidden
* indent region:
*
* - `ArrowLeft` at column `minColumn` (with that line's first `minColumn`
* characters all whitespace) jumps to the end of the previous line
* instead of stepping into the indent.
* - `ArrowRight` at the end of a line jumps to column `minColumn` of the
* next line (when the next line is indented at least that far) instead
* of landing at column 0.
*
* Useful when the editor is rendered in a horizontally-shifted view (for
* example a collapsed code block whose left padding is translated off
* screen) where columns below `minColumn` are not visible. Leave
* `undefined` for default arrow-key behavior.
*/
minColumn?: number;
/**
* First row of the visible region. When set, `ArrowUp` on this row and
* `ArrowLeft` at the start of this row are blocked (no caret movement)
* and `onBoundary` is invoked. Useful when content above the visible
* region is hidden and the host wants a chance to reveal it.
*/
minRow?: number;
/**
* Last row of the visible region. When set, `ArrowDown` on this row and
* `ArrowRight` at the end of this row are blocked (no caret movement)
* and `onBoundary` is invoked.
*/
maxRow?: number;
/**
* Called when the user attempts to navigate past `minRow`/`maxRow` via
* arrow keys. When `onBoundary` is provided, the navigation is allowed
* to proceed natively so the host can react (e.g. expand a collapsed
* code block) and the caret continues moving in the now-visible
* content. When `onBoundary` is omitted, the navigation is blocked
* (caret stays put).
*/
onBoundary?: () => void;
/**
* CSS selector identifying the elements that represent selectable
* "lines" inside the editable. When set, and only while the caret is
* actually inside an element matching the selector:
*
* - `ArrowLeft` at column 0 jumps synchronously to the end of the
* previous line.
* - `ArrowRight` at the end of a line jumps synchronously to the start
* of the next line.
*
* Useful when the editable contains intentionally-empty whitespace
* text nodes between block-level children (e.g. newline text nodes
* separating `.line` spans inside a `.frame`). Without this, the
* browser would place the caret in those gap nodes on horizontal
* navigation, making `ArrowLeft`/`ArrowRight` appear to no-op.
*
* Vertical navigation (`ArrowUp`/`ArrowDown`) is intentionally left to
* the browser so wrapped visual lines in `pre-wrap` layouts continue
* to behave natively. Gap nodes styled with `line-height: 0` are
* skipped by browsers vertically without intervention.
*
* The selector is matched against the caret's containing element via
* `Element.closest`, so non-`.line` render paths (e.g. plain-string
* editables) never trigger the wrap behavior.
*/
caretSelector?: string;
/**
* Optional async pre-parse hook invoked before each `onChange` flush.
* When provided, the parser receives the post-edit `text` and caret
* `position` plus an `AbortSignal` that fires when a newer keystroke
* supersedes this flush. Its resolved value is forwarded as the third
* argument to `onChange`, allowing the host to cache an already-parsed
* HAST (or any other derived state) keyed off the same source string.
*
* If `preParse` is omitted, `onChange` runs synchronously inside the
* keyup / debounce handler as before. If it is provided, the React
* state sync is delayed until the returned promise settles. Structural
* edits that need a synchronous re-render (Enter, paste, cut, undo/redo,
* programmatic `edit.update`/`edit.insert`, `minColumn` blank-line
* collapse) bypass `preParse` and fire `onChange` immediately without
* a third argument.
*/
preParse?: (text: string, position: Position, signal: AbortSignal) => Promise<TPreParseResult>;
/**
* Loads the editing engine module on demand. Supplied by `CodeProvider` via
* context (eager → bundled, resolves instantly; lazy → dynamic `import()`).
* When omitted, `useEditable` falls back to a built-in dynamic import so
* editing still works without a provider.
*/
engineLoader?: EditingEngineLoader;
/**
* Controls when the editing engine loads once the block is editable:
* `'eager'` (default) loads it immediately; `'interaction'` defers until the
* user hovers, focuses, or clicks the editable.
*/
activation?: 'eager' | 'interaction';
/**
* Called once when the block is first activated for editing — immediately in
* `'eager'` mode, or on first engagement (hover / focus / click) in
* `'interaction'` mode. Lets the host warm the rest of the live-editing
* dependencies (grammars, worker) at the right moment, especially when
* `'interaction'` has deferred them.
*/
onActivate?: () => void;
}
export interface Edit {
/** Replaces the entire content of the editable while adjusting the caret position. */
update(content: string): void;
/** Inserts new text at the caret position while deleting text in range of the offset (which accepts negative offsets). */
insert(append: string, offset?: number): void;
/** Positions the caret where specified */
move(pos: number | {
row: number;
column: number;
}): void;
/** Returns the current editor state, as usually received in onChange */
getState(): {
text: string;
position: Position;
};
}
export type Bounds = {
minColumn?: number;
minRow?: number;
maxRow?: number;
onBoundary?: () => void;
caretSelector?: string;
preParse?: (text: string, position: Position, signal: AbortSignal) => Promise<unknown>;
};
/**
* Everything {@link createEditableEngine} needs from its host hook. `useEditable`
* owns this state and these refs so they survive this module's lazy load; the
* engine only reads and mutates them, and they are shared by reference so the
* engine's handlers always observe live values.
*/
export interface EditableEngineContext {
elementRef: {
current: HTMLElement | undefined | null;
};
state: State;
observerRef: {
current: MutationObserver | null;
};
boundsRef: {
current: Bounds;
};
configRef: {
current: Options;
};
unblock: (value: never[]) => void;
}
/**
* The heavy editing runtime bound to a host element. `setup` applies
* `contentEditable` and binds the keyboard/paste/caret handlers; `observeAndRestore`
* runs the per-render MutationObserver + caret-restore pass. Each returns its cleanup.
*/
export interface EditableEngine {
edit: Edit;
observeAndRestore(): (() => void) | undefined;
setup(): (() => void) | undefined;
}
export type CreateEditableEngine = (ctx: EditableEngineContext) => EditableEngine;
/**
* Resolves the editing engine factory. `CodeProvider` supplies one via context
* (eager → bundled, resolves instantly; lazy → dynamic `import()`); `useEditable`
* also has a built-in fallback so editing works without a provider.
*/
export type EditableEngineLoader = () => Promise<CreateEditableEngine>;
/**
* Builds the editing engine for a host element. This module statically imports
* the heavy editing utilities (`useEditableUtils`, `cloneRangeWithInlineStyles`,
* `stripLeadingPerLine`) and `react-dom`, so the bundler emits it as a separate
* chunk that `useEditable` loads on demand — read-only code blocks never pull it in.
*/
export declare const createEditableEngine: CreateEditableEngine;
export {};