Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More control on editor.normalizeNode #5295

Merged
merged 7 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/two-books-bow.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 12 additions & 6 deletions docs/api/nodes/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/07-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions packages/slate-react/src/plugin/with-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import ReactDOM from 'react-dom'
import {
BaseEditor,
Editor,
Element,
Node,
Operation,
Path,
Point,
Range,
Transforms,
Element,
} from 'slate'
import {
TextDiff,
Expand All @@ -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.
*
Expand Down Expand Up @@ -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)
Expand All @@ -334,7 +335,7 @@ export const withReact = <T extends BaseEditor>(
onContextChange()
}

onChange()
onChange(options)
})
}

Expand Down
23 changes: 18 additions & 5 deletions packages/slate/src/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Editor,
Element,
Node,
NodeEntry,
Operation,
Path,
PathRef,
Expand All @@ -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.
Expand Down Expand Up @@ -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') {
Expand All @@ -93,7 +94,7 @@ export const createEditor = (): Editor => {

Promise.resolve().then(() => {
FLUSHING.set(editor, false)
editor.onChange()
editor.onChange({ operation: op })
editor.operations = []
})
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
59 changes: 37 additions & 22 deletions packages/slate/src/interfaces/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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>
Expand Down Expand Up @@ -145,6 +154,7 @@ export interface EditorNodesOptions<T extends Node> {

export interface EditorNormalizeOptions {
force?: boolean
operation?: Operation
}

export interface EditorParentOptions {
Expand Down Expand Up @@ -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) || []
}
Expand Down Expand Up @@ -1057,29 +1067,34 @@ 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)

// 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++
}
})
},
Expand Down