Editing behavior is the path from user intent to committed Plite state. Use this page for the runtime pipeline; use Selection And DOM for caret, native selection, and DOM coverage rules.
Most editing bugs come from putting behavior in the wrong layer.
| Surface | Use it when | Owner |
|---|---|---|
Editable event props | One React editable needs a local browser shortcut or event hook. | plitejs/react |
editor.update((tx) => ...) |
| A command should change the document, selection, marks, roots, or state. |
plitejs |
Plugin commands | A reusable semantic action should compose across browser, programmatic, and test callers. | plitejs |
domCommands.insertData interceptor | Paste or drop ingress needs package-owned DOM policy. | plitejs/dom |
| DOM coverage boundaries | Model content exists but its DOM is intentionally hidden or virtualized. | plitejs/dom and plitejs/react |
@platejs/test | A behavior claim needs model, DOM, native selection, focus, trace, screenshot, or follow-up typing proof. | @platejs/test |
Use Editable for UI-local event interception. Use transactions and plugins for editor behavior that should survive another input path.
Plite edits run through explicit owners.
| Stage | What happens | Owner |
|---|---|---|
| Browser event | The browser sends key, beforeinput, input, paste, cut, drop, focus, drag, or selection events. | Browser |
| Editable handler | Editable runs app handlers and decides whether Plite should continue. | plitejs/react |
| Input import | Plite imports the relevant DOM/native selection when the browser owns the current edit target. | plitejs/react and plitejs/dom |
| Command dispatch | Pure plugin command handlers consume, delegate, or compose typed semantic actions. | plitejs |
| Transaction | editor.update((tx) => ...) groups model writes into one runtime change. | plitejs |
| Canonical change | Plite builds one root-aware DocumentChange for the complete transaction. | plitejs |
| Corrections | Plugin corrections repair changed ranges; construction and fitting enforce canonical shape. | plitejs |
| Commit | Subscribers, history, React, replay, collaboration adapters, and proof tools observe one committed change. | plitejs |
| Render and repair | React renders the new state and exports a valid DOM/native selection when needed. | plitejs/react |
| Proof | Browser tests assert the model, DOM, native selection, focus, trace, and follow-up typing that matter for the claim. | @platejs/test |
The important rule is simple: user intent can arrive through many browser paths, but Plite behavior should land in the transaction pipeline when it changes editor state.
Editable event props are the right tool for editor-local UI behavior.
import { Editable } from "plitejs/react";
<Editable
onKeyDown={(event, { editor }) => {
if (!(event.metaKey && event.key === "k")) return false;
editor.update((tx) => {
tx.text.insert("link");
});
return true;
}}
/>;import { Editable } from "plitejs/react";
<Editable
onKeyDown={(event, { editor }) => {
if (!(event.metaKey && event.key === "k")) return false;
editor.update((tx) => {
tx.text.insert("link");
});
return true;
}}
/>;Return true when your handler owns the event. Return false when Plite should keep running its default behavior.
Use Plite React Event Handling for the exact handler return contract.
Transactions are the write boundary. They group related changes and publish one commit.
editor.update((tx) => {
tx.text.insert("Hello");
tx.marks.toggle("bold");
tx.selection.collapse({ edge: "end" });
});editor.update((tx) => {
tx.text.insert("Hello");
tx.marks.toggle("bold");
tx.selection.collapse({ edge: "end" });
});Keep writes inside one update when they belong to one user action. That gives history, subscribers, React rendering, change replay, and proof tooling one commit to observe.
Use Transforms for the concept guide and Transforms API for the exact transaction groups.
Reusable semantic behavior belongs in pure plugin command handlers when it should apply outside one React event.
import {
definePlugin,
editorCommands,
RangeApi,
} from "plitejs";
const shortcuts = definePlugin("shortcuts", {
commands: ({ handle }) => [
handle(editorCommands.insertText, ({ input, state }) => {
const selection = state.selection();
if (input.text !== " " || !selection || !RangeApi.isCollapsed(selection)) {
return false;
}
return state.transaction((tx) => {
tx.nodes.set({ type: "heading-one" });
tx.text.insert(input.text, input.options);
});
}),
],
});import {
definePlugin,
editorCommands,
RangeApi,
} from "plitejs";
const shortcuts = definePlugin("shortcuts", {
commands: ({ handle }) => [
handle(editorCommands.insertText, ({ input, state }) => {
const selection = state.selection();
if (input.text !== " " || !selection || !RangeApi.isCollapsed(selection)) {
return false;
}
return state.
Use built-in definitions such as editorCommands.insertText,
editorCommands.insertBreak, editorCommands.delete, and
editorCommands.replaceSlice. A handler returns false or an immutable
TransactionSpec. Returning false delegates to the next handler or the
built-in implementation. Use around(descriptor, handler) from the command
factory only when a handler must rewrite input or compose with downstream
behavior.
DocumentChange is the replay boundary. Plite corrects the affected regions
before publishing the final change.
editor.update({ tags: "remote-import" }, (tx) => {
tx.changes.apply(DocumentChange.fromJSON(remoteChange));
});editor.update({ tags: "remote-import" }, (tx) => {
tx.changes.apply(DocumentChange.fromJSON(remoteChange));
});Use Document Changes for replay and mapping. Use Normalizing when a structural edit can leave the document temporarily invalid.
A finished update publishes one commit. Runtime subscribers can observe it, history can batch it, React can render from it, and browser proof can inspect the aftermath.
const unsubscribe = editor.subscribeCommit((commit) => {
if (commit.changed.has("document") || commit.dirtyStateKeys.length > 0) {
saveDocument(editor.read.value());
}
});const unsubscribe = editor.subscribeCommit((commit) => {
if (commit.changed.has("document") || commit.dirtyStateKeys.length > 0) {
saveDocument(editor.read.value());
}
});React UI should read narrow editor facts through hooks where possible. App services can subscribe to commits when they need persistence, analytics, replay, or sync work.
Model-only tests do not prove browser editing. DOM-only tests do not prove Plite correctness.
@platejs/test reads the editor through a browser handle that the app under
test installs. Call installBrowserHandle() once at module scope in a test or
development entry, before the first editor mounts. Production entries never
call it, so their bundles drop the handle.
import { installBrowserHandle } from "plitejs/react";
installBrowserHandle();import { installBrowserHandle } from "plitejs/react";
installBrowserHandle();Plate apps import the same installer from platejs/react.
Each Playwright project sets use.clipboardTransport to 'native', 'event'
or 'handle', typed by BrowserTestOptions from @platejs/test/playwright.
Clipboard paste helpers fail when that transport did not apply the paste.
import { openExample } from "@platejs/test/playwright";
const editor = await openExample(page, "plaintext", {
ready: { editor: "visible" },
});
await editor.focus();
await editor.type("Hello");
await editor.assert.text("Hello");
await editor.assert.selection({
anchor: { path: [0, 0], offset: 5 },
focus: { path: [0, 0], offset: 5 },
});
await editor.assert.noDoubleSelectionHighlight();import { openExample } from "@platejs/test/playwright";
const editor = await openExample(page, "plaintext", {
ready: { editor: "visible" },
});
await editor.focus();
await editor.type("Hello");
await editor.assert.text("Hello");
await editor.assert.selection({
anchor: { path: [0, 0], offset: 5 },
focus: { path: [0, 0], offset: 5 },
});
await editor.assert.noDoubleSelectionHighlight();Use browser proof when a claim depends on browser events, focus, native selection, screenshots, clipboard, replay, or follow-up typing.
| Goal | Start with |
|---|---|
| Add one local hotkey | Plite React Event Handling |
| Write a reusable command | Commands and Transforms |
| Change Enter, Backspace, Delete, or typed text behavior | Commands and Plugins |
| Preserve valid document shape | Normalizing |
| Apply document changes from storage or sync | Canonical Change Substrate |
| Debug caret or DOM selection bugs | Selection And DOM |
| Own paste, copy, drop, or fragment import policy | Clipboard And Paste |
| Build comments, highlights, diagnostics, or floating UI | Decorations, annotations, and widgets |
| Prove a browser editing claim | Browser proof |
Done. You can now place an editing behavior in the layer that owns it.