mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-22 14:21:54 +02:00
More control on editor.normalizeNode
(#5295)
* feat * fix * Create two-books-bow.md * docs * feat * fix
This commit is contained in:
parent
d0d1cb981b
commit
84f811a79c
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
|
||||
markableVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry) => void
|
||||
onChange: () => void
|
||||
onChange: (options?: { operation?: Operation }) => void
|
||||
|
||||
// Overrideable core actions.
|
||||
addMark: (key: string, value: any) => void
|
||||
@ -40,7 +40,7 @@ interface Editor {
|
||||
- [Instance methods](editor.md#instance-methods)
|
||||
- [Schema-specific methods to override](editor.md#schema-specific-instance-methods-to-override)
|
||||
- [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)
|
||||
- [Mark Methods](editor.md/#mark-methods)
|
||||
- [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.
|
||||
|
||||
Options: `{force?: boolean}`
|
||||
Options: `{force?: boolean; operation?: Operation}`
|
||||
|
||||
#### `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.
|
||||
|
||||
### Normalize method
|
||||
### Normalize methods
|
||||
|
||||
#### `normalizeNode(entry: NodeEntry) => void`
|
||||
#### `normalizeNode(entry: NodeEntry, { operation }) => void`
|
||||
|
||||
[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
|
||||
|
||||
#### `onChange() => void`
|
||||
#### `onChange(options?: { operation?: Operation }) => void`
|
||||
|
||||
Called when there is a change in the editor.
|
||||
|
||||
|
@ -14,7 +14,7 @@ interface Editor {
|
||||
isVoid: (element: Element) => boolean
|
||||
markableVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry) => void
|
||||
onChange: () => void
|
||||
onChange: (options?: { operation?: Operation }) => void
|
||||
// Overrideable core actions.
|
||||
addMark: (key: string, value: any) => void
|
||||
apply: (operation: Operation) => void
|
||||
|
@ -2,13 +2,13 @@ import ReactDOM from 'react-dom'
|
||||
import {
|
||||
BaseEditor,
|
||||
Editor,
|
||||
Element,
|
||||
Node,
|
||||
Operation,
|
||||
Path,
|
||||
Point,
|
||||
Range,
|
||||
Transforms,
|
||||
Element,
|
||||
} from 'slate'
|
||||
import {
|
||||
TextDiff,
|
||||
@ -28,14 +28,15 @@ import {
|
||||
EDITOR_TO_ON_CHANGE,
|
||||
EDITOR_TO_PENDING_ACTION,
|
||||
EDITOR_TO_PENDING_DIFFS,
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS,
|
||||
EDITOR_TO_PENDING_SELECTION,
|
||||
EDITOR_TO_SCHEDULE_FLUSH,
|
||||
EDITOR_TO_USER_MARKS,
|
||||
EDITOR_TO_USER_SELECTION,
|
||||
NODE_TO_KEY,
|
||||
EDITOR_TO_SCHEDULE_FLUSH,
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS,
|
||||
} from '../utils/weak-maps'
|
||||
import { ReactEditor } from './react-editor'
|
||||
|
||||
/**
|
||||
* `withReact` adds React and DOM specific behaviors to the editor.
|
||||
*
|
||||
@ -322,7 +323,7 @@ export const withReact = <T extends BaseEditor>(
|
||||
return false
|
||||
}
|
||||
|
||||
e.onChange = () => {
|
||||
e.onChange = options => {
|
||||
// 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
|
||||
// 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()
|
||||
}
|
||||
|
||||
onChange()
|
||||
onChange(options)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
Editor,
|
||||
Element,
|
||||
Node,
|
||||
NodeEntry,
|
||||
Operation,
|
||||
Path,
|
||||
PathRef,
|
||||
@ -13,8 +12,8 @@ import {
|
||||
Text,
|
||||
Transforms,
|
||||
} from './'
|
||||
import { DIRTY_PATHS, DIRTY_PATH_KEYS, FLUSHING } from './utils/weak-maps'
|
||||
import { TextUnit } from './interfaces/types'
|
||||
import { DIRTY_PATH_KEYS, DIRTY_PATHS, FLUSHING } from './utils/weak-maps'
|
||||
|
||||
/**
|
||||
* Create a new Slate `Editor` object.
|
||||
@ -81,7 +80,9 @@ export const createEditor = (): Editor => {
|
||||
DIRTY_PATH_KEYS.set(editor, dirtyPathKeys)
|
||||
Transforms.transform(editor, op)
|
||||
editor.operations.push(op)
|
||||
Editor.normalize(editor)
|
||||
Editor.normalize(editor, {
|
||||
operation: op,
|
||||
})
|
||||
|
||||
// Clear any formats applied to the cursor if the selection changes.
|
||||
if (op.type === 'set_selection') {
|
||||
@ -93,7 +94,7 @@ export const createEditor = (): Editor => {
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
FLUSHING.set(editor, false)
|
||||
editor.onChange()
|
||||
editor.onChange({ operation: op })
|
||||
editor.operations = []
|
||||
})
|
||||
}
|
||||
@ -208,7 +209,7 @@ export const createEditor = (): Editor => {
|
||||
}
|
||||
},
|
||||
|
||||
normalizeNode: (entry: NodeEntry) => {
|
||||
normalizeNode: entry => {
|
||||
const [node, path] = entry
|
||||
|
||||
// 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
|
||||
|
@ -17,28 +17,28 @@ import {
|
||||
Text,
|
||||
} from '..'
|
||||
import {
|
||||
DIRTY_PATHS,
|
||||
getCharacterDistance,
|
||||
getWordDistance,
|
||||
splitByCharacterDistance,
|
||||
} from '../utils/string'
|
||||
import {
|
||||
DIRTY_PATH_KEYS,
|
||||
DIRTY_PATHS,
|
||||
NORMALIZING,
|
||||
PATH_REFS,
|
||||
POINT_REFS,
|
||||
RANGE_REFS,
|
||||
} from '../utils/weak-maps'
|
||||
import {
|
||||
getWordDistance,
|
||||
getCharacterDistance,
|
||||
splitByCharacterDistance,
|
||||
} from '../utils/string'
|
||||
import { Descendant } from './node'
|
||||
import { Element } from './element'
|
||||
import { Descendant } from './node'
|
||||
import {
|
||||
LeafEdge,
|
||||
MaximizeMode,
|
||||
RangeDirection,
|
||||
SelectionMode,
|
||||
TextDirection,
|
||||
TextUnit,
|
||||
TextUnitAdjustment,
|
||||
RangeDirection,
|
||||
MaximizeMode,
|
||||
} from './types'
|
||||
|
||||
export type BaseSelection = Range | null
|
||||
@ -62,8 +62,8 @@ export interface BaseEditor {
|
||||
isInline: (element: Element) => boolean
|
||||
isVoid: (element: Element) => boolean
|
||||
markableVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry) => void
|
||||
onChange: () => void
|
||||
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
|
||||
onChange: (options?: { operation?: Operation }) => void
|
||||
|
||||
// Overrideable core actions.
|
||||
addMark: (key: string, value: any) => void
|
||||
@ -78,7 +78,16 @@ export interface BaseEditor {
|
||||
insertNode: (node: Node) => void
|
||||
insertText: (text: 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>
|
||||
@ -145,6 +154,7 @@ export interface EditorNodesOptions<T extends Node> {
|
||||
|
||||
export interface EditorNormalizeOptions {
|
||||
force?: boolean
|
||||
operation?: Operation
|
||||
}
|
||||
|
||||
export interface EditorParentOptions {
|
||||
@ -1007,7 +1017,7 @@ export const Editor: EditorInterface = {
|
||||
*/
|
||||
|
||||
normalize(editor: Editor, options: EditorNormalizeOptions = {}): void {
|
||||
const { force = false } = options
|
||||
const { force = false, operation } = options
|
||||
const getDirtyPaths = (editor: 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.
|
||||
*/
|
||||
if (Element.isElement(node) && node.children.length === 0) {
|
||||
editor.normalizeNode(entry)
|
||||
editor.normalizeNode(entry, { operation })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const max = getDirtyPaths(editor).length * 42 // HACK: better way?
|
||||
let m = 0
|
||||
const dirtyPaths = getDirtyPaths(editor)
|
||||
|
||||
let iteration = 0
|
||||
|
||||
while (getDirtyPaths(editor).length !== 0) {
|
||||
if (m > max) {
|
||||
throw new Error(`
|
||||
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.
|
||||
`)
|
||||
if (
|
||||
!editor.shouldNormalize({
|
||||
iteration,
|
||||
dirtyPaths: getDirtyPaths(editor),
|
||||
operation,
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
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 (Node.has(editor, dirtyPath)) {
|
||||
const entry = Editor.node(editor, dirtyPath)
|
||||
editor.normalizeNode(entry)
|
||||
editor.normalizeNode(entry, { operation })
|
||||
}
|
||||
m++
|
||||
iteration++
|
||||
}
|
||||
})
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user