@portabletext/editor 1.12.3 → 1.14.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.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/lib/_chunks-cjs/selector.get-text-before.cjs +320 -0
  3. package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -0
  4. package/lib/_chunks-es/selector.get-text-before.js +321 -0
  5. package/lib/_chunks-es/selector.get-text-before.js.map +1 -0
  6. package/lib/{index.esm.js → index.cjs} +1954 -1513
  7. package/lib/index.cjs.map +1 -0
  8. package/lib/{index.d.mts → index.d.cts} +4249 -304
  9. package/lib/index.d.ts +4249 -304
  10. package/lib/index.js +1974 -1488
  11. package/lib/index.js.map +1 -1
  12. package/lib/selectors/index.cjs +35 -0
  13. package/lib/selectors/index.cjs.map +1 -0
  14. package/lib/selectors/index.d.cts +243 -0
  15. package/lib/selectors/index.d.ts +243 -0
  16. package/lib/selectors/index.js +36 -0
  17. package/lib/selectors/index.js.map +1 -0
  18. package/package.json +25 -17
  19. package/src/editor/Editable.tsx +61 -6
  20. package/src/editor/PortableTextEditor.tsx +19 -4
  21. package/src/editor/__tests__/handleClick.test.tsx +4 -4
  22. package/src/editor/behavior/behavior.action.insert-block-object.ts +1 -1
  23. package/src/editor/behavior/behavior.action.insert-break.ts +24 -27
  24. package/src/editor/behavior/behavior.action.insert-inline-object.ts +58 -0
  25. package/src/editor/behavior/behavior.action.insert-span.ts +1 -1
  26. package/src/editor/behavior/behavior.action.list-item.ts +100 -0
  27. package/src/editor/behavior/behavior.action.style.ts +108 -0
  28. package/src/editor/behavior/behavior.action.text-block.set.ts +25 -0
  29. package/src/editor/behavior/behavior.action.text-block.unset.ts +17 -0
  30. package/src/editor/behavior/behavior.actions.ts +266 -75
  31. package/src/editor/behavior/behavior.code-editor.ts +76 -0
  32. package/src/editor/behavior/behavior.core.block-objects.ts +52 -19
  33. package/src/editor/behavior/behavior.core.decorators.ts +9 -6
  34. package/src/editor/behavior/behavior.core.lists.ts +139 -17
  35. package/src/editor/behavior/behavior.core.ts +7 -2
  36. package/src/editor/behavior/behavior.guards.ts +28 -0
  37. package/src/editor/behavior/behavior.links.ts +9 -9
  38. package/src/editor/behavior/behavior.markdown.ts +69 -80
  39. package/src/editor/behavior/behavior.types.ts +121 -60
  40. package/src/editor/{use-editor.ts → create-editor.ts} +13 -8
  41. package/src/editor/editor-event-listener.tsx +2 -2
  42. package/src/editor/editor-machine.ts +57 -15
  43. package/src/editor/editor-provider.tsx +5 -5
  44. package/src/editor/editor-selector.ts +49 -0
  45. package/src/editor/editor-snapshot.ts +22 -0
  46. package/src/editor/get-value.ts +11 -0
  47. package/src/editor/plugins/create-with-event-listeners.ts +93 -5
  48. package/src/editor/plugins/createWithEditableAPI.ts +69 -20
  49. package/src/editor/plugins/createWithHotKeys.ts +0 -101
  50. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +1 -55
  51. package/src/editor/plugins/with-plugins.ts +4 -8
  52. package/src/editor/{behavior/behavior.utils.block-offset.test.ts → utils/utils.block-offset.test.ts} +1 -1
  53. package/src/editor/{behavior/behavior.utils.block-offset.ts → utils/utils.block-offset.ts} +1 -8
  54. package/src/editor/{behavior/behavior.utils.reverse-selection.ts → utils/utils.reverse-selection.ts} +3 -5
  55. package/src/editor/utils/utils.ts +21 -0
  56. package/src/index.ts +13 -9
  57. package/src/selectors/index.ts +15 -0
  58. package/src/selectors/selector.get-active-list-item.ts +37 -0
  59. package/src/{editor/behavior/behavior.utils.get-selection-text.ts → selectors/selector.get-selection-text.ts} +10 -15
  60. package/src/selectors/selector.get-text-before.ts +41 -0
  61. package/src/selectors/selectors.ts +329 -0
  62. package/src/types/editor.ts +0 -60
  63. package/src/utils/is-hotkey.test.ts +99 -46
  64. package/src/utils/is-hotkey.ts +1 -1
  65. package/src/utils/operationToPatches.ts +5 -0
  66. package/src/utils/paths.ts +4 -11
  67. package/src/utils/ranges.ts +3 -3
  68. package/lib/index.esm.js.map +0 -1
  69. package/lib/index.mjs +0 -7372
  70. package/lib/index.mjs.map +0 -1
  71. package/src/editor/behavior/behavior.utils.ts +0 -218
  72. package/src/editor/behavior/behavior.utilts.get-text-before.ts +0 -31
  73. package/src/editor/plugins/createWithPortableTextLists.ts +0 -172
  74. /package/src/editor/{behavior/behavior.utils.get-start-point.ts → utils/utils.get-start-point.ts} +0 -0
  75. /package/src/editor/{behavior/behavior.utils.is-keyed-segment.ts → utils/utils.is-keyed-segment.ts} +0 -0
@@ -1,30 +1,61 @@
1
1
  import {isPortableTextTextBlock} from '@sanity/types'
2
- import {defineBehavior} from './behavior.types'
3
2
  import {
4
3
  getFocusBlockObject,
5
4
  getFocusTextBlock,
6
5
  getNextBlock,
7
6
  getPreviousBlock,
8
- isEmptyTextBlock,
9
7
  selectionIsCollapsed,
10
- } from './behavior.utils'
8
+ } from '../../selectors/selectors'
9
+ import {isHotkey} from '../../utils/is-hotkey'
10
+ import {isEmptyTextBlock} from '../utils/utils'
11
+ import {defineBehavior} from './behavior.types'
12
+
13
+ const arrowDownOnLonelyBlockObject = defineBehavior({
14
+ on: 'key.down',
15
+ guard: ({context, event}) => {
16
+ const isArrowDown = isHotkey('ArrowDown', event.keyboardEvent)
17
+ const focusBlockObject = getFocusBlockObject({context})
18
+ const nextBlock = getNextBlock({context})
19
+
20
+ return isArrowDown && focusBlockObject && !nextBlock
21
+ },
22
+ actions: [() => [{type: 'insert.text block', placement: 'after'}]],
23
+ })
24
+
25
+ const arrowUpOnLonelyBlockObject = defineBehavior({
26
+ on: 'key.down',
27
+ guard: ({context, event}) => {
28
+ const isArrowUp = isHotkey('ArrowUp', event.keyboardEvent)
29
+ const focusBlockObject = getFocusBlockObject({context})
30
+ const previousBlock = getPreviousBlock({context})
31
+
32
+ return isArrowUp && focusBlockObject && !previousBlock
33
+ },
34
+ actions: [
35
+ () => [
36
+ {type: 'insert.text block', placement: 'before'},
37
+ {type: 'select previous block'},
38
+ ],
39
+ ],
40
+ })
11
41
 
12
42
  const breakingBlockObject = defineBehavior({
13
- on: 'insert break',
43
+ on: 'insert.break',
14
44
  guard: ({context}) => {
15
- const focusBlockObject = getFocusBlockObject(context)
45
+ const focusBlockObject = getFocusBlockObject({context})
46
+ const collapsedSelection = selectionIsCollapsed({context})
16
47
 
17
- return !!focusBlockObject
48
+ return collapsedSelection && focusBlockObject !== undefined
18
49
  },
19
- actions: [() => [{type: 'insert text block', placement: 'after'}]],
50
+ actions: [() => [{type: 'insert.text block', placement: 'after'}]],
20
51
  })
21
52
 
22
53
  const deletingEmptyTextBlockAfterBlockObject = defineBehavior({
23
- on: 'delete backward',
54
+ on: 'delete.backward',
24
55
  guard: ({context}) => {
25
- const focusTextBlock = getFocusTextBlock(context)
26
- const selectionCollapsed = selectionIsCollapsed(context)
27
- const previousBlock = getPreviousBlock(context)
56
+ const focusTextBlock = getFocusTextBlock({context})
57
+ const selectionCollapsed = selectionIsCollapsed({context})
58
+ const previousBlock = getPreviousBlock({context})
28
59
 
29
60
  if (!focusTextBlock || !selectionCollapsed || !previousBlock) {
30
61
  return false
@@ -40,9 +71,9 @@ const deletingEmptyTextBlockAfterBlockObject = defineBehavior({
40
71
  return false
41
72
  },
42
73
  actions: [
43
- (_, {focusTextBlock, previousBlock}) => [
74
+ ({focusTextBlock, previousBlock}) => [
44
75
  {
45
- type: 'delete block',
76
+ type: 'delete.block',
46
77
  blockPath: focusTextBlock.path,
47
78
  },
48
79
  {
@@ -57,11 +88,11 @@ const deletingEmptyTextBlockAfterBlockObject = defineBehavior({
57
88
  })
58
89
 
59
90
  const deletingEmptyTextBlockBeforeBlockObject = defineBehavior({
60
- on: 'delete forward',
91
+ on: 'delete.forward',
61
92
  guard: ({context}) => {
62
- const focusTextBlock = getFocusTextBlock(context)
63
- const selectionCollapsed = selectionIsCollapsed(context)
64
- const nextBlock = getNextBlock(context)
93
+ const focusTextBlock = getFocusTextBlock({context})
94
+ const selectionCollapsed = selectionIsCollapsed({context})
95
+ const nextBlock = getNextBlock({context})
65
96
 
66
97
  if (!focusTextBlock || !selectionCollapsed || !nextBlock) {
67
98
  return false
@@ -77,9 +108,9 @@ const deletingEmptyTextBlockBeforeBlockObject = defineBehavior({
77
108
  return false
78
109
  },
79
110
  actions: [
80
- (_, {focusTextBlock, nextBlock}) => [
111
+ ({focusTextBlock, nextBlock}) => [
81
112
  {
82
- type: 'delete block',
113
+ type: 'delete.block',
83
114
  blockPath: focusTextBlock.path,
84
115
  },
85
116
  {
@@ -94,6 +125,8 @@ const deletingEmptyTextBlockBeforeBlockObject = defineBehavior({
94
125
  })
95
126
 
96
127
  export const coreBlockObjectBehaviors = {
128
+ arrowDownOnLonelyBlockObject,
129
+ arrowUpOnLonelyBlockObject,
97
130
  breakingBlockObject,
98
131
  deletingEmptyTextBlockAfterBlockObject,
99
132
  deletingEmptyTextBlockBeforeBlockObject,
@@ -2,11 +2,12 @@ import {defineBehavior} from './behavior.types'
2
2
 
3
3
  const decoratorAdd = defineBehavior({
4
4
  on: 'decorator.add',
5
+ guard: ({event}) => ({decorator: event.decorator}),
5
6
  actions: [
6
- ({event}) => [
7
+ ({decorator}) => [
7
8
  {
8
9
  type: 'decorator.add',
9
- decorator: event.decorator,
10
+ decorator,
10
11
  },
11
12
  {
12
13
  type: 'reselect',
@@ -17,11 +18,12 @@ const decoratorAdd = defineBehavior({
17
18
 
18
19
  const decoratorRemove = defineBehavior({
19
20
  on: 'decorator.remove',
21
+ guard: ({event}) => ({decorator: event.decorator}),
20
22
  actions: [
21
- ({event}) => [
23
+ ({decorator}) => [
22
24
  {
23
25
  type: 'decorator.remove',
24
- decorator: event.decorator,
26
+ decorator,
25
27
  },
26
28
  {
27
29
  type: 'reselect',
@@ -32,11 +34,12 @@ const decoratorRemove = defineBehavior({
32
34
 
33
35
  const decoratorToggle = defineBehavior({
34
36
  on: 'decorator.toggle',
37
+ guard: ({event}) => ({decorator: event.decorator}),
35
38
  actions: [
36
- ({event}) => [
39
+ ({decorator}) => [
37
40
  {
38
41
  type: 'decorator.toggle',
39
- decorator: event.decorator,
42
+ decorator,
40
43
  },
41
44
  {
42
45
  type: 'reselect',
@@ -1,16 +1,23 @@
1
- import {defineBehavior} from './behavior.types'
2
1
  import {
2
+ getFocusListBlock,
3
3
  getFocusSpan,
4
4
  getFocusTextBlock,
5
+ getSelectedBlocks,
5
6
  selectionIsCollapsed,
6
- } from './behavior.utils'
7
+ } from '../../selectors/selectors'
8
+ import {isHotkey} from '../../utils/is-hotkey'
9
+ import {isEmptyTextBlock} from '../utils/utils'
10
+ import {createGuards} from './behavior.guards'
11
+ import {defineBehavior} from './behavior.types'
12
+
13
+ const MAX_LIST_LEVEL = 10
7
14
 
8
15
  const clearListOnBackspace = defineBehavior({
9
- on: 'delete backward',
16
+ on: 'delete.backward',
10
17
  guard: ({context}) => {
11
- const selectionCollapsed = selectionIsCollapsed(context)
12
- const focusTextBlock = getFocusTextBlock(context)
13
- const focusSpan = getFocusSpan(context)
18
+ const selectionCollapsed = selectionIsCollapsed({context})
19
+ const focusTextBlock = getFocusTextBlock({context})
20
+ const focusSpan = getFocusSpan({context})
14
21
 
15
22
  if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
16
23
  return false
@@ -27,22 +34,22 @@ const clearListOnBackspace = defineBehavior({
27
34
  return false
28
35
  },
29
36
  actions: [
30
- (_, {focusTextBlock}) => [
37
+ ({focusTextBlock}) => [
31
38
  {
32
- type: 'unset block',
39
+ type: 'text block.unset',
33
40
  props: ['listItem', 'level'],
34
- paths: [focusTextBlock.path],
41
+ at: focusTextBlock.path,
35
42
  },
36
43
  ],
37
44
  ],
38
45
  })
39
46
 
40
47
  const unindentListOnBackspace = defineBehavior({
41
- on: 'delete backward',
48
+ on: 'delete.backward',
42
49
  guard: ({context}) => {
43
- const selectionCollapsed = selectionIsCollapsed(context)
44
- const focusTextBlock = getFocusTextBlock(context)
45
- const focusSpan = getFocusSpan(context)
50
+ const selectionCollapsed = selectionIsCollapsed({context})
51
+ const focusTextBlock = getFocusTextBlock({context})
52
+ const focusSpan = getFocusSpan({context})
46
53
 
47
54
  if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
48
55
  return false
@@ -63,14 +70,129 @@ const unindentListOnBackspace = defineBehavior({
63
70
  return false
64
71
  },
65
72
  actions: [
66
- (_, {focusTextBlock, level}) => [
73
+ ({focusTextBlock, level}) => [
67
74
  {
68
- type: 'set block',
75
+ type: 'text block.set',
69
76
  level,
70
- paths: [focusTextBlock.path],
77
+ at: focusTextBlock.path,
71
78
  },
72
79
  ],
73
80
  ],
74
81
  })
75
82
 
76
- export const coreListBehaviors = {clearListOnBackspace, unindentListOnBackspace}
83
+ const clearListOnEnter = defineBehavior({
84
+ on: 'insert.break',
85
+ guard: ({context}) => {
86
+ const focusListBlock = getFocusListBlock({context})
87
+ const selectionCollapsed = selectionIsCollapsed({context})
88
+
89
+ if (!focusListBlock || !selectionCollapsed) {
90
+ return false
91
+ }
92
+
93
+ if (!isEmptyTextBlock(focusListBlock.node)) {
94
+ return false
95
+ }
96
+
97
+ return {focusListBlock}
98
+ },
99
+ actions: [
100
+ ({focusListBlock}) => [
101
+ {
102
+ type: 'text block.unset',
103
+ props: ['listItem', 'level'],
104
+ at: focusListBlock.path,
105
+ },
106
+ ],
107
+ ],
108
+ })
109
+
110
+ const indentListOnTab = defineBehavior({
111
+ on: 'key.down',
112
+ guard: ({context, event}) => {
113
+ const isTab = isHotkey('Tab', event.keyboardEvent)
114
+
115
+ if (!isTab) {
116
+ return false
117
+ }
118
+
119
+ const selectedBlocks = getSelectedBlocks({context})
120
+ const guards = createGuards(context)
121
+ const selectedListBlocks = selectedBlocks.flatMap((block) =>
122
+ guards.isListBlock(block.node)
123
+ ? [
124
+ {
125
+ node: block.node,
126
+ path: block.path,
127
+ },
128
+ ]
129
+ : [],
130
+ )
131
+
132
+ if (selectedListBlocks.length === selectedBlocks.length) {
133
+ return {selectedListBlocks}
134
+ }
135
+
136
+ return false
137
+ },
138
+ actions: [
139
+ ({selectedListBlocks}) =>
140
+ selectedListBlocks.map((selectedListBlock) => ({
141
+ type: 'text block.set',
142
+ level: Math.min(
143
+ MAX_LIST_LEVEL,
144
+ Math.max(1, selectedListBlock.node.level + 1),
145
+ ),
146
+ at: selectedListBlock.path,
147
+ })),
148
+ ],
149
+ })
150
+
151
+ const unindentListOnShiftTab = defineBehavior({
152
+ on: 'key.down',
153
+ guard: ({context, event}) => {
154
+ const isShiftTab = isHotkey('Shift+Tab', event.keyboardEvent)
155
+
156
+ if (!isShiftTab) {
157
+ return false
158
+ }
159
+
160
+ const selectedBlocks = getSelectedBlocks({context})
161
+ const guards = createGuards(context)
162
+ const selectedListBlocks = selectedBlocks.flatMap((block) =>
163
+ guards.isListBlock(block.node)
164
+ ? [
165
+ {
166
+ node: block.node,
167
+ path: block.path,
168
+ },
169
+ ]
170
+ : [],
171
+ )
172
+
173
+ if (selectedListBlocks.length === selectedBlocks.length) {
174
+ return {selectedListBlocks}
175
+ }
176
+
177
+ return false
178
+ },
179
+ actions: [
180
+ ({selectedListBlocks}) =>
181
+ selectedListBlocks.map((selectedListBlock) => ({
182
+ type: 'text block.set',
183
+ level: Math.min(
184
+ MAX_LIST_LEVEL,
185
+ Math.max(1, selectedListBlock.node.level - 1),
186
+ ),
187
+ at: selectedListBlock.path,
188
+ })),
189
+ ],
190
+ })
191
+
192
+ export const coreListBehaviors = {
193
+ clearListOnBackspace,
194
+ unindentListOnBackspace,
195
+ clearListOnEnter,
196
+ indentListOnTab,
197
+ unindentListOnShiftTab,
198
+ }
@@ -4,8 +4,8 @@ import {coreListBehaviors} from './behavior.core.lists'
4
4
  import {defineBehavior} from './behavior.types'
5
5
 
6
6
  const softReturn = defineBehavior({
7
- on: 'insert soft break',
8
- actions: [() => [{type: 'insert text', text: '\n'}]],
7
+ on: 'insert.soft break',
8
+ actions: [() => [{type: 'insert.text', text: '\n'}]],
9
9
  })
10
10
 
11
11
  /**
@@ -16,11 +16,16 @@ export const coreBehaviors = [
16
16
  coreDecoratorBehaviors.decoratorAdd,
17
17
  coreDecoratorBehaviors.decoratorRemove,
18
18
  coreDecoratorBehaviors.decoratorToggle,
19
+ coreBlockObjectBehaviors.arrowDownOnLonelyBlockObject,
20
+ coreBlockObjectBehaviors.arrowUpOnLonelyBlockObject,
19
21
  coreBlockObjectBehaviors.breakingBlockObject,
20
22
  coreBlockObjectBehaviors.deletingEmptyTextBlockAfterBlockObject,
21
23
  coreBlockObjectBehaviors.deletingEmptyTextBlockBeforeBlockObject,
22
24
  coreListBehaviors.clearListOnBackspace,
23
25
  coreListBehaviors.unindentListOnBackspace,
26
+ coreListBehaviors.clearListOnEnter,
27
+ coreListBehaviors.indentListOnTab,
28
+ coreListBehaviors.unindentListOnShiftTab,
24
29
  ]
25
30
 
26
31
  /**
@@ -0,0 +1,28 @@
1
+ import {
2
+ isPortableTextListBlock,
3
+ isPortableTextTextBlock,
4
+ type PortableTextListBlock,
5
+ type PortableTextTextBlock,
6
+ } from '@sanity/types'
7
+ import type {PortableTextMemberSchemaTypes} from '../../types/editor'
8
+
9
+ /**
10
+ * @alpha
11
+ */
12
+ export type BehaviorGuards = ReturnType<typeof createGuards>
13
+
14
+ export function createGuards({
15
+ schema,
16
+ }: {
17
+ schema: PortableTextMemberSchemaTypes
18
+ }) {
19
+ function isListBlock(block: unknown): block is PortableTextListBlock {
20
+ return isPortableTextListBlock(block) && block._type === schema.block.name
21
+ }
22
+
23
+ function isTextBlock(block: unknown): block is PortableTextTextBlock {
24
+ return isPortableTextTextBlock(block) && block._type === schema.block.name
25
+ }
26
+
27
+ return {isListBlock, isTextBlock}
28
+ }
@@ -1,6 +1,6 @@
1
+ import {getFocusSpan, selectionIsCollapsed} from '../../selectors/selectors'
1
2
  import type {PortableTextMemberSchemaTypes} from '../../types/editor'
2
3
  import {defineBehavior} from './behavior.types'
3
- import {getFocusSpan, selectionIsCollapsed} from './behavior.utils'
4
4
 
5
5
  /**
6
6
  * @alpha
@@ -19,8 +19,8 @@ export function createLinkBehaviors(config: LinkBehaviorsConfig) {
19
19
  const pasteLinkOnSelection = defineBehavior({
20
20
  on: 'paste',
21
21
  guard: ({context, event}) => {
22
- const selectionCollapsed = selectionIsCollapsed(context)
23
- const text = event.clipboardData.getData('text/plain')
22
+ const selectionCollapsed = selectionIsCollapsed({context})
23
+ const text = event.data.getData('text/plain')
24
24
  const url = looksLikeUrl(text) ? text : undefined
25
25
  const annotation =
26
26
  url !== undefined
@@ -34,7 +34,7 @@ export function createLinkBehaviors(config: LinkBehaviorsConfig) {
34
34
  return false
35
35
  },
36
36
  actions: [
37
- (_, {annotation}) => [
37
+ ({annotation}) => [
38
38
  {
39
39
  type: 'annotation.add',
40
40
  annotation,
@@ -45,14 +45,14 @@ export function createLinkBehaviors(config: LinkBehaviorsConfig) {
45
45
  const pasteLinkAtCaret = defineBehavior({
46
46
  on: 'paste',
47
47
  guard: ({context, event}) => {
48
- const focusSpan = getFocusSpan(context)
49
- const selectionCollapsed = selectionIsCollapsed(context)
48
+ const focusSpan = getFocusSpan({context})
49
+ const selectionCollapsed = selectionIsCollapsed({context})
50
50
 
51
51
  if (!focusSpan || !selectionCollapsed) {
52
52
  return false
53
53
  }
54
54
 
55
- const text = event.clipboardData.getData('text/plain')
55
+ const text = event.data.getData('text/plain')
56
56
  const url = looksLikeUrl(text) ? text : undefined
57
57
  const annotation =
58
58
  url !== undefined
@@ -66,9 +66,9 @@ export function createLinkBehaviors(config: LinkBehaviorsConfig) {
66
66
  return false
67
67
  },
68
68
  actions: [
69
- (_, {annotation, url}) => [
69
+ ({annotation, url}) => [
70
70
  {
71
- type: 'insert span',
71
+ type: 'insert.span',
72
72
  text: url,
73
73
  annotations: [annotation],
74
74
  },