mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-07-31 20:40:19 +02:00
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
This commit is contained in:
5
.changeset/red-mice-mate.md
Normal file
5
.changeset/red-mice-mate.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'slate': minor
|
||||
---
|
||||
|
||||
Allow to prevent data-loss in normalizeNode
|
@@ -418,7 +418,7 @@ Check if a value is a void `Element` object.
|
||||
|
||||
### Normalize methods
|
||||
|
||||
#### `normalizeNode(entry: NodeEntry, { operation }) => void`
|
||||
#### `normalizeNode(entry: NodeEntry, { operation, fallbackElement }) => void`
|
||||
|
||||
[Normalize](../../concepts/11-normalizing.md) a Node according to the schema.
|
||||
|
||||
|
@@ -90,7 +90,7 @@ Or you can even define custom "normalizations" that take place to ensure that li
|
||||
```javascript
|
||||
const { normalizeNode } = editor
|
||||
|
||||
editor.normalizeNode = entry => {
|
||||
editor.normalizeNode = (entry, options) => {
|
||||
const [node, path] = entry
|
||||
|
||||
if (Element.isElement(node) && node.type === 'link') {
|
||||
@@ -98,7 +98,7 @@ editor.normalizeNode = entry => {
|
||||
return
|
||||
}
|
||||
|
||||
normalizeNode(entry)
|
||||
normalizeNode(entry, options)
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -10,7 +10,7 @@ Slate editors come with a few built-in constraints out of the box. These constra
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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'
|
||||
const withParagraphs = editor => {
|
||||
const { normalizeNode } = editor
|
||||
|
||||
editor.normalizeNode = entry => {
|
||||
editor.normalizeNode = (entry, options) => {
|
||||
const [node, path] = entry
|
||||
|
||||
// If the element is a paragraph, ensure its children are valid.
|
||||
@@ -48,7 +48,7 @@ const withParagraphs = editor => {
|
||||
}
|
||||
|
||||
// Fall back to the original `normalizeNode` to enforce other constraints.
|
||||
normalizeNode(entry)
|
||||
normalizeNode(entry, options)
|
||||
}
|
||||
|
||||
return editor
|
||||
@@ -135,7 +135,7 @@ For example, consider a normalization that ensured `link` elements have a valid
|
||||
const withLinks = editor => {
|
||||
const { normalizeNode } = editor
|
||||
|
||||
editor.normalizeNode = entry => {
|
||||
editor.normalizeNode = (entry, options) => {
|
||||
const [node, path] = entry
|
||||
|
||||
if (
|
||||
@@ -148,7 +148,7 @@ const withLinks = editor => {
|
||||
return
|
||||
}
|
||||
|
||||
normalizeNode(entry)
|
||||
normalizeNode(entry, options)
|
||||
}
|
||||
|
||||
return editor
|
||||
|
@@ -145,11 +145,11 @@ const SlateEditor = ({ sharedType, provider }) => {
|
||||
|
||||
// Ensure editor always has at least 1 valid child
|
||||
const { normalizeNode } = e
|
||||
e.normalizeNode = entry => {
|
||||
e.normalizeNode = (entry, options) => {
|
||||
const [node] = entry
|
||||
|
||||
if (!Editor.isEditor(node) || node.children.length > 0) {
|
||||
return normalizeNode(entry)
|
||||
return normalizeNode(entry, options)
|
||||
}
|
||||
|
||||
Transforms.insertNodes(editor, initialValue, { at: [0] })
|
||||
@@ -369,11 +369,11 @@ const SlateEditor = ({ sharedType, provider }) => {
|
||||
|
||||
// Ensure editor always has at least 1 valid child
|
||||
const { normalizeNode } = e
|
||||
e.normalizeNode = entry => {
|
||||
e.normalizeNode = (entry, options) => {
|
||||
const [node] = entry
|
||||
|
||||
if (!Editor.isEditor(node) || node.children.length > 0) {
|
||||
return normalizeNode(entry)
|
||||
return normalizeNode(entry, options)
|
||||
}
|
||||
|
||||
Transforms.insertNodes(editor, initialValue, { at: [0] })
|
||||
|
@@ -7,7 +7,8 @@ import { Editor } from '../interfaces/editor'
|
||||
|
||||
export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
|
||||
editor,
|
||||
entry
|
||||
entry,
|
||||
options
|
||||
) => {
|
||||
const [node, path] = entry
|
||||
|
||||
@@ -54,7 +55,14 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
|
||||
// text.
|
||||
if (isInlineOrText !== shouldHaveInlines) {
|
||||
if (isInlineOrText) {
|
||||
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
|
||||
if (options?.fallbackElement) {
|
||||
Transforms.wrapNodes(editor, options.fallbackElement(), {
|
||||
at: path.concat(n),
|
||||
voids: true,
|
||||
})
|
||||
} else {
|
||||
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
|
||||
}
|
||||
} else {
|
||||
Transforms.unwrapNodes(editor, { at: path.concat(n), voids: true })
|
||||
}
|
||||
|
@@ -54,7 +54,13 @@ export interface BaseEditor {
|
||||
isElementReadOnly: (element: Element) => boolean
|
||||
isSelectable: (element: Element) => boolean
|
||||
markableVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
|
||||
normalizeNode: (
|
||||
entry: NodeEntry,
|
||||
options?: {
|
||||
operation?: Operation
|
||||
fallbackElement?: () => Element
|
||||
}
|
||||
) => void
|
||||
onChange: (options?: { operation?: Operation }) => void
|
||||
shouldNormalize: ({
|
||||
iteration,
|
||||
|
@@ -25,8 +25,14 @@ describe('slate', () => {
|
||||
assert.deepEqual(editor.selection, output.selection)
|
||||
})
|
||||
fixtures(__dirname, 'normalization', ({ module }) => {
|
||||
const { input, output } = module
|
||||
const { input, output, withFallbackElement } = module
|
||||
const editor = withTest(input)
|
||||
if (withFallbackElement) {
|
||||
const { normalizeNode } = editor
|
||||
editor.normalizeNode = (entry, options) => {
|
||||
normalizeNode(entry, { ...options, fallbackElement: () => ({}) })
|
||||
}
|
||||
}
|
||||
Editor.normalize(editor, { force: true })
|
||||
assert.deepEqual(editor.children, output.children)
|
||||
assert.deepEqual(editor.selection, output.selection)
|
||||
|
@@ -0,0 +1,33 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from '../..'
|
||||
|
||||
export const withFallbackElement = true
|
||||
|
||||
export const input = (
|
||||
<editor>
|
||||
<block>
|
||||
<block>one</block>
|
||||
<inline>two</inline>
|
||||
<block>three</block>
|
||||
<inline>four</inline>
|
||||
</block>
|
||||
</editor>
|
||||
)
|
||||
export const output = (
|
||||
<editor>
|
||||
<block>
|
||||
<block>one</block>
|
||||
<block>
|
||||
<text />
|
||||
<inline>two</inline>
|
||||
<text />
|
||||
</block>
|
||||
<block>three</block>
|
||||
<block>
|
||||
<text />
|
||||
<inline>four</inline>
|
||||
<text />
|
||||
</block>
|
||||
</block>
|
||||
</editor>
|
||||
)
|
@@ -0,0 +1,29 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from '../..'
|
||||
|
||||
export const withFallbackElement = true
|
||||
|
||||
export const input = (
|
||||
<editor>
|
||||
<inline>one</inline>
|
||||
<block>two</block>
|
||||
<inline>three</inline>
|
||||
<block>four</block>
|
||||
</editor>
|
||||
)
|
||||
export const output = (
|
||||
<editor>
|
||||
<block>
|
||||
<text />
|
||||
<inline>one</inline>
|
||||
<text />
|
||||
</block>
|
||||
<block>two</block>
|
||||
<block>
|
||||
<text />
|
||||
<inline>three</inline>
|
||||
<text />
|
||||
</block>
|
||||
<block>four</block>
|
||||
</editor>
|
||||
)
|
@@ -0,0 +1,25 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from '../..'
|
||||
|
||||
export const withFallbackElement = true
|
||||
|
||||
export const input = (
|
||||
<editor>
|
||||
<text>one</text>
|
||||
<block>two</block>
|
||||
<text>three</text>
|
||||
<block>four</block>
|
||||
</editor>
|
||||
)
|
||||
export const output = (
|
||||
<editor>
|
||||
<block>
|
||||
<text>one</text>
|
||||
</block>
|
||||
<block>two</block>
|
||||
<block>
|
||||
<text>three</text>
|
||||
</block>
|
||||
<block>four</block>
|
||||
</editor>
|
||||
)
|
Reference in New Issue
Block a user