@portabletext/editor 1.8.0 → 1.9.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/lib/index.d.mts +76 -11
- package/lib/index.d.ts +76 -11
- package/lib/index.esm.js +133 -27
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +133 -27
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +133 -27
- package/lib/index.mjs.map +1 -1
- package/package.json +10 -10
- package/src/editor/Editable.tsx +46 -31
- package/src/editor/behavior/behavior.action.insert-span.ts +48 -0
- package/src/editor/behavior/behavior.actions.ts +20 -1
- package/src/editor/behavior/behavior.links.ts +91 -0
- package/src/editor/behavior/behavior.types.ts +13 -0
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +6 -2
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@portabletext/toolkit": "^2.0.16",
|
|
61
|
-
"@sanity/block-tools": "^3.64.
|
|
61
|
+
"@sanity/block-tools": "^3.64.1",
|
|
62
62
|
"@sanity/diff-match-patch": "^3.1.1",
|
|
63
|
-
"@sanity/pkg-utils": "^6.11.
|
|
64
|
-
"@sanity/schema": "^3.64.
|
|
65
|
-
"@sanity/types": "^3.64.
|
|
63
|
+
"@sanity/pkg-utils": "^6.11.11",
|
|
64
|
+
"@sanity/schema": "^3.64.1",
|
|
65
|
+
"@sanity/types": "^3.64.1",
|
|
66
66
|
"@testing-library/jest-dom": "^6.6.3",
|
|
67
67
|
"@testing-library/react": "^16.0.1",
|
|
68
68
|
"@types/debug": "^4.1.5",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
|
74
74
|
"@typescript-eslint/parser": "^8.14.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.3.3",
|
|
76
|
-
"@vitest/browser": "^2.1.
|
|
76
|
+
"@vitest/browser": "^2.1.5",
|
|
77
77
|
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
|
78
78
|
"eslint": "8.57.1",
|
|
79
79
|
"eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
|
@@ -85,14 +85,14 @@
|
|
|
85
85
|
"styled-components": "^6.1.13",
|
|
86
86
|
"typescript": "5.6.3",
|
|
87
87
|
"vite": "^5.4.11",
|
|
88
|
-
"vitest": "^2.1.
|
|
88
|
+
"vitest": "^2.1.5",
|
|
89
89
|
"vitest-browser-react": "^0.0.3",
|
|
90
90
|
"@sanity/gherkin-driver": "^0.0.1"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@sanity/block-tools": "^3.64.
|
|
94
|
-
"@sanity/schema": "^3.64.
|
|
95
|
-
"@sanity/types": "^3.64.
|
|
93
|
+
"@sanity/block-tools": "^3.64.1",
|
|
94
|
+
"@sanity/schema": "^3.64.1",
|
|
95
|
+
"@sanity/types": "^3.64.1",
|
|
96
96
|
"react": "^16.9 || ^17 || ^18",
|
|
97
97
|
"rxjs": "^7.8.1",
|
|
98
98
|
"styled-components": "^6.1.13"
|
package/src/editor/Editable.tsx
CHANGED
|
@@ -428,16 +428,6 @@ export const PortableTextEditable = forwardRef<
|
|
|
428
428
|
// Handle incoming pasting events in the editor
|
|
429
429
|
const handlePaste = useCallback(
|
|
430
430
|
(event: ClipboardEvent<HTMLDivElement>): Promise<void> | void => {
|
|
431
|
-
event.preventDefault()
|
|
432
|
-
if (!slateEditor.selection) {
|
|
433
|
-
return
|
|
434
|
-
}
|
|
435
|
-
if (!onPaste) {
|
|
436
|
-
debug('Pasting normally')
|
|
437
|
-
slateEditor.insertData(event.clipboardData)
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
|
|
441
431
|
const value = PortableTextEditor.getValue(portableTextEditor)
|
|
442
432
|
const ptRange = toPortableTextRange(
|
|
443
433
|
value,
|
|
@@ -445,19 +435,21 @@ export const PortableTextEditable = forwardRef<
|
|
|
445
435
|
schemaTypes,
|
|
446
436
|
)
|
|
447
437
|
const path = ptRange?.focus.path || []
|
|
448
|
-
const onPasteResult = onPaste({event, value, path, schemaTypes})
|
|
438
|
+
const onPasteResult = onPaste?.({event, value, path, schemaTypes})
|
|
439
|
+
|
|
440
|
+
if (onPasteResult || !slateEditor.selection) {
|
|
441
|
+
event.preventDefault()
|
|
449
442
|
|
|
450
|
-
if (onPasteResult === undefined) {
|
|
451
|
-
debug('No result from custom paste handler, pasting normally')
|
|
452
|
-
slateEditor.insertData(event.clipboardData)
|
|
453
|
-
} else {
|
|
454
443
|
// Resolve it as promise (can be either async promise or sync return value)
|
|
455
444
|
editorActor.send({type: 'loading'})
|
|
445
|
+
|
|
456
446
|
Promise.resolve(onPasteResult)
|
|
457
447
|
.then((result) => {
|
|
458
448
|
debug('Custom paste function from client resolved', result)
|
|
449
|
+
|
|
459
450
|
if (!result || !result.insert) {
|
|
460
451
|
debug('No result from custom paste handler, pasting normally')
|
|
452
|
+
|
|
461
453
|
slateEditor.insertData(event.clipboardData)
|
|
462
454
|
} else if (result.insert) {
|
|
463
455
|
slateEditor.insertFragment(
|
|
@@ -474,12 +466,26 @@ export const PortableTextEditable = forwardRef<
|
|
|
474
466
|
})
|
|
475
467
|
.catch((error) => {
|
|
476
468
|
console.error(error)
|
|
469
|
+
|
|
477
470
|
return error
|
|
478
471
|
})
|
|
479
472
|
.finally(() => {
|
|
480
473
|
editorActor.send({type: 'done loading'})
|
|
481
474
|
})
|
|
475
|
+
} else if (event.nativeEvent.clipboardData) {
|
|
476
|
+
event.preventDefault()
|
|
477
|
+
|
|
478
|
+
editorActor.send({
|
|
479
|
+
type: 'behavior event',
|
|
480
|
+
behaviorEvent: {
|
|
481
|
+
type: 'paste',
|
|
482
|
+
clipboardData: event.nativeEvent.clipboardData,
|
|
483
|
+
},
|
|
484
|
+
editor: slateEditor,
|
|
485
|
+
})
|
|
482
486
|
}
|
|
487
|
+
|
|
488
|
+
debug('No result from custom paste handler, pasting normally')
|
|
483
489
|
},
|
|
484
490
|
[editorActor, onPaste, portableTextEditor, schemaTypes, slateEditor],
|
|
485
491
|
)
|
|
@@ -515,23 +521,32 @@ export const PortableTextEditable = forwardRef<
|
|
|
515
521
|
if (onClick) {
|
|
516
522
|
onClick(event)
|
|
517
523
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const node = Node.descendant(slateEditor, path.slice(0, 1)) as
|
|
524
|
+
|
|
525
|
+
const focusBlockPath = slateEditor.selection
|
|
526
|
+
? slateEditor.selection.focus.path.slice(0, 1)
|
|
527
|
+
: undefined
|
|
528
|
+
const focusBlock = focusBlockPath
|
|
529
|
+
? (Node.descendant(slateEditor, focusBlockPath) as
|
|
525
530
|
| SlateTextBlock
|
|
526
|
-
| VoidElement
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
531
|
+
| VoidElement)
|
|
532
|
+
: undefined
|
|
533
|
+
const [_, lastNodePath] = Node.last(slateEditor, [])
|
|
534
|
+
const lastBlockPath = lastNodePath.slice(0, 1)
|
|
535
|
+
const lastNodeFocused = focusBlockPath
|
|
536
|
+
? Path.equals(lastBlockPath, focusBlockPath)
|
|
537
|
+
: false
|
|
538
|
+
const lastBlockIsVoid = focusBlock
|
|
539
|
+
? !slateEditor.isTextBlock(focusBlock)
|
|
540
|
+
: false
|
|
541
|
+
const collapsedSelection =
|
|
542
|
+
slateEditor.selection && SlateRange.isCollapsed(slateEditor.selection)
|
|
543
|
+
|
|
544
|
+
if (collapsedSelection && lastNodeFocused && lastBlockIsVoid) {
|
|
545
|
+
Transforms.insertNodes(
|
|
546
|
+
slateEditor,
|
|
547
|
+
slateEditor.pteCreateTextBlock({decorators: []}),
|
|
548
|
+
)
|
|
549
|
+
slateEditor.onChange()
|
|
535
550
|
}
|
|
536
551
|
},
|
|
537
552
|
[onClick, slateEditor],
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {Editor, Transforms} from 'slate'
|
|
2
|
+
import type {BehaviourActionImplementation} from './behavior.actions'
|
|
3
|
+
|
|
4
|
+
export const insertSpanActionImplementation: BehaviourActionImplementation<
|
|
5
|
+
'insert span'
|
|
6
|
+
> = ({context, action}) => {
|
|
7
|
+
if (!action.editor.selection) {
|
|
8
|
+
console.error('Unable to perform action without selection', action)
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [focusBlock, focusBlockPath] = Array.from(
|
|
13
|
+
Editor.nodes(action.editor, {
|
|
14
|
+
at: action.editor.selection.focus.path,
|
|
15
|
+
match: (node) => action.editor.isTextBlock(node),
|
|
16
|
+
}),
|
|
17
|
+
)[0] ?? [undefined, undefined]
|
|
18
|
+
|
|
19
|
+
if (!focusBlock || !focusBlockPath) {
|
|
20
|
+
console.error('Unable to perform action without focus block', action)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const markDefs = focusBlock.markDefs ?? []
|
|
25
|
+
const annotations = action.annotations
|
|
26
|
+
? action.annotations.map((annotation) => ({
|
|
27
|
+
_type: annotation.name,
|
|
28
|
+
_key: context.keyGenerator(),
|
|
29
|
+
...annotation.value,
|
|
30
|
+
}))
|
|
31
|
+
: undefined
|
|
32
|
+
|
|
33
|
+
if (annotations && annotations.length > 0) {
|
|
34
|
+
Transforms.setNodes(action.editor, {
|
|
35
|
+
markDefs: [...markDefs, ...annotations],
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Transforms.insertNodes(action.editor, {
|
|
40
|
+
_type: 'span',
|
|
41
|
+
_key: context.keyGenerator(),
|
|
42
|
+
text: action.text,
|
|
43
|
+
marks: [
|
|
44
|
+
...(annotations?.map((annotation) => annotation._key) ?? []),
|
|
45
|
+
...(action.decorators ?? []),
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
insertBreakActionImplementation,
|
|
24
24
|
insertSoftBreakActionImplementation,
|
|
25
25
|
} from './behavior.action.insert-break'
|
|
26
|
+
import {insertSpanActionImplementation} from './behavior.action.insert-span'
|
|
26
27
|
import type {
|
|
27
28
|
BehaviorAction,
|
|
28
29
|
BehaviorEvent,
|
|
@@ -116,6 +117,7 @@ const behaviorActionImplementations: BehaviourActionImplementations = {
|
|
|
116
117
|
'insert block object': insertBlockObjectActionImplementation,
|
|
117
118
|
'insert break': insertBreakActionImplementation,
|
|
118
119
|
'insert soft break': insertSoftBreakActionImplementation,
|
|
120
|
+
'insert span': insertSpanActionImplementation,
|
|
119
121
|
'insert text': ({action}) => {
|
|
120
122
|
insertText(action.editor, action.text)
|
|
121
123
|
},
|
|
@@ -137,6 +139,9 @@ const behaviorActionImplementations: BehaviourActionImplementations = {
|
|
|
137
139
|
'effect': ({action}) => {
|
|
138
140
|
action.effect()
|
|
139
141
|
},
|
|
142
|
+
'paste': ({action}) => {
|
|
143
|
+
action.editor.insertData(action.clipboardData)
|
|
144
|
+
},
|
|
140
145
|
'select': ({action}) => {
|
|
141
146
|
const newSelection = toSlateRange(action.selection, action.editor)
|
|
142
147
|
|
|
@@ -178,6 +183,13 @@ export function performAction({
|
|
|
178
183
|
})
|
|
179
184
|
break
|
|
180
185
|
}
|
|
186
|
+
case 'insert span': {
|
|
187
|
+
behaviorActionImplementations['insert span']({
|
|
188
|
+
context,
|
|
189
|
+
action,
|
|
190
|
+
})
|
|
191
|
+
break
|
|
192
|
+
}
|
|
181
193
|
case 'insert text block': {
|
|
182
194
|
behaviorActionImplementations['insert text block']({
|
|
183
195
|
context,
|
|
@@ -311,11 +323,18 @@ function performDefaultAction({
|
|
|
311
323
|
})
|
|
312
324
|
break
|
|
313
325
|
}
|
|
314
|
-
|
|
326
|
+
case 'insert text': {
|
|
315
327
|
behaviorActionImplementations['insert text']({
|
|
316
328
|
context,
|
|
317
329
|
action,
|
|
318
330
|
})
|
|
331
|
+
break
|
|
332
|
+
}
|
|
333
|
+
default: {
|
|
334
|
+
behaviorActionImplementations.paste({
|
|
335
|
+
context,
|
|
336
|
+
action,
|
|
337
|
+
})
|
|
319
338
|
}
|
|
320
339
|
}
|
|
321
340
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {PortableTextMemberSchemaTypes} from '../../types/editor'
|
|
2
|
+
import {defineBehavior} from './behavior.types'
|
|
3
|
+
import {getFocusSpan, selectionIsCollapsed} from './behavior.utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @alpha
|
|
7
|
+
*/
|
|
8
|
+
export type LinkBehaviorsConfig = {
|
|
9
|
+
mapLinkAnnotation?: (config: {
|
|
10
|
+
schema: PortableTextMemberSchemaTypes
|
|
11
|
+
url: string
|
|
12
|
+
}) => {name: string; value: {[prop: string]: unknown}} | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @alpha
|
|
17
|
+
*/
|
|
18
|
+
export function createLinkBehaviors(config: LinkBehaviorsConfig) {
|
|
19
|
+
const pasteLinkOnSelection = defineBehavior({
|
|
20
|
+
on: 'paste',
|
|
21
|
+
guard: ({context, event}) => {
|
|
22
|
+
const selectionCollapsed = selectionIsCollapsed(context)
|
|
23
|
+
const text = event.clipboardData.getData('text/plain')
|
|
24
|
+
const url = looksLikeUrl(text) ? text : undefined
|
|
25
|
+
const annotation =
|
|
26
|
+
url !== undefined
|
|
27
|
+
? config.mapLinkAnnotation?.({url, schema: context.schema})
|
|
28
|
+
: undefined
|
|
29
|
+
|
|
30
|
+
if (annotation && !selectionCollapsed) {
|
|
31
|
+
return {annotation}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false
|
|
35
|
+
},
|
|
36
|
+
actions: [
|
|
37
|
+
(_, {annotation}) => [
|
|
38
|
+
{
|
|
39
|
+
type: 'annotation.add',
|
|
40
|
+
annotation,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
const pasteLinkAtCaret = defineBehavior({
|
|
46
|
+
on: 'paste',
|
|
47
|
+
guard: ({context, event}) => {
|
|
48
|
+
const focusSpan = getFocusSpan(context)
|
|
49
|
+
const selectionCollapsed = selectionIsCollapsed(context)
|
|
50
|
+
|
|
51
|
+
if (!focusSpan || !selectionCollapsed) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const text = event.clipboardData.getData('text/plain')
|
|
56
|
+
const url = looksLikeUrl(text) ? text : undefined
|
|
57
|
+
const annotation =
|
|
58
|
+
url !== undefined
|
|
59
|
+
? config.mapLinkAnnotation?.({url, schema: context.schema})
|
|
60
|
+
: undefined
|
|
61
|
+
|
|
62
|
+
if (url && annotation && selectionCollapsed) {
|
|
63
|
+
return {focusSpan, annotation, url}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false
|
|
67
|
+
},
|
|
68
|
+
actions: [
|
|
69
|
+
(_, {annotation, url}) => [
|
|
70
|
+
{
|
|
71
|
+
type: 'insert span',
|
|
72
|
+
text: url,
|
|
73
|
+
annotations: [annotation],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const linkBehaviors = [pasteLinkOnSelection, pasteLinkAtCaret]
|
|
80
|
+
|
|
81
|
+
return linkBehaviors
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function looksLikeUrl(text: string) {
|
|
85
|
+
let looksLikeUrl = false
|
|
86
|
+
try {
|
|
87
|
+
new URL(text)
|
|
88
|
+
looksLikeUrl = true
|
|
89
|
+
} catch {}
|
|
90
|
+
return looksLikeUrl
|
|
91
|
+
}
|
|
@@ -74,6 +74,10 @@ export type BehaviorEvent =
|
|
|
74
74
|
text: string
|
|
75
75
|
options?: TextInsertTextOptions
|
|
76
76
|
}
|
|
77
|
+
| {
|
|
78
|
+
type: 'paste'
|
|
79
|
+
clipboardData: NonNullable<ClipboardEvent['clipboardData']>
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
83
|
* @alpha
|
|
@@ -99,6 +103,15 @@ export type BehaviorActionIntend =
|
|
|
99
103
|
name: string
|
|
100
104
|
value?: {[prop: string]: unknown}
|
|
101
105
|
}
|
|
106
|
+
| {
|
|
107
|
+
type: 'insert span'
|
|
108
|
+
text: string
|
|
109
|
+
annotations?: Array<{
|
|
110
|
+
name: string
|
|
111
|
+
value: {[prop: string]: unknown}
|
|
112
|
+
}>
|
|
113
|
+
decorators?: Array<string>
|
|
114
|
+
}
|
|
102
115
|
| {
|
|
103
116
|
type: 'insert text block'
|
|
104
117
|
decorators: Array<string>
|
|
@@ -837,12 +837,16 @@ export function isDecoratorActive({
|
|
|
837
837
|
return false
|
|
838
838
|
}
|
|
839
839
|
|
|
840
|
-
const
|
|
840
|
+
const selectedTextNodes = Array.from(
|
|
841
841
|
Editor.nodes(editor, {match: Text.isText, at: editor.selection}),
|
|
842
842
|
)
|
|
843
843
|
|
|
844
|
+
if (selectedTextNodes.length === 0) {
|
|
845
|
+
return false
|
|
846
|
+
}
|
|
847
|
+
|
|
844
848
|
if (Range.isExpanded(editor.selection)) {
|
|
845
|
-
return
|
|
849
|
+
return selectedTextNodes.every((n) => {
|
|
846
850
|
const [node] = n
|
|
847
851
|
|
|
848
852
|
return node.marks?.includes(decorator)
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ export {
|
|
|
5
5
|
createMarkdownBehaviors,
|
|
6
6
|
type MarkdownBehaviorsConfig,
|
|
7
7
|
} from './editor/behavior/behavior.markdown'
|
|
8
|
+
export {
|
|
9
|
+
createLinkBehaviors,
|
|
10
|
+
type LinkBehaviorsConfig,
|
|
11
|
+
} from './editor/behavior/behavior.links'
|
|
8
12
|
export {
|
|
9
13
|
defineBehavior,
|
|
10
14
|
type Behavior,
|