@portabletext/editor 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.esm.js +64 -57
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +64 -57
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +64 -57
- package/lib/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +73 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -51,18 +51,18 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jest/globals": "^29.7.0",
|
|
54
|
-
"@playwright/test": "1.46.
|
|
54
|
+
"@playwright/test": "1.46.1",
|
|
55
55
|
"@portabletext/toolkit": "^2.0.15",
|
|
56
|
-
"@sanity/block-tools": "^3.
|
|
56
|
+
"@sanity/block-tools": "^3.55.0",
|
|
57
57
|
"@sanity/diff-match-patch": "^3.1.1",
|
|
58
58
|
"@sanity/eslint-config-i18n": "^1.1.0",
|
|
59
59
|
"@sanity/eslint-config-studio": "^4.0.0",
|
|
60
|
-
"@sanity/pkg-utils": "^6.10.
|
|
61
|
-
"@sanity/schema": "^3.
|
|
60
|
+
"@sanity/pkg-utils": "^6.10.10",
|
|
61
|
+
"@sanity/schema": "^3.55.0",
|
|
62
62
|
"@sanity/test": "0.0.1-alpha.1",
|
|
63
|
-
"@sanity/types": "^3.
|
|
63
|
+
"@sanity/types": "^3.55.0",
|
|
64
64
|
"@sanity/ui": "^2.8.8",
|
|
65
|
-
"@sanity/util": "^3.
|
|
65
|
+
"@sanity/util": "^3.55.0",
|
|
66
66
|
"@testing-library/dom": "^10.4.0",
|
|
67
67
|
"@testing-library/react": "^16.0.0",
|
|
68
68
|
"@types/debug": "^4.1.5",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@types/react": "^18.3.3",
|
|
75
75
|
"@types/react-dom": "^18.3.0",
|
|
76
76
|
"@types/ws": "~8.5.12",
|
|
77
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
78
|
-
"@typescript-eslint/parser": "^8.
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
|
78
|
+
"@typescript-eslint/parser": "^8.2.0",
|
|
79
79
|
"@vitejs/plugin-react": "^4.3.1",
|
|
80
80
|
"dotenv": "^16.4.5",
|
|
81
81
|
"eslint": "^8.57.0",
|
|
@@ -84,14 +84,14 @@
|
|
|
84
84
|
"eslint-import-resolver-typescript": "^3.6.1",
|
|
85
85
|
"eslint-plugin-import": "^2.29.1",
|
|
86
86
|
"eslint-plugin-prettier": "^5.2.1",
|
|
87
|
-
"eslint-plugin-react-compiler": "0.0.0-experimental-
|
|
87
|
+
"eslint-plugin-react-compiler": "0.0.0-experimental-eeb1b2a-20240818",
|
|
88
88
|
"eslint-plugin-tsdoc": "^0.3.0",
|
|
89
89
|
"eslint-plugin-unicorn": "^55.0.0",
|
|
90
90
|
"eslint-plugin-unused-imports": "^4.1.3",
|
|
91
91
|
"express": "^4.19.2",
|
|
92
92
|
"express-ws": "^5.0.2",
|
|
93
93
|
"jest": "^29.7.0",
|
|
94
|
-
"jest-dev-server": "^10.
|
|
94
|
+
"jest-dev-server": "^10.1.0",
|
|
95
95
|
"jest-environment-jsdom": "^29.7.0",
|
|
96
96
|
"jest-environment-node": "^29.7.0",
|
|
97
97
|
"node-ipc": "npm:@node-ipc/compat@9.2.5",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"styled-components": "^6.1.12",
|
|
102
102
|
"tsx": "^4.17.0",
|
|
103
103
|
"typescript": "5.5.4",
|
|
104
|
-
"vite": "^5.4.
|
|
104
|
+
"vite": "^5.4.2"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@sanity/block-tools": "^3.47.1",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
|
|
10
10
|
import {isEqual, uniq} from 'lodash'
|
|
11
11
|
import {type Subject} from 'rxjs'
|
|
12
|
-
import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate'
|
|
12
|
+
import {type Descendant, Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
type EditorChange,
|
|
@@ -54,29 +54,38 @@ export function createWithPortableTextMarkModel(
|
|
|
54
54
|
|
|
55
55
|
// Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks
|
|
56
56
|
editor.normalizeNode = (nodeEntry) => {
|
|
57
|
-
normalizeNode(nodeEntry)
|
|
58
|
-
if (
|
|
59
|
-
editor.operations.some((op) =>
|
|
60
|
-
[
|
|
61
|
-
'insert_node',
|
|
62
|
-
'insert_text',
|
|
63
|
-
'merge_node',
|
|
64
|
-
'remove_node',
|
|
65
|
-
'remove_text',
|
|
66
|
-
'set_node',
|
|
67
|
-
].includes(op.type),
|
|
68
|
-
)
|
|
69
|
-
) {
|
|
70
|
-
mergeSpans(editor)
|
|
71
|
-
}
|
|
72
57
|
const [node, path] = nodeEntry
|
|
58
|
+
|
|
73
59
|
const isSpan = Text.isText(node) && node._type === types.span.name
|
|
74
60
|
const isTextBlock = editor.isTextBlock(node)
|
|
61
|
+
|
|
62
|
+
if (editor.isTextBlock(node)) {
|
|
63
|
+
const children = Node.children(editor, path)
|
|
64
|
+
|
|
65
|
+
for (const [child, childPath] of children) {
|
|
66
|
+
const nextNode = node.children[childPath[1] + 1]
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
editor.isTextSpan(child) &&
|
|
70
|
+
editor.isTextSpan(nextNode) &&
|
|
71
|
+
isEqual(child.marks, nextNode.marks)
|
|
72
|
+
) {
|
|
73
|
+
debug(
|
|
74
|
+
'Merging spans',
|
|
75
|
+
JSON.stringify(child, null, 2),
|
|
76
|
+
JSON.stringify(nextNode, null, 2),
|
|
77
|
+
)
|
|
78
|
+
Transforms.mergeNodes(editor, {at: [childPath[0], childPath[1] + 1], voids: true})
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
if (isSpan || isTextBlock) {
|
|
76
85
|
if (isSpan && !Array.isArray(node.marks)) {
|
|
77
86
|
debug('Adding .marks to span node')
|
|
78
87
|
Transforms.setNodes(editor, {marks: []}, {at: path})
|
|
79
|
-
|
|
88
|
+
return
|
|
80
89
|
}
|
|
81
90
|
const hasSpanMarks = isSpan && (node.marks || []).length > 0
|
|
82
91
|
if (hasSpanMarks) {
|
|
@@ -100,7 +109,7 @@ export function createWithPortableTextMarkModel(
|
|
|
100
109
|
{marks: spanMarks.filter((mark) => !orphanedMarks.includes(mark))},
|
|
101
110
|
{at: path},
|
|
102
111
|
)
|
|
103
|
-
|
|
112
|
+
return
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
}
|
|
@@ -124,7 +133,7 @@ export function createWithPortableTextMarkModel(
|
|
|
124
133
|
// eslint-disable-next-line max-depth
|
|
125
134
|
if (!isNormalized) {
|
|
126
135
|
Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
|
|
127
|
-
|
|
136
|
+
return
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -148,7 +157,7 @@ export function createWithPortableTextMarkModel(
|
|
|
148
157
|
{markDefs: uniq([...oldDefs, ...op.properties.markDefs])},
|
|
149
158
|
{at: targetPath, voids: false},
|
|
150
159
|
)
|
|
151
|
-
|
|
160
|
+
return
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
163
|
// Make sure marks are reset, if a block is split at the end.
|
|
@@ -169,7 +178,7 @@ export function createWithPortableTextMarkModel(
|
|
|
169
178
|
child.marks.length > 0
|
|
170
179
|
) {
|
|
171
180
|
Transforms.setNodes(editor, {marks: []}, {at: childPath, voids: false})
|
|
172
|
-
|
|
181
|
+
return
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
// Make sure markDefs are reset, if a block is split at start.
|
|
@@ -192,19 +201,10 @@ export function createWithPortableTextMarkModel(
|
|
|
192
201
|
(!block.children[0].marks || block.children[0].marks.length === 0)
|
|
193
202
|
) {
|
|
194
203
|
Transforms.setNodes(editor, {markDefs: []}, {at: blockPath})
|
|
195
|
-
|
|
204
|
+
return
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
}
|
|
199
|
-
// Empty marks if text is empty
|
|
200
|
-
if (
|
|
201
|
-
isSpan &&
|
|
202
|
-
Array.isArray(node.marks) &&
|
|
203
|
-
(!node.marks || (node.marks.length > 0 && node.text === ''))
|
|
204
|
-
) {
|
|
205
|
-
Transforms.setNodes(editor, {marks: []}, {at: path, voids: false})
|
|
206
|
-
editor.onChange()
|
|
207
|
-
}
|
|
208
208
|
}
|
|
209
209
|
// Check consistency of markDefs (unless we are merging two nodes)
|
|
210
210
|
if (
|
|
@@ -229,9 +229,11 @@ export function createWithPortableTextMarkModel(
|
|
|
229
229
|
},
|
|
230
230
|
{at: path},
|
|
231
231
|
)
|
|
232
|
-
|
|
232
|
+
return
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
+
|
|
236
|
+
normalizeNode(nodeEntry)
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
editor.apply = (op) => {
|
|
@@ -343,6 +345,18 @@ export function createWithPortableTextMarkModel(
|
|
|
343
345
|
editor.onChange()
|
|
344
346
|
return
|
|
345
347
|
}
|
|
348
|
+
|
|
349
|
+
const nodeHasMarks = node.marks !== undefined && node.marks.length > 0
|
|
350
|
+
|
|
351
|
+
if (nodeHasMarks && deletingAllText) {
|
|
352
|
+
Editor.withoutNormalizing(editor, () => {
|
|
353
|
+
apply(op)
|
|
354
|
+
Transforms.setNodes(editor, {marks: []}, {at: op.path})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
editor.onChange()
|
|
358
|
+
return
|
|
359
|
+
}
|
|
346
360
|
}
|
|
347
361
|
}
|
|
348
362
|
|
|
@@ -353,34 +367,35 @@ export function createWithPortableTextMarkModel(
|
|
|
353
367
|
editor.addMark = (mark: string) => {
|
|
354
368
|
if (editor.selection) {
|
|
355
369
|
if (Range.isExpanded(editor.selection)) {
|
|
356
|
-
// Split if needed
|
|
357
|
-
Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
|
|
358
|
-
// Use new selection
|
|
359
|
-
const splitTextNodes = [
|
|
360
|
-
...Editor.nodes(editor, {at: editor.selection, match: Text.isText}),
|
|
361
|
-
]
|
|
362
|
-
const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark))
|
|
363
|
-
|
|
364
|
-
if (shouldRemoveMark) {
|
|
365
|
-
editor.removeMark(mark)
|
|
366
|
-
return editor
|
|
367
|
-
}
|
|
368
370
|
Editor.withoutNormalizing(editor, () => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
)
|
|
381
|
-
}
|
|
371
|
+
// Split if needed
|
|
372
|
+
Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
|
|
373
|
+
// Use new selection
|
|
374
|
+
const splitTextNodes = Range.isRange(editor.selection)
|
|
375
|
+
? [...Editor.nodes(editor, {at: editor.selection, match: Text.isText})]
|
|
376
|
+
: []
|
|
377
|
+
const shouldRemoveMark =
|
|
378
|
+
splitTextNodes.length > 1 &&
|
|
379
|
+
splitTextNodes.every((node) => node[0].marks?.includes(mark))
|
|
380
|
+
|
|
381
|
+
if (shouldRemoveMark) {
|
|
382
|
+
editor.removeMark(mark)
|
|
383
|
+
} else {
|
|
384
|
+
splitTextNodes.forEach(([node, path]) => {
|
|
385
|
+
const marks = [
|
|
386
|
+
...(Array.isArray(node.marks) ? node.marks : []).filter(
|
|
387
|
+
(eMark: string) => eMark !== mark,
|
|
388
|
+
),
|
|
389
|
+
mark,
|
|
390
|
+
]
|
|
391
|
+
Transforms.setNodes(
|
|
392
|
+
editor,
|
|
393
|
+
{marks},
|
|
394
|
+
{at: path, match: Text.isText, split: true, hanging: true},
|
|
395
|
+
)
|
|
396
|
+
})
|
|
397
|
+
}
|
|
382
398
|
})
|
|
383
|
-
Editor.normalize(editor)
|
|
384
399
|
} else {
|
|
385
400
|
const existingMarks: string[] =
|
|
386
401
|
{
|
|
@@ -486,36 +501,4 @@ export function createWithPortableTextMarkModel(
|
|
|
486
501
|
}
|
|
487
502
|
return editor
|
|
488
503
|
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Normalize re-marked spans in selection
|
|
492
|
-
*/
|
|
493
|
-
function mergeSpans(editor: PortableTextSlateEditor) {
|
|
494
|
-
const {selection} = editor
|
|
495
|
-
|
|
496
|
-
if (selection) {
|
|
497
|
-
const textNodesInSelection = Array.from(
|
|
498
|
-
Editor.nodes(editor, {
|
|
499
|
-
at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]),
|
|
500
|
-
match: Text.isText,
|
|
501
|
-
reverse: true,
|
|
502
|
-
}),
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
for (const [node, path] of textNodesInSelection) {
|
|
506
|
-
const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined]
|
|
507
|
-
const nextPath = [path[0], path[1] + 1]
|
|
508
|
-
|
|
509
|
-
if (editor.isTextBlock(parent)) {
|
|
510
|
-
const nextNode = parent.children[nextPath[1]]
|
|
511
|
-
|
|
512
|
-
if (Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
|
|
513
|
-
debug('Merging spans')
|
|
514
|
-
Transforms.mergeNodes(editor, {at: nextPath, voids: true})
|
|
515
|
-
editor.onChange()
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
504
|
}
|