@portabletext/editor 0.0.0

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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. package/src/utils/withoutPatching.ts +14 -0
@@ -0,0 +1,308 @@
1
+ import {
2
+ type ArrayDefinition,
3
+ type ArraySchemaType,
4
+ type BlockSchemaType,
5
+ type ObjectSchemaType,
6
+ type Path,
7
+ type PortableTextBlock,
8
+ type PortableTextChild,
9
+ type PortableTextObject,
10
+ type SpanSchemaType,
11
+ } from '@sanity/types'
12
+ import {Component, type MutableRefObject, type PropsWithChildren} from 'react'
13
+ import {Subject} from 'rxjs'
14
+
15
+ import {
16
+ type EditableAPI,
17
+ type EditableAPIDeleteOptions,
18
+ type EditorChange,
19
+ type EditorChanges,
20
+ type EditorSelection,
21
+ type PatchObservable,
22
+ type PortableTextMemberSchemaTypes,
23
+ } from '../types/editor'
24
+ import {debugWithName} from '../utils/debug'
25
+ import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
26
+ import {compileType} from '../utils/schema'
27
+ import {SlateContainer} from './components/SlateContainer'
28
+ import {Synchronizer} from './components/Synchronizer'
29
+ import {defaultKeyGenerator} from './hooks/usePortableTextEditorKeyGenerator'
30
+
31
+ const debug = debugWithName('component:PortableTextEditor')
32
+
33
+ /**
34
+ * Props for the PortableTextEditor component
35
+ *
36
+ * @public
37
+ */
38
+ /**
39
+ * Props for the PortableTextEditor component
40
+ *
41
+ * @public
42
+ */
43
+ export type PortableTextEditorProps = PropsWithChildren<{
44
+ /**
45
+ * Function that gets called when the editor changes the value
46
+ */
47
+ onChange: (change: EditorChange) => void
48
+
49
+ /**
50
+ * Schema type for the portable text field
51
+ */
52
+ schemaType: ArraySchemaType<PortableTextBlock> | ArrayDefinition
53
+
54
+ /**
55
+ * Maximum number of blocks to allow within the editor
56
+ */
57
+ maxBlocks?: number | string
58
+
59
+ /**
60
+ * Whether or not the editor should be in read-only mode
61
+ */
62
+ readOnly?: boolean
63
+
64
+ /**
65
+ * The current value of the portable text field
66
+ */
67
+ value?: PortableTextBlock[]
68
+
69
+ /**
70
+ * Function used to generate keys for array items (`_key`)
71
+ */
72
+ keyGenerator?: () => string
73
+
74
+ /**
75
+ * Observable of local and remote patches for the edited value.
76
+ */
77
+ patches$?: PatchObservable
78
+
79
+ /**
80
+ * Backward compatibility (renamed to patches$).
81
+ */
82
+ incomingPatches$?: PatchObservable
83
+
84
+ /**
85
+ * A ref to the editor instance
86
+ */
87
+ editorRef?: MutableRefObject<PortableTextEditor | null>
88
+ }>
89
+
90
+ /**
91
+ * The main Portable Text Editor component.
92
+ * @public
93
+ */
94
+ export class PortableTextEditor extends Component<PortableTextEditorProps> {
95
+ /**
96
+ * An observable of all the editor changes.
97
+ */
98
+ public change$: EditorChanges = new Subject()
99
+ /**
100
+ * A lookup table for all the relevant schema types for this portable text type.
101
+ */
102
+ public schemaTypes: PortableTextMemberSchemaTypes
103
+ /**
104
+ * The editor API (currently implemented with Slate).
105
+ */
106
+ private editable?: EditableAPI
107
+
108
+ constructor(props: PortableTextEditorProps) {
109
+ super(props)
110
+
111
+ if (!props.schemaType) {
112
+ throw new Error('PortableTextEditor: missing "type" property')
113
+ }
114
+
115
+ if (props.incomingPatches$) {
116
+ console.warn(`The prop 'incomingPatches$' is deprecated and renamed to 'patches$'`)
117
+ }
118
+
119
+ this.change$.next({type: 'loading', isLoading: true})
120
+
121
+ this.schemaTypes = getPortableTextMemberSchemaTypes(
122
+ props.schemaType.hasOwnProperty('jsonType')
123
+ ? props.schemaType
124
+ : compileType(props.schemaType),
125
+ )
126
+ }
127
+
128
+ componentDidUpdate(prevProps: PortableTextEditorProps) {
129
+ // Set up the schema type lookup table again if the source schema type changes
130
+ if (this.props.schemaType !== prevProps.schemaType) {
131
+ this.schemaTypes = getPortableTextMemberSchemaTypes(
132
+ this.props.schemaType.hasOwnProperty('jsonType')
133
+ ? this.props.schemaType
134
+ : compileType(this.props.schemaType),
135
+ )
136
+ }
137
+ if (this.props.editorRef !== prevProps.editorRef && this.props.editorRef) {
138
+ this.props.editorRef.current = this
139
+ }
140
+ }
141
+
142
+ public setEditable = (editable: EditableAPI) => {
143
+ this.editable = {...this.editable, ...editable}
144
+ }
145
+
146
+ render() {
147
+ const {onChange, value, children, patches$, incomingPatches$} = this.props
148
+ const {change$} = this
149
+ const _patches$ = incomingPatches$ || patches$ // Backward compatibility
150
+
151
+ const maxBlocks =
152
+ typeof this.props.maxBlocks === 'undefined'
153
+ ? undefined
154
+ : parseInt(this.props.maxBlocks.toString(), 10) || undefined
155
+
156
+ const readOnly = Boolean(this.props.readOnly)
157
+ const keyGenerator = this.props.keyGenerator || defaultKeyGenerator
158
+ return (
159
+ <SlateContainer
160
+ keyGenerator={keyGenerator}
161
+ maxBlocks={maxBlocks}
162
+ patches$={_patches$}
163
+ portableTextEditor={this}
164
+ readOnly={readOnly}
165
+ >
166
+ <Synchronizer
167
+ change$={change$}
168
+ keyGenerator={keyGenerator}
169
+ onChange={onChange}
170
+ portableTextEditor={this}
171
+ readOnly={readOnly}
172
+ value={value}
173
+ >
174
+ {children}
175
+ </Synchronizer>
176
+ </SlateContainer>
177
+ )
178
+ }
179
+
180
+ // Static API methods
181
+ static activeAnnotations = (editor: PortableTextEditor): PortableTextObject[] => {
182
+ return editor && editor.editable ? editor.editable.activeAnnotations() : []
183
+ }
184
+ static isAnnotationActive = (
185
+ editor: PortableTextEditor,
186
+ annotationType: PortableTextObject['_type'],
187
+ ): boolean => {
188
+ return editor && editor.editable ? editor.editable.isAnnotationActive(annotationType) : false
189
+ }
190
+ static addAnnotation = (
191
+ editor: PortableTextEditor,
192
+ type: ObjectSchemaType,
193
+ value?: {[prop: string]: unknown},
194
+ ): {spanPath: Path; markDefPath: Path} | undefined => editor.editable?.addAnnotation(type, value)
195
+ static blur = (editor: PortableTextEditor): void => {
196
+ debug('Host blurred')
197
+ editor.editable?.blur()
198
+ }
199
+ static delete = (
200
+ editor: PortableTextEditor,
201
+ selection: EditorSelection,
202
+ options?: EditableAPIDeleteOptions,
203
+ ) => editor.editable?.delete(selection, options)
204
+ static findDOMNode = (
205
+ editor: PortableTextEditor,
206
+ element: PortableTextBlock | PortableTextChild,
207
+ ) => {
208
+ // eslint-disable-next-line react/no-find-dom-node
209
+ return editor.editable?.findDOMNode(element)
210
+ }
211
+ static findByPath = (editor: PortableTextEditor, path: Path) => {
212
+ return editor.editable?.findByPath(path) || []
213
+ }
214
+ static focus = (editor: PortableTextEditor): void => {
215
+ debug('Host requesting focus')
216
+ editor.editable?.focus()
217
+ }
218
+ static focusBlock = (editor: PortableTextEditor) => {
219
+ return editor.editable?.focusBlock()
220
+ }
221
+ static focusChild = (editor: PortableTextEditor): PortableTextChild | undefined => {
222
+ return editor.editable?.focusChild()
223
+ }
224
+ static getSelection = (editor: PortableTextEditor) => {
225
+ return editor.editable ? editor.editable.getSelection() : null
226
+ }
227
+ static getValue = (editor: PortableTextEditor) => {
228
+ return editor.editable?.getValue()
229
+ }
230
+ static hasBlockStyle = (editor: PortableTextEditor, blockStyle: string) => {
231
+ return editor.editable?.hasBlockStyle(blockStyle)
232
+ }
233
+ static hasListStyle = (editor: PortableTextEditor, listStyle: string) => {
234
+ return editor.editable?.hasListStyle(listStyle)
235
+ }
236
+ static isCollapsedSelection = (editor: PortableTextEditor) =>
237
+ editor.editable?.isCollapsedSelection()
238
+ static isExpandedSelection = (editor: PortableTextEditor) =>
239
+ editor.editable?.isExpandedSelection()
240
+ static isMarkActive = (editor: PortableTextEditor, mark: string) =>
241
+ editor.editable?.isMarkActive(mark)
242
+ static insertChild = (
243
+ editor: PortableTextEditor,
244
+ type: SpanSchemaType | ObjectSchemaType,
245
+ value?: {[prop: string]: unknown},
246
+ ): Path | undefined => {
247
+ debug(`Host inserting child`)
248
+ return editor.editable?.insertChild(type, value)
249
+ }
250
+ static insertBlock = (
251
+ editor: PortableTextEditor,
252
+ type: BlockSchemaType | ObjectSchemaType,
253
+ value?: {[prop: string]: unknown},
254
+ ): Path | undefined => {
255
+ return editor.editable?.insertBlock(type, value)
256
+ }
257
+ static insertBreak = (editor: PortableTextEditor): void => {
258
+ return editor.editable?.insertBreak()
259
+ }
260
+ static isVoid = (editor: PortableTextEditor, element: PortableTextBlock | PortableTextChild) => {
261
+ return editor.editable?.isVoid(element)
262
+ }
263
+ static isObjectPath = (editor: PortableTextEditor, path: Path): boolean => {
264
+ if (!path || !Array.isArray(path)) return false
265
+ const isChildObjectEditPath = path.length > 3 && path[1] === 'children'
266
+ const isBlockObjectEditPath = path.length > 1 && path[1] !== 'children'
267
+ return isBlockObjectEditPath || isChildObjectEditPath
268
+ }
269
+ static marks = (editor: PortableTextEditor) => {
270
+ return editor.editable?.marks()
271
+ }
272
+ static select = (editor: PortableTextEditor, selection: EditorSelection | null) => {
273
+ debug(`Host setting selection`, selection)
274
+ editor.editable?.select(selection)
275
+ }
276
+ static removeAnnotation = (editor: PortableTextEditor, type: ObjectSchemaType) =>
277
+ editor.editable?.removeAnnotation(type)
278
+ static toggleBlockStyle = (editor: PortableTextEditor, blockStyle: string) => {
279
+ debug(`Host is toggling block style`)
280
+ return editor.editable?.toggleBlockStyle(blockStyle)
281
+ }
282
+ static toggleList = (editor: PortableTextEditor, listStyle: string): void => {
283
+ return editor.editable?.toggleList(listStyle)
284
+ }
285
+ static toggleMark = (editor: PortableTextEditor, mark: string): void => {
286
+ debug(`Host toggling mark`, mark)
287
+ editor.editable?.toggleMark(mark)
288
+ }
289
+ static getFragment = (editor: PortableTextEditor): PortableTextBlock[] | undefined => {
290
+ debug(`Host getting fragment`)
291
+ return editor.editable?.getFragment()
292
+ }
293
+ static undo = (editor: PortableTextEditor): void => {
294
+ debug('Host undoing')
295
+ editor.editable?.undo()
296
+ }
297
+ static redo = (editor: PortableTextEditor): void => {
298
+ debug('Host redoing')
299
+ editor.editable?.redo()
300
+ }
301
+ static isSelectionsOverlapping = (
302
+ editor: PortableTextEditor,
303
+ selectionA: EditorSelection,
304
+ selectionB: EditorSelection,
305
+ ) => {
306
+ return editor.editable?.isSelectionsOverlapping(selectionA, selectionB)
307
+ }
308
+ }
@@ -0,0 +1,386 @@
1
+ import {describe, expect, it, jest} from '@jest/globals'
2
+ /* eslint-disable no-irregular-whitespace */
3
+ import {type PortableTextBlock} from '@sanity/types'
4
+ import {render, waitFor} from '@testing-library/react'
5
+ import {createRef, type RefObject} from 'react'
6
+
7
+ import {type EditorSelection} from '../..'
8
+ import {PortableTextEditor} from '../PortableTextEditor'
9
+ import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester'
10
+
11
+ const helloBlock: PortableTextBlock = {
12
+ _key: '123',
13
+ _type: 'myTestBlockType',
14
+ markDefs: [],
15
+ children: [{_key: '567', _type: 'span', text: 'Hello', marks: []}],
16
+ }
17
+
18
+ const renderPlaceholder = () => 'Jot something down here'
19
+
20
+ describe('initialization', () => {
21
+ it('receives initial onChange events and has custom placeholder', async () => {
22
+ const editorRef: RefObject<PortableTextEditor> = createRef()
23
+ const onChange = jest.fn()
24
+ const {container} = render(
25
+ <PortableTextEditorTester
26
+ onChange={onChange}
27
+ renderPlaceholder={renderPlaceholder}
28
+ ref={editorRef}
29
+ schemaType={schemaType}
30
+ value={undefined}
31
+ />,
32
+ )
33
+
34
+ await waitFor(() => {
35
+ expect(editorRef.current).not.toBe(null)
36
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
37
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: undefined})
38
+ expect(container).toMatchInlineSnapshot(`
39
+ <div>
40
+ <div
41
+ aria-describedby="desc_foo"
42
+ aria-multiline="true"
43
+ autocapitalize="false"
44
+ autocorrect="false"
45
+ class="pt-editable"
46
+ contenteditable="true"
47
+ data-slate-editor="true"
48
+ data-slate-node="value"
49
+ role="textbox"
50
+ spellcheck="false"
51
+ style="position: relative; white-space: pre-wrap; word-wrap: break-word;"
52
+ zindex="-1"
53
+ >
54
+ <div
55
+ class="pt-block pt-text-block pt-text-block-style-normal"
56
+ data-slate-node="element"
57
+ >
58
+ <div
59
+ draggable="false"
60
+ >
61
+ <div>
62
+ <span
63
+ data-slate-node="text"
64
+ >
65
+ <span
66
+ contenteditable="false"
67
+ style="position: absolute; user-select: none; pointer-events: none; left: 0px; right: 0px;"
68
+ >
69
+ Jot something down here
70
+ </span>
71
+ <span
72
+ data-slate-leaf="true"
73
+ >
74
+ <span
75
+ data-slate-length="0"
76
+ data-slate-zero-width="n"
77
+ >
78
+ 
79
+ <br />
80
+ </span>
81
+ </span>
82
+ </span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ `)
89
+ })
90
+ })
91
+ it('takes value from props and confirms it by emitting value change event', async () => {
92
+ const initialValue = [helloBlock]
93
+ const onChange = jest.fn()
94
+ const editorRef = createRef<PortableTextEditor>()
95
+ render(
96
+ <PortableTextEditorTester
97
+ ref={editorRef}
98
+ onChange={onChange}
99
+ schemaType={schemaType}
100
+ value={initialValue}
101
+ />,
102
+ )
103
+ const normalizedEditorValue = [{...initialValue[0], style: 'normal'}]
104
+ await waitFor(() => {
105
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
106
+ })
107
+ if (editorRef.current) {
108
+ expect(PortableTextEditor.getValue(editorRef.current)).toStrictEqual(normalizedEditorValue)
109
+ }
110
+ })
111
+
112
+ it('takes initial selection from props', async () => {
113
+ const editorRef: RefObject<PortableTextEditor> = createRef()
114
+ const initialValue = [helloBlock]
115
+ const initialSelection: EditorSelection = {
116
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
117
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
118
+ backward: false,
119
+ }
120
+ const onChange = jest.fn()
121
+ render(
122
+ <PortableTextEditorTester
123
+ onChange={onChange}
124
+ ref={editorRef}
125
+ selection={initialSelection}
126
+ schemaType={schemaType}
127
+ value={initialValue}
128
+ />,
129
+ )
130
+ await waitFor(() => {
131
+ if (editorRef.current) {
132
+ PortableTextEditor.focus(editorRef.current)
133
+ expect(PortableTextEditor.getSelection(editorRef.current)).toStrictEqual(initialSelection)
134
+ }
135
+ })
136
+ })
137
+
138
+ it('updates editor selection from new prop and keeps object equality in editor.getSelection()', async () => {
139
+ const editorRef: RefObject<PortableTextEditor> = createRef()
140
+ const initialValue = [helloBlock]
141
+ const initialSelection: EditorSelection = {
142
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
143
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
144
+ backward: false,
145
+ }
146
+ const newSelection: EditorSelection = {
147
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
148
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 3},
149
+ backward: false,
150
+ }
151
+ const onChange = jest.fn()
152
+ const {rerender} = render(
153
+ <PortableTextEditorTester
154
+ onChange={onChange}
155
+ ref={editorRef}
156
+ selection={initialSelection}
157
+ schemaType={schemaType}
158
+ value={initialValue}
159
+ />,
160
+ )
161
+ await waitFor(() => {
162
+ if (editorRef.current) {
163
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
164
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
165
+ const sel = PortableTextEditor.getSelection(editorRef.current)
166
+ PortableTextEditor.focus(editorRef.current)
167
+
168
+ // Test for object equality here!
169
+ const anotherSel = PortableTextEditor.getSelection(editorRef.current)
170
+ expect(PortableTextEditor.getSelection(editorRef.current)).toStrictEqual(initialSelection)
171
+ expect(sel).toBe(anotherSel)
172
+ }
173
+ })
174
+ rerender(
175
+ <PortableTextEditorTester
176
+ onChange={onChange}
177
+ ref={editorRef}
178
+ selection={newSelection}
179
+ schemaType={schemaType}
180
+ value={initialValue}
181
+ />,
182
+ )
183
+ waitFor(() => {
184
+ if (editorRef.current) {
185
+ expect(PortableTextEditor.getSelection(editorRef.current)).toEqual(newSelection)
186
+ }
187
+ })
188
+ })
189
+
190
+ it('handles empty array value', async () => {
191
+ const editorRef: RefObject<PortableTextEditor> = createRef()
192
+ const initialValue: PortableTextBlock[] = []
193
+ const initialSelection: EditorSelection = {
194
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
195
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
196
+ }
197
+ const onChange = jest.fn()
198
+ render(
199
+ <PortableTextEditorTester
200
+ onChange={onChange}
201
+ ref={editorRef}
202
+ selection={initialSelection}
203
+ schemaType={schemaType}
204
+ value={initialValue}
205
+ />,
206
+ )
207
+ await waitFor(() => {
208
+ if (editorRef.current) {
209
+ expect(onChange).not.toHaveBeenCalledWith({
210
+ type: 'invalidValue',
211
+ value: initialValue,
212
+ resolution: {
213
+ action: 'Unset the value',
214
+ description: 'Editor value must be an array of Portable Text blocks, or undefined.',
215
+ item: initialValue,
216
+ patches: [
217
+ {
218
+ path: [],
219
+ type: 'unset',
220
+ },
221
+ ],
222
+ },
223
+ })
224
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
225
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
226
+ }
227
+ })
228
+ })
229
+ it('validates a non-initial value', async () => {
230
+ const editorRef: RefObject<PortableTextEditor> = createRef()
231
+ let value: PortableTextBlock[] = [helloBlock]
232
+ const initialSelection: EditorSelection = {
233
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
234
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
235
+ }
236
+ const onChange = jest.fn()
237
+ let _rerender: any
238
+ await waitFor(() => {
239
+ render(
240
+ <PortableTextEditorTester
241
+ onChange={onChange}
242
+ ref={editorRef}
243
+ selection={initialSelection}
244
+ schemaType={schemaType}
245
+ value={value}
246
+ />,
247
+ )
248
+ _rerender = render
249
+ })
250
+ await waitFor(() => {
251
+ expect(onChange).not.toHaveBeenCalledWith({
252
+ type: 'invalidValue',
253
+ value,
254
+ resolution: {
255
+ action: 'Unset the value',
256
+ description: 'Editor value must be an array of Portable Text blocks, or undefined.',
257
+ item: value,
258
+ patches: [
259
+ {
260
+ path: [],
261
+ type: 'unset',
262
+ },
263
+ ],
264
+ },
265
+ })
266
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value})
267
+ })
268
+ value = [{_type: 'banana', _key: '123'}]
269
+ const newOnChange = jest.fn()
270
+ _rerender(
271
+ <PortableTextEditorTester
272
+ onChange={newOnChange}
273
+ ref={editorRef}
274
+ selection={initialSelection}
275
+ schemaType={schemaType}
276
+ value={value}
277
+ />,
278
+ )
279
+ await waitFor(() => {
280
+ expect(newOnChange).toHaveBeenCalledWith({
281
+ type: 'invalidValue',
282
+ value,
283
+ resolution: {
284
+ action: 'Remove the block',
285
+ description: "Block with _key '123' has invalid _type 'banana'",
286
+ item: value[0],
287
+ patches: [
288
+ {
289
+ path: [{_key: '123'}],
290
+ type: 'unset',
291
+ },
292
+ ],
293
+ i18n: {
294
+ action: 'inputs.portable-text.invalid-value.disallowed-type.action',
295
+ description: 'inputs.portable-text.invalid-value.disallowed-type.description',
296
+ values: {
297
+ key: '123',
298
+ typeName: 'banana',
299
+ },
300
+ },
301
+ },
302
+ })
303
+ })
304
+ })
305
+ it("doesn't crash when containing a invalid block somewhere inside the content", async () => {
306
+ const editorRef: RefObject<PortableTextEditor> = createRef()
307
+ const initialValue: PortableTextBlock[] = [
308
+ helloBlock,
309
+ {
310
+ _key: 'abc',
311
+ _type: 'myTestBlockType',
312
+ markDefs: [],
313
+ children: [{_key: 'def', _type: 'span', marks: []}],
314
+ },
315
+ ]
316
+ const initialSelection: EditorSelection = {
317
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
318
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
319
+ }
320
+ const onChange = jest.fn()
321
+ render(
322
+ <PortableTextEditorTester
323
+ onChange={onChange}
324
+ ref={editorRef}
325
+ selection={initialSelection}
326
+ schemaType={schemaType}
327
+ value={initialValue}
328
+ />,
329
+ )
330
+ await waitFor(() => {
331
+ if (editorRef.current) {
332
+ expect(onChange).toHaveBeenCalledWith({
333
+ type: 'invalidValue',
334
+ value: initialValue,
335
+ resolution: {
336
+ action: 'Write an empty text property to the object',
337
+ description:
338
+ "Child with _key 'def' in block with key 'abc' has missing or invalid text property!",
339
+ i18n: {
340
+ action: 'inputs.portable-text.invalid-value.invalid-span-text.action',
341
+ description: 'inputs.portable-text.invalid-value.invalid-span-text.description',
342
+ values: {
343
+ key: 'abc',
344
+ childKey: 'def',
345
+ },
346
+ },
347
+ item: {
348
+ _key: 'abc',
349
+ _type: 'myTestBlockType',
350
+ children: [
351
+ {
352
+ _key: 'def',
353
+ _type: 'span',
354
+ marks: [],
355
+ },
356
+ ],
357
+ markDefs: [],
358
+ },
359
+ patches: [
360
+ {
361
+ path: [
362
+ {
363
+ _key: 'abc',
364
+ },
365
+ 'children',
366
+ {
367
+ _key: 'def',
368
+ },
369
+ ],
370
+ type: 'set',
371
+ value: {
372
+ _key: 'def',
373
+ _type: 'span',
374
+ marks: [],
375
+ text: '',
376
+ },
377
+ },
378
+ ],
379
+ },
380
+ })
381
+ }
382
+ })
383
+ expect(onChange).not.toHaveBeenCalledWith({type: 'value', value: initialValue})
384
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
385
+ })
386
+ })