Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

The editor instance

The runtime Edytor object behind every view, its members, the Block, Text and InlineBlock handles, and how to read state reactively.

Each <Edytor> view creates one runtime object, the editor instance. It holds the document the view renders, the selection, the plugins and the command dispatcher. You need it to read or change content from code, or to build UI around the editor (toolbars, menus, status bars).

Getting the instance

From the parent, bind it:

<script lang="ts">
	import { Edytor, type EdytorInstance } from 'edytor';

	let edytor = $state<EdytorInstance>();
</script>

<Edytor bind:edytor />
<button onclick={() => edytor?.historyUndo()}>Undo</button>

From a component rendered inside the editor (a block snippet, an inline block, a plugin’s UI), call useEdytor(). It reads the instance from Svelte context and throws No Edytor found outside an editor:

<script lang="ts">
	import { useEdytor } from 'edytor';

	const edytor = useEdytor();
</script>

The instance exists as soon as the component is created, but edytor.root stays undefined until the document is ready (edytor.synced is then true). With a sync provider that can take a moment.

Members

Member Type What it is
value JSONBlock The whole document as JSON, { type: 'root', children }. Reactive.
root Block | undefined The handle of the root block. root.children are the top-level blocks.
synced boolean The view is bound to a ready document.
document EdytorDocument The document this view renders: shared by every view of it, and usable without a view.
facade EdytorDoc The document operations, as this view sees them (see below).
selection EdytorSelection The selection value and its setters. See Selection.
transact(fn) <T>(fn: () => T) => T Run several changes as one transaction and one update. A throw from fn does not undo the changes made before it; normalization still runs on them, and fn’s error is the one thrown (a normalizer that fails then is logged).
dispatcher Dispatcher Runs every command. dispatcher.last is the last command’s result; a command of several steps (a Tab over several sibling groups, Turn into over several blocks) reads applied when any step applied. dispatcher.lead(plan, body), dispatcher.dispatch(operation, payload, context, body, prepare?) and dispatcher.caret(text, offset, ops?) let a plugin issue one refusable command of its own. See Triggers: one plan.
idToBlock Handles Handle lookup: get(id) (live blocks only), block(id), has(id).
canMoveBlocks(req), moveBlocks(req) Relative block moves. See Commands.
historyUndo(), historyRedo() () => void Undo and redo, restoring this view’s selection. Each call sets dispatcher.last: applied, noop when the stack is empty, or refused in a readonly view or a read-only document. See History.
readonly boolean Whether the view refuses edits. Follows the readonly prop.
node HTMLElement | undefined The editable root element, once mounted.
overlay Overlay The chrome layer beside the root, overlay.layer once mounted. overlay.add(measure) runs measure(origin) once per invalidated frame: it reads layout (origin is the layer’s rect) and returns its DOM writes, which run after every measure has read. overlay.mount(component, props, name, zIndex, measure) mounts a component in a fixed host placed by measure(host, origin); overlay.invalidate() asks for a frame. add and mount answer their teardown. See Styling.
blocks, marks, inlineBlocks Map<string, Definition> The registered definitions, by type.
kinds KindRow[] The kind catalogue. See Blocks.
commands, runCommand(id) Registered commands (block.heading2, …) and an async runner that resolves to false when the command is missing or disabled.
defaultChild(parent) (parent: Block) => string The type a new child of parent takes.
awareness Awareness The document’s presence channel. See Collaboration.
cells Cells The reactive render state, one cell per visible block (below).
clear() () => boolean Replace the whole document with one empty block and put the caret in it, as one undo step. It is a command: a readonly view or a read-only document refuses it (it answers false, and dispatcher.last reads refused), so a viewer never wipes the shared document. Plugin hooks do not see it.

edytor.facade and edytor.document

edytor.document is the EdytorDocument: it owns the CRDT document, the shared history, awareness and the facade. Several views can render one document; see Collaboration.

edytor.facade is the document’s facade seen through this view. It has every document operation (insertText, formatRange, insertBlock, moveBlocks, …) and read (toJSON, childrenIds, blockTypeOf, …). It differs from edytor.document.facade in one way: while the document has no block, the view shows a local empty paragraph, and edytor.facade.virtual() returns its id. An operation you issue on that id through edytor.facade creates the block first. The JSON (edytor.value) never contains it.

Facade operations are document writes: they skip plugin hooks and normalization. Handle methods go through the dispatcher and do both. Commands compares them.

Handles

Block, Text and InlineBlock are handles: small objects that name a piece of the document by id and read the current state from the document each time you access a getter.

const block = edytor.idToBlock.get('page-title'); // undefined if not live
if (block) {
	block.type; // 'heading'
	block.data; // { level: 'h1' }
	block.children; // Block[]
	block.firstText?.stringContent; // 'A calmer place to think'
	block.value; // this block's JSONBlock
}

Handles are not reactive. A template that reads block.type does not update when the type changes. Read handles in event handlers and commands, and use the reactive sources below in templates.

Handle Identity Useful members
Block One per block id: the same object for as long as the block exists. id, type, data, parent, children, content, index, depth, firstText, lastText, value, isInTree, isEmpty (no content and no children; an inline atom is content, so a block holding only a mention is not empty), hasContent, movable, convertible (false for a void block, an island and a list container; true for an emptied document’s virtual paragraph, which a Turn into creates with its kind; a divider, from Turn into or insertDividerAtSelection, is created with a paragraph after it), isContainer (it renders only its children, like a list), isListItem, list (the list an item shows in, also after it was outdented out of a nested list into the item holding it), node, textAtOffset(offset), setData(data) and the block commands (insertBlockAfter, splitBlock, setBlock, removeBlock, nestBlock, unNestBlock, …). setData(data) and assigning type run setBlock. nestBlock, unNestBlock, moveBlock and moveBlocks open a closed toggle the block lands in or that adopts blocks, as the keys do.
Text A text segment of a block: the ordinal-th segment between inline blocks. It has no identity beyond that position. parent, ordinal, stringContent, length, value (runs), isInDocument, node, and the text commands (insertText, deleteText, markText, removeMarksFromText, setText).
InlineBlock One per inline block id. id, type, data, parent, value, selected, isInDocument, setData(data) (its block’s setInlineData command).

Because a Text is a position, don’t keep one across edits to find “the same text” later. Keep the block id and an offset, or a selection anchor.

A handle can outlive what it names. Check block.isInTree, text.isInDocument or atom.isInDocument before acting on one you stored; idToBlock.get(id) returns undefined for a block that is gone.

Inside a command, handle getters see the command’s own earlier writes, so a normalizer or a plugin can read the state its operation produced.

Reading state reactively

Use these in templates, $derived and $effect:

  • Block snippets receive a view object, block, whose type, data, selected and focused are reactive. block.handle is the non-reactive handle for commands.

    {#snippet callout({ block, content })}
    	<span>{block.data.icon}</span>
    	{@render content()}
    	{#if block.selected}<small>selected</small>{/if}
    {/snippet}
  • edytor.cells is the render state: cells.rootIds (the top-level ids) and cells.get(id), a frozen { id, type, data, childIds, runs } that is replaced whenever the block changes. Both are reactive:

    <script lang="ts">
    	const outline = $derived(
    		(edytor?.cells?.rootIds ?? [])
    			.map((id) => edytor!.cells!.get(id))
    			.filter((cell) => cell?.type === 'heading')
    	);
    </script>
  • edytor.value is reactive and recomputes the whole JSON after each change. Fine for a word count or a save button; use cells for anything per block.

  • edytor.selection.value, selection.state and selection.projection are reactive, as are selection.selectedBlocks and selection.focusedBlocks.

Everything else, including handles, facade reads and definitions, is imperative: read it when you need it.

Was this page helpful?