1
0
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:
Ziad Beyens
2023-02-22 12:54:35 +01:00
committed by GitHub
parent d0d1cb981b
commit 84f811a79c
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 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.

View File

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

View File

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

View File

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

View File

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