@portabletext/editor 1.7.0 → 1.8.0

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.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@portabletext/patches": "1.1.0",
46
- "@xstate/react": "^4.1.3",
46
+ "@xstate/react": "^5.0.0",
47
47
  "debug": "^4.3.4",
48
48
  "get-random-values-esm": "^1.0.2",
49
49
  "is-hotkey-esm": "^1.0.0",
@@ -54,15 +54,15 @@
54
54
  "slate-dom": "^0.111.0",
55
55
  "slate-react": "0.111.0",
56
56
  "use-effect-event": "^1.0.2",
57
- "xstate": "^5.18.2"
57
+ "xstate": "^5.19.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@portabletext/toolkit": "^2.0.16",
61
- "@sanity/block-tools": "^3.63.0",
61
+ "@sanity/block-tools": "^3.64.0",
62
62
  "@sanity/diff-match-patch": "^3.1.1",
63
63
  "@sanity/pkg-utils": "^6.11.10",
64
- "@sanity/schema": "^3.63.0",
65
- "@sanity/types": "^3.63.0",
64
+ "@sanity/schema": "^3.64.0",
65
+ "@sanity/types": "^3.64.0",
66
66
  "@testing-library/jest-dom": "^6.6.3",
67
67
  "@testing-library/react": "^16.0.1",
68
68
  "@types/debug": "^4.1.5",
@@ -90,9 +90,9 @@
90
90
  "@sanity/gherkin-driver": "^0.0.1"
91
91
  },
92
92
  "peerDependencies": {
93
- "@sanity/block-tools": "^3.63.0",
94
- "@sanity/schema": "^3.63.0",
95
- "@sanity/types": "^3.63.0",
93
+ "@sanity/block-tools": "^3.64.0",
94
+ "@sanity/schema": "^3.64.0",
95
+ "@sanity/types": "^3.64.0",
96
96
  "react": "^16.9 || ^17 || ^18",
97
97
  "rxjs": "^7.8.1",
98
98
  "styled-components": "^6.1.13"
@@ -136,6 +136,7 @@ export class PortableTextEditor extends Component<
136
136
  if (props.editor) {
137
137
  const editor = props.editor as Editor
138
138
  this.editorActor = editor._internal.editorActor
139
+ this.slateEditor = editor._internal.slateEditor
139
140
  this.editorActor.start()
140
141
  this.schemaTypes = this.editorActor.getSnapshot().context.schema
141
142
  } else {
@@ -163,6 +164,10 @@ export class PortableTextEditor extends Component<
163
164
  })
164
165
  this.editorActor.start()
165
166
 
167
+ this.slateEditor = createSlateEditor({
168
+ editorActor: this.editorActor,
169
+ })
170
+
166
171
  if (props.readOnly) {
167
172
  this.editorActor.send({
168
173
  type: 'toggle readOnly',
@@ -179,9 +184,6 @@ export class PortableTextEditor extends Component<
179
184
  })
180
185
  }
181
186
  }
182
- this.slateEditor = createSlateEditor({
183
- editorActor: this.editorActor,
184
- })
185
187
  this.editable = createEditableAPI(
186
188
  this.slateEditor.instance,
187
189
  this.editorActor,
@@ -230,10 +232,6 @@ export class PortableTextEditor extends Component<
230
232
  }
231
233
  }
232
234
 
233
- componentWillUnmount(): void {
234
- this.slateEditor.destroy()
235
- }
236
-
237
235
  public setEditable = (editable: EditableAPI) => {
238
236
  this.editable = {...this.editable, ...editable}
239
237
  }
@@ -10,6 +10,7 @@ import type {PortableTextMemberSchemaTypes} from '../../types/editor'
10
10
  import {toSlateRange} from '../../utils/ranges'
11
11
  import {
12
12
  addAnnotationActionImplementation,
13
+ insertBlockObjectActionImplementation,
13
14
  removeAnnotationActionImplementation,
14
15
  toggleAnnotationActionImplementation,
15
16
  } from '../plugins/createWithEditableAPI'
@@ -112,6 +113,7 @@ const behaviorActionImplementations: BehaviourActionImplementations = {
112
113
  })
113
114
  }
114
115
  },
116
+ 'insert block object': insertBlockObjectActionImplementation,
115
117
  'insert break': insertBreakActionImplementation,
116
118
  'insert soft break': insertSoftBreakActionImplementation,
117
119
  'insert text': ({action}) => {
@@ -169,6 +171,13 @@ export function performAction({
169
171
  })
170
172
  break
171
173
  }
174
+ case 'insert block object': {
175
+ behaviorActionImplementations['insert block object']({
176
+ context,
177
+ action,
178
+ })
179
+ break
180
+ }
172
181
  case 'insert text block': {
173
182
  behaviorActionImplementations['insert text block']({
174
183
  context,
@@ -1,3 +1,4 @@
1
+ import {isPortableTextSpan} from '@portabletext/toolkit'
1
2
  import type {PortableTextMemberSchemaTypes} from '../../types/editor'
2
3
  import {defineBehavior} from './behavior.types'
3
4
  import {
@@ -10,18 +11,23 @@ import {
10
11
  * @alpha
11
12
  */
12
13
  export type MarkdownBehaviorsConfig = {
13
- mapDefaultStyle: (schema: PortableTextMemberSchemaTypes) => string | undefined
14
- mapHeadingStyle: (
14
+ mapBreakObject?: (
15
+ schema: PortableTextMemberSchemaTypes,
16
+ ) => {name: string; value?: {[prop: string]: unknown}} | undefined
17
+ mapDefaultStyle?: (
18
+ schema: PortableTextMemberSchemaTypes,
19
+ ) => string | undefined
20
+ mapHeadingStyle?: (
15
21
  schema: PortableTextMemberSchemaTypes,
16
22
  level: number,
17
23
  ) => string | undefined
18
- mapBlockquoteStyle: (
24
+ mapBlockquoteStyle?: (
19
25
  schema: PortableTextMemberSchemaTypes,
20
26
  ) => string | undefined
21
- mapUnorderedListStyle: (
27
+ mapUnorderedListStyle?: (
22
28
  schema: PortableTextMemberSchemaTypes,
23
29
  ) => string | undefined
24
- mapOrderedListStyle: (
30
+ mapOrderedListStyle?: (
25
31
  schema: PortableTextMemberSchemaTypes,
26
32
  ) => string | undefined
27
33
  }
@@ -49,7 +55,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
49
55
 
50
56
  const caretAtTheEndOfQuote = context.selection.focus.offset === 1
51
57
  const looksLikeMarkdownQuote = /^>/.test(focusSpan.node.text)
52
- const blockquoteStyle = config.mapBlockquoteStyle(context.schema)
58
+ const blockquoteStyle = config.mapBlockquoteStyle?.(context.schema)
53
59
 
54
60
  if (
55
61
  caretAtTheEndOfQuote &&
@@ -95,6 +101,66 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
95
101
  ],
96
102
  ],
97
103
  })
104
+ const automaticBreak = defineBehavior({
105
+ on: 'insert text',
106
+ guard: ({context, event}) => {
107
+ const isDash = event.text === '-'
108
+
109
+ if (!isDash) {
110
+ return false
111
+ }
112
+
113
+ const breakObject = config.mapBreakObject?.(context.schema)
114
+ const focusBlock = getFocusTextBlock(context)
115
+ const selectionCollapsed = selectionIsCollapsed(context)
116
+
117
+ if (!breakObject || !focusBlock || !selectionCollapsed) {
118
+ return false
119
+ }
120
+
121
+ const onlyText = focusBlock.node.children.every(isPortableTextSpan)
122
+ const blockText = focusBlock.node.children
123
+ .map((child) => child.text ?? '')
124
+ .join('')
125
+
126
+ if (onlyText && blockText === '--') {
127
+ return {breakObject, focusBlock}
128
+ }
129
+
130
+ return false
131
+ },
132
+ actions: [
133
+ () => [
134
+ {
135
+ type: 'insert text',
136
+ text: '-',
137
+ },
138
+ ],
139
+ (_, {breakObject, focusBlock}) => [
140
+ {
141
+ type: 'insert block object',
142
+ ...breakObject,
143
+ },
144
+ {
145
+ type: 'delete',
146
+ selection: {
147
+ anchor: {
148
+ path: focusBlock.path,
149
+ offset: 0,
150
+ },
151
+ focus: {
152
+ path: focusBlock.path,
153
+ offset: 0,
154
+ },
155
+ },
156
+ },
157
+ {
158
+ type: 'insert text block',
159
+ decorators: [],
160
+ },
161
+ ],
162
+ ],
163
+ })
98
164
  const automaticHeadingOnSpace = defineBehavior({
99
165
  on: 'insert text',
100
166
  guard: ({context, event}) => {
@@ -125,7 +191,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
125
191
 
126
192
  const headingStyle =
127
193
  headingLevel !== undefined
128
- ? config.mapHeadingStyle(context.schema, headingLevel)
194
+ ? config.mapHeadingStyle?.(context.schema, headingLevel)
129
195
  : undefined
130
196
 
131
197
  if (headingLevel !== undefined && headingStyle !== undefined) {
@@ -188,7 +254,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
188
254
  focusTextBlock.node.children[0]._key === focusSpan.node._key &&
189
255
  context.selection.focus.offset === 0
190
256
 
191
- const defaultStyle = config.mapDefaultStyle(context.schema)
257
+ const defaultStyle = config.mapDefaultStyle?.(context.schema)
192
258
 
193
259
  if (
194
260
  atTheBeginningOfBLock &&
@@ -227,9 +293,9 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
227
293
  return false
228
294
  }
229
295
 
230
- const defaultStyle = config.mapDefaultStyle(context.schema)
296
+ const defaultStyle = config.mapDefaultStyle?.(context.schema)
231
297
  const looksLikeUnorderedList = /^(-|\*)/.test(focusSpan.node.text)
232
- const unorderedListStyle = config.mapUnorderedListStyle(context.schema)
298
+ const unorderedListStyle = config.mapUnorderedListStyle?.(context.schema)
233
299
  const caretAtTheEndOfUnorderedList = context.selection.focus.offset === 1
234
300
 
235
301
  if (
@@ -248,7 +314,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
248
314
  }
249
315
 
250
316
  const looksLikeOrderedList = /^1./.test(focusSpan.node.text)
251
- const orderedListStyle = config.mapOrderedListStyle(context.schema)
317
+ const orderedListStyle = config.mapOrderedListStyle?.(context.schema)
252
318
  const caretAtTheEndOfOrderedList = context.selection.focus.offset === 2
253
319
 
254
320
  if (
@@ -302,6 +368,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
302
368
 
303
369
  const markdownBehaviors = [
304
370
  automaticBlockquoteOnSpace,
371
+ automaticBreak,
305
372
  automaticHeadingOnSpace,
306
373
  clearStyleOnBackspace,
307
374
  automaticListOnSpace,
@@ -94,6 +94,11 @@ export type BehaviorGuard<
94
94
  */
95
95
  export type BehaviorActionIntend =
96
96
  | BehaviorEvent
97
+ | {
98
+ type: 'insert block object'
99
+ name: string
100
+ value?: {[prop: string]: unknown}
101
+ }
97
102
  | {
98
103
  type: 'insert text block'
99
104
  decorators: Array<string>
@@ -12,10 +12,12 @@ type SlateEditorConfig = {
12
12
  editorActor: EditorActor
13
13
  }
14
14
 
15
+ /**
16
+ * @internal
17
+ */
15
18
  export type SlateEditor = {
16
19
  instance: PortableTextSlateEditor
17
20
  initialValue: Array<Descendant>
18
- destroy: () => void
19
21
  }
20
22
 
21
23
  const slateEditors = new WeakMap<EditorActor, SlateEditor>()
@@ -45,12 +47,8 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
45
47
  unsubscriptions.push(subscription())
46
48
  }
47
49
 
48
- const initialValue = [instance.pteCreateTextBlock({decorators: []})]
49
-
50
- const slateEditor: SlateEditor = {
51
- instance,
52
- initialValue,
53
- destroy: () => {
50
+ config.editorActor.subscribe((snapshot) => {
51
+ if (snapshot.status !== 'active') {
54
52
  debug('Destroying Slate editor')
55
53
  instance.destroy()
56
54
  for (const unsubscribe of unsubscriptions) {
@@ -58,7 +56,14 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
58
56
  }
59
57
  subscriptions = []
60
58
  unsubscriptions = []
61
- },
59
+ }
60
+ })
61
+
62
+ const initialValue = [instance.pteCreateTextBlock({decorators: []})]
63
+
64
+ const slateEditor: SlateEditor = {
65
+ instance,
66
+ initialValue,
62
67
  }
63
68
 
64
69
  slateEditors.set(config.editorActor, slateEditor)
@@ -206,77 +206,18 @@ export function createEditableAPI(
206
206
  type: TSchemaType,
207
207
  value?: {[prop: string]: any},
208
208
  ): Path => {
209
- const block = toSlateValue(
210
- [
211
- {
212
- _key: editorActor.getSnapshot().context.keyGenerator(),
213
- _type: type.name,
214
- ...(value ? value : {}),
215
- },
216
- ],
217
- {schemaTypes: editorActor.getSnapshot().context.schema},
218
- )[0] as unknown as Node
219
-
220
- if (!editor.selection) {
221
- const lastBlock = Array.from(
222
- Editor.nodes(editor, {
223
- match: (n) => !Editor.isEditor(n),
224
- at: [],
225
- reverse: true,
226
- }),
227
- )[0]
228
-
229
- // If there is no selection, let's just insert the new block at the
230
- // end of the document
231
- Editor.insertNode(editor, block)
232
-
233
- if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
234
- // And if the last block was an empty text block, let's remove
235
- // that too
236
- Transforms.removeNodes(editor, {at: lastBlock[1]})
237
- }
238
-
239
- editor.onChange()
240
-
241
- return (
242
- toPortableTextRange(
243
- fromSlateValue(
244
- editor.children,
245
- types.block.name,
246
- KEY_TO_VALUE_ELEMENT.get(editor),
247
- ),
248
- editor.selection,
249
- types,
250
- )?.focus.path ?? []
251
- )
252
- }
253
-
254
- const focusBlock = Array.from(
255
- Editor.nodes(editor, {
256
- at: editor.selection.focus.path.slice(0, 1),
257
- match: (n) => n._type === types.block.name,
258
- }),
259
- )[0]
260
-
261
- Editor.insertNode(editor, block)
262
-
263
- if (focusBlock && isEqualToEmptyEditor([focusBlock[0]], types)) {
264
- Transforms.removeNodes(editor, {at: focusBlock[1]})
265
- }
266
-
267
- editor.onChange()
268
-
269
- return (
270
- toPortableTextRange(
271
- fromSlateValue(
272
- editor.children,
273
- types.block.name,
274
- KEY_TO_VALUE_ELEMENT.get(editor),
275
- ),
276
- editor.selection,
277
- types,
278
- )?.focus.path || []
279
- )
209
+ return insertBlockObjectActionImplementation({
210
+ context: {
211
+ keyGenerator: editorActor.getSnapshot().context.keyGenerator,
212
+ schema: types,
213
+ },
214
+ action: {
215
+ type: 'insert block object',
216
+ name: type.name,
217
+ value,
218
+ editor,
219
+ },
220
+ })
280
221
  },
281
222
  hasBlockStyle: (style: string): boolean => {
282
223
  try {
@@ -546,6 +487,85 @@ export function createEditableAPI(
546
487
  return editableApi
547
488
  }
548
489
 
490
+ export const insertBlockObjectActionImplementation: BehaviourActionImplementation<
491
+ 'insert block object',
492
+ Path
493
+ > = ({context, action}) => {
494
+ const editor = action.editor
495
+ const types = context.schema
496
+ const block = toSlateValue(
497
+ [
498
+ {
499
+ _key: context.keyGenerator(),
500
+ _type: action.name,
501
+ ...(action.value ? action.value : {}),
502
+ },
503
+ ],
504
+ {schemaTypes: context.schema},
505
+ )[0] as unknown as Node
506
+
507
+ if (!editor.selection) {
508
+ const lastBlock = Array.from(
509
+ Editor.nodes(editor, {
510
+ match: (n) => !Editor.isEditor(n),
511
+ at: [],
512
+ reverse: true,
513
+ }),
514
+ )[0]
515
+
516
+ // If there is no selection, let's just insert the new block at the
517
+ // end of the document
518
+ Editor.insertNode(editor, block)
519
+
520
+ if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
521
+ // And if the last block was an empty text block, let's remove
522
+ // that too
523
+ Transforms.removeNodes(editor, {at: lastBlock[1]})
524
+ }
525
+
526
+ editor.onChange()
527
+
528
+ return (
529
+ toPortableTextRange(
530
+ fromSlateValue(
531
+ editor.children,
532
+ types.block.name,
533
+ KEY_TO_VALUE_ELEMENT.get(editor),
534
+ ),
535
+ editor.selection,
536
+ types,
537
+ )?.focus.path ?? []
538
+ )
539
+ }
540
+
541
+ const focusBlock = Array.from(
542
+ Editor.nodes(editor, {
543
+ at: editor.selection.focus.path.slice(0, 1),
544
+ match: (n) => n._type === types.block.name,
545
+ }),
546
+ )[0]
547
+
548
+ Editor.insertNode(editor, block)
549
+
550
+ if (focusBlock && isEqualToEmptyEditor([focusBlock[0]], types)) {
551
+ Transforms.removeNodes(editor, {at: focusBlock[1]})
552
+ }
553
+
554
+ editor.onChange()
555
+
556
+ return (
557
+ toPortableTextRange(
558
+ fromSlateValue(
559
+ editor.children,
560
+ types.block.name,
561
+ KEY_TO_VALUE_ELEMENT.get(editor),
562
+ ),
563
+ editor.selection,
564
+ types,
565
+ )?.focus.path || []
566
+ )
567
+ }
568
+
549
569
  function isAnnotationActive({
550
570
  editor,
551
571
  annotation,
@@ -1,7 +1,7 @@
1
1
  import type {BaseOperation, Editor, Node, NodeEntry} from 'slate'
2
2
  import type {PortableTextSlateEditor} from '../../types/editor'
3
- import type {createEditorOptions} from '../../types/options'
4
3
  import {createOperationToPatches} from '../../utils/operationToPatches'
4
+ import type {EditorActor} from '../editor-machine'
5
5
  import {createWithEventListeners} from './create-with-event-listeners'
6
6
  import {createWithMaxBlocks} from './createWithMaxBlocks'
7
7
  import {createWithObjectKeys} from './createWithObjectKeys'
@@ -26,9 +26,14 @@ const originalFnMap = new WeakMap<
26
26
  OriginalEditorFunctions
27
27
  >()
28
28
 
29
+ type PluginsOptions = {
30
+ editorActor: EditorActor
31
+ subscriptions: Array<() => () => void>
32
+ }
33
+
29
34
  export const withPlugins = <T extends Editor>(
30
35
  editor: T,
31
- options: createEditorOptions,
36
+ options: PluginsOptions,
32
37
  ): PortableTextSlateEditor => {
33
38
  const e = editor as T & PortableTextSlateEditor
34
39
  const {editorActor} = options
@@ -7,6 +7,7 @@ import {useActorRef, useSelector} from '@xstate/react'
7
7
  import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
8
8
  import {compileType} from '../utils/schema'
9
9
  import type {Behavior, PickFromUnion} from './behavior/behavior.types'
10
+ import {createSlateEditor, type SlateEditor} from './create-slate-editor'
10
11
  import {compileSchemaDefinition, type SchemaDefinition} from './define-schema'
11
12
  import {
12
13
  editorMachine,
@@ -54,6 +55,7 @@ export type Editor = {
54
55
  readOnly: boolean
55
56
  _internal: {
56
57
  editorActor: EditorActor
58
+ slateEditor: SlateEditor
57
59
  }
58
60
  }
59
61
 
@@ -74,6 +76,7 @@ export function useEditor(config: EditorConfig): Editor {
74
76
  ),
75
77
  },
76
78
  })
79
+ const slateEditor = createSlateEditor({editorActor})
77
80
  const readOnly = useSelector(editorActor, (s) => s.context.readOnly)
78
81
 
79
82
  return {
@@ -84,6 +87,7 @@ export function useEditor(config: EditorConfig): Editor {
84
87
  readOnly,
85
88
  _internal: {
86
89
  editorActor,
90
+ slateEditor,
87
91
  },
88
92
  }
89
93
  }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  type BehaviorGuard,
16
16
  type PickFromUnion,
17
17
  } from './editor/behavior/behavior.types'
18
+ export type {SlateEditor} from './editor/create-slate-editor'
18
19
  export {
19
20
  defineSchema,
20
21
  type BaseDefinition,
@@ -44,4 +45,4 @@ export {
44
45
  type EditorEvent,
45
46
  } from './editor/use-editor'
46
47
  export * from './types/editor'
47
- export * from './types/options'
48
+ export type {HotkeyOptions} from './types/options'
@@ -1,15 +1,6 @@
1
1
  import type {BaseSyntheticEvent} from 'react'
2
- import type {EditorActor} from '../editor/editor-machine'
3
2
  import type {PortableTextEditor} from '../editor/PortableTextEditor'
4
3
 
5
- /**
6
- * @internal
7
- */
8
- export type createEditorOptions = {
9
- editorActor: EditorActor
10
- subscriptions: Array<() => () => void>
11
- }
12
-
13
4
  /**
14
5
  * @beta
15
6
  */
@@ -2,9 +2,9 @@ import type {PortableTextTextBlock} from '@sanity/types'
2
2
  import {createEditor, type Descendant} from 'slate'
3
3
  import {beforeEach, describe, expect, it} from 'vitest'
4
4
  import {createActor} from 'xstate'
5
- import {editorMachine} from '../..'
6
5
  import {schemaType} from '../../editor/__tests__/PortableTextEditorTester'
7
6
  import {coreBehaviors} from '../../editor/behavior/behavior.core'
7
+ import {editorMachine} from '../../editor/editor-machine'
8
8
  import {defaultKeyGenerator} from '../../editor/key-generator'
9
9
  import {withPlugins} from '../../editor/plugins/with-plugins'
10
10
  import {getPortableTextMemberSchemaTypes} from '../getPortableTextMemberSchemaTypes'
@@ -2,9 +2,9 @@ import type {Patch} from '@portabletext/patches'
2
2
  import {createEditor, type Descendant} from 'slate'
3
3
  import {beforeEach, describe, expect, it} from 'vitest'
4
4
  import {createActor} from 'xstate'
5
- import {editorMachine} from '../..'
6
5
  import {schemaType} from '../../editor/__tests__/PortableTextEditorTester'
7
6
  import {coreBehaviors} from '../../editor/behavior/behavior.core'
7
+ import {editorMachine} from '../../editor/editor-machine'
8
8
  import {defaultKeyGenerator} from '../../editor/key-generator'
9
9
  import {withPlugins} from '../../editor/plugins/with-plugins'
10
10
  import {createApplyPatch} from '../applyPatch'