@portabletext/editor 1.54.2 → 1.54.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.54.2",
3
+ "version": "1.54.4",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -75,9 +75,9 @@
75
75
  "lodash": "^4.17.21",
76
76
  "lodash.startcase": "^4.4.0",
77
77
  "react-compiler-runtime": "19.1.0-rc.2",
78
- "slate": "0.115.1",
79
- "slate-dom": "^0.114.0",
80
- "slate-react": "0.114.2",
78
+ "slate": "0.117.0",
79
+ "slate-dom": "^0.116.0",
80
+ "slate-react": "0.117.1",
81
81
  "use-effect-event": "^1.0.2",
82
82
  "xstate": "^5.19.4",
83
83
  "@portabletext/patches": "1.1.5",
@@ -86,7 +86,7 @@
86
86
  "devDependencies": {
87
87
  "@portabletext/toolkit": "^2.0.17",
88
88
  "@sanity/diff-match-patch": "^3.2.0",
89
- "@sanity/pkg-utils": "^7.6.2",
89
+ "@sanity/pkg-utils": "^7.8.4",
90
90
  "@sanity/schema": "^3.93.0",
91
91
  "@sanity/types": "^3.93.0",
92
92
  "@testing-library/react": "^16.3.0",
@@ -52,10 +52,7 @@ import {RenderText, type RenderTextProps} from './components/render-text'
52
52
  import {EditorActorContext} from './editor-actor-context'
53
53
  import {usePortableTextEditor} from './hooks/usePortableTextEditor'
54
54
  import {createWithHotkeys} from './plugins/createWithHotKeys'
55
- import {
56
- createDecorate,
57
- rangeDecorationsMachine,
58
- } from './range-decorations-machine'
55
+ import {rangeDecorationsMachine} from './range-decorations-machine'
59
56
  import {RelayActorContext} from './relay-actor-context'
60
57
 
61
58
  const debug = debugWithName('component:Editable')
@@ -168,10 +165,9 @@ export const PortableTextEditable = forwardRef<
168
165
  skipSetup: !editorActor.getSnapshot().matches({setup: 'setting up'}),
169
166
  },
170
167
  })
171
- useSelector(rangeDecorationsActor, (s) => s.context.updateCount)
172
- const decorate = useMemo(
173
- () => createDecorate(rangeDecorationsActor),
174
- [rangeDecorationsActor],
168
+ const decorate = useSelector(
169
+ rangeDecorationsActor,
170
+ (s) => s.context.decorate?.fn,
175
171
  )
176
172
 
177
173
  useEffect(() => {
@@ -37,6 +37,7 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
37
37
  KEY_TO_VALUE_ELEMENT.set(instance, {})
38
38
  KEY_TO_SLATE_ELEMENT.set(instance, {})
39
39
 
40
+ instance.decoratedRanges = []
40
41
  instance.decoratorState = {}
41
42
  instance.markState = undefined
42
43
  instance.value = [
@@ -13,6 +13,9 @@ export type BaseDefinition = {
13
13
  title?: string
14
14
  }
15
15
 
16
+ /**
17
+ * @public
18
+ */
16
19
  export type FieldDefinition = {
17
20
  name: string
18
21
  type: 'string' | 'number' | 'boolean' | 'array' | 'object'
@@ -1,9 +1,8 @@
1
- import {Editor, Path} from 'slate'
1
+ import {Editor} from 'slate'
2
2
  import {debugWithName} from '../../internal-utils/debug'
3
3
  import {isChangingRemotely} from '../../internal-utils/withChanges'
4
4
  import {isRedoing, isUndoing} from '../../internal-utils/withUndoRedo'
5
5
  import type {PortableTextSlateEditor} from '../../types/editor'
6
- import type {SlateTextBlock, VoidElement} from '../../types/slate'
7
6
  import type {EditorActor} from '../editor-machine'
8
7
 
9
8
  const debug = debugWithName('plugin:withPlaceholderBlock')
@@ -45,22 +44,23 @@ export function createWithPlaceholderBlock(
45
44
  }
46
45
 
47
46
  if (op.type === 'remove_node') {
48
- const node = op.node as SlateTextBlock | VoidElement
49
- if (op.path[0] === 0 && Editor.isVoid(editor, node)) {
50
- // Check next path, if it exists, do nothing
51
- const nextPath = Path.next(op.path)
52
- // Is removing the first block which is a void (not a text block), add a new empty text block in it, if there is no other element in the next path
53
- if (!editor.children[nextPath[0]]) {
54
- debug('Adding placeholder block')
55
- Editor.insertNode(
56
- editor,
57
- editor.pteCreateTextBlock({decorators: []}),
58
- )
59
- }
47
+ const blockIndex = op.path.at(0)
48
+ const isLonelyBlock =
49
+ op.path.length === 1 &&
50
+ blockIndex === 0 &&
51
+ editor.children.length === 1
52
+ const isBlockObject =
53
+ op.node._type !== editorActor.getSnapshot().context.schema.block.name
54
+
55
+ if (isLonelyBlock && isBlockObject) {
56
+ debug('Adding placeholder block')
57
+ Editor.insertNode(editor, editor.pteCreateTextBlock({decorators: []}))
60
58
  }
61
59
  }
60
+
62
61
  apply(op)
63
62
  }
63
+
64
64
  return editor
65
65
  }
66
66
  }
@@ -12,7 +12,6 @@ import {
12
12
  assign,
13
13
  fromCallback,
14
14
  setup,
15
- type ActorRefFrom,
16
15
  type AnyEventObject,
17
16
  type CallbackLogicFunction,
18
17
  } from 'xstate'
@@ -42,18 +41,17 @@ const slateOperationCallback: CallbackLogicFunction<
42
41
  }
43
42
  }
44
43
 
45
- type DecoratedRange = BaseRange & {rangeDecoration: RangeDecoration}
44
+ export type DecoratedRange = BaseRange & {rangeDecoration: RangeDecoration}
46
45
 
47
46
  export const rangeDecorationsMachine = setup({
48
47
  types: {
49
48
  context: {} as {
50
- decoratedRanges: Array<DecoratedRange>
51
49
  pendingRangeDecorations: Array<RangeDecoration>
52
50
  skipSetup: boolean
53
51
  readOnly: boolean
54
52
  schema: EditorSchema
55
53
  slateEditor: PortableTextSlateEditor
56
- updateCount: number
54
+ decorate: {fn: (nodeEntry: NodeEntry) => Array<BaseRange>}
57
55
  },
58
56
  input: {} as {
59
57
  rangeDecorations: Array<RangeDecoration>
@@ -89,131 +87,126 @@ export const rangeDecorationsMachine = setup({
89
87
  return event.rangeDecorations
90
88
  },
91
89
  }),
92
- 'set up initial range decorations': assign({
93
- decoratedRanges: ({context}) => {
94
- const rangeDecorationState: Array<DecoratedRange> = []
95
-
96
- for (const rangeDecoration of context.pendingRangeDecorations) {
97
- const slateRange = toSlateRange(
98
- rangeDecoration.selection,
99
- context.slateEditor,
100
- )
101
-
102
- if (!Range.isRange(slateRange)) {
103
- rangeDecoration.onMoved?.({
104
- newSelection: null,
105
- rangeDecoration,
106
- origin: 'local',
107
- })
108
- continue
109
- }
90
+ 'set up initial range decorations': ({context}) => {
91
+ const rangeDecorationState: Array<DecoratedRange> = []
110
92
 
111
- rangeDecorationState.push({
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,
112
102
  rangeDecoration,
113
- ...slateRange,
103
+ origin: 'local',
114
104
  })
105
+ continue
115
106
  }
116
107
 
117
- return rangeDecorationState
118
- },
119
- }),
120
- 'update range decorations': assign({
121
- decoratedRanges: ({context, event}) => {
122
- if (event.type !== 'range decorations updated') {
123
- return context.decoratedRanges
124
- }
108
+ rangeDecorationState.push({
109
+ rangeDecoration,
110
+ ...slateRange,
111
+ })
112
+ }
125
113
 
126
- const rangeDecorationState: Array<DecoratedRange> = []
114
+ context.slateEditor.decoratedRanges = rangeDecorationState
115
+ },
116
+ 'update range decorations': ({context, event}) => {
117
+ if (event.type !== 'range decorations updated') {
118
+ return
119
+ }
127
120
 
128
- for (const rangeDecoration of event.rangeDecorations) {
129
- const slateRange = toSlateRange(
130
- rangeDecoration.selection,
131
- context.slateEditor,
132
- )
121
+ const rangeDecorationState: Array<DecoratedRange> = []
133
122
 
134
- if (!Range.isRange(slateRange)) {
135
- rangeDecoration.onMoved?.({
136
- newSelection: null,
137
- rangeDecoration,
138
- origin: 'local',
139
- })
140
- continue
141
- }
123
+ for (const rangeDecoration of event.rangeDecorations) {
124
+ const slateRange = toSlateRange(
125
+ rangeDecoration.selection,
126
+ context.slateEditor,
127
+ )
142
128
 
143
- rangeDecorationState.push({
129
+ if (!Range.isRange(slateRange)) {
130
+ rangeDecoration.onMoved?.({
131
+ newSelection: null,
144
132
  rangeDecoration,
145
- ...slateRange,
133
+ origin: 'local',
146
134
  })
135
+ continue
147
136
  }
148
137
 
149
- return rangeDecorationState
150
- },
151
- }),
152
- 'move range decorations': assign({
153
- decoratedRanges: ({context, event}) => {
154
- if (event.type !== 'slate operation') {
155
- return context.decoratedRanges
138
+ rangeDecorationState.push({
139
+ rangeDecoration,
140
+ ...slateRange,
141
+ })
142
+ }
143
+
144
+ context.slateEditor.decoratedRanges = rangeDecorationState
145
+ },
146
+
147
+ 'move range decorations': ({context, event}) => {
148
+ if (event.type !== 'slate operation') {
149
+ return
150
+ }
151
+
152
+ const rangeDecorationState: Array<DecoratedRange> = []
153
+
154
+ for (const decoratedRange of context.slateEditor.decoratedRanges) {
155
+ const slateRange = toSlateRange(
156
+ decoratedRange.rangeDecoration.selection,
157
+ context.slateEditor,
158
+ )
159
+
160
+ if (!Range.isRange(slateRange)) {
161
+ decoratedRange.rangeDecoration.onMoved?.({
162
+ newSelection: null,
163
+ rangeDecoration: decoratedRange.rangeDecoration,
164
+ origin: 'local',
165
+ })
166
+ continue
156
167
  }
157
168
 
158
- const rangeDecorationState: Array<DecoratedRange> = []
159
-
160
- for (const decoratedRange of context.decoratedRanges) {
161
- const slateRange = toSlateRange(
162
- decoratedRange.rangeDecoration.selection,
163
- context.slateEditor,
164
- )
165
-
166
- if (!Range.isRange(slateRange)) {
167
- decoratedRange.rangeDecoration.onMoved?.({
168
- newSelection: null,
169
- rangeDecoration: decoratedRange.rangeDecoration,
170
- origin: 'local',
171
- })
172
- continue
173
- }
174
-
175
- let newRange: BaseRange | null | undefined
176
-
177
- newRange = moveRangeByOperation(slateRange, event.operation)
178
- if (
179
- (newRange && newRange !== slateRange) ||
180
- (newRange === null && slateRange)
181
- ) {
182
- const newRangeSelection = newRange
183
- ? slateRangeToSelection({
184
- schema: context.schema,
185
- editor: context.slateEditor,
186
- range: newRange,
187
- })
188
- : null
189
-
190
- decoratedRange.rangeDecoration.onMoved?.({
191
- newSelection: newRangeSelection,
192
- rangeDecoration: decoratedRange.rangeDecoration,
193
- origin: 'local',
194
- })
195
- }
196
-
197
- // If the newRange is null, it means that the range is not valid anymore and should be removed
198
- // If it's undefined, it means that the slateRange is still valid and should be kept
199
- if (newRange !== null) {
200
- rangeDecorationState.push({
201
- ...(newRange || slateRange),
202
- rangeDecoration: {
203
- ...decoratedRange.rangeDecoration,
204
- selection: slateRangeToSelection({
205
- schema: context.schema,
206
- editor: context.slateEditor,
207
- range: newRange,
208
- }),
209
- },
210
- })
211
- }
169
+ let newRange: BaseRange | null | undefined
170
+
171
+ newRange = moveRangeByOperation(slateRange, event.operation)
172
+ if (
173
+ (newRange && newRange !== slateRange) ||
174
+ (newRange === null && slateRange)
175
+ ) {
176
+ const newRangeSelection = newRange
177
+ ? slateRangeToSelection({
178
+ schema: context.schema,
179
+ editor: context.slateEditor,
180
+ range: newRange,
181
+ })
182
+ : null
183
+
184
+ decoratedRange.rangeDecoration.onMoved?.({
185
+ newSelection: newRangeSelection,
186
+ rangeDecoration: decoratedRange.rangeDecoration,
187
+ origin: 'local',
188
+ })
212
189
  }
213
190
 
214
- return rangeDecorationState
215
- },
216
- }),
191
+ // If the newRange is null, it means that the range is not valid anymore and should be removed
192
+ // If it's undefined, it means that the slateRange is still valid and should be kept
193
+ if (newRange !== null) {
194
+ rangeDecorationState.push({
195
+ ...(newRange || slateRange),
196
+ rangeDecoration: {
197
+ ...decoratedRange.rangeDecoration,
198
+ selection: slateRangeToSelection({
199
+ schema: context.schema,
200
+ editor: context.slateEditor,
201
+ range: newRange,
202
+ }),
203
+ },
204
+ })
205
+ }
206
+ }
207
+
208
+ context.slateEditor.decoratedRanges = rangeDecorationState
209
+ },
217
210
  'assign readOnly': assign({
218
211
  readOnly: ({context, event}) => {
219
212
  if (event.type !== 'update read only') {
@@ -223,9 +216,11 @@ export const rangeDecorationsMachine = setup({
223
216
  return event.readOnly
224
217
  },
225
218
  }),
226
- 'increment update count': assign({
227
- updateCount: ({context}) => {
228
- return context.updateCount + 1
219
+ 'update decorate': assign({
220
+ decorate: ({context}) => {
221
+ return {
222
+ fn: createDecorate(context.schema, context.slateEditor),
223
+ }
229
224
  },
230
225
  }),
231
226
  },
@@ -235,13 +230,14 @@ export const rangeDecorationsMachine = setup({
235
230
  guards: {
236
231
  'has pending range decorations': ({context}) =>
237
232
  context.pendingRangeDecorations.length > 0,
238
- 'has range decorations': ({context}) => context.decoratedRanges.length > 0,
233
+ 'has range decorations': ({context}) =>
234
+ context.slateEditor.decoratedRanges.length > 0,
239
235
  'has different decorations': ({context, event}) => {
240
236
  if (event.type !== 'range decorations updated') {
241
237
  return false
242
238
  }
243
239
 
244
- const existingRangeDecorations = context.decoratedRanges.map(
240
+ const existingRangeDecorations = context.slateEditor.decoratedRanges.map(
245
241
  (decoratedRange) => ({
246
242
  anchor: decoratedRange.rangeDecoration.selection?.anchor,
247
243
  focus: decoratedRange.rangeDecoration.selection?.focus,
@@ -271,7 +267,7 @@ export const rangeDecorationsMachine = setup({
271
267
  skipSetup: input.skipSetup,
272
268
  schema: input.schema,
273
269
  slateEditor: input.slateEditor,
274
- updateCount: 0,
270
+ decorate: {fn: createDecorate(input.schema, input.slateEditor)},
275
271
  }),
276
272
  invoke: {
277
273
  src: 'slate operation listener',
@@ -289,10 +285,7 @@ export const rangeDecorationsMachine = setup({
289
285
  {
290
286
  guard: and(['should skip setup', 'has pending range decorations']),
291
287
  target: 'ready',
292
- actions: [
293
- 'set up initial range decorations',
294
- 'increment update count',
295
- ],
288
+ actions: ['set up initial range decorations', 'update decorate'],
296
289
  },
297
290
  {
298
291
  guard: 'should skip setup',
@@ -307,10 +300,7 @@ export const rangeDecorationsMachine = setup({
307
300
  {
308
301
  target: 'ready',
309
302
  guard: 'has pending range decorations',
310
- actions: [
311
- 'set up initial range decorations',
312
- 'increment update count',
313
- ],
303
+ actions: ['set up initial range decorations', 'update decorate'],
314
304
  },
315
305
  {
316
306
  target: 'ready',
@@ -324,7 +314,7 @@ export const rangeDecorationsMachine = setup({
324
314
  'range decorations updated': {
325
315
  target: '.idle',
326
316
  guard: 'has different decorations',
327
- actions: ['update range decorations', 'increment update count'],
317
+ actions: ['update range decorations', 'update decorate'],
328
318
  },
329
319
  },
330
320
  states: {
@@ -347,16 +337,12 @@ export const rangeDecorationsMachine = setup({
347
337
  },
348
338
  })
349
339
 
350
- export function createDecorate(
351
- rangeDecorationActor: ActorRefFrom<typeof rangeDecorationsMachine>,
340
+ function createDecorate(
341
+ schema: EditorSchema,
342
+ slateEditor: PortableTextSlateEditor,
352
343
  ) {
353
344
  return function decorate([node, path]: NodeEntry): Array<BaseRange> {
354
- if (
355
- isEqualToEmptyEditor(
356
- rangeDecorationActor.getSnapshot().context.slateEditor.children,
357
- rangeDecorationActor.getSnapshot().context.schema,
358
- )
359
- ) {
345
+ if (isEqualToEmptyEditor(slateEditor.children, schema)) {
360
346
  return [
361
347
  {
362
348
  anchor: {
@@ -387,28 +373,23 @@ export function createDecorate(
387
373
  return []
388
374
  }
389
375
 
390
- return rangeDecorationActor
391
- .getSnapshot()
392
- .context.decoratedRanges.filter((decoratedRange) => {
393
- // Special case in order to only return one decoration for collapsed ranges
394
- if (Range.isCollapsed(decoratedRange)) {
395
- // Collapsed ranges should only be decorated if they are on a block child level (length 2)
396
- return node.children.some(
397
- (_, childIndex) =>
398
- Path.equals(decoratedRange.anchor.path, [
399
- blockIndex,
400
- childIndex,
401
- ]) &&
402
- Path.equals(decoratedRange.focus.path, [blockIndex, childIndex]),
403
- )
404
- }
405
-
406
- return (
407
- Range.intersection(decoratedRange, {
408
- anchor: {path, offset: 0},
409
- focus: {path, offset: 0},
410
- }) || Range.includes(decoratedRange, path)
376
+ return slateEditor.decoratedRanges.filter((decoratedRange) => {
377
+ // Special case in order to only return one decoration for collapsed ranges
378
+ if (Range.isCollapsed(decoratedRange)) {
379
+ // Collapsed ranges should only be decorated if they are on a block child level (length 2)
380
+ return node.children.some(
381
+ (_, childIndex) =>
382
+ Path.equals(decoratedRange.anchor.path, [blockIndex, childIndex]) &&
383
+ Path.equals(decoratedRange.focus.path, [blockIndex, childIndex]),
411
384
  )
412
- })
385
+ }
386
+
387
+ return (
388
+ Range.intersection(decoratedRange, {
389
+ anchor: {path, offset: 0},
390
+ focus: {path, offset: 0},
391
+ }) || Range.includes(decoratedRange, path)
392
+ )
393
+ })
413
394
  }
414
395
  }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  export {
18
18
  defineSchema,
19
19
  type BaseDefinition,
20
+ type FieldDefinition,
20
21
  type SchemaDefinition,
21
22
  } from './editor/editor-schema'
22
23
  export type {EditorSchema} from './editor/editor-schema'
@@ -29,6 +29,7 @@ import type {DOMNode} from 'slate-dom'
29
29
  import type {ReactEditor} from 'slate-react'
30
30
  import type {PortableTextEditableProps} from '../editor/Editable'
31
31
  import type {PortableTextEditor} from '../editor/PortableTextEditor'
32
+ import type {DecoratedRange} from '../editor/range-decorations-machine'
32
33
  import type {MarkState} from '../internal-utils/mark-state'
33
34
  import type {BlockPath} from './paths'
34
35
 
@@ -126,6 +127,7 @@ export interface PortableTextSlateEditor extends ReactEditor {
126
127
  isTextSpan: (value: unknown) => value is PortableTextSpan
127
128
  isListBlock: (value: unknown) => value is PortableTextListBlock
128
129
  value: Array<PortableTextBlock>
130
+ decoratedRanges: Array<DecoratedRange>
129
131
  decoratorState: Record<string, boolean | undefined>
130
132
  markState: MarkState | undefined
131
133