portabletext / portabletext/editor
Behaviors that delete text during `insert.break` crash on Android
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 277
- Forks
- 21
- Avg merge
- 14h 23m
- Merged PRs (30d)
- 133
Description
Description
Custom behaviors that intercept insert.break and perform text-level deletions (e.g., raise({ type: 'delete.text' })) crash on Android with:
Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
Steps to reproduce
- Create a behavior that intercepts
insert.breakand deletes text:
const dissolveOnBreak = defineBehavior({
on: 'insert.break',
guard: ({ snapshot }) => {
const focusTextBlock = getFocusTextBlock(snapshot)
if (!focusTextBlock) return false
const text = focusTextBlock.node.children.map(c => c.text).join('')
if (text === '- ') return { focusTextBlock, prefixLength: 2 }
return false
},
actions: [
(_, { focusTextBlock, prefixLength }) => [
raise({
type: 'delete.text',
at: {
anchor: { path: focusTextBlock.path, offset: 0 },
focus: { path: focusTextBlock.path, offset: prefixLength },
},
}),
],
],
})
- Open the editor on an Android device
- Type
-(dash space) - Press Enter
- Crash
Expected behavior
The behavior deletes the prefix text and the block is dissolved — same as on desktop.
Actual behavior
removeChild DOM error. The editor crashes.
Analysis
On Android, beforeinput for insertParagraph is not cancelable — the browser modifies the DOM (splits the block) before PTE's behavior pipeline fires insert.break. restoreDOM reverts the structural split, but when the behavior then raises delete.text, Slate tries to reconcile the text deletion against a DOM that was already mutated by the native Enter handling.
PTE's own list dissolution behaviors avoid this because they use metadata operations (block.unset for listItem/level) which don't touch DOM text nodes. But any behavior that modifies text content during insert.break hits this race condition.
Workaround
Use block-level operations instead of text deletion:
actions: [
(_, { focusTextBlock }) => [
raise({
type: 'insert.block',
block: { /* empty block */ },
placement: 'after',
select: 'start',
}),
raise({
type: 'delete.block',
at: focusTextBlock.path,
}),
],
]
Environment
- Android Chrome (tested on multiple devices)
- Works fine on desktop browsers and iOS Safari
- PTE 6.0.0
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the behavior on Android Chrome with the provided dissolveOnBreak example, then trace the insert.break behavior pipeline around restoreDOM and the subsequent delete.text operation. Done means pressing Enter after typing '- ' deletes the prefix and dissolves the block without a removeChild crash, while preserving the existing desktop and iOS behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, typescript
- Domain
- frontend, mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100