@portabletext/editor 1.52.4 → 1.52.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.52.4",
3
+ "version": "1.52.6",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -80,8 +80,8 @@
80
80
  "slate-react": "0.114.2",
81
81
  "use-effect-event": "^1.0.2",
82
82
  "xstate": "^5.19.4",
83
- "@portabletext/block-tools": "1.1.30",
84
- "@portabletext/patches": "1.1.4"
83
+ "@portabletext/patches": "1.1.4",
84
+ "@portabletext/block-tools": "1.1.30"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@portabletext/toolkit": "^2.0.17",
@@ -534,24 +534,9 @@ export const PortableTextEditable = forwardRef<
534
534
 
535
535
  if (!event.isDefaultPrevented()) {
536
536
  relayActor.send({type: 'focused', event})
537
-
538
- const selection = slateEditor.selection
539
- ? slateRangeToSelection({
540
- schema: editorActor.getSnapshot().context.schema,
541
- editor: slateEditor,
542
- range: slateEditor.selection,
543
- })
544
- : null
545
-
546
- if (selection) {
547
- editorActor.send({
548
- type: 'update selection',
549
- selection,
550
- })
551
- }
552
537
  }
553
538
  },
554
- [editorActor, onFocus, slateEditor, relayActor],
539
+ [onFocus, relayActor],
555
540
  )
556
541
 
557
542
  const handleClick = useCallback(
@@ -1,10 +1,9 @@
1
- import {
2
- isPortableTextSpan,
3
- type Path,
4
- type PortableTextBlock,
5
- type PortableTextChild,
6
- type PortableTextObject,
7
- type PortableTextTextBlock,
1
+ import type {
2
+ Path,
3
+ PortableTextBlock,
4
+ PortableTextChild,
5
+ PortableTextObject,
6
+ PortableTextTextBlock,
8
7
  } from '@sanity/types'
9
8
  import {
10
9
  Editor,
@@ -29,6 +28,7 @@ import {
29
28
  SLATE_TO_PORTABLE_TEXT_RANGE,
30
29
  } from '../../internal-utils/weakMaps'
31
30
  import {addAnnotationOperationImplementation} from '../../operations/behavior.operation.annotation.add'
31
+ import {isActiveAnnotation} from '../../selectors'
32
32
  import type {
33
33
  EditableAPI,
34
34
  EditableAPIDeleteOptions,
@@ -396,7 +396,12 @@ export function createEditableAPI(
396
396
  isAnnotationActive: (
397
397
  annotationType: PortableTextObject['_type'],
398
398
  ): boolean => {
399
- return isAnnotationActive({editor, annotation: {name: annotationType}})
399
+ const snapshot = getEditorSnapshot({
400
+ editorActorSnapshot: editorActor.getSnapshot(),
401
+ slateEditorInstance: editor,
402
+ })
403
+
404
+ return isActiveAnnotation(annotationType)(snapshot)
400
405
  },
401
406
  addAnnotation: (type, value) => {
402
407
  let paths: ReturnType<EditableAPI['addAnnotation']> = undefined
@@ -546,59 +551,3 @@ export function createEditableAPI(
546
551
 
547
552
  return editableApi
548
553
  }
549
-
550
- function isAnnotationActive({
551
- editor,
552
- annotation,
553
- }: {
554
- editor: PortableTextSlateEditor
555
- annotation: {
556
- name: string
557
- }
558
- }) {
559
- if (!editor.selection || editor.selection.focus.path.length < 2) {
560
- return false
561
- }
562
-
563
- try {
564
- const spans = [
565
- ...Editor.nodes(editor, {
566
- at: editor.selection,
567
- match: (node) => Text.isText(node),
568
- }),
569
- ]
570
-
571
- if (spans.length === 0) {
572
- return false
573
- }
574
-
575
- if (
576
- spans.some(
577
- ([span]) =>
578
- !isPortableTextSpan(span) || !span.marks || span.marks?.length === 0,
579
- )
580
- )
581
- return false
582
-
583
- const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => {
584
- const [block] = Editor.node(editor, path, {depth: 1})
585
- if (editor.isTextBlock(block) && block.markDefs) {
586
- return [...accMarkDefs, ...block.markDefs]
587
- }
588
- return accMarkDefs
589
- }, [] as PortableTextObject[])
590
-
591
- return spans.every(([span]) => {
592
- if (!isPortableTextSpan(span)) return false
593
-
594
- const spanMarkDefs = span.marks?.map(
595
- (markKey) =>
596
- selectionMarkDefs.find((def) => def?._key === markKey)?._type,
597
- )
598
-
599
- return spanMarkDefs?.includes(annotation.name)
600
- })
601
- } catch {
602
- return false
603
- }
604
- }
@@ -0,0 +1,51 @@
1
+ import {slateRangeToSelection} from '../../internal-utils/slate-utils'
2
+ import {SLATE_TO_PORTABLE_TEXT_RANGE} from '../../internal-utils/weakMaps'
3
+ import type {PortableTextSlateEditor} from '../../types/editor'
4
+ import type {EditorActor} from '../editor-machine'
5
+
6
+ export function pluginUpdateSelection({
7
+ editor,
8
+ editorActor,
9
+ }: {
10
+ editor: PortableTextSlateEditor
11
+ editorActor: EditorActor
12
+ }) {
13
+ const updateSelection = () => {
14
+ if (editor.selection) {
15
+ const existingSelection = SLATE_TO_PORTABLE_TEXT_RANGE.get(
16
+ editor.selection,
17
+ )
18
+
19
+ if (existingSelection) {
20
+ editorActor.send({
21
+ type: 'update selection',
22
+ selection: existingSelection,
23
+ })
24
+ } else {
25
+ const selection = slateRangeToSelection({
26
+ schema: editorActor.getSnapshot().context.schema,
27
+ editor,
28
+ range: editor.selection,
29
+ })
30
+
31
+ SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, selection)
32
+
33
+ editorActor.send({type: 'update selection', selection})
34
+ }
35
+ } else {
36
+ editorActor.send({type: 'update selection', selection: null})
37
+ }
38
+ }
39
+
40
+ const {onChange} = editor
41
+
42
+ editor.onChange = () => {
43
+ onChange()
44
+
45
+ if (!editorActor.getSnapshot().matches({setup: 'setting up'})) {
46
+ updateSelection()
47
+ }
48
+ }
49
+
50
+ return editor
51
+ }
@@ -8,11 +8,11 @@ import {createWithObjectKeys} from './createWithObjectKeys'
8
8
  import {createWithPatches} from './createWithPatches'
9
9
  import {createWithPlaceholderBlock} from './createWithPlaceholderBlock'
10
10
  import {createWithPortableTextMarkModel} from './createWithPortableTextMarkModel'
11
- import {createWithPortableTextSelections} from './createWithPortableTextSelections'
12
11
  import {createWithSchemaTypes} from './createWithSchemaTypes'
13
12
  import {createWithUndoRedo} from './createWithUndoRedo'
14
13
  import {createWithUtils} from './createWithUtils'
15
14
  import {pluginUpdateMarkState} from './slate-plugin.update-mark-state'
15
+ import {pluginUpdateSelection} from './slate-plugin.update-selection'
16
16
  import {pluginUpdateValue} from './slate-plugin.update-value'
17
17
 
18
18
  export interface OriginalEditorFunctions {
@@ -54,8 +54,6 @@ export const withPlugins = <T extends Editor>(
54
54
  const withUtils = createWithUtils({
55
55
  editorActor,
56
56
  })
57
- const withPortableTextSelections =
58
- createWithPortableTextSelections(editorActor)
59
57
  const withEventListeners = createWithEventListeners(editorActor)
60
58
 
61
59
  // Ordering is important here, selection dealing last, data manipulation in the middle and core model stuff first.
@@ -68,13 +66,14 @@ export const withPlugins = <T extends Editor>(
68
66
  withMaxBlocks(
69
67
  withUndoRedo(
70
68
  withPatches(
71
- withPortableTextSelections(
72
- pluginUpdateValue(
69
+ pluginUpdateValue(
70
+ editorActor.getSnapshot().context,
71
+ pluginUpdateMarkState(
73
72
  editorActor.getSnapshot().context,
74
- pluginUpdateMarkState(
75
- editorActor.getSnapshot().context,
76
- e,
77
- ),
73
+ pluginUpdateSelection({
74
+ editorActor,
75
+ editor: e,
76
+ }),
78
77
  ),
79
78
  ),
80
79
  ),
@@ -1,7 +1,7 @@
1
1
  import type {Patch} from '@portabletext/patches'
2
2
  import type {PortableTextBlock} from '@sanity/types'
3
3
  import type {FocusEvent} from 'react'
4
- import {emit, setup, type ActorRefFrom} from 'xstate'
4
+ import {assign, emit, setup, type ActorRefFrom} from 'xstate'
5
5
  import type {EditorSelection, InvalidValueResolution} from '../types/editor'
6
6
 
7
7
  /**
@@ -96,14 +96,61 @@ export type RelayActor = ActorRefFrom<typeof relayMachine>
96
96
 
97
97
  export const relayMachine = setup({
98
98
  types: {
99
+ context: {} as {
100
+ prevSelection: EditorSelection
101
+ lastEventWasFocused: boolean
102
+ },
99
103
  events: {} as InternalEditorEmittedEvent,
100
104
  emitted: {} as InternalEditorEmittedEvent,
101
105
  },
102
106
  }).createMachine({
103
107
  id: 'relay',
108
+ context: {
109
+ prevSelection: null,
110
+ lastEventWasFocused: false,
111
+ },
104
112
  on: {
113
+ 'focused': {
114
+ actions: [
115
+ assign({
116
+ lastEventWasFocused: true,
117
+ }),
118
+ emit(({event}) => event),
119
+ ],
120
+ },
121
+ 'selection': [
122
+ {
123
+ guard: ({context}) => context.lastEventWasFocused,
124
+ actions: [
125
+ assign({
126
+ prevSelection: ({event}) => event.selection,
127
+ }),
128
+ emit(({event}) => event),
129
+ assign({
130
+ lastEventWasFocused: false,
131
+ }),
132
+ ],
133
+ },
134
+ {
135
+ guard: ({context, event}) => context.prevSelection !== event.selection,
136
+ actions: [
137
+ assign({
138
+ prevSelection: ({event}) => event.selection,
139
+ }),
140
+ emit(({event}) => event),
141
+ assign({
142
+ lastEventWasFocused: false,
143
+ }),
144
+ ],
145
+ },
146
+ ],
105
147
  '*': {
106
- actions: emit(({event}) => event),
148
+ actions: [
149
+ emit(({event}) => event),
150
+ assign({
151
+ lastEventWasFocused: false,
152
+ }),
153
+ ],
107
154
  },
108
155
  },
109
156
  })
@@ -1,8 +1,9 @@
1
1
  import {Range} from 'slate'
2
2
  import type {EditorSchema} from '../editor/editor-schema'
3
+ import {getSelectedSpans} from '../selectors'
3
4
  import type {PortableTextSlateEditor} from '../types/editor'
4
5
  import {getNextSpan, getPreviousSpan} from './sibling-utils'
5
- import {getFocusBlock, getFocusSpan, getSelectedSpans} from './slate-utils'
6
+ import {getFocusBlock, getFocusSpan, slateRangeToSelection} from './slate-utils'
6
7
 
7
8
  export type MarkState = {
8
9
  state: 'changed' | 'unchanged'
@@ -36,18 +37,39 @@ export function getMarkState({
36
37
  }
37
38
 
38
39
  if (Range.isExpanded(editor.selection)) {
39
- const selectedSpans = getSelectedSpans({editor})
40
+ const selection = editor.selection
41
+ ? slateRangeToSelection({
42
+ schema,
43
+ editor,
44
+ range: editor.selection,
45
+ })
46
+ : null
47
+
48
+ const selectedSpans = getSelectedSpans({
49
+ context: {
50
+ value: editor.value,
51
+ selection,
52
+ schema,
53
+ converters: [],
54
+ keyGenerator: () => '',
55
+ readOnly: false,
56
+ },
57
+ beta: {
58
+ activeAnnotations: [],
59
+ activeDecorators: [],
60
+ },
61
+ })
40
62
 
41
63
  let index = 0
42
64
  let marks: Array<string> = []
43
65
 
44
- for (const [span] of selectedSpans) {
66
+ for (const span of selectedSpans) {
45
67
  if (index === 0) {
46
- marks = span.marks ?? []
68
+ marks = span.node.marks ?? []
47
69
  } else {
48
70
  if (
49
- span.marks?.length === 0 ||
50
- (span.marks ?? [])?.some((mark) => !marks.includes(mark))
71
+ span.node.marks?.length === 0 ||
72
+ (span.node.marks ?? [])?.some((mark) => !marks.includes(mark))
51
73
  ) {
52
74
  marks = []
53
75
  }
@@ -91,27 +91,6 @@ export function getFocusSpan({
91
91
  return [undefined, undefined]
92
92
  }
93
93
 
94
- export function getSelectedSpans({
95
- editor,
96
- }: {
97
- editor: PortableTextSlateEditor
98
- }): Array<[node: PortableTextSpan, path: Path]> {
99
- if (!editor.selection) {
100
- return []
101
- }
102
-
103
- try {
104
- return Array.from(
105
- Editor.nodes(editor, {
106
- at: editor.selection,
107
- match: (node) => editor.isTextSpan(node),
108
- }),
109
- )
110
- } catch {
111
- return []
112
- }
113
- }
114
-
115
94
  export function getSelectionStartBlock({
116
95
  editor,
117
96
  }: {
@@ -1,60 +0,0 @@
1
- import type {BaseRange} from 'slate'
2
- import {debugWithName} from '../../internal-utils/debug'
3
- import {slateRangeToSelection} from '../../internal-utils/slate-utils'
4
- import {SLATE_TO_PORTABLE_TEXT_RANGE} from '../../internal-utils/weakMaps'
5
- import type {EditorSelection, PortableTextSlateEditor} from '../../types/editor'
6
- import type {EditorActor} from '../editor-machine'
7
-
8
- const debug = debugWithName('plugin:withPortableTextSelections')
9
- const debugVerbose = debug.enabled && false
10
-
11
- // This plugin will make sure that we emit a PT selection whenever the editor has changed.
12
- export function createWithPortableTextSelections(
13
- editorActor: EditorActor,
14
- ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
15
- let prevSelection: BaseRange | null = null
16
- return function withPortableTextSelections(
17
- editor: PortableTextSlateEditor,
18
- ): PortableTextSlateEditor {
19
- const emitPortableTextSelection = () => {
20
- if (prevSelection !== editor.selection) {
21
- let ptRange: EditorSelection | null = null
22
- if (editor.selection) {
23
- const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection)
24
- if (existing) {
25
- ptRange = existing
26
- } else {
27
- ptRange = slateRangeToSelection({
28
- schema: editorActor.getSnapshot().context.schema,
29
- editor,
30
- range: editor.selection,
31
- })
32
- SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
33
- }
34
- }
35
- if (debugVerbose) {
36
- debug(
37
- `Emitting selection ${JSON.stringify(ptRange || null)} (${JSON.stringify(
38
- editor.selection,
39
- )})`,
40
- )
41
- }
42
- if (ptRange) {
43
- editorActor.send({type: 'update selection', selection: ptRange})
44
- } else {
45
- editorActor.send({type: 'update selection', selection: null})
46
- }
47
- }
48
- prevSelection = editor.selection
49
- }
50
-
51
- const {onChange} = editor
52
- editor.onChange = () => {
53
- onChange()
54
- if (!editorActor.getSnapshot().matches({setup: 'setting up'})) {
55
- emitPortableTextSelection()
56
- }
57
- }
58
- return editor
59
- }
60
- }