@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,389 @@
|
|
|
1
|
+
import {describe, expect, it, jest} from '@jest/globals'
|
|
2
|
+
import {type PortableTextBlock} from '@sanity/types'
|
|
3
|
+
import {render, waitFor} from '@testing-library/react'
|
|
4
|
+
import {createRef, type RefObject} from 'react'
|
|
5
|
+
|
|
6
|
+
import {PortableTextEditor} from '../PortableTextEditor'
|
|
7
|
+
import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester'
|
|
8
|
+
|
|
9
|
+
describe('when PTE would display warnings, instead it self solves', () => {
|
|
10
|
+
it('when child at index is missing required _key in block with _key', async () => {
|
|
11
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
12
|
+
const initialValue = [
|
|
13
|
+
{
|
|
14
|
+
_key: 'abc',
|
|
15
|
+
_type: 'myTestBlockType',
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
_type: 'span',
|
|
19
|
+
marks: [],
|
|
20
|
+
text: 'Hello with a new key',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
markDefs: [],
|
|
24
|
+
style: 'normal',
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const onChange = jest.fn()
|
|
29
|
+
render(
|
|
30
|
+
<PortableTextEditorTester
|
|
31
|
+
onChange={onChange}
|
|
32
|
+
ref={editorRef}
|
|
33
|
+
schemaType={schemaType}
|
|
34
|
+
value={initialValue}
|
|
35
|
+
/>,
|
|
36
|
+
)
|
|
37
|
+
await waitFor(() => {
|
|
38
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
39
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
40
|
+
})
|
|
41
|
+
await waitFor(() => {
|
|
42
|
+
if (editorRef.current) {
|
|
43
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
44
|
+
{
|
|
45
|
+
_key: 'abc',
|
|
46
|
+
_type: 'myTestBlockType',
|
|
47
|
+
children: [
|
|
48
|
+
{
|
|
49
|
+
_key: '4',
|
|
50
|
+
_type: 'span',
|
|
51
|
+
text: 'Hello with a new key',
|
|
52
|
+
marks: [],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
markDefs: [],
|
|
56
|
+
style: 'normal',
|
|
57
|
+
},
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('allows missing .markDefs', async () => {
|
|
64
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
65
|
+
const initialValue = [
|
|
66
|
+
{
|
|
67
|
+
_key: 'abc',
|
|
68
|
+
_type: 'myTestBlockType',
|
|
69
|
+
children: [
|
|
70
|
+
{
|
|
71
|
+
_key: 'def',
|
|
72
|
+
_type: 'span',
|
|
73
|
+
marks: [],
|
|
74
|
+
text: 'No markDefs',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
style: 'normal',
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const onChange = jest.fn()
|
|
82
|
+
render(
|
|
83
|
+
<PortableTextEditorTester
|
|
84
|
+
onChange={onChange}
|
|
85
|
+
ref={editorRef}
|
|
86
|
+
schemaType={schemaType}
|
|
87
|
+
value={initialValue}
|
|
88
|
+
/>,
|
|
89
|
+
)
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
92
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
93
|
+
})
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
if (editorRef.current) {
|
|
96
|
+
PortableTextEditor.focus(editorRef.current)
|
|
97
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
98
|
+
{
|
|
99
|
+
_key: 'abc',
|
|
100
|
+
_type: 'myTestBlockType',
|
|
101
|
+
children: [
|
|
102
|
+
{
|
|
103
|
+
_key: 'def',
|
|
104
|
+
_type: 'span',
|
|
105
|
+
text: 'No markDefs',
|
|
106
|
+
marks: [],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
style: 'normal',
|
|
110
|
+
},
|
|
111
|
+
])
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('adds missing .children', async () => {
|
|
117
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
118
|
+
const initialValue = [
|
|
119
|
+
{
|
|
120
|
+
_key: 'abc',
|
|
121
|
+
_type: 'myTestBlockType',
|
|
122
|
+
style: 'normal',
|
|
123
|
+
markDefs: [],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
_key: 'def',
|
|
127
|
+
_type: 'myTestBlockType',
|
|
128
|
+
style: 'normal',
|
|
129
|
+
children: [],
|
|
130
|
+
markDefs: [],
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const onChange = jest.fn()
|
|
135
|
+
render(
|
|
136
|
+
<PortableTextEditorTester
|
|
137
|
+
onChange={onChange}
|
|
138
|
+
ref={editorRef}
|
|
139
|
+
schemaType={schemaType}
|
|
140
|
+
value={initialValue}
|
|
141
|
+
/>,
|
|
142
|
+
)
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
145
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
146
|
+
})
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
if (editorRef.current) {
|
|
149
|
+
PortableTextEditor.focus(editorRef.current)
|
|
150
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
151
|
+
{
|
|
152
|
+
_key: 'abc',
|
|
153
|
+
_type: 'myTestBlockType',
|
|
154
|
+
children: [
|
|
155
|
+
{
|
|
156
|
+
_key: '5',
|
|
157
|
+
_type: 'span',
|
|
158
|
+
text: '',
|
|
159
|
+
marks: [],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
markDefs: [],
|
|
163
|
+
style: 'normal',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
_key: 'def',
|
|
167
|
+
_type: 'myTestBlockType',
|
|
168
|
+
children: [
|
|
169
|
+
{
|
|
170
|
+
_key: '6',
|
|
171
|
+
_type: 'span',
|
|
172
|
+
text: '',
|
|
173
|
+
marks: [],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
markDefs: [],
|
|
177
|
+
style: 'normal',
|
|
178
|
+
},
|
|
179
|
+
])
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('removes orphaned marks', async () => {
|
|
185
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
186
|
+
const initialValue = [
|
|
187
|
+
{
|
|
188
|
+
_key: 'abc',
|
|
189
|
+
_type: 'myTestBlockType',
|
|
190
|
+
style: 'normal',
|
|
191
|
+
markDefs: [],
|
|
192
|
+
children: [
|
|
193
|
+
{
|
|
194
|
+
_key: 'def',
|
|
195
|
+
_type: 'span',
|
|
196
|
+
marks: ['ghi'],
|
|
197
|
+
text: 'Hello',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
const onChange = jest.fn()
|
|
204
|
+
render(
|
|
205
|
+
<PortableTextEditorTester
|
|
206
|
+
onChange={onChange}
|
|
207
|
+
ref={editorRef}
|
|
208
|
+
schemaType={schemaType}
|
|
209
|
+
value={initialValue}
|
|
210
|
+
/>,
|
|
211
|
+
)
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
214
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
215
|
+
})
|
|
216
|
+
await waitFor(() => {
|
|
217
|
+
if (editorRef.current) {
|
|
218
|
+
PortableTextEditor.focus(editorRef.current)
|
|
219
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
220
|
+
{
|
|
221
|
+
_key: 'abc',
|
|
222
|
+
_type: 'myTestBlockType',
|
|
223
|
+
children: [
|
|
224
|
+
{
|
|
225
|
+
_key: 'def',
|
|
226
|
+
_type: 'span',
|
|
227
|
+
text: 'Hello',
|
|
228
|
+
marks: [],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
markDefs: [],
|
|
232
|
+
style: 'normal',
|
|
233
|
+
},
|
|
234
|
+
])
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('removes orphaned marksDefs', async () => {
|
|
240
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
241
|
+
const initialValue = [
|
|
242
|
+
{
|
|
243
|
+
_key: 'abc',
|
|
244
|
+
_type: 'myTestBlockType',
|
|
245
|
+
style: 'normal',
|
|
246
|
+
markDefs: [
|
|
247
|
+
{
|
|
248
|
+
_key: 'ghi',
|
|
249
|
+
_type: 'link',
|
|
250
|
+
href: 'https://sanity.io',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
children: [
|
|
254
|
+
{
|
|
255
|
+
_key: 'def',
|
|
256
|
+
_type: 'span',
|
|
257
|
+
marks: [],
|
|
258
|
+
text: 'Hello',
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
const onChange = jest.fn()
|
|
265
|
+
render(
|
|
266
|
+
<PortableTextEditorTester
|
|
267
|
+
onChange={onChange}
|
|
268
|
+
ref={editorRef}
|
|
269
|
+
schemaType={schemaType}
|
|
270
|
+
value={initialValue}
|
|
271
|
+
/>,
|
|
272
|
+
)
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
275
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
276
|
+
})
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
if (editorRef.current) {
|
|
279
|
+
PortableTextEditor.focus(editorRef.current)
|
|
280
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
281
|
+
{
|
|
282
|
+
_key: 'abc',
|
|
283
|
+
_type: 'myTestBlockType',
|
|
284
|
+
children: [
|
|
285
|
+
{
|
|
286
|
+
_key: 'def',
|
|
287
|
+
_type: 'span',
|
|
288
|
+
text: 'Hello',
|
|
289
|
+
marks: [],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
markDefs: [],
|
|
293
|
+
style: 'normal',
|
|
294
|
+
},
|
|
295
|
+
])
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('allows missing .markDefs', async () => {
|
|
301
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
302
|
+
const initialValue = [
|
|
303
|
+
{
|
|
304
|
+
_key: 'abc',
|
|
305
|
+
_type: 'myTestBlockType',
|
|
306
|
+
children: [
|
|
307
|
+
{
|
|
308
|
+
_key: 'def',
|
|
309
|
+
_type: 'span',
|
|
310
|
+
marks: [],
|
|
311
|
+
text: 'No markDefs',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
style: 'normal',
|
|
315
|
+
},
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
const onChange = jest.fn()
|
|
319
|
+
render(
|
|
320
|
+
<PortableTextEditorTester
|
|
321
|
+
onChange={onChange}
|
|
322
|
+
ref={editorRef}
|
|
323
|
+
schemaType={schemaType}
|
|
324
|
+
value={initialValue}
|
|
325
|
+
/>,
|
|
326
|
+
)
|
|
327
|
+
await waitFor(() => {
|
|
328
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
329
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
330
|
+
})
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
if (editorRef.current) {
|
|
333
|
+
PortableTextEditor.focus(editorRef.current)
|
|
334
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
335
|
+
{
|
|
336
|
+
_key: 'abc',
|
|
337
|
+
_type: 'myTestBlockType',
|
|
338
|
+
children: [
|
|
339
|
+
{
|
|
340
|
+
_key: 'def',
|
|
341
|
+
_type: 'span',
|
|
342
|
+
text: 'No markDefs',
|
|
343
|
+
marks: [],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
style: 'normal',
|
|
347
|
+
},
|
|
348
|
+
])
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('allows empty array of blocks', async () => {
|
|
354
|
+
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
355
|
+
const initialValue = [] as PortableTextBlock[]
|
|
356
|
+
|
|
357
|
+
const onChange = jest.fn()
|
|
358
|
+
render(
|
|
359
|
+
<PortableTextEditorTester
|
|
360
|
+
onChange={onChange}
|
|
361
|
+
ref={editorRef}
|
|
362
|
+
schemaType={schemaType}
|
|
363
|
+
value={initialValue}
|
|
364
|
+
/>,
|
|
365
|
+
)
|
|
366
|
+
await waitFor(() => {
|
|
367
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
368
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
369
|
+
})
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
if (editorRef.current) {
|
|
372
|
+
PortableTextEditor.focus(editorRef.current)
|
|
373
|
+
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
374
|
+
{
|
|
375
|
+
_key: '5',
|
|
376
|
+
_type: 'myTestBlockType',
|
|
377
|
+
children: [{_key: '4', _type: 'span', marks: [], text: ''}],
|
|
378
|
+
markDefs: [],
|
|
379
|
+
style: 'normal',
|
|
380
|
+
},
|
|
381
|
+
])
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
|
|
386
|
+
expect(onChange).toHaveBeenCalledWith({type: 'ready'})
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// This utils are inspired from https://github.dev/mwood23/slate-test-utils/blob/master/src/buildTestHarness.tsx
|
|
2
|
+
import {act, fireEvent, type render} from '@testing-library/react'
|
|
3
|
+
import {parseHotkey} from 'is-hotkey-esm'
|
|
4
|
+
|
|
5
|
+
export async function triggerKeyboardEvent(hotkey: string, element: Element): Promise<void> {
|
|
6
|
+
return act(async () => {
|
|
7
|
+
const eventProps = parseHotkey(hotkey)
|
|
8
|
+
const values = hotkey.split('+')
|
|
9
|
+
|
|
10
|
+
fireEvent(
|
|
11
|
+
element,
|
|
12
|
+
new window.KeyboardEvent('keydown', {
|
|
13
|
+
key: values[values.length - 1],
|
|
14
|
+
code: `${eventProps.which}`,
|
|
15
|
+
keyCode: eventProps.which,
|
|
16
|
+
bubbles: true,
|
|
17
|
+
...eventProps,
|
|
18
|
+
}),
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getEditableElement(component: ReturnType<typeof render>): Promise<Element> {
|
|
24
|
+
await act(async () => component)
|
|
25
|
+
const element = component.container.querySelector('[data-slate-editor="true"]')
|
|
26
|
+
if (!element) {
|
|
27
|
+
throw new Error('Could not find element')
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Manually add this because JSDom doesn't implement this and Slate checks for it
|
|
31
|
+
* internally before doing stuff.
|
|
32
|
+
*
|
|
33
|
+
* https://github.com/jsdom/jsdom/issues/1670
|
|
34
|
+
*/
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
element.isContentEditable = true
|
|
38
|
+
return element
|
|
39
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DragEvent,
|
|
3
|
+
type MutableRefObject,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import {Editor, type Element as SlateElement, Path, Transforms} from 'slate'
|
|
12
|
+
import {ReactEditor, useSlateStatic} from 'slate-react'
|
|
13
|
+
|
|
14
|
+
import {debugWithName} from '../../utils/debug'
|
|
15
|
+
import {
|
|
16
|
+
IS_DRAGGING,
|
|
17
|
+
IS_DRAGGING_BLOCK_ELEMENT,
|
|
18
|
+
IS_DRAGGING_BLOCK_TARGET_POSITION,
|
|
19
|
+
IS_DRAGGING_ELEMENT_TARGET,
|
|
20
|
+
} from '../../utils/weakMaps'
|
|
21
|
+
|
|
22
|
+
const debug = debugWithName('components:DraggableBlock')
|
|
23
|
+
const debugRenders = false
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export interface DraggableBlockProps {
|
|
29
|
+
children: ReactNode
|
|
30
|
+
element: SlateElement
|
|
31
|
+
readOnly: boolean
|
|
32
|
+
blockRef: MutableRefObject<HTMLDivElement | null>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Implements drag and drop functionality on editor block nodes
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export const DraggableBlock = ({children, element, readOnly, blockRef}: DraggableBlockProps) => {
|
|
40
|
+
const editor = useSlateStatic()
|
|
41
|
+
const dragGhostRef: MutableRefObject<undefined | HTMLElement> = useRef()
|
|
42
|
+
const [isDragOver, setIsDragOver] = useState(false)
|
|
43
|
+
const isVoid = useMemo(() => Editor.isVoid(editor, element), [editor, element])
|
|
44
|
+
const isInline = useMemo(() => Editor.isInline(editor, element), [editor, element])
|
|
45
|
+
|
|
46
|
+
const [blockElement, setBlockElement] = useState<HTMLElement | null>(null)
|
|
47
|
+
|
|
48
|
+
useEffect(
|
|
49
|
+
() => setBlockElement(blockRef ? blockRef.current : ReactEditor.toDOMNode(editor, element)),
|
|
50
|
+
[editor, element, blockRef],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Note: this is called not for the dragging block, but for the targets when the block is dragged over them
|
|
54
|
+
const handleDragOver = useCallback(
|
|
55
|
+
(event: DragEvent) => {
|
|
56
|
+
const isMyDragOver = IS_DRAGGING_BLOCK_ELEMENT.get(editor)
|
|
57
|
+
// debug('Drag over', blockElement)
|
|
58
|
+
if (!isMyDragOver || !blockElement) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
event.preventDefault()
|
|
62
|
+
event.dataTransfer.dropEffect = 'move'
|
|
63
|
+
IS_DRAGGING_ELEMENT_TARGET.set(editor, element)
|
|
64
|
+
const elementRect = blockElement.getBoundingClientRect()
|
|
65
|
+
const offset = elementRect.top
|
|
66
|
+
const height = elementRect.height
|
|
67
|
+
const Y = event.pageY
|
|
68
|
+
const loc = Math.abs(offset - Y)
|
|
69
|
+
let position: 'top' | 'bottom' = 'bottom'
|
|
70
|
+
if (element === editor.children[0]) {
|
|
71
|
+
position = 'top'
|
|
72
|
+
} else if (loc < height / 2) {
|
|
73
|
+
position = 'top'
|
|
74
|
+
IS_DRAGGING_BLOCK_TARGET_POSITION.set(editor, position)
|
|
75
|
+
} else {
|
|
76
|
+
position = 'bottom'
|
|
77
|
+
IS_DRAGGING_BLOCK_TARGET_POSITION.set(editor, position)
|
|
78
|
+
}
|
|
79
|
+
if (isMyDragOver === element) {
|
|
80
|
+
event.dataTransfer.dropEffect = 'none'
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
setIsDragOver(true)
|
|
84
|
+
},
|
|
85
|
+
[blockElement, editor, element],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Note: this is called not for the dragging block, but for the targets when the block is dragged over them
|
|
89
|
+
const handleDragLeave = useCallback(() => {
|
|
90
|
+
setIsDragOver(false)
|
|
91
|
+
}, [])
|
|
92
|
+
|
|
93
|
+
// Note: this is called for the dragging block
|
|
94
|
+
const handleDragEnd = useCallback(
|
|
95
|
+
(event: DragEvent) => {
|
|
96
|
+
const targetBlock = IS_DRAGGING_ELEMENT_TARGET.get(editor)
|
|
97
|
+
if (targetBlock) {
|
|
98
|
+
IS_DRAGGING.set(editor, false)
|
|
99
|
+
event.preventDefault()
|
|
100
|
+
event.stopPropagation()
|
|
101
|
+
IS_DRAGGING_ELEMENT_TARGET.delete(editor)
|
|
102
|
+
if (dragGhostRef.current) {
|
|
103
|
+
debug('Removing drag ghost')
|
|
104
|
+
document.body.removeChild(dragGhostRef.current)
|
|
105
|
+
}
|
|
106
|
+
const dragPosition = IS_DRAGGING_BLOCK_TARGET_POSITION.get(editor)
|
|
107
|
+
IS_DRAGGING_BLOCK_TARGET_POSITION.delete(editor)
|
|
108
|
+
let targetPath = ReactEditor.findPath(editor, targetBlock)
|
|
109
|
+
const myPath = ReactEditor.findPath(editor, element)
|
|
110
|
+
const isBefore = Path.isBefore(myPath, targetPath)
|
|
111
|
+
if (dragPosition === 'bottom' && !isBefore) {
|
|
112
|
+
// If it is already at the bottom, don't do anything.
|
|
113
|
+
if (targetPath[0] >= editor.children.length - 1) {
|
|
114
|
+
debug('target is already at the bottom, not moving')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const originalPath = targetPath
|
|
118
|
+
targetPath = Path.next(targetPath)
|
|
119
|
+
debug(
|
|
120
|
+
`Adjusting targetPath from ${JSON.stringify(originalPath)} to ${JSON.stringify(
|
|
121
|
+
targetPath,
|
|
122
|
+
)}`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
if (dragPosition === 'top' && isBefore && targetPath[0] !== editor.children.length - 1) {
|
|
126
|
+
const originalPath = targetPath
|
|
127
|
+
targetPath = Path.previous(targetPath)
|
|
128
|
+
debug(
|
|
129
|
+
`Adjusting targetPath from ${JSON.stringify(originalPath)} to ${JSON.stringify(
|
|
130
|
+
targetPath,
|
|
131
|
+
)}`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
if (Path.equals(targetPath, myPath)) {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
debug('targetPath and myPath is the same, not moving')
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
debug(
|
|
140
|
+
`Moving element ${element._key} from path ${JSON.stringify(myPath)} to ${JSON.stringify(
|
|
141
|
+
targetPath,
|
|
142
|
+
)} (${dragPosition})`,
|
|
143
|
+
)
|
|
144
|
+
Transforms.moveNodes(editor, {at: myPath, to: targetPath})
|
|
145
|
+
editor.onChange()
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
debug('No target element, not doing anything')
|
|
149
|
+
},
|
|
150
|
+
[editor, element],
|
|
151
|
+
)
|
|
152
|
+
// Note: this is called not for the dragging block, but for the drop target
|
|
153
|
+
const handleDrop = useCallback(
|
|
154
|
+
(event: DragEvent) => {
|
|
155
|
+
if (IS_DRAGGING_BLOCK_ELEMENT.get(editor)) {
|
|
156
|
+
debug('On drop (prevented)', element)
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
event.stopPropagation()
|
|
159
|
+
setIsDragOver(false)
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
[editor, element],
|
|
163
|
+
)
|
|
164
|
+
// Note: this is called for the dragging block
|
|
165
|
+
const handleDrag = useCallback(
|
|
166
|
+
(event: DragEvent) => {
|
|
167
|
+
if (!isVoid) {
|
|
168
|
+
IS_DRAGGING_BLOCK_ELEMENT.delete(editor)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
IS_DRAGGING.set(editor, true)
|
|
172
|
+
IS_DRAGGING_BLOCK_ELEMENT.set(editor, element)
|
|
173
|
+
event.stopPropagation() // Stop propagation so that leafs don't get this and take focus/selection!
|
|
174
|
+
|
|
175
|
+
const target = event.target
|
|
176
|
+
|
|
177
|
+
if (target instanceof HTMLElement) {
|
|
178
|
+
target.style.opacity = '1'
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[editor, element, isVoid],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
// Note: this is called for the dragging block
|
|
185
|
+
const handleDragStart = useCallback(
|
|
186
|
+
(event: DragEvent) => {
|
|
187
|
+
if (!isVoid || isInline) {
|
|
188
|
+
debug('Not dragging block')
|
|
189
|
+
IS_DRAGGING_BLOCK_ELEMENT.delete(editor)
|
|
190
|
+
IS_DRAGGING.set(editor, false)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
debug('Drag start')
|
|
194
|
+
IS_DRAGGING.set(editor, true)
|
|
195
|
+
if (event.dataTransfer) {
|
|
196
|
+
event.dataTransfer.setData('application/portable-text', 'something')
|
|
197
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
198
|
+
}
|
|
199
|
+
// Clone blockElement so that it will not be visually clipped by scroll-containers etc.
|
|
200
|
+
// The application that uses the portable-text-editor may indicate the element used as
|
|
201
|
+
// drag ghost by adding a truthy data attribute 'data-pt-drag-ghost-element' to a HTML element.
|
|
202
|
+
if (blockElement && blockElement instanceof HTMLElement) {
|
|
203
|
+
let dragGhost = blockElement.cloneNode(true) as HTMLElement
|
|
204
|
+
const customGhost = dragGhost.querySelector('[data-pt-drag-ghost-element]')
|
|
205
|
+
if (customGhost) {
|
|
206
|
+
dragGhost = customGhost as HTMLElement
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Set the `data-dragged` attribute so the consumer can style the element while it’s dragged
|
|
210
|
+
dragGhost.setAttribute('data-dragged', '')
|
|
211
|
+
|
|
212
|
+
if (document.body) {
|
|
213
|
+
dragGhostRef.current = dragGhost
|
|
214
|
+
dragGhost.style.position = 'absolute'
|
|
215
|
+
dragGhost.style.left = '-99999px'
|
|
216
|
+
dragGhost.style.boxSizing = 'border-box'
|
|
217
|
+
document.body.appendChild(dragGhost)
|
|
218
|
+
const rect = blockElement.getBoundingClientRect()
|
|
219
|
+
const x = event.clientX - rect.left
|
|
220
|
+
const y = event.clientY - rect.top
|
|
221
|
+
dragGhost.style.width = `${rect.width}px`
|
|
222
|
+
dragGhost.style.height = `${rect.height}px`
|
|
223
|
+
event.dataTransfer.setDragImage(dragGhost, x, y)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
handleDrag(event)
|
|
227
|
+
},
|
|
228
|
+
[blockElement, editor, handleDrag, isInline, isVoid],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const isDraggingOverFirstBlock =
|
|
232
|
+
isDragOver && editor.children[0] === IS_DRAGGING_ELEMENT_TARGET.get(editor)
|
|
233
|
+
const isDraggingOverLastBlock =
|
|
234
|
+
isDragOver &&
|
|
235
|
+
editor.children[editor.children.length - 1] === IS_DRAGGING_ELEMENT_TARGET.get(editor)
|
|
236
|
+
const dragPosition = IS_DRAGGING_BLOCK_TARGET_POSITION.get(editor)
|
|
237
|
+
|
|
238
|
+
const isDraggingOverTop =
|
|
239
|
+
isDraggingOverFirstBlock ||
|
|
240
|
+
(isDragOver && !isDraggingOverFirstBlock && !isDraggingOverLastBlock && dragPosition === 'top')
|
|
241
|
+
const isDraggingOverBottom =
|
|
242
|
+
isDraggingOverLastBlock ||
|
|
243
|
+
(isDragOver &&
|
|
244
|
+
!isDraggingOverFirstBlock &&
|
|
245
|
+
!isDraggingOverLastBlock &&
|
|
246
|
+
dragPosition === 'bottom')
|
|
247
|
+
|
|
248
|
+
const dropIndicator = useMemo(
|
|
249
|
+
() => (
|
|
250
|
+
<div
|
|
251
|
+
className="pt-drop-indicator"
|
|
252
|
+
style={{
|
|
253
|
+
position: 'absolute',
|
|
254
|
+
width: '100%',
|
|
255
|
+
height: 1,
|
|
256
|
+
borderBottom: '1px solid currentColor',
|
|
257
|
+
zIndex: 5,
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
),
|
|
261
|
+
[],
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if (readOnly) {
|
|
265
|
+
return <>{children}</>
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (debugRenders) {
|
|
269
|
+
debug('render')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div
|
|
274
|
+
draggable={isVoid}
|
|
275
|
+
onDragStart={handleDragStart}
|
|
276
|
+
onDrag={handleDrag}
|
|
277
|
+
onDragOver={handleDragOver}
|
|
278
|
+
onDragLeave={handleDragLeave}
|
|
279
|
+
onDragEnd={handleDragEnd}
|
|
280
|
+
onDrop={handleDrop}
|
|
281
|
+
>
|
|
282
|
+
{isDraggingOverTop && dropIndicator}
|
|
283
|
+
{children}
|
|
284
|
+
{isDraggingOverBottom && dropIndicator}
|
|
285
|
+
</div>
|
|
286
|
+
)
|
|
287
|
+
}
|