Skip to content

Commit 9994531

Browse files
committed
fix: use keyed paths in the rendering pipeline
1 parent 73a56db commit 9994531

10 files changed

Lines changed: 224 additions & 78 deletions

File tree

.changeset/render-keyed-paths.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@portabletext/editor': patch
3+
---
4+
5+
fix: use keyed paths in the rendering pipeline

packages/editor/src/editor/render.inline-object.tsx

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type {PortableTextChild, PortableTextObject} from '@portabletext/schema'
22
import {useContext, useRef, type ReactElement} from 'react'
3-
import {getNode} from '../node-traversal/get-node'
43
import {serializePath} from '../paths/serialize-path'
54
import type {Path} from '../slate/interfaces/path'
65
import type {RenderElementProps} from '../slate/react/components/editable'
7-
import {useSlateStatic} from '../slate/react/hooks/use-slate-static'
86
import type {BlockChildRenderProps, RenderChildFunction} from '../types/editor'
97
import type {EditorSchema} from './editor-schema'
108
import {RenderDefaultInlineObject} from './render.default-object'
@@ -20,7 +18,6 @@ export function RenderInlineObject(props: {
2018
schema: EditorSchema
2119
}) {
2220
const inlineObjectRef = useRef<HTMLElement>(null)
23-
const slateEditor = useSlateStatic()
2421

2522
const inlineObjectSchemaType = props.schema.inlineObjects.find(
2623
(schemaType) => schemaType.name === props.element._type,
@@ -32,27 +29,10 @@ export function RenderInlineObject(props: {
3229
)
3330
}
3431

35-
const blockEntry = getNode(slateEditor, props.path.slice(0, 1))
36-
const block = blockEntry?.node
37-
38-
if (!block) {
39-
console.error(
40-
`Unable to find parent block of inline object ${props.element._key}`,
41-
)
42-
}
43-
44-
const path = block
45-
? [{_key: block._key}, 'children', {_key: props.element._key}]
46-
: undefined
47-
4832
const selectionState = useContext(SelectionStateContext)
49-
const serializedPath = path ? serializePath(path) : undefined
50-
const selected = serializedPath
51-
? selectionState.selectedChildPaths.has(serializedPath)
52-
: false
53-
const focused = serializedPath
54-
? selectionState.focusedChildPath === serializedPath
55-
: false
33+
const serializedPath = serializePath(props.path)
34+
const selected = selectionState.selectedChildPaths.has(serializedPath)
35+
const focused = selectionState.focusedChildPath === serializedPath
5636

5737
const inlineObject = props.element as unknown as PortableTextChild
5838

@@ -70,14 +50,14 @@ export function RenderInlineObject(props: {
7050
style={{display: 'inline-block'}}
7151
draggable={!props.readOnly}
7252
>
73-
{props.renderChild && path && inlineObjectSchemaType ? (
53+
{props.renderChild && inlineObjectSchemaType ? (
7454
<RenderChild
7555
renderChild={props.renderChild}
7656
annotations={[]}
7757
editorElementRef={inlineObjectRef}
7858
selected={selected}
7959
focused={focused}
80-
path={path}
60+
path={props.path}
8161
schemaType={inlineObjectSchemaType}
8262
value={inlineObject}
8363
>

packages/editor/src/editor/render.span.tsx

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {InlineObjectSchemaType} from '@portabletext/schema'
22
import {isTextBlock} from '@portabletext/schema'
33
import {useSelector} from '@xstate/react'
4-
import {useContext, useMemo, useRef, type ReactElement} from 'react'
4+
import {useContext, useRef, type ReactElement} from 'react'
55
import {serializePath} from '../paths/serialize-path'
66
import type {RenderLeafProps} from '../slate/react/components/editable'
77
import {useSlateStatic} from '../slate/react/hooks/use-slate-static'
@@ -40,22 +40,10 @@ export function RenderSpan(props: RenderSpanProps) {
4040
? parent
4141
: undefined
4242

43-
const path = useMemo(
44-
() =>
45-
block
46-
? [{_key: block._key}, 'children', {_key: props.leaf._key}]
47-
: undefined,
48-
[block, props.leaf._key],
49-
)
50-
5143
const selectionState = useContext(SelectionStateContext)
52-
const serializedPath = path ? serializePath(path) : undefined
53-
const focused = serializedPath
54-
? selectionState.focusedChildPath === serializedPath
55-
: false
56-
const selected = serializedPath
57-
? selectionState.selectedChildPaths.has(serializedPath)
58-
: false
44+
const serializedPath = serializePath(props.path)
45+
const focused = selectionState.focusedChildPath === serializedPath
46+
const selected = selectionState.selectedChildPaths.has(serializedPath)
5947

6048
const decoratorSchemaTypes = editorActor
6149
.getSnapshot()
@@ -95,13 +83,13 @@ export function RenderSpan(props: RenderSpanProps) {
9583
(dec) => dec.name === mark,
9684
)
9785

98-
if (path && decoratorSchemaType && props.renderDecorator) {
86+
if (decoratorSchemaType && props.renderDecorator) {
9987
children = (
10088
<RenderDecorator
10189
renderDecorator={props.renderDecorator}
10290
editorElementRef={spanRef}
10391
focused={focused}
104-
path={path}
92+
path={props.path}
10593
selected={selected}
10694
schemaType={decoratorSchemaType}
10795
value={mark}
@@ -120,15 +108,15 @@ export function RenderSpan(props: RenderSpanProps) {
120108
(t) => t.name === annotationMarkDef._type,
121109
)
122110
if (annotationSchemaType) {
123-
if (block && path && props.renderAnnotation) {
111+
if (block && props.renderAnnotation) {
124112
children = (
125113
<span ref={spanRef}>
126114
<RenderAnnotation
127115
renderAnnotation={props.renderAnnotation}
128116
block={block}
129117
editorElementRef={spanRef}
130118
focused={focused}
131-
path={path}
119+
path={props.path}
132120
selected={selected}
133121
schemaType={annotationSchemaType}
134122
value={annotationMarkDef}
@@ -146,7 +134,7 @@ export function RenderSpan(props: RenderSpanProps) {
146134
/**
147135
* Support `renderChild` render function for the Span itself
148136
*/
149-
if (block && path && props.renderChild) {
137+
if (block && props.renderChild) {
150138
const child = block.children.find(
151139
(_child) => _child._key === props.leaf._key,
152140
) // Ensure object equality
@@ -158,7 +146,7 @@ export function RenderSpan(props: RenderSpanProps) {
158146
annotations={annotationMarkDefs}
159147
editorElementRef={spanRef}
160148
focused={focused}
161-
path={path}
149+
path={props.path}
162150
schemaType={schemaType}
163151
selected={selected}
164152
value={child}

packages/editor/src/slate/react/components/editable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export interface RenderLeafProps {
126126
*/
127127
leaf: PortableTextSpan
128128
text: PortableTextSpan
129+
path: Path
129130
attributes: {
130131
'data-slate-leaf': true
131132
}
@@ -1944,7 +1945,6 @@ export const Editable = forwardRef(
19441945
)}
19451946
>
19461947
<Children
1947-
parentDataPath=""
19481948
decorations={decorations}
19491949
node={editor}
19501950
path={[]}

packages/editor/src/slate/react/components/element.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
import React, {type JSX} from 'react'
66
import {getText} from '../../../node-traversal/get-text'
77
import {isInline as isInlinePath} from '../../../node-traversal/is-inline'
8+
import {serializePath} from '../../../paths/serialize-path'
89
import {isElementDecorationsEqual} from '../../dom/utils/range-list'
910
import type {Path} from '../../interfaces/path'
1011
import type {DecoratedRange} from '../../interfaces/text'
@@ -30,7 +31,6 @@ const defaultRenderElement = (props: RenderElementProps) => (
3031
*/
3132

3233
const Element = (props: {
33-
dataPath: string
3434
decorations: DecoratedRange[]
3535
element: PortableTextTextBlock | PortableTextObject
3636
path: Path
@@ -40,19 +40,18 @@ const Element = (props: {
4040
renderLeaf?: (props: RenderLeafProps) => JSX.Element
4141
}) => {
4242
const {
43-
dataPath,
4443
decorations: parentDecorations,
4544
element,
4645
renderElement = defaultRenderElement,
4746
renderPlaceholder,
4847
renderLeaf,
4948
renderText,
5049
} = props
50+
const dataPath = serializePath(props.path)
5151
const editor = useSlateStatic()
5252
const isInline = isInlinePath(editor, props.path)
5353
const decorations = useDecorations(element, props.path, parentDecorations)
5454
const children = useChildren({
55-
parentDataPath: dataPath,
5655
decorations,
5756
node: element,
5857
path: props.path,
@@ -101,7 +100,6 @@ const Element = (props: {
101100

102101
const MemoizedElement = React.memo(Element, (prev, next) => {
103102
return (
104-
prev.dataPath === next.dataPath &&
105103
prev.element === next.element &&
106104
pathEquals(prev.path, next.path) &&
107105
prev.renderElement === next.renderElement &&

packages/editor/src/slate/react/components/leaf.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ const Leaf = (props: {
174174
children,
175175
leaf,
176176
text,
177+
path,
177178
leafPosition,
178179
})
179180
}

packages/editor/src/slate/react/components/object-node.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {PortableTextObject} from '@portabletext/schema'
22
import React, {type JSX} from 'react'
3+
import {serializePath} from '../../../paths/serialize-path'
34
import {IS_ANDROID} from '../../dom/utils/environment'
45
import {isElementDecorationsEqual} from '../../dom/utils/range-list'
56
import type {Path} from '../../interfaces/path'
@@ -25,20 +26,19 @@ const defaultRenderElement = (props: RenderElementProps) => {
2526
* contain a hidden spacer with a zero-width text node for selection anchoring.
2627
*/
2728
const ObjectNodeComponent = (props: {
28-
dataPath: string
2929
decorations: DecoratedRange[]
3030
objectNode: PortableTextObject
3131
isInline: boolean
3232
path: Path
3333
renderElement?: (props: RenderElementProps) => JSX.Element
3434
}) => {
3535
const {
36-
dataPath,
3736
objectNode,
3837
isInline,
3938
path,
4039
renderElement = defaultRenderElement,
4140
} = props
41+
const dataPath = serializePath(path)
4242
const readOnly = useReadOnly()
4343

4444
const attributes: RenderElementProps['attributes'] = {
@@ -85,7 +85,6 @@ const ObjectNodeComponent = (props: {
8585

8686
const MemoizedObjectNode = React.memo(ObjectNodeComponent, (prev, next) => {
8787
return (
88-
prev.dataPath === next.dataPath &&
8988
prev.objectNode === next.objectNode &&
9089
pathEquals(prev.path, next.path) &&
9190
prev.renderElement === next.renderElement &&

packages/editor/src/slate/react/components/text.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
PortableTextTextBlock,
44
} from '@portabletext/schema'
55
import React, {type JSX} from 'react'
6+
import {serializePath} from '../../../paths/serialize-path'
67
import {isTextDecorationsEqual} from '../../dom/utils/range-list'
78
import type {Path} from '../../interfaces/path'
89
import type {DecoratedRange} from '../../interfaces/text'
@@ -23,7 +24,6 @@ const defaultRenderText = (props: RenderTextProps) => <DefaultText {...props} />
2324
*/
2425

2526
const Text = (props: {
26-
dataPath: string
2727
decorations: DecoratedRange[]
2828
isLast: boolean
2929
parent: PortableTextTextBlock
@@ -34,7 +34,6 @@ const Text = (props: {
3434
text: PortableTextSpan
3535
}) => {
3636
const {
37-
dataPath,
3837
decorations: parentDecorations,
3938
isLast,
4039
parent,
@@ -67,6 +66,8 @@ const Text = (props: {
6766
)
6867
}
6968

69+
const dataPath = serializePath(path)
70+
7071
const attributes: {
7172
'data-slate-node': 'text'
7273
'data-pt-path': string

packages/editor/src/slate/react/hooks/use-children.tsx

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import {useSlateStatic} from './use-slate-static'
3838
*/
3939

4040
const useChildren = (props: {
41-
parentDataPath: string
4241
decorations: DecoratedRange[]
4342
node: Editor | Node
4443
path: Path
@@ -48,7 +47,6 @@ const useChildren = (props: {
4847
renderLeaf?: (props: RenderLeafProps) => JSX.Element
4948
}): React.ReactNode => {
5049
const {
51-
parentDataPath,
5250
decorations,
5351
node,
5452
path: parentPath,
@@ -97,19 +95,18 @@ const useChildren = (props: {
9795

9896
const renderElementComponent = useCallback(
9997
(node: PortableTextTextBlock | PortableTextObject, i: number) => {
100-
const nodeDataPath =
101-
parentDataPath === ''
102-
? `[_key=="${node._key}"]`
103-
: `${parentDataPath}.${childFieldName}[_key=="${node._key}"]`
98+
const nodePath: Path =
99+
parentPath.length === 0
100+
? [{_key: node._key}]
101+
: [...parentPath, childFieldName, {_key: node._key}]
104102

105103
return (
106104
<ElementContext key={`provider-${node._key}`} value={node}>
107105
<ElementComponent
108-
dataPath={nodeDataPath}
109106
decorations={decorationsByChild[i] ?? []}
110107
element={node}
111108
key={node._key}
112-
path={parentPath.concat(i)}
109+
path={nodePath}
113110
renderElement={renderElement}
114111
renderPlaceholder={renderPlaceholder}
115112
renderLeaf={renderLeaf}
@@ -120,7 +117,6 @@ const useChildren = (props: {
120117
},
121118
[
122119
childFieldName,
123-
parentDataPath,
124120
decorationsByChild,
125121
parentPath,
126122
renderElement,
@@ -141,19 +137,18 @@ const useChildren = (props: {
141137
)
142138
}
143139

144-
const nodeDataPath =
145-
parentDataPath !== ''
146-
? `${parentDataPath}.children[_key=="${node._key}"]`
147-
: ''
140+
const nodePath: Path =
141+
parentPath.length === 0
142+
? [{_key: node._key}]
143+
: [...parentPath, 'children', {_key: node._key}]
148144

149145
return (
150146
<TextComponent
151-
dataPath={nodeDataPath}
152147
decorations={decorationsByChild[index] ?? []}
153148
key={node._key}
154149
isLast={index === children.length - 1}
155150
parent={textBlockParent}
156-
path={parentPath.concat(index)}
151+
path={nodePath}
157152
renderPlaceholder={renderPlaceholder}
158153
renderLeaf={renderLeaf}
159154
renderText={renderText}
@@ -166,19 +161,18 @@ const useChildren = (props: {
166161
node: PortableTextObject,
167162
index: number,
168163
) => {
169-
const nodeDataPath =
170-
parentDataPath === ''
171-
? `[_key=="${node._key}"]`
172-
: `${parentDataPath}.${childFieldName}[_key=="${node._key}"]`
164+
const nodePath: Path =
165+
parentPath.length === 0
166+
? [{_key: node._key}]
167+
: [...parentPath, childFieldName, {_key: node._key}]
173168

174169
return (
175170
<ObjectNodeComponent
176-
dataPath={nodeDataPath}
177171
decorations={decorationsByChild[index] ?? []}
178172
isInline={textBlockParent !== undefined}
179173
key={node._key}
180174
objectNode={node}
181-
path={parentPath.concat(index)}
175+
path={nodePath}
182176
renderElement={renderElement}
183177
/>
184178
)

0 commit comments

Comments
 (0)