@portabletext/editor 1.44.4 → 1.44.6
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/lib/index.cjs +255 -95
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +257 -96
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/src/editor/Editable.tsx +43 -169
- package/src/editor/__tests__/RangeDecorations.test.tsx +26 -0
- package/src/editor/range-decorations-machine.ts +346 -0
- package/src/editor/withSyncRangeDecorations.ts +0 -32
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import {isEqual} from 'lodash'
|
|
2
|
+
import {
|
|
3
|
+
Path,
|
|
4
|
+
Range,
|
|
5
|
+
type BaseRange,
|
|
6
|
+
type NodeEntry,
|
|
7
|
+
type Operation,
|
|
8
|
+
} from 'slate'
|
|
9
|
+
import {
|
|
10
|
+
and,
|
|
11
|
+
assertEvent,
|
|
12
|
+
assign,
|
|
13
|
+
fromCallback,
|
|
14
|
+
setup,
|
|
15
|
+
type ActorRefFrom,
|
|
16
|
+
type AnyEventObject,
|
|
17
|
+
type CallbackLogicFunction,
|
|
18
|
+
} from 'xstate'
|
|
19
|
+
import type {EditorSchema} from '..'
|
|
20
|
+
import {moveRangeByOperation, toSlateRange} from '../internal-utils/ranges'
|
|
21
|
+
import {slateRangeToSelection} from '../internal-utils/slate-utils'
|
|
22
|
+
import {isEqualToEmptyEditor} from '../internal-utils/values'
|
|
23
|
+
import type {PortableTextSlateEditor, RangeDecoration} from '../types/editor'
|
|
24
|
+
|
|
25
|
+
const slateOperationCallback: CallbackLogicFunction<
|
|
26
|
+
AnyEventObject,
|
|
27
|
+
{type: 'slate operation'; operation: Operation},
|
|
28
|
+
{slateEditor: PortableTextSlateEditor}
|
|
29
|
+
> = ({input, sendBack}) => {
|
|
30
|
+
const originalApply = input.slateEditor.apply
|
|
31
|
+
|
|
32
|
+
input.slateEditor.apply = (op) => {
|
|
33
|
+
if (op.type !== 'set_selection') {
|
|
34
|
+
sendBack({type: 'slate operation', operation: op})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
originalApply(op)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
input.slateEditor.apply = originalApply
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type DecoratedRange = BaseRange & {rangeDecoration: RangeDecoration}
|
|
46
|
+
|
|
47
|
+
export const rangeDecorationsMachine = setup({
|
|
48
|
+
types: {
|
|
49
|
+
context: {} as {
|
|
50
|
+
decoratedRanges: Array<DecoratedRange>
|
|
51
|
+
pendingRangeDecorations: Array<RangeDecoration>
|
|
52
|
+
readOnly: boolean
|
|
53
|
+
schema: EditorSchema
|
|
54
|
+
slateEditor: PortableTextSlateEditor
|
|
55
|
+
},
|
|
56
|
+
input: {} as {
|
|
57
|
+
rangeDecorations: Array<RangeDecoration>
|
|
58
|
+
readOnly: boolean
|
|
59
|
+
schema: EditorSchema
|
|
60
|
+
slateEditor: PortableTextSlateEditor
|
|
61
|
+
},
|
|
62
|
+
events: {} as
|
|
63
|
+
| {
|
|
64
|
+
type: 'ready'
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
type: 'range decorations updated'
|
|
68
|
+
rangeDecorations: Array<RangeDecoration>
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
type: 'slate operation'
|
|
72
|
+
operation: Operation
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
type: 'update read only'
|
|
76
|
+
readOnly: boolean
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
actions: {
|
|
80
|
+
'update pending range decorations': assign({
|
|
81
|
+
pendingRangeDecorations: ({event}) => {
|
|
82
|
+
assertEvent(event, 'range decorations updated')
|
|
83
|
+
|
|
84
|
+
return event.rangeDecorations
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
'set up initial range decorations': assign({
|
|
88
|
+
decoratedRanges: ({context, event}) => {
|
|
89
|
+
assertEvent(event, 'ready')
|
|
90
|
+
|
|
91
|
+
const rangeDecorationState: Array<DecoratedRange> = []
|
|
92
|
+
|
|
93
|
+
for (const rangeDecoration of context.pendingRangeDecorations) {
|
|
94
|
+
const slateRange = toSlateRange(
|
|
95
|
+
rangeDecoration.selection,
|
|
96
|
+
context.slateEditor,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (!Range.isRange(slateRange)) {
|
|
100
|
+
rangeDecoration.onMoved?.({
|
|
101
|
+
newSelection: null,
|
|
102
|
+
rangeDecoration,
|
|
103
|
+
origin: 'local',
|
|
104
|
+
})
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
rangeDecorationState.push({
|
|
109
|
+
rangeDecoration,
|
|
110
|
+
...slateRange,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return rangeDecorationState
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
'update range decorations': assign({
|
|
118
|
+
decoratedRanges: ({context, event}) => {
|
|
119
|
+
assertEvent(event, 'range decorations updated')
|
|
120
|
+
|
|
121
|
+
const rangeDecorationState: Array<DecoratedRange> = []
|
|
122
|
+
|
|
123
|
+
for (const rangeDecoration of event.rangeDecorations) {
|
|
124
|
+
const slateRange = toSlateRange(
|
|
125
|
+
rangeDecoration.selection,
|
|
126
|
+
context.slateEditor,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (!Range.isRange(slateRange)) {
|
|
130
|
+
rangeDecoration.onMoved?.({
|
|
131
|
+
newSelection: null,
|
|
132
|
+
rangeDecoration,
|
|
133
|
+
origin: 'local',
|
|
134
|
+
})
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
rangeDecorationState.push({
|
|
139
|
+
rangeDecoration,
|
|
140
|
+
...slateRange,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return rangeDecorationState
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
'move range decorations': assign({
|
|
148
|
+
decoratedRanges: ({context, event}) => {
|
|
149
|
+
assertEvent(event, 'slate operation')
|
|
150
|
+
|
|
151
|
+
const rangeDecorationState: Array<DecoratedRange> = []
|
|
152
|
+
|
|
153
|
+
for (const decoratedRange of context.decoratedRanges) {
|
|
154
|
+
const slateRange = toSlateRange(
|
|
155
|
+
decoratedRange.rangeDecoration.selection,
|
|
156
|
+
context.slateEditor,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if (!Range.isRange(slateRange)) {
|
|
160
|
+
decoratedRange.rangeDecoration.onMoved?.({
|
|
161
|
+
newSelection: null,
|
|
162
|
+
rangeDecoration: decoratedRange.rangeDecoration,
|
|
163
|
+
origin: 'local',
|
|
164
|
+
})
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let newRange: BaseRange | null | undefined
|
|
169
|
+
|
|
170
|
+
newRange = moveRangeByOperation(slateRange, event.operation)
|
|
171
|
+
if (
|
|
172
|
+
(newRange && newRange !== slateRange) ||
|
|
173
|
+
(newRange === null && slateRange)
|
|
174
|
+
) {
|
|
175
|
+
const newRangeSelection = newRange
|
|
176
|
+
? slateRangeToSelection({
|
|
177
|
+
schema: context.schema,
|
|
178
|
+
editor: context.slateEditor,
|
|
179
|
+
range: newRange,
|
|
180
|
+
})
|
|
181
|
+
: null
|
|
182
|
+
|
|
183
|
+
decoratedRange.rangeDecoration.onMoved?.({
|
|
184
|
+
newSelection: newRangeSelection,
|
|
185
|
+
rangeDecoration: decoratedRange.rangeDecoration,
|
|
186
|
+
origin: 'local',
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If the newRange is null, it means that the range is not valid anymore and should be removed
|
|
191
|
+
// If it's undefined, it means that the slateRange is still valid and should be kept
|
|
192
|
+
if (newRange !== null) {
|
|
193
|
+
rangeDecorationState.push({
|
|
194
|
+
...(newRange || slateRange),
|
|
195
|
+
rangeDecoration: decoratedRange.rangeDecoration,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return rangeDecorationState
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
'assign readOnly': assign({
|
|
204
|
+
readOnly: ({event}) => {
|
|
205
|
+
assertEvent(event, 'update read only')
|
|
206
|
+
return event.readOnly
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
actors: {
|
|
211
|
+
'slate operation listener': fromCallback(slateOperationCallback),
|
|
212
|
+
},
|
|
213
|
+
guards: {
|
|
214
|
+
'has range decorations': ({context}) => context.decoratedRanges.length > 0,
|
|
215
|
+
'has different decorations': ({context, event}) => {
|
|
216
|
+
assertEvent(event, 'range decorations updated')
|
|
217
|
+
|
|
218
|
+
return !isEqual(context.pendingRangeDecorations, event.rangeDecorations)
|
|
219
|
+
},
|
|
220
|
+
'not read only': ({context}) => !context.readOnly,
|
|
221
|
+
},
|
|
222
|
+
}).createMachine({
|
|
223
|
+
id: 'range decorations',
|
|
224
|
+
context: ({input}) => ({
|
|
225
|
+
readOnly: input.readOnly,
|
|
226
|
+
pendingRangeDecorations: input.rangeDecorations,
|
|
227
|
+
decoratedRanges: [],
|
|
228
|
+
schema: input.schema,
|
|
229
|
+
slateEditor: input.slateEditor,
|
|
230
|
+
}),
|
|
231
|
+
invoke: {
|
|
232
|
+
src: 'slate operation listener',
|
|
233
|
+
input: ({context}) => ({slateEditor: context.slateEditor}),
|
|
234
|
+
},
|
|
235
|
+
on: {
|
|
236
|
+
'update read only': {
|
|
237
|
+
actions: ['assign readOnly'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
initial: 'idle',
|
|
241
|
+
states: {
|
|
242
|
+
idle: {
|
|
243
|
+
on: {
|
|
244
|
+
'range decorations updated': {
|
|
245
|
+
actions: ['update pending range decorations'],
|
|
246
|
+
},
|
|
247
|
+
'ready': {
|
|
248
|
+
target: 'ready',
|
|
249
|
+
actions: ['set up initial range decorations'],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
ready: {
|
|
254
|
+
initial: 'idle',
|
|
255
|
+
on: {
|
|
256
|
+
'range decorations updated': {
|
|
257
|
+
target: '.idle',
|
|
258
|
+
guard: 'has different decorations',
|
|
259
|
+
actions: [
|
|
260
|
+
'update range decorations',
|
|
261
|
+
'update pending range decorations',
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
states: {
|
|
266
|
+
'idle': {
|
|
267
|
+
on: {
|
|
268
|
+
'slate operation': {
|
|
269
|
+
target: 'moving range decorations',
|
|
270
|
+
guard: and(['has range decorations', 'not read only']),
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
'moving range decorations': {
|
|
275
|
+
entry: ['move range decorations'],
|
|
276
|
+
always: {
|
|
277
|
+
target: 'idle',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
export function createDecorate(
|
|
286
|
+
rangeDecorationActor: ActorRefFrom<typeof rangeDecorationsMachine>,
|
|
287
|
+
) {
|
|
288
|
+
return function decorate([, path]: NodeEntry): Array<BaseRange> {
|
|
289
|
+
if (
|
|
290
|
+
isEqualToEmptyEditor(
|
|
291
|
+
rangeDecorationActor.getSnapshot().context.slateEditor.children,
|
|
292
|
+
rangeDecorationActor.getSnapshot().context.schema,
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
295
|
+
return [
|
|
296
|
+
{
|
|
297
|
+
anchor: {
|
|
298
|
+
path: [0, 0],
|
|
299
|
+
offset: 0,
|
|
300
|
+
},
|
|
301
|
+
focus: {
|
|
302
|
+
path: [0, 0],
|
|
303
|
+
offset: 0,
|
|
304
|
+
},
|
|
305
|
+
placeholder: true,
|
|
306
|
+
} as BaseRange,
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Editor node has a path length of 0 (should never be decorated)
|
|
311
|
+
if (path.length === 0) {
|
|
312
|
+
return []
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const result = rangeDecorationActor
|
|
316
|
+
.getSnapshot()
|
|
317
|
+
.context.decoratedRanges.filter((item) => {
|
|
318
|
+
// Special case in order to only return one decoration for collapsed ranges
|
|
319
|
+
if (Range.isCollapsed(item)) {
|
|
320
|
+
// Collapsed ranges should only be decorated if they are on a block child level (length 2)
|
|
321
|
+
if (path.length !== 2) {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
Path.equals(item.focus.path, path) &&
|
|
327
|
+
Path.equals(item.anchor.path, path)
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Include decorations that either include or intersects with this path
|
|
332
|
+
return (
|
|
333
|
+
Range.intersection(item, {
|
|
334
|
+
anchor: {path, offset: 0},
|
|
335
|
+
focus: {path, offset: 0},
|
|
336
|
+
}) || Range.includes(item, path)
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
if (result.length > 0) {
|
|
341
|
+
return result
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return []
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type {BaseEditor, Operation} from 'slate'
|
|
2
|
-
import type {ReactEditor} from 'slate-react'
|
|
3
|
-
import type {PortableTextSlateEditor} from '../types/editor'
|
|
4
|
-
import type {EditorActor} from './editor-machine'
|
|
5
|
-
|
|
6
|
-
// React Compiler considers `slateEditor` as immutable, and opts-out if we do this inline in a useEffect, doing it in a function moves it out of the scope, and opts-in again for the rest of the component.
|
|
7
|
-
export function withSyncRangeDecorations({
|
|
8
|
-
editorActor,
|
|
9
|
-
slateEditor,
|
|
10
|
-
syncRangeDecorations,
|
|
11
|
-
}: {
|
|
12
|
-
editorActor: EditorActor
|
|
13
|
-
slateEditor: BaseEditor & ReactEditor & PortableTextSlateEditor
|
|
14
|
-
syncRangeDecorations: (operation?: Operation) => void
|
|
15
|
-
}) {
|
|
16
|
-
const originalApply = slateEditor.apply
|
|
17
|
-
|
|
18
|
-
slateEditor.apply = (op: Operation) => {
|
|
19
|
-
originalApply(op)
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
!editorActor.getSnapshot().matches({'edit mode': 'read only'}) &&
|
|
23
|
-
op.type !== 'set_selection'
|
|
24
|
-
) {
|
|
25
|
-
syncRangeDecorations(op)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return () => {
|
|
30
|
-
slateEditor.apply = originalApply
|
|
31
|
-
}
|
|
32
|
-
}
|