@portabletext/editor 2.11.0 → 2.12.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.
@@ -1,5 +1,5 @@
1
1
  import { Behavior, Editor, EditorEmittedEvent, EditorSchema } from "../_chunks-dts/behavior.types.action.js";
2
- import * as react21 from "react";
2
+ import * as react22 from "react";
3
3
  import React from "react";
4
4
  /**
5
5
  * @beta
@@ -181,7 +181,7 @@ type MarkdownPluginConfig = MarkdownBehaviorsConfig & {
181
181
  */
182
182
  declare function MarkdownPlugin(props: {
183
183
  config: MarkdownPluginConfig;
184
- }): react21.JSX.Element;
184
+ }): react22.JSX.Element;
185
185
  /**
186
186
  * @beta
187
187
  * Restrict the editor to one line. The plugin takes care of blocking
@@ -192,5 +192,5 @@ declare function MarkdownPlugin(props: {
192
192
  *
193
193
  * @deprecated Install the plugin from `@portabletext/plugin-one-line`
194
194
  */
195
- declare function OneLinePlugin(): react21.JSX.Element;
195
+ declare function OneLinePlugin(): react22.JSX.Element;
196
196
  export { BehaviorPlugin, DecoratorShortcutPlugin, EditorRefPlugin, EventListenerPlugin, MarkdownPlugin, type MarkdownPluginConfig, OneLinePlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -51,6 +51,12 @@
51
51
  "require": "./lib/selectors/index.cjs",
52
52
  "default": "./lib/selectors/index.js"
53
53
  },
54
+ "./test": {
55
+ "default": "./src/test/_exports/index.ts"
56
+ },
57
+ "./test/vitest": {
58
+ "default": "./src/test/vitest/_exports/index.ts"
59
+ },
54
60
  "./utils": {
55
61
  "source": "./src/utils/_exports/index.ts",
56
62
  "import": "./lib/utils/index.js",
@@ -80,9 +86,9 @@
80
86
  "slate-react": "0.117.4",
81
87
  "xstate": "^5.21.0",
82
88
  "@portabletext/block-tools": "^3.5.6",
89
+ "@portabletext/keyboard-shortcuts": "^1.1.1",
83
90
  "@portabletext/patches": "^1.1.8",
84
- "@portabletext/schema": "^1.2.0",
85
- "@portabletext/keyboard-shortcuts": "^1.1.1"
91
+ "@portabletext/schema": "^1.2.0"
86
92
  },
87
93
  "devDependencies": {
88
94
  "@sanity/diff-match-patch": "^3.2.0",
@@ -1,8 +1,28 @@
1
+ import {createKeyboardShortcut} from '@portabletext/keyboard-shortcuts'
2
+ import {isTextBlock} from '@portabletext/schema'
1
3
  import {defaultKeyboardShortcuts} from '../keyboard-shortcuts/default-keyboard-shortcuts'
2
- import * as selectors from '../selectors'
4
+ import {
5
+ getFocusBlock,
6
+ getFocusInlineObject,
7
+ getPreviousBlock,
8
+ isSelectionCollapsed,
9
+ } from '../selectors'
10
+ import {getBlockEndPoint, isEmptyTextBlock} from '../utils'
3
11
  import {raise} from './behavior.types.action'
4
12
  import {defineBehavior} from './behavior.types.behavior'
5
13
 
14
+ const shiftLeft = createKeyboardShortcut({
15
+ default: [
16
+ {
17
+ key: 'ArrowLeft',
18
+ shift: true,
19
+ meta: false,
20
+ ctrl: false,
21
+ alt: false,
22
+ },
23
+ ],
24
+ })
25
+
6
26
  export const abstractKeyboardBehaviors = [
7
27
  /**
8
28
  * Allow raising an `insert.break` event when pressing Enter on an inline
@@ -12,8 +32,8 @@ export const abstractKeyboardBehaviors = [
12
32
  on: 'keyboard.keydown',
13
33
  guard: ({snapshot, event}) =>
14
34
  defaultKeyboardShortcuts.break.guard(event.originEvent) &&
15
- selectors.isSelectionCollapsed(snapshot) &&
16
- selectors.getFocusInlineObject(snapshot),
35
+ isSelectionCollapsed(snapshot) &&
36
+ getFocusInlineObject(snapshot),
17
37
  actions: [() => [raise({type: 'insert.break'})]],
18
38
  }),
19
39
 
@@ -48,4 +68,68 @@ export const abstractKeyboardBehaviors = [
48
68
  defaultKeyboardShortcuts.history.redo.guard(event.originEvent),
49
69
  actions: [() => [raise({type: 'history.redo'})]],
50
70
  }),
71
+
72
+ /**
73
+ * Fix edge case where Shift+ArrowLeft didn't reduce a selection hanging
74
+ * onto an empty text block.
75
+ */
76
+ defineBehavior({
77
+ on: 'keyboard.keydown',
78
+ guard: ({snapshot, event}) => {
79
+ if (!snapshot.context.selection || !shiftLeft.guard(event.originEvent)) {
80
+ return false
81
+ }
82
+
83
+ const focusBlock = getFocusBlock(snapshot)
84
+
85
+ if (!focusBlock) {
86
+ return false
87
+ }
88
+
89
+ const previousBlock = getPreviousBlock({
90
+ ...snapshot,
91
+ context: {
92
+ ...snapshot.context,
93
+ selection: {
94
+ anchor: {
95
+ path: focusBlock.path,
96
+ offset: 0,
97
+ },
98
+ focus: {
99
+ path: focusBlock.path,
100
+ offset: 0,
101
+ },
102
+ },
103
+ },
104
+ })
105
+
106
+ if (!previousBlock) {
107
+ return false
108
+ }
109
+
110
+ const hanging =
111
+ isTextBlock(snapshot.context, focusBlock.node) &&
112
+ snapshot.context.selection.focus.offset === 0
113
+
114
+ if (hanging && isEmptyTextBlock(snapshot.context, focusBlock.node)) {
115
+ return {previousBlock, selection: snapshot.context.selection}
116
+ }
117
+
118
+ return false
119
+ },
120
+ actions: [
121
+ ({snapshot}, {previousBlock, selection}) => [
122
+ raise({
123
+ type: 'select',
124
+ at: {
125
+ anchor: selection.anchor,
126
+ focus: getBlockEndPoint({
127
+ context: snapshot.context,
128
+ block: previousBlock,
129
+ }),
130
+ },
131
+ }),
132
+ ],
133
+ ],
134
+ }),
51
135
  ]
@@ -0,0 +1,52 @@
1
+ import {getActiveAnnotationsMarks} from '../selectors/selector.get-active-annotation-marks'
2
+ import {getActiveDecorators} from '../selectors/selector.get-active-decorators'
3
+ import {getFocusSpan} from '../selectors/selector.get-focus-span'
4
+ import {getMarkState} from '../selectors/selector.get-mark-state'
5
+ import {raise} from './behavior.types.action'
6
+ import {defineBehavior} from './behavior.types.behavior'
7
+
8
+ export const coreInsertBehaviors = [
9
+ defineBehavior({
10
+ on: 'insert.text',
11
+ guard: ({snapshot}) => {
12
+ const focusSpan = getFocusSpan(snapshot)
13
+
14
+ if (!focusSpan) {
15
+ return false
16
+ }
17
+
18
+ const markState = getMarkState(snapshot)
19
+ const activeDecorators = getActiveDecorators(snapshot)
20
+ const activeAnnotations = getActiveAnnotationsMarks(snapshot)
21
+
22
+ if (markState && markState.state === 'unchanged') {
23
+ const markStateDecorators = (markState.marks ?? []).filter((mark) =>
24
+ snapshot.context.schema.decorators
25
+ .map((decorator) => decorator.name)
26
+ .includes(mark),
27
+ )
28
+
29
+ if (
30
+ markStateDecorators.length === activeDecorators.length &&
31
+ markStateDecorators.every((mark) => activeDecorators.includes(mark))
32
+ ) {
33
+ return false
34
+ }
35
+ }
36
+
37
+ return {activeDecorators, activeAnnotations}
38
+ },
39
+ actions: [
40
+ ({snapshot, event}, {activeDecorators, activeAnnotations}) => [
41
+ raise({
42
+ type: 'insert.child',
43
+ child: {
44
+ _type: snapshot.context.schema.span.name,
45
+ text: event.text,
46
+ marks: [...activeDecorators, ...activeAnnotations],
47
+ },
48
+ }),
49
+ ],
50
+ ],
51
+ }),
52
+ ]
@@ -3,6 +3,7 @@ import {coreAnnotationBehaviors} from './behavior.core.annotations'
3
3
  import {coreBlockObjectBehaviors} from './behavior.core.block-objects'
4
4
  import {coreDecoratorBehaviors} from './behavior.core.decorators'
5
5
  import {coreDndBehaviors} from './behavior.core.dnd'
6
+ import {coreInsertBehaviors} from './behavior.core.insert'
6
7
  import {coreInsertBreakBehaviors} from './behavior.core.insert-break'
7
8
  import {coreListBehaviors} from './behavior.core.lists'
8
9
 
@@ -20,6 +21,7 @@ export const coreBehaviorsConfig = [
20
21
  coreBlockObjectBehaviors.breakingBlockObject,
21
22
  coreBlockObjectBehaviors.deletingEmptyTextBlockAfterBlockObject,
22
23
  coreBlockObjectBehaviors.deletingEmptyTextBlockBeforeBlockObject,
24
+ ...coreInsertBehaviors,
23
25
  coreListBehaviors.clearListOnBackspace,
24
26
  coreListBehaviors.unindentListOnBackspace,
25
27
  coreListBehaviors.mergeTextIntoListOnDelete,
@@ -168,6 +168,10 @@ export function performEvent({
168
168
  // action prevented
169
169
  defaultBehaviorOverwritten = true
170
170
 
171
+ if (eventBehavior.actions.length === 0) {
172
+ nativeEventPrevented = true
173
+ }
174
+
171
175
  for (const actionSet of eventBehavior.actions) {
172
176
  const actionsSnapshot = getSnapshot()
173
177
 
@@ -3,9 +3,9 @@ import {createTestKeyGenerator} from '@portabletext/test'
3
3
  import React from 'react'
4
4
  import {describe, expect, test} from 'vitest'
5
5
  import {InternalSlateEditorRefPlugin} from '../plugins/plugin.internal.slate-editor-ref'
6
+ import {createTestEditor} from '../test/vitest'
6
7
  import type {PortableTextSlateEditor} from '../types/editor'
7
8
  import {getFocusSpan} from './slate-utils'
8
- import {createTestEditor} from './test-editor'
9
9
 
10
10
  describe(getFocusSpan.name, () => {
11
11
  const keyGenerator = createTestKeyGenerator()
@@ -1,5 +1,6 @@
1
1
  import {isTextBlock} from '@portabletext/schema'
2
2
  import {Transforms} from 'slate'
3
+ import {EDITOR_TO_PENDING_SELECTION} from 'slate-dom'
3
4
  import {parseInlineObject, parseSpan} from '../internal-utils/parse-blocks'
4
5
  import {getFocusBlock, getFocusSpan} from '../internal-utils/slate-utils'
5
6
  import {VOID_CHILD_KEY} from '../internal-utils/values'
@@ -54,6 +55,13 @@ export const insertChildOperationImplementation: BehaviorOperationImplementation
54
55
  })
55
56
  }
56
57
 
58
+ // This makes sure the selection is set correctly when event handling is run
59
+ // through Slate's Android input handling
60
+ EDITOR_TO_PENDING_SELECTION.set(
61
+ operation.editor,
62
+ operation.editor.selection,
63
+ )
64
+
57
65
  return
58
66
  }
59
67
 
@@ -1,76 +1,8 @@
1
1
  import {Transforms} from 'slate'
2
- import {EDITOR_TO_PENDING_SELECTION} from 'slate-dom'
3
- import type {EditorSnapshot} from '../editor/editor-snapshot'
4
- import {
5
- getFocusSpan,
6
- slateRangeToSelection,
7
- } from '../internal-utils/slate-utils'
8
- import {getActiveAnnotationsMarks} from '../selectors/selector.get-active-annotation-marks'
9
- import {getActiveDecorators} from '../selectors/selector.get-active-decorators'
10
- import {getMarkState} from '../selectors/selector.get-mark-state'
11
2
  import type {BehaviorOperationImplementation} from './behavior.operations'
12
3
 
13
4
  export const insertTextOperationImplementation: BehaviorOperationImplementation<
14
5
  'insert.text'
15
- > = ({context, operation}) => {
16
- const snapshot: EditorSnapshot = {
17
- blockIndexMap: operation.editor.blockIndexMap,
18
- context: {
19
- value: operation.editor.value,
20
- selection: operation.editor.selection
21
- ? slateRangeToSelection({
22
- schema: context.schema,
23
- editor: operation.editor,
24
- range: operation.editor.selection,
25
- })
26
- : null,
27
- schema: context.schema,
28
- keyGenerator: context.keyGenerator,
29
- converters: [],
30
- readOnly: false,
31
- },
32
- decoratorState: operation.editor.decoratorState,
33
- }
34
-
35
- const markState = getMarkState(snapshot)
36
- const activeDecorators = getActiveDecorators(snapshot)
37
- const activeAnnotations = getActiveAnnotationsMarks(snapshot)
38
-
39
- const [focusSpan] = getFocusSpan({
40
- editor: operation.editor,
41
- })
42
-
43
- if (!focusSpan) {
44
- Transforms.insertText(operation.editor, operation.text)
45
- return
46
- }
47
-
48
- if (markState && markState.state === 'unchanged') {
49
- const markStateDecorators = (markState.marks ?? []).filter((mark) =>
50
- context.schema.decorators
51
- .map((decorator) => decorator.name)
52
- .includes(mark),
53
- )
54
-
55
- if (
56
- markStateDecorators.length === activeDecorators.length &&
57
- markStateDecorators.every((mark) => activeDecorators.includes(mark))
58
- ) {
59
- Transforms.insertText(operation.editor, operation.text)
60
- return
61
- }
62
- }
63
-
64
- Transforms.insertNodes(operation.editor, {
65
- _type: focusSpan._type,
66
- _key: context.keyGenerator(),
67
- text: operation.text,
68
- marks: [...activeDecorators, ...activeAnnotations],
69
- })
70
-
71
- // This makes sure the selection is set correctly when event handling is run
72
- // through Slate's Android input handling
73
- EDITOR_TO_PENDING_SELECTION.set(operation.editor, operation.editor.selection)
74
-
75
- operation.editor.decoratorState = {}
6
+ > = ({operation}) => {
7
+ Transforms.insertText(operation.editor, operation.text)
76
8
  }
@@ -1,7 +1,7 @@
1
1
  import {getTersePt} from '@portabletext/test'
2
2
  import {userEvent} from '@vitest/browser/context'
3
3
  import {describe, expect, test, vi} from 'vitest'
4
- import {createTestEditor} from '../internal-utils/test-editor'
4
+ import {createTestEditor} from '../test/vitest'
5
5
  import {AutoCloseBracketsPlugin} from './plugin.internal.auto-close-brackets'
6
6
 
7
7
  describe(AutoCloseBracketsPlugin.name, () => {
@@ -2,8 +2,8 @@ import {defineSchema} from '@portabletext/schema'
2
2
  import {getTersePt} from '@portabletext/test'
3
3
  import {userEvent} from '@vitest/browser/context'
4
4
  import {describe, expect, test, vi} from 'vitest'
5
- import {createTestEditor} from '../internal-utils/test-editor'
6
5
  import {getTextMarks} from '../internal-utils/text-marks'
6
+ import {createTestEditor} from '../test/vitest'
7
7
  import {MarkdownPlugin} from './plugin.markdown'
8
8
 
9
9
  describe(MarkdownPlugin.name, () => {
@@ -0,0 +1 @@
1
+ export * from '../index'
@@ -0,0 +1,102 @@
1
+ import {parseTersePtString} from '@portabletext/test'
2
+ import {createParameterType, type ParameterType} from 'racejar'
3
+
4
+ /**
5
+ * @internal
6
+ */
7
+ export type Parameter = {
8
+ [K in keyof typeof parameterType]: (typeof parameterType)[K] extends ParameterType<
9
+ infer TParameterType
10
+ >
11
+ ? TParameterType
12
+ : never
13
+ }
14
+
15
+ const parameterType = {
16
+ annotation: createParameterType<'comment' | 'link'>({
17
+ name: 'annotation',
18
+ matcher: /"(comment|link)"/,
19
+ }),
20
+ blockObject: createParameterType<'image'>({
21
+ name: 'block-object',
22
+ matcher: /"(image)"/,
23
+ }),
24
+ button: createParameterType<string>({
25
+ name: 'button',
26
+ matcher: /"(([A-Z]|[a-z]|{|}|>|\/|\+)+)"/,
27
+ type: String,
28
+ }),
29
+ decorator: createParameterType<'em' | 'strong'>({
30
+ name: 'decorator',
31
+ matcher: /"(em|strong)"/,
32
+ }),
33
+ index: createParameterType({
34
+ name: 'index',
35
+ matcher: /"(\d)"/,
36
+ type: Number,
37
+ transform: (input) => Number.parseInt(input, 10),
38
+ }),
39
+ inlineObject: createParameterType<'stock-ticker'>({
40
+ name: 'inline-object',
41
+ matcher: /"(stock-ticker)"/,
42
+ }),
43
+ key: createParameterType<'key'>({
44
+ name: 'key',
45
+ matcher: /"([a-z]\d)"/,
46
+ }),
47
+ keyKeys: createParameterType<Array<string>>({
48
+ name: 'keyKeys',
49
+ matcher: /"(([a-z]\d)(,([a-z]\d))*)"/,
50
+ type: Array,
51
+ transform: (input) => input.split(','),
52
+ }),
53
+ marks: createParameterType<Array<string>>({
54
+ name: 'marks',
55
+ matcher: /"((strong|em|[a-z]\d)(,(strong|em|[a-z]\d))*)"/,
56
+ type: Array,
57
+ transform: (input) => input.split(','),
58
+ }),
59
+ placement: createParameterType<'auto' | 'after' | 'before'>({
60
+ name: 'placement',
61
+ matcher: /"(auto|after|before)"/,
62
+ }),
63
+ selectPosition: createParameterType<'start' | 'end' | 'none'>({
64
+ name: 'select-position',
65
+ matcher: /"(start|end|none)"/,
66
+ }),
67
+ style: createParameterType({
68
+ name: 'style',
69
+ matcher: /"(normal|blockquote|h\d)"/,
70
+ }),
71
+ tersePt: createParameterType<Array<string>>({
72
+ name: 'terse-pt',
73
+ matcher: /"([a-z-,#>:\\n \d|{}'"‘’“”?]*)"/u,
74
+ type: Array,
75
+ transform: parseTersePtString,
76
+ }),
77
+ text: createParameterType<string>({
78
+ name: 'text',
79
+ matcher: /"([a-z]*)"/u,
80
+ type: String,
81
+ }),
82
+ }
83
+
84
+ /**
85
+ * @internal
86
+ */
87
+ export const parameterTypes = [
88
+ parameterType.annotation,
89
+ parameterType.blockObject,
90
+ parameterType.button,
91
+ parameterType.decorator,
92
+ parameterType.index,
93
+ parameterType.inlineObject,
94
+ parameterType.key,
95
+ parameterType.keyKeys,
96
+ parameterType.marks,
97
+ parameterType.placement,
98
+ parameterType.selectPosition,
99
+ parameterType.style,
100
+ parameterType.tersePt,
101
+ parameterType.text,
102
+ ]
@@ -0,0 +1 @@
1
+ export * from './gherkin-parameter-types'
@@ -0,0 +1 @@
1
+ export * from '../index'
@@ -0,0 +1,3 @@
1
+ export * from './step-context'
2
+ export * from './step-definitions'
3
+ export * from './test-editor'
@@ -0,0 +1,11 @@
1
+ import type {Locator} from '@vitest/browser/context'
2
+ import type {Editor} from '../../editor'
3
+
4
+ /**
5
+ * @internal
6
+ */
7
+ export type Context = {
8
+ editor: Editor
9
+ locator: Locator
10
+ keyMap?: Map<string, string>
11
+ }