@portabletext/editor 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/lib/index.d.mts +911 -0
- package/lib/index.d.ts +911 -0
- package/lib/index.esm.js +4896 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +4874 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +4896 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/editor/Editable.tsx +683 -0
- package/src/editor/PortableTextEditor.tsx +308 -0
- package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
- package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
- package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
- package/src/editor/__tests__/handleClick.test.tsx +218 -0
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
- package/src/editor/__tests__/utils.ts +39 -0
- package/src/editor/components/DraggableBlock.tsx +287 -0
- package/src/editor/components/Element.tsx +279 -0
- package/src/editor/components/Leaf.tsx +288 -0
- package/src/editor/components/SlateContainer.tsx +81 -0
- package/src/editor/components/Synchronizer.tsx +190 -0
- package/src/editor/hooks/usePortableTextEditor.ts +23 -0
- package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
- package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
- package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
- package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
- package/src/editor/hooks/useSyncValue.test.tsx +125 -0
- package/src/editor/hooks/useSyncValue.ts +372 -0
- package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
- package/src/editor/nodes/DefaultObject.tsx +15 -0
- package/src/editor/nodes/index.ts +189 -0
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
- package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
- package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
- package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
- package/src/editor/plugins/createWithEditableAPI.ts +573 -0
- package/src/editor/plugins/createWithHotKeys.ts +304 -0
- package/src/editor/plugins/createWithInsertBreak.ts +45 -0
- package/src/editor/plugins/createWithInsertData.ts +359 -0
- package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
- package/src/editor/plugins/createWithObjectKeys.ts +63 -0
- package/src/editor/plugins/createWithPatches.ts +274 -0
- package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
- package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
- package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
- package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
- package/src/editor/plugins/createWithUndoRedo.ts +494 -0
- package/src/editor/plugins/createWithUtils.ts +81 -0
- package/src/editor/plugins/index.ts +155 -0
- package/src/index.ts +11 -0
- package/src/patch/PatchEvent.ts +33 -0
- package/src/patch/applyPatch.ts +29 -0
- package/src/patch/array.ts +89 -0
- package/src/patch/arrayInsert.ts +27 -0
- package/src/patch/object.ts +39 -0
- package/src/patch/patches.ts +53 -0
- package/src/patch/primitive.ts +43 -0
- package/src/patch/string.ts +51 -0
- package/src/types/editor.ts +576 -0
- package/src/types/options.ts +17 -0
- package/src/types/patch.ts +65 -0
- package/src/types/slate.ts +25 -0
- package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
- package/src/utils/__tests__/operationToPatches.test.ts +421 -0
- package/src/utils/__tests__/patchToOperations.test.ts +293 -0
- package/src/utils/__tests__/ranges.test.ts +18 -0
- package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
- package/src/utils/__tests__/values.test.ts +253 -0
- package/src/utils/applyPatch.ts +407 -0
- package/src/utils/bufferUntil.ts +15 -0
- package/src/utils/debug.ts +12 -0
- package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
- package/src/utils/operationToPatches.ts +357 -0
- package/src/utils/patches.ts +36 -0
- package/src/utils/paths.ts +60 -0
- package/src/utils/ranges.ts +77 -0
- package/src/utils/schema.ts +8 -0
- package/src/utils/selection.ts +65 -0
- package/src/utils/ucs2Indices.ts +67 -0
- package/src/utils/validateValue.ts +394 -0
- package/src/utils/values.ts +208 -0
- package/src/utils/weakMaps.ts +24 -0
- package/src/utils/withChanges.ts +25 -0
- package/src/utils/withPreserveKeys.ts +14 -0
- package/src/utils/withoutPatching.ts +14 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPortableTextSpan,
|
|
3
|
+
type ObjectSchemaType,
|
|
4
|
+
type Path,
|
|
5
|
+
type PortableTextBlock,
|
|
6
|
+
type PortableTextChild,
|
|
7
|
+
type PortableTextObject,
|
|
8
|
+
type PortableTextTextBlock,
|
|
9
|
+
type SchemaType,
|
|
10
|
+
} from '@sanity/types'
|
|
11
|
+
import {Editor, Element as SlateElement, Node, Range, Text, Transforms} from 'slate'
|
|
12
|
+
import {ReactEditor} from 'slate-react'
|
|
13
|
+
import {type DOMNode} from 'slate-react/dist/utils/dom'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type EditableAPIDeleteOptions,
|
|
17
|
+
type EditorSelection,
|
|
18
|
+
type PortableTextMemberSchemaTypes,
|
|
19
|
+
type PortableTextSlateEditor,
|
|
20
|
+
} from '../../types/editor'
|
|
21
|
+
import {debugWithName} from '../../utils/debug'
|
|
22
|
+
import {toPortableTextRange, toSlateRange} from '../../utils/ranges'
|
|
23
|
+
import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values'
|
|
24
|
+
import {KEY_TO_VALUE_ELEMENT, SLATE_TO_PORTABLE_TEXT_RANGE} from '../../utils/weakMaps'
|
|
25
|
+
import {type PortableTextEditor} from '../PortableTextEditor'
|
|
26
|
+
|
|
27
|
+
const debug = debugWithName('API:editable')
|
|
28
|
+
|
|
29
|
+
export function createWithEditableAPI(
|
|
30
|
+
portableTextEditor: PortableTextEditor,
|
|
31
|
+
types: PortableTextMemberSchemaTypes,
|
|
32
|
+
keyGenerator: () => string,
|
|
33
|
+
) {
|
|
34
|
+
return function withEditableAPI(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
35
|
+
portableTextEditor.setEditable({
|
|
36
|
+
focus: (): void => {
|
|
37
|
+
ReactEditor.focus(editor)
|
|
38
|
+
},
|
|
39
|
+
blur: (): void => {
|
|
40
|
+
ReactEditor.blur(editor)
|
|
41
|
+
},
|
|
42
|
+
toggleMark: (mark: string): void => {
|
|
43
|
+
editor.pteToggleMark(mark)
|
|
44
|
+
},
|
|
45
|
+
toggleList: (listStyle: string): void => {
|
|
46
|
+
editor.pteToggleListItem(listStyle)
|
|
47
|
+
},
|
|
48
|
+
toggleBlockStyle: (blockStyle: string): void => {
|
|
49
|
+
editor.pteToggleBlockStyle(blockStyle)
|
|
50
|
+
},
|
|
51
|
+
isMarkActive: (mark: string): boolean => {
|
|
52
|
+
// Try/catch this, as Slate may error because the selection is currently wrong
|
|
53
|
+
// TODO: catch only relevant error from Slate
|
|
54
|
+
try {
|
|
55
|
+
return editor.pteIsMarkActive(mark)
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn(err)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
marks: (): string[] => {
|
|
62
|
+
return (
|
|
63
|
+
{
|
|
64
|
+
...(Editor.marks(editor) || {}),
|
|
65
|
+
}.marks || []
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
undo: (): void => editor.undo(),
|
|
69
|
+
redo: (): void => editor.redo(),
|
|
70
|
+
select: (selection: EditorSelection): void => {
|
|
71
|
+
const slateSelection = toSlateRange(selection, editor)
|
|
72
|
+
if (slateSelection) {
|
|
73
|
+
Transforms.select(editor, slateSelection)
|
|
74
|
+
} else {
|
|
75
|
+
Transforms.deselect(editor)
|
|
76
|
+
}
|
|
77
|
+
editor.onChange()
|
|
78
|
+
},
|
|
79
|
+
focusBlock: (): PortableTextBlock | undefined => {
|
|
80
|
+
if (editor.selection) {
|
|
81
|
+
const block = Node.descendant(editor, editor.selection.focus.path.slice(0, 1))
|
|
82
|
+
if (block) {
|
|
83
|
+
return fromSlateValue([block], types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined
|
|
87
|
+
},
|
|
88
|
+
focusChild: (): PortableTextChild | undefined => {
|
|
89
|
+
if (editor.selection) {
|
|
90
|
+
const block = Node.descendant(editor, editor.selection.focus.path.slice(0, 1))
|
|
91
|
+
if (block && editor.isTextBlock(block)) {
|
|
92
|
+
const ptBlock = fromSlateValue(
|
|
93
|
+
[block],
|
|
94
|
+
types.block.name,
|
|
95
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
96
|
+
)[0] as PortableTextTextBlock
|
|
97
|
+
return ptBlock.children[editor.selection.focus.path[1]]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined
|
|
101
|
+
},
|
|
102
|
+
insertChild: (type: SchemaType, value?: {[prop: string]: any}): Path => {
|
|
103
|
+
if (!editor.selection) {
|
|
104
|
+
throw new Error('The editor has no selection')
|
|
105
|
+
}
|
|
106
|
+
const [focusBlock] = Array.from(
|
|
107
|
+
Editor.nodes(editor, {
|
|
108
|
+
at: editor.selection.focus.path.slice(0, 1),
|
|
109
|
+
match: (n) => n._type === types.block.name,
|
|
110
|
+
}),
|
|
111
|
+
)[0] || [undefined]
|
|
112
|
+
if (!focusBlock) {
|
|
113
|
+
throw new Error('No focused text block')
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
type.name !== types.span.name &&
|
|
117
|
+
!types.inlineObjects.some((t) => t.name === type.name)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error('This type cannot be inserted as a child to a text block')
|
|
120
|
+
}
|
|
121
|
+
const block = toSlateValue(
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
_key: keyGenerator(),
|
|
125
|
+
_type: types.block.name,
|
|
126
|
+
children: [
|
|
127
|
+
{
|
|
128
|
+
_key: keyGenerator(),
|
|
129
|
+
_type: type.name,
|
|
130
|
+
...(value ? value : {}),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
portableTextEditor,
|
|
136
|
+
)[0] as unknown as SlateElement
|
|
137
|
+
const child = block.children[0]
|
|
138
|
+
const focusChildPath = editor.selection.focus.path.slice(0, 2)
|
|
139
|
+
const isSpanNode = child._type === types.span.name
|
|
140
|
+
const focusNode = Node.get(editor, focusChildPath)
|
|
141
|
+
|
|
142
|
+
// If we are inserting a span, and currently have focus on an inline object,
|
|
143
|
+
// move the selection to the next span (guaranteed by normalizing rules) before inserting it.
|
|
144
|
+
if (isSpanNode && focusNode._type !== types.span.name) {
|
|
145
|
+
debug('Inserting span child next to inline object child, moving selection + 1')
|
|
146
|
+
editor.move({distance: 1, unit: 'character'})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Transforms.insertNodes(editor, child, {
|
|
150
|
+
select: true,
|
|
151
|
+
at: editor.selection,
|
|
152
|
+
})
|
|
153
|
+
editor.onChange()
|
|
154
|
+
return (
|
|
155
|
+
toPortableTextRange(
|
|
156
|
+
fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
|
|
157
|
+
editor.selection,
|
|
158
|
+
types,
|
|
159
|
+
)?.focus.path || []
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
insertBlock: (type: SchemaType, value?: {[prop: string]: any}): Path => {
|
|
163
|
+
if (!editor.selection) {
|
|
164
|
+
throw new Error('The editor has no selection')
|
|
165
|
+
}
|
|
166
|
+
const block = toSlateValue(
|
|
167
|
+
[
|
|
168
|
+
{
|
|
169
|
+
_key: keyGenerator(),
|
|
170
|
+
_type: type.name,
|
|
171
|
+
...(value ? value : {}),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
portableTextEditor,
|
|
175
|
+
)[0] as unknown as Node
|
|
176
|
+
const [focusBlock] = Array.from(
|
|
177
|
+
Editor.nodes(editor, {
|
|
178
|
+
at: editor.selection.focus.path.slice(0, 1),
|
|
179
|
+
match: (n) => n._type === types.block.name,
|
|
180
|
+
}),
|
|
181
|
+
)[0] || [undefined]
|
|
182
|
+
|
|
183
|
+
const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types)
|
|
184
|
+
|
|
185
|
+
if (isEmptyTextBlock) {
|
|
186
|
+
// If the text block is empty, remove it before inserting the new block.
|
|
187
|
+
Transforms.removeNodes(editor, {at: editor.selection})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Editor.insertNode(editor, block)
|
|
191
|
+
editor.onChange()
|
|
192
|
+
return (
|
|
193
|
+
toPortableTextRange(
|
|
194
|
+
fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
|
|
195
|
+
editor.selection,
|
|
196
|
+
types,
|
|
197
|
+
)?.focus.path || []
|
|
198
|
+
)
|
|
199
|
+
},
|
|
200
|
+
hasBlockStyle: (style: string): boolean => {
|
|
201
|
+
try {
|
|
202
|
+
return editor.pteHasBlockStyle(style)
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// This is fine.
|
|
205
|
+
// debug(err)
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
hasListStyle: (listStyle: string): boolean => {
|
|
210
|
+
try {
|
|
211
|
+
return editor.pteHasListStyle(listStyle)
|
|
212
|
+
} catch (err) {
|
|
213
|
+
// This is fine.
|
|
214
|
+
// debug(err)
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
isVoid: (element: PortableTextBlock | PortableTextChild) => {
|
|
219
|
+
return ![types.block.name, types.span.name].includes(element._type)
|
|
220
|
+
},
|
|
221
|
+
findByPath: (
|
|
222
|
+
path: Path,
|
|
223
|
+
): [PortableTextBlock | PortableTextChild | undefined, Path | undefined] => {
|
|
224
|
+
const slatePath = toSlateRange(
|
|
225
|
+
{focus: {path, offset: 0}, anchor: {path, offset: 0}},
|
|
226
|
+
editor,
|
|
227
|
+
)
|
|
228
|
+
if (slatePath) {
|
|
229
|
+
const [block, blockPath] = Editor.node(editor, slatePath.focus.path.slice(0, 1))
|
|
230
|
+
if (block && blockPath && typeof block._key === 'string') {
|
|
231
|
+
if (path.length === 1 && slatePath.focus.path.length === 1) {
|
|
232
|
+
return [fromSlateValue([block], types.block.name)[0], [{_key: block._key}]]
|
|
233
|
+
}
|
|
234
|
+
const ptBlock = fromSlateValue(
|
|
235
|
+
[block],
|
|
236
|
+
types.block.name,
|
|
237
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
238
|
+
)[0]
|
|
239
|
+
if (editor.isTextBlock(ptBlock)) {
|
|
240
|
+
const ptChild = ptBlock.children[slatePath.focus.path[1]]
|
|
241
|
+
if (ptChild) {
|
|
242
|
+
return [ptChild, [{_key: block._key}, 'children', {_key: ptChild._key}]]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return [undefined, undefined]
|
|
248
|
+
},
|
|
249
|
+
findDOMNode: (element: PortableTextBlock | PortableTextChild): DOMNode | undefined => {
|
|
250
|
+
let node: DOMNode | undefined
|
|
251
|
+
try {
|
|
252
|
+
const [item] = Array.from(
|
|
253
|
+
Editor.nodes(editor, {
|
|
254
|
+
at: [],
|
|
255
|
+
match: (n) => n._key === element._key,
|
|
256
|
+
}) || [],
|
|
257
|
+
)[0] || [undefined]
|
|
258
|
+
node = ReactEditor.toDOMNode(editor, item)
|
|
259
|
+
} catch (err) {
|
|
260
|
+
// Nothing
|
|
261
|
+
}
|
|
262
|
+
return node
|
|
263
|
+
},
|
|
264
|
+
activeAnnotations: (): PortableTextObject[] => {
|
|
265
|
+
if (!editor.selection || editor.selection.focus.path.length < 2) {
|
|
266
|
+
return []
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const activeAnnotations: PortableTextObject[] = []
|
|
270
|
+
const spans = Editor.nodes(editor, {
|
|
271
|
+
at: editor.selection,
|
|
272
|
+
match: (node) =>
|
|
273
|
+
Text.isText(node) &&
|
|
274
|
+
node.marks !== undefined &&
|
|
275
|
+
Array.isArray(node.marks) &&
|
|
276
|
+
node.marks.length > 0,
|
|
277
|
+
})
|
|
278
|
+
for (const [span, path] of spans) {
|
|
279
|
+
const [block] = Editor.node(editor, path, {depth: 1})
|
|
280
|
+
if (editor.isTextBlock(block)) {
|
|
281
|
+
block.markDefs?.forEach((def) => {
|
|
282
|
+
if (
|
|
283
|
+
Text.isText(span) &&
|
|
284
|
+
span.marks &&
|
|
285
|
+
Array.isArray(span.marks) &&
|
|
286
|
+
span.marks.includes(def._key)
|
|
287
|
+
) {
|
|
288
|
+
activeAnnotations.push(def)
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return activeAnnotations
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return []
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
isAnnotationActive: (annotationType: PortableTextObject['_type']): boolean => {
|
|
299
|
+
if (!editor.selection || editor.selection.focus.path.length < 2) {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const spans = [
|
|
305
|
+
...Editor.nodes(editor, {
|
|
306
|
+
at: editor.selection,
|
|
307
|
+
match: (node) => Text.isText(node),
|
|
308
|
+
}),
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
spans.some(
|
|
313
|
+
([span]) => !isPortableTextSpan(span) || !span.marks || span.marks?.length === 0,
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
return false
|
|
317
|
+
|
|
318
|
+
const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => {
|
|
319
|
+
const [block] = Editor.node(editor, path, {depth: 1})
|
|
320
|
+
if (editor.isTextBlock(block) && block.markDefs) {
|
|
321
|
+
return [...accMarkDefs, ...block.markDefs]
|
|
322
|
+
}
|
|
323
|
+
return accMarkDefs
|
|
324
|
+
}, [] as PortableTextObject[])
|
|
325
|
+
|
|
326
|
+
return spans.every(([span]) => {
|
|
327
|
+
if (!isPortableTextSpan(span)) return false
|
|
328
|
+
|
|
329
|
+
const spanMarkDefs = span.marks?.map(
|
|
330
|
+
(markKey) => selectionMarkDefs.find((def) => def?._key === markKey)?._type,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return spanMarkDefs?.includes(annotationType)
|
|
334
|
+
})
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
addAnnotation: (
|
|
340
|
+
type: ObjectSchemaType,
|
|
341
|
+
value?: {[prop: string]: unknown},
|
|
342
|
+
): {spanPath: Path; markDefPath: Path} | undefined => {
|
|
343
|
+
const {selection: originalSelection} = editor
|
|
344
|
+
let returnValue: {spanPath: Path; markDefPath: Path} | undefined = undefined
|
|
345
|
+
if (originalSelection) {
|
|
346
|
+
const [block] = Editor.node(editor, originalSelection.focus, {depth: 1})
|
|
347
|
+
if (!editor.isTextBlock(block)) {
|
|
348
|
+
return undefined
|
|
349
|
+
}
|
|
350
|
+
if (Range.isCollapsed(originalSelection)) {
|
|
351
|
+
editor.pteExpandToWord()
|
|
352
|
+
editor.onChange()
|
|
353
|
+
}
|
|
354
|
+
const [textNode] = Editor.node(editor, originalSelection.focus, {depth: 2})
|
|
355
|
+
|
|
356
|
+
// If we still have a selection, add the annotation to the selected text
|
|
357
|
+
if (editor.selection) {
|
|
358
|
+
Editor.withoutNormalizing(editor, () => {
|
|
359
|
+
// Add markDefs to the block
|
|
360
|
+
const annotationKey = keyGenerator()
|
|
361
|
+
Transforms.setNodes(
|
|
362
|
+
editor,
|
|
363
|
+
{
|
|
364
|
+
markDefs: [
|
|
365
|
+
...(block.markDefs || []),
|
|
366
|
+
{_type: type.name, _key: annotationKey, ...value} as PortableTextObject,
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
{at: originalSelection.focus},
|
|
370
|
+
)
|
|
371
|
+
editor.onChange()
|
|
372
|
+
|
|
373
|
+
// Split if needed
|
|
374
|
+
Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
|
|
375
|
+
editor.onChange()
|
|
376
|
+
|
|
377
|
+
// Add marks to the span node
|
|
378
|
+
if (editor.selection && Text.isText(textNode)) {
|
|
379
|
+
Transforms.setNodes(
|
|
380
|
+
editor,
|
|
381
|
+
{
|
|
382
|
+
marks: [...((textNode.marks || []) as string[]), annotationKey],
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
at: editor.selection,
|
|
386
|
+
match: (n) => n._type === types.span.name,
|
|
387
|
+
},
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
editor.onChange()
|
|
391
|
+
if (editor.selection) {
|
|
392
|
+
// Insert an empty string to continue writing non-annotated text
|
|
393
|
+
Transforms.insertNodes(
|
|
394
|
+
editor,
|
|
395
|
+
[{_type: 'span', text: '', marks: [], _key: keyGenerator()}],
|
|
396
|
+
{
|
|
397
|
+
at: Range.end(editor.selection),
|
|
398
|
+
},
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
const newPortableTextEditorSelection = toPortableTextRange(
|
|
402
|
+
fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
|
|
403
|
+
editor.selection,
|
|
404
|
+
types,
|
|
405
|
+
)
|
|
406
|
+
if (newPortableTextEditorSelection) {
|
|
407
|
+
returnValue = {
|
|
408
|
+
spanPath: newPortableTextEditorSelection.focus.path,
|
|
409
|
+
markDefPath: [{_key: block._key}, 'markDefs', {_key: annotationKey}],
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
Editor.normalize(editor)
|
|
414
|
+
editor.onChange()
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return returnValue
|
|
418
|
+
},
|
|
419
|
+
delete: (selection: EditorSelection, options?: EditableAPIDeleteOptions): void => {
|
|
420
|
+
if (selection) {
|
|
421
|
+
const range = toSlateRange(selection, editor)
|
|
422
|
+
const hasRange = range && range.anchor.path.length > 0 && range.focus.path.length > 0
|
|
423
|
+
if (!hasRange) {
|
|
424
|
+
throw new Error('Invalid range')
|
|
425
|
+
}
|
|
426
|
+
if (range) {
|
|
427
|
+
if (!options?.mode || options?.mode === 'selected') {
|
|
428
|
+
debug(`Deleting content in selection`)
|
|
429
|
+
Transforms.delete(editor, {
|
|
430
|
+
at: range,
|
|
431
|
+
hanging: true,
|
|
432
|
+
voids: true,
|
|
433
|
+
})
|
|
434
|
+
editor.onChange()
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
if (options?.mode === 'blocks') {
|
|
438
|
+
debug(`Deleting blocks touched by selection`)
|
|
439
|
+
Transforms.removeNodes(editor, {
|
|
440
|
+
at: range,
|
|
441
|
+
voids: true,
|
|
442
|
+
match: (node) => {
|
|
443
|
+
return (
|
|
444
|
+
editor.isTextBlock(node) ||
|
|
445
|
+
(!editor.isTextBlock(node) && SlateElement.isElement(node))
|
|
446
|
+
)
|
|
447
|
+
},
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
if (options?.mode === 'children') {
|
|
451
|
+
debug(`Deleting children touched by selection`)
|
|
452
|
+
Transforms.removeNodes(editor, {
|
|
453
|
+
at: range,
|
|
454
|
+
voids: true,
|
|
455
|
+
match: (node) => {
|
|
456
|
+
return (
|
|
457
|
+
node._type === types.span.name || // Text children
|
|
458
|
+
(!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks
|
|
459
|
+
)
|
|
460
|
+
},
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
// If the editor was emptied, insert a placeholder block
|
|
464
|
+
// directly into the editor's children. We don't want to do this
|
|
465
|
+
// through a Transform (because that would trigger a change event
|
|
466
|
+
// that would insert the placeholder into the actual value
|
|
467
|
+
// which should remain empty)
|
|
468
|
+
if (editor.children.length === 0) {
|
|
469
|
+
editor.children = [editor.pteCreateEmptyBlock()]
|
|
470
|
+
}
|
|
471
|
+
editor.onChange()
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
removeAnnotation: (type: ObjectSchemaType): void => {
|
|
476
|
+
let {selection} = editor
|
|
477
|
+
debug('Removing annotation', type)
|
|
478
|
+
if (selection) {
|
|
479
|
+
// Select the whole annotation if collapsed
|
|
480
|
+
if (Range.isCollapsed(selection)) {
|
|
481
|
+
const [node, nodePath] = Editor.node(editor, selection, {depth: 2})
|
|
482
|
+
if (Text.isText(node) && node.marks && typeof node.text === 'string') {
|
|
483
|
+
Transforms.select(editor, nodePath)
|
|
484
|
+
selection = editor.selection
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Do this without normalization or span references will be unstable!
|
|
488
|
+
Editor.withoutNormalizing(editor, () => {
|
|
489
|
+
if (selection && Range.isExpanded(selection)) {
|
|
490
|
+
selection = editor.selection
|
|
491
|
+
if (!selection) {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
// Find the selected block, to identify the annotation to remove
|
|
495
|
+
const blocks = [
|
|
496
|
+
...Editor.nodes(editor, {
|
|
497
|
+
at: selection,
|
|
498
|
+
match: (node) => {
|
|
499
|
+
return (
|
|
500
|
+
editor.isTextBlock(node) &&
|
|
501
|
+
Array.isArray(node.markDefs) &&
|
|
502
|
+
node.markDefs.some((def) => def._type === type.name)
|
|
503
|
+
)
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
]
|
|
507
|
+
const removedMarks: string[] = []
|
|
508
|
+
|
|
509
|
+
// Removes the marks from the text nodes
|
|
510
|
+
blocks.forEach(([block]) => {
|
|
511
|
+
if (editor.isTextBlock(block) && Array.isArray(block.markDefs)) {
|
|
512
|
+
const marksToRemove = block.markDefs.filter((def) => def._type === type.name)
|
|
513
|
+
marksToRemove.forEach((def) => {
|
|
514
|
+
if (!removedMarks.includes(def._key)) removedMarks.push(def._key)
|
|
515
|
+
Editor.removeMark(editor, def._key)
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
Editor.normalize(editor)
|
|
522
|
+
editor.onChange()
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
getSelection: (): EditorSelection | null => {
|
|
526
|
+
let ptRange: EditorSelection = null
|
|
527
|
+
if (editor.selection) {
|
|
528
|
+
const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection)
|
|
529
|
+
if (existing) {
|
|
530
|
+
return existing
|
|
531
|
+
}
|
|
532
|
+
ptRange = toPortableTextRange(
|
|
533
|
+
fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
|
|
534
|
+
editor.selection,
|
|
535
|
+
types,
|
|
536
|
+
)
|
|
537
|
+
SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
|
|
538
|
+
}
|
|
539
|
+
return ptRange
|
|
540
|
+
},
|
|
541
|
+
getValue: () => {
|
|
542
|
+
return fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))
|
|
543
|
+
},
|
|
544
|
+
isCollapsedSelection: () => {
|
|
545
|
+
return !!editor.selection && Range.isCollapsed(editor.selection)
|
|
546
|
+
},
|
|
547
|
+
isExpandedSelection: () => {
|
|
548
|
+
return !!editor.selection && Range.isExpanded(editor.selection)
|
|
549
|
+
},
|
|
550
|
+
insertBreak: () => {
|
|
551
|
+
editor.insertBreak()
|
|
552
|
+
editor.onChange()
|
|
553
|
+
},
|
|
554
|
+
getFragment: () => {
|
|
555
|
+
return fromSlateValue(editor.getFragment(), types.block.name)
|
|
556
|
+
},
|
|
557
|
+
isSelectionsOverlapping: (selectionA: EditorSelection, selectionB: EditorSelection) => {
|
|
558
|
+
// Convert the selections to Slate ranges
|
|
559
|
+
const rangeA = toSlateRange(selectionA, editor)
|
|
560
|
+
const rangeB = toSlateRange(selectionB, editor)
|
|
561
|
+
|
|
562
|
+
// Make sure the ranges are valid
|
|
563
|
+
const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
|
|
564
|
+
|
|
565
|
+
// Check if the ranges are overlapping
|
|
566
|
+
const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
|
|
567
|
+
|
|
568
|
+
return isOverlapping
|
|
569
|
+
},
|
|
570
|
+
})
|
|
571
|
+
return editor
|
|
572
|
+
}
|
|
573
|
+
}
|