@portabletext/editor 1.11.2 → 1.12.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 +11 -0
- package/lib/index.d.mts +26 -7
- package/lib/index.d.ts +26 -7
- package/lib/index.esm.js +317 -134
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +316 -133
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +317 -134
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/editor/behavior/behavior.action-utils.insert-block.ts +61 -0
- package/src/editor/behavior/behavior.action.insert-block-object.ts +25 -0
- package/src/editor/behavior/behavior.actions.ts +88 -32
- package/src/editor/behavior/behavior.core.block-objects.ts +5 -11
- package/src/editor/behavior/behavior.markdown.ts +149 -62
- package/src/editor/behavior/behavior.types.ts +22 -6
- package/src/editor/behavior/behavior.utils.block-offset.test.ts +143 -0
- package/src/editor/behavior/behavior.utils.block-offset.ts +101 -0
- package/src/editor/behavior/behavior.utils.ts +13 -2
- package/src/editor/plugins/createWithEditableAPI.ts +22 -87
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"is-hotkey-esm": "^1.0.0",
|
|
50
50
|
"lodash": "^4.17.21",
|
|
51
51
|
"lodash.startcase": "^4.4.0",
|
|
52
|
-
"react-compiler-runtime": "19.0.0-beta-
|
|
52
|
+
"react-compiler-runtime": "19.0.0-beta-df7b47d-20241124",
|
|
53
53
|
"slate": "0.110.2",
|
|
54
54
|
"slate-dom": "^0.111.0",
|
|
55
55
|
"slate-react": "0.111.0",
|
|
@@ -74,11 +74,12 @@
|
|
|
74
74
|
"@typescript-eslint/parser": "^8.15.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.3.3",
|
|
76
76
|
"@vitest/browser": "^2.1.5",
|
|
77
|
-
"babel-plugin-react-compiler": "19.0.0-beta-
|
|
77
|
+
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
|
78
78
|
"eslint": "8.57.1",
|
|
79
|
-
"eslint-plugin-react-compiler": "19.0.0-beta-
|
|
79
|
+
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
|
80
80
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
81
81
|
"jsdom": "^25.0.1",
|
|
82
|
+
"racejar": "1.0.0",
|
|
82
83
|
"react": "^18.3.1",
|
|
83
84
|
"react-dom": "^18.3.1",
|
|
84
85
|
"rxjs": "^7.8.1",
|
|
@@ -86,8 +87,7 @@
|
|
|
86
87
|
"typescript": "5.6.3",
|
|
87
88
|
"vite": "^5.4.11",
|
|
88
89
|
"vitest": "^2.1.5",
|
|
89
|
-
"vitest-browser-react": "^0.0.3"
|
|
90
|
-
"@sanity/gherkin-driver": "^0.0.1"
|
|
90
|
+
"vitest-browser-react": "^0.0.3"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"@sanity/block-tools": "^3.64.3",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {Editor, Transforms, type Descendant} from 'slate'
|
|
2
|
+
import type {
|
|
3
|
+
PortableTextMemberSchemaTypes,
|
|
4
|
+
PortableTextSlateEditor,
|
|
5
|
+
} from '../../types/editor'
|
|
6
|
+
import {isEqualToEmptyEditor} from '../../utils/values'
|
|
7
|
+
|
|
8
|
+
export function insertBlock({
|
|
9
|
+
block,
|
|
10
|
+
placement,
|
|
11
|
+
editor,
|
|
12
|
+
schema,
|
|
13
|
+
}: {
|
|
14
|
+
block: Descendant
|
|
15
|
+
placement: 'auto' | 'after'
|
|
16
|
+
editor: PortableTextSlateEditor
|
|
17
|
+
schema: PortableTextMemberSchemaTypes
|
|
18
|
+
}) {
|
|
19
|
+
if (!editor.selection) {
|
|
20
|
+
const lastBlock = Array.from(
|
|
21
|
+
Editor.nodes(editor, {
|
|
22
|
+
match: (n) => !Editor.isEditor(n),
|
|
23
|
+
at: [],
|
|
24
|
+
reverse: true,
|
|
25
|
+
}),
|
|
26
|
+
)[0]
|
|
27
|
+
|
|
28
|
+
// If there is no selection, let's just insert the new block at the
|
|
29
|
+
// end of the document
|
|
30
|
+
Editor.insertNode(editor, block)
|
|
31
|
+
|
|
32
|
+
if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], schema)) {
|
|
33
|
+
// And if the last block was an empty text block, let's remove
|
|
34
|
+
// that too
|
|
35
|
+
Transforms.removeNodes(editor, {at: lastBlock[1]})
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const [focusBlock, focusBlockPath] = Array.from(
|
|
39
|
+
Editor.nodes(editor, {
|
|
40
|
+
at: editor.selection.focus.path.slice(0, 1),
|
|
41
|
+
match: (n) => !Editor.isEditor(n),
|
|
42
|
+
}),
|
|
43
|
+
)[0] ?? [undefined, undefined]
|
|
44
|
+
|
|
45
|
+
if (placement === 'after') {
|
|
46
|
+
const nextPath = [focusBlockPath[0] + 1]
|
|
47
|
+
|
|
48
|
+
Transforms.insertNodes(editor, block, {at: nextPath})
|
|
49
|
+
Transforms.select(editor, {
|
|
50
|
+
anchor: {path: [nextPath[0], 0], offset: 0},
|
|
51
|
+
focus: {path: [nextPath[0], 0], offset: 0},
|
|
52
|
+
})
|
|
53
|
+
} else {
|
|
54
|
+
Editor.insertNode(editor, block)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (focusBlock && isEqualToEmptyEditor([focusBlock], schema)) {
|
|
58
|
+
Transforms.removeNodes(editor, {at: focusBlockPath})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {toSlateValue} from '../../utils/values'
|
|
2
|
+
import {insertBlock} from './behavior.action-utils.insert-block'
|
|
3
|
+
import type {BehaviorActionImplementation} from './behavior.actions'
|
|
4
|
+
|
|
5
|
+
export const insertBlockObjectActionImplementation: BehaviorActionImplementation<
|
|
6
|
+
'insert block object'
|
|
7
|
+
> = ({context, action}) => {
|
|
8
|
+
const block = toSlateValue(
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
_key: context.keyGenerator(),
|
|
12
|
+
_type: action.blockObject.name,
|
|
13
|
+
...(action.blockObject.value ? action.blockObject.value : {}),
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
{schemaTypes: context.schema},
|
|
17
|
+
)[0]
|
|
18
|
+
|
|
19
|
+
insertBlock({
|
|
20
|
+
block,
|
|
21
|
+
placement: action.placement,
|
|
22
|
+
editor: action.editor,
|
|
23
|
+
schema: context.schema,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deleteBackward,
|
|
3
|
-
deleteForward,
|
|
4
|
-
Editor,
|
|
5
|
-
insertText,
|
|
6
|
-
Transforms,
|
|
7
|
-
} from 'slate'
|
|
1
|
+
import {deleteBackward, deleteForward, insertText, Transforms} from 'slate'
|
|
8
2
|
import {ReactEditor} from 'slate-react'
|
|
9
3
|
import type {PortableTextMemberSchemaTypes} from '../../types/editor'
|
|
10
4
|
import {toSlateRange} from '../../utils/ranges'
|
|
5
|
+
import {fromSlateValue, toSlateValue} from '../../utils/values'
|
|
6
|
+
import {KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
|
|
11
7
|
import {
|
|
12
8
|
addAnnotationActionImplementation,
|
|
13
|
-
insertBlockObjectActionImplementation,
|
|
14
9
|
removeAnnotationActionImplementation,
|
|
15
10
|
toggleAnnotationActionImplementation,
|
|
16
11
|
} from '../plugins/createWithEditableAPI'
|
|
@@ -19,6 +14,8 @@ import {
|
|
|
19
14
|
removeDecoratorActionImplementation,
|
|
20
15
|
toggleDecoratorActionImplementation,
|
|
21
16
|
} from '../plugins/createWithPortableTextMarkModel'
|
|
17
|
+
import {insertBlock} from './behavior.action-utils.insert-block'
|
|
18
|
+
import {insertBlockObjectActionImplementation} from './behavior.action.insert-block-object'
|
|
22
19
|
import {
|
|
23
20
|
insertBreakActionImplementation,
|
|
24
21
|
insertSoftBreakActionImplementation,
|
|
@@ -29,6 +26,7 @@ import type {
|
|
|
29
26
|
BehaviorEvent,
|
|
30
27
|
PickFromUnion,
|
|
31
28
|
} from './behavior.types'
|
|
29
|
+
import {blockOffsetToSpanSelectionPoint} from './behavior.utils.block-offset'
|
|
32
30
|
|
|
33
31
|
export type BehaviorActionContext = {
|
|
34
32
|
keyGenerator: () => string
|
|
@@ -94,25 +92,61 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
|
|
|
94
92
|
'delete forward': ({action}) => {
|
|
95
93
|
deleteForward(action.editor, action.unit)
|
|
96
94
|
},
|
|
97
|
-
'delete': ({action}) => {
|
|
98
|
-
const
|
|
95
|
+
'delete block': ({action}) => {
|
|
96
|
+
const range = toSlateRange(
|
|
97
|
+
{
|
|
98
|
+
anchor: {path: action.blockPath, offset: 0},
|
|
99
|
+
focus: {path: action.blockPath, offset: 0},
|
|
100
|
+
},
|
|
101
|
+
action.editor,
|
|
102
|
+
)
|
|
99
103
|
|
|
100
|
-
if (!
|
|
101
|
-
console.error(
|
|
102
|
-
`Could not find Slate location from selection ${action.selection}`,
|
|
103
|
-
)
|
|
104
|
+
if (!range) {
|
|
105
|
+
console.error('Unable to find Slate range from selection points')
|
|
104
106
|
return
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
Transforms.removeNodes(action.editor, {
|
|
110
|
+
at: range,
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
'delete text': ({context, action}) => {
|
|
114
|
+
const value = fromSlateValue(
|
|
115
|
+
action.editor.children,
|
|
116
|
+
context.schema.block.name,
|
|
117
|
+
KEY_TO_VALUE_ELEMENT.get(action.editor),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const anchor = blockOffsetToSpanSelectionPoint({
|
|
121
|
+
value,
|
|
122
|
+
blockOffset: action.anchor,
|
|
123
|
+
})
|
|
124
|
+
const focus = blockOffsetToSpanSelectionPoint({
|
|
125
|
+
value,
|
|
126
|
+
blockOffset: action.focus,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if (!anchor || !focus) {
|
|
130
|
+
console.error('Unable to find anchor or focus selection point')
|
|
131
|
+
return
|
|
115
132
|
}
|
|
133
|
+
|
|
134
|
+
const range = toSlateRange(
|
|
135
|
+
{
|
|
136
|
+
anchor,
|
|
137
|
+
focus,
|
|
138
|
+
},
|
|
139
|
+
action.editor,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (!range) {
|
|
143
|
+
console.error('Unable to find Slate range from selection points')
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Transforms.delete(action.editor, {
|
|
148
|
+
at: range,
|
|
149
|
+
})
|
|
116
150
|
},
|
|
117
151
|
'insert block object': insertBlockObjectActionImplementation,
|
|
118
152
|
'insert break': insertBreakActionImplementation,
|
|
@@ -122,18 +156,33 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
|
|
|
122
156
|
insertText(action.editor, action.text)
|
|
123
157
|
},
|
|
124
158
|
'insert text block': ({context, action}) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
_type: context.schema.block.name,
|
|
128
|
-
style: context.schema.styles[0].value ?? 'normal',
|
|
129
|
-
markDefs: [],
|
|
130
|
-
children: [
|
|
159
|
+
const block = toSlateValue(
|
|
160
|
+
[
|
|
131
161
|
{
|
|
132
162
|
_key: context.keyGenerator(),
|
|
133
|
-
_type:
|
|
134
|
-
|
|
163
|
+
_type: context.schema.block.name,
|
|
164
|
+
style: context.schema.styles[0].value ?? 'normal',
|
|
165
|
+
markDefs: [],
|
|
166
|
+
children: action.textBlock?.children?.map((child) => ({
|
|
167
|
+
...child,
|
|
168
|
+
_key: context.keyGenerator(),
|
|
169
|
+
})) ?? [
|
|
170
|
+
{
|
|
171
|
+
_type: context.schema.span.name,
|
|
172
|
+
_key: context.keyGenerator(),
|
|
173
|
+
text: '',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
135
176
|
},
|
|
136
177
|
],
|
|
178
|
+
{schemaTypes: context.schema},
|
|
179
|
+
)[0]
|
|
180
|
+
|
|
181
|
+
insertBlock({
|
|
182
|
+
block,
|
|
183
|
+
editor: action.editor,
|
|
184
|
+
schema: context.schema,
|
|
185
|
+
placement: action.placement,
|
|
137
186
|
})
|
|
138
187
|
},
|
|
139
188
|
'effect': ({action}) => {
|
|
@@ -169,8 +218,15 @@ export function performAction({
|
|
|
169
218
|
action: BehaviorAction
|
|
170
219
|
}) {
|
|
171
220
|
switch (action.type) {
|
|
172
|
-
case 'delete': {
|
|
173
|
-
behaviorActionImplementations
|
|
221
|
+
case 'delete block': {
|
|
222
|
+
behaviorActionImplementations['delete block']({
|
|
223
|
+
context,
|
|
224
|
+
action,
|
|
225
|
+
})
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
case 'delete text': {
|
|
229
|
+
behaviorActionImplementations['delete text']({
|
|
174
230
|
context,
|
|
175
231
|
action,
|
|
176
232
|
})
|
|
@@ -16,7 +16,7 @@ const breakingBlockObject = defineBehavior({
|
|
|
16
16
|
|
|
17
17
|
return !!focusBlockObject
|
|
18
18
|
},
|
|
19
|
-
actions: [() => [{type: 'insert text block',
|
|
19
|
+
actions: [() => [{type: 'insert text block', placement: 'after'}]],
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
const deletingEmptyTextBlockAfterBlockObject = defineBehavior({
|
|
@@ -42,11 +42,8 @@ const deletingEmptyTextBlockAfterBlockObject = defineBehavior({
|
|
|
42
42
|
actions: [
|
|
43
43
|
(_, {focusTextBlock, previousBlock}) => [
|
|
44
44
|
{
|
|
45
|
-
type: 'delete',
|
|
46
|
-
|
|
47
|
-
anchor: {path: focusTextBlock.path, offset: 0},
|
|
48
|
-
focus: {path: focusTextBlock.path, offset: 0},
|
|
49
|
-
},
|
|
45
|
+
type: 'delete block',
|
|
46
|
+
blockPath: focusTextBlock.path,
|
|
50
47
|
},
|
|
51
48
|
{
|
|
52
49
|
type: 'select',
|
|
@@ -82,11 +79,8 @@ const deletingEmptyTextBlockBeforeBlockObject = defineBehavior({
|
|
|
82
79
|
actions: [
|
|
83
80
|
(_, {focusTextBlock, nextBlock}) => [
|
|
84
81
|
{
|
|
85
|
-
type: 'delete',
|
|
86
|
-
|
|
87
|
-
anchor: {path: focusTextBlock.path, offset: 0},
|
|
88
|
-
focus: {path: focusTextBlock.path, offset: 0},
|
|
89
|
-
},
|
|
82
|
+
type: 'delete block',
|
|
83
|
+
blockPath: focusTextBlock.path,
|
|
90
84
|
},
|
|
91
85
|
{
|
|
92
86
|
type: 'select',
|