@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/lib/index.d.mts +911 -0
- package/lib/index.d.ts +911 -0
- package/lib/index.esm.js +4896 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +4874 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +4896 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/editor/Editable.tsx +683 -0
- package/src/editor/PortableTextEditor.tsx +308 -0
- package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
- package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
- package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
- package/src/editor/__tests__/handleClick.test.tsx +218 -0
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
- package/src/editor/__tests__/utils.ts +39 -0
- package/src/editor/components/DraggableBlock.tsx +287 -0
- package/src/editor/components/Element.tsx +279 -0
- package/src/editor/components/Leaf.tsx +288 -0
- package/src/editor/components/SlateContainer.tsx +81 -0
- package/src/editor/components/Synchronizer.tsx +190 -0
- package/src/editor/hooks/usePortableTextEditor.ts +23 -0
- package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
- package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
- package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
- package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
- package/src/editor/hooks/useSyncValue.test.tsx +125 -0
- package/src/editor/hooks/useSyncValue.ts +372 -0
- package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
- package/src/editor/nodes/DefaultObject.tsx +15 -0
- package/src/editor/nodes/index.ts +189 -0
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
- package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
- package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
- package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
- package/src/editor/plugins/createWithEditableAPI.ts +573 -0
- package/src/editor/plugins/createWithHotKeys.ts +304 -0
- package/src/editor/plugins/createWithInsertBreak.ts +45 -0
- package/src/editor/plugins/createWithInsertData.ts +359 -0
- package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
- package/src/editor/plugins/createWithObjectKeys.ts +63 -0
- package/src/editor/plugins/createWithPatches.ts +274 -0
- package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
- package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
- package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
- package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
- package/src/editor/plugins/createWithUndoRedo.ts +494 -0
- package/src/editor/plugins/createWithUtils.ts +81 -0
- package/src/editor/plugins/index.ts +155 -0
- package/src/index.ts +11 -0
- package/src/patch/PatchEvent.ts +33 -0
- package/src/patch/applyPatch.ts +29 -0
- package/src/patch/array.ts +89 -0
- package/src/patch/arrayInsert.ts +27 -0
- package/src/patch/object.ts +39 -0
- package/src/patch/patches.ts +53 -0
- package/src/patch/primitive.ts +43 -0
- package/src/patch/string.ts +51 -0
- package/src/types/editor.ts +576 -0
- package/src/types/options.ts +17 -0
- package/src/types/patch.ts +65 -0
- package/src/types/slate.ts +25 -0
- package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
- package/src/utils/__tests__/operationToPatches.test.ts +421 -0
- package/src/utils/__tests__/patchToOperations.test.ts +293 -0
- package/src/utils/__tests__/ranges.test.ts +18 -0
- package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
- package/src/utils/__tests__/values.test.ts +253 -0
- package/src/utils/applyPatch.ts +407 -0
- package/src/utils/bufferUntil.ts +15 -0
- package/src/utils/debug.ts +12 -0
- package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
- package/src/utils/operationToPatches.ts +357 -0
- package/src/utils/patches.ts +36 -0
- package/src/utils/paths.ts +60 -0
- package/src/utils/ranges.ts +77 -0
- package/src/utils/schema.ts +8 -0
- package/src/utils/selection.ts +65 -0
- package/src/utils/ucs2Indices.ts +67 -0
- package/src/utils/validateValue.ts +394 -0
- package/src/utils/values.ts +208 -0
- package/src/utils/weakMaps.ts +24 -0
- package/src/utils/withChanges.ts +25 -0
- package/src/utils/withPreserveKeys.ts +14 -0
- 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
|
+
})
|