Blocks
Block kinds, nesting, void and island blocks, default children, and the kind catalogue that powers menus and markdown shortcuts.
Every block has a kind, its type. Kinds come from plugins: each plugin’s blocks record declares the kinds it provides, how they render and how they behave structurally. This page covers the kinds that ship with Edytor and the structural rules every kind can opt into. To define your own, see Custom blocks.
Bundled kinds
<Edytor> includes richTextPlugin and imagePlugin by default. richTextPlugin provides:
| Type | Renders | data |
Notes |
|---|---|---|---|
paragraph |
div > p |
The default block type. | |
heading |
h1–h3 |
level: 'h1' | 'h2' | 'h3' |
Without a level, h1; any other level renders and copies as h3. |
bulleted-list-item |
li |
Continues on Enter. | |
numbered-list-item |
li |
Continues on Enter. | |
todo-item |
div with a checkbox |
checked: boolean |
Clicking the checkbox writes checked. Continues on Enter. |
toggle |
details |
Children form the collapsible body. Continues on Enter; a header over its children (container). |
|
callout |
div |
icon: string |
The icon defaults to 💡. A header over its children (container). |
quote |
blockquote |
A header over its children (container). |
|
divider |
hr |
Void, no content. Converting a block to it adds a paragraph after it for the caret. | |
ordered-list, unordered-list |
ol, ul |
Containers: children only, default child list-item. |
|
list-item |
li |
||
details, horizontalRule |
details, hr |
Not offered in menus (they have no presets). |
Continuing kinds and container headers are described in Enter and Backspace by role.
imagePlugin provides image, a void figure with an image from data.src and an editable caption (see Image).
codePlugin provides code, an island whose children are codeLine blocks (default child codeLine), highlighted as you type.
Nesting
Any block can hold children unless its kind is void. Children render inside the parent, wherever the kind’s snippet places them.
In the editor:
- Tab nests the current block under the block before it. Shift+Tab moves it out, after its parent; the blocks nested after it follow as its children, as in any outliner. Over a selection of sibling blocks, both keys move them together and keep them selected.
- Backspace at the start of a nested block that is its parent’s last child moves it out one level instead of merging (a heading, list item or other menu kind first turns into a paragraph; see Enter and Backspace by role).
- Block handles drag blocks before, after or inside other blocks.
edytor.moveBlocksandedytor.canMoveBlocksdo the same from code (see Commands).
Deleting a block, or a block selection, does not delete the children that were not selected: they take the deleted block’s place. block.removeBlock({ keepChildren: false }) removes a whole subtree.
Void blocks
A void block (void: true) is a self-contained unit, such as a divider, an image or an embed. It is not part of the text flow:
- Its element is
contenteditable="false"; the editor does not edit its markup. Controls inside it (buttons, inputs) keep working. - It cannot receive children. Nothing can be inserted, moved or dropped into it. Turning a block with children into a void kind (
setBlockType,setBlock, a menu conversion) moves the children out, right after it; they take the default type of that slot when they came from an island.setBlockwithchildrenfor a void kind is refused. A child that still lands under it (a collaborator nests one at the same moment, orvalueplaces one there) is shown right after it instead (concurrent editing). - It never merges with a neighbor. Backspace at the start of the block after it selects the void block; a second Backspace deletes it.
- Slash commands and markdown shortcuts don’t convert it.
A void kind can still render its content. That text stays editable, which is how an image gets an editable caption.
Island blocks
An island (island: true) is editable inside but structurally sealed from the rest of the document. The code block is one: its lines are child blocks, but they belong to the code block only.
- Blocks inside an island cannot be moved, and nothing can be moved or dropped into it. The island itself moves as one block.
- Content never merges across its boundary. Backspace at the start of its first line does not join the line into the block before the island; with a single line, it selects the whole island.
- A range delete that starts inside an island never merges out of it.
- Extending a block selection with Shift+↑/↓ from outside never steps into an island’s inner blocks.
- Slash commands and markdown shortcuts don’t convert the island block itself.
- When an island is deleted, its children take its place and are converted to the default child type of their new parent. A line a collaborator adds to the island at the same time shows the same way; undoing the delete shows it as a line of the island again.
The difference in one line: a void keeps the editor out, an island keeps the structure in.
These roles come from the plugins of the views attached to a document. A document edited with no view (a headless createDocument, a script) takes them from its semantics option instead: pass defaultSemantics for the bundled kinds (see Documents). The server room applies defaultSemantics itself (see the room).
Containers
A kind that declares rendersContent: false has no text of its own. It only renders its children, like ordered-list, unordered-list and code. The caret never lands in a container’s own slot. A list container that a deletion empties is removed.
Default children
When the editor creates a block next to the caret, the new block takes its parent’s default child type. This covers Enter at the start or end of a block, the second half of a block split with Enter, and the lines of an island that merges out. At the root, pressing Enter at the end of a heading or a quote therefore creates a paragraph; inside an ordered-list, it creates a list-item.
Continuing kinds (bulleted and numbered items, to-dos, toggles) continue themselves instead, and a block with both text and children keeps its kind when Enter splits it at its end: see Enter and Backspace by role.
Where the type comes from:
- A kind declares it with
defaultChild.ordered-listandunordered-listdeclarelist-item;codedeclarescodeLine. - Kinds without one, and the root, use the document’s default type,
paragraph. - Two plugins declaring different default children for the same kind is an error (
SemanticConflictError) when the editor starts.
Read the answer at runtime with edytor.defaultChild(parentBlock), which returns a type name.
Kinds and presets
A kind lists the ways to create it as presets. Each preset is a row in the kind catalogue, edytor.kinds, which the slash menu, the markdown shortcuts and your own block menus read:
edytor.kinds;
// [
// { id: 'block.paragraph', label: 'Text', icon: 'T', keywords: ['paragraph', 'plain'], value: { type: 'paragraph', data: {} }, replaces: false },
// { id: 'block.heading1', label: 'Heading 1', markdown: ['# '], value: { type: 'heading', data: { level: 'h1' } }, replaces: false },
// { id: 'block.heading2', label: 'Heading 2', … },
// { id: 'block.code', label: 'Code', markdown: ['```'], replaces: true, … },
// …
// ]
Each row has the preset’s label, icon, keywords, data, markdown prefixes and slash menu group, plus:
| Field | Meaning |
|---|---|
id |
The command id: block.<type>, numbered from 1 when a kind has several presets (block.heading2). |
value |
The conversion: type, the preset’s data, and the kind’s empty shape if it has one. |
replaces |
true when converting replaces the block’s content and children (a divider, a code block). |
Convert a block with convertToKind, or run the row’s command by id:
import { convertToKind } from 'edytor';
const row = edytor.kinds.find((kind) => kind.id === 'block.quote');
const block = edytor.selection.state.startBlock;
if (row) convertToKind(edytor, block, row); // true when it applied
await edytor.runCommand('block.heading2'); // converts the block holding the selection
A “Turn into” menu that keeps the block’s text lists the rows with replaces: false:
{#each edytor.kinds.filter((kind) => !kind.replaces) as kind (kind.id)}
<button onclick={() => convertToKind(edytor, block, kind)}>{kind.icon} {kind.label}</button>
{/each}
Only convertible blocks convert: block.convertible is false for void blocks, islands and blocks inside an island.