Skip to content

Commit 11b957a

Browse files
authored
Add way to prevent data loss in normalizeNode (#5878)
* fix(docs): Consider passed options when overriding normalizeNode * feat: Allow to prevent data-loss on normalizeNode When overriding normalizeNode, you can specify a `wrapperElement` that is used to wrap text & inline nodes which would otherwise be deleted in the normalization path if they are not allowed. * changeset
1 parent ffe3f8c commit 11b957a

File tree

11 files changed

+128
-16
lines changed

11 files changed

+128
-16
lines changed

.changeset/red-mice-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'slate': minor
3+
---
4+
5+
Allow to prevent data-loss in normalizeNode

docs/api/nodes/editor.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ Check if a value is a void `Element` object.
418418

419419
### Normalize methods
420420

421-
#### `normalizeNode(entry: NodeEntry, { operation }) => void`
421+
#### `normalizeNode(entry: NodeEntry, { operation, fallbackElement }) => void`
422422

423423
[Normalize](../../concepts/11-normalizing.md) a Node according to the schema.
424424

docs/concepts/07-editor.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,15 @@ Or you can even define custom "normalizations" that take place to ensure that li
9090
```javascript
9191
const { normalizeNode } = editor
9292

93-
editor.normalizeNode = entry => {
93+
editor.normalizeNode = (entry, options) => {
9494
const [node, path] = entry
9595

9696
if (Element.isElement(node) && node.type === 'link') {
9797
// ...
9898
return
9999
}
100100

101-
normalizeNode(entry)
101+
normalizeNode(entry, options)
102102
}
103103
```
104104

docs/concepts/11-normalizing.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Slate editors come with a few built-in constraints out of the box. These constra
1010

1111
1. **All `Element` nodes must contain at least one `Text` descendant** — even [Void Elements](./02-nodes.md#voids). If an element node does not contain any children, an empty text node will be added as its only child. This constraint exists to ensure that the selection's anchor and focus points \(which rely on referencing text nodes\) can always be placed inside any node. Without this, empty elements \(or void elements\) wouldn't be selectable.
1212
2. **Two adjacent texts with the same custom properties will be merged.** If two adjacent text nodes have the same formatting, they're merged into a single text node with a combined text string of the two. This exists to prevent the text nodes from only ever expanding in count in the document, since both adding and removing formatting results in splitting text nodes.
13-
3. **Block nodes can only contain other blocks, or inline and text nodes.** For example, a `paragraph` block cannot have another `paragraph` block element _and_ a `link` inline element as children at the same time. The type of children allowed is determined by the first child, and any other non-conforming children are removed. This ensures that common richtext behaviors like "splitting a block in two" function consistently.
13+
3. **Block nodes can only contain other blocks, or inline and text nodes.** For example, a `paragraph` block cannot have another `paragraph` block element _and_ a `link` inline element as children at the same time. The type of children allowed is determined by the first child. Any other non-conforming children are tried to be converted (if possible) or removed. This ensures that common richtext behaviors like "splitting a block in two" function consistently. Conversion of block nodes is done by unwrapping the block node; conversion of inline/text nodes is performed by wrapping such nodes into a `fallbackElement` if specified in the `normalizeNode` options. The `fallbackElement` can be specified by editors overriding the `normalizeNode` function.
1414
4. **Inline nodes cannot be the first or last child of a parent block, nor can it be next to another inline node in the children array.** If this is the case, an empty text node will be added to correct this to be in compliance with the constraint.
1515
5. **The top-level editor node can only contain block nodes.** If any of the top-level children are inline or text nodes they will be removed. This ensures that there are always block nodes in the editor so that behaviors like "splitting a block in two" work as expected.
1616
6. **Nodes must be JSON-serializable.** For example, avoid using `undefined` in your data model. This ensures that [operations](./05-operations.md) are also JSON-serializable, a property which is assumed by collaboration libraries.
@@ -34,7 +34,7 @@ import { Transforms, Element, Node } from 'slate'
3434
const withParagraphs = editor => {
3535
const { normalizeNode } = editor
3636

37-
editor.normalizeNode = entry => {
37+
editor.normalizeNode = (entry, options) => {
3838
const [node, path] = entry
3939

4040
// If the element is a paragraph, ensure its children are valid.
@@ -48,7 +48,7 @@ const withParagraphs = editor => {
4848
}
4949

5050
// Fall back to the original `normalizeNode` to enforce other constraints.
51-
normalizeNode(entry)
51+
normalizeNode(entry, options)
5252
}
5353

5454
return editor
@@ -135,7 +135,7 @@ For example, consider a normalization that ensured `link` elements have a valid
135135
const withLinks = editor => {
136136
const { normalizeNode } = editor
137137

138-
editor.normalizeNode = entry => {
138+
editor.normalizeNode = (entry, options) => {
139139
const [node, path] = entry
140140

141141
if (
@@ -148,7 +148,7 @@ const withLinks = editor => {
148148
return
149149
}
150150

151-
normalizeNode(entry)
151+
normalizeNode(entry, options)
152152
}
153153

154154
return editor

docs/walkthroughs/07-enabling-collaborative-editing.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,11 @@ const SlateEditor = ({ sharedType, provider }) => {
145145

146146
// Ensure editor always has at least 1 valid child
147147
const { normalizeNode } = e
148-
e.normalizeNode = entry => {
148+
e.normalizeNode = (entry, options) => {
149149
const [node] = entry
150150

151151
if (!Editor.isEditor(node) || node.children.length > 0) {
152-
return normalizeNode(entry)
152+
return normalizeNode(entry, options)
153153
}
154154

155155
Transforms.insertNodes(editor, initialValue, { at: [0] })
@@ -369,11 +369,11 @@ const SlateEditor = ({ sharedType, provider }) => {
369369

370370
// Ensure editor always has at least 1 valid child
371371
const { normalizeNode } = e
372-
e.normalizeNode = entry => {
372+
e.normalizeNode = (entry, options) => {
373373
const [node] = entry
374374

375375
if (!Editor.isEditor(node) || node.children.length > 0) {
376-
return normalizeNode(entry)
376+
return normalizeNode(entry, options)
377377
}
378378

379379
Transforms.insertNodes(editor, initialValue, { at: [0] })

packages/slate/src/core/normalize-node.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { Editor } from '../interfaces/editor'
77

88
export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
99
editor,
10-
entry
10+
entry,
11+
options
1112
) => {
1213
const [node, path] = entry
1314

@@ -54,7 +55,14 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
5455
// text.
5556
if (isInlineOrText !== shouldHaveInlines) {
5657
if (isInlineOrText) {
57-
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
58+
if (options?.fallbackElement) {
59+
Transforms.wrapNodes(editor, options.fallbackElement(), {
60+
at: path.concat(n),
61+
voids: true,
62+
})
63+
} else {
64+
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
65+
}
5866
} else {
5967
Transforms.unwrapNodes(editor, { at: path.concat(n), voids: true })
6068
}

packages/slate/src/interfaces/editor.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ export interface BaseEditor {
5454
isElementReadOnly: (element: Element) => boolean
5555
isSelectable: (element: Element) => boolean
5656
markableVoid: (element: Element) => boolean
57-
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
57+
normalizeNode: (
58+
entry: NodeEntry,
59+
options?: {
60+
operation?: Operation
61+
fallbackElement?: () => Element
62+
}
63+
) => void
5864
onChange: (options?: { operation?: Operation }) => void
5965
shouldNormalize: ({
6066
iteration,

packages/slate/test/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ describe('slate', () => {
2525
assert.deepEqual(editor.selection, output.selection)
2626
})
2727
fixtures(__dirname, 'normalization', ({ module }) => {
28-
const { input, output } = module
28+
const { input, output, withFallbackElement } = module
2929
const editor = withTest(input)
30+
if (withFallbackElement) {
31+
const { normalizeNode } = editor
32+
editor.normalizeNode = (entry, options) => {
33+
normalizeNode(entry, { ...options, fallbackElement: () => ({}) })
34+
}
35+
}
3036
Editor.normalize(editor, { force: true })
3137
assert.deepEqual(editor.children, output.children)
3238
assert.deepEqual(editor.selection, output.selection)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/** @jsx jsx */
2+
import { jsx } from '../..'
3+
4+
export const withFallbackElement = true
5+
6+
export const input = (
7+
<editor>
8+
<block>
9+
<block>one</block>
10+
<inline>two</inline>
11+
<block>three</block>
12+
<inline>four</inline>
13+
</block>
14+
</editor>
15+
)
16+
export const output = (
17+
<editor>
18+
<block>
19+
<block>one</block>
20+
<block>
21+
<text />
22+
<inline>two</inline>
23+
<text />
24+
</block>
25+
<block>three</block>
26+
<block>
27+
<text />
28+
<inline>four</inline>
29+
<text />
30+
</block>
31+
</block>
32+
</editor>
33+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/** @jsx jsx */
2+
import { jsx } from '../..'
3+
4+
export const withFallbackElement = true
5+
6+
export const input = (
7+
<editor>
8+
<inline>one</inline>
9+
<block>two</block>
10+
<inline>three</inline>
11+
<block>four</block>
12+
</editor>
13+
)
14+
export const output = (
15+
<editor>
16+
<block>
17+
<text />
18+
<inline>one</inline>
19+
<text />
20+
</block>
21+
<block>two</block>
22+
<block>
23+
<text />
24+
<inline>three</inline>
25+
<text />
26+
</block>
27+
<block>four</block>
28+
</editor>
29+
)

0 commit comments

Comments
 (0)