Clipboard work crosses browser events, Plite fragments, transactions, DOM
coverage, and browser proof. Use this page to decide whether a paste, copy, or
drop policy belongs in Editable, an extension, editor.api.dom.clipboard, or a
fragment transform.
Paste bugs usually come from mixing browser event ownership with model insertion ownership.
| Need | Start with | Owner |
|---|---|---|
| One editor instance needs a local paste/drop hook | Editable onPaste or onDrop | @platejs/plite-react |
| A reusable package owns paste/drop import policy | extension contribution clipboardHandler(...) | @platejs/plite-dom |
| A host format needs parsing or serialization | Inline a codec in hostCodecs | @platejs/plite-dom |
Framework code needs to import a DataTransfer | editor.api.dom.clipboard.insertData(data) | @platejs/plite-dom through @platejs/plite-react |
| Parsed or structural content is already decoded | tx.slice.replace(slice, options?) | @platejs/plite |
| Decoded content must fit a detached parent | state.slice.fitContent(slice, { parent, root? }) | @platejs/plite |
| Copy or drag must include hidden model content | DOM coverage copyPolicy plus model-backed clipboard data | @platejs/plite-dom and @platejs/plite-react |
| The claim depends on real browser clipboard behavior | @platejs/browser clipboard helpers | @platejs/browser |
Use Editable for local event interception. Use a DOM clipboard handler when
the behavior should apply to native paste, drop, browser tests, and every React
surface that installs the extension.
Clipboard data enters Plite through explicit layers.
| Stage | What happens | Owner |
|---|---|---|
| Browser event | The browser produces paste, cut, copy, dragstart, or drop with a DataTransfer. | Browser |
| Editable handler | App handlers can handle the event or let Plite continue. | @platejs/plite-react |
| Clipboard handlers | Descriptor-owned clipboardHandler(...) contributions can claim the payload. | @platejs/plite-dom |
| DOM clipboard import | Plite reads its internal fragment, then registered host codecs, then plain text. | @platejs/plite-dom |
| Transaction | A parsed slice is fitted at the actual range and applied through one canonical replacement. | @platejs/plite |
| Commit and render | Plite publishes one change; React renders and repairs selection. | @platejs/plite and @platejs/plite-react |
| Proof | Browser tests assert model content, DOM/native selection where needed, focus, clipboard payload, and follow-up typing. | @platejs/browser |
Do not close a paste bug with only a model assertion when the failure was in the browser event, DOM clipboard payload, native selection, or follow-up typing.
Use clipboardHandler(...) when a feature owns a reusable DOM import rule.
import { defineExtension } from "@platejs/plite";
import { clipboardHandler } from "@platejs/plite-dom";
const pasteTodoPrefix = defineExtension("paste-todo-prefix", {
contributions: [
clipboardHandler({
insertData(data, { next, transaction }) {
const text = data.getData("text/plain");
if (!text.startsWith("todo:")) {
return next();
}
transaction.text.insert(text.slice("todo:".length).trim());
return true;
},
}),
],
});import { defineExtension } from "@platejs/plite";
import { clipboardHandler } from "@platejs/plite-dom";
const pasteTodoPrefix = defineExtension("paste-todo-prefix", {
contributions: [
clipboardHandler({
insertData(data, { next, transaction }) {
const text = data.getData("text/plain");
if (!text.startsWith("todo:")) {
return next();
}
transaction.text.insert(text.slice("todo:".
The handler receives the transaction owned by DOM clipboard ingress. Return
true when the extension handled the payload. Return next() when Plite
should keep running the exact-slice and plain-text import path in that
transaction. DataTransfer does not enter @platejs/plite; headless code
starts from a ContentSlice.
Use this for package-owned import rules such as custom inline syntax, pasted URLs, product fragments, and table-specific paste policy. Do not put those rules in Plite core unless the rule is part of Plite's model contract.
React editors expose DOM clipboard helpers through editor.api.dom.clipboard.
editor.api.dom.clipboard.insertData(dataTransfer);
editor.api.dom.clipboard.insertFragmentData(dataTransfer);
editor.api.dom.clipboard.insertTextData(dataTransfer);
editor.api.dom.clipboard.readSlice(dataTransfer);
editor.api.dom.clipboard.writeSelection(dataTransfer);
editor.api.dom.clipboard.writeSlice(dataTransfer, { slice });editor.api.dom.clipboard.insertData(dataTransfer);
editor.api.dom.clipboard.insertFragmentData(dataTransfer);
editor.api.dom.clipboard.insertTextData(dataTransfer);
editor.api.dom.clipboard.readSlice(dataTransfer);
editor.api.dom.clipboard.writeSelection(dataTransfer);
editor.api.dom.clipboard.writeSlice(dataTransfer, { slice });Use these APIs from framework bridges, tests, or low-level event code that
already has a DataTransfer. insertData owns a transaction when called
directly and joins the active transaction when framework code already opened
one. Clipboard handlers mutate through the supplied transaction.
readSlice distinguishes { kind: "absent" }, malformed MIME or HTML data as
{ kind: "invalid", source }, and { kind: "slice", slice }. writeSlice
writes one exact ContentSlice plus optional host formats. This keeps missing,
invalid, and valid empty clipboard payloads distinct.
Plite writes plain text, HTML, and an internal Plite fragment payload. The
fragment payload uses application/${clipboardFormatKey}, so editors with
different keys do not blindly import each other's internal JSON.
Registered host codecs add schema-aware MIME formats without putting DOM types
in Plite core. A parser returns one intact ContentSlice; Plite preserves its
open edge depths and detached secondary roots, then fits the complete slice
against the actual paste range. Keep a codec inline in hostCodecs; use
defineHostCodec only when the same codec is reused. Configuration
fails for duplicate codec keys, unknown schema targets, and overlapping
element/text-property claims. A codec rejects invalid external payloads with
null.
Well-formed slices that do not fit leave the transaction untouched and continue
to the next codec or plain-text fallback. Returning a malformed slice is a codec
programming error reported to the editor lifecycle error sink; the dispatcher
continues to the next eligible codec without publishing a partial write.
Use tx.fragment.replace(...) for known-closed content. The compiled schema
fits the content at the actual target.
editor.update((tx) => {
tx.fragment.replace([
{
type: "paragraph",
children: [{ text: "Pasted paragraph" }],
},
]);
});editor.update((tx) => {
tx.fragment.replace([
{
type: "paragraph",
children: [{ text: "Pasted paragraph" }],
},
]);
});Codecs and transport boundaries preserve open edges with ContentSlice.
import { ContentSlice } from "@platejs/plite";
const slice = ContentSlice.fromJSON({
content: decodedContent,
openEnd: 1,
openStart: 1,
roots: {
"note:1": decodedNote,
},
});
editor.update.slice.replace(slice);import { ContentSlice } from "@platejs/plite";
const slice = ContentSlice.fromJSON({
content: decodedContent,
openEnd: 1,
openStart: 1,
roots: {
"note:1": decodedNote,
},
});
editor.update.slice.replace(slice);ContentSlice has one transport shape:
{ content, openStart, openEnd, roots? }. roots carries the transitive
detached secondary roots referenced by the slice content. Inserting the slice
remaps copied keys deterministically and keeps shared aliases together.
Core slice replacement is structural and schema-fitted. Grid-aware table paste, spreadsheet mapping, and product-specific merge rules belong in the table or product extension that understands those structures.
When table code has a detached destination cell, call
state.slice.fitContent(slice, { parent, root? }). It returns frozen,
grammar-valid children or null without publishing editor state. The table
extension still owns row/column mapping, spans, and multi-cell replacement.
Copy and drag can involve model content whose DOM is hidden, staged, or virtualized. DOM coverage boundaries decide whether hidden content participates in copy, find, and selection conversion.
Use DOM Coverage Boundaries
for copyPolicy, findPolicy, selectionPolicy, and materialization behavior.
Use Selection And DOM when a copy or paste bug also
depends on caret position or native selection repair.
Clipboard proof should name the layer that can fail.
| Claim | Useful proof |
|---|---|
| The model inserted the right content | model text, fragment, canonical change, and selection |
| The DOM payload was imported correctly | browser clipboard helper or dispatched DataTransfer |
| Hidden content copied correctly | copied plain text, HTML, Plite fragment, and DOM coverage policy |
| Selection survived paste | model selection, DOM/native selection where observable, and follow-up typing |
| A feature owns paste policy | focused DOM contribution test plus browser paste smoke |
Use Browser for clipboard helpers and Editing Behavior for the full event-to-commit pipeline.