@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.
- package/lib/_chunks-dts/index.d.ts +16 -10
- package/lib/_chunks-es/util.slice-blocks.js +44 -25
- package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
- package/lib/_chunks-es/util.slice-text-block.js.map +1 -1
- package/lib/index.js +203 -251
- package/lib/index.js.map +1 -1
- package/lib/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/behaviors/behavior.types.event.ts +1 -1
- package/src/editor/Editable.tsx +5 -4
- package/src/editor/PortableTextEditor.tsx +14 -0
- package/src/editor/plugins/createWithEditableAPI.ts +18 -83
- package/src/editor/plugins/createWithPatches.ts +1 -1
- package/src/editor/plugins/createWithPlaceholderBlock.ts +5 -1
- package/src/editor/plugins/with-plugins.ts +8 -14
- package/src/editor/sync-machine.ts +10 -5
- package/src/internal-utils/applyPatch.ts +19 -14
- package/src/operations/behavior.operation.block.set.ts +82 -27
- package/src/operations/behavior.operation.block.unset.ts +26 -58
- package/src/operations/behavior.operation.delete.ts +19 -1
- package/src/operations/behavior.operation.insert.block.ts +4 -4
- package/src/types/editor.ts +0 -9
- package/src/utils/parse-blocks.ts +64 -39
- package/src/utils/util.is-empty-text-block.ts +1 -1
- package/src/behaviors/behavior.decorator-pair.ts +0 -212
- package/src/behaviors/behavior.markdown.ts +0 -478
- package/src/editor/plugins/createWithUtils.ts +0 -52
- package/src/internal-utils/get-text-to-emphasize.test.ts +0 -60
- package/src/internal-utils/get-text-to-emphasize.ts +0 -40
|
@@ -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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
247
|
+
if (isEmptyTextBlock(context, endBlock)) {
|
|
248
248
|
Transforms.insertNodes(editor, [block], {
|
|
249
249
|
at: endBlockPath,
|
|
250
250
|
select: false,
|
package/src/types/editor.ts
CHANGED
|
@@ -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
|
|
148
|
-
|
|
149
|
-
:
|
|
150
|
-
|
|
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
|
-
}
|