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:
| Element | Command | Works as CKEditor's | It |
|---|---|---|---|
inline block | ElementInlineCommand | bold | wraps the selected text, or unwraps it |
block | ElementBlockCommand | heading | renames the blocks, or back to paragraphs |
container | ElementContainerCommand | block quote | wraps the blocks, or unwraps them |
container with listItem | ElementListCommand | lists | puts the blocks in a list, each in an item |
empty, and inline container | ElementInsertCommand | horizontal line | inserts the element, and selects it |
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.
| Key | Does |
|---|---|
element | The model element: of the schema, or of another plugin, e.g. paragraph. Default: the key. |
title | The title of the command, and the label of the button. |
icon | The icon of the command and of the button, e.g. IconBold of CKEditor. |
keystroke | The 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
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
{
"bold": { "type": "block", "isInline": true, "tag": "strong" },
"italic": { "type": "block", "isInline": true, "tag": "em" },
"underline": { "type": "block", "isInline": true, "tag": "u" }
}| On | It |
|---|---|
| selected text | wraps the text that is not in the element yet, in each block, and merges it with the elements next to it |
| text all in elements | unwraps the elements, the whole ones: <bold>a[b]c</bold> is a[b]c |
| the cursor | inserts an empty element, with the cursor in it |
| the cursor in one | moves the cursor out, before or after it, at its start or end; unwraps it in the middle, or removes it when empty |
| an element | unwraps 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
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
{
"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:
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.