Formatting logic can live directly inside event handlers, but it gets
repetitive once the same behavior is used from keyboard shortcuts, toolbar
buttons, menu items, or tests. A command is reusable editor logic. Keep command
reads in editor.read.<group>.<method>() or editor.read(...), and command
writes in editor.update.<group>.<method>() or editor.update(...).
Start by moving the bold and code-block logic into plain functions:
import { ElementApi, type Editor } from "@platejs/plite";
const isBoldActive = (editor: Editor) => {
return
import { ElementApi, type Editor } from "@platejs/plite";
const isBoldActive = (editor: Editor) => {
return editor.read.marks()?.bold === true;
};
const isCodeBlockActive = (editor: Editor) => {
const match = editor.read.nodes.find({
match: (node) => ElementApi.isElement(node) && node.type === "code",
});
return Boolean(match);
};
const toggleBold = (editor: Editor) => {
editor.update.marks.toggle("bold");
};
const toggleCodeBlock = (editor: Editor) => {
const isActive = isCodeBlockActive(editor);
editor.update((tx) => {
tx.nodes.set(
{ type: isActive ? "paragraph" : "code" },
{
match: (node) =>
ElementApi.isElement(node) && !tx.schema.isInline(node),
}
);
});
};These functions are not added to the editor object. They are normal JavaScript functions that receive an editor.
Use Editable onKeyDown for keyboard shortcuts that belong to one editor UI:
import { Editable, Plite, usePliteEditor } from "@platejs/plite-react";
const App = () => {
const editor = usePliteEditor({ initialValue });
return (
<Plite editor={editor}>
<Editable
onKeyDown={(event, { editor }) => {
if (event.key === "`" && event.ctrlKey) {
event.preventDefault();
toggleCodeBlock(editor);
return true;
}
if (event.key === "b" && event.ctrlKey) {
event.preventDefault();
toggleBold(editor);
return true;
}
}}
renderElement={renderElement}
renderLeaf={renderLeaf}
/>
</Plite>
);
};import { Editable, Plite, usePliteEditor } from "@platejs/plite-react";
const App = () => {
const editor = usePliteEditor({ initialValue });
return (
<Plite editor={editor}>
<Editable
onKeyDown={(event, { editor }) => {
if (event.key === "`" && event.ctrlKey) {
event.preventDefault();
toggleCodeBlock(editor);
return true;
}
if (event.key === "b"
Use extension commands for reusable semantic behavior. This keeps the same
model rule available to keyboard input, native input, toolbar logic,
programmatic calls, and tests.
import {
defineExtension,
editorCommands,
ElementApi,
PointApi,
RangeApi,
} from "@platejs/plite";
const markdownBlocks = defineExtension("markdown-blocks", {
commands: ({ around, handle }) => [
handle(editorCommands.delete, ({ input, state }) => {
const selection = state.selection();
if (
input.direction === "backward" &&
input.unit === "character" &&
selection &&
RangeApi.isCollapsed(selection)
) {
const blockEntry = state.nodes.block({ at: selection });
if (blockEntry) {
const [block, blockPath] = blockEntry;
const start = state.points.start(blockPath);
if (
ElementApi.isElement(block) &&
block.type !== "paragraph" &&
PointApi.equals(selection.anchor, start)
) {
return state.transaction((tx) => {
tx.blocks.set({ type: "paragraph" }, { at: blockPath });
});
}
}
}
return false;
}),
around(editorCommands.insertBreak, ({ next, state }) => {
const selection = state.selection();
if (selection && RangeApi.isCollapsed(selection)) {
const blockEntry = state.nodes.block({ at: selection });
if (blockEntry) {
const [block, blockPath] = blockEntry;
if (ElementApi.isElement(block) && block.type === "heading-one") {
const start = state.points.start(blockPath);
if (PointApi.equals(selection.anchor, start)) {
return next.after(
state.transaction((tx) => {
tx.blocks.set({ type: "paragraph" }, { at: blockPath });
})
);
}
}
}
}
return next();
}),
],
});
const App = () => {
const editor = usePliteEditor({
extensions: [markdownBlocks],
initialValue,
});
return (
<Plite editor={editor}>
<Editable renderElement={renderElement} renderLeaf={renderLeaf} />
</Plite>
);
};import {
defineExtension,
editorCommands,
ElementApi,
PointApi,
RangeApi,
} from "@platejs/plite";
const markdownBlocks = defineExtension("markdown-blocks", {
commands: ({ around, handle }) => [
handle(editorCommands.delete, ({ input, state }) => {
const selection = state.selection();
if (
input.direction === "backward" &&
input.unit === "character" &&
selection &&
The same functions can be called from toolbar buttons:
const Toolbar = ({ editor }) => {
return (
<div>
<button
onMouseDown={(event) => {
event.preventDefault();
toggleBold(editor);
}}
>
Bold
</button>
<button
onMouseDown={(event) => {
event.preventDefault();
toggleCodeBlock(editor);
}}
>
Code Block
</button>
</div>
);
};const Toolbar = ({ editor }) => {
return (
<div>
<button
onMouseDown={(event) => {
event.preventDefault();
toggleBold(editor);
}}
>
Bold
</button>
<button
onMouseDown={(event) => {
event.preventDefault();
toggleCodeBlock(editor);
}}
>
Code Block
</
Plain functions are enough for app code. Extensions can expose typed state
and tx namespaces when a behavior needs to be shared across editors.
Raw Plite does not ship product commands like lists, headings, or links. Those belong in extensions or higher-level frameworks.