@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,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This plugin will make the editor support undo/redo on the local state only.
|
|
3
|
+
* The undo/redo steps are rebased against incoming patches since the step occurred.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, parsePatch} from '@sanity/diff-match-patch'
|
|
7
|
+
import {type ObjectSchemaType, type PortableTextBlock} from '@sanity/types'
|
|
8
|
+
import {flatten, isEqual} from 'lodash'
|
|
9
|
+
import {type Descendant, Editor, Operation, Path, type SelectionOperation, Transforms} from 'slate'
|
|
10
|
+
|
|
11
|
+
import {type PatchObservable, type PortableTextSlateEditor} from '../../types/editor'
|
|
12
|
+
import {type Patch} from '../../types/patch'
|
|
13
|
+
import {debugWithName} from '../../utils/debug'
|
|
14
|
+
import {fromSlateValue} from '../../utils/values'
|
|
15
|
+
import {withPreserveKeys} from '../../utils/withPreserveKeys'
|
|
16
|
+
|
|
17
|
+
const debug = debugWithName('plugin:withUndoRedo')
|
|
18
|
+
const debugVerbose = debug.enabled && false
|
|
19
|
+
|
|
20
|
+
const SAVING = new WeakMap<Editor, boolean | undefined>()
|
|
21
|
+
const REMOTE_PATCHES = new WeakMap<
|
|
22
|
+
Editor,
|
|
23
|
+
{
|
|
24
|
+
patch: Patch
|
|
25
|
+
time: Date
|
|
26
|
+
snapshot: PortableTextBlock[] | undefined
|
|
27
|
+
previousSnapshot: PortableTextBlock[] | undefined
|
|
28
|
+
}[]
|
|
29
|
+
>()
|
|
30
|
+
const UNDO_STEP_LIMIT = 1000
|
|
31
|
+
|
|
32
|
+
const isSaving = (editor: Editor): boolean | undefined => {
|
|
33
|
+
const state = SAVING.get(editor)
|
|
34
|
+
return state === undefined ? true : state
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Options {
|
|
38
|
+
patches$?: PatchObservable
|
|
39
|
+
readOnly: boolean
|
|
40
|
+
blockSchemaType: ObjectSchemaType
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const getRemotePatches = (editor: Editor) => {
|
|
44
|
+
if (!REMOTE_PATCHES.get(editor)) {
|
|
45
|
+
REMOTE_PATCHES.set(editor, [])
|
|
46
|
+
}
|
|
47
|
+
return REMOTE_PATCHES.get(editor) || []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createWithUndoRedo(
|
|
51
|
+
options: Options,
|
|
52
|
+
): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
|
|
53
|
+
const {readOnly, patches$, blockSchemaType} = options
|
|
54
|
+
|
|
55
|
+
return (editor: PortableTextSlateEditor) => {
|
|
56
|
+
let previousSnapshot: PortableTextBlock[] | undefined = fromSlateValue(
|
|
57
|
+
editor.children,
|
|
58
|
+
blockSchemaType.name,
|
|
59
|
+
)
|
|
60
|
+
const remotePatches = getRemotePatches(editor)
|
|
61
|
+
if (patches$) {
|
|
62
|
+
editor.subscriptions.push(() => {
|
|
63
|
+
debug('Subscribing to patches')
|
|
64
|
+
const sub = patches$.subscribe(({patches, snapshot}) => {
|
|
65
|
+
let reset = false
|
|
66
|
+
patches.forEach((patch) => {
|
|
67
|
+
if (!reset && patch.origin !== 'local' && remotePatches) {
|
|
68
|
+
if (patch.type === 'unset' && patch.path.length === 0) {
|
|
69
|
+
debug('Someone else cleared the content, resetting undo/redo history')
|
|
70
|
+
editor.history = {undos: [], redos: []}
|
|
71
|
+
remotePatches.splice(0, remotePatches.length)
|
|
72
|
+
SAVING.set(editor, true)
|
|
73
|
+
reset = true
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
remotePatches.push({patch, time: new Date(), snapshot, previousSnapshot})
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
previousSnapshot = snapshot
|
|
80
|
+
})
|
|
81
|
+
return () => {
|
|
82
|
+
debug('Unsubscribing to patches')
|
|
83
|
+
sub.unsubscribe()
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
editor.history = {undos: [], redos: []}
|
|
88
|
+
const {apply} = editor
|
|
89
|
+
editor.apply = (op: Operation) => {
|
|
90
|
+
if (readOnly) {
|
|
91
|
+
apply(op)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const {operations, history} = editor
|
|
95
|
+
const {undos} = history
|
|
96
|
+
const step = undos[undos.length - 1]
|
|
97
|
+
const lastOp = step && step.operations && step.operations[step.operations.length - 1]
|
|
98
|
+
const overwrite = shouldOverwrite(op, lastOp)
|
|
99
|
+
const save = isSaving(editor)
|
|
100
|
+
|
|
101
|
+
let merge = true
|
|
102
|
+
if (save) {
|
|
103
|
+
if (!step) {
|
|
104
|
+
merge = false
|
|
105
|
+
} else if (operations.length === 0) {
|
|
106
|
+
merge = shouldMerge(op, lastOp) || overwrite
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (step && merge) {
|
|
110
|
+
step.operations.push(op)
|
|
111
|
+
} else {
|
|
112
|
+
const newStep = {
|
|
113
|
+
operations: [...(editor.selection === null ? [] : [createSelectOperation(editor)]), op],
|
|
114
|
+
timestamp: new Date(),
|
|
115
|
+
}
|
|
116
|
+
undos.push(newStep)
|
|
117
|
+
debug('Created new undo step', step)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
while (undos.length > UNDO_STEP_LIMIT) {
|
|
121
|
+
undos.shift()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (shouldClear(op)) {
|
|
125
|
+
history.redos = []
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
apply(op)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
editor.undo = () => {
|
|
132
|
+
if (readOnly) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
const {undos} = editor.history
|
|
136
|
+
if (undos.length > 0) {
|
|
137
|
+
const step = undos[undos.length - 1]
|
|
138
|
+
debug('Undoing', step)
|
|
139
|
+
if (step.operations.length > 0) {
|
|
140
|
+
const otherPatches = remotePatches.filter((item) => item.time >= step.timestamp)
|
|
141
|
+
let transformedOperations = step.operations
|
|
142
|
+
otherPatches.forEach((item) => {
|
|
143
|
+
transformedOperations = flatten(
|
|
144
|
+
transformedOperations.map((op) =>
|
|
145
|
+
transformOperation(editor, item.patch, op, item.snapshot, item.previousSnapshot),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
try {
|
|
150
|
+
Editor.withoutNormalizing(editor, () => {
|
|
151
|
+
withPreserveKeys(editor, () => {
|
|
152
|
+
withoutSaving(editor, () => {
|
|
153
|
+
transformedOperations
|
|
154
|
+
.map(Operation.inverse)
|
|
155
|
+
.reverse()
|
|
156
|
+
// eslint-disable-next-line max-nested-callbacks
|
|
157
|
+
.forEach((op) => {
|
|
158
|
+
editor.apply(op)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
editor.normalize()
|
|
164
|
+
editor.onChange()
|
|
165
|
+
} catch (err) {
|
|
166
|
+
debug('Could not perform undo step', err)
|
|
167
|
+
remotePatches.splice(0, remotePatches.length)
|
|
168
|
+
Transforms.deselect(editor)
|
|
169
|
+
editor.history = {undos: [], redos: []}
|
|
170
|
+
SAVING.set(editor, true)
|
|
171
|
+
editor.onChange()
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
editor.history.redos.push(step)
|
|
175
|
+
editor.history.undos.pop()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
editor.redo = () => {
|
|
181
|
+
if (readOnly) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
const {redos} = editor.history
|
|
185
|
+
if (redos.length > 0) {
|
|
186
|
+
const step = redos[redos.length - 1]
|
|
187
|
+
debug('Redoing', step)
|
|
188
|
+
if (step.operations.length > 0) {
|
|
189
|
+
const otherPatches = remotePatches.filter((item) => item.time >= step.timestamp)
|
|
190
|
+
let transformedOperations = step.operations
|
|
191
|
+
otherPatches.forEach((item) => {
|
|
192
|
+
transformedOperations = flatten(
|
|
193
|
+
transformedOperations.map((op) =>
|
|
194
|
+
transformOperation(editor, item.patch, op, item.snapshot, item.previousSnapshot),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
try {
|
|
199
|
+
Editor.withoutNormalizing(editor, () => {
|
|
200
|
+
withPreserveKeys(editor, () => {
|
|
201
|
+
withoutSaving(editor, () => {
|
|
202
|
+
// eslint-disable-next-line max-nested-callbacks
|
|
203
|
+
transformedOperations.forEach((op) => {
|
|
204
|
+
editor.apply(op)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
editor.normalize()
|
|
210
|
+
editor.onChange()
|
|
211
|
+
} catch (err) {
|
|
212
|
+
debug('Could not perform redo step', err)
|
|
213
|
+
remotePatches.splice(0, remotePatches.length)
|
|
214
|
+
Transforms.deselect(editor)
|
|
215
|
+
editor.history = {undos: [], redos: []}
|
|
216
|
+
SAVING.set(editor, true)
|
|
217
|
+
editor.onChange()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
editor.history.undos.push(step)
|
|
221
|
+
editor.history.redos.pop()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Plugin return
|
|
227
|
+
return editor
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* This will adjust the operation paths and offsets according to the
|
|
233
|
+
* remote patches by other editors since the step operations was performed.
|
|
234
|
+
*/
|
|
235
|
+
function transformOperation(
|
|
236
|
+
editor: PortableTextSlateEditor,
|
|
237
|
+
patch: Patch,
|
|
238
|
+
operation: Operation,
|
|
239
|
+
snapshot: PortableTextBlock[] | undefined,
|
|
240
|
+
previousSnapshot: PortableTextBlock[] | undefined,
|
|
241
|
+
): Operation[] {
|
|
242
|
+
if (debugVerbose) {
|
|
243
|
+
debug(`Adjusting '${operation.type}' operation paths for '${patch.type}' patch`)
|
|
244
|
+
debug(`Operation ${JSON.stringify(operation)}`)
|
|
245
|
+
debug(`Patch ${JSON.stringify(patch)}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const transformedOperation = {...operation}
|
|
249
|
+
|
|
250
|
+
if (patch.type === 'insert' && patch.path.length === 1) {
|
|
251
|
+
const insertBlockIndex = (snapshot || []).findIndex((blk) =>
|
|
252
|
+
isEqual({_key: blk._key}, patch.path[0]),
|
|
253
|
+
)
|
|
254
|
+
debug(
|
|
255
|
+
`Adjusting block path (+${patch.items.length}) for '${transformedOperation.type}' operation and patch '${patch.type}'`,
|
|
256
|
+
)
|
|
257
|
+
return [adjustBlockPath(transformedOperation, patch.items.length, insertBlockIndex)]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (patch.type === 'unset' && patch.path.length === 1) {
|
|
261
|
+
const unsetBlockIndex = (previousSnapshot || []).findIndex((blk) =>
|
|
262
|
+
isEqual({_key: blk._key}, patch.path[0]),
|
|
263
|
+
)
|
|
264
|
+
// If this operation is targeting the same block that got removed, return empty
|
|
265
|
+
if (
|
|
266
|
+
'path' in transformedOperation &&
|
|
267
|
+
Array.isArray(transformedOperation.path) &&
|
|
268
|
+
transformedOperation.path[0] === unsetBlockIndex
|
|
269
|
+
) {
|
|
270
|
+
debug('Skipping transformation that targeted removed block')
|
|
271
|
+
return []
|
|
272
|
+
}
|
|
273
|
+
if (debugVerbose) {
|
|
274
|
+
debug(`Selection ${JSON.stringify(editor.selection)}`)
|
|
275
|
+
debug(
|
|
276
|
+
`Adjusting block path (-1) for '${transformedOperation.type}' operation and patch '${patch.type}'`,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
return [adjustBlockPath(transformedOperation, -1, unsetBlockIndex)]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Someone reset the whole value
|
|
283
|
+
if (patch.type === 'unset' && patch.path.length === 0) {
|
|
284
|
+
debug(`Adjusting selection for unset everything patch and ${operation.type} operation`)
|
|
285
|
+
return []
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (patch.type === 'diffMatchPatch') {
|
|
289
|
+
const operationTargetBlock = findOperationTargetBlock(editor, transformedOperation)
|
|
290
|
+
if (!operationTargetBlock || !isEqual({_key: operationTargetBlock._key}, patch.path[0])) {
|
|
291
|
+
return [transformedOperation]
|
|
292
|
+
}
|
|
293
|
+
const diffPatches = parsePatch(patch.value)
|
|
294
|
+
diffPatches.forEach((diffPatch) => {
|
|
295
|
+
let adjustOffsetBy = 0
|
|
296
|
+
let changedOffset = diffPatch.utf8Start1
|
|
297
|
+
const {diffs} = diffPatch
|
|
298
|
+
diffs.forEach((diff, index) => {
|
|
299
|
+
const [diffType, text] = diff
|
|
300
|
+
if (diffType === DIFF_INSERT) {
|
|
301
|
+
adjustOffsetBy += text.length
|
|
302
|
+
changedOffset += text.length
|
|
303
|
+
} else if (diffType === DIFF_DELETE) {
|
|
304
|
+
adjustOffsetBy -= text.length
|
|
305
|
+
changedOffset -= text.length
|
|
306
|
+
} else if (diffType === DIFF_EQUAL) {
|
|
307
|
+
// Only up to the point where there are no other changes
|
|
308
|
+
if (!diffs.slice(index).every(([dType]) => dType === DIFF_EQUAL)) {
|
|
309
|
+
changedOffset += text.length
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
// Adjust accordingly if someone inserted text in the same node before us
|
|
314
|
+
if (transformedOperation.type === 'insert_text') {
|
|
315
|
+
if (changedOffset < transformedOperation.offset) {
|
|
316
|
+
transformedOperation.offset += adjustOffsetBy
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Adjust accordingly if someone removed text in the same node before us
|
|
320
|
+
if (transformedOperation.type === 'remove_text') {
|
|
321
|
+
if (changedOffset <= transformedOperation.offset - transformedOperation.text.length) {
|
|
322
|
+
transformedOperation.offset += adjustOffsetBy
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Adjust set_selection operation's points to new offset
|
|
326
|
+
if (transformedOperation.type === 'set_selection') {
|
|
327
|
+
const currentFocus = transformedOperation.properties?.focus
|
|
328
|
+
? {...transformedOperation.properties.focus}
|
|
329
|
+
: undefined
|
|
330
|
+
const currentAnchor = transformedOperation?.properties?.anchor
|
|
331
|
+
? {...transformedOperation.properties.anchor}
|
|
332
|
+
: undefined
|
|
333
|
+
const newFocus = transformedOperation?.newProperties?.focus
|
|
334
|
+
? {...transformedOperation.newProperties.focus}
|
|
335
|
+
: undefined
|
|
336
|
+
const newAnchor = transformedOperation?.newProperties?.anchor
|
|
337
|
+
? {...transformedOperation.newProperties.anchor}
|
|
338
|
+
: undefined
|
|
339
|
+
if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) {
|
|
340
|
+
const points = [currentFocus, currentAnchor, newFocus, newAnchor]
|
|
341
|
+
points.forEach((point) => {
|
|
342
|
+
if (point && changedOffset < point.offset) {
|
|
343
|
+
point.offset += adjustOffsetBy
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
if (currentFocus && currentAnchor) {
|
|
347
|
+
transformedOperation.properties = {
|
|
348
|
+
focus: currentFocus,
|
|
349
|
+
anchor: currentAnchor,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (newFocus && newAnchor) {
|
|
353
|
+
transformedOperation.newProperties = {
|
|
354
|
+
focus: newFocus,
|
|
355
|
+
anchor: newAnchor,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
return [transformedOperation]
|
|
362
|
+
}
|
|
363
|
+
return [transformedOperation]
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Adjust the block path for a operation
|
|
367
|
+
*/
|
|
368
|
+
function adjustBlockPath(operation: Operation, level: number, blockIndex: number): Operation {
|
|
369
|
+
const transformedOperation = {...operation}
|
|
370
|
+
if (
|
|
371
|
+
blockIndex >= 0 &&
|
|
372
|
+
transformedOperation.type !== 'set_selection' &&
|
|
373
|
+
Array.isArray(transformedOperation.path) &&
|
|
374
|
+
transformedOperation.path[0] >= blockIndex + level &&
|
|
375
|
+
transformedOperation.path[0] + level > -1
|
|
376
|
+
) {
|
|
377
|
+
const newPath = [transformedOperation.path[0] + level, ...transformedOperation.path.slice(1)]
|
|
378
|
+
transformedOperation.path = newPath
|
|
379
|
+
}
|
|
380
|
+
if (transformedOperation.type === 'set_selection') {
|
|
381
|
+
const currentFocus = transformedOperation.properties?.focus
|
|
382
|
+
? {...transformedOperation.properties.focus}
|
|
383
|
+
: undefined
|
|
384
|
+
const currentAnchor = transformedOperation?.properties?.anchor
|
|
385
|
+
? {...transformedOperation.properties.anchor}
|
|
386
|
+
: undefined
|
|
387
|
+
const newFocus = transformedOperation?.newProperties?.focus
|
|
388
|
+
? {...transformedOperation.newProperties.focus}
|
|
389
|
+
: undefined
|
|
390
|
+
const newAnchor = transformedOperation?.newProperties?.anchor
|
|
391
|
+
? {...transformedOperation.newProperties.anchor}
|
|
392
|
+
: undefined
|
|
393
|
+
if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) {
|
|
394
|
+
const points = [currentFocus, currentAnchor, newFocus, newAnchor]
|
|
395
|
+
points.forEach((point) => {
|
|
396
|
+
if (point && point.path[0] >= blockIndex + level && point.path[0] + level > -1) {
|
|
397
|
+
point.path = [point.path[0] + level, ...point.path.slice(1)]
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
if (currentFocus && currentAnchor) {
|
|
401
|
+
transformedOperation.properties = {
|
|
402
|
+
focus: currentFocus,
|
|
403
|
+
anchor: currentAnchor,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (newFocus && newAnchor) {
|
|
407
|
+
transformedOperation.newProperties = {
|
|
408
|
+
focus: newFocus,
|
|
409
|
+
anchor: newAnchor,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// // Assign fresh point objects (we don't want to mutate the original ones)
|
|
415
|
+
return transformedOperation
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Helper functions for editor.apply above
|
|
419
|
+
|
|
420
|
+
const shouldMerge = (op: Operation, prev: Operation | undefined): boolean => {
|
|
421
|
+
if (op.type === 'set_selection') {
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Text input
|
|
426
|
+
if (
|
|
427
|
+
prev &&
|
|
428
|
+
op.type === 'insert_text' &&
|
|
429
|
+
prev.type === 'insert_text' &&
|
|
430
|
+
op.offset === prev.offset + prev.text.length &&
|
|
431
|
+
Path.equals(op.path, prev.path) &&
|
|
432
|
+
op.text !== ' ' // Tokenize between words
|
|
433
|
+
) {
|
|
434
|
+
return true
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Text deletion
|
|
438
|
+
if (
|
|
439
|
+
prev &&
|
|
440
|
+
op.type === 'remove_text' &&
|
|
441
|
+
prev.type === 'remove_text' &&
|
|
442
|
+
op.offset + op.text.length === prev.offset &&
|
|
443
|
+
Path.equals(op.path, prev.path)
|
|
444
|
+
) {
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Don't merge
|
|
449
|
+
return false
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const shouldOverwrite = (op: Operation, prev: Operation | undefined): boolean => {
|
|
453
|
+
if (prev && op.type === 'set_selection' && prev.type === 'set_selection') {
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return false
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const shouldClear = (op: Operation): boolean => {
|
|
461
|
+
if (op.type === 'set_selection') {
|
|
462
|
+
return false
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return true
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function withoutSaving(editor: Editor, fn: () => void): void {
|
|
469
|
+
const prev = isSaving(editor)
|
|
470
|
+
SAVING.set(editor, false)
|
|
471
|
+
fn()
|
|
472
|
+
SAVING.set(editor, prev)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function createSelectOperation(editor: Editor): SelectionOperation {
|
|
476
|
+
return {
|
|
477
|
+
type: 'set_selection',
|
|
478
|
+
properties: {...editor.selection},
|
|
479
|
+
newProperties: {...editor.selection},
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function findOperationTargetBlock(
|
|
484
|
+
editor: PortableTextSlateEditor,
|
|
485
|
+
operation: Operation,
|
|
486
|
+
): Descendant | undefined {
|
|
487
|
+
let block: Descendant | undefined
|
|
488
|
+
if (operation.type === 'set_selection' && editor.selection) {
|
|
489
|
+
block = editor.children[editor.selection.focus.path[0]]
|
|
490
|
+
} else if ('path' in operation) {
|
|
491
|
+
block = editor.children[operation.path[0]]
|
|
492
|
+
}
|
|
493
|
+
return block
|
|
494
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {Editor, Range, Text, Transforms} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
|
|
4
|
+
import {debugWithName} from '../../utils/debug'
|
|
5
|
+
import {toSlateValue} from '../../utils/values'
|
|
6
|
+
import {type PortableTextEditor} from '../PortableTextEditor'
|
|
7
|
+
|
|
8
|
+
const debug = debugWithName('plugin:withUtils')
|
|
9
|
+
|
|
10
|
+
interface Options {
|
|
11
|
+
schemaTypes: PortableTextMemberSchemaTypes
|
|
12
|
+
keyGenerator: () => string
|
|
13
|
+
portableTextEditor: PortableTextEditor
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* This plugin makes various util commands available in the editor
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
export function createWithUtils({schemaTypes, keyGenerator, portableTextEditor}: Options) {
|
|
20
|
+
return function withUtils(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
21
|
+
// Expands the the selection to wrap around the word the focus is at
|
|
22
|
+
editor.pteExpandToWord = () => {
|
|
23
|
+
const {selection} = editor
|
|
24
|
+
if (selection && !Range.isExpanded(selection)) {
|
|
25
|
+
const [textNode] = Editor.node(editor, selection.focus, {depth: 2})
|
|
26
|
+
if (!textNode || !Text.isText(textNode) || textNode.text.length === 0) {
|
|
27
|
+
debug(`pteExpandToWord: Can't expand to word here`)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const {focus} = selection
|
|
31
|
+
const focusOffset = focus.offset
|
|
32
|
+
const charsBefore = textNode.text.slice(0, focusOffset)
|
|
33
|
+
const charsAfter = textNode.text.slice(focusOffset, -1)
|
|
34
|
+
const isEmpty = (str: string) => str.match(/\s/g)
|
|
35
|
+
const whiteSpaceBeforeIndex = charsBefore
|
|
36
|
+
.split('')
|
|
37
|
+
.reverse()
|
|
38
|
+
.findIndex((str) => isEmpty(str))
|
|
39
|
+
const newStartOffset =
|
|
40
|
+
whiteSpaceBeforeIndex > -1 ? charsBefore.length - whiteSpaceBeforeIndex : 0
|
|
41
|
+
const whiteSpaceAfterIndex = charsAfter.split('').findIndex((obj) => isEmpty(obj))
|
|
42
|
+
const newEndOffset =
|
|
43
|
+
charsBefore.length +
|
|
44
|
+
(whiteSpaceAfterIndex > -1 ? whiteSpaceAfterIndex : charsAfter.length + 1)
|
|
45
|
+
if (!(newStartOffset === newEndOffset || isNaN(newStartOffset) || isNaN(newEndOffset))) {
|
|
46
|
+
debug('pteExpandToWord: Expanding to focused word')
|
|
47
|
+
Transforms.setSelection(editor, {
|
|
48
|
+
anchor: {...selection.anchor, offset: newStartOffset},
|
|
49
|
+
focus: {...selection.focus, offset: newEndOffset},
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
debug(`pteExpandToWord: Can't expand to word here`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
editor.pteCreateEmptyBlock = () => {
|
|
58
|
+
const block = toSlateValue(
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
_type: schemaTypes.block.name,
|
|
62
|
+
_key: keyGenerator(),
|
|
63
|
+
style: schemaTypes.styles[0].value || 'normal',
|
|
64
|
+
markDefs: [],
|
|
65
|
+
children: [
|
|
66
|
+
{
|
|
67
|
+
_type: 'span',
|
|
68
|
+
_key: keyGenerator(),
|
|
69
|
+
text: '',
|
|
70
|
+
marks: [],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
portableTextEditor,
|
|
76
|
+
)[0]
|
|
77
|
+
return block
|
|
78
|
+
}
|
|
79
|
+
return editor
|
|
80
|
+
}
|
|
81
|
+
}
|