Headless reference

quoin-editor/headless is the editing engine and its editable view, with none of the canvas UI. It exports everything in the Engine reference and the names here on top of it: the React layer that draws a document, and the controller that binds an editor to the page.

Each one carries the signature the published types carry, read from the package’s own source each time the site is built. Headless shows the layer working, in one page of code.

Hooks and components

The hook that creates the editor and its controller, and the components that draw the document and keep the selection.

BlockPlaceholder

The text an empty block shows in its main region, read again whenever the editor state changes.

type BlockPlaceholder = (state: EditorState, block: EngineBlock) => string | undefined;

BlockPlaceholder on GitHub

BlockRenderer

type BlockRenderer = ComponentType<BlockRenderProps>;

BlockRenderer on GitHub

BlockRenderProps

type BlockRenderProps = { block: EngineBlock; controller: ViewController };

BlockRenderProps on GitHub

EditableRegion

function EditableRegion(props: EditableRegionProps): JSX.Element

EditableRegion on GitHub

EditableRegionProps

type EditableRegionProps = {
  block: string;
  region: string;
  label: string;
  /** An id for the host, for a control that the host's text names. */
  id?: string;
  placeholder?: string;
  className?: string;
};

EditableRegionProps on GitHub

EditorView

The editable view with none of the canvas UI: one editable host per text block, drawn by the renderers given to it. It keeps the element it drew for each block, so define renderers once rather than on every render.

function EditorView(props: EditorViewProps): JSX.Element

EditorView on GitHub

EditorViewProps

type EditorViewProps = {
  controller: ViewController;
  renderers?: Readonly<Record<string, BlockRenderer>>;
  className?: string;
  id?: string;
  'aria-label'?: string;
  /** What the editor's accessible description says, in place of the English way out. */
  hint?: string;
  /** The accessible name of the paragraph an empty document offers to type into. */
  paragraphLabel?: string;
  /** The accessible name of the hidden textbox that holds a selection across blocks. */
  selectedContentLabel?: string;
  /** The name of a block selected as a whole, such as an image's alt text, in place of `selectedContentLabel`. */
  blockLabel?: BlockLabel;
  /** The text and accessible name of a block whose type no renderer draws, in place of the English one. */
  unsupportedBlockLabel?: string;
  /** The accessible name of what a block draws in place of itself when its renderer throws. */
  blockErrorLabel?: string;
  /** Told of each error a block's renderer throws, besides React's own report of a caught error. */
  onBlockError?: (error: Error) => void;
  /** The text of the host an empty document draws. */
  placeholder?: string;
  /** The placeholder of each empty block, by default the name of a heading and nothing for other blocks. */
  blockPlaceholder?: BlockPlaceholder;
  /** What the live region says when blocks are selected, given how many it draws, in place of the English words. */
  blocksSelectedLabel?: (count: number) => string;
  /** Controls drawn over the blocks, such as a gutter, positioned against the editor's root. */
  children?: ReactNode;
};

EditorViewProps on GitHub

useEditorState

function useEditorState<T>(
  controller: ViewController,
  selector: (state: EditorState) => T,
  isEqual?: (a: T, b: T) => boolean,
): T

useEditorState on GitHub

useQuoinEditor

The editor and the controller that binds it to the page, for a host that draws its own chrome around EditorView. The editor, its extensions and its render options are read once; the callbacks are read through a ref, so a new function identity on every render changes nothing.

function useQuoinEditor(options: UseQuoinEditorOptions): ViewController

useQuoinEditor on GitHub

UseQuoinEditorOptions

type UseQuoinEditorOptions = {
  blocks: QuoinBlock[];
  onChange: (blocks: QuoinBlock[]) => void;
  extensions?: readonly Extension[];
  readOnly?: boolean;
  onTransaction?: (tr: Transaction, state: EditorState) => void;
  onFiles?: (files: File[], at: Pos | null) => void;
  render?: RenderOptions;
};

UseQuoinEditorOptions on GitHub

useViewController

function useViewController(): ViewController

useViewController on GitHub

The controller

The object that binds an editor to the page: it dispatches transactions, holds the editable hosts and keeps the DOM in step.

Announcer

type Announcer = {
  announce(message: string): void;
  current(): Spoken | null;
  subscribe(listener: () => void): () => void;
};

Announcer on GitHub

Engagement

type Engagement = {
  readonly engaged: boolean;
  engage(): void;
  subscribe(listener: () => void): () => void;
};

Engagement on GitHub

FocusTarget

type FocusTarget = Pos | 'start' | 'end';

FocusTarget on GitHub

HostEntry

type HostEntry = { readonly block: string; readonly region: string; readonly element: HTMLElement };

HostEntry on GitHub

hostKey

function hostKey(block: string, region: string): string

hostKey on GitHub

HostRegistry

type HostRegistry = {
  register(block: string, region: string, element: HTMLElement): () => void;
  get(block: string, region: string): HTMLElement | undefined;
  locate(node: Node | null): HostEntry | null;
  all(): IterableIterator<HostEntry>;
};

HostRegistry on GitHub

Spoken

type Spoken = {
  readonly message: string;
  readonly state: EditorState | undefined;
  readonly serial: number;
  readonly at: number;
};

Spoken on GitHub

ViewController

class ViewController {
  readonly editor: Editor;
  readonly registry: HostRegistry;
  readonly keyInterceptors: Set<KeyInterceptor>;
  readonly nodeReveal: NodeReveal;
  readonly announcer: Announcer;
  readonly engagement: Engagement;
  readonly isMac: boolean;
  readonly onFiles: ViewControllerOptions['onFiles'];
  goalX: number | null;
  nativeVerticalMove: boolean;
  dragging: boolean;
  composingKey: string | null;
  constructor(options: ViewControllerOptions);
  get root(): HTMLElement | null;
  get readOnly(): boolean;
  get sink(): HTMLElement | null;
  get emptyHost(): HTMLElement | null;
  attach(root: HTMLElement): () => void;
  registerBlockElement(id: string, element: HTMLElement): () => void;
  blockElement(id: string): HTMLElement | undefined;
  blockAt(node: Node): string | undefined;
  registerSink(element: HTMLElement): () => void;
  registerEmptyHost(element: HTMLElement): () => void;
  isEditorElement(element: Element): boolean;
  decorationsFor(block: string, region: string): readonly Decoration[];
  dispatch(tr: Transaction): void;
  edit(change: () => boolean): boolean;
  afterRender(): void;
  refreshHosts(): void;
  run(command: Command): boolean;
  announce(message: string): void;
  renderHost(block: string, region: string): void;
  domSelection(): globalThis.Selection | null;
  syncSelectionToDom(local?: boolean): void;
  readSelectionFromDom(): void;
  focus(target?: FocusTarget): void;
  setReadOnly(readOnly: boolean): void;
  subscribeReadOnly(listener: () => void): () => void;
  destroy(): void;
}

ViewController on GitHub

ViewControllerOptions

type ViewControllerOptions = {
  editor: Editor;
  flush?: (fn: () => void) => void;
  render?: RenderOptions;
  onFiles?: (files: File[], at: Pos | null) => void;
  readOnly?: boolean;
  /** Which key is Mod. Defaults to the platform, as the editor's own isMac does, and must agree with it. */
  isMac?: boolean;
};

ViewControllerOptions on GitHub

The DOM map

Inline nodes drawn into an element, and the map between a position in the document and a point in the DOM.

AtomRenderer

type AtomRenderer = (node: AtomNode, doc: Document) => HTMLElement;

AtomRenderer on GitHub

DomMap

type DomMap = {
  readonly length: number;
  toDom(offset: number, assoc?: -1 | 1): DomPoint;
  fromDom(node: Node, offset: number): number | null;
};

DomMap on GitHub

domMapOf

function domMapOf(host: HTMLElement): DomMap | undefined

domMapOf on GitHub

DomPoint

type DomPoint = { node: Node; offset: number };

DomPoint on GitHub

renderInline

function renderInline(
  host: HTMLElement,
  inline: Inline,
  decorations: readonly Decoration[],
  options?: RenderOptions,
): DomMap

renderInline on GitHub

RenderOptions

type RenderOptions = {
  atoms?: Readonly<Record<string, AtomRenderer>>;
  widgets?: Readonly<Record<string, WidgetRenderer>>;
  atomText?: (node: AtomNode) => string;
};

RenderOptions on GitHub

WidgetRenderer

type WidgetRenderer = (decoration: Extract<Decoration, { kind: 'widget' }>, doc: Document) => HTMLElement;

WidgetRenderer on GitHub

Selection

What the view needs to tell a selection inside one block from one that crosses several.

BlockLabel

What the editor calls a block selected as a whole, given the schema it can read the block's text with.

type BlockLabel = (block: EngineBlock, schema: Schema) => string;

BlockLabel on GitHub

NodeReveal

type NodeReveal = {
  /** Marks a block a press is selecting, which is under the pointer already. */
  pressed(block: string): void;
  /** Forgets the block shown once the selection leaves it, however it leaves, so coming back brings it into view. */
  follow(selection: Selection | null): void;
  /** Brings the block into view once; a change made here also brings in the block a settle left where it was. */
  show(selection: Selection | null, local?: boolean): void;
  /** Counts the block a selection holds as shown, and as owed to the next change made here if it was not shown yet. */
  settle(selection: Selection | null): void;
};

NodeReveal on GitHub

Keys

Where a host takes a key before the editor does.

KeyInterceptor

type KeyInterceptor = (event: KeyboardEvent) => boolean;

KeyInterceptor on GitHub

Caret placement

Where a point on the screen puts the caret, which the arrow keys and a pointer both ask for.

caretFromPoint

function caretFromPoint(doc: Document, x: number, y: number): CaretPoint | null

caretFromPoint on GitHub

CaretPoint

type CaretPoint = { node: Node; offset: number };

CaretPoint on GitHub

Esc

Type to search the guide, the comparison and the changelog.