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 (an item of a list container leaves the list instead, see Containers). 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.
An island keeps whatever structure its interior builds: a table island of rows of cells, or a callout island holding a heading and nested paragraphs, keeps its kinds and nesting. An island that also declares lines: true is an island of lines, like the code block:
- Every direct child shows as the island’s
defaultChild(acodeLine) and holds no children, even when an undo or a collaborator’s edit puts another kind or a nested block there. A block nested under a line shows right after the island instead, and stays movable. - Inserting under a line is refused, and a block of the line kind outside such an island shows as its parent’s default child.
- The first line never merges into the island’s own content when the island renders none (
rendersContent: false).
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.
A list holds only its items. Turn into converts the items a selection holds, never the list itself. An item turned into another kind leaves the list, where Shift + Tab lifts it: before the list, after it, or between its two halves. Out of a list nested right in a list it leaves both. Turned into the kind it already shows as (Numbered list in an ordered list), an item stays, and so does a block a merge left in the list turned into its own kind (Heading 2 on a heading). A divider or a code block that Turn into adds after an item with text goes after the item outside the list, which splits there. A pasted list of the list’s own kind (an <ol> into an ordered list) gives items; another kind keeps its kind. Enter in an empty item outdents it the same way, one level per press, until it ends the list. A list that a Turn into or an outdent split stays two lists once the block between them goes: the second one’s numbering restarts, and its first item does not nest under the first list’s last item. Undo right after the split restores one list. List containers come from JSON, the API or a paste of an Edytor fragment; an HTML paste (from Google Docs, Word or a web page) gives flat bulleted and numbered items, as the lists you type (1. , - ) do, and flat items never split. The keys at an item’s edges are listed in Enter and Backspace by role.
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 the kind has an empty shape (a divider, a code block). It converts in place only a block that holds nothing; a block with text or children stays intact and the new block is inserted after it. See presets. |
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 caret's block, or every selected or touched block
A kind’s command converts the caret’s block, every block of a block selection, or every block a text range touches that shows its own text (a closed toggle’s hidden body is not touched, and a list around the items stays a list), as one undo step. A replacing kind acts only on the block holding the selection’s start. See presets.
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.