mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-07-31 20:40:19 +02:00
More control on editor.normalizeNode
(#5295)
* feat * fix * Create two-books-bow.md * docs * feat * fix
This commit is contained in:
21
.changeset/two-books-bow.md
Normal file
21
.changeset/two-books-bow.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
'slate': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
New `editor` method that can be overridden to control when the normalization should stop. Default behavior (unchanged) is to throw an error when it iterates over 42 times the dirty paths length.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
shouldNormalize: ({
|
||||||
|
iteration,
|
||||||
|
dirtyPaths,
|
||||||
|
operation,
|
||||||
|
}: {
|
||||||
|
iteration: number
|
||||||
|
dirtyPaths: Path[]
|
||||||
|
operation?: Operation
|
||||||
|
}) => boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
- `editor.onChange` signature change: `(options?: { operation?: Operation }) => void` where `operation` is triggering the function.
|
||||||
|
- `editor.normalizeNode` signature change: `(entry: NodeEntry, options?: { operation?: Operation }) => void` where `operation` is triggering the function.
|
||||||
|
- `EditorNormalizeOptions` new option `operation?: Operation` where `operation` is triggering the function.
|
@@ -14,7 +14,7 @@ interface Editor {
|
|||||||
isVoid: (element: Element) => boolean
|
isVoid: (element: Element) => boolean
|
||||||
markableVoid: (element: Element) => boolean
|
markableVoid: (element: Element) => boolean
|
||||||
normalizeNode: (entry: NodeEntry) => void
|
normalizeNode: (entry: NodeEntry) => void
|
||||||
onChange: () => void
|
onChange: (options?: { operation?: Operation }) => void
|
||||||
|
|
||||||
// Overrideable core actions.
|
// Overrideable core actions.
|
||||||
addMark: (key: string, value: any) => void
|
addMark: (key: string, value: any) => void
|
||||||
@@ -40,7 +40,7 @@ interface Editor {
|
|||||||
- [Instance methods](editor.md#instance-methods)
|
- [Instance methods](editor.md#instance-methods)
|
||||||
- [Schema-specific methods to override](editor.md#schema-specific-instance-methods-to-override)
|
- [Schema-specific methods to override](editor.md#schema-specific-instance-methods-to-override)
|
||||||
- [Element Type Methods](editor.md/#element-type-methods)
|
- [Element Type Methods](editor.md/#element-type-methods)
|
||||||
- [Normalize Method](editor.md/#normalize-method)
|
- [Normalize Methods](editor.md/#normalize-methods)
|
||||||
- [Callback Method](editor.md/#callback-method)
|
- [Callback Method](editor.md/#callback-method)
|
||||||
- [Mark Methods](editor.md/#mark-methods)
|
- [Mark Methods](editor.md/#mark-methods)
|
||||||
- [getFragment Method](editor.md/#getfragment-method)
|
- [getFragment Method](editor.md/#getfragment-method)
|
||||||
@@ -341,7 +341,7 @@ Check if a value is a void `Element` object.
|
|||||||
|
|
||||||
Normalize any dirty objects in the editor.
|
Normalize any dirty objects in the editor.
|
||||||
|
|
||||||
Options: `{force?: boolean}`
|
Options: `{force?: boolean; operation?: Operation}`
|
||||||
|
|
||||||
#### `Editor.withoutNormalizing(editor: Editor, fn: () => void) => void`
|
#### `Editor.withoutNormalizing(editor: Editor, fn: () => void) => void`
|
||||||
|
|
||||||
@@ -410,15 +410,21 @@ Check if a value is an inline `Element` object.
|
|||||||
|
|
||||||
Check if a value is a void `Element` object.
|
Check if a value is a void `Element` object.
|
||||||
|
|
||||||
### Normalize method
|
### Normalize methods
|
||||||
|
|
||||||
#### `normalizeNode(entry: NodeEntry) => void`
|
#### `normalizeNode(entry: NodeEntry, { operation }) => void`
|
||||||
|
|
||||||
[Normalize](../../concepts/11-normalizing.md) a Node according to the schema.
|
[Normalize](../../concepts/11-normalizing.md) a Node according to the schema.
|
||||||
|
|
||||||
|
#### `shouldNormalize: (options) => boolean`
|
||||||
|
|
||||||
|
Override this method to prevent normalizing the editor.
|
||||||
|
|
||||||
|
Options: `{ iteration: number; dirtyPaths: Path[]; operation?: Operation(entry: NodeEntry, { operation }`
|
||||||
|
|
||||||
### Callback method
|
### Callback method
|
||||||
|
|
||||||
#### `onChange() => void`
|
#### `onChange(options?: { operation?: Operation }) => void`
|
||||||
|
|
||||||
Called when there is a change in the editor.
|
Called when there is a change in the editor.
|
||||||
|
|
||||||
|
@@ -14,7 +14,7 @@ interface Editor {
|
|||||||
isVoid: (element: Element) => boolean
|
isVoid: (element: Element) => boolean
|
||||||
markableVoid: (element: Element) => boolean
|
markableVoid: (element: Element) => boolean
|
||||||
normalizeNode: (entry: NodeEntry) => void
|
normalizeNode: (entry: NodeEntry) => void
|
||||||
onChange: () => void
|
onChange: (options?: { operation?: Operation }) => void
|
||||||
// Overrideable core actions.
|
// Overrideable core actions.
|
||||||
addMark: (key: string, value: any) => void
|
addMark: (key: string, value: any) => void
|
||||||
apply: (operation: Operation) => void
|
apply: (operation: Operation) => void
|
||||||
|
@@ -2,13 +2,13 @@ import ReactDOM from 'react-dom'
|
|||||||
import {
|
import {
|
||||||
BaseEditor,
|
BaseEditor,
|
||||||
Editor,
|
Editor,
|
||||||
|
Element,
|
||||||
Node,
|
Node,
|
||||||
Operation,
|
Operation,
|
||||||
Path,
|
Path,
|
||||||
Point,
|
Point,
|
||||||
Range,
|
Range,
|
||||||
Transforms,
|
Transforms,
|
||||||
Element,
|
|
||||||
} from 'slate'
|
} from 'slate'
|
||||||
import {
|
import {
|
||||||
TextDiff,
|
TextDiff,
|
||||||
@@ -28,14 +28,15 @@ import {
|
|||||||
EDITOR_TO_ON_CHANGE,
|
EDITOR_TO_ON_CHANGE,
|
||||||
EDITOR_TO_PENDING_ACTION,
|
EDITOR_TO_PENDING_ACTION,
|
||||||
EDITOR_TO_PENDING_DIFFS,
|
EDITOR_TO_PENDING_DIFFS,
|
||||||
|
EDITOR_TO_PENDING_INSERTION_MARKS,
|
||||||
EDITOR_TO_PENDING_SELECTION,
|
EDITOR_TO_PENDING_SELECTION,
|
||||||
|
EDITOR_TO_SCHEDULE_FLUSH,
|
||||||
EDITOR_TO_USER_MARKS,
|
EDITOR_TO_USER_MARKS,
|
||||||
EDITOR_TO_USER_SELECTION,
|
EDITOR_TO_USER_SELECTION,
|
||||||
NODE_TO_KEY,
|
NODE_TO_KEY,
|
||||||
EDITOR_TO_SCHEDULE_FLUSH,
|
|
||||||
EDITOR_TO_PENDING_INSERTION_MARKS,
|
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
import { ReactEditor } from './react-editor'
|
import { ReactEditor } from './react-editor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `withReact` adds React and DOM specific behaviors to the editor.
|
* `withReact` adds React and DOM specific behaviors to the editor.
|
||||||
*
|
*
|
||||||
@@ -322,7 +323,7 @@ export const withReact = <T extends BaseEditor>(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
e.onChange = () => {
|
e.onChange = options => {
|
||||||
// COMPAT: React doesn't batch `setState` hook calls, which means that the
|
// COMPAT: React doesn't batch `setState` hook calls, which means that the
|
||||||
// children and selection can get out of sync for one render pass. So we
|
// children and selection can get out of sync for one render pass. So we
|
||||||
// have to use this unstable API to ensure it batches them. (2019/12/03)
|
// have to use this unstable API to ensure it batches them. (2019/12/03)
|
||||||
@@ -334,7 +335,7 @@ export const withReact = <T extends BaseEditor>(
|
|||||||
onContextChange()
|
onContextChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange()
|
onChange(options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@ import {
|
|||||||
Editor,
|
Editor,
|
||||||
Element,
|
Element,
|
||||||
Node,
|
Node,
|
||||||
NodeEntry,
|
|
||||||
Operation,
|
Operation,
|
||||||
Path,
|
Path,
|
||||||
PathRef,
|
PathRef,
|
||||||
@@ -13,8 +12,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Transforms,
|
Transforms,
|
||||||
} from './'
|
} from './'
|
||||||
import { DIRTY_PATHS, DIRTY_PATH_KEYS, FLUSHING } from './utils/weak-maps'
|
|
||||||
import { TextUnit } from './interfaces/types'
|
import { TextUnit } from './interfaces/types'
|
||||||
|
import { DIRTY_PATH_KEYS, DIRTY_PATHS, FLUSHING } from './utils/weak-maps'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Slate `Editor` object.
|
* Create a new Slate `Editor` object.
|
||||||
@@ -81,7 +80,9 @@ export const createEditor = (): Editor => {
|
|||||||
DIRTY_PATH_KEYS.set(editor, dirtyPathKeys)
|
DIRTY_PATH_KEYS.set(editor, dirtyPathKeys)
|
||||||
Transforms.transform(editor, op)
|
Transforms.transform(editor, op)
|
||||||
editor.operations.push(op)
|
editor.operations.push(op)
|
||||||
Editor.normalize(editor)
|
Editor.normalize(editor, {
|
||||||
|
operation: op,
|
||||||
|
})
|
||||||
|
|
||||||
// Clear any formats applied to the cursor if the selection changes.
|
// Clear any formats applied to the cursor if the selection changes.
|
||||||
if (op.type === 'set_selection') {
|
if (op.type === 'set_selection') {
|
||||||
@@ -93,7 +94,7 @@ export const createEditor = (): Editor => {
|
|||||||
|
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
FLUSHING.set(editor, false)
|
FLUSHING.set(editor, false)
|
||||||
editor.onChange()
|
editor.onChange({ operation: op })
|
||||||
editor.operations = []
|
editor.operations = []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -208,7 +209,7 @@ export const createEditor = (): Editor => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
normalizeNode: (entry: NodeEntry) => {
|
normalizeNode: entry => {
|
||||||
const [node, path] = entry
|
const [node, path] = entry
|
||||||
|
|
||||||
// There are no core normalizations for text nodes.
|
// There are no core normalizations for text nodes.
|
||||||
@@ -412,6 +413,18 @@ export const createEditor = (): Editor => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldNormalize: ({ iteration, dirtyPaths }) => {
|
||||||
|
const maxIterations = dirtyPaths.length * 42 // HACK: better way?
|
||||||
|
|
||||||
|
if (iteration > maxIterations) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not completely normalize the editor after ${maxIterations} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return editor
|
return editor
|
||||||
|
@@ -17,28 +17,28 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '..'
|
} from '..'
|
||||||
import {
|
import {
|
||||||
DIRTY_PATHS,
|
getCharacterDistance,
|
||||||
|
getWordDistance,
|
||||||
|
splitByCharacterDistance,
|
||||||
|
} from '../utils/string'
|
||||||
|
import {
|
||||||
DIRTY_PATH_KEYS,
|
DIRTY_PATH_KEYS,
|
||||||
|
DIRTY_PATHS,
|
||||||
NORMALIZING,
|
NORMALIZING,
|
||||||
PATH_REFS,
|
PATH_REFS,
|
||||||
POINT_REFS,
|
POINT_REFS,
|
||||||
RANGE_REFS,
|
RANGE_REFS,
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
import {
|
|
||||||
getWordDistance,
|
|
||||||
getCharacterDistance,
|
|
||||||
splitByCharacterDistance,
|
|
||||||
} from '../utils/string'
|
|
||||||
import { Descendant } from './node'
|
|
||||||
import { Element } from './element'
|
import { Element } from './element'
|
||||||
|
import { Descendant } from './node'
|
||||||
import {
|
import {
|
||||||
LeafEdge,
|
LeafEdge,
|
||||||
|
MaximizeMode,
|
||||||
|
RangeDirection,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
TextDirection,
|
TextDirection,
|
||||||
TextUnit,
|
TextUnit,
|
||||||
TextUnitAdjustment,
|
TextUnitAdjustment,
|
||||||
RangeDirection,
|
|
||||||
MaximizeMode,
|
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export type BaseSelection = Range | null
|
export type BaseSelection = Range | null
|
||||||
@@ -62,8 +62,8 @@ export interface BaseEditor {
|
|||||||
isInline: (element: Element) => boolean
|
isInline: (element: Element) => boolean
|
||||||
isVoid: (element: Element) => boolean
|
isVoid: (element: Element) => boolean
|
||||||
markableVoid: (element: Element) => boolean
|
markableVoid: (element: Element) => boolean
|
||||||
normalizeNode: (entry: NodeEntry) => void
|
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
|
||||||
onChange: () => void
|
onChange: (options?: { operation?: Operation }) => void
|
||||||
|
|
||||||
// Overrideable core actions.
|
// Overrideable core actions.
|
||||||
addMark: (key: string, value: any) => void
|
addMark: (key: string, value: any) => void
|
||||||
@@ -78,7 +78,16 @@ export interface BaseEditor {
|
|||||||
insertNode: (node: Node) => void
|
insertNode: (node: Node) => void
|
||||||
insertText: (text: string) => void
|
insertText: (text: string) => void
|
||||||
removeMark: (key: string) => void
|
removeMark: (key: string) => void
|
||||||
getDirtyPaths: (op: Operation) => Path[]
|
getDirtyPaths: (operation: Operation) => Path[]
|
||||||
|
shouldNormalize: ({
|
||||||
|
iteration,
|
||||||
|
dirtyPaths,
|
||||||
|
operation,
|
||||||
|
}: {
|
||||||
|
iteration: number
|
||||||
|
dirtyPaths: Path[]
|
||||||
|
operation?: Operation
|
||||||
|
}) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Editor = ExtendedType<'Editor', BaseEditor>
|
export type Editor = ExtendedType<'Editor', BaseEditor>
|
||||||
@@ -145,6 +154,7 @@ export interface EditorNodesOptions<T extends Node> {
|
|||||||
|
|
||||||
export interface EditorNormalizeOptions {
|
export interface EditorNormalizeOptions {
|
||||||
force?: boolean
|
force?: boolean
|
||||||
|
operation?: Operation
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorParentOptions {
|
export interface EditorParentOptions {
|
||||||
@@ -1007,7 +1017,7 @@ export const Editor: EditorInterface = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
normalize(editor: Editor, options: EditorNormalizeOptions = {}): void {
|
normalize(editor: Editor, options: EditorNormalizeOptions = {}): void {
|
||||||
const { force = false } = options
|
const { force = false, operation } = options
|
||||||
const getDirtyPaths = (editor: Editor) => {
|
const getDirtyPaths = (editor: Editor) => {
|
||||||
return DIRTY_PATHS.get(editor) || []
|
return DIRTY_PATHS.get(editor) || []
|
||||||
}
|
}
|
||||||
@@ -1057,19 +1067,24 @@ export const Editor: EditorInterface = {
|
|||||||
by definition adding children to an empty node can't cause other paths to change.
|
by definition adding children to an empty node can't cause other paths to change.
|
||||||
*/
|
*/
|
||||||
if (Element.isElement(node) && node.children.length === 0) {
|
if (Element.isElement(node) && node.children.length === 0) {
|
||||||
editor.normalizeNode(entry)
|
editor.normalizeNode(entry, { operation })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const max = getDirtyPaths(editor).length * 42 // HACK: better way?
|
const dirtyPaths = getDirtyPaths(editor)
|
||||||
let m = 0
|
|
||||||
|
let iteration = 0
|
||||||
|
|
||||||
while (getDirtyPaths(editor).length !== 0) {
|
while (getDirtyPaths(editor).length !== 0) {
|
||||||
if (m > max) {
|
if (
|
||||||
throw new Error(`
|
!editor.shouldNormalize({
|
||||||
Could not completely normalize the editor after ${max} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.
|
iteration,
|
||||||
`)
|
dirtyPaths: getDirtyPaths(editor),
|
||||||
|
operation,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirtyPath = popDirtyPath(editor)
|
const dirtyPath = popDirtyPath(editor)
|
||||||
@@ -1077,9 +1092,9 @@ export const Editor: EditorInterface = {
|
|||||||
// If the node doesn't exist in the tree, it does not need to be normalized.
|
// If the node doesn't exist in the tree, it does not need to be normalized.
|
||||||
if (Node.has(editor, dirtyPath)) {
|
if (Node.has(editor, dirtyPath)) {
|
||||||
const entry = Editor.node(editor, dirtyPath)
|
const entry = Editor.node(editor, dirtyPath)
|
||||||
editor.normalizeNode(entry)
|
editor.normalizeNode(entry, { operation })
|
||||||
}
|
}
|
||||||
m++
|
iteration++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user