1
0
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:
Ziad Beyens 2023-02-22 12:54:35 +01:00 committed by GitHub
parent d0d1cb981b
commit 84f811a79c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 39 deletions

View 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.

View File

@ -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.

View File

@ -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

View File

@ -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)
})
}

View File

@ -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

View File

@ -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++
}
})
},