@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,274 @@
|
|
|
1
|
+
/* eslint-disable max-nested-callbacks */
|
|
2
|
+
import {type Subject} from 'rxjs'
|
|
3
|
+
import {
|
|
4
|
+
type Descendant,
|
|
5
|
+
Editor,
|
|
6
|
+
type InsertNodeOperation,
|
|
7
|
+
type InsertTextOperation,
|
|
8
|
+
type MergeNodeOperation,
|
|
9
|
+
type MoveNodeOperation,
|
|
10
|
+
type Operation,
|
|
11
|
+
type RemoveNodeOperation,
|
|
12
|
+
type RemoveTextOperation,
|
|
13
|
+
type SetNodeOperation,
|
|
14
|
+
type SplitNodeOperation,
|
|
15
|
+
} from 'slate'
|
|
16
|
+
|
|
17
|
+
import {insert, setIfMissing, unset} from '../../patch/PatchEvent'
|
|
18
|
+
import {
|
|
19
|
+
type EditorChange,
|
|
20
|
+
type PatchObservable,
|
|
21
|
+
type PortableTextMemberSchemaTypes,
|
|
22
|
+
type PortableTextSlateEditor,
|
|
23
|
+
} from '../../types/editor'
|
|
24
|
+
import {type Patch} from '../../types/patch'
|
|
25
|
+
import {createApplyPatch} from '../../utils/applyPatch'
|
|
26
|
+
import {debugWithName} from '../../utils/debug'
|
|
27
|
+
import {fromSlateValue, isEqualToEmptyEditor} from '../../utils/values'
|
|
28
|
+
import {IS_PROCESSING_REMOTE_CHANGES, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
|
|
29
|
+
import {withRemoteChanges} from '../../utils/withChanges'
|
|
30
|
+
import {isPatching, PATCHING, withoutPatching} from '../../utils/withoutPatching'
|
|
31
|
+
import {withPreserveKeys} from '../../utils/withPreserveKeys'
|
|
32
|
+
import {withoutSaving} from './createWithUndoRedo'
|
|
33
|
+
|
|
34
|
+
const debug = debugWithName('plugin:withPatches')
|
|
35
|
+
const debugVerbose = false
|
|
36
|
+
|
|
37
|
+
export interface PatchFunctions {
|
|
38
|
+
insertNodePatch: (
|
|
39
|
+
editor: PortableTextSlateEditor,
|
|
40
|
+
operation: InsertNodeOperation,
|
|
41
|
+
previousChildren: Descendant[],
|
|
42
|
+
) => Patch[]
|
|
43
|
+
insertTextPatch: (
|
|
44
|
+
editor: PortableTextSlateEditor,
|
|
45
|
+
operation: InsertTextOperation,
|
|
46
|
+
previousChildren: Descendant[],
|
|
47
|
+
) => Patch[]
|
|
48
|
+
mergeNodePatch: (
|
|
49
|
+
editor: PortableTextSlateEditor,
|
|
50
|
+
operation: MergeNodeOperation,
|
|
51
|
+
previousChildren: Descendant[],
|
|
52
|
+
) => Patch[]
|
|
53
|
+
moveNodePatch: (
|
|
54
|
+
editor: PortableTextSlateEditor,
|
|
55
|
+
operation: MoveNodeOperation,
|
|
56
|
+
previousChildren: Descendant[],
|
|
57
|
+
) => Patch[]
|
|
58
|
+
removeNodePatch: (
|
|
59
|
+
editor: PortableTextSlateEditor,
|
|
60
|
+
operation: RemoveNodeOperation,
|
|
61
|
+
previousChildren: Descendant[],
|
|
62
|
+
) => Patch[]
|
|
63
|
+
removeTextPatch: (
|
|
64
|
+
editor: PortableTextSlateEditor,
|
|
65
|
+
operation: RemoveTextOperation,
|
|
66
|
+
previousChildren: Descendant[],
|
|
67
|
+
) => Patch[]
|
|
68
|
+
setNodePatch: (
|
|
69
|
+
editor: PortableTextSlateEditor,
|
|
70
|
+
operation: SetNodeOperation,
|
|
71
|
+
previousChildren: Descendant[],
|
|
72
|
+
) => Patch[]
|
|
73
|
+
splitNodePatch: (
|
|
74
|
+
editor: PortableTextSlateEditor,
|
|
75
|
+
operation: SplitNodeOperation,
|
|
76
|
+
previousChildren: Descendant[],
|
|
77
|
+
) => Patch[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface Options {
|
|
81
|
+
change$: Subject<EditorChange>
|
|
82
|
+
keyGenerator: () => string
|
|
83
|
+
patches$?: PatchObservable
|
|
84
|
+
patchFunctions: PatchFunctions
|
|
85
|
+
readOnly: boolean
|
|
86
|
+
schemaTypes: PortableTextMemberSchemaTypes
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createWithPatches({
|
|
90
|
+
change$,
|
|
91
|
+
patches$,
|
|
92
|
+
patchFunctions,
|
|
93
|
+
readOnly,
|
|
94
|
+
schemaTypes,
|
|
95
|
+
}: Options): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
|
|
96
|
+
// The previous editor children are needed to figure out the _key of deleted nodes
|
|
97
|
+
// The editor.children would no longer contain that information if the node is already deleted.
|
|
98
|
+
let previousChildren: Descendant[]
|
|
99
|
+
|
|
100
|
+
const applyPatch = createApplyPatch(schemaTypes)
|
|
101
|
+
|
|
102
|
+
return function withPatches(editor: PortableTextSlateEditor) {
|
|
103
|
+
IS_PROCESSING_REMOTE_CHANGES.set(editor, false)
|
|
104
|
+
PATCHING.set(editor, true)
|
|
105
|
+
previousChildren = [...editor.children]
|
|
106
|
+
|
|
107
|
+
const {apply} = editor
|
|
108
|
+
let bufferedPatches: Patch[] = []
|
|
109
|
+
|
|
110
|
+
const handleBufferedRemotePatches = () => {
|
|
111
|
+
if (bufferedPatches.length === 0) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
const patches = bufferedPatches
|
|
115
|
+
bufferedPatches = []
|
|
116
|
+
let changed = false
|
|
117
|
+
withRemoteChanges(editor, () => {
|
|
118
|
+
Editor.withoutNormalizing(editor, () => {
|
|
119
|
+
withoutPatching(editor, () => {
|
|
120
|
+
withoutSaving(editor, () => {
|
|
121
|
+
withPreserveKeys(editor, () => {
|
|
122
|
+
patches.forEach((patch) => {
|
|
123
|
+
if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`)
|
|
124
|
+
changed = applyPatch(editor, patch)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
if (changed) {
|
|
131
|
+
editor.normalize()
|
|
132
|
+
editor.onChange()
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handlePatches = ({patches}: {patches: Patch[]}) => {
|
|
138
|
+
const remotePatches = patches.filter((p) => p.origin !== 'local')
|
|
139
|
+
if (remotePatches.length === 0) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
bufferedPatches = bufferedPatches.concat(remotePatches)
|
|
143
|
+
handleBufferedRemotePatches()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (patches$) {
|
|
147
|
+
editor.subscriptions.push(() => {
|
|
148
|
+
debug('Subscribing to patches$')
|
|
149
|
+
const sub = patches$.subscribe(handlePatches)
|
|
150
|
+
return () => {
|
|
151
|
+
debug('Unsubscribing to patches$')
|
|
152
|
+
sub.unsubscribe()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
editor.apply = (operation: Operation): void | Editor => {
|
|
158
|
+
if (readOnly) {
|
|
159
|
+
apply(operation)
|
|
160
|
+
return editor
|
|
161
|
+
}
|
|
162
|
+
let patches: Patch[] = []
|
|
163
|
+
|
|
164
|
+
// Update previous children here before we apply
|
|
165
|
+
previousChildren = editor.children
|
|
166
|
+
|
|
167
|
+
const editorWasEmpty = isEqualToEmptyEditor(previousChildren, schemaTypes)
|
|
168
|
+
|
|
169
|
+
// Apply the operation
|
|
170
|
+
apply(operation)
|
|
171
|
+
|
|
172
|
+
const editorIsEmpty = isEqualToEmptyEditor(editor.children, schemaTypes)
|
|
173
|
+
|
|
174
|
+
if (!isPatching(editor)) {
|
|
175
|
+
if (debugVerbose && debug.enabled)
|
|
176
|
+
debug(`Editor is not producing patch for operation ${operation.type}`, operation)
|
|
177
|
+
return editor
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If the editor was empty and now isn't, insert the placeholder into it.
|
|
181
|
+
if (editorWasEmpty && !editorIsEmpty && operation.type !== 'set_selection') {
|
|
182
|
+
patches.push(insert(previousChildren, 'before', [0]))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
switch (operation.type) {
|
|
186
|
+
case 'insert_text':
|
|
187
|
+
patches = [
|
|
188
|
+
...patches,
|
|
189
|
+
...patchFunctions.insertTextPatch(editor, operation, previousChildren),
|
|
190
|
+
]
|
|
191
|
+
break
|
|
192
|
+
case 'remove_text':
|
|
193
|
+
patches = [
|
|
194
|
+
...patches,
|
|
195
|
+
...patchFunctions.removeTextPatch(editor, operation, previousChildren),
|
|
196
|
+
]
|
|
197
|
+
break
|
|
198
|
+
case 'remove_node':
|
|
199
|
+
patches = [
|
|
200
|
+
...patches,
|
|
201
|
+
...patchFunctions.removeNodePatch(editor, operation, previousChildren),
|
|
202
|
+
]
|
|
203
|
+
break
|
|
204
|
+
case 'split_node':
|
|
205
|
+
patches = [
|
|
206
|
+
...patches,
|
|
207
|
+
...patchFunctions.splitNodePatch(editor, operation, previousChildren),
|
|
208
|
+
]
|
|
209
|
+
break
|
|
210
|
+
case 'insert_node':
|
|
211
|
+
patches = [
|
|
212
|
+
...patches,
|
|
213
|
+
...patchFunctions.insertNodePatch(editor, operation, previousChildren),
|
|
214
|
+
]
|
|
215
|
+
break
|
|
216
|
+
case 'set_node':
|
|
217
|
+
patches = [
|
|
218
|
+
...patches,
|
|
219
|
+
...patchFunctions.setNodePatch(editor, operation, previousChildren),
|
|
220
|
+
]
|
|
221
|
+
break
|
|
222
|
+
case 'merge_node':
|
|
223
|
+
patches = [
|
|
224
|
+
...patches,
|
|
225
|
+
...patchFunctions.mergeNodePatch(editor, operation, previousChildren),
|
|
226
|
+
]
|
|
227
|
+
break
|
|
228
|
+
case 'move_node':
|
|
229
|
+
patches = [
|
|
230
|
+
...patches,
|
|
231
|
+
...patchFunctions.moveNodePatch(editor, operation, previousChildren),
|
|
232
|
+
]
|
|
233
|
+
break
|
|
234
|
+
case 'set_selection':
|
|
235
|
+
default:
|
|
236
|
+
// Do nothing
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Unset the value if a operation made the editor empty
|
|
240
|
+
if (
|
|
241
|
+
!editorWasEmpty &&
|
|
242
|
+
editorIsEmpty &&
|
|
243
|
+
['merge_node', 'set_node', 'remove_text', 'remove_node'].includes(operation.type)
|
|
244
|
+
) {
|
|
245
|
+
patches = [...patches, unset([])]
|
|
246
|
+
change$.next({
|
|
247
|
+
type: 'unset',
|
|
248
|
+
previousValue: fromSlateValue(
|
|
249
|
+
previousChildren,
|
|
250
|
+
schemaTypes.block.name,
|
|
251
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
252
|
+
),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Prepend patches with setIfMissing if going from empty editor to something involving a patch.
|
|
257
|
+
if (editorWasEmpty && patches.length > 0) {
|
|
258
|
+
patches = [setIfMissing([], []), ...patches]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Emit all patches
|
|
262
|
+
if (patches.length > 0) {
|
|
263
|
+
patches.forEach((patch) => {
|
|
264
|
+
change$.next({
|
|
265
|
+
type: 'patch',
|
|
266
|
+
patch: {...patch, origin: 'local'},
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
return editor
|
|
271
|
+
}
|
|
272
|
+
return editor
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {Editor, Path} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {type PortableTextSlateEditor} from '../../types/editor'
|
|
4
|
+
import {type SlateTextBlock, type VoidElement} from '../../types/slate'
|
|
5
|
+
import {debugWithName} from '../../utils/debug'
|
|
6
|
+
|
|
7
|
+
const debug = debugWithName('plugin:withPlaceholderBlock')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keep a "placeholder" block present when the editor is empty
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
export function createWithPlaceholderBlock(): (
|
|
14
|
+
editor: PortableTextSlateEditor,
|
|
15
|
+
) => PortableTextSlateEditor {
|
|
16
|
+
return function withPlaceholderBlock(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
17
|
+
const {apply} = editor
|
|
18
|
+
|
|
19
|
+
editor.apply = (op) => {
|
|
20
|
+
if (op.type === 'remove_node') {
|
|
21
|
+
const node = op.node as SlateTextBlock | VoidElement
|
|
22
|
+
if (op.path[0] === 0 && Editor.isVoid(editor, node)) {
|
|
23
|
+
// Check next path, if it exists, do nothing
|
|
24
|
+
const nextPath = Path.next(op.path)
|
|
25
|
+
// Is removing the first block which is a void (not a text block), add a new empty text block in it, if there is no other element in the next path
|
|
26
|
+
if (!editor.children[nextPath[0]]) {
|
|
27
|
+
debug('Adding placeholder block')
|
|
28
|
+
Editor.insertNode(editor, editor.pteCreateEmptyBlock())
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
apply(op)
|
|
33
|
+
}
|
|
34
|
+
return editor
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {Editor, type Node, Path, Text as SlateText, Transforms} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
|
|
4
|
+
import {debugWithName} from '../../utils/debug'
|
|
5
|
+
|
|
6
|
+
const debug = debugWithName('plugin:withPortableTextBlockStyle')
|
|
7
|
+
|
|
8
|
+
export function createWithPortableTextBlockStyle(
|
|
9
|
+
types: PortableTextMemberSchemaTypes,
|
|
10
|
+
): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
|
|
11
|
+
const defaultStyle = types.styles[0].value
|
|
12
|
+
return function withPortableTextBlockStyle(
|
|
13
|
+
editor: PortableTextSlateEditor,
|
|
14
|
+
): PortableTextSlateEditor {
|
|
15
|
+
// Extend Slate's default normalization to reset split node to normal style
|
|
16
|
+
// if there is no text at the right end of the split.
|
|
17
|
+
const {normalizeNode} = editor
|
|
18
|
+
editor.normalizeNode = (nodeEntry) => {
|
|
19
|
+
normalizeNode(nodeEntry)
|
|
20
|
+
const [, path] = nodeEntry
|
|
21
|
+
for (const op of editor.operations) {
|
|
22
|
+
if (
|
|
23
|
+
op.type === 'split_node' &&
|
|
24
|
+
op.path.length === 1 &&
|
|
25
|
+
editor.isTextBlock(op.properties) &&
|
|
26
|
+
op.properties.style !== defaultStyle &&
|
|
27
|
+
op.path[0] === path[0] &&
|
|
28
|
+
!Path.equals(path, op.path)
|
|
29
|
+
) {
|
|
30
|
+
const [child] = Editor.node(editor, [op.path[0] + 1, 0])
|
|
31
|
+
if (SlateText.isText(child) && child.text === '') {
|
|
32
|
+
debug(`Normalizing split node to ${defaultStyle} style`, op)
|
|
33
|
+
Transforms.setNodes(editor, {style: defaultStyle}, {at: [op.path[0] + 1], voids: false})
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
editor.pteHasBlockStyle = (style: string): boolean => {
|
|
40
|
+
if (!editor.selection) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
const selectedBlocks = [
|
|
44
|
+
...Editor.nodes(editor, {
|
|
45
|
+
at: editor.selection,
|
|
46
|
+
match: (node) => editor.isTextBlock(node) && node.style === style,
|
|
47
|
+
}),
|
|
48
|
+
]
|
|
49
|
+
if (selectedBlocks.length > 0) {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
editor.pteToggleBlockStyle = (blockStyle: string): void => {
|
|
56
|
+
if (!editor.selection) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const selectedBlocks = [
|
|
60
|
+
...Editor.nodes(editor, {
|
|
61
|
+
at: editor.selection,
|
|
62
|
+
match: (node) => editor.isTextBlock(node),
|
|
63
|
+
}),
|
|
64
|
+
]
|
|
65
|
+
selectedBlocks.forEach(([node, path]) => {
|
|
66
|
+
if (editor.isTextBlock(node) && node.style === blockStyle) {
|
|
67
|
+
debug(`Unsetting block style '${blockStyle}'`)
|
|
68
|
+
Transforms.setNodes(editor, {...node, style: defaultStyle} as Partial<Node>, {
|
|
69
|
+
at: path,
|
|
70
|
+
})
|
|
71
|
+
} else {
|
|
72
|
+
if (blockStyle) {
|
|
73
|
+
debug(`Setting style '${blockStyle}'`)
|
|
74
|
+
} else {
|
|
75
|
+
debug('Setting default style', defaultStyle)
|
|
76
|
+
}
|
|
77
|
+
Transforms.setNodes(
|
|
78
|
+
editor,
|
|
79
|
+
{
|
|
80
|
+
...node,
|
|
81
|
+
style: blockStyle || defaultStyle,
|
|
82
|
+
} as Partial<Node>,
|
|
83
|
+
{at: path},
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
editor.onChange()
|
|
88
|
+
}
|
|
89
|
+
return editor
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {Editor, Element, type Node, Text, Transforms} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
|
|
4
|
+
import {debugWithName} from '../../utils/debug'
|
|
5
|
+
|
|
6
|
+
const debug = debugWithName('plugin:withPortableTextLists')
|
|
7
|
+
const MAX_LIST_LEVEL = 10
|
|
8
|
+
|
|
9
|
+
export function createWithPortableTextLists(types: PortableTextMemberSchemaTypes) {
|
|
10
|
+
return function withPortableTextLists(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
11
|
+
editor.pteToggleListItem = (listItemStyle: string) => {
|
|
12
|
+
const isActive = editor.pteHasListStyle(listItemStyle)
|
|
13
|
+
if (isActive) {
|
|
14
|
+
debug(`Remove list item '${listItemStyle}'`)
|
|
15
|
+
editor.pteUnsetListItem(listItemStyle)
|
|
16
|
+
} else {
|
|
17
|
+
debug(`Add list item '${listItemStyle}'`)
|
|
18
|
+
editor.pteSetListItem(listItemStyle)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
editor.pteUnsetListItem = (listItemStyle: string) => {
|
|
23
|
+
if (!editor.selection) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const selectedBlocks = [
|
|
27
|
+
...Editor.nodes(editor, {
|
|
28
|
+
at: editor.selection,
|
|
29
|
+
match: (node) => Element.isElement(node) && node._type === types.block.name,
|
|
30
|
+
}),
|
|
31
|
+
]
|
|
32
|
+
selectedBlocks.forEach(([node, path]) => {
|
|
33
|
+
if (editor.isListBlock(node)) {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
35
|
+
const {listItem, level, ...rest} = node
|
|
36
|
+
const newNode = {
|
|
37
|
+
...rest,
|
|
38
|
+
listItem: undefined,
|
|
39
|
+
level: undefined,
|
|
40
|
+
} as Partial<Node>
|
|
41
|
+
debug(`Unsetting list '${listItemStyle}'`)
|
|
42
|
+
Transforms.setNodes(editor, newNode, {at: path})
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
editor.pteSetListItem = (listItemStyle: string) => {
|
|
48
|
+
if (!editor.selection) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const selectedBlocks = [
|
|
52
|
+
...Editor.nodes(editor, {
|
|
53
|
+
at: editor.selection,
|
|
54
|
+
match: (node) => editor.isTextBlock(node),
|
|
55
|
+
}),
|
|
56
|
+
]
|
|
57
|
+
selectedBlocks.forEach(([node, path]) => {
|
|
58
|
+
debug(`Setting list '${listItemStyle}'`)
|
|
59
|
+
Transforms.setNodes(
|
|
60
|
+
editor,
|
|
61
|
+
{
|
|
62
|
+
...node,
|
|
63
|
+
level: 1,
|
|
64
|
+
listItem: listItemStyle || (types.lists[0] && types.lists[0].value),
|
|
65
|
+
} as Partial<Node>,
|
|
66
|
+
{at: path},
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
editor.pteEndList = () => {
|
|
72
|
+
if (!editor.selection) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
const selectedBlocks = [
|
|
76
|
+
...Editor.nodes(editor, {
|
|
77
|
+
at: editor.selection,
|
|
78
|
+
match: (node) =>
|
|
79
|
+
Element.isElement(node) &&
|
|
80
|
+
editor.isListBlock(node) &&
|
|
81
|
+
node.children.length === 1 &&
|
|
82
|
+
Text.isText(node.children[0]) &&
|
|
83
|
+
node.children[0].text === '',
|
|
84
|
+
}),
|
|
85
|
+
]
|
|
86
|
+
if (selectedBlocks.length === 0) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
selectedBlocks.forEach(([node, path]) => {
|
|
90
|
+
if (Element.isElement(node)) {
|
|
91
|
+
debug('Unset list')
|
|
92
|
+
Transforms.setNodes(
|
|
93
|
+
editor,
|
|
94
|
+
{
|
|
95
|
+
...node,
|
|
96
|
+
level: undefined,
|
|
97
|
+
listItem: undefined,
|
|
98
|
+
},
|
|
99
|
+
{at: path},
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
return true // Note: we are exiting the plugin chain by not returning editor (or hotkey plugin 'enter' will fire)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
editor.pteIncrementBlockLevels = (reverse?: boolean): boolean => {
|
|
107
|
+
if (!editor.selection) {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
const selectedBlocks = [
|
|
111
|
+
...Editor.nodes(editor, {
|
|
112
|
+
at: editor.selection,
|
|
113
|
+
match: (node) => !!editor.isListBlock(node),
|
|
114
|
+
}),
|
|
115
|
+
]
|
|
116
|
+
if (selectedBlocks.length === 0) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
selectedBlocks.forEach(([node, path]) => {
|
|
120
|
+
if (editor.isListBlock(node)) {
|
|
121
|
+
let level = node.level || 1
|
|
122
|
+
if (reverse) {
|
|
123
|
+
level--
|
|
124
|
+
debug('Decrementing list level', Math.min(MAX_LIST_LEVEL, Math.max(1, level)))
|
|
125
|
+
} else {
|
|
126
|
+
level++
|
|
127
|
+
debug('Incrementing list level', Math.min(MAX_LIST_LEVEL, Math.max(1, level)))
|
|
128
|
+
}
|
|
129
|
+
Transforms.setNodes(
|
|
130
|
+
editor,
|
|
131
|
+
{level: Math.min(MAX_LIST_LEVEL, Math.max(1, level))},
|
|
132
|
+
{at: path},
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
editor.pteHasListStyle = (listStyle: string): boolean => {
|
|
140
|
+
if (!editor.selection) {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
const selectedBlocks = [
|
|
144
|
+
...Editor.nodes(editor, {
|
|
145
|
+
at: editor.selection,
|
|
146
|
+
match: (node) => editor.isTextBlock(node),
|
|
147
|
+
}),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
if (selectedBlocks.length > 0) {
|
|
151
|
+
return selectedBlocks.every(
|
|
152
|
+
([node]) => editor.isListBlock(node) && node.listItem === listStyle,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return editor
|
|
159
|
+
}
|
|
160
|
+
}
|