@portabletext/editor 1.0.18 → 1.0.19
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 +47 -56
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +47 -56
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +47 -56
- package/lib/index.mjs.map +1 -1
- package/package.json +7 -3
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +0 -533
- package/src/editor/plugins/createWithEditableAPI.ts +10 -1
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +97 -118
- package/src/utils/values.ts +0 -1
- package/src/editor/plugins/__tests__/withHotkeys.test.tsx +0 -212
- package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +0 -220
- package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +0 -133
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
} from '../../types/editor'
|
|
19
19
|
import {debugWithName} from '../../utils/debug'
|
|
20
20
|
import {toPortableTextRange} from '../../utils/ranges'
|
|
21
|
-
import {EMPTY_MARKS} from '../../utils/values'
|
|
22
21
|
import {isChangingRemotely} from '../../utils/withChanges'
|
|
23
22
|
import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
|
|
24
23
|
|
|
@@ -56,9 +55,6 @@ export function createWithPortableTextMarkModel(
|
|
|
56
55
|
editor.normalizeNode = (nodeEntry) => {
|
|
57
56
|
const [node, path] = nodeEntry
|
|
58
57
|
|
|
59
|
-
const isSpan = Text.isText(node) && node._type === types.span.name
|
|
60
|
-
const isTextBlock = editor.isTextBlock(node)
|
|
61
|
-
|
|
62
58
|
if (editor.isTextBlock(node)) {
|
|
63
59
|
const children = Node.children(editor, path)
|
|
64
60
|
|
|
@@ -81,131 +77,89 @@ export function createWithPortableTextMarkModel(
|
|
|
81
77
|
}
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
return
|
|
113
|
-
}
|
|
80
|
+
/**
|
|
81
|
+
* Add missing .marks to span nodes
|
|
82
|
+
*/
|
|
83
|
+
if (editor.isTextSpan(node) && !Array.isArray(node.marks)) {
|
|
84
|
+
debug('Adding .marks to span node')
|
|
85
|
+
Transforms.setNodes(editor, {marks: []}, {at: path})
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove annotations from empty spans
|
|
91
|
+
*/
|
|
92
|
+
if (editor.isTextSpan(node)) {
|
|
93
|
+
const blockPath = Path.parent(path)
|
|
94
|
+
const [block] = Editor.node(editor, blockPath)
|
|
95
|
+
const decorators = types.decorators.map((decorator) => decorator.value)
|
|
96
|
+
const annotations = node.marks?.filter((mark) => !decorators.includes(mark))
|
|
97
|
+
|
|
98
|
+
if (editor.isTextBlock(block)) {
|
|
99
|
+
if (node.text === '' && annotations && annotations.length > 0) {
|
|
100
|
+
debug('Removing annotations from empty span node')
|
|
101
|
+
Transforms.setNodes(
|
|
102
|
+
editor,
|
|
103
|
+
{marks: node.marks?.filter((mark) => decorators.includes(mark))},
|
|
104
|
+
{at: path},
|
|
105
|
+
)
|
|
106
|
+
return
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// eslint-disable-next-line max-depth
|
|
134
|
-
if (!isNormalized) {
|
|
135
|
-
Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Make sure markDefs are copied over to new block when splitting a block.
|
|
141
|
-
if (
|
|
142
|
-
op.type === 'split_node' &&
|
|
143
|
-
op.path.length === 1 &&
|
|
144
|
-
Element.isElementProps(op.properties) &&
|
|
145
|
-
op.properties._type === types.block.name &&
|
|
146
|
-
'markDefs' in op.properties &&
|
|
147
|
-
Array.isArray(op.properties.markDefs) &&
|
|
148
|
-
op.properties.markDefs.length > 0 &&
|
|
149
|
-
op.path[0] + 1 < editor.children.length
|
|
150
|
-
) {
|
|
151
|
-
const [targetBlock, targetPath] = Editor.node(editor, [op.path[0] + 1])
|
|
152
|
-
debug(`Copying markDefs over to split block`, op)
|
|
153
|
-
if (editor.isTextBlock(targetBlock)) {
|
|
154
|
-
const oldDefs = (Array.isArray(targetBlock.markDefs) && targetBlock.markDefs) || []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove orphaned annotations from child spans of block nodes
|
|
113
|
+
*/
|
|
114
|
+
if (editor.isTextBlock(node)) {
|
|
115
|
+
const decorators = types.decorators.map((decorator) => decorator.value)
|
|
116
|
+
|
|
117
|
+
for (const [child, childPath] of Node.children(editor, path)) {
|
|
118
|
+
if (editor.isTextSpan(child)) {
|
|
119
|
+
const marks = child.marks ?? []
|
|
120
|
+
const orphanedAnnotations = marks.filter((mark) => {
|
|
121
|
+
return !decorators.includes(mark) && !node.markDefs?.find((def) => def._key === mark)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (orphanedAnnotations.length > 0) {
|
|
125
|
+
debug('Removing orphaned annotations from span node')
|
|
155
126
|
Transforms.setNodes(
|
|
156
127
|
editor,
|
|
157
|
-
{
|
|
158
|
-
{at:
|
|
128
|
+
{marks: marks.filter((mark) => !orphanedAnnotations.includes(mark))},
|
|
129
|
+
{at: childPath},
|
|
159
130
|
)
|
|
160
131
|
return
|
|
161
132
|
}
|
|
162
133
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
(op.properties as unknown as Descendant)._type === types.block.name &&
|
|
189
|
-
'markDefs' in op.properties &&
|
|
190
|
-
Array.isArray(op.properties.markDefs) &&
|
|
191
|
-
op.properties.markDefs.length > 0
|
|
192
|
-
) {
|
|
193
|
-
const [block, blockPath] = Editor.node(editor, [op.path[0]])
|
|
194
|
-
if (
|
|
195
|
-
editor.isTextBlock(block) &&
|
|
196
|
-
block.children.length === 1 &&
|
|
197
|
-
block.markDefs &&
|
|
198
|
-
block.markDefs.length > 0 &&
|
|
199
|
-
Text.isText(block.children[0]) &&
|
|
200
|
-
block.children[0].text === '' &&
|
|
201
|
-
(!block.children[0].marks || block.children[0].marks.length === 0)
|
|
202
|
-
) {
|
|
203
|
-
Transforms.setNodes(editor, {markDefs: []}, {at: blockPath})
|
|
204
|
-
return
|
|
205
|
-
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove orphaned annotations from span nodes
|
|
139
|
+
*/
|
|
140
|
+
if (editor.isTextSpan(node)) {
|
|
141
|
+
const blockPath = Path.parent(path)
|
|
142
|
+
const [block] = Editor.node(editor, blockPath)
|
|
143
|
+
|
|
144
|
+
if (editor.isTextBlock(block)) {
|
|
145
|
+
const decorators = types.decorators.map((decorator) => decorator.value)
|
|
146
|
+
const marks = node.marks ?? []
|
|
147
|
+
const orphanedAnnotations = marks.filter((mark) => {
|
|
148
|
+
return !decorators.includes(mark) && !block.markDefs?.find((def) => def._key === mark)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if (orphanedAnnotations.length > 0) {
|
|
152
|
+
debug('Removing orphaned annotations from span node')
|
|
153
|
+
Transforms.setNodes(
|
|
154
|
+
editor,
|
|
155
|
+
{marks: marks.filter((mark) => !orphanedAnnotations.includes(mark))},
|
|
156
|
+
{at: path},
|
|
157
|
+
)
|
|
158
|
+
return
|
|
206
159
|
}
|
|
207
160
|
}
|
|
208
161
|
}
|
|
162
|
+
|
|
209
163
|
// Check consistency of markDefs (unless we are merging two nodes)
|
|
210
164
|
if (
|
|
211
165
|
editor.isTextBlock(node) &&
|
|
@@ -360,6 +314,31 @@ export function createWithPortableTextMarkModel(
|
|
|
360
314
|
}
|
|
361
315
|
}
|
|
362
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Copy over markDefs when merging blocks
|
|
319
|
+
*/
|
|
320
|
+
if (
|
|
321
|
+
op.type === 'merge_node' &&
|
|
322
|
+
op.path.length === 1 &&
|
|
323
|
+
'markDefs' in op.properties &&
|
|
324
|
+
op.properties._type === types.block.name &&
|
|
325
|
+
Array.isArray(op.properties.markDefs) &&
|
|
326
|
+
op.properties.markDefs.length > 0 &&
|
|
327
|
+
op.path[0] - 1 >= 0
|
|
328
|
+
) {
|
|
329
|
+
const [targetBlock, targetPath] = Editor.node(editor, [op.path[0] - 1])
|
|
330
|
+
|
|
331
|
+
if (editor.isTextBlock(targetBlock)) {
|
|
332
|
+
const oldDefs = (Array.isArray(targetBlock.markDefs) && targetBlock.markDefs) || []
|
|
333
|
+
const newMarkDefs = uniq([...oldDefs, ...op.properties.markDefs])
|
|
334
|
+
|
|
335
|
+
debug(`Copying markDefs over to merged block`, op)
|
|
336
|
+
Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
|
|
337
|
+
apply(op)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
363
342
|
apply(op)
|
|
364
343
|
}
|
|
365
344
|
|
package/src/utils/values.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {type Descendant, Element, type Node, Text} from 'slate'
|
|
|
11
11
|
import {type PortableTextMemberSchemaTypes} from '../types/editor'
|
|
12
12
|
|
|
13
13
|
export const EMPTY_MARKDEFS: PortableTextObject[] = []
|
|
14
|
-
export const EMPTY_MARKS: string[] = []
|
|
15
14
|
|
|
16
15
|
export const VOID_CHILD_KEY = 'void-child'
|
|
17
16
|
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import {describe, expect, it, jest} from '@jest/globals'
|
|
2
|
-
import {render, waitFor} from '@testing-library/react'
|
|
3
|
-
import {createRef, type RefObject} from 'react'
|
|
4
|
-
|
|
5
|
-
import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester'
|
|
6
|
-
import {getEditableElement, triggerKeyboardEvent} from '../../__tests__/utils'
|
|
7
|
-
import {PortableTextEditor} from '../../PortableTextEditor'
|
|
8
|
-
|
|
9
|
-
const newBlock = {
|
|
10
|
-
_type: 'myTestBlockType',
|
|
11
|
-
_key: '3',
|
|
12
|
-
style: 'normal',
|
|
13
|
-
markDefs: [],
|
|
14
|
-
children: [
|
|
15
|
-
{
|
|
16
|
-
_type: 'span',
|
|
17
|
-
_key: '2',
|
|
18
|
-
text: '',
|
|
19
|
-
marks: [],
|
|
20
|
-
},
|
|
21
|
-
],
|
|
22
|
-
}
|
|
23
|
-
describe('plugin:withHotkeys: .ArrowDown', () => {
|
|
24
|
-
it('a new block is added if the user is focused on the only block which is void, and presses arrow down.', async () => {
|
|
25
|
-
const initialValue = [
|
|
26
|
-
{
|
|
27
|
-
_key: 'a',
|
|
28
|
-
_type: 'someObject',
|
|
29
|
-
},
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
const initialSelection = {
|
|
33
|
-
focus: {path: [{_key: 'a'}], offset: 0},
|
|
34
|
-
anchor: {path: [{_key: 'a'}], offset: 0},
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
38
|
-
const onChange = jest.fn()
|
|
39
|
-
const component = render(
|
|
40
|
-
<PortableTextEditorTester
|
|
41
|
-
onChange={onChange}
|
|
42
|
-
ref={editorRef}
|
|
43
|
-
schemaType={schemaType}
|
|
44
|
-
value={initialValue}
|
|
45
|
-
/>,
|
|
46
|
-
)
|
|
47
|
-
const element = await getEditableElement(component)
|
|
48
|
-
|
|
49
|
-
const editor = editorRef.current
|
|
50
|
-
const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
|
|
51
|
-
await waitFor(async () => {
|
|
52
|
-
if (editor && inlineType && editor) {
|
|
53
|
-
PortableTextEditor.focus(editor)
|
|
54
|
-
PortableTextEditor.select(editor, initialSelection)
|
|
55
|
-
PortableTextEditor.insertBreak(editor)
|
|
56
|
-
await triggerKeyboardEvent('ArrowDown', element)
|
|
57
|
-
|
|
58
|
-
const value = PortableTextEditor.getValue(editor)
|
|
59
|
-
expect(value).toEqual([initialValue[0], newBlock])
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
it('a new block is added if the user is focused on the last block which is void, and presses arrow down.', async () => {
|
|
64
|
-
const initialValue = [
|
|
65
|
-
{
|
|
66
|
-
_type: 'myTestBlockType',
|
|
67
|
-
_key: 'a',
|
|
68
|
-
style: 'normal',
|
|
69
|
-
markDefs: [],
|
|
70
|
-
children: [
|
|
71
|
-
{
|
|
72
|
-
_type: 'span',
|
|
73
|
-
_key: 'a1',
|
|
74
|
-
text: 'This is the first block',
|
|
75
|
-
marks: [],
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
_key: 'b',
|
|
81
|
-
_type: 'someObject',
|
|
82
|
-
},
|
|
83
|
-
]
|
|
84
|
-
const initialSelection = {
|
|
85
|
-
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2},
|
|
86
|
-
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2},
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
90
|
-
const onChange = jest.fn()
|
|
91
|
-
const component = render(
|
|
92
|
-
<PortableTextEditorTester
|
|
93
|
-
onChange={onChange}
|
|
94
|
-
ref={editorRef}
|
|
95
|
-
schemaType={schemaType}
|
|
96
|
-
value={initialValue}
|
|
97
|
-
/>,
|
|
98
|
-
)
|
|
99
|
-
const element = await getEditableElement(component)
|
|
100
|
-
|
|
101
|
-
const editor = editorRef.current
|
|
102
|
-
const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
|
|
103
|
-
await waitFor(async () => {
|
|
104
|
-
if (editor && inlineType && element) {
|
|
105
|
-
PortableTextEditor.focus(editor)
|
|
106
|
-
PortableTextEditor.select(editor, initialSelection)
|
|
107
|
-
await triggerKeyboardEvent('ArrowDown', element)
|
|
108
|
-
const value = PortableTextEditor.getValue(editor)
|
|
109
|
-
// Arrow down on the text block should not add a new block
|
|
110
|
-
expect(value).toEqual(initialValue)
|
|
111
|
-
// Focus on the object block
|
|
112
|
-
PortableTextEditor.select(editor, {
|
|
113
|
-
focus: {path: [{_key: 'b'}], offset: 0},
|
|
114
|
-
anchor: {path: [{_key: 'b'}], offset: 0},
|
|
115
|
-
})
|
|
116
|
-
await triggerKeyboardEvent('ArrowDown', element)
|
|
117
|
-
const value2 = PortableTextEditor.getValue(editor)
|
|
118
|
-
expect(value2).toEqual([
|
|
119
|
-
initialValue[0],
|
|
120
|
-
initialValue[1],
|
|
121
|
-
{
|
|
122
|
-
_type: 'myTestBlockType',
|
|
123
|
-
_key: '3',
|
|
124
|
-
style: 'normal',
|
|
125
|
-
markDefs: [],
|
|
126
|
-
children: [
|
|
127
|
-
{
|
|
128
|
-
_type: 'span',
|
|
129
|
-
_key: '2',
|
|
130
|
-
text: '',
|
|
131
|
-
marks: [],
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
])
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
describe('plugin:withHotkeys: .ArrowUp', () => {
|
|
141
|
-
it('a new block is added at the top, when pressing arrow up, because first block is void, the new block can be deleted with backspace.', async () => {
|
|
142
|
-
const initialValue = [
|
|
143
|
-
{
|
|
144
|
-
_key: 'b',
|
|
145
|
-
_type: 'someObject',
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
_type: 'myTestBlockType',
|
|
149
|
-
_key: 'a',
|
|
150
|
-
style: 'normal',
|
|
151
|
-
markDefs: [],
|
|
152
|
-
children: [
|
|
153
|
-
{
|
|
154
|
-
_type: 'span',
|
|
155
|
-
_key: 'a1',
|
|
156
|
-
text: 'This is the first block',
|
|
157
|
-
marks: [],
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
const initialSelection = {
|
|
164
|
-
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2},
|
|
165
|
-
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2},
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const editorRef: RefObject<PortableTextEditor> = createRef()
|
|
169
|
-
const onChange = jest.fn()
|
|
170
|
-
const component = render(
|
|
171
|
-
<PortableTextEditorTester
|
|
172
|
-
onChange={onChange}
|
|
173
|
-
ref={editorRef}
|
|
174
|
-
schemaType={schemaType}
|
|
175
|
-
value={initialValue}
|
|
176
|
-
/>,
|
|
177
|
-
)
|
|
178
|
-
const element = await getEditableElement(component)
|
|
179
|
-
|
|
180
|
-
const editor = editorRef.current
|
|
181
|
-
const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
|
|
182
|
-
await waitFor(async () => {
|
|
183
|
-
if (editor && inlineType && element) {
|
|
184
|
-
PortableTextEditor.focus(editor)
|
|
185
|
-
PortableTextEditor.select(editor, initialSelection)
|
|
186
|
-
await triggerKeyboardEvent('ArrowUp', element)
|
|
187
|
-
// Arrow down on the text block should not add a new block
|
|
188
|
-
expect(PortableTextEditor.getValue(editor)).toEqual(initialValue)
|
|
189
|
-
// Focus on the object block
|
|
190
|
-
PortableTextEditor.select(editor, {
|
|
191
|
-
focus: {path: [{_key: 'b'}], offset: 0},
|
|
192
|
-
anchor: {path: [{_key: 'b'}], offset: 0},
|
|
193
|
-
})
|
|
194
|
-
await triggerKeyboardEvent('ArrowUp', element)
|
|
195
|
-
expect(PortableTextEditor.getValue(editor)).toEqual([
|
|
196
|
-
newBlock,
|
|
197
|
-
initialValue[0],
|
|
198
|
-
initialValue[1],
|
|
199
|
-
])
|
|
200
|
-
// Pressing arrow up again won't add a new block
|
|
201
|
-
await triggerKeyboardEvent('ArrowUp', element)
|
|
202
|
-
expect(PortableTextEditor.getValue(editor)).toEqual([
|
|
203
|
-
newBlock,
|
|
204
|
-
initialValue[0],
|
|
205
|
-
initialValue[1],
|
|
206
|
-
])
|
|
207
|
-
await triggerKeyboardEvent('Backspace', element)
|
|
208
|
-
expect(PortableTextEditor.getValue(editor)).toEqual(initialValue)
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
})
|