Skip to content

Schema commands and buttons ​

ElementSchemaCommands gives every element of the schema its command, toolbar button and keystroke. The command of an element is the one of its kind: formatting wraps text in the element, a block renames blocks, a container wraps them, and the other elements are inserted. Each works as the CKEditor feature of that kind:

ElementCommandWorks as CKEditor'sIt
inline blockElementInlineCommandboldwraps the selected text, or unwraps it
blockElementBlockCommandheadingrenames the blocks, or back to paragraphs
containerElementContainerCommandblock quotewraps the blocks, or unwraps them
container with listItemElementListCommandlistsputs the blocks in a list, each in an item
empty, and inline containerElementInsertCommandhorizontal lineinserts the element, and selects it
ts
import { IconBold, IconHorizontalLine, IconQuote } from 'ckeditor5';
import { ElementSchema, ElementSchemaCommands } from 'ckeditor5-elements';

// In the editor config:
plugins: [..., ElementSchema, ElementSchemaCommands],
toolbar: ['bold', 'callout', 'divider'],
elementSchema: schema,
elementSchemaCommands: {
	bold: { title: 'Bold', icon: IconBold, keystroke: 'Ctrl+B' },
	callout: { title: 'Callout', icon: IconQuote },
	divider: { title: 'Divider', icon: IconHorizontalLine }
}

Each key of elementSchemaCommands is the name of the command and of its toolbar button. With CKEditor's own feature in the same editor, e.g. Bold, use another key: two buttons cannot have the same name.

KeyDoes
elementThe model element: of the schema, or of another plugin, e.g. paragraph. Default: the key.
titleThe title of the command, and the label of the button.
iconThe icon of the command and of the button, e.g. IconBold of CKEditor.
keystrokeThe keystroke of the command, e.g. 'Ctrl+B' (⌘B on a Mac).

The plugin makes commands and buttons only: how an element looks, e.g. a bold as <strong>, is the tag of the element, with the converters of the schema.

The commands are made when the elements are registered, after the other plugins are initialized. The editor is not created when an element is unknown.

Formatting: bold, italic, underline ​

Formatting is elements too. A bold, an italic or an underline is an inline block of the schema: an element in the line of text, with its own name and attributes, which nests in other elements and holds others, is part of the breadcrumbs path and of the element features, and is what the data has. ElementInlineCommand toggles it on the selection, as CKEditor's bold toggles its mark on the text, and works as an element does: e.g. it can be empty.

The editor
ts
import { Essentials, IconBold, IconItalic, IconUnderline, Paragraph, SourceEditing, _getModelData } from 'ckeditor5';
import { ElementBreadcrumbs, ElementSchema, ElementSchemaCommands } from 'ckeditor5-elements';
import formatting from './shared/formatting.json';
import type { Demo } from './shared/demo';

export default {
	config: {
		// No `Bold`, `Italic` and `Underline` of CKEditor: the buttons are the ones of `ElementSchemaCommands`.
		plugins: [Essentials, SourceEditing, Paragraph, ElementSchema, ElementSchemaCommands, ElementBreadcrumbs],
		toolbar: {
			items: ['undo', 'redo', 'sourceEditing', '|', 'bold', 'italic', 'underline', '-', 'breadcrumbs'],
			shouldNotGroupWhenFull: true
		},
		// The elements are `<strong>`, `<em>` and `<u>`: the `tag` of each one, with the converters of the schema.
		elementSchema: formatting,
		elementSchemaConverters: true,
		elementSchemaCommands: {
			bold: { title: 'Bold', icon: IconBold, keystroke: 'Ctrl+B' },
			italic: { title: 'Italic', icon: IconItalic, keystroke: 'Ctrl+I' },
			underline: { title: 'Underline', icon: IconUnderline, keystroke: 'Ctrl+U' }
		}
	},
	data:
		'<p>Select text and press <strong>Bold</strong>, <em>Italic</em> or <u>Underline</u>, or ' +
		'<strong><em>both</em></strong> at once: each one is an <strong>element</strong>, as the breadcrumbs and the ' +
		'model below show.</p>' +
		'<p>Put the cursor here, press <strong>Ctrl+B</strong> and type: the text goes in a new bold. Press the right ' +
		'arrow at the end of a <strong>bold</strong>: the first press goes out of it, and what is typed is not bold.</p>',
	// Shows the model below the editor, as it changes: elements, where CKEditor's features would have attributes.
	ready: (editor, container) => {
		const model = document.createElement('div');
		const update = (): void => {
			model.textContent = _getModelData(editor.model, { withoutSelection: true });
		};

		model.className = 'editor-demo__below editor-demo__model';
		container.append(model);
		editor.model.document.on('change:data', update);
		update();
	}
} satisfies Demo;
formatting.json
json
{
	"bold": { "type": "block", "isInline": true, "tag": "strong" },
	"italic": { "type": "block", "isInline": true, "tag": "em" },
	"underline": { "type": "block", "isInline": true, "tag": "u" }
}
OnIt
selected textwraps the text that is not in the element yet, in each block, and merges it with the elements next to it
text all in elementsunwraps the elements, the whole ones: <bold>a[b]c</bold> is a[b]c
the cursorinserts an empty element, with the cursor in it
the cursor in onemoves the cursor out, before or after it, at its start or end; unwraps it in the middle, or removes it when empty
an elementunwraps the element it is in, or wraps it, e.g. a selected inline object

It is "on" when the cursor or the element is in the element, or all the selected text is. The model is a tree of elements, down to the formatting:

<paragraph>a <bold>b <italic>c</italic></bold> d</paragraph>

An empty element takes the cursor and what is typed: see Typing in an empty inline element.

Two-step caret movement ​

At an end of an element, the cursor can be in it or out of it, and the two look the same. The element schema makes the left and right arrow keys move it in two steps there, for every inline element with text, as CKEditor's two-step caret movement does at the ends of a bold: the first step goes into the element (or out of it) without moving on the screen, the next one moves in the text.

a[]<bold>bc</bold>   →   a<bold>[]bc</bold>   →   a<bold>b[]c</bold>
a<bold>bc[]</bold>d  →   a<bold>bc</bold>[]d  →   a<bold>bc</bold>d[]

What is typed goes where the cursor is, in the element or out of it, and the bold button shows which. (The browser would type at a place of its own, the end of the text before the boundary: there, the plugin types instead of it.) Nested elements take one step each. Pressing the keystroke at the end of an element moves the cursor out too.

Blocks, containers, lists and inserted elements ​

The editor
ts
import {
	Essentials,
	IconCodeBlock,
	IconHeading2,
	IconHorizontalLine,
	IconNumberedList,
	IconObjectInline,
	IconQuote,
	IconUser,
	Paragraph,
	SourceEditing,
	_getModelData
} from 'ckeditor5';
import { ElementBreadcrumbs, ElementSchema, ElementSchemaCommands, ElementSelectable } from 'ckeditor5-elements';
import commands from './shared/commands.json';
import type { Demo } from './shared/demo';

export default {
	config: {
		plugins: [
			Essentials,
			SourceEditing,
			Paragraph,
			ElementSchema,
			ElementSelectable,
			ElementSchemaCommands,
			ElementBreadcrumbs
		],
		toolbar: {
			items: [
				'undo',
				'redo',
				'sourceEditing',
				'|',
				'title',
				'callout',
				'code',
				'steps',
				'divider',
				'badge',
				'chip',
				'-',
				'breadcrumbs'
			],
			shouldNotGroupWhenFull: true
		},
		elementSchema: commands,
		elementSchemaConverters: true,
		// The command of each element is the one of its kind: a block, a container, a list, an inserted element.
		elementSchemaCommands: {
			title: { title: 'Title', icon: IconHeading2 },
			callout: { title: 'Callout', icon: IconQuote },
			code: { title: 'Code', icon: IconCodeBlock },
			steps: { title: 'Steps', icon: IconNumberedList },
			divider: { title: 'Divider', icon: IconHorizontalLine },
			badge: { title: 'Badge', icon: IconUser },
			chip: { title: 'Chip', icon: IconObjectInline }
		}
	},
	data:
		'<div class="title">Blocks, containers, lists and inserted elements</div>' +
		'<p>Put the cursor here and press Title: the paragraph is renamed. Press it again: it is a paragraph.</p>' +
		'<div class="callout"><p>Press Callout in this callout: it is unwrapped. Select paragraphs and press it: they ' +
		'are wrapped.</p></div>' +
		'<pre class="code">Code keeps its whitespace:\n  press Enter for a new line,\n  and Enter on an empty last line ' +
		'to leave it.</pre>' +
		'<div class="steps"><div class="step"><p>Steps are a list: press Enter at the end of this step.</p></div>' +
		'<div class="step"><p>Press Enter in an empty step to leave the list.</p></div></div>' +
		'<p>Press Divider in the middle of this line: it splits it. Badge inserts an inline object, and Chip an inline ' +
		'box with a paragraph to type in.</p>',
	// Shows the model below the editor, as it changes: elements, where CKEditor's features would have attributes.
	ready: (editor, container) => {
		const model = document.createElement('div');
		const update = (): void => {
			model.textContent = _getModelData(editor.model, { withoutSelection: true });
		};

		model.className = 'editor-demo__below editor-demo__model';
		container.append(model);
		editor.model.document.on('change:data', update);
		update();
	}
} satisfies Demo;
commands.json
json
{
	"title": { "type": "block" },
	"callout": { "type": "container" },
	"code": { "type": "block", "verbatim": true },
	"steps": { "type": "container", "listItem": "step", "editingTag": "ol" },
	"step": { "type": "container", "editingTag": "li" },
	"divider": { "type": "empty" },
	"badge": { "type": "empty", "isInline": true },
	"chip": { "type": "container", "isInline": true }
}

Blocks ​

A block (the title) works as a heading, with ElementBlockCommand: the blocks of the selection are renamed, and keep their content. When they all are titles, they are turned back into paragraphs: the defaultElement of the command, which can be changed. Blocks that cannot be renamed, as a divider, or a block where a title is not allowed, are left as they are. It is "on" when all the blocks are titles.

A verbatim block (the code) is a block too: its command works as a code block's does, renaming the blocks. In it, Enter inserts a new line.

Containers ​

A container (the callout) works as a block quote, with ElementContainerCommand: the selected blocks go in a new callout. With blocks at different levels, it wraps their ancestors in the parent they share. In a callout, it unwraps the whole callout. It is "on" in a callout.

Lists ​

A list (the steps, a container with listItem) works as CKEditor's lists, with ElementListCommand: the selected blocks go in a new list, each one in an item. In a list, it unwraps the list and its items; in another list with the same items, it turns that list into its own, e.g. a bulleted list into a numbered one. It is "on" in its list. In an item, Enter makes the next item, and leaves the list in an empty one.

Inserted elements ​

An empty element (the divider, the inline badge) works as a horizontal line or an inline image, with ElementInsertCommand: it is inserted in place of the selection, and selected. A block-level one in the middle of a paragraph splits it. An inline container (the chip) is inserted the same way, with a paragraph and the cursor in it. It does not toggle: it is never "on".

In a menu, for an element ​

The commands are element commands: each one is for the selection, or for a given position or element, with executeFor(), and getState() tells whether it is enabled and "on" there. They have a title and an icon, so they are menu items as they are, e.g. of the context menu, for the element that is right-clicked:

ts
elementContextMenu: (element, editor) => [editor.commands.get('title'), editor.commands.get('callout')];

For a given element, a block is renamed, a container wraps it (or unwraps the one it is in), and an inserted element goes after it.

A command of your own ​

The commands are the commands of one element, which work for any element, of the schema or not. A command of your own for an element of the schema extends their base, ElementCommand: see A command of your own.