UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

233 lines 10.3 kB
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 {};