@portabletext/editor 3.1.0 → 3.1.1

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": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -3,7 +3,6 @@ import {withReact} from 'slate-react'
3
3
  import {buildIndexMaps} from '../internal-utils/build-index-maps'
4
4
  import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
5
5
  import {debugWithName} from '../internal-utils/debug'
6
- import {toSlateBlock} from '../internal-utils/values'
7
6
  import type {PortableTextSlateEditor} from '../types/editor'
8
7
  import type {EditorActor} from './editor-machine'
9
8
  import {withPlugins} from './plugins/with-plugins'
@@ -56,15 +55,9 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
56
55
  },
57
56
  )
58
57
 
59
- const initialValue = [
60
- toSlateBlock(placeholderBlock, {
61
- schemaTypes: config.editorActor.getSnapshot().context.schema,
62
- }),
63
- ]
64
-
65
58
  const slateEditor: SlateEditor = {
66
59
  instance,
67
- initialValue,
60
+ initialValue: [placeholderBlock],
68
61
  }
69
62
 
70
63
  return slateEditor
@@ -110,6 +110,7 @@ export function createWithPatches({
110
110
  previousValue = editor.value
111
111
 
112
112
  const editorWasEmpty = isEqualToEmptyEditor(
113
+ editorActor.getSnapshot().context.initialValue,
113
114
  previousValue,
114
115
  editorActor.getSnapshot().context.schema,
115
116
  )
@@ -118,6 +119,7 @@ export function createWithPatches({
118
119
  apply(operation)
119
120
 
120
121
  const editorIsEmpty = isEqualToEmptyEditor(
122
+ editorActor.getSnapshot().context.initialValue,
121
123
  editor.value,
122
124
  editorActor.getSnapshot().context.schema,
123
125
  )
@@ -10,6 +10,7 @@ import {isEqual, uniq} from 'lodash'
10
10
  import {Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
11
11
  import {isRedoing} from '../../history/slate-plugin.redoing'
12
12
  import {isUndoing} from '../../history/slate-plugin.undoing'
13
+ import {createPlaceholderBlock} from '../../internal-utils/create-placeholder-block'
13
14
  import {debugWithName} from '../../internal-utils/debug'
14
15
  import {getNextSpan, getPreviousSpan} from '../../internal-utils/sibling-utils'
15
16
  import type {BehaviorOperationImplementation} from '../../operations/behavior.operations'
@@ -19,6 +20,7 @@ import type {EditorActor} from '../editor-machine'
19
20
  import {getEditorSnapshot} from '../editor-selector'
20
21
  import {withNormalizeNode} from '../with-normalizing-node'
21
22
  import {isChangingRemotely} from '../withChanges'
23
+ import {withoutPatching} from '../withoutPatching'
22
24
 
23
25
  const debug = debugWithName('plugin:withPortableTextMarkModel')
24
26
 
@@ -38,6 +40,18 @@ export function createWithPortableTextMarkModel(
38
40
  editor.normalizeNode = (nodeEntry) => {
39
41
  const [node, path] = nodeEntry
40
42
 
43
+ if (Editor.isEditor(node) && node.children.length === 0) {
44
+ withoutPatching(editor, () => {
45
+ withNormalizeNode(editor, () => {
46
+ Transforms.insertNodes(
47
+ editor,
48
+ createPlaceholderBlock(editorActor.getSnapshot().context),
49
+ {at: [0], select: true},
50
+ )
51
+ })
52
+ })
53
+ }
54
+
41
55
  if (editor.isTextBlock(node)) {
42
56
  const children = Node.children(editor, path)
43
57
 
@@ -6,7 +6,6 @@ import type {RelayActor} from '../relay-machine'
6
6
  import {createWithEventListeners} from './create-with-event-listeners'
7
7
  import {createWithObjectKeys} from './createWithObjectKeys'
8
8
  import {createWithPatches} from './createWithPatches'
9
- import {createWithPlaceholderBlock} from './createWithPlaceholderBlock'
10
9
  import {createWithPortableTextMarkModel} from './createWithPortableTextMarkModel'
11
10
  import {createWithSchemaTypes} from './createWithSchemaTypes'
12
11
  import {pluginUpdateSelection} from './slate-plugin.update-selection'
@@ -45,8 +44,6 @@ export const withPlugins = <T extends Editor>(
45
44
  })
46
45
  const withPortableTextMarkModel = createWithPortableTextMarkModel(editorActor)
47
46
 
48
- const withPlaceholderBlock = createWithPlaceholderBlock(editorActor)
49
-
50
47
  const withEventListeners = createWithEventListeners(editorActor)
51
48
 
52
49
  // Ordering is important here, selection dealing last, data manipulation in the middle and core model stuff first.
@@ -54,16 +51,14 @@ export const withPlugins = <T extends Editor>(
54
51
  withSchemaTypes(
55
52
  withObjectKeys(
56
53
  withPortableTextMarkModel(
57
- withPlaceholderBlock(
58
- withUndoRedo(
59
- withPatches(
60
- pluginUpdateValue(
61
- editorActor.getSnapshot().context,
62
- pluginUpdateSelection({
63
- editorActor,
64
- editor: e,
65
- }),
66
- ),
54
+ withUndoRedo(
55
+ withPatches(
56
+ pluginUpdateValue(
57
+ editorActor.getSnapshot().context,
58
+ pluginUpdateSelection({
59
+ editorActor,
60
+ editor: e,
61
+ }),
67
62
  ),
68
63
  ),
69
64
  ),
@@ -18,8 +18,8 @@ import {
18
18
  import {moveRangeByOperation} from '../internal-utils/move-range-by-operation'
19
19
  import {slateRangeToSelection} from '../internal-utils/slate-utils'
20
20
  import {toSlateRange} from '../internal-utils/to-slate-range'
21
- import {isEqualToEmptyEditor} from '../internal-utils/values'
22
21
  import type {PortableTextSlateEditor, RangeDecoration} from '../types/editor'
22
+ import {isEmptyTextBlock} from '../utils'
23
23
  import type {EditorSchema} from './editor-schema'
24
24
 
25
25
  const slateOperationCallback: CallbackLogicFunction<
@@ -282,7 +282,9 @@ export const rangeDecorationsMachine = setup({
282
282
  skipSetup: input.skipSetup,
283
283
  schema: input.schema,
284
284
  slateEditor: input.slateEditor,
285
- decorate: {fn: createDecorate(input.schema, input.slateEditor)},
285
+ decorate: {
286
+ fn: createDecorate(input.schema, input.slateEditor),
287
+ },
286
288
  }),
287
289
  invoke: {
288
290
  src: 'slate operation listener',
@@ -357,7 +359,15 @@ function createDecorate(
357
359
  slateEditor: PortableTextSlateEditor,
358
360
  ) {
359
361
  return function decorate([node, path]: NodeEntry): Array<BaseRange> {
360
- if (isEqualToEmptyEditor(slateEditor.value, schema)) {
362
+ const defaultStyle = schema.styles.at(0)?.name
363
+ const editorOnlyContainsEmptyParagraph =
364
+ slateEditor.value.length === 1 &&
365
+ isEmptyTextBlock({schema}, slateEditor.value[0]) &&
366
+ (!slateEditor.value[0].style ||
367
+ slateEditor.value[0].style === defaultStyle) &&
368
+ !slateEditor.value[0].listItem
369
+
370
+ if (editorOnlyContainsEmptyParagraph) {
361
371
  return [
362
372
  {
363
373
  anchor: {
@@ -17,7 +17,6 @@ import {
17
17
  type CallbackLogicFunction,
18
18
  } from 'xstate'
19
19
  import {pluginWithoutHistory} from '../history/slate-plugin.without-history'
20
- import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
21
20
  import {debugWithName} from '../internal-utils/debug'
22
21
  import {validateValue} from '../internal-utils/validateValue'
23
22
  import {toSlateBlock, VOID_CHILD_KEY} from '../internal-utils/values'
@@ -425,10 +424,8 @@ async function updateValue({
425
424
  debug('Value is empty')
426
425
 
427
426
  clearEditor({
428
- context,
429
427
  slateEditor,
430
428
  doneSyncing,
431
- hadSelection,
432
429
  })
433
430
 
434
431
  isChanged = true
@@ -575,18 +572,11 @@ async function* getStreamedBlocks({value}: {value: Array<PortableTextBlock>}) {
575
572
  * Remove all blocks and insert a placeholder block
576
573
  */
577
574
  function clearEditor({
578
- context,
579
575
  slateEditor,
580
576
  doneSyncing,
581
- hadSelection,
582
577
  }: {
583
- context: {
584
- keyGenerator: () => string
585
- schema: EditorSchema
586
- }
587
578
  slateEditor: PortableTextSlateEditor
588
579
  doneSyncing: boolean
589
- hadSelection: boolean
590
580
  }) {
591
581
  Editor.withoutNormalizing(slateEditor, () => {
592
582
  pluginWithoutHistory(slateEditor, () => {
@@ -596,10 +586,6 @@ function clearEditor({
596
586
  return
597
587
  }
598
588
 
599
- if (hadSelection) {
600
- Transforms.deselect(slateEditor)
601
- }
602
-
603
589
  const childrenLength = slateEditor.children.length
604
590
 
605
591
  slateEditor.children.forEach((_, index) => {
@@ -607,15 +593,6 @@ function clearEditor({
607
593
  at: [childrenLength - 1 - index],
608
594
  })
609
595
  })
610
-
611
- Transforms.insertNodes(slateEditor, createPlaceholderBlock(context), {
612
- at: [0],
613
- })
614
-
615
- // Add a new selection in the top of the document
616
- if (hadSelection) {
617
- Transforms.select(slateEditor, [0, 0])
618
- }
619
596
  })
620
597
  })
621
598
  })
@@ -21,14 +21,15 @@ import type {EditorContext} from '../editor/editor-snapshot'
21
21
  import {KEY_TO_SLATE_ELEMENT} from '../editor/weakMaps'
22
22
  import type {PortableTextSlateEditor} from '../types/editor'
23
23
  import {isKeyedSegment} from '../utils/util.is-keyed-segment'
24
- import {createPlaceholderBlock} from './create-placeholder-block'
25
24
  import {isEqualToEmptyEditor, toSlateBlock} from './values'
26
25
 
27
26
  /**
28
27
  * Creates a function that can apply a patch onto a PortableTextSlateEditor.
29
28
  */
30
29
  export function createApplyPatch(
31
- context: Pick<EditorContext, 'schema' | 'keyGenerator'>,
30
+ context: Pick<EditorContext, 'schema' | 'keyGenerator'> & {
31
+ initialValue: Array<PortableTextBlock> | undefined
32
+ },
32
33
  ): (editor: PortableTextSlateEditor, patch: Patch) => boolean {
33
34
  return (editor: PortableTextSlateEditor, patch: Patch): boolean => {
34
35
  let changed = false
@@ -39,7 +40,7 @@ export function createApplyPatch(
39
40
  changed = insertPatch(context, editor, patch)
40
41
  break
41
42
  case 'unset':
42
- changed = unsetPatch(context, editor, patch)
43
+ changed = unsetPatch(editor, patch)
43
44
  break
44
45
  case 'set':
45
46
  changed = setPatch(editor, patch)
@@ -118,13 +119,31 @@ function diffMatchPatch(
118
119
  }
119
120
 
120
121
  function insertPatch(
121
- context: Pick<EditorContext, 'schema' | 'keyGenerator'>,
122
+ context: Pick<EditorContext, 'schema' | 'keyGenerator'> & {
123
+ initialValue: Array<PortableTextBlock> | undefined
124
+ },
122
125
  editor: PortableTextSlateEditor,
123
126
  patch: InsertPatch,
124
127
  ) {
125
128
  const block = findBlock(editor.children, patch.path)
126
129
 
127
130
  if (!block) {
131
+ if (patch.path.length === 1 && patch.path[0] === 0) {
132
+ const blocksToInsert = patch.items.map((item) =>
133
+ toSlateBlock(
134
+ item as PortableTextBlock,
135
+ {schemaTypes: context.schema},
136
+ KEY_TO_SLATE_ELEMENT.get(editor),
137
+ ),
138
+ )
139
+
140
+ Transforms.insertNodes(editor, blocksToInsert, {
141
+ at: [0],
142
+ })
143
+
144
+ return true
145
+ }
146
+
128
147
  return false
129
148
  }
130
149
 
@@ -147,6 +166,7 @@ function insertPatch(
147
166
  position === 'after' ? targetBlockIndex + 1 : targetBlockIndex
148
167
 
149
168
  const editorWasEmptyBefore = isEqualToEmptyEditor(
169
+ context.initialValue,
150
170
  editor.value,
151
171
  context.schema,
152
172
  )
@@ -418,14 +438,9 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
418
438
  return true
419
439
  }
420
440
 
421
- function unsetPatch(
422
- context: Pick<EditorContext, 'keyGenerator' | 'schema'>,
423
- editor: PortableTextSlateEditor,
424
- patch: UnsetPatch,
425
- ) {
441
+ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch) {
426
442
  // Value
427
443
  if (patch.path.length === 0) {
428
- const previousSelection = editor.selection
429
444
  Transforms.deselect(editor)
430
445
 
431
446
  const children = Node.children(editor, [], {
@@ -436,15 +451,6 @@ function unsetPatch(
436
451
  Transforms.removeNodes(editor, {at: path})
437
452
  }
438
453
 
439
- Transforms.insertNodes(editor, createPlaceholderBlock(context))
440
- if (previousSelection) {
441
- Transforms.select(editor, {
442
- anchor: {path: [0, 0], offset: 0},
443
- focus: {path: [0, 0], offset: 0},
444
- })
445
- }
446
- // call OnChange here to emit the new selection
447
- editor.onChange()
448
454
  return true
449
455
  }
450
456
 
@@ -456,27 +462,6 @@ function unsetPatch(
456
462
 
457
463
  // Single blocks
458
464
  if (patch.path.length === 1) {
459
- if (editor.children.length === 1) {
460
- // `unset`ing the last block should be treated similar to `unset`ing the
461
- // entire editor value
462
- const previousSelection = editor.selection
463
-
464
- Transforms.deselect(editor)
465
- Transforms.removeNodes(editor, {at: [block.index]})
466
- Transforms.insertNodes(editor, createPlaceholderBlock(context))
467
-
468
- if (previousSelection) {
469
- Transforms.select(editor, {
470
- anchor: {path: [0, 0], offset: 0},
471
- focus: {path: [0, 0], offset: 0},
472
- })
473
- }
474
-
475
- editor.onChange()
476
-
477
- return true
478
- }
479
-
480
465
  Transforms.removeNodes(editor, {at: [block.index]})
481
466
 
482
467
  return true
@@ -169,9 +169,14 @@ export function fromSlateBlock(
169
169
  }
170
170
 
171
171
  export function isEqualToEmptyEditor(
172
+ initialValue: Array<PortableTextBlock> | undefined,
172
173
  blocks: Array<Descendant> | Array<PortableTextBlock>,
173
174
  schemaTypes: EditorSchema,
174
175
  ): boolean {
176
+ if (!blocks) {
177
+ return false
178
+ }
179
+
175
180
  // Must have exactly one block
176
181
  if (blocks.length !== 1) {
177
182
  return false
@@ -240,5 +245,22 @@ export function isEqualToEmptyEditor(
240
245
  return false
241
246
  }
242
247
 
248
+ if (
249
+ Object.keys(firstBlock).some(
250
+ (key) =>
251
+ key !== '_type' &&
252
+ key !== '_key' &&
253
+ key !== 'children' &&
254
+ key !== 'markDefs' &&
255
+ key !== 'style',
256
+ )
257
+ ) {
258
+ return false
259
+ }
260
+
261
+ if (isEqual(initialValue, [firstBlock])) {
262
+ return false
263
+ }
264
+
243
265
  return true
244
266
  }
@@ -1,7 +1,6 @@
1
1
  import {isSpan, isTextBlock} from '@portabletext/schema'
2
2
  import {deleteText, Editor, Element, Range, Transforms} from 'slate'
3
3
  import {DOMEditor} from 'slate-dom'
4
- import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
5
4
  import {slateRangeToSelection} from '../internal-utils/slate-utils'
6
5
  import {toSlateRange} from '../internal-utils/to-slate-range'
7
6
  import {VOID_CHILD_KEY} from '../internal-utils/values'
@@ -68,10 +67,6 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
68
67
  mode: 'highest',
69
68
  })
70
69
 
71
- if (operation.editor.children.length === 0) {
72
- Transforms.insertNodes(operation.editor, createPlaceholderBlock(context))
73
- }
74
-
75
70
  return
76
71
  }
77
72
 
@@ -10,6 +10,7 @@ import {
10
10
  type Descendant,
11
11
  } from 'slate'
12
12
  import {DOMEditor} from 'slate-dom'
13
+ import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
13
14
  import {getFocusBlock, getFocusChild} from '../internal-utils/slate-utils'
14
15
  import {toSlateRange} from '../internal-utils/to-slate-range'
15
16
  import {toSlateBlock} from '../internal-utils/values'
@@ -70,6 +71,10 @@ export function insertBlock(options: {
70
71
  })
71
72
  : editor.selection
72
73
 
74
+ if (editor.children.length === 0) {
75
+ Transforms.insertNodes(editor, createPlaceholderBlock(context), {at: [0]})
76
+ }
77
+
73
78
  // Fall back to the start and end of the editor if neither an editor
74
79
  // selection nor an `at` range is provided
75
80
  const start = at ? Range.start(at) : Editor.start(editor, [])
@@ -1,71 +0,0 @@
1
- import {Editor} from 'slate'
2
- import {isRedoing} from '../../history/slate-plugin.redoing'
3
- import {isUndoing} from '../../history/slate-plugin.undoing'
4
- import {createPlaceholderBlock} from '../../internal-utils/create-placeholder-block'
5
- import {debugWithName} from '../../internal-utils/debug'
6
- import type {PortableTextSlateEditor} from '../../types/editor'
7
- import type {EditorActor} from '../editor-machine'
8
- import {isChangingRemotely} from '../withChanges'
9
-
10
- const debug = debugWithName('plugin:withPlaceholderBlock')
11
-
12
- /**
13
- * Keep a "placeholder" block present when the editor is empty
14
- *
15
- */
16
- export function createWithPlaceholderBlock(
17
- editorActor: EditorActor,
18
- ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
19
- return function withPlaceholderBlock(
20
- editor: PortableTextSlateEditor,
21
- ): PortableTextSlateEditor {
22
- const {apply} = editor
23
-
24
- editor.apply = (op) => {
25
- if (editorActor.getSnapshot().matches({'edit mode': 'read only'})) {
26
- apply(op)
27
- return
28
- }
29
-
30
- /**
31
- * We don't want to run any side effects when the editor is processing
32
- * remote changes.
33
- */
34
- if (isChangingRemotely(editor)) {
35
- apply(op)
36
- return
37
- }
38
-
39
- /**
40
- * We don't want to run any side effects when the editor is undoing or
41
- * redoing operations.
42
- */
43
- if (isUndoing(editor) || isRedoing(editor)) {
44
- apply(op)
45
- return
46
- }
47
-
48
- if (op.type === 'remove_node') {
49
- const blockIndex = op.path.at(0)
50
- const isLonelyBlock =
51
- op.path.length === 1 &&
52
- blockIndex === 0 &&
53
- editor.children.length === 1
54
- const isBlockObject =
55
- op.node._type !== editorActor.getSnapshot().context.schema.block.name
56
-
57
- if (isLonelyBlock && isBlockObject) {
58
- debug('Adding placeholder block')
59
- Editor.insertNode(
60
- editor,
61
- createPlaceholderBlock(editorActor.getSnapshot().context),
62
- )
63
- }
64
- }
65
-
66
- apply(op)
67
- }
68
-
69
- return editor
70
- }
71
- }