Full example
An editor with all the features of CKEditor 5 Elements together, on top of CKEditor's common features: headings, lists, tables, block quotes, a note element with a warning type, and the elements of a JSON file (a tip, steps and a status). The features act on elements, whichever plugin they come from: a heading, a table or a note is an element as the ones of the file are.
More examples are editors of XML documents: the DocBook XML editor and the DITA XML editor, WYSIWYG, with the XML of the document below them.
- Right-click a block, or press Shift+F10: its menu toggles a note, inserts, duplicates, moves, copies, pastes or deletes it.
- Put the cursor in a note or a heading: its floating toolbar acts on the whole element.
- Click a breadcrumb on the second toolbar line: the menu of that element.
- Alt+Enter (Option+Enter on macOS): quick insert.
- The tip, the steps and the status come from
schema.json, converted one to one. - Click the frame of a note, the tip or a step, or the status: it is selected as a whole. At the end of a block, the arrow keys select the next element; Esc in a note selects the note.
- Click an image, the medium or the linked image: a modal with its attributes.
- Source shows the data: the elements, with their attributes, as they are saved.
- Alt+F10 focuses the toolbars; Alt+0 lists all the keystrokes.
Source
The example is the ckeditor5-elements-example package. It imports everything from ckeditor5 and ckeditor5-elements, as an app would. To run it as a page of its own:
sh
git clone https://github.com/ivankild/ckeditor5-elements.git
cd ckeditor5-elements
pnpm install
pnpm exampleconfig.ts: the editor config
ts
import {
BlockQuote,
Bold,
Code,
Essentials,
Heading,
HorizontalLine,
ImageBlock,
ImageCaption,
ImageInline,
ImageInsert,
Italic,
Link,
LinkImage,
List,
MediaEmbed,
Paragraph,
SourceEditing,
Table,
TableToolbar,
type EditorConfig
} from 'ckeditor5';
import {
ElementBreadcrumbs,
ElementActions,
ElementAttributes,
ElementClickable,
ElementClipboard,
ElementContextMenu,
ElementSchema,
ElementSelectable,
ElementToolbar,
ElementQuickInsert,
insertIcon,
pasteIcon,
type ElementClickableDefinition,
type ElementMenuConfig
} from 'ckeditor5-elements';
import { Note } from './note.js';
import schema from './schema.json';
/**
* The menu of an element, for the context menu and the breadcrumbs: toggles, inserting, the element actions, the
* clipboard, and deleting.
*/
export const elementMenu: ElementMenuConfig = (element, editor) => [
editor.commands.get('note'),
editor.commands.get('warning'),
'|',
{
title: 'Insert',
icon: insertIcon,
items: [editor.commands.get('quickInsertBefore'), editor.commands.get('quickInsertAfter')]
},
editor.commands.get('duplicateElement'),
'|',
editor.commands.get('moveElementUp'),
editor.commands.get('moveElementDown'),
'|',
editor.commands.get('copyElement'),
editor.commands.get('cutElement'),
{
title: 'Paste',
icon: pasteIcon,
items: [
editor.commands.get('pasteElement'),
'|',
editor.commands.get('pasteElementBefore'),
editor.commands.get('pasteElementAfter')
]
},
'|',
editor.commands.get('showElementAttributes'),
editor.commands.get('deleteElement')
];
/**
* Shows the attributes of the clicked element, with the command of `ElementAttributes`.
*/
const showAttributes: ElementClickableDefinition['callback'] = (element, editor) => {
editor.commands.get('showElementAttributes')?.executeFor(element);
};
const ACTIONS = ['moveElementUp', 'moveElementDown', '|', 'duplicateElement', 'deleteElement'];
/**
* The editor config of the full example: CKEditor's common features, the `Note` element, the elements of `schema.json`,
* and all the features of `ckeditor5-elements`. Without `attachTo`, `licenseKey` and the data.
*/
export const exampleConfig: EditorConfig = {
plugins: [
Essentials,
SourceEditing,
Paragraph,
Heading,
Bold,
Italic,
Code,
Link,
List,
BlockQuote,
Table,
TableToolbar,
HorizontalLine,
ImageBlock,
ImageInline,
ImageCaption,
ImageInsert,
LinkImage,
MediaEmbed,
Note,
ElementSchema,
ElementSelectable,
ElementContextMenu,
ElementClipboard,
ElementActions,
ElementAttributes,
ElementToolbar,
ElementClickable,
ElementQuickInsert,
ElementBreadcrumbs
],
toolbar: {
items: [
'undo',
'redo',
'sourceEditing',
'|',
'heading',
'|',
'bold',
'italic',
'code',
'link',
'|',
'bulletedList',
'numberedList',
'blockQuote',
'insertImage',
'mediaEmbed',
'insertTable',
'horizontalLine',
'|',
'note',
'warning',
// The breadcrumbs on a toolbar line of their own.
'-',
'breadcrumbs'
],
shouldNotGroupWhenFull: true
},
table: { contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells'] },
// A provider that needs no network: the preview of an embedded medium is a box with its address.
mediaEmbed: {
providers: [
{
name: 'example',
url: /^https:\/\/example\.com\/(.+)/,
html: match => `<div class="example-media">Media: <code>${match[0]}</code></div>`
}
]
},
// The elements of the JSON file, with their converters: `div` and `span`, the name as the class.
elementSchema: schema,
elementSchemaConverters: true,
// Its containers and empty elements are selectable; the notes too.
elementSelectable: ['note'],
elementContextMenu: elementMenu,
elementBreadcrumbs: { menu: elementMenu },
elementClickable: [
// Click an image, a medium, or anything with a link (a linked image): a modal with its attributes.
{
element: element =>
['imageBlock', 'imageInline', 'media'].includes(element.name) || element.hasAttribute('linkHref'),
callback: showAttributes
},
// Double-click a note: the same modal.
{ element: 'note', event: 'dblclick', callback: showAttributes }
],
elementToolbar: [
// "Note" unwraps the note; the actions act on the whole note.
{ element: 'note', items: ['note', '|', ...ACTIONS], ariaLabel: 'Note toolbar' },
{ element: 'tip', items: ACTIONS, ariaLabel: 'Tip toolbar' },
{
element: element => element.name.startsWith('heading'),
items: ['heading', '|', ...ACTIONS],
ariaLabel: 'Heading toolbar'
}
],
elementQuickInsert: {
labels: {
paragraph: 'Paragraph',
heading1: 'Heading 1',
heading2: 'Heading 2',
heading3: 'Heading 3',
note: 'Note',
blockQuote: 'Block quote',
media: 'Media',
horizontalLine: 'Horizontal line',
softBreak: 'Line break',
tip: 'Tip',
tipTitle: 'Tip title',
steps: 'Steps',
step: 'Step',
status: 'Status'
},
// These need rows and cells, or an address: insert them with their toolbar buttons.
exclude: ['table', 'imageBlock', 'imageInline', 'media']
}
};schema.json: the elements of the element schema
json
{
"tip": { "type": "container", "allowAttributes": ["tone"] },
"tipTitle": { "type": "block", "allowIn": ["tip"], "editingTag": "header" },
"steps": { "type": "block" },
"step": { "type": "container", "isInline": true, "allowIn": ["steps"] },
"status": { "type": "empty", "isInline": true, "allowAttributes": ["state"] }
}note.ts: the note element
ts
import { Plugin, type Editor } from 'ckeditor5';
import { ElementButtonView, ElementContainerCommand } from 'ckeditor5-elements';
const noteIcon =
'<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">' +
'<path d="M4 3h12v14H4zm1.5 1.5v11h9v-11zM7 7h6v1.5H7zm0 3h6v1.5H7z"/></svg>';
const warningIcon =
'<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="' +
'M10 2.2 18.6 17H1.4zm0 3L4 15.5h12zM9.25 8h1.5v4h-1.5zm0 5h1.5v1.5h-1.5z"/></svg>';
/**
* Wraps blocks with a note, or unwraps the note they are in. With a `type`, only notes of that type: an
* `ElementCommand` matches an element by its name and attributes.
*/
export class NoteCommand extends ElementContainerCommand {
constructor(editor: Editor, type?: string) {
super(editor, 'note');
if (type) {
this.elementAttributes = { type };
}
}
}
/**
* A note: an element that holds blocks, `<aside class="note">`. A warning is a note with the `warning` type,
* `<aside class="note warning">`.
*
* The `note` command toggles a note (any note is "on" for it), and the `warning` command toggles a warning. Their
* toolbar buttons have the same names.
*/
export class Note extends Plugin {
public static get pluginName(): 'Note' {
return 'Note' as const;
}
public init(): void {
const editor = this.editor;
const t = editor.t;
editor.model.schema.register('note', {
allowWhere: '$block',
allowContentOf: '$root',
allowAttributes: ['type']
});
editor.conversion.elementToElement({ model: 'note', view: { name: 'aside', classes: 'note' } });
editor.conversion.attributeToAttribute({
model: { name: 'note', key: 'type', values: ['warning'] },
view: { warning: { key: 'class', value: 'warning' } }
});
this._addCommand('note', new NoteCommand(editor), t('Note'), noteIcon);
this._addCommand('warning', new NoteCommand(editor, 'warning'), t('Warning'), warningIcon);
}
private _addCommand(name: 'note' | 'warning', command: NoteCommand, title: string, icon: string): void {
const editor = this.editor;
command.set({ title, icon });
editor.commands.add(name, command);
editor.ui.componentFactory.add(name, locale => new ElementButtonView(locale, command));
}
}
declare module 'ckeditor5' {
interface PluginsMap {
[Note.pluginName]: Note;
}
interface CommandsMap {
note: NoteCommand;
warning: NoteCommand;
}
}main.ts: creating the editor
ts
import { ClassicEditor } from 'ckeditor5';
import { exampleConfig, exampleData } from './index.js';
import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-elements/index.css';
import './content.css';
import './style.css';
const element = document.querySelector<HTMLElement>('#editor');
if (!element) {
throw new Error('The page has no #editor element.');
}
const editor = await ClassicEditor.create({
...exampleConfig,
attachTo: element,
licenseKey: 'GPL',
root: { initialData: exampleData }
});
// To try the API in the browser console, e.g. `editor.commands.get('moveElementUp').executeFor(element)`.
Object.assign(window, { editor });