Skip to content

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 example
config.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 });