Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Plugins

What an Edytor plugin is, how to pass plugins to the editor, how plugin order decides conflicts, and which plugins ship with the package.

Everything the editor knows about content comes from plugins: block kinds, marks, inline atoms, key bindings, and hooks that can inspect or veto every edit. The core ships no block kinds of its own; <Edytor> adds the rich text, image and arrow move plugins by default, so <Edytor /> alone is a working rich text editor.

What a plugin is

A plugin is a function of the editor that returns definitions and hooks:

import type { Plugin } from 'edytor';

export const loggerPlugin: Plugin = (edytor) => ({
	onBeforeOperation: ({ operation }) => {
		console.log('about to run', operation);
	},
	onChange: (value) => {
		console.log('document now has', value.children?.length, 'top-level blocks');
	}
});

The function runs once, when the editor is created. It receives the Edytor instance, so it can keep state in its closure and call editor methods from its hooks. Everything it returns is optional:

Field What it contributes
blocks Block kinds, see Blocks
marks Text marks, see Marks
inlineBlocks Inline atoms, see Inline blocks
hotkeys Key bindings, see Hotkeys
commands Named commands for menus such as the slash menu
placeholder Text for empty blocks, see Placeholder
onBeforeOperation and more Hooks into every edit, the DOM and the clipboard

Writing plugins documents every field and hook.

Passing plugins to the editor

Pass an array to the plugins prop of <Edytor>:

<script lang="ts">
	import {
		Edytor,
		codePlugin,
		slashMenuPlugin,
		toolbarPlugin,
		markdownShortcutsPlugin,
		blockMenuPlugin
	} from 'edytor';

	const plugins = [codePlugin, markdownShortcutsPlugin, slashMenuPlugin, toolbarPlugin, blockMenuPlugin];
</script>

<Edytor {plugins} />

Plugins are read once, when the component mounts. Changing the array afterwards has no effect.

Default plugins

<Edytor> completes the list with the plugins every editor needs, after yours so your kinds, marks and keys take precedence, unless the list already has them:

Plugin Position Replaced by
arrowMovePlugin after yours arrowMovePlugin in your list
imagePlugin after yours imagePlugin or any createImagePlugin(...) in your list
richTextPlugin last richTextPlugin in your list
Block handles first the blockHandles prop, or a handles plugin in your list

The example above therefore runs block handles, your five plugins, then arrow move, image and rich text. A default you list yourself stays where you put it, so older code that passes plugins={[richTextPlugin]} still works.

Pass defaultPlugins={false} to get exactly your list (block handles still follow blockHandles). The defaults are added by the <Edytor> component, per view; the editor instance underneath has none of its own.

Plugin order

The array order is the precedence order. Two rules follow from it:

  • Definitions: first wins. When two plugins define the same block kind, mark, inline atom or command id, the one listed first is used and the later one is ignored. To replace a definition from a bundled plugin, list your plugin before it.
  • Prevention: first wins. Hooks and key bindings run in list order. The first plugin that calls prevent() stops the operation or claims the key, and the plugins after it are not asked.

Order therefore matters whenever two plugins touch the same thing: the same kind name, the same chord, or the same operation. A plugin that overrides or refuses something a bundled plugin does goes before it in the list. The three defaults are added after your plugins, so yours already come first; to let a default win over one of your plugins, list the default yourself before it.

One exception: two plugins that declare different defaultChild values for the same block kind are an error, whatever their order. The editor throws when it is created.

Why plugins are Svelte files

Block, mark and inline atom definitions render through Svelte snippets. A snippet can only be declared in a .svelte file, and a component’s <script module> block can reference the snippets declared in its markup. So a plugin that renders anything is usually a .svelte file that exports the plugin from <script module>:

<script module lang="ts">
	import type { Snippet } from 'svelte';
	import type { Plugin } from 'edytor';

	export const highlightPlugin: Plugin = () => ({
		marks: { marker: { snippet: marker } }
	});
</script>

{#snippet marker({ content }: { content: Snippet })}
	<mark class="marker">{@render content()}</mark>
{/snippet}

Import it like any other module: import { highlightPlugin } from './HighlightPlugin.svelte'. A plugin that only adds hooks or key bindings can be a plain .ts file.

Bundled plugins

Plugin Export What it adds
Rich text richTextPlugin Paragraphs, headings, lists, to-dos, toggles, callouts, quotes, dividers, ten marks, and Notion’s shortcuts. On by default
Code codePlugin Code blocks with syntax highlighting and a Copy button
Image imagePlugin, createImagePlugin Notion’s image block: embed from a link or your upload, with an editable caption. On by default
Block handles blockHandlesPlugin, createBlockHandlesPlugin A + and a drag grip beside each block, drag and drop, keyboard moves. On by default
Block menu blockMenuPlugin, createBlockMenuPlugin Notion’s block menu on a grip click: search, Turn into, Duplicate, Move, Delete
Slash menu slashMenuPlugin, createSlashMenuPlugin A sectioned command menu opened by typing /
Toolbar toolbarPlugin, createToolbarPlugin A floating toolbar over text selections: Turn into, link, marks and colors
Markdown shortcuts markdownShortcutsPlugin Block prefixes such as # or - , and inline **bold**, *italic*, `code`, ~strike~
Arrow move arrowMovePlugin Mod + Shift + ↑/↓ moves the caret’s block; Mod + ↑/↓ moves selected blocks. On by default
Mention (not exported) A reference implementation of an inline atom

All exports come from the package root, edytor. The create… factories take options, including snippets that replace the menus’ and handles’ markup (see Menus and handles). For the matching document look, add the Notion theme.

Was this page helpful?