@portabletext/editor 1.49.0 → 1.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.49.0",
3
+ "version": "1.49.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -79,15 +79,15 @@
79
79
  "slate-react": "0.114.2",
80
80
  "use-effect-event": "^1.0.2",
81
81
  "xstate": "^5.19.2",
82
- "@portabletext/block-tools": "1.1.23",
83
- "@portabletext/patches": "1.1.3"
82
+ "@portabletext/patches": "1.1.3",
83
+ "@portabletext/block-tools": "1.1.24"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@portabletext/toolkit": "^2.0.17",
87
87
  "@sanity/diff-match-patch": "^3.2.0",
88
88
  "@sanity/pkg-utils": "^7.2.2",
89
- "@sanity/schema": "^3.87.1",
90
- "@sanity/types": "^3.87.1",
89
+ "@sanity/schema": "^3.88.0",
90
+ "@sanity/types": "^3.88.0",
91
91
  "@testing-library/jest-dom": "^6.6.3",
92
92
  "@testing-library/react": "^16.3.0",
93
93
  "@types/debug": "^4.1.12",
@@ -114,8 +114,8 @@
114
114
  "racejar": "1.2.4"
115
115
  },
116
116
  "peerDependencies": {
117
- "@sanity/schema": "^3.87.1",
118
- "@sanity/types": "^3.87.1",
117
+ "@sanity/schema": "^3.88.0",
118
+ "@sanity/types": "^3.88.0",
119
119
  "react": "^16.9 || ^17 || ^18 || ^19",
120
120
  "rxjs": "^7.8.2"
121
121
  },
@@ -10,7 +10,6 @@ import {
10
10
  useRef,
11
11
  useState,
12
12
  type ClipboardEvent,
13
- type CSSProperties,
14
13
  type FocusEventHandler,
15
14
  type KeyboardEvent,
16
15
  type MutableRefObject,
@@ -53,8 +52,9 @@ import type {
53
52
  import type {HotkeyOptions} from '../types/options'
54
53
  import {isSelectionCollapsed} from '../utils'
55
54
  import {getSelectionEndPoint} from '../utils/util.get-selection-end-point'
56
- import {Leaf} from './components/Leaf'
57
55
  import {RenderElement} from './components/render-element'
56
+ import {RenderLeaf} from './components/render-leaf'
57
+ import {RenderText, type RenderTextProps} from './components/render-text'
58
58
  import {EditorActorContext} from './editor-actor-context'
59
59
  import {getEditorSnapshot} from './editor-selector'
60
60
  import {usePortableTextEditor} from './hooks/usePortableTextEditor'
@@ -67,14 +67,6 @@ import {
67
67
 
68
68
  const debug = debugWithName('component:Editable')
69
69
 
70
- const PLACEHOLDER_STYLE: CSSProperties = {
71
- position: 'absolute',
72
- userSelect: 'none',
73
- pointerEvents: 'none',
74
- left: 0,
75
- right: 0,
76
- }
77
-
78
70
  /**
79
71
  * @public
80
72
  */
@@ -245,47 +237,20 @@ export const PortableTextEditable = forwardRef<
245
237
 
246
238
  const renderLeaf = useCallback(
247
239
  (
248
- lProps: RenderLeafProps & {
240
+ leafProps: RenderLeafProps & {
249
241
  leaf: Text & {placeholder?: boolean; rangeDecoration?: RangeDecoration}
250
242
  },
251
- ) => {
252
- if (lProps.leaf._type === 'span') {
253
- let rendered = (
254
- <Leaf
255
- {...lProps}
256
- editorActor={editorActor}
257
- schemaTypes={portableTextEditor.schemaTypes}
258
- renderAnnotation={renderAnnotation}
259
- renderChild={renderChild}
260
- renderDecorator={renderDecorator}
261
- readOnly={readOnly}
262
- />
263
- )
264
- if (
265
- renderPlaceholder &&
266
- lProps.leaf.placeholder &&
267
- lProps.text.text === ''
268
- ) {
269
- return (
270
- <>
271
- <span style={PLACEHOLDER_STYLE} contentEditable={false}>
272
- {renderPlaceholder()}
273
- </span>
274
- {rendered}
275
- </>
276
- )
277
- }
278
- const decoration = lProps.leaf.rangeDecoration
279
- if (decoration) {
280
- rendered = decoration.component({children: rendered})
281
- }
282
- return rendered
283
- }
284
- return lProps.children
285
- },
243
+ ) => (
244
+ <RenderLeaf
245
+ {...leafProps}
246
+ readOnly={readOnly}
247
+ renderAnnotation={renderAnnotation}
248
+ renderChild={renderChild}
249
+ renderDecorator={renderDecorator}
250
+ renderPlaceholder={renderPlaceholder}
251
+ />
252
+ ),
286
253
  [
287
- editorActor,
288
- portableTextEditor,
289
254
  readOnly,
290
255
  renderAnnotation,
291
256
  renderChild,
@@ -294,6 +259,11 @@ export const PortableTextEditable = forwardRef<
294
259
  ],
295
260
  )
296
261
 
262
+ const renderText = useCallback(
263
+ (props: RenderTextProps) => <RenderText {...props} />,
264
+ [],
265
+ )
266
+
297
267
  const restoreSelectionFromProps = useCallback(() => {
298
268
  if (propsSelection) {
299
269
  debug(`Selection from props ${JSON.stringify(propsSelection)}`)
@@ -1225,6 +1195,7 @@ export const PortableTextEditable = forwardRef<
1225
1195
  renderPlaceholder={undefined}
1226
1196
  renderElement={renderElement}
1227
1197
  renderLeaf={renderLeaf}
1198
+ renderText={renderText}
1228
1199
  scrollSelectionIntoView={scrollSelectionIntoViewToSlate}
1229
1200
  />
1230
1201
  )
@@ -59,6 +59,9 @@ describe('initialization', () => {
59
59
  >
60
60
  <div>
61
61
  <span
62
+ data-child-key="k1"
63
+ data-child-name="span"
64
+ data-child-type="span"
62
65
  data-slate-node="text"
63
66
  >
64
67
  <span
@@ -68,9 +71,6 @@ describe('initialization', () => {
68
71
  Jot something down here
69
72
  </span>
70
73
  <span
71
- data-child-key="k1"
72
- data-child-name="span"
73
- data-child-type="span"
74
74
  data-slate-leaf="true"
75
75
  >
76
76
  <span
@@ -125,7 +125,7 @@ describe('RangeDecorations', () => {
125
125
  )
126
126
  await waitFor(() => {
127
127
  expect([rangeDecorationIteration, 'updated-with-different']).toEqual([
128
- 3,
128
+ 2,
129
129
  'updated-with-different',
130
130
  ])
131
131
  })
@@ -152,7 +152,7 @@ describe('RangeDecorations', () => {
152
152
  )
153
153
  await waitFor(() => {
154
154
  expect([rangeDecorationIteration, 'updated-with-different']).toEqual([
155
- 4,
155
+ 3,
156
156
  'updated-with-different',
157
157
  ])
158
158
  })
@@ -53,7 +53,6 @@ export function RenderBlockObject(props: {
53
53
 
54
54
  return (
55
55
  <div
56
- key={props.element._key}
57
56
  {...props.attributes}
58
57
  className="pt-block pt-object-block"
59
58
  data-block-key={props.element._key}
@@ -57,19 +57,16 @@ export function RenderInlineObject(props: {
57
57
  }
58
58
 
59
59
  return (
60
- <span {...props.attributes}>
60
+ <span
61
+ {...props.attributes}
62
+ draggable={!props.readOnly}
63
+ className="pt-inline-object"
64
+ data-child-key={props.inlineObject._key}
65
+ data-child-name={props.inlineObject._type}
66
+ data-child-type="object"
67
+ >
61
68
  {props.children}
62
- <span
63
- draggable={!props.readOnly}
64
- className="pt-inline-object"
65
- data-testid="pt-inline-object"
66
- ref={inlineObjectRef}
67
- key={props.element._key}
68
- style={{display: 'inline-block'}}
69
- data-child-key={props.inlineObject._key}
70
- data-child-name={props.inlineObject._type}
71
- data-child-type="object"
72
- >
69
+ <span ref={inlineObjectRef} style={{display: 'inline-block'}}>
73
70
  {props.renderChild && block && legacySchemaType ? (
74
71
  props.renderChild({
75
72
  annotations: [],
@@ -0,0 +1,64 @@
1
+ import {useSelector} from '@xstate/react'
2
+ import {useContext, type CSSProperties} from 'react'
3
+ import type {Text} from 'slate'
4
+ import type {RenderLeafProps} from 'slate-react'
5
+ import type {
6
+ RangeDecoration,
7
+ RenderAnnotationFunction,
8
+ RenderChildFunction,
9
+ RenderDecoratorFunction,
10
+ RenderPlaceholderFunction,
11
+ } from '../../types/editor'
12
+ import {EditorActorContext} from '../editor-actor-context'
13
+ import {RenderSpan} from './render-span'
14
+
15
+ const PLACEHOLDER_STYLE: CSSProperties = {
16
+ position: 'absolute',
17
+ userSelect: 'none',
18
+ pointerEvents: 'none',
19
+ left: 0,
20
+ right: 0,
21
+ }
22
+
23
+ export function RenderLeaf(
24
+ props: RenderLeafProps & {
25
+ leaf: Text & {placeholder?: boolean; rangeDecoration?: RangeDecoration}
26
+ readOnly: boolean
27
+ renderAnnotation?: RenderAnnotationFunction
28
+ renderChild?: RenderChildFunction
29
+ renderDecorator?: RenderDecoratorFunction
30
+ renderPlaceholder?: RenderPlaceholderFunction
31
+ },
32
+ ) {
33
+ const editorActor = useContext(EditorActorContext)
34
+ const schema = useSelector(editorActor, (s) => s.context.schema)
35
+
36
+ if (props.leaf._type !== schema.span.name) {
37
+ return props.children
38
+ }
39
+
40
+ let renderedSpan = <RenderSpan {...props} />
41
+
42
+ if (
43
+ props.renderPlaceholder &&
44
+ props.leaf.placeholder &&
45
+ props.text.text === ''
46
+ ) {
47
+ return (
48
+ <>
49
+ <span style={PLACEHOLDER_STYLE} contentEditable={false}>
50
+ {props.renderPlaceholder()}
51
+ </span>
52
+ {renderedSpan}
53
+ </>
54
+ )
55
+ }
56
+
57
+ const rangeDecoration = props.leaf.rangeDecoration
58
+
59
+ if (rangeDecoration) {
60
+ renderedSpan = rangeDecoration.component({children: renderedSpan})
61
+ }
62
+
63
+ return renderedSpan
64
+ }
@@ -0,0 +1,260 @@
1
+ import {useSelector} from '@xstate/react'
2
+ import {isEqual, uniq} from 'lodash'
3
+ import {
4
+ startTransition,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type ReactElement,
12
+ } from 'react'
13
+ import {useSelected, useSlateStatic, type RenderLeafProps} from 'slate-react'
14
+ import type {
15
+ RenderAnnotationFunction,
16
+ RenderChildFunction,
17
+ RenderDecoratorFunction,
18
+ } from '../../types/editor'
19
+ import {EditorActorContext} from '../editor-actor-context'
20
+ import {usePortableTextEditor} from '../hooks/usePortableTextEditor'
21
+ import {PortableTextEditor} from '../PortableTextEditor'
22
+
23
+ export interface RenderSpanProps extends RenderLeafProps {
24
+ children: ReactElement<any>
25
+ renderAnnotation?: RenderAnnotationFunction
26
+ renderChild?: RenderChildFunction
27
+ renderDecorator?: RenderDecoratorFunction
28
+ readOnly: boolean
29
+ }
30
+
31
+ export function RenderSpan(props: RenderSpanProps) {
32
+ const slateEditor = useSlateStatic()
33
+ const editorActor = useContext(EditorActorContext)
34
+ const legacySchema = useSelector(editorActor, (s) =>
35
+ s.context.getLegacySchema(),
36
+ )
37
+ const spanRef = useRef<HTMLElement>(null)
38
+ const portableTextEditor = usePortableTextEditor()
39
+ const blockSelected = useSelected()
40
+ const [focused, setFocused] = useState(false)
41
+ const [selected, setSelected] = useState(false)
42
+
43
+ const parent = props.children.props.parent
44
+ const block = parent && slateEditor.isTextBlock(parent) ? parent : undefined
45
+
46
+ const path = useMemo(
47
+ () =>
48
+ block
49
+ ? [{_key: block._key}, 'children', {_key: props.leaf._key}]
50
+ : undefined,
51
+ [block, props.leaf._key],
52
+ )
53
+
54
+ const decoratorSchemaTypes = editorActor
55
+ .getSnapshot()
56
+ .context.schema.decorators.map((decorator) => decorator.name)
57
+
58
+ const decorators = uniq(
59
+ (props.leaf.marks ?? []).filter((mark) =>
60
+ decoratorSchemaTypes.includes(mark),
61
+ ),
62
+ )
63
+
64
+ const annotationMarkDefs = (props.leaf.marks ?? []).flatMap((mark) => {
65
+ if (decoratorSchemaTypes.includes(mark)) {
66
+ return []
67
+ }
68
+
69
+ const markDef = block?.markDefs?.find((markDef) => markDef._key === mark)
70
+
71
+ if (markDef) {
72
+ return [markDef]
73
+ }
74
+
75
+ return []
76
+ })
77
+
78
+ const shouldTrackSelectionAndFocus =
79
+ annotationMarkDefs.length > 0 && blockSelected
80
+
81
+ useEffect(() => {
82
+ if (!shouldTrackSelectionAndFocus) {
83
+ setFocused(false)
84
+ return
85
+ }
86
+
87
+ const sel = PortableTextEditor.getSelection(portableTextEditor)
88
+
89
+ if (
90
+ sel &&
91
+ isEqual(sel.focus.path, path) &&
92
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
93
+ ) {
94
+ startTransition(() => {
95
+ setFocused(true)
96
+ })
97
+ }
98
+ }, [shouldTrackSelectionAndFocus, path, portableTextEditor])
99
+
100
+ // Function to check if this leaf is currently inside the user's text selection
101
+ const setSelectedFromRange = useCallback(() => {
102
+ if (!shouldTrackSelectionAndFocus) {
103
+ return
104
+ }
105
+
106
+ const winSelection = window.getSelection()
107
+
108
+ if (!winSelection) {
109
+ setSelected(false)
110
+ return
111
+ }
112
+
113
+ if (winSelection && winSelection.rangeCount > 0) {
114
+ const range = winSelection.getRangeAt(0)
115
+
116
+ if (spanRef.current && range.intersectsNode(spanRef.current)) {
117
+ setSelected(true)
118
+ } else {
119
+ setSelected(false)
120
+ }
121
+ } else {
122
+ setSelected(false)
123
+ }
124
+ }, [shouldTrackSelectionAndFocus])
125
+
126
+ useEffect(() => {
127
+ if (!shouldTrackSelectionAndFocus) {
128
+ return undefined
129
+ }
130
+
131
+ const onBlur = editorActor.on('blurred', () => {
132
+ setFocused(false)
133
+ setSelected(false)
134
+ })
135
+
136
+ const onFocus = editorActor.on('focused', () => {
137
+ const sel = PortableTextEditor.getSelection(portableTextEditor)
138
+
139
+ if (
140
+ sel &&
141
+ isEqual(sel.focus.path, path) &&
142
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
143
+ ) {
144
+ setFocused(true)
145
+ }
146
+
147
+ setSelectedFromRange()
148
+ })
149
+
150
+ const onSelection = editorActor.on('selection', (event) => {
151
+ if (
152
+ event.selection &&
153
+ isEqual(event.selection.focus.path, path) &&
154
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
155
+ ) {
156
+ setFocused(true)
157
+ } else {
158
+ setFocused(false)
159
+ }
160
+ setSelectedFromRange()
161
+ })
162
+
163
+ return () => {
164
+ onBlur.unsubscribe()
165
+ onFocus.unsubscribe()
166
+ onSelection.unsubscribe()
167
+ }
168
+ }, [
169
+ editorActor,
170
+ path,
171
+ portableTextEditor,
172
+ setSelectedFromRange,
173
+ shouldTrackSelectionAndFocus,
174
+ ])
175
+
176
+ useEffect(() => setSelectedFromRange(), [setSelectedFromRange])
177
+
178
+ let children = props.children
179
+
180
+ /**
181
+ * Support `renderDecorator` render function for each Decorator
182
+ */
183
+ for (const mark of decorators) {
184
+ const legacyDecoratorSchemaType = legacySchema.decorators.find(
185
+ (dec) => dec.value === mark,
186
+ )
187
+
188
+ if (path && legacyDecoratorSchemaType && props.renderDecorator) {
189
+ children = props.renderDecorator({
190
+ children: children,
191
+ editorElementRef: spanRef,
192
+ focused,
193
+ path,
194
+ selected,
195
+ schemaType: legacyDecoratorSchemaType,
196
+ value: mark,
197
+ type: legacyDecoratorSchemaType,
198
+ })
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Support `renderAnnotation` render function for each Annotation
204
+ */
205
+ for (const annotationMarkDef of annotationMarkDefs) {
206
+ const legacyAnnotationSchemaType = legacySchema.annotations.find(
207
+ (t) => t.name === annotationMarkDef._type,
208
+ )
209
+ if (legacyAnnotationSchemaType) {
210
+ if (block && path && props.renderAnnotation) {
211
+ children = (
212
+ <span ref={spanRef}>
213
+ {props.renderAnnotation({
214
+ block,
215
+ children: children,
216
+ editorElementRef: spanRef,
217
+ focused,
218
+ path,
219
+ selected,
220
+ schemaType: legacyAnnotationSchemaType,
221
+ value: annotationMarkDef,
222
+ type: legacyAnnotationSchemaType,
223
+ })}
224
+ </span>
225
+ )
226
+ } else {
227
+ children = <span ref={spanRef}>{children}</span>
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Support `renderChild` render function for the Span itself
234
+ */
235
+ if (block && path && props.renderChild) {
236
+ const child = block.children.find(
237
+ (_child) => _child._key === props.leaf._key,
238
+ ) // Ensure object equality
239
+
240
+ if (child) {
241
+ children = props.renderChild({
242
+ annotations: annotationMarkDefs,
243
+ children: children,
244
+ editorElementRef: spanRef,
245
+ focused,
246
+ path,
247
+ schemaType: legacySchema.span,
248
+ selected,
249
+ value: child,
250
+ type: legacySchema.span,
251
+ })
252
+ }
253
+ }
254
+
255
+ return (
256
+ <span {...props.attributes} ref={spanRef}>
257
+ {children}
258
+ </span>
259
+ )
260
+ }
@@ -103,7 +103,6 @@ export function RenderTextBlock(props: {
103
103
 
104
104
  return (
105
105
  <div
106
- key={props.element._key}
107
106
  {...props.attributes}
108
107
  className={[
109
108
  'pt-block',
@@ -0,0 +1,18 @@
1
+ import type {Editable} from 'slate-react'
2
+
3
+ export type RenderTextProps = Parameters<
4
+ NonNullable<React.ComponentProps<typeof Editable>['renderText']>
5
+ >[0]
6
+
7
+ export function RenderText(props: RenderTextProps) {
8
+ return (
9
+ <span
10
+ {...props.attributes}
11
+ data-child-key={props.text._key}
12
+ data-child-name={props.text._type}
13
+ data-child-type="span"
14
+ >
15
+ {props.children}
16
+ </span>
17
+ )
18
+ }