Skip to content

Clipboard ​

ElementClipboard registers clipboard commands that act on a whole element:

CommandDoes
copyElementCopies the element to the clipboard, as HTML and text.
cutElementCopies the element and removes it.
pasteElementReplaces the element with the clipboard content.
pasteElementBeforeInserts the clipboard content before the element.
pasteElementAfterInserts the clipboard content after the element.

They are ElementOrPositionCommands: the target is an element, the parent element of a position, or the element at the selection. They have titles, so they work as buttons and as context menu items. Copying stays enabled in read-only mode.

The parts of a structure, such as table cells, table rows and captions, cannot stand alone, so the commands are disabled for them: copy a whole table, or the paragraphs in its cells.

The clipboard pipeline ​

The commands go through CKEditor's clipboard pipeline, like Ctrl+C and Ctrl+V, so other plugins handle the content as usual:

  • Copied content goes through outputTransformation of ClipboardPipeline, and clipboardOutput of the editing view document.
  • Pasted content goes through clipboardInput of the editing view document, then inputTransformation and contentInsertion of ClipboardPipeline. So paste from Office cleans up content from Word, and image upload uploads the images on the clipboard (they are passed as files, as the browser passes them on Ctrl+V).
  • Pasting moves the selection to the pasted content, as Ctrl+V does. It is one undo step.

Secure contexts only

The commands use the asynchronous Clipboard API, so they work only on HTTPS or localhost. The browser may ask for permission to read the clipboard when pasting. When it does not allow it, the command fails: buttons and menu items show a warning with the Notification plugin.

Toolbar buttons ​

ElementOrPositionButtonView turns the commands into toolbar buttons. Without a target, they act on the block with the cursor.

Source
ts
import { Essentials, Heading, Paragraph, Plugin, SourceEditing } from 'ckeditor5';
import { ElementClipboard, ElementOrPositionButtonView } from 'ckeditor5-elements';
import type { Demo } from './shared/demo';

/**
 * Toolbar buttons for the clipboard commands. Without a target, they act on the element at the selection.
 */
class ClipboardButtons extends Plugin {
	public static get requires(): readonly [typeof ElementClipboard] {
		return [ElementClipboard] as const;
	}

	public init(): void {
		const editor = this.editor;

		for (const name of ['copyElement', 'cutElement', 'pasteElementBefore', 'pasteElementAfter'] as const) {
			const command = editor.commands.get(name);

			if (command) {
				editor.ui.componentFactory.add(
					name,
					locale => new ElementOrPositionButtonView(locale, command, { withText: true })
				);
			}
		}
	}
}

export default {
	config: {
		plugins: [Essentials, SourceEditing, Paragraph, Heading, ClipboardButtons],
		toolbar: [
			'undo',
			'redo',
			'sourceEditing',
			'|',
			'heading',
			'|',
			'copyElement',
			'cutElement',
			'pasteElementBefore',
			'pasteElementAfter'
		]
	},
	data:
		'<h2>Put the cursor in a block</h2>' +
		'<p>Then use <b>Copy</b> or <b>Cut</b>: they act on the block with the cursor.</p>' +
		'<p><b>Paste before</b> and <b>Paste after</b> insert the clipboard next to it.</p>'
} satisfies Demo;

From code ​

Execute the commands with a target element. They return a promise, as the Clipboard API is asynchronous.

ts
await editor.execute('copyElement', element);
await editor.execute('pasteElementAfter', otherElement);
await editor.execute('cutElement'); // The element at the selection.
Source
ts
import { Essentials, Paragraph, SourceEditing, type ClassicEditor, type ModelElement } from 'ckeditor5';
import { ElementClipboard } from 'ckeditor5-elements';
import type { Demo } from './shared/demo';

/**
 * Buttons outside the editor that execute the clipboard commands for specific elements.
 */
function addControls(editor: ClassicEditor, container: HTMLElement): void {
	const root = editor.model.document.getRoot();
	const controls = document.createElement('div');
	const status = document.createElement('div');

	const block = (index: number): ModelElement | undefined => {
		const node = root?.getChild(index < 0 ? root.childCount + index : index);

		return node?.is('element') ? node : undefined;
	};

	const actions: [label: string, run: () => Promise<void> | undefined, done: string][] = [
		['Copy the first block', () => run(block(0), element => editor.execute('copyElement', element)), 'Copied.'],
		[
			'Paste after the last block',
			() => run(block(-1), element => editor.execute('pasteElementAfter', element)),
			'Pasted.'
		],
		['Cut the block at the selection', () => editor.execute('cutElement'), 'Cut.']
	];

	controls.className = 'editor-demo__controls';
	status.className = 'editor-demo__status';

	for (const [label, action, done] of actions) {
		const button = document.createElement('button');

		button.textContent = label;
		button.addEventListener('click', () => {
			action()
				?.then(() => (status.textContent = done))
				.catch((error: unknown) => (status.textContent = `Failed: ${String(error)}`));
		});
		controls.append(button);
	}

	container.append(controls, status);
}

function run(
	element: ModelElement | undefined,
	execute: (element: ModelElement) => Promise<void>
): Promise<void> | undefined {
	return element ? execute(element) : undefined;
}

export default {
	config: {
		plugins: [Essentials, SourceEditing, Paragraph, ElementClipboard],
		toolbar: ['undo', 'redo', 'sourceEditing']
	},
	data: '<p>The first block.</p><p>The second block.</p><p>The last block.</p>',
	ready: addControls
} satisfies Demo;