@portabletext/editor 3.0.8 → 3.1.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.
@@ -1,9 +1,10 @@
1
- import {isTextBlock} from '@portabletext/schema'
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
4
  import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
5
5
  import {slateRangeToSelection} from '../internal-utils/slate-utils'
6
6
  import {toSlateRange} from '../internal-utils/to-slate-range'
7
+ import {VOID_CHILD_KEY} from '../internal-utils/values'
7
8
  import type {PortableTextSlateEditor} from '../types/editor'
8
9
  import {getBlockKeyFromSelectionPoint} from '../utils/util.selection-point'
9
10
  import type {BehaviorOperationImplementation} from './behavior.operations'
@@ -74,6 +75,23 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
74
75
  return
75
76
  }
76
77
 
78
+ if (operation.unit === 'child') {
79
+ const range = at ?? operation.editor.selection ?? undefined
80
+
81
+ if (!range) {
82
+ throw new Error('Unable to delete children without a selection')
83
+ }
84
+
85
+ Transforms.removeNodes(operation.editor, {
86
+ at: range,
87
+ match: (node) =>
88
+ (isSpan(context, node) && node._key !== VOID_CHILD_KEY) ||
89
+ ('__inline' in node && node.__inline === true),
90
+ })
91
+
92
+ return
93
+ }
94
+
77
95
  if (operation.direction === 'backward' && operation.unit === 'line') {
78
96
  const range = at ?? operation.editor.selection ?? undefined
79
97
 
@@ -12,7 +12,7 @@ import {
12
12
  import {DOMEditor} from 'slate-dom'
13
13
  import {getFocusBlock, getFocusChild} from '../internal-utils/slate-utils'
14
14
  import {toSlateRange} from '../internal-utils/to-slate-range'
15
- import {isEqualToEmptyEditor, toSlateBlock} from '../internal-utils/values'
15
+ import {toSlateBlock} from '../internal-utils/values'
16
16
  import type {EditorSelection, PortableTextSlateEditor} from '../types/editor'
17
17
  import {parseBlock} from '../utils/parse-blocks'
18
18
  import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
@@ -121,7 +121,7 @@ export function insertBlock(options: {
121
121
  } else {
122
122
  // placement === 'auto'
123
123
 
124
- if (endBlock && isEqualToEmptyEditor([endBlock], context.schema)) {
124
+ if (isEmptyTextBlock(context, endBlock)) {
125
125
  Transforms.insertNodes(editor, [block], {
126
126
  at: endBlockPath,
127
127
  select: false,
@@ -234,7 +234,7 @@ export function insertBlock(options: {
234
234
  Transforms.select(editor, atAfterInsert)
235
235
  }
236
236
 
237
- if (focusBlock && isEqualToEmptyEditor([focusBlock], context.schema)) {
237
+ if (isEmptyTextBlock(context, focusBlock)) {
238
238
  Transforms.removeNodes(editor, {at: focusBlockPath})
239
239
  }
240
240
 
@@ -244,7 +244,7 @@ export function insertBlock(options: {
244
244
  if (editor.isTextBlock(endBlock) && editor.isTextBlock(block)) {
245
245
  const selectionStartPoint = Range.start(at)
246
246
 
247
- if (isEqualToEmptyEditor([endBlock], context.schema)) {
247
+ if (isEmptyTextBlock(context, endBlock)) {
248
248
  Transforms.insertNodes(editor, [block], {
249
249
  at: endBlockPath,
250
250
  select: false,
@@ -152,15 +152,6 @@ export interface PortableTextSlateEditor extends ReactEditor {
152
152
  */
153
153
  pteWithHotKeys: (event: KeyboardEvent<HTMLDivElement>) => void
154
154
 
155
- /**
156
- * Helper function that creates a text block
157
- */
158
- pteCreateTextBlock: (options: {
159
- decorators: Array<string>
160
- listItem?: string
161
- level?: number
162
- }) => Descendant
163
-
164
155
  /**
165
156
  * Undo
166
157
  */
@@ -144,45 +144,10 @@ export function parseTextBlock({
144
144
  const _key =
145
145
  typeof block._key === 'string' ? block._key : context.keyGenerator()
146
146
 
147
- const unparsedMarkDefs: Array<unknown> = Array.isArray(block.markDefs)
148
- ? block.markDefs
149
- : []
150
- const markDefKeyMap = new Map<string, string>()
151
- const markDefs = unparsedMarkDefs.flatMap((markDef) => {
152
- if (!isTypedObject(markDef)) {
153
- return []
154
- }
155
-
156
- const schemaType = context.schema.annotations.find(
157
- ({name}) => name === markDef._type,
158
- )
159
-
160
- if (!schemaType) {
161
- return []
162
- }
163
-
164
- if (typeof markDef._key !== 'string') {
165
- // If the `markDef` doesn't have a `_key` then we don't know what spans
166
- // it belongs to and therefore we have to discard it.
167
- return []
168
- }
169
-
170
- const parsedAnnotation = parseObject({
171
- object: markDef,
172
- context: {
173
- schemaType,
174
- keyGenerator: context.keyGenerator,
175
- },
176
- options,
177
- })
178
-
179
- if (!parsedAnnotation) {
180
- return []
181
- }
182
-
183
- markDefKeyMap.set(markDef._key, parsedAnnotation._key)
184
-
185
- return [parsedAnnotation]
147
+ const {markDefs, markDefKeyMap} = parseMarkDefs({
148
+ context,
149
+ markDefs: block.markDefs,
150
+ options,
186
151
  })
187
152
 
188
153
  const unparsedChildren: Array<unknown> = Array.isArray(block.children)
@@ -279,6 +244,66 @@ export function parseTextBlock({
279
244
  return parsedBlock
280
245
  }
281
246
 
247
+ export function parseMarkDefs({
248
+ context,
249
+ markDefs,
250
+ options,
251
+ }: {
252
+ context: Pick<EditorContext, 'keyGenerator' | 'schema'>
253
+ markDefs: unknown
254
+ options: {validateFields: boolean}
255
+ }): {
256
+ markDefs: Array<PortableTextObject>
257
+ markDefKeyMap: Map<string, string>
258
+ } {
259
+ const unparsedMarkDefs: Array<unknown> = Array.isArray(markDefs)
260
+ ? markDefs
261
+ : []
262
+ const markDefKeyMap = new Map<string, string>()
263
+
264
+ const parsedMarkDefs = unparsedMarkDefs.flatMap((markDef) => {
265
+ if (!isTypedObject(markDef)) {
266
+ return []
267
+ }
268
+
269
+ const schemaType = context.schema.annotations.find(
270
+ ({name}) => name === markDef._type,
271
+ )
272
+
273
+ if (!schemaType) {
274
+ return []
275
+ }
276
+
277
+ if (typeof markDef._key !== 'string') {
278
+ // If the `markDef` doesn't have a `_key` then we don't know what spans
279
+ // it belongs to and therefore we have to discard it.
280
+ return []
281
+ }
282
+
283
+ const parsedAnnotation = parseObject({
284
+ object: markDef,
285
+ context: {
286
+ schemaType,
287
+ keyGenerator: context.keyGenerator,
288
+ },
289
+ options,
290
+ })
291
+
292
+ if (!parsedAnnotation) {
293
+ return []
294
+ }
295
+
296
+ markDefKeyMap.set(markDef._key, parsedAnnotation._key)
297
+
298
+ return [parsedAnnotation]
299
+ })
300
+
301
+ return {
302
+ markDefs: parsedMarkDefs,
303
+ markDefKeyMap,
304
+ }
305
+ }
306
+
282
307
  export function parseChild({
283
308
  child,
284
309
  context,
@@ -8,7 +8,7 @@ import {getTextBlockText} from './util.get-text-block-text'
8
8
  */
9
9
  export function isEmptyTextBlock(
10
10
  context: Pick<EditorContext, 'schema'>,
11
- block: PortableTextBlock,
11
+ block: PortableTextBlock | unknown,
12
12
  ) {
13
13
  if (!isTextBlock(context, block)) {
14
14
  return false
@@ -1,212 +0,0 @@
1
- import type {EditorSchema} from '../editor/editor-schema'
2
- import {createPairRegex} from '../internal-utils/get-text-to-emphasize'
3
- import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
4
- import {getPreviousInlineObject} from '../selectors/selector.get-previous-inline-object'
5
- import {getSelectionStartPoint} from '../selectors/selector.get-selection-start-point'
6
- import {getBlockTextBefore} from '../selectors/selector.get-text-before'
7
- import type {BlockOffset} from '../types/block-offset'
8
- import {spanSelectionPointToBlockOffset} from '../utils/util.block-offset'
9
- import {blockOffsetsToSelection} from '../utils/util.block-offsets-to-selection'
10
- import {childSelectionPointToBlockOffset} from '../utils/util.child-selection-point-to-block-offset'
11
- import {effect, execute} from './behavior.types.action'
12
- import {defineBehavior} from './behavior.types.behavior'
13
-
14
- export function createDecoratorPairBehavior(config: {
15
- decorator: ({schema}: {schema: EditorSchema}) => string | undefined
16
- pair: {char: string; amount: number}
17
- onDecorate: (offset: BlockOffset) => void
18
- }) {
19
- if (config.pair.amount < 1) {
20
- console.warn(
21
- `The amount of characters in the pair should be greater than 0`,
22
- )
23
- }
24
-
25
- const pairRegex = createPairRegex(config.pair.char, config.pair.amount)
26
- const regEx = new RegExp(`(${pairRegex})$`)
27
-
28
- return defineBehavior({
29
- on: 'insert.text',
30
- guard: ({snapshot, event}) => {
31
- if (config.pair.amount < 1) {
32
- return false
33
- }
34
-
35
- const decorator = config.decorator({schema: snapshot.context.schema})
36
-
37
- if (decorator === undefined) {
38
- return false
39
- }
40
-
41
- const focusTextBlock = getFocusTextBlock(snapshot)
42
- const selectionStartPoint = getSelectionStartPoint(snapshot)
43
- const selectionStartOffset = selectionStartPoint
44
- ? spanSelectionPointToBlockOffset({
45
- context: {
46
- schema: snapshot.context.schema,
47
- value: snapshot.context.value,
48
- },
49
- selectionPoint: selectionStartPoint,
50
- })
51
- : undefined
52
-
53
- if (!focusTextBlock || !selectionStartOffset) {
54
- return false
55
- }
56
-
57
- const textBefore = getBlockTextBefore(snapshot)
58
- const newText = `${textBefore}${event.text}`
59
- const textToDecorate = newText.match(regEx)?.at(0)
60
-
61
- if (textToDecorate === undefined) {
62
- return false
63
- }
64
-
65
- const prefixOffsets = {
66
- anchor: {
67
- path: focusTextBlock.path,
68
- // Example: "foo **bar**".length - "**bar**".length = 4
69
- offset: newText.length - textToDecorate.length,
70
- },
71
- focus: {
72
- path: focusTextBlock.path,
73
- // Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
74
- offset:
75
- newText.length -
76
- textToDecorate.length +
77
- config.pair.char.length * config.pair.amount,
78
- },
79
- }
80
-
81
- const suffixOffsets = {
82
- anchor: {
83
- path: focusTextBlock.path,
84
- // Example: "foo **bar*|" (10) + "*".length - 2 = 9
85
- offset:
86
- selectionStartOffset.offset +
87
- event.text.length -
88
- config.pair.char.length * config.pair.amount,
89
- },
90
- focus: {
91
- path: focusTextBlock.path,
92
- // Example: "foo **bar*|" (10) + "*".length = 11
93
- offset: selectionStartOffset.offset + event.text.length,
94
- },
95
- }
96
-
97
- // If the prefix is more than one character, then we need to check if
98
- // there is an inline object inside it
99
- if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
100
- const prefixSelection = blockOffsetsToSelection({
101
- context: snapshot.context,
102
- offsets: prefixOffsets,
103
- })
104
- const inlineObjectBeforePrefixFocus = getPreviousInlineObject({
105
- ...snapshot,
106
- context: {
107
- ...snapshot.context,
108
- selection: prefixSelection
109
- ? {
110
- anchor: prefixSelection.focus,
111
- focus: prefixSelection.focus,
112
- }
113
- : null,
114
- },
115
- })
116
- const inlineObjectBeforePrefixFocusOffset =
117
- inlineObjectBeforePrefixFocus
118
- ? childSelectionPointToBlockOffset({
119
- context: {
120
- schema: snapshot.context.schema,
121
- value: snapshot.context.value,
122
- },
123
- selectionPoint: {
124
- path: inlineObjectBeforePrefixFocus.path,
125
- offset: 0,
126
- },
127
- })
128
- : undefined
129
-
130
- if (
131
- inlineObjectBeforePrefixFocusOffset &&
132
- inlineObjectBeforePrefixFocusOffset.offset >
133
- prefixOffsets.anchor.offset &&
134
- inlineObjectBeforePrefixFocusOffset.offset <
135
- prefixOffsets.focus.offset
136
- ) {
137
- return false
138
- }
139
- }
140
-
141
- // If the suffix is more than one character, then we need to check if
142
- // there is an inline object inside it
143
- if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
144
- const previousInlineObject = getPreviousInlineObject(snapshot)
145
- const previousInlineObjectOffset = previousInlineObject
146
- ? childSelectionPointToBlockOffset({
147
- context: {
148
- schema: snapshot.context.schema,
149
- value: snapshot.context.value,
150
- },
151
- selectionPoint: {
152
- path: previousInlineObject.path,
153
- offset: 0,
154
- },
155
- })
156
- : undefined
157
-
158
- if (
159
- previousInlineObjectOffset &&
160
- previousInlineObjectOffset.offset > suffixOffsets.anchor.offset &&
161
- previousInlineObjectOffset.offset < suffixOffsets.focus.offset
162
- ) {
163
- return false
164
- }
165
- }
166
-
167
- return {
168
- prefixOffsets,
169
- suffixOffsets,
170
- decorator,
171
- }
172
- },
173
- actions: [
174
- // Insert the text as usual in its own undo step
175
- ({event}) => [execute(event)],
176
- (_, {prefixOffsets, suffixOffsets, decorator}) => [
177
- // Decorate the text between the prefix and suffix
178
- execute({
179
- type: 'decorator.add',
180
- decorator,
181
- at: {
182
- anchor: prefixOffsets.focus,
183
- focus: suffixOffsets.anchor,
184
- },
185
- }),
186
- // Delete the suffix
187
- execute({
188
- type: 'delete.text',
189
- at: suffixOffsets,
190
- }),
191
- // Delete the prefix
192
- execute({
193
- type: 'delete.text',
194
- at: prefixOffsets,
195
- }),
196
- // Toggle the decorator off so the next inserted text isn't emphasized
197
- execute({
198
- type: 'decorator.remove',
199
- decorator,
200
- }),
201
- effect(() => {
202
- config.onDecorate({
203
- ...suffixOffsets.anchor,
204
- offset:
205
- suffixOffsets.anchor.offset -
206
- (prefixOffsets.focus.offset - prefixOffsets.anchor.offset),
207
- })
208
- }),
209
- ],
210
- ],
211
- })
212
- }