@portabletext/editor 1.5.4 → 1.5.5
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 +76 -18
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +76 -18
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +76 -18
- package/lib/index.mjs.map +1 -1
- package/package.json +12 -36
- package/src/editor/behavior/behavior.markdown.ts +129 -27
- package/src/editor/plugins/createWithUndoRedo.ts +22 -0
- package/src/utils/ucs2Indices.ts +0 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -56,55 +56,33 @@
|
|
|
56
56
|
"xstate": "^5.18.2"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@babel/preset-env": "^7.26.0",
|
|
60
|
-
"@babel/preset-react": "^7.25.9",
|
|
61
|
-
"@jest/globals": "^29.7.0",
|
|
62
|
-
"@jest/types": "^29.6.3",
|
|
63
|
-
"@playwright/test": "1.48.2",
|
|
64
59
|
"@portabletext/toolkit": "^2.0.16",
|
|
65
|
-
"@sanity/block-tools": "^3.
|
|
60
|
+
"@sanity/block-tools": "^3.63.0",
|
|
66
61
|
"@sanity/diff-match-patch": "^3.1.1",
|
|
67
62
|
"@sanity/pkg-utils": "^6.11.8",
|
|
68
|
-
"@sanity/schema": "^3.
|
|
69
|
-
"@sanity/types": "^3.
|
|
70
|
-
"@sanity/
|
|
71
|
-
"@sanity/util": "^3.62.3",
|
|
72
|
-
"@testing-library/dom": "^10.4.0",
|
|
63
|
+
"@sanity/schema": "^3.63.0",
|
|
64
|
+
"@sanity/types": "^3.63.0",
|
|
65
|
+
"@sanity/util": "^3.63.0",
|
|
73
66
|
"@testing-library/jest-dom": "^6.6.3",
|
|
74
67
|
"@testing-library/react": "^16.0.1",
|
|
75
|
-
"@testing-library/user-event": "^14.5.2",
|
|
76
68
|
"@types/debug": "^4.1.5",
|
|
77
|
-
"@types/express": "^4.17.21",
|
|
78
|
-
"@types/express-ws": "^3.0.5",
|
|
79
69
|
"@types/lodash": "^4.17.13",
|
|
80
70
|
"@types/lodash.startcase": "^4.4.9",
|
|
81
|
-
"@types/node": "^18.19.8",
|
|
82
|
-
"@types/node-ipc": "^9.2.3",
|
|
83
71
|
"@types/react": "^18.3.12",
|
|
84
72
|
"@types/react-dom": "^18.3.1",
|
|
85
|
-
"@
|
|
86
|
-
"@typescript-eslint/
|
|
87
|
-
"@typescript-eslint/parser": "^8.12.2",
|
|
73
|
+
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
|
74
|
+
"@typescript-eslint/parser": "^8.13.0",
|
|
88
75
|
"@vitejs/plugin-react": "^4.3.3",
|
|
89
76
|
"@vitest/browser": "^2.1.4",
|
|
90
77
|
"babel-plugin-react-compiler": "beta",
|
|
91
|
-
"dotenv": "^16.4.5",
|
|
92
78
|
"eslint": "8.57.1",
|
|
93
79
|
"eslint-plugin-react-compiler": "beta",
|
|
94
80
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
95
|
-
"express": "^4.21.1",
|
|
96
|
-
"express-ws": "^5.0.2",
|
|
97
|
-
"jest": "^29.7.0",
|
|
98
|
-
"jest-dev-server": "^10.1.4",
|
|
99
|
-
"jest-environment-node": "^29.7.0",
|
|
100
81
|
"jsdom": "^25.0.1",
|
|
101
|
-
"node-ipc": "npm:@node-ipc/compat@9.2.5",
|
|
102
|
-
"playwright": "^1.48.2",
|
|
103
82
|
"react": "^18.3.1",
|
|
104
83
|
"react-dom": "^18.3.1",
|
|
105
84
|
"rxjs": "^7.8.1",
|
|
106
85
|
"styled-components": "^6.1.13",
|
|
107
|
-
"ts-node": "^10.9.2",
|
|
108
86
|
"typescript": "5.6.3",
|
|
109
87
|
"vite": "^5.4.10",
|
|
110
88
|
"vitest": "^2.1.4",
|
|
@@ -112,10 +90,10 @@
|
|
|
112
90
|
"@sanity/gherkin-driver": "^0.0.1"
|
|
113
91
|
},
|
|
114
92
|
"peerDependencies": {
|
|
115
|
-
"@sanity/block-tools": "^3.
|
|
116
|
-
"@sanity/schema": "^3.
|
|
117
|
-
"@sanity/types": "^3.
|
|
118
|
-
"@sanity/util": "^3.
|
|
93
|
+
"@sanity/block-tools": "^3.63.0",
|
|
94
|
+
"@sanity/schema": "^3.63.0",
|
|
95
|
+
"@sanity/types": "^3.63.0",
|
|
96
|
+
"@sanity/util": "^3.63.0",
|
|
119
97
|
"react": "^16.9 || ^17 || ^18",
|
|
120
98
|
"rxjs": "^7.8.1",
|
|
121
99
|
"styled-components": "^6.1.13"
|
|
@@ -135,8 +113,6 @@
|
|
|
135
113
|
"dev": "pkg-utils watch",
|
|
136
114
|
"lint:fix": "biome lint --write .",
|
|
137
115
|
"test": "vitest --run",
|
|
138
|
-
"test:watch": "vitest"
|
|
139
|
-
"test:e2e-legacy": "jest --config=e2e-tests/e2e.config.ts",
|
|
140
|
-
"test:e2e-legacy:watch": "jest --config=e2e-tests/e2e.config.ts --watch"
|
|
116
|
+
"test:watch": "vitest"
|
|
141
117
|
}
|
|
142
118
|
}
|
|
@@ -30,7 +30,7 @@ export type MarkdownBehaviorsConfig = {
|
|
|
30
30
|
* @alpha
|
|
31
31
|
*/
|
|
32
32
|
export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
33
|
-
const
|
|
33
|
+
const automaticBlockquoteOnSpace = defineBehavior({
|
|
34
34
|
on: 'insert text',
|
|
35
35
|
guard: ({context, event}) => {
|
|
36
36
|
const isSpace = event.text === ' '
|
|
@@ -47,20 +47,15 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
47
47
|
return false
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
const headingStyle = config.mapHeadingStyle(
|
|
52
|
-
context.schema,
|
|
53
|
-
focusSpan.node.text.length,
|
|
54
|
-
)
|
|
55
|
-
|
|
50
|
+
const caretAtTheEndOfQuote = context.selection.focus.offset === 1
|
|
56
51
|
const looksLikeMarkdownQuote = /^>/.test(focusSpan.node.text)
|
|
57
52
|
const blockquoteStyle = config.mapBlockquoteStyle(context.schema)
|
|
58
53
|
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
if (
|
|
55
|
+
caretAtTheEndOfQuote &&
|
|
56
|
+
looksLikeMarkdownQuote &&
|
|
57
|
+
blockquoteStyle !== undefined
|
|
58
|
+
) {
|
|
64
59
|
return {focusTextBlock, focusSpan, style: blockquoteStyle}
|
|
65
60
|
}
|
|
66
61
|
|
|
@@ -87,10 +82,91 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
87
82
|
{
|
|
88
83
|
type: 'delete',
|
|
89
84
|
selection: {
|
|
90
|
-
anchor: {
|
|
85
|
+
anchor: {
|
|
86
|
+
path: focusSpan.path,
|
|
87
|
+
offset: 0,
|
|
88
|
+
},
|
|
89
|
+
focus: {
|
|
90
|
+
path: focusSpan.path,
|
|
91
|
+
offset: 2,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
const automaticHeadingOnSpace = defineBehavior({
|
|
99
|
+
on: 'insert text',
|
|
100
|
+
guard: ({context, event}) => {
|
|
101
|
+
const isSpace = event.text === ' '
|
|
102
|
+
|
|
103
|
+
if (!isSpace) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const selectionCollapsed = selectionIsCollapsed(context)
|
|
108
|
+
const focusTextBlock = getFocusTextBlock(context)
|
|
109
|
+
const focusSpan = getFocusSpan(context)
|
|
110
|
+
|
|
111
|
+
if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const markdownHeadingSearch = /^#+/.exec(focusSpan.node.text)
|
|
116
|
+
const headingLevel = markdownHeadingSearch
|
|
117
|
+
? markdownHeadingSearch[0].length
|
|
118
|
+
: undefined
|
|
119
|
+
const caretAtTheEndOfHeading =
|
|
120
|
+
context.selection.focus.offset === headingLevel
|
|
121
|
+
|
|
122
|
+
if (!caretAtTheEndOfHeading) {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const headingStyle =
|
|
127
|
+
headingLevel !== undefined
|
|
128
|
+
? config.mapHeadingStyle(context.schema, headingLevel)
|
|
129
|
+
: undefined
|
|
130
|
+
|
|
131
|
+
if (headingLevel !== undefined && headingStyle !== undefined) {
|
|
132
|
+
return {
|
|
133
|
+
focusTextBlock,
|
|
134
|
+
focusSpan,
|
|
135
|
+
style: headingStyle,
|
|
136
|
+
level: headingLevel,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false
|
|
141
|
+
},
|
|
142
|
+
actions: [
|
|
143
|
+
() => [
|
|
144
|
+
{
|
|
145
|
+
type: 'insert text',
|
|
146
|
+
text: ' ',
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
(_, {focusTextBlock, focusSpan, style, level}) => [
|
|
150
|
+
{
|
|
151
|
+
type: 'unset block',
|
|
152
|
+
props: ['listItem', 'level'],
|
|
153
|
+
paths: [focusTextBlock.path],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'set block',
|
|
157
|
+
style,
|
|
158
|
+
paths: [focusTextBlock.path],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: 'delete',
|
|
162
|
+
selection: {
|
|
163
|
+
anchor: {
|
|
164
|
+
path: focusSpan.path,
|
|
165
|
+
offset: 0,
|
|
166
|
+
},
|
|
91
167
|
focus: {
|
|
92
168
|
path: focusSpan.path,
|
|
93
|
-
offset:
|
|
169
|
+
offset: level + 1,
|
|
94
170
|
},
|
|
95
171
|
},
|
|
96
172
|
},
|
|
@@ -108,13 +184,16 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
108
184
|
return false
|
|
109
185
|
}
|
|
110
186
|
|
|
187
|
+
const atTheBeginningOfBLock =
|
|
188
|
+
focusTextBlock.node.children[0]._key === focusSpan.node._key &&
|
|
189
|
+
context.selection.focus.offset === 0
|
|
190
|
+
|
|
111
191
|
const defaultStyle = config.mapDefaultStyle(context.schema)
|
|
112
192
|
|
|
113
193
|
if (
|
|
194
|
+
atTheBeginningOfBLock &&
|
|
114
195
|
defaultStyle &&
|
|
115
|
-
focusTextBlock.node.
|
|
116
|
-
focusTextBlock.node.style !== config.mapDefaultStyle(context.schema) &&
|
|
117
|
-
focusSpan.node.text === ''
|
|
196
|
+
focusTextBlock.node.style !== defaultStyle
|
|
118
197
|
) {
|
|
119
198
|
return {defaultStyle, focusTextBlock}
|
|
120
199
|
}
|
|
@@ -131,7 +210,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
131
210
|
],
|
|
132
211
|
],
|
|
133
212
|
})
|
|
134
|
-
|
|
135
213
|
const automaticListOnSpace = defineBehavior({
|
|
136
214
|
on: 'insert text',
|
|
137
215
|
guard: ({context, event}) => {
|
|
@@ -149,18 +227,38 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
149
227
|
return false
|
|
150
228
|
}
|
|
151
229
|
|
|
152
|
-
const looksLikeUnorderedList =
|
|
230
|
+
const looksLikeUnorderedList = /^(-|\*)/.test(focusSpan.node.text)
|
|
153
231
|
const unorderedListStyle = config.mapUnorderedListStyle(context.schema)
|
|
232
|
+
const caretAtTheEndOfUnorderedList = context.selection.focus.offset === 1
|
|
154
233
|
|
|
155
|
-
if (
|
|
156
|
-
|
|
234
|
+
if (
|
|
235
|
+
caretAtTheEndOfUnorderedList &&
|
|
236
|
+
looksLikeUnorderedList &&
|
|
237
|
+
unorderedListStyle !== undefined
|
|
238
|
+
) {
|
|
239
|
+
return {
|
|
240
|
+
focusTextBlock,
|
|
241
|
+
focusSpan,
|
|
242
|
+
listItem: unorderedListStyle,
|
|
243
|
+
listItemLength: 1,
|
|
244
|
+
}
|
|
157
245
|
}
|
|
158
246
|
|
|
159
247
|
const looksLikeOrderedList = /^1./.test(focusSpan.node.text)
|
|
160
248
|
const orderedListStyle = config.mapOrderedListStyle(context.schema)
|
|
249
|
+
const caretAtTheEndOfOrderedList = context.selection.focus.offset === 2
|
|
161
250
|
|
|
162
|
-
if (
|
|
163
|
-
|
|
251
|
+
if (
|
|
252
|
+
caretAtTheEndOfOrderedList &&
|
|
253
|
+
looksLikeOrderedList &&
|
|
254
|
+
orderedListStyle !== undefined
|
|
255
|
+
) {
|
|
256
|
+
return {
|
|
257
|
+
focusTextBlock,
|
|
258
|
+
focusSpan,
|
|
259
|
+
listItem: orderedListStyle,
|
|
260
|
+
listItemLength: 2,
|
|
261
|
+
}
|
|
164
262
|
}
|
|
165
263
|
|
|
166
264
|
return false
|
|
@@ -172,7 +270,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
172
270
|
text: ' ',
|
|
173
271
|
},
|
|
174
272
|
],
|
|
175
|
-
(_, {focusTextBlock, focusSpan, listItem}) => [
|
|
273
|
+
(_, {focusTextBlock, focusSpan, listItem, listItemLength}) => [
|
|
176
274
|
{
|
|
177
275
|
type: 'unset block',
|
|
178
276
|
props: ['style'],
|
|
@@ -187,10 +285,13 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
187
285
|
{
|
|
188
286
|
type: 'delete',
|
|
189
287
|
selection: {
|
|
190
|
-
anchor: {
|
|
288
|
+
anchor: {
|
|
289
|
+
path: focusSpan.path,
|
|
290
|
+
offset: 0,
|
|
291
|
+
},
|
|
191
292
|
focus: {
|
|
192
293
|
path: focusSpan.path,
|
|
193
|
-
offset:
|
|
294
|
+
offset: listItemLength + 1,
|
|
194
295
|
},
|
|
195
296
|
},
|
|
196
297
|
},
|
|
@@ -199,7 +300,8 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
199
300
|
})
|
|
200
301
|
|
|
201
302
|
const markdownBehaviors = [
|
|
202
|
-
|
|
303
|
+
automaticBlockquoteOnSpace,
|
|
304
|
+
automaticHeadingOnSpace,
|
|
203
305
|
clearStyleOnBackspace,
|
|
204
306
|
automaticListOnSpace,
|
|
205
307
|
]
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
import type {PortableTextSlateEditor} from '../../types/editor'
|
|
24
24
|
import {debugWithName} from '../../utils/debug'
|
|
25
25
|
import {fromSlateValue} from '../../utils/values'
|
|
26
|
+
import {isChangingRemotely} from '../../utils/withChanges'
|
|
26
27
|
import {
|
|
28
|
+
isRedoing,
|
|
29
|
+
isUndoing,
|
|
27
30
|
setIsRedoing,
|
|
28
31
|
setIsUndoing,
|
|
29
32
|
withRedoing,
|
|
@@ -115,6 +118,25 @@ export function createWithUndoRedo(
|
|
|
115
118
|
apply(op)
|
|
116
119
|
return
|
|
117
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* We don't want to run any side effects when the editor is processing
|
|
124
|
+
* remote changes.
|
|
125
|
+
*/
|
|
126
|
+
if (isChangingRemotely(editor)) {
|
|
127
|
+
apply(op)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* We don't want to run any side effects when the editor is undoing or
|
|
133
|
+
* redoing operations.
|
|
134
|
+
*/
|
|
135
|
+
if (isUndoing(editor) || isRedoing(editor)) {
|
|
136
|
+
apply(op)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
118
140
|
const {operations, history} = editor
|
|
119
141
|
const {undos} = history
|
|
120
142
|
const step = undos[undos.length - 1]
|
package/src/utils/ucs2Indices.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type {Patch} from '@sanity/diff-match-patch'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Takes a `patches` array as produced by diff-match-patch and adjusts the
|
|
5
|
-
* `start1` and `start2` properties so that they refer to UCS-2 index instead
|
|
6
|
-
* of a UTF-8 index.
|
|
7
|
-
*
|
|
8
|
-
* @param patches - The patches to adjust
|
|
9
|
-
* @param base - The base string to use for counting bytes
|
|
10
|
-
* @returns A new array of patches with adjusted indicies
|
|
11
|
-
* @beta
|
|
12
|
-
*/
|
|
13
|
-
export function adjustIndiciesToUcs2(patches: Patch[], base: string): Patch[] {
|
|
14
|
-
let byteOffset = 0
|
|
15
|
-
let idx = 0 // index into the input.
|
|
16
|
-
|
|
17
|
-
function advanceTo(target: number) {
|
|
18
|
-
while (byteOffset < target) {
|
|
19
|
-
const codePoint = base.codePointAt(idx)
|
|
20
|
-
if (typeof codePoint === 'undefined') {
|
|
21
|
-
// Reached the end of the base string - the indicies won't be correct,
|
|
22
|
-
// but we also cannot advance any further to find a closer index.
|
|
23
|
-
return idx
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
byteOffset += utf8len(codePoint)
|
|
27
|
-
|
|
28
|
-
// This is encoded as a surrogate pair.
|
|
29
|
-
if (codePoint > 0xffff) {
|
|
30
|
-
idx += 2
|
|
31
|
-
} else {
|
|
32
|
-
idx += 1
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Theoretically, we should have reached target - however, due to differences in
|
|
37
|
-
// `base` from the string that the patch was originally based upon, occurences
|
|
38
|
-
// _can_ happen where we go beyond the target due to surrogate pairs or similar.
|
|
39
|
-
// In the PTE, this is okayish - best effort matching is good enough.
|
|
40
|
-
return idx
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const adjusted: Patch[] = []
|
|
44
|
-
for (const patch of patches) {
|
|
45
|
-
adjusted.push({
|
|
46
|
-
diffs: patch.diffs.map((diff) => [...diff]),
|
|
47
|
-
start1: advanceTo(patch.start1),
|
|
48
|
-
start2: advanceTo(patch.start2),
|
|
49
|
-
utf8Start1: patch.utf8Start1,
|
|
50
|
-
utf8Start2: patch.utf8Start2,
|
|
51
|
-
length1: patch.length1,
|
|
52
|
-
length2: patch.length2,
|
|
53
|
-
utf8Length1: patch.utf8Length1,
|
|
54
|
-
utf8Length2: patch.utf8Length2,
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return adjusted
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function utf8len(codePoint: number): 1 | 2 | 3 | 4 {
|
|
62
|
-
// See table at https://en.wikipedia.org/wiki/UTF-8
|
|
63
|
-
if (codePoint <= 0x007f) return 1
|
|
64
|
-
if (codePoint <= 0x07ff) return 2
|
|
65
|
-
if (codePoint <= 0xffff) return 3
|
|
66
|
-
return 4
|
|
67
|
-
}
|