@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.
@@ -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
- }