Creating Plugins
Extending the Editor by writing your first Extension.
Every Scrivr feature — built-in or custom — is an Extension. Extensions declare schema
contributions, keymaps, commands, layout handlers, and lifecycle hooks through a single
Extension.create() call.
Extension.create()
import { Extension } from '@scrivr/core';
const MyExtension = Extension.create<MyOptions>({
name: 'myExtension',
defaultOptions: {
myOption: 'default',
},
// ...config hooks
});The generic type parameter <MyOptions> makes your options typesafe. defaultOptions provides
fallback values — callers can override with .configure({ myOption: 'custom' }).
Configuration hooks
addKeymap
Returns a map of keyboard shortcuts to ProseMirror commands.
addKeymap() {
return {
'Mod-Shift-k': (state, dispatch) => {
if (dispatch) {
dispatch(state.tr.insertText('→'));
}
return true;
},
};
},addCommands
Contributes commands to editor.commands. Each command factory receives the current
state and returns a ProseMirror command function.
addCommands() {
return {
insertArrow: () => (state, dispatch) => {
if (dispatch) {
dispatch(state.tr.insertText('→'));
}
return true;
},
};
},After registration: editor.commands.insertArrow().
addProseMirrorPlugins
Contributes low-level ProseMirror plugins for event handling or decorations.
addProseMirrorPlugins() {
return [
new Plugin({
props: {
handleKeyDown(view, event) {
if (event.key === 'Tab') {
// custom tab handling
return true; // mark as handled
}
return false;
},
},
}),
];
},onEditorReady
Called once after the editor instance is fully constructed. Use to register subscriptions, overlay render handlers, or connect external services that need the live editor.
Return a cleanup function — it is called automatically when editor.destroy() runs.
onEditorReady(editor) {
const unsubscribe = editor.subscribe(() => {
// react to state changes
});
const unregister = editor.addOverlayRenderHandler((ctx, pageNumber, pageConfig, charMap) => {
// draw custom canvas overlay
});
return () => {
unsubscribe();
unregister();
};
},Minimal custom extension example
This extension adds a Mod-Shift-h shortcut that inserts "Hello!" at the cursor:
import { Extension } from '@scrivr/core';
const HelloExtension = Extension.create({
name: 'hello',
addKeymap() {
return {
'Mod-Shift-h': (state, dispatch) => {
if (dispatch) {
dispatch(state.tr.insertText('Hello!'));
}
return true;
},
};
},
addCommands() {
return {
sayHello: () => (state, dispatch) => {
if (dispatch) {
dispatch(state.tr.insertText('Hello!'));
}
return true;
},
};
},
});
// Use it
const editor = new Editor({
extensions: [StarterKit, HelloExtension],
});
editor.commands.sayHello();Composing sub-extensions
To bundle multiple extensions into one, implement the schema/behaviour hooks for each member
directly, or call .resolve() on each sub-extension and merge the results. This is the pattern
StarterKit uses internally — it aggregates all built-in extensions by resolving and merging
their nodes, marks, keymap, commands, etc. into a single Extension.create() config.
For most use cases, simply pass multiple extensions to the extensions array in EditorOptions
rather than building an aggregator extension:
const editor = new Editor({
extensions: [
StarterKit,
MyExtensionA,
MyExtensionB.configure({ option: true }),
],
});