Skip to content

Commit 5b2d812

Browse files
committed
feat: implement findPath without relying on DOM
1 parent a61baa9 commit 5b2d812

File tree

6 files changed

+103
-0
lines changed

6 files changed

+103
-0
lines changed

packages/slate-dom/src/plugin/dom-editor.ts

+8
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ export const DOMEditor: DOMEditorInterface = {
387387
},
388388

389389
findPath: (editor, node) => {
390+
const newVersionPath = editor.findPath(node)
390391
const path: Path = []
391392
let child = node
392393

@@ -395,6 +396,13 @@ export const DOMEditor: DOMEditorInterface = {
395396

396397
if (parent == null) {
397398
if (Editor.isEditor(child)) {
399+
if (!Path.equals(newVersionPath, path)) {
400+
throw new Error(
401+
`path mismatch, expected ${JSON.stringify(
402+
path
403+
)}, got ${JSON.stringify(newVersionPath)}`
404+
)
405+
}
398406
return path
399407
} else {
400408
break

packages/slate/src/core/normalize-node.ts

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Element } from '../interfaces/element'
44
import { Transforms } from '../interfaces/transforms'
55
import { Descendant, Node } from '../interfaces/node'
66
import { Editor } from '../interfaces/editor'
7+
import { getNodeToParent } from '../editor/find-path'
78

89
export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
910
editor,
@@ -26,6 +27,14 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
2627
return
2728
}
2829

30+
// Cache dirty nodes child-parent relationships for editor.findPath()
31+
if (!Text.isText(node)) {
32+
const nodeToParent = getNodeToParent(editor)
33+
node.children.forEach(child => {
34+
nodeToParent.set(child, node)
35+
})
36+
}
37+
2938
// Determine whether the node should have block or inline children.
3039
const shouldHaveInlines = Editor.isEditor(node)
3140
? false

packages/slate/src/create-editor.ts

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
edges,
2424
elementReadOnly,
2525
end,
26+
findPath,
2627
first,
2728
fragment,
2829
getVoid,
@@ -94,6 +95,9 @@ export const createEditor = (): Editor => {
9495
operations: [],
9596
selection: null,
9697
marks: null,
98+
_caches: {
99+
nodeToParent: undefined,
100+
},
97101
isElementReadOnly: () => false,
98102
isInline: () => false,
99103
isSelectable: () => true,
@@ -130,6 +134,7 @@ export const createEditor = (): Editor => {
130134
edges: (...args) => edges(editor, ...args),
131135
elementReadOnly: (...args) => elementReadOnly(editor, ...args),
132136
end: (...args) => end(editor, ...args),
137+
findPath: (...args) => findPath(editor, ...args),
133138
first: (...args) => first(editor, ...args),
134139
fragment: (...args) => fragment(editor, ...args),
135140
getMarks: (...args) => marks(editor, ...args),
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Text } from '../interfaces/text'
2+
import { BaseEditor, EditorInterface } from '../interfaces/editor'
3+
import { Ancestor } from '../interfaces/node'
4+
import { Scrubber } from '../interfaces/scrubber'
5+
6+
export const findPath: EditorInterface['findPath'] = (editor, node) => {
7+
if (node === editor) {
8+
return []
9+
}
10+
11+
const nodeToParent = getNodeToParent(editor)
12+
13+
const parent = nodeToParent.get(node)
14+
if (!parent) {
15+
throw new Error(
16+
`Unable to find the path for Slate node (parent not found): ${Scrubber.stringify(
17+
node
18+
)}`
19+
)
20+
}
21+
22+
const parentPath = findPath(editor, parent)
23+
24+
const index = parent.children.indexOf(node)
25+
if (index < 0) {
26+
throw new Error(
27+
`Unable to find the path for Slate node (node is not child of its parent): ${Scrubber.stringify(
28+
node
29+
)}`
30+
)
31+
}
32+
33+
return [...parentPath, index]
34+
}
35+
36+
export function getNodeToParent(editor: BaseEditor) {
37+
let nodeToParent = editor._caches.nodeToParent
38+
if (nodeToParent) {
39+
return nodeToParent
40+
}
41+
42+
nodeToParent = new WeakMap()
43+
editor._caches.nodeToParent = nodeToParent
44+
const parents: Ancestor[] = [editor]
45+
46+
let parent = parents.pop()
47+
while (parent) {
48+
for (const child of parent.children) {
49+
nodeToParent.set(child, parent)
50+
if (!Text.isText(child)) {
51+
parents.push(child)
52+
}
53+
}
54+
parent = parents.pop()
55+
}
56+
57+
return nodeToParent
58+
}

packages/slate/src/editor/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './delete-fragment'
88
export * from './edges'
99
export * from './element-read-only'
1010
export * from './end'
11+
export * from './find-path'
1112
export * from './first'
1213
export * from './fragment'
1314
export * from './get-void'

packages/slate/src/interfaces/editor.ts

+22
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export interface BaseEditor {
4646
operations: Operation[]
4747
marks: EditorMarks | null
4848

49+
/**
50+
* @internal Caches for internal use.
51+
*/
52+
_caches: EditorCaches
53+
4954
// Overrideable core methods.
5055

5156
apply: (operation: Operation) => void
@@ -122,6 +127,7 @@ export interface BaseEditor {
122127
edges: OmitFirstArg<typeof Editor.edges>
123128
elementReadOnly: OmitFirstArg<typeof Editor.elementReadOnly>
124129
end: OmitFirstArg<typeof Editor.end>
130+
findPath: OmitFirstArg<typeof Editor.findPath>
125131
first: OmitFirstArg<typeof Editor.first>
126132
fragment: OmitFirstArg<typeof Editor.fragment>
127133
getMarks: OmitFirstArg<typeof Editor.marks>
@@ -180,6 +186,13 @@ export type Selection = ExtendedType<'Selection', BaseSelection>
180186

181187
export type EditorMarks = Omit<Text, 'text'>
182188

189+
export interface EditorCaches {
190+
/**
191+
* Editor children snapshot after the last flush.
192+
*/
193+
nodeToParent: WeakMap<Descendant, Ancestor> | undefined
194+
}
195+
183196
export interface EditorAboveOptions<T extends Ancestor> {
184197
at?: Location
185198
match?: NodeMatch<T>
@@ -389,6 +402,11 @@ export interface EditorInterface {
389402
*/
390403
first: (editor: Editor, at: Location) => NodeEntry
391404

405+
/**
406+
* Find the path of Slate node.
407+
*/
408+
findPath: (editor: Editor, node: Node) => Path
409+
392410
/**
393411
* Get the fragment at a location.
394412
*/
@@ -767,6 +785,10 @@ export const Editor: EditorInterface = {
767785
return editor.end(at)
768786
},
769787

788+
findPath(editor, node) {
789+
return editor.findPath(node)
790+
},
791+
770792
first(editor, at) {
771793
return editor.first(at)
772794
},

0 commit comments

Comments
 (0)