@portabletext/editor 1.6.0 → 1.7.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/README.md +5 -0
- package/lib/index.d.mts +3317 -3488
- package/lib/index.d.ts +3317 -3488
- package/lib/index.esm.js +4337 -4089
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +4325 -4077
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +4337 -4089
- package/lib/index.mjs.map +1 -1
- package/package.json +10 -11
- package/src/editor/Editable.tsx +26 -28
- package/src/editor/PortableTextEditor.tsx +90 -66
- package/src/editor/behavior/behavior.action.insert-break.ts +12 -2
- package/src/editor/behavior/behavior.actions.ts +51 -11
- package/src/editor/behavior/behavior.types.ts +23 -0
- package/src/editor/components/Synchronizer.tsx +11 -4
- package/src/editor/create-slate-editor.tsx +67 -0
- package/src/editor/editor-machine.ts +58 -8
- package/src/editor/key-generator.ts +30 -1
- package/src/editor/plugins/create-with-event-listeners.ts +62 -1
- package/src/editor/plugins/createWithEditableAPI.ts +800 -728
- package/src/editor/plugins/createWithMaxBlocks.ts +7 -2
- package/src/editor/plugins/createWithPatches.ts +4 -4
- package/src/editor/plugins/createWithPlaceholderBlock.ts +8 -3
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +3 -4
- package/src/editor/plugins/createWithUndoRedo.ts +6 -7
- package/src/editor/plugins/createWithUtils.ts +2 -8
- package/src/editor/plugins/{index.ts → with-plugins.ts} +22 -79
- package/src/editor/use-editor.ts +46 -14
- package/src/editor/withSyncRangeDecorations.ts +20 -0
- package/src/index.ts +9 -1
- package/src/types/editor.ts +0 -1
- package/src/types/options.ts +1 -3
- package/src/utils/__tests__/operationToPatches.test.ts +7 -14
- package/src/utils/__tests__/patchToOperations.test.ts +4 -7
- package/src/editor/components/SlateContainer.tsx +0 -79
- package/src/editor/hooks/usePortableTextReadOnly.ts +0 -20
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
EditableAPI,
|
|
23
23
|
EditableAPIDeleteOptions,
|
|
24
24
|
EditorSelection,
|
|
25
|
-
PortableTextMemberSchemaTypes,
|
|
26
25
|
PortableTextSlateEditor,
|
|
27
26
|
} from '../../types/editor'
|
|
28
27
|
import {debugWithName} from '../../utils/debug'
|
|
@@ -36,235 +35,205 @@ import {
|
|
|
36
35
|
KEY_TO_VALUE_ELEMENT,
|
|
37
36
|
SLATE_TO_PORTABLE_TEXT_RANGE,
|
|
38
37
|
} from '../../utils/weakMaps'
|
|
38
|
+
import type {BehaviourActionImplementation} from '../behavior/behavior.actions'
|
|
39
39
|
import type {EditorActor} from '../editor-machine'
|
|
40
|
-
import type {PortableTextEditor} from '../PortableTextEditor'
|
|
41
40
|
import {isDecoratorActive} from './createWithPortableTextMarkModel'
|
|
42
41
|
|
|
43
42
|
const debug = debugWithName('API:editable')
|
|
44
43
|
|
|
45
|
-
export function
|
|
44
|
+
export function createEditableAPI(
|
|
45
|
+
editor: PortableTextSlateEditor,
|
|
46
46
|
editorActor: EditorActor,
|
|
47
|
-
portableTextEditor: PortableTextEditor,
|
|
48
|
-
types: PortableTextMemberSchemaTypes,
|
|
49
47
|
) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
focus
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
blur
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
const types = editorActor.getSnapshot().context.schema
|
|
49
|
+
|
|
50
|
+
const editableApi: EditableAPI = {
|
|
51
|
+
focus: (): void => {
|
|
52
|
+
ReactEditor.focus(editor)
|
|
53
|
+
},
|
|
54
|
+
blur: (): void => {
|
|
55
|
+
ReactEditor.blur(editor)
|
|
56
|
+
},
|
|
57
|
+
toggleMark: (mark: string): void => {
|
|
58
|
+
editorActor.send({
|
|
59
|
+
type: 'behavior event',
|
|
60
|
+
behaviorEvent: {
|
|
61
|
+
type: 'decorator.toggle',
|
|
62
|
+
decorator: mark,
|
|
63
|
+
},
|
|
64
|
+
editor,
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
toggleList: (listStyle: string): void => {
|
|
68
|
+
editor.pteToggleListItem(listStyle)
|
|
69
|
+
},
|
|
70
|
+
toggleBlockStyle: (blockStyle: string): void => {
|
|
71
|
+
editor.pteToggleBlockStyle(blockStyle)
|
|
72
|
+
},
|
|
73
|
+
isMarkActive: (mark: string): boolean => {
|
|
74
|
+
// Try/catch this, as Slate may error because the selection is currently wrong
|
|
75
|
+
// TODO: catch only relevant error from Slate
|
|
76
|
+
try {
|
|
77
|
+
return isDecoratorActive({editor, decorator: mark})
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.warn(err)
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
marks: (): string[] => {
|
|
84
|
+
return (
|
|
85
|
+
{
|
|
86
|
+
...(Editor.marks(editor) || {}),
|
|
87
|
+
}.marks || []
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
undo: (): void => editor.undo(),
|
|
91
|
+
redo: (): void => editor.redo(),
|
|
92
|
+
select: (selection: EditorSelection): void => {
|
|
93
|
+
const slateSelection = toSlateRange(selection, editor)
|
|
94
|
+
if (slateSelection) {
|
|
95
|
+
Transforms.select(editor, slateSelection)
|
|
96
|
+
} else {
|
|
97
|
+
Transforms.deselect(editor)
|
|
98
|
+
}
|
|
99
|
+
editor.onChange()
|
|
100
|
+
},
|
|
101
|
+
focusBlock: (): PortableTextBlock | undefined => {
|
|
102
|
+
if (editor.selection) {
|
|
103
|
+
const block = Node.descendant(
|
|
67
104
|
editor,
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
toggleList: (listStyle: string): void => {
|
|
71
|
-
editor.pteToggleListItem(listStyle)
|
|
72
|
-
},
|
|
73
|
-
toggleBlockStyle: (blockStyle: string): void => {
|
|
74
|
-
editor.pteToggleBlockStyle(blockStyle)
|
|
75
|
-
},
|
|
76
|
-
isMarkActive: (mark: string): boolean => {
|
|
77
|
-
// Try/catch this, as Slate may error because the selection is currently wrong
|
|
78
|
-
// TODO: catch only relevant error from Slate
|
|
79
|
-
try {
|
|
80
|
-
return isDecoratorActive({editor, decorator: mark})
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.warn(err)
|
|
83
|
-
return false
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
marks: (): string[] => {
|
|
87
|
-
return (
|
|
88
|
-
{
|
|
89
|
-
...(Editor.marks(editor) || {}),
|
|
90
|
-
}.marks || []
|
|
105
|
+
editor.selection.focus.path.slice(0, 1),
|
|
91
106
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Transforms.select(editor, slateSelection)
|
|
99
|
-
} else {
|
|
100
|
-
Transforms.deselect(editor)
|
|
101
|
-
}
|
|
102
|
-
editor.onChange()
|
|
103
|
-
},
|
|
104
|
-
focusBlock: (): PortableTextBlock | undefined => {
|
|
105
|
-
if (editor.selection) {
|
|
106
|
-
const block = Node.descendant(
|
|
107
|
-
editor,
|
|
108
|
-
editor.selection.focus.path.slice(0, 1),
|
|
109
|
-
)
|
|
110
|
-
if (block) {
|
|
111
|
-
return fromSlateValue(
|
|
112
|
-
[block],
|
|
113
|
-
types.block.name,
|
|
114
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
115
|
-
)[0]
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return undefined
|
|
119
|
-
},
|
|
120
|
-
focusChild: (): PortableTextChild | undefined => {
|
|
121
|
-
if (editor.selection) {
|
|
122
|
-
const block = Node.descendant(
|
|
123
|
-
editor,
|
|
124
|
-
editor.selection.focus.path.slice(0, 1),
|
|
125
|
-
)
|
|
126
|
-
if (block && editor.isTextBlock(block)) {
|
|
127
|
-
const ptBlock = fromSlateValue(
|
|
128
|
-
[block],
|
|
129
|
-
types.block.name,
|
|
130
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
131
|
-
)[0] as PortableTextTextBlock
|
|
132
|
-
return ptBlock.children[editor.selection.focus.path[1]]
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return undefined
|
|
136
|
-
},
|
|
137
|
-
insertChild: <TSchemaType extends {name: string}>(
|
|
138
|
-
type: TSchemaType,
|
|
139
|
-
value?: {[prop: string]: any},
|
|
140
|
-
): Path => {
|
|
141
|
-
if (!editor.selection) {
|
|
142
|
-
throw new Error('The editor has no selection')
|
|
143
|
-
}
|
|
144
|
-
const [focusBlock] = Array.from(
|
|
145
|
-
Editor.nodes(editor, {
|
|
146
|
-
at: editor.selection.focus.path.slice(0, 1),
|
|
147
|
-
match: (n) => n._type === types.block.name,
|
|
148
|
-
}),
|
|
149
|
-
)[0] || [undefined]
|
|
150
|
-
if (!focusBlock) {
|
|
151
|
-
throw new Error('No focused text block')
|
|
152
|
-
}
|
|
153
|
-
if (
|
|
154
|
-
type.name !== types.span.name &&
|
|
155
|
-
!types.inlineObjects.some((t) => t.name === type.name)
|
|
156
|
-
) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
'This type cannot be inserted as a child to a text block',
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
const block = toSlateValue(
|
|
162
|
-
[
|
|
163
|
-
{
|
|
164
|
-
_key: editorActor.getSnapshot().context.keyGenerator(),
|
|
165
|
-
_type: types.block.name,
|
|
166
|
-
children: [
|
|
167
|
-
{
|
|
168
|
-
_key: editorActor.getSnapshot().context.keyGenerator(),
|
|
169
|
-
_type: type.name,
|
|
170
|
-
...(value ? value : {}),
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
},
|
|
174
|
-
],
|
|
175
|
-
portableTextEditor,
|
|
176
|
-
)[0] as unknown as SlateElement
|
|
177
|
-
const child = block.children[0]
|
|
178
|
-
const focusChildPath = editor.selection.focus.path.slice(0, 2)
|
|
179
|
-
const isSpanNode = child._type === types.span.name
|
|
180
|
-
const focusNode = Node.get(editor, focusChildPath)
|
|
181
|
-
|
|
182
|
-
// If we are inserting a span, and currently have focus on an inline object,
|
|
183
|
-
// move the selection to the next span (guaranteed by normalizing rules) before inserting it.
|
|
184
|
-
if (isSpanNode && focusNode._type !== types.span.name) {
|
|
185
|
-
debug(
|
|
186
|
-
'Inserting span child next to inline object child, moving selection + 1',
|
|
187
|
-
)
|
|
188
|
-
editor.move({distance: 1, unit: 'character'})
|
|
107
|
+
if (block) {
|
|
108
|
+
return fromSlateValue(
|
|
109
|
+
[block],
|
|
110
|
+
types.block.name,
|
|
111
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
112
|
+
)[0]
|
|
189
113
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
fromSlateValue(
|
|
199
|
-
editor.children,
|
|
200
|
-
types.block.name,
|
|
201
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
202
|
-
),
|
|
203
|
-
editor.selection,
|
|
204
|
-
types,
|
|
205
|
-
)?.focus.path || []
|
|
114
|
+
}
|
|
115
|
+
return undefined
|
|
116
|
+
},
|
|
117
|
+
focusChild: (): PortableTextChild | undefined => {
|
|
118
|
+
if (editor.selection) {
|
|
119
|
+
const block = Node.descendant(
|
|
120
|
+
editor,
|
|
121
|
+
editor.selection.focus.path.slice(0, 1),
|
|
206
122
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
[
|
|
214
|
-
{
|
|
215
|
-
_key: editorActor.getSnapshot().context.keyGenerator(),
|
|
216
|
-
_type: type.name,
|
|
217
|
-
...(value ? value : {}),
|
|
218
|
-
},
|
|
219
|
-
],
|
|
220
|
-
portableTextEditor,
|
|
221
|
-
)[0] as unknown as Node
|
|
222
|
-
|
|
223
|
-
if (!editor.selection) {
|
|
224
|
-
const lastBlock = Array.from(
|
|
225
|
-
Editor.nodes(editor, {
|
|
226
|
-
match: (n) => !Editor.isEditor(n),
|
|
227
|
-
at: [],
|
|
228
|
-
reverse: true,
|
|
229
|
-
}),
|
|
230
|
-
)[0]
|
|
231
|
-
|
|
232
|
-
// If there is no selection, let's just insert the new block at the
|
|
233
|
-
// end of the document
|
|
234
|
-
Editor.insertNode(editor, block)
|
|
235
|
-
|
|
236
|
-
if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
|
|
237
|
-
// And if the last block was an empty text block, let's remove
|
|
238
|
-
// that too
|
|
239
|
-
Transforms.removeNodes(editor, {at: lastBlock[1]})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
editor.onChange()
|
|
243
|
-
|
|
244
|
-
return (
|
|
245
|
-
toPortableTextRange(
|
|
246
|
-
fromSlateValue(
|
|
247
|
-
editor.children,
|
|
248
|
-
types.block.name,
|
|
249
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
250
|
-
),
|
|
251
|
-
editor.selection,
|
|
252
|
-
types,
|
|
253
|
-
)?.focus.path ?? []
|
|
254
|
-
)
|
|
123
|
+
if (block && editor.isTextBlock(block)) {
|
|
124
|
+
const ptBlock = fromSlateValue(
|
|
125
|
+
[block],
|
|
126
|
+
types.block.name,
|
|
127
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
128
|
+
)[0] as PortableTextTextBlock
|
|
129
|
+
return ptBlock.children[editor.selection.focus.path[1]]
|
|
255
130
|
}
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
},
|
|
134
|
+
insertChild: <TSchemaType extends {name: string}>(
|
|
135
|
+
type: TSchemaType,
|
|
136
|
+
value?: {[prop: string]: any},
|
|
137
|
+
): Path => {
|
|
138
|
+
if (!editor.selection) {
|
|
139
|
+
throw new Error('The editor has no selection')
|
|
140
|
+
}
|
|
141
|
+
const [focusBlock] = Array.from(
|
|
142
|
+
Editor.nodes(editor, {
|
|
143
|
+
at: editor.selection.focus.path.slice(0, 1),
|
|
144
|
+
match: (n) => n._type === types.block.name,
|
|
145
|
+
}),
|
|
146
|
+
)[0] || [undefined]
|
|
147
|
+
if (!focusBlock) {
|
|
148
|
+
throw new Error('No focused text block')
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
type.name !== types.span.name &&
|
|
152
|
+
!types.inlineObjects.some((t) => t.name === type.name)
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'This type cannot be inserted as a child to a text block',
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
const block = toSlateValue(
|
|
159
|
+
[
|
|
160
|
+
{
|
|
161
|
+
_key: editorActor.getSnapshot().context.keyGenerator(),
|
|
162
|
+
_type: types.block.name,
|
|
163
|
+
children: [
|
|
164
|
+
{
|
|
165
|
+
_key: editorActor.getSnapshot().context.keyGenerator(),
|
|
166
|
+
_type: type.name,
|
|
167
|
+
...(value ? value : {}),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
{schemaTypes: editorActor.getSnapshot().context.schema},
|
|
173
|
+
)[0] as unknown as SlateElement
|
|
174
|
+
const child = block.children[0]
|
|
175
|
+
const focusChildPath = editor.selection.focus.path.slice(0, 2)
|
|
176
|
+
const isSpanNode = child._type === types.span.name
|
|
177
|
+
const focusNode = Node.get(editor, focusChildPath)
|
|
178
|
+
|
|
179
|
+
// If we are inserting a span, and currently have focus on an inline object,
|
|
180
|
+
// move the selection to the next span (guaranteed by normalizing rules) before inserting it.
|
|
181
|
+
if (isSpanNode && focusNode._type !== types.span.name) {
|
|
182
|
+
debug(
|
|
183
|
+
'Inserting span child next to inline object child, moving selection + 1',
|
|
184
|
+
)
|
|
185
|
+
editor.move({distance: 1, unit: 'character'})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Transforms.insertNodes(editor, child, {
|
|
189
|
+
select: true,
|
|
190
|
+
at: editor.selection,
|
|
191
|
+
})
|
|
192
|
+
editor.onChange()
|
|
193
|
+
return (
|
|
194
|
+
toPortableTextRange(
|
|
195
|
+
fromSlateValue(
|
|
196
|
+
editor.children,
|
|
197
|
+
types.block.name,
|
|
198
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
199
|
+
),
|
|
200
|
+
editor.selection,
|
|
201
|
+
types,
|
|
202
|
+
)?.focus.path || []
|
|
203
|
+
)
|
|
204
|
+
},
|
|
205
|
+
insertBlock: <TSchemaType extends {name: string}>(
|
|
206
|
+
type: TSchemaType,
|
|
207
|
+
value?: {[prop: string]: any},
|
|
208
|
+
): Path => {
|
|
209
|
+
const block = toSlateValue(
|
|
210
|
+
[
|
|
211
|
+
{
|
|
212
|
+
_key: editorActor.getSnapshot().context.keyGenerator(),
|
|
213
|
+
_type: type.name,
|
|
214
|
+
...(value ? value : {}),
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
{schemaTypes: editorActor.getSnapshot().context.schema},
|
|
218
|
+
)[0] as unknown as Node
|
|
256
219
|
|
|
257
|
-
|
|
220
|
+
if (!editor.selection) {
|
|
221
|
+
const lastBlock = Array.from(
|
|
258
222
|
Editor.nodes(editor, {
|
|
259
|
-
|
|
260
|
-
|
|
223
|
+
match: (n) => !Editor.isEditor(n),
|
|
224
|
+
at: [],
|
|
225
|
+
reverse: true,
|
|
261
226
|
}),
|
|
262
227
|
)[0]
|
|
263
228
|
|
|
229
|
+
// If there is no selection, let's just insert the new block at the
|
|
230
|
+
// end of the document
|
|
264
231
|
Editor.insertNode(editor, block)
|
|
265
232
|
|
|
266
|
-
if (
|
|
267
|
-
|
|
233
|
+
if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
|
|
234
|
+
// And if the last block was an empty text block, let's remove
|
|
235
|
+
// that too
|
|
236
|
+
Transforms.removeNodes(editor, {at: lastBlock[1]})
|
|
268
237
|
}
|
|
269
238
|
|
|
270
239
|
editor.onChange()
|
|
@@ -278,572 +247,675 @@ export function createWithEditableAPI(
|
|
|
278
247
|
),
|
|
279
248
|
editor.selection,
|
|
280
249
|
types,
|
|
281
|
-
)?.focus.path
|
|
250
|
+
)?.focus.path ?? []
|
|
282
251
|
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const focusBlock = Array.from(
|
|
255
|
+
Editor.nodes(editor, {
|
|
256
|
+
at: editor.selection.focus.path.slice(0, 1),
|
|
257
|
+
match: (n) => n._type === types.block.name,
|
|
258
|
+
}),
|
|
259
|
+
)[0]
|
|
260
|
+
|
|
261
|
+
Editor.insertNode(editor, block)
|
|
262
|
+
|
|
263
|
+
if (focusBlock && isEqualToEmptyEditor([focusBlock[0]], types)) {
|
|
264
|
+
Transforms.removeNodes(editor, {at: focusBlock[1]})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
editor.onChange()
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
toPortableTextRange(
|
|
271
|
+
fromSlateValue(
|
|
272
|
+
editor.children,
|
|
273
|
+
types.block.name,
|
|
274
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
275
|
+
),
|
|
276
|
+
editor.selection,
|
|
277
|
+
types,
|
|
278
|
+
)?.focus.path || []
|
|
279
|
+
)
|
|
280
|
+
},
|
|
281
|
+
hasBlockStyle: (style: string): boolean => {
|
|
282
|
+
try {
|
|
283
|
+
return editor.pteHasBlockStyle(style)
|
|
284
|
+
} catch {
|
|
285
|
+
// This is fine.
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
hasListStyle: (listStyle: string): boolean => {
|
|
290
|
+
try {
|
|
291
|
+
return editor.pteHasListStyle(listStyle)
|
|
292
|
+
} catch {
|
|
293
|
+
// This is fine.
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
isVoid: (element: PortableTextBlock | PortableTextChild) => {
|
|
298
|
+
return ![types.block.name, types.span.name].includes(element._type)
|
|
299
|
+
},
|
|
300
|
+
findByPath: (
|
|
301
|
+
path: Path,
|
|
302
|
+
): [
|
|
303
|
+
PortableTextBlock | PortableTextChild | undefined,
|
|
304
|
+
Path | undefined,
|
|
305
|
+
] => {
|
|
306
|
+
const slatePath = toSlateRange(
|
|
307
|
+
{focus: {path, offset: 0}, anchor: {path, offset: 0}},
|
|
308
|
+
editor,
|
|
309
|
+
)
|
|
310
|
+
if (slatePath) {
|
|
311
|
+
const [block, blockPath] = Editor.node(
|
|
311
312
|
editor,
|
|
313
|
+
slatePath.focus.path.slice(0, 1),
|
|
312
314
|
)
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
315
|
+
if (block && blockPath && typeof block._key === 'string') {
|
|
316
|
+
if (path.length === 1 && slatePath.focus.path.length === 1) {
|
|
317
|
+
return [
|
|
318
|
+
fromSlateValue([block], types.block.name)[0],
|
|
319
|
+
[{_key: block._key}],
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
const ptBlock = fromSlateValue(
|
|
323
|
+
[block],
|
|
324
|
+
types.block.name,
|
|
325
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
326
|
+
)[0]
|
|
327
|
+
if (editor.isTextBlock(ptBlock)) {
|
|
328
|
+
const ptChild = ptBlock.children[slatePath.focus.path[1]]
|
|
329
|
+
if (ptChild) {
|
|
320
330
|
return [
|
|
321
|
-
|
|
322
|
-
[{_key: block._key}],
|
|
331
|
+
ptChild,
|
|
332
|
+
[{_key: block._key}, 'children', {_key: ptChild._key}],
|
|
323
333
|
]
|
|
324
334
|
}
|
|
325
|
-
const ptBlock = fromSlateValue(
|
|
326
|
-
[block],
|
|
327
|
-
types.block.name,
|
|
328
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
329
|
-
)[0]
|
|
330
|
-
if (editor.isTextBlock(ptBlock)) {
|
|
331
|
-
const ptChild = ptBlock.children[slatePath.focus.path[1]]
|
|
332
|
-
if (ptChild) {
|
|
333
|
-
return [
|
|
334
|
-
ptChild,
|
|
335
|
-
[{_key: block._key}, 'children', {_key: ptChild._key}],
|
|
336
|
-
]
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
335
|
}
|
|
340
336
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
337
|
+
}
|
|
338
|
+
return [undefined, undefined]
|
|
339
|
+
},
|
|
340
|
+
findDOMNode: (
|
|
341
|
+
element: PortableTextBlock | PortableTextChild,
|
|
342
|
+
): DOMNode | undefined => {
|
|
343
|
+
let node: DOMNode | undefined
|
|
344
|
+
try {
|
|
345
|
+
const [item] = Array.from(
|
|
346
|
+
Editor.nodes(editor, {
|
|
347
|
+
at: [],
|
|
348
|
+
match: (n) => n._key === element._key,
|
|
349
|
+
}) || [],
|
|
350
|
+
)[0] || [undefined]
|
|
351
|
+
node = ReactEditor.toDOMNode(editor, item)
|
|
352
|
+
} catch {
|
|
353
|
+
// Nothing
|
|
354
|
+
}
|
|
355
|
+
return node
|
|
356
|
+
},
|
|
357
|
+
activeAnnotations: (): PortableTextObject[] => {
|
|
358
|
+
if (!editor.selection || editor.selection.focus.path.length < 2) {
|
|
359
|
+
return []
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const activeAnnotations: PortableTextObject[] = []
|
|
363
|
+
const spans = Editor.nodes(editor, {
|
|
364
|
+
at: editor.selection,
|
|
365
|
+
match: (node) =>
|
|
366
|
+
Text.isText(node) &&
|
|
367
|
+
node.marks !== undefined &&
|
|
368
|
+
Array.isArray(node.marks) &&
|
|
369
|
+
node.marks.length > 0,
|
|
370
|
+
})
|
|
371
|
+
for (const [span, path] of spans) {
|
|
372
|
+
const [block] = Editor.node(editor, path, {depth: 1})
|
|
373
|
+
if (editor.isTextBlock(block)) {
|
|
374
|
+
block.markDefs?.forEach((def) => {
|
|
375
|
+
if (
|
|
376
|
+
Text.isText(span) &&
|
|
377
|
+
span.marks &&
|
|
378
|
+
Array.isArray(span.marks) &&
|
|
379
|
+
span.marks.includes(def._key)
|
|
380
|
+
) {
|
|
381
|
+
activeAnnotations.push(def)
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
}
|
|
357
385
|
}
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
386
|
+
return activeAnnotations
|
|
387
|
+
} catch {
|
|
388
|
+
return []
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
isAnnotationActive: (
|
|
392
|
+
annotationType: PortableTextObject['_type'],
|
|
393
|
+
): boolean => {
|
|
394
|
+
return isAnnotationActive({editor, annotation: {name: annotationType}})
|
|
395
|
+
},
|
|
396
|
+
addAnnotation: (type, value) => {
|
|
397
|
+
let paths: ReturnType<EditableAPI['addAnnotation']> = undefined
|
|
398
|
+
|
|
399
|
+
Editor.withoutNormalizing(editor, () => {
|
|
400
|
+
paths = addAnnotationActionImplementation({
|
|
401
|
+
context: {
|
|
402
|
+
keyGenerator: editorActor.getSnapshot().context.keyGenerator,
|
|
403
|
+
schema: types,
|
|
404
|
+
},
|
|
405
|
+
action: {
|
|
406
|
+
type: 'annotation.add',
|
|
407
|
+
annotation: {name: type.name, value: value ?? {}},
|
|
408
|
+
editor,
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
editor.onChange()
|
|
413
|
+
|
|
414
|
+
return paths
|
|
415
|
+
},
|
|
416
|
+
delete: (
|
|
417
|
+
selection: EditorSelection,
|
|
418
|
+
options?: EditableAPIDeleteOptions,
|
|
419
|
+
): void => {
|
|
420
|
+
if (selection) {
|
|
421
|
+
const range = toSlateRange(selection, editor)
|
|
422
|
+
const hasRange =
|
|
423
|
+
range && range.anchor.path.length > 0 && range.focus.path.length > 0
|
|
424
|
+
if (!hasRange) {
|
|
425
|
+
throw new Error('Invalid range')
|
|
363
426
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
427
|
+
if (range) {
|
|
428
|
+
if (!options?.mode || options?.mode === 'selected') {
|
|
429
|
+
debug(`Deleting content in selection`)
|
|
430
|
+
Transforms.delete(editor, {
|
|
431
|
+
at: range,
|
|
432
|
+
hanging: true,
|
|
433
|
+
voids: true,
|
|
434
|
+
})
|
|
435
|
+
editor.onChange()
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
if (options?.mode === 'blocks') {
|
|
439
|
+
debug(`Deleting blocks touched by selection`)
|
|
440
|
+
Transforms.removeNodes(editor, {
|
|
441
|
+
at: range,
|
|
442
|
+
voids: true,
|
|
443
|
+
match: (node) => {
|
|
444
|
+
return (
|
|
445
|
+
editor.isTextBlock(node) ||
|
|
446
|
+
(!editor.isTextBlock(node) && SlateElement.isElement(node))
|
|
447
|
+
)
|
|
448
|
+
},
|
|
449
|
+
})
|
|
388
450
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
451
|
+
if (options?.mode === 'children') {
|
|
452
|
+
debug(`Deleting children touched by selection`)
|
|
453
|
+
Transforms.removeNodes(editor, {
|
|
454
|
+
at: range,
|
|
455
|
+
voids: true,
|
|
456
|
+
match: (node) => {
|
|
457
|
+
return (
|
|
458
|
+
node._type === types.span.name || // Text children
|
|
459
|
+
(!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks
|
|
460
|
+
)
|
|
461
|
+
},
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
// If the editor was emptied, insert a placeholder block
|
|
465
|
+
// directly into the editor's children. We don't want to do this
|
|
466
|
+
// through a Transform (because that would trigger a change event
|
|
467
|
+
// that would insert the placeholder into the actual value
|
|
468
|
+
// which should remain empty)
|
|
469
|
+
if (editor.children.length === 0) {
|
|
470
|
+
editor.children = [editor.pteCreateTextBlock({decorators: []})]
|
|
471
|
+
}
|
|
472
|
+
editor.onChange()
|
|
392
473
|
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
removeAnnotation: <TSchemaType extends {name: string}>(
|
|
477
|
+
type: TSchemaType,
|
|
478
|
+
): void => {
|
|
479
|
+
editorActor.send({
|
|
480
|
+
type: 'behavior event',
|
|
481
|
+
behaviorEvent: {
|
|
482
|
+
type: 'annotation.remove',
|
|
483
|
+
annotation: {name: type.name},
|
|
484
|
+
},
|
|
485
|
+
editor,
|
|
486
|
+
})
|
|
487
|
+
},
|
|
488
|
+
getSelection: (): EditorSelection | null => {
|
|
489
|
+
let ptRange: EditorSelection = null
|
|
490
|
+
if (editor.selection) {
|
|
491
|
+
const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection)
|
|
492
|
+
if (existing) {
|
|
493
|
+
return existing
|
|
399
494
|
}
|
|
495
|
+
ptRange = toPortableTextRange(
|
|
496
|
+
fromSlateValue(
|
|
497
|
+
editor.children,
|
|
498
|
+
types.block.name,
|
|
499
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
500
|
+
),
|
|
501
|
+
editor.selection,
|
|
502
|
+
types,
|
|
503
|
+
)
|
|
504
|
+
SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
|
|
505
|
+
}
|
|
506
|
+
return ptRange
|
|
507
|
+
},
|
|
508
|
+
getValue: () => {
|
|
509
|
+
return fromSlateValue(
|
|
510
|
+
editor.children,
|
|
511
|
+
types.block.name,
|
|
512
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
513
|
+
)
|
|
514
|
+
},
|
|
515
|
+
isCollapsedSelection: () => {
|
|
516
|
+
return !!editor.selection && Range.isCollapsed(editor.selection)
|
|
517
|
+
},
|
|
518
|
+
isExpandedSelection: () => {
|
|
519
|
+
return !!editor.selection && Range.isExpanded(editor.selection)
|
|
520
|
+
},
|
|
521
|
+
insertBreak: () => {
|
|
522
|
+
editor.insertBreak()
|
|
523
|
+
editor.onChange()
|
|
524
|
+
},
|
|
525
|
+
getFragment: () => {
|
|
526
|
+
return fromSlateValue(editor.getFragment(), types.block.name)
|
|
527
|
+
},
|
|
528
|
+
isSelectionsOverlapping: (
|
|
529
|
+
selectionA: EditorSelection,
|
|
530
|
+
selectionB: EditorSelection,
|
|
531
|
+
) => {
|
|
532
|
+
// Convert the selections to Slate ranges
|
|
533
|
+
const rangeA = toSlateRange(selectionA, editor)
|
|
534
|
+
const rangeB = toSlateRange(selectionB, editor)
|
|
535
|
+
|
|
536
|
+
// Make sure the ranges are valid
|
|
537
|
+
const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
|
|
538
|
+
|
|
539
|
+
// Check if the ranges are overlapping
|
|
540
|
+
const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
|
|
541
|
+
|
|
542
|
+
return isOverlapping
|
|
543
|
+
},
|
|
544
|
+
}
|
|
400
545
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
...Editor.nodes(editor, {
|
|
404
|
-
at: editor.selection,
|
|
405
|
-
match: (node) => Text.isText(node),
|
|
406
|
-
}),
|
|
407
|
-
]
|
|
408
|
-
|
|
409
|
-
if (spans.length === 0) {
|
|
410
|
-
return false
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
spans.some(
|
|
415
|
-
([span]) =>
|
|
416
|
-
!isPortableTextSpan(span) ||
|
|
417
|
-
!span.marks ||
|
|
418
|
-
span.marks?.length === 0,
|
|
419
|
-
)
|
|
420
|
-
)
|
|
421
|
-
return false
|
|
546
|
+
return editableApi
|
|
547
|
+
}
|
|
422
548
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
549
|
+
function isAnnotationActive({
|
|
550
|
+
editor,
|
|
551
|
+
annotation,
|
|
552
|
+
}: {
|
|
553
|
+
editor: PortableTextSlateEditor
|
|
554
|
+
annotation: {
|
|
555
|
+
name: string
|
|
556
|
+
}
|
|
557
|
+
}) {
|
|
558
|
+
if (!editor.selection || editor.selection.focus.path.length < 2) {
|
|
559
|
+
return false
|
|
560
|
+
}
|
|
430
561
|
|
|
431
|
-
|
|
432
|
-
|
|
562
|
+
try {
|
|
563
|
+
const spans = [
|
|
564
|
+
...Editor.nodes(editor, {
|
|
565
|
+
at: editor.selection,
|
|
566
|
+
match: (node) => Text.isText(node),
|
|
567
|
+
}),
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
if (spans.length === 0) {
|
|
571
|
+
return false
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (
|
|
575
|
+
spans.some(
|
|
576
|
+
([span]) =>
|
|
577
|
+
!isPortableTextSpan(span) || !span.marks || span.marks?.length === 0,
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
return false
|
|
581
|
+
|
|
582
|
+
const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => {
|
|
583
|
+
const [block] = Editor.node(editor, path, {depth: 1})
|
|
584
|
+
if (editor.isTextBlock(block) && block.markDefs) {
|
|
585
|
+
return [...accMarkDefs, ...block.markDefs]
|
|
586
|
+
}
|
|
587
|
+
return accMarkDefs
|
|
588
|
+
}, [] as PortableTextObject[])
|
|
589
|
+
|
|
590
|
+
return spans.every(([span]) => {
|
|
591
|
+
if (!isPortableTextSpan(span)) return false
|
|
592
|
+
|
|
593
|
+
const spanMarkDefs = span.marks?.map(
|
|
594
|
+
(markKey) =>
|
|
595
|
+
selectionMarkDefs.find((def) => def?._key === markKey)?._type,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return spanMarkDefs?.includes(annotation.name)
|
|
599
|
+
})
|
|
600
|
+
} catch {
|
|
601
|
+
return false
|
|
602
|
+
}
|
|
603
|
+
}
|
|
433
604
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
605
|
+
/**
|
|
606
|
+
* @public
|
|
607
|
+
*/
|
|
608
|
+
export type AddedAnnotationPaths = {
|
|
609
|
+
/**
|
|
610
|
+
* @deprecated An annotation may be applied to multiple blocks, resulting
|
|
611
|
+
* in multiple `markDef`'s being created. Use `markDefPaths` instead.
|
|
612
|
+
*/
|
|
613
|
+
markDefPath: Path
|
|
614
|
+
markDefPaths: Array<Path>
|
|
615
|
+
/**
|
|
616
|
+
* @deprecated Does not return anything meaningful since an annotation
|
|
617
|
+
* can span multiple blocks and spans. If references the span closest
|
|
618
|
+
* to the focus point of the selection.
|
|
619
|
+
*/
|
|
620
|
+
spanPath: Path
|
|
621
|
+
}
|
|
438
622
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
623
|
+
export const addAnnotationActionImplementation: BehaviourActionImplementation<
|
|
624
|
+
'annotation.add',
|
|
625
|
+
AddedAnnotationPaths | undefined
|
|
626
|
+
> = ({context, action}) => {
|
|
627
|
+
const editor = action.editor
|
|
628
|
+
const {selection: originalSelection} = editor
|
|
629
|
+
let paths: AddedAnnotationPaths | undefined = undefined
|
|
630
|
+
|
|
631
|
+
if (originalSelection) {
|
|
632
|
+
if (Range.isCollapsed(originalSelection)) {
|
|
633
|
+
editor.pteExpandToWord()
|
|
634
|
+
editor.onChange()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// If we still have a selection, add the annotation to the selected text
|
|
638
|
+
if (editor.selection) {
|
|
639
|
+
let spanPath: Path | undefined
|
|
640
|
+
let markDefPath: Path | undefined
|
|
641
|
+
const markDefPaths: Path[] = []
|
|
642
|
+
|
|
643
|
+
if (!editor.selection) {
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const selectedBlocks = Editor.nodes(editor, {
|
|
648
|
+
at: editor.selection,
|
|
649
|
+
match: (node) => editor.isTextBlock(node),
|
|
650
|
+
reverse: Range.isBackward(editor.selection),
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
for (const [block, blockPath] of selectedBlocks) {
|
|
654
|
+
if (block.children.length === 0) {
|
|
655
|
+
continue
|
|
443
656
|
}
|
|
444
|
-
},
|
|
445
|
-
addAnnotation: (type, value) => {
|
|
446
|
-
const {selection: originalSelection} = editor
|
|
447
|
-
let returnValue: ReturnType<EditableAPI['addAnnotation']> | undefined =
|
|
448
|
-
undefined
|
|
449
|
-
|
|
450
|
-
if (originalSelection) {
|
|
451
|
-
if (Range.isCollapsed(originalSelection)) {
|
|
452
|
-
editor.pteExpandToWord()
|
|
453
|
-
editor.onChange()
|
|
454
|
-
}
|
|
455
657
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
let markDefPath: Path | undefined
|
|
460
|
-
const markDefPaths: Path[] = []
|
|
461
|
-
|
|
462
|
-
Editor.withoutNormalizing(editor, () => {
|
|
463
|
-
if (!editor.selection) {
|
|
464
|
-
return
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const selectedBlocks = Editor.nodes(editor, {
|
|
468
|
-
at: editor.selection,
|
|
469
|
-
match: (node) => editor.isTextBlock(node),
|
|
470
|
-
reverse: Range.isBackward(editor.selection),
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
for (const [block, blockPath] of selectedBlocks) {
|
|
474
|
-
if (block.children.length === 0) {
|
|
475
|
-
continue
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
block.children.length === 1 &&
|
|
480
|
-
block.children[0].text === ''
|
|
481
|
-
) {
|
|
482
|
-
continue
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const annotationKey = editorActor
|
|
486
|
-
.getSnapshot()
|
|
487
|
-
.context.keyGenerator()
|
|
488
|
-
const markDefs = block.markDefs ?? []
|
|
489
|
-
const existingMarkDef = markDefs.find(
|
|
490
|
-
(markDef) =>
|
|
491
|
-
markDef._type === type.name &&
|
|
492
|
-
markDef._key === annotationKey,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
if (existingMarkDef === undefined) {
|
|
496
|
-
Transforms.setNodes(
|
|
497
|
-
editor,
|
|
498
|
-
{
|
|
499
|
-
markDefs: [
|
|
500
|
-
...markDefs,
|
|
501
|
-
{
|
|
502
|
-
_type: type.name,
|
|
503
|
-
_key: annotationKey,
|
|
504
|
-
...value,
|
|
505
|
-
},
|
|
506
|
-
],
|
|
507
|
-
},
|
|
508
|
-
{at: blockPath},
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
markDefPath = [
|
|
512
|
-
{_key: block._key},
|
|
513
|
-
'markDefs',
|
|
514
|
-
{_key: annotationKey},
|
|
515
|
-
]
|
|
516
|
-
if (Range.isBackward(editor.selection)) {
|
|
517
|
-
markDefPaths.unshift(markDefPath)
|
|
518
|
-
} else {
|
|
519
|
-
markDefPaths.push(markDefPath)
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
Transforms.setNodes(
|
|
524
|
-
editor,
|
|
525
|
-
{},
|
|
526
|
-
{match: Text.isText, split: true},
|
|
527
|
-
)
|
|
658
|
+
if (block.children.length === 1 && block.children[0].text === '') {
|
|
659
|
+
continue
|
|
660
|
+
}
|
|
528
661
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!Range.includes(editor.selection, path)) {
|
|
537
|
-
continue
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const marks = span.marks ?? []
|
|
541
|
-
const existingSameTypeAnnotations = marks.filter((mark) =>
|
|
542
|
-
markDefs.some(
|
|
543
|
-
(markDef) =>
|
|
544
|
-
markDef._key === mark && markDef._type === type.name,
|
|
545
|
-
),
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
Transforms.setNodes(
|
|
549
|
-
editor,
|
|
550
|
-
{
|
|
551
|
-
marks: [
|
|
552
|
-
...marks.filter(
|
|
553
|
-
(mark) => !existingSameTypeAnnotations.includes(mark),
|
|
554
|
-
),
|
|
555
|
-
annotationKey,
|
|
556
|
-
],
|
|
557
|
-
},
|
|
558
|
-
{at: path},
|
|
559
|
-
)
|
|
560
|
-
spanPath = [{_key: block._key}, 'children', {_key: span._key}]
|
|
561
|
-
}
|
|
562
|
-
}
|
|
662
|
+
const annotationKey = context.keyGenerator()
|
|
663
|
+
const markDefs = block.markDefs ?? []
|
|
664
|
+
const existingMarkDef = markDefs.find(
|
|
665
|
+
(markDef) =>
|
|
666
|
+
markDef._type === action.annotation.name &&
|
|
667
|
+
markDef._key === annotationKey,
|
|
668
|
+
)
|
|
563
669
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
return returnValue
|
|
576
|
-
},
|
|
577
|
-
delete: (
|
|
578
|
-
selection: EditorSelection,
|
|
579
|
-
options?: EditableAPIDeleteOptions,
|
|
580
|
-
): void => {
|
|
581
|
-
if (selection) {
|
|
582
|
-
const range = toSlateRange(selection, editor)
|
|
583
|
-
const hasRange =
|
|
584
|
-
range && range.anchor.path.length > 0 && range.focus.path.length > 0
|
|
585
|
-
if (!hasRange) {
|
|
586
|
-
throw new Error('Invalid range')
|
|
587
|
-
}
|
|
588
|
-
if (range) {
|
|
589
|
-
if (!options?.mode || options?.mode === 'selected') {
|
|
590
|
-
debug(`Deleting content in selection`)
|
|
591
|
-
Transforms.delete(editor, {
|
|
592
|
-
at: range,
|
|
593
|
-
hanging: true,
|
|
594
|
-
voids: true,
|
|
595
|
-
})
|
|
596
|
-
editor.onChange()
|
|
597
|
-
return
|
|
598
|
-
}
|
|
599
|
-
if (options?.mode === 'blocks') {
|
|
600
|
-
debug(`Deleting blocks touched by selection`)
|
|
601
|
-
Transforms.removeNodes(editor, {
|
|
602
|
-
at: range,
|
|
603
|
-
voids: true,
|
|
604
|
-
match: (node) => {
|
|
605
|
-
return (
|
|
606
|
-
editor.isTextBlock(node) ||
|
|
607
|
-
(!editor.isTextBlock(node) && SlateElement.isElement(node))
|
|
608
|
-
)
|
|
609
|
-
},
|
|
610
|
-
})
|
|
611
|
-
}
|
|
612
|
-
if (options?.mode === 'children') {
|
|
613
|
-
debug(`Deleting children touched by selection`)
|
|
614
|
-
Transforms.removeNodes(editor, {
|
|
615
|
-
at: range,
|
|
616
|
-
voids: true,
|
|
617
|
-
match: (node) => {
|
|
618
|
-
return (
|
|
619
|
-
node._type === types.span.name || // Text children
|
|
620
|
-
(!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks
|
|
621
|
-
)
|
|
670
|
+
if (existingMarkDef === undefined) {
|
|
671
|
+
Transforms.setNodes(
|
|
672
|
+
editor,
|
|
673
|
+
{
|
|
674
|
+
markDefs: [
|
|
675
|
+
...markDefs,
|
|
676
|
+
{
|
|
677
|
+
_type: action.annotation.name,
|
|
678
|
+
_key: annotationKey,
|
|
679
|
+
...action.annotation.value,
|
|
622
680
|
},
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
editor.onChange()
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
{at: blockPath},
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
markDefPath = [{_key: block._key}, 'markDefs', {_key: annotationKey}]
|
|
687
|
+
if (Range.isBackward(editor.selection)) {
|
|
688
|
+
markDefPaths.unshift(markDefPath)
|
|
689
|
+
} else {
|
|
690
|
+
markDefPaths.push(markDefPath)
|
|
634
691
|
}
|
|
635
692
|
}
|
|
636
|
-
},
|
|
637
|
-
removeAnnotation: <TSchemaType extends {name: string}>(
|
|
638
|
-
type: TSchemaType,
|
|
639
|
-
): void => {
|
|
640
|
-
debug('Removing annotation', type)
|
|
641
693
|
|
|
642
|
-
|
|
643
|
-
if (!editor.selection) {
|
|
644
|
-
return
|
|
645
|
-
}
|
|
694
|
+
Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
|
|
646
695
|
|
|
647
|
-
|
|
648
|
-
const [block, blockPath] = Editor.node(editor, editor.selection, {
|
|
649
|
-
depth: 1,
|
|
650
|
-
})
|
|
696
|
+
const children = Node.children(editor, blockPath)
|
|
651
697
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
698
|
+
for (const [span, path] of children) {
|
|
699
|
+
if (!editor.isTextSpan(span)) {
|
|
700
|
+
continue
|
|
701
|
+
}
|
|
655
702
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
)
|
|
703
|
+
if (!Range.includes(editor.selection, path)) {
|
|
704
|
+
continue
|
|
705
|
+
}
|
|
660
706
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
)
|
|
707
|
+
const marks = span.marks ?? []
|
|
708
|
+
const existingSameTypeAnnotations = marks.filter((mark) =>
|
|
709
|
+
markDefs.some(
|
|
710
|
+
(markDef) =>
|
|
711
|
+
markDef._key === mark &&
|
|
712
|
+
markDef._type === action.annotation.name,
|
|
713
|
+
),
|
|
714
|
+
)
|
|
668
715
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
716
|
+
Transforms.setNodes(
|
|
717
|
+
editor,
|
|
718
|
+
{
|
|
719
|
+
marks: [
|
|
720
|
+
...marks.filter(
|
|
721
|
+
(mark) => !existingSameTypeAnnotations.includes(mark),
|
|
722
|
+
),
|
|
723
|
+
annotationKey,
|
|
724
|
+
],
|
|
725
|
+
},
|
|
726
|
+
{at: path},
|
|
727
|
+
)
|
|
728
|
+
spanPath = [{_key: block._key}, 'children', {_key: span._key}]
|
|
729
|
+
}
|
|
730
|
+
}
|
|
672
731
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
732
|
+
if (markDefPath && spanPath) {
|
|
733
|
+
paths = {
|
|
734
|
+
markDefPath,
|
|
735
|
+
markDefPaths,
|
|
736
|
+
spanPath,
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return paths
|
|
742
|
+
}
|
|
676
743
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
744
|
+
export const removeAnnotationActionImplementation: BehaviourActionImplementation<
|
|
745
|
+
'annotation.remove'
|
|
746
|
+
> = ({action}) => {
|
|
747
|
+
const editor = action.editor
|
|
680
748
|
|
|
681
|
-
|
|
682
|
-
[span: PortableTextSpan, path: SlatePath]
|
|
683
|
-
> = []
|
|
749
|
+
debug('Removing annotation', action.annotation.name)
|
|
684
750
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (!editor.isTextSpan(child)) {
|
|
689
|
-
continue
|
|
690
|
-
}
|
|
751
|
+
if (!editor.selection) {
|
|
752
|
+
return
|
|
753
|
+
}
|
|
691
754
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
755
|
+
if (Range.isCollapsed(editor.selection)) {
|
|
756
|
+
const [block, blockPath] = Editor.node(editor, editor.selection, {
|
|
757
|
+
depth: 1,
|
|
758
|
+
})
|
|
695
759
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
break
|
|
700
|
-
}
|
|
701
|
-
}
|
|
760
|
+
if (!editor.isTextBlock(block)) {
|
|
761
|
+
return
|
|
762
|
+
}
|
|
702
763
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
764
|
+
const markDefs = block.markDefs ?? []
|
|
765
|
+
const potentialAnnotations = markDefs.filter(
|
|
766
|
+
(markDef) => markDef._type === action.annotation.name,
|
|
767
|
+
)
|
|
706
768
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
769
|
+
const [selectedChild, selectedChildPath] = Editor.node(
|
|
770
|
+
editor,
|
|
771
|
+
editor.selection,
|
|
772
|
+
{
|
|
773
|
+
depth: 2,
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
if (!editor.isTextSpan(selectedChild)) {
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const annotationToRemove = selectedChild.marks?.find((mark) =>
|
|
782
|
+
potentialAnnotations.some((markDef) => markDef._key === mark),
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
if (!annotationToRemove) {
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const previousSpansWithSameAnnotation: Array<
|
|
790
|
+
[span: PortableTextSpan, path: SlatePath]
|
|
791
|
+
> = []
|
|
792
|
+
|
|
793
|
+
for (const [child, childPath] of Node.children(editor, blockPath, {
|
|
794
|
+
reverse: true,
|
|
795
|
+
})) {
|
|
796
|
+
if (!editor.isTextSpan(child)) {
|
|
797
|
+
continue
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!SlatePath.isBefore(childPath, selectedChildPath)) {
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (child.marks?.includes(annotationToRemove)) {
|
|
805
|
+
previousSpansWithSameAnnotation.push([child, childPath])
|
|
806
|
+
} else {
|
|
807
|
+
break
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const nextSpansWithSameAnnotation: Array<
|
|
812
|
+
[span: PortableTextSpan, path: SlatePath]
|
|
813
|
+
> = []
|
|
814
|
+
|
|
815
|
+
for (const [child, childPath] of Node.children(editor, blockPath)) {
|
|
816
|
+
if (!editor.isTextSpan(child)) {
|
|
817
|
+
continue
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (!SlatePath.isAfter(childPath, selectedChildPath)) {
|
|
821
|
+
continue
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (child.marks?.includes(annotationToRemove)) {
|
|
825
|
+
nextSpansWithSameAnnotation.push([child, childPath])
|
|
826
|
+
} else {
|
|
827
|
+
break
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
for (const [child, childPath] of [
|
|
832
|
+
...previousSpansWithSameAnnotation,
|
|
833
|
+
[selectedChild, selectedChildPath] as const,
|
|
834
|
+
...nextSpansWithSameAnnotation,
|
|
835
|
+
]) {
|
|
836
|
+
Transforms.setNodes(
|
|
837
|
+
editor,
|
|
838
|
+
{
|
|
839
|
+
marks: child.marks?.filter((mark) => mark !== annotationToRemove),
|
|
840
|
+
},
|
|
841
|
+
{at: childPath},
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
Transforms.setNodes(
|
|
846
|
+
editor,
|
|
847
|
+
{},
|
|
848
|
+
{
|
|
849
|
+
match: (node) => editor.isTextSpan(node),
|
|
850
|
+
split: true,
|
|
851
|
+
hanging: true,
|
|
852
|
+
},
|
|
853
|
+
)
|
|
711
854
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
855
|
+
const blocks = Editor.nodes(editor, {
|
|
856
|
+
at: editor.selection,
|
|
857
|
+
match: (node) => editor.isTextBlock(node),
|
|
858
|
+
})
|
|
715
859
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
} else {
|
|
719
|
-
break
|
|
720
|
-
}
|
|
721
|
-
}
|
|
860
|
+
for (const [block, blockPath] of blocks) {
|
|
861
|
+
const children = Node.children(editor, blockPath)
|
|
722
862
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
]) {
|
|
728
|
-
Transforms.setNodes(
|
|
729
|
-
editor,
|
|
730
|
-
{
|
|
731
|
-
marks: child.marks?.filter(
|
|
732
|
-
(mark) => mark !== annotationToRemove,
|
|
733
|
-
),
|
|
734
|
-
},
|
|
735
|
-
{at: childPath},
|
|
736
|
-
)
|
|
737
|
-
}
|
|
738
|
-
} else {
|
|
739
|
-
Transforms.setNodes(
|
|
740
|
-
editor,
|
|
741
|
-
{},
|
|
742
|
-
{
|
|
743
|
-
match: (node) => editor.isTextSpan(node),
|
|
744
|
-
split: true,
|
|
745
|
-
hanging: true,
|
|
746
|
-
},
|
|
747
|
-
)
|
|
863
|
+
for (const [child, childPath] of children) {
|
|
864
|
+
if (!editor.isTextSpan(child)) {
|
|
865
|
+
continue
|
|
866
|
+
}
|
|
748
867
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
})
|
|
868
|
+
if (!Range.includes(editor.selection, childPath)) {
|
|
869
|
+
continue
|
|
870
|
+
}
|
|
753
871
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
continue
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
if (!Range.includes(editor.selection, childPath)) {
|
|
763
|
-
continue
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const markDefs = block.markDefs ?? []
|
|
767
|
-
const marks = child.marks ?? []
|
|
768
|
-
const marksWithoutAnnotation = marks.filter((mark) => {
|
|
769
|
-
const markDef = markDefs.find(
|
|
770
|
-
(markDef) => markDef._key === mark,
|
|
771
|
-
)
|
|
772
|
-
return markDef?._type !== type.name
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
if (marksWithoutAnnotation.length !== marks.length) {
|
|
776
|
-
Transforms.setNodes(
|
|
777
|
-
editor,
|
|
778
|
-
{
|
|
779
|
-
marks: marksWithoutAnnotation,
|
|
780
|
-
},
|
|
781
|
-
{at: childPath},
|
|
782
|
-
)
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
872
|
+
const markDefs = block.markDefs ?? []
|
|
873
|
+
const marks = child.marks ?? []
|
|
874
|
+
const marksWithoutAnnotation = marks.filter((mark) => {
|
|
875
|
+
const markDef = markDefs.find((markDef) => markDef._key === mark)
|
|
876
|
+
return markDef?._type !== action.annotation.name
|
|
787
877
|
})
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
ptRange = toPortableTextRange(
|
|
798
|
-
fromSlateValue(
|
|
799
|
-
editor.children,
|
|
800
|
-
types.block.name,
|
|
801
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
802
|
-
),
|
|
803
|
-
editor.selection,
|
|
804
|
-
types,
|
|
878
|
+
|
|
879
|
+
if (marksWithoutAnnotation.length !== marks.length) {
|
|
880
|
+
Transforms.setNodes(
|
|
881
|
+
editor,
|
|
882
|
+
{
|
|
883
|
+
marks: marksWithoutAnnotation,
|
|
884
|
+
},
|
|
885
|
+
{at: childPath},
|
|
805
886
|
)
|
|
806
|
-
SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
|
|
807
887
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
editor.children,
|
|
813
|
-
types.block.name,
|
|
814
|
-
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
815
|
-
)
|
|
816
|
-
},
|
|
817
|
-
isCollapsedSelection: () => {
|
|
818
|
-
return !!editor.selection && Range.isCollapsed(editor.selection)
|
|
819
|
-
},
|
|
820
|
-
isExpandedSelection: () => {
|
|
821
|
-
return !!editor.selection && Range.isExpanded(editor.selection)
|
|
822
|
-
},
|
|
823
|
-
insertBreak: () => {
|
|
824
|
-
editor.insertBreak()
|
|
825
|
-
editor.onChange()
|
|
826
|
-
},
|
|
827
|
-
getFragment: () => {
|
|
828
|
-
return fromSlateValue(editor.getFragment(), types.block.name)
|
|
829
|
-
},
|
|
830
|
-
isSelectionsOverlapping: (
|
|
831
|
-
selectionA: EditorSelection,
|
|
832
|
-
selectionB: EditorSelection,
|
|
833
|
-
) => {
|
|
834
|
-
// Convert the selections to Slate ranges
|
|
835
|
-
const rangeA = toSlateRange(selectionA, editor)
|
|
836
|
-
const rangeB = toSlateRange(selectionB, editor)
|
|
837
|
-
|
|
838
|
-
// Make sure the ranges are valid
|
|
839
|
-
const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
|
|
840
|
-
|
|
841
|
-
// Check if the ranges are overlapping
|
|
842
|
-
const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
843
892
|
|
|
844
|
-
|
|
893
|
+
export const toggleAnnotationActionImplementation: BehaviourActionImplementation<
|
|
894
|
+
'annotation.toggle',
|
|
895
|
+
AddedAnnotationPaths | undefined
|
|
896
|
+
> = ({context, action}) => {
|
|
897
|
+
const isActive = isAnnotationActive({
|
|
898
|
+
editor: action.editor,
|
|
899
|
+
annotation: {name: action.annotation.name},
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
if (isActive) {
|
|
903
|
+
removeAnnotationActionImplementation({
|
|
904
|
+
context,
|
|
905
|
+
action: {
|
|
906
|
+
type: 'annotation.remove',
|
|
907
|
+
annotation: action.annotation,
|
|
908
|
+
editor: action.editor,
|
|
909
|
+
},
|
|
910
|
+
})
|
|
911
|
+
} else {
|
|
912
|
+
return addAnnotationActionImplementation({
|
|
913
|
+
context,
|
|
914
|
+
action: {
|
|
915
|
+
type: 'annotation.add',
|
|
916
|
+
annotation: action.annotation,
|
|
917
|
+
editor: action.editor,
|
|
845
918
|
},
|
|
846
919
|
})
|
|
847
|
-
return editor
|
|
848
920
|
}
|
|
849
921
|
}
|