@portabletext/editor 1.0.8 → 1.0.10
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 +18 -5
- package/lib/index.d.ts +18 -5
- package/lib/index.esm.js +188 -177
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +187 -176
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +188 -177
- package/lib/index.mjs.map +1 -1
- package/package.json +20 -22
- package/src/editor/Editable.tsx +4 -7
- package/src/editor/PortableTextEditor.tsx +31 -17
- package/src/editor/__tests__/RangeDecorations.test.tsx +4 -4
- package/src/editor/components/Synchronizer.tsx +15 -46
- package/src/editor/hooks/usePortableTextEditor.ts +1 -0
- package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +3 -0
- package/src/editor/hooks/usePortableTextEditorSelection.tsx +63 -0
- package/src/editor/plugins/createWithHotKeys.ts +5 -1
- package/src/types/editor.ts +7 -0
- package/src/types/options.ts +6 -0
- package/src/utils/withChanges.ts +0 -7
- package/src/editor/hooks/usePortableTextEditorSelection.ts +0 -22
- package/src/editor/hooks/usePortableTextEditorValue.ts +0 -16
- package/src/utils/bufferUntil.ts +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"src"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@portabletext/patches": "1.0
|
|
45
|
+
"@portabletext/patches": "1.1.0",
|
|
46
46
|
"debug": "^4.3.4",
|
|
47
47
|
"is-hotkey-esm": "^1.0.0",
|
|
48
48
|
"lodash": "^4.17.21",
|
|
@@ -50,32 +50,31 @@
|
|
|
50
50
|
"slate-react": "0.101.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
54
53
|
"@jest/globals": "^29.7.0",
|
|
55
|
-
"@playwright/test": "1.45.
|
|
54
|
+
"@playwright/test": "1.45.3",
|
|
56
55
|
"@portabletext/toolkit": "^2.0.15",
|
|
57
|
-
"@sanity/block-tools": "^3.
|
|
56
|
+
"@sanity/block-tools": "^3.52.2",
|
|
58
57
|
"@sanity/diff-match-patch": "^3.1.1",
|
|
59
58
|
"@sanity/eslint-config-i18n": "^1.1.0",
|
|
60
59
|
"@sanity/eslint-config-studio": "^4.0.0",
|
|
61
|
-
"@sanity/pkg-utils": "^6.10.
|
|
62
|
-
"@sanity/schema": "^3.
|
|
60
|
+
"@sanity/pkg-utils": "^6.10.6",
|
|
61
|
+
"@sanity/schema": "^3.52.2",
|
|
63
62
|
"@sanity/test": "0.0.1-alpha.1",
|
|
64
|
-
"@sanity/types": "^3.
|
|
65
|
-
"@sanity/ui": "^2.
|
|
66
|
-
"@sanity/util": "^3.
|
|
63
|
+
"@sanity/types": "^3.52.2",
|
|
64
|
+
"@sanity/ui": "^2.8.8",
|
|
65
|
+
"@sanity/util": "^3.52.2",
|
|
67
66
|
"@testing-library/react": "^13.4.0",
|
|
68
67
|
"@types/debug": "^4.1.5",
|
|
69
68
|
"@types/express": "^4.17.21",
|
|
70
69
|
"@types/express-ws": "^3.0.4",
|
|
71
|
-
"@types/lodash": "^4.17.
|
|
70
|
+
"@types/lodash": "^4.17.7",
|
|
72
71
|
"@types/node": "^18.19.8",
|
|
73
72
|
"@types/node-ipc": "^9.2.3",
|
|
74
73
|
"@types/react": "^18.3.3",
|
|
75
74
|
"@types/react-dom": "^18.3.0",
|
|
76
|
-
"@types/ws": "~8.5.
|
|
77
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
78
|
-
"@typescript-eslint/parser": "^7.
|
|
75
|
+
"@types/ws": "~8.5.11",
|
|
76
|
+
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
|
77
|
+
"@typescript-eslint/parser": "^7.17.0",
|
|
79
78
|
"@vitejs/plugin-react": "^4.3.1",
|
|
80
79
|
"dotenv": "^16.4.5",
|
|
81
80
|
"eslint": "^8.57.0",
|
|
@@ -83,11 +82,11 @@
|
|
|
83
82
|
"eslint-config-sanity": "^7.1.2",
|
|
84
83
|
"eslint-import-resolver-typescript": "^3.6.1",
|
|
85
84
|
"eslint-plugin-import": "^2.29.1",
|
|
86
|
-
"eslint-plugin-prettier": "^5.1
|
|
87
|
-
"eslint-plugin-react-compiler": "0.0.0-experimental-
|
|
85
|
+
"eslint-plugin-prettier": "^5.2.1",
|
|
86
|
+
"eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
|
|
88
87
|
"eslint-plugin-tsdoc": "^0.3.0",
|
|
89
88
|
"eslint-plugin-unicorn": "^54.0.0",
|
|
90
|
-
"eslint-plugin-unused-imports": "^4.0.
|
|
89
|
+
"eslint-plugin-unused-imports": "^4.0.1",
|
|
91
90
|
"express": "^4.19.2",
|
|
92
91
|
"express-ws": "^5.0.2",
|
|
93
92
|
"jest": "^29.7.0",
|
|
@@ -97,11 +96,10 @@
|
|
|
97
96
|
"node-ipc": "npm:@node-ipc/compat@9.2.5",
|
|
98
97
|
"react": "^18.3.1",
|
|
99
98
|
"react-dom": "^18.3.1",
|
|
100
|
-
"rimraf": "^3.0.2",
|
|
101
99
|
"rxjs": "^7.8.1",
|
|
102
|
-
"styled-components": "^6.1.
|
|
103
|
-
"tsx": "^4.
|
|
104
|
-
"typescript": "^5.
|
|
100
|
+
"styled-components": "^6.1.12",
|
|
101
|
+
"tsx": "^4.16.2",
|
|
102
|
+
"typescript": "^5.5.3",
|
|
105
103
|
"vite": "^4.5.3"
|
|
106
104
|
},
|
|
107
105
|
"peerDependencies": {
|
|
@@ -123,7 +121,7 @@
|
|
|
123
121
|
"build": "pkg-utils build --strict --check --clean",
|
|
124
122
|
"check:lint": "eslint .",
|
|
125
123
|
"check:types": "tsc --project tsconfig.lib.json",
|
|
126
|
-
"clean": "
|
|
124
|
+
"clean": "del .turbo && del lib && del node_modules",
|
|
127
125
|
"dev": "pkg-utils watch",
|
|
128
126
|
"dev:e2e-server": "cd ./e2e-tests/ && tsx serve",
|
|
129
127
|
"lint:fix": "eslint . --fix",
|
package/src/editor/Editable.tsx
CHANGED
|
@@ -79,8 +79,6 @@ interface BaseRangeWithDecoration extends BaseRange {
|
|
|
79
79
|
rangeDecoration: RangeDecoration
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const EMPTY_DECORATIONS_STATE: BaseRangeWithDecoration[] = []
|
|
83
|
-
|
|
84
82
|
/**
|
|
85
83
|
* @public
|
|
86
84
|
*/
|
|
@@ -142,8 +140,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
|
|
|
142
140
|
const ref = useRef<HTMLDivElement | null>(null)
|
|
143
141
|
const [editableElement, setEditableElement] = useState<HTMLDivElement | null>(null)
|
|
144
142
|
const [hasInvalidValue, setHasInvalidValue] = useState(false)
|
|
145
|
-
const [rangeDecorationState, setRangeDecorationsState] =
|
|
146
|
-
useState<BaseRangeWithDecoration[]>(EMPTY_DECORATIONS_STATE)
|
|
143
|
+
const [rangeDecorationState, setRangeDecorationsState] = useState<BaseRangeWithDecoration[]>([])
|
|
147
144
|
|
|
148
145
|
// Forward ref to parent component
|
|
149
146
|
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
|
|
@@ -296,7 +293,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
|
|
|
296
293
|
return
|
|
297
294
|
}
|
|
298
295
|
}
|
|
299
|
-
setRangeDecorationsState(
|
|
296
|
+
setRangeDecorationsState([])
|
|
300
297
|
},
|
|
301
298
|
[portableTextEditor, rangeDecorations, schemaTypes, slateEditor],
|
|
302
299
|
)
|
|
@@ -619,7 +616,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
|
|
|
619
616
|
}
|
|
620
617
|
// Editor node has a path length of 0 (should never be decorated)
|
|
621
618
|
if (path.length === 0) {
|
|
622
|
-
return
|
|
619
|
+
return []
|
|
623
620
|
}
|
|
624
621
|
const result = rangeDecorationState.filter((item) => {
|
|
625
622
|
// Special case in order to only return one decoration for collapsed ranges
|
|
@@ -639,7 +636,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
|
|
|
639
636
|
if (result.length > 0) {
|
|
640
637
|
return result
|
|
641
638
|
}
|
|
642
|
-
return
|
|
639
|
+
return []
|
|
643
640
|
},
|
|
644
641
|
[slateEditor, schemaTypes, rangeDecorationState],
|
|
645
642
|
)
|
|
@@ -26,15 +26,16 @@ import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSc
|
|
|
26
26
|
import {compileType} from '../utils/schema'
|
|
27
27
|
import {SlateContainer} from './components/SlateContainer'
|
|
28
28
|
import {Synchronizer} from './components/Synchronizer'
|
|
29
|
-
import {
|
|
29
|
+
import {PortableTextEditorContext} from './hooks/usePortableTextEditor'
|
|
30
|
+
import {
|
|
31
|
+
defaultKeyGenerator,
|
|
32
|
+
PortableTextEditorKeyGeneratorContext,
|
|
33
|
+
} from './hooks/usePortableTextEditorKeyGenerator'
|
|
34
|
+
import {PortableTextEditorSelectionProvider} from './hooks/usePortableTextEditorSelection'
|
|
35
|
+
import {PortableTextEditorReadOnlyContext} from './hooks/usePortableTextReadOnly'
|
|
30
36
|
|
|
31
37
|
const debug = debugWithName('component:PortableTextEditor')
|
|
32
38
|
|
|
33
|
-
/**
|
|
34
|
-
* Props for the PortableTextEditor component
|
|
35
|
-
*
|
|
36
|
-
* @public
|
|
37
|
-
*/
|
|
38
39
|
/**
|
|
39
40
|
* Props for the PortableTextEditor component
|
|
40
41
|
*
|
|
@@ -109,7 +110,7 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
|
|
|
109
110
|
super(props)
|
|
110
111
|
|
|
111
112
|
if (!props.schemaType) {
|
|
112
|
-
throw new Error('PortableTextEditor: missing "
|
|
113
|
+
throw new Error('PortableTextEditor: missing "schemaType" property')
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
if (props.incomingPatches$) {
|
|
@@ -143,6 +144,14 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
|
|
|
143
144
|
this.editable = {...this.editable, ...editable}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
private getValue = () => {
|
|
148
|
+
if (this.editable) {
|
|
149
|
+
return this.editable.getValue()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
146
155
|
render() {
|
|
147
156
|
const {onChange, value, children, patches$, incomingPatches$} = this.props
|
|
148
157
|
const {change$} = this
|
|
@@ -163,16 +172,21 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
|
|
|
163
172
|
portableTextEditor={this}
|
|
164
173
|
readOnly={readOnly}
|
|
165
174
|
>
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
<PortableTextEditorKeyGeneratorContext.Provider value={keyGenerator}>
|
|
176
|
+
<PortableTextEditorContext.Provider value={this}>
|
|
177
|
+
<PortableTextEditorReadOnlyContext.Provider value={readOnly}>
|
|
178
|
+
<PortableTextEditorSelectionProvider change$={change$}>
|
|
179
|
+
<Synchronizer
|
|
180
|
+
change$={change$}
|
|
181
|
+
getValue={this.getValue}
|
|
182
|
+
onChange={onChange}
|
|
183
|
+
value={value}
|
|
184
|
+
/>
|
|
185
|
+
{children}
|
|
186
|
+
</PortableTextEditorSelectionProvider>
|
|
187
|
+
</PortableTextEditorReadOnlyContext.Provider>
|
|
188
|
+
</PortableTextEditorContext.Provider>
|
|
189
|
+
</PortableTextEditorKeyGeneratorContext.Provider>
|
|
176
190
|
</SlateContainer>
|
|
177
191
|
)
|
|
178
192
|
}
|
|
@@ -46,7 +46,7 @@ describe('RangeDecorations', () => {
|
|
|
46
46
|
/>,
|
|
47
47
|
)
|
|
48
48
|
await waitFor(() => {
|
|
49
|
-
expect([rangeDecorationIteration, 'initial']).toEqual([
|
|
49
|
+
expect([rangeDecorationIteration, 'initial']).toEqual([1, 'initial'])
|
|
50
50
|
})
|
|
51
51
|
// Re-render with the same range decorations
|
|
52
52
|
rerender(
|
|
@@ -59,7 +59,7 @@ describe('RangeDecorations', () => {
|
|
|
59
59
|
/>,
|
|
60
60
|
)
|
|
61
61
|
await waitFor(() => {
|
|
62
|
-
expect([rangeDecorationIteration, 'initial']).toEqual([
|
|
62
|
+
expect([rangeDecorationIteration, 'initial']).toEqual([1, 'initial'])
|
|
63
63
|
})
|
|
64
64
|
// Update the range decorations, a new object with identical values
|
|
65
65
|
rangeDecorations = [
|
|
@@ -82,7 +82,7 @@ describe('RangeDecorations', () => {
|
|
|
82
82
|
)
|
|
83
83
|
await waitFor(() => {
|
|
84
84
|
expect([rangeDecorationIteration, 'updated-with-equal-values']).toEqual([
|
|
85
|
-
|
|
85
|
+
1,
|
|
86
86
|
'updated-with-equal-values',
|
|
87
87
|
])
|
|
88
88
|
})
|
|
@@ -107,7 +107,7 @@ describe('RangeDecorations', () => {
|
|
|
107
107
|
)
|
|
108
108
|
await waitFor(() => {
|
|
109
109
|
expect([rangeDecorationIteration, 'updated-with-different']).toEqual([
|
|
110
|
-
|
|
110
|
+
2,
|
|
111
111
|
'updated-with-different',
|
|
112
112
|
])
|
|
113
113
|
})
|
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
import {type Patch} from '@portabletext/patches'
|
|
2
2
|
import {type PortableTextBlock} from '@sanity/types'
|
|
3
3
|
import {throttle} from 'lodash'
|
|
4
|
-
import {
|
|
5
|
-
type PropsWithChildren,
|
|
6
|
-
startTransition,
|
|
7
|
-
useCallback,
|
|
8
|
-
useEffect,
|
|
9
|
-
useMemo,
|
|
10
|
-
useRef,
|
|
11
|
-
useState,
|
|
12
|
-
} from 'react'
|
|
4
|
+
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
|
13
5
|
import {Editor} from 'slate'
|
|
14
6
|
import {useSlate} from 'slate-react'
|
|
15
7
|
|
|
16
|
-
import {type EditorChange, type EditorChanges
|
|
8
|
+
import {type EditorChange, type EditorChanges} from '../../types/editor'
|
|
17
9
|
import {debugWithName} from '../../utils/debug'
|
|
18
10
|
import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps'
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {PortableTextEditorValueContext} from '../hooks/usePortableTextEditorValue'
|
|
23
|
-
import {PortableTextEditorReadOnlyContext} from '../hooks/usePortableTextReadOnly'
|
|
11
|
+
import {usePortableTextEditor} from '../hooks/usePortableTextEditor'
|
|
12
|
+
import {usePortableTextEditorKeyGenerator} from '../hooks/usePortableTextEditorKeyGenerator'
|
|
13
|
+
import {usePortableTextEditorReadOnlyStatus} from '../hooks/usePortableTextReadOnly'
|
|
24
14
|
import {useSyncValue} from '../hooks/useSyncValue'
|
|
25
|
-
import {PortableTextEditor} from '../PortableTextEditor'
|
|
26
15
|
|
|
27
16
|
const debug = debugWithName('component:PortableTextEditor:Synchronizer')
|
|
28
17
|
const debugVerbose = debug.enabled && false
|
|
@@ -34,13 +23,11 @@ const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
|
|
|
34
23
|
/**
|
|
35
24
|
* @internal
|
|
36
25
|
*/
|
|
37
|
-
export interface SynchronizerProps
|
|
26
|
+
export interface SynchronizerProps {
|
|
38
27
|
change$: EditorChanges
|
|
39
|
-
|
|
40
|
-
keyGenerator: () => string
|
|
28
|
+
getValue: () => Array<PortableTextBlock> | undefined
|
|
41
29
|
onChange: (change: EditorChange) => void
|
|
42
|
-
|
|
43
|
-
value: PortableTextBlock[] | undefined
|
|
30
|
+
value: Array<PortableTextBlock> | undefined
|
|
44
31
|
}
|
|
45
32
|
|
|
46
33
|
/**
|
|
@@ -48,8 +35,10 @@ export interface SynchronizerProps extends PropsWithChildren {
|
|
|
48
35
|
* @internal
|
|
49
36
|
*/
|
|
50
37
|
export function Synchronizer(props: SynchronizerProps) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
38
|
+
const portableTextEditor = usePortableTextEditor()
|
|
39
|
+
const keyGenerator = usePortableTextEditorKeyGenerator()
|
|
40
|
+
const readOnly = usePortableTextEditorReadOnlyStatus()
|
|
41
|
+
const {change$, getValue, onChange, value} = props
|
|
53
42
|
const pendingPatches = useRef<Patch[]>([])
|
|
54
43
|
|
|
55
44
|
const syncValue = useSyncValue({
|
|
@@ -71,12 +60,12 @@ export function Synchronizer(props: SynchronizerProps) {
|
|
|
71
60
|
if (debugVerbose) {
|
|
72
61
|
debug(`Patches:\n${JSON.stringify(pendingPatches.current, null, 2)}`)
|
|
73
62
|
}
|
|
74
|
-
const snapshot =
|
|
63
|
+
const snapshot = getValue()
|
|
75
64
|
change$.next({type: 'mutation', patches: pendingPatches.current, snapshot})
|
|
76
65
|
pendingPatches.current = []
|
|
77
66
|
}
|
|
78
67
|
IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
|
|
79
|
-
}, [slateEditor,
|
|
68
|
+
}, [slateEditor, getValue, change$])
|
|
80
69
|
|
|
81
70
|
const onFlushPendingPatchesThrottled = useMemo(() => {
|
|
82
71
|
return throttle(
|
|
@@ -116,14 +105,6 @@ export function Synchronizer(props: SynchronizerProps) {
|
|
|
116
105
|
onFlushPendingPatchesThrottled()
|
|
117
106
|
onChange(next)
|
|
118
107
|
break
|
|
119
|
-
case 'selection':
|
|
120
|
-
// Set the selection state in a transition, we don't need the state immediately.
|
|
121
|
-
startTransition(() => {
|
|
122
|
-
if (debugVerbose) debug('Setting selection')
|
|
123
|
-
setSelection(next.selection)
|
|
124
|
-
})
|
|
125
|
-
onChange(next) // Keep this out of the startTransition!
|
|
126
|
-
break
|
|
127
108
|
default:
|
|
128
109
|
onChange(next)
|
|
129
110
|
}
|
|
@@ -174,17 +155,5 @@ export function Synchronizer(props: SynchronizerProps) {
|
|
|
174
155
|
}
|
|
175
156
|
}, [change$, syncValue, value])
|
|
176
157
|
|
|
177
|
-
return
|
|
178
|
-
<PortableTextEditorKeyGeneratorContext.Provider value={keyGenerator}>
|
|
179
|
-
<PortableTextEditorContext.Provider value={portableTextEditor}>
|
|
180
|
-
<PortableTextEditorValueContext.Provider value={value}>
|
|
181
|
-
<PortableTextEditorReadOnlyContext.Provider value={readOnly}>
|
|
182
|
-
<PortableTextEditorSelectionContext.Provider value={selection}>
|
|
183
|
-
{props.children}
|
|
184
|
-
</PortableTextEditorSelectionContext.Provider>
|
|
185
|
-
</PortableTextEditorReadOnlyContext.Provider>
|
|
186
|
-
</PortableTextEditorValueContext.Provider>
|
|
187
|
-
</PortableTextEditorContext.Provider>
|
|
188
|
-
</PortableTextEditorKeyGeneratorContext.Provider>
|
|
189
|
-
)
|
|
158
|
+
return null
|
|
190
159
|
}
|
|
@@ -8,6 +8,7 @@ import {type PortableTextEditor} from '../PortableTextEditor'
|
|
|
8
8
|
export const PortableTextEditorContext = createContext<PortableTextEditor | null>(null)
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
+
* @public
|
|
11
12
|
* Get the current editor object from the React context.
|
|
12
13
|
*/
|
|
13
14
|
export const usePortableTextEditor = (): PortableTextEditor => {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {createContext, startTransition, useContext, useEffect, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
import {type EditorChanges, type EditorSelection} from '../../types/editor'
|
|
4
|
+
import {debugWithName} from '../../utils/debug'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A React context for sharing the editor selection.
|
|
8
|
+
*/
|
|
9
|
+
const PortableTextEditorSelectionContext = createContext<EditorSelection | null>(null)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @public
|
|
13
|
+
* Get the current editor selection from the React context.
|
|
14
|
+
*/
|
|
15
|
+
export const usePortableTextEditorSelection = (): EditorSelection => {
|
|
16
|
+
const selection = useContext(PortableTextEditorSelectionContext)
|
|
17
|
+
|
|
18
|
+
if (selection === undefined) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`The \`usePortableTextEditorSelection\` hook must be used inside the <PortableTextEditor> component's context.`,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
return selection
|
|
24
|
+
}
|
|
25
|
+
const debug = debugWithName('component:PortableTextEditor:SelectionProvider')
|
|
26
|
+
const debugVerbose = debug.enabled && false
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export function PortableTextEditorSelectionProvider(
|
|
32
|
+
props: React.PropsWithChildren<{
|
|
33
|
+
change$: EditorChanges
|
|
34
|
+
}>,
|
|
35
|
+
) {
|
|
36
|
+
const {change$} = props
|
|
37
|
+
const [selection, setSelection] = useState<EditorSelection>(null)
|
|
38
|
+
|
|
39
|
+
// Subscribe to, and handle changes from the editor
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
debug('Subscribing to selection changes$')
|
|
42
|
+
const subscription = change$.subscribe((next): void => {
|
|
43
|
+
if (next.type === 'selection') {
|
|
44
|
+
// Set the selection state in a transition, we don't need the state immediately.
|
|
45
|
+
startTransition(() => {
|
|
46
|
+
if (debugVerbose) debug('Setting selection')
|
|
47
|
+
setSelection(next.selection)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
debug('Unsubscribing to selection changes$')
|
|
54
|
+
subscription.unsubscribe()
|
|
55
|
+
}
|
|
56
|
+
}, [change$])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<PortableTextEditorSelectionContext.Provider value={selection}>
|
|
60
|
+
{props.children}
|
|
61
|
+
</PortableTextEditorSelectionContext.Provider>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -195,12 +195,16 @@ export function createWithHotkeys(
|
|
|
195
195
|
) as SlateTextBlock | VoidElement
|
|
196
196
|
const focusBlockPath = editor.selection.focus.path.slice(0, 1)
|
|
197
197
|
const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
|
|
198
|
+
const isTextBlock = isPortableTextTextBlock(focusBlock)
|
|
199
|
+
const isEmptyFocusBlock =
|
|
200
|
+
isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === ''
|
|
198
201
|
|
|
199
202
|
if (
|
|
200
203
|
nextBlock &&
|
|
201
204
|
focusBlock &&
|
|
202
205
|
!Editor.isVoid(editor, focusBlock) &&
|
|
203
|
-
Editor.isVoid(editor, nextBlock)
|
|
206
|
+
Editor.isVoid(editor, nextBlock) &&
|
|
207
|
+
isEmptyFocusBlock
|
|
204
208
|
) {
|
|
205
209
|
debug('Preventing deleting void block below')
|
|
206
210
|
event.preventDefault()
|
package/src/types/editor.ts
CHANGED
|
@@ -375,6 +375,9 @@ export type EditorChange =
|
|
|
375
375
|
| UnsetChange
|
|
376
376
|
| ValueChange
|
|
377
377
|
|
|
378
|
+
/**
|
|
379
|
+
* @beta
|
|
380
|
+
*/
|
|
378
381
|
export type EditorChanges = Subject<EditorChange>
|
|
379
382
|
|
|
380
383
|
/** @beta */
|
|
@@ -384,6 +387,10 @@ export type OnPasteResult =
|
|
|
384
387
|
path?: Path
|
|
385
388
|
}
|
|
386
389
|
| undefined
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @beta
|
|
393
|
+
*/
|
|
387
394
|
export type OnPasteResultOrPromise = OnPasteResult | Promise<OnPasteResult>
|
|
388
395
|
|
|
389
396
|
/** @beta */
|
package/src/types/options.ts
CHANGED
|
@@ -3,6 +3,9 @@ import {type BaseSyntheticEvent} from 'react'
|
|
|
3
3
|
import {type PortableTextEditor} from '../editor/PortableTextEditor'
|
|
4
4
|
import {type PatchObservable} from './editor'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
6
9
|
export type createEditorOptions = {
|
|
7
10
|
keyGenerator: () => string
|
|
8
11
|
patches$?: PatchObservable
|
|
@@ -11,6 +14,9 @@ export type createEditorOptions = {
|
|
|
11
14
|
maxBlocks?: number
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @beta
|
|
19
|
+
*/
|
|
14
20
|
export type HotkeyOptions = {
|
|
15
21
|
marks?: Record<string, string>
|
|
16
22
|
custom?: Record<string, (event: BaseSyntheticEvent, editor: PortableTextEditor) => void>
|
package/src/utils/withChanges.ts
CHANGED
|
@@ -13,13 +13,6 @@ export function isChangingRemotely(editor: Editor): boolean | undefined {
|
|
|
13
13
|
return IS_PROCESSING_REMOTE_CHANGES.get(editor)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function withLocalChanges(editor: Editor, fn: () => void): void {
|
|
17
|
-
const prev = isChangingLocally(editor) || false
|
|
18
|
-
IS_PROCESSING_LOCAL_CHANGES.set(editor, true)
|
|
19
|
-
fn()
|
|
20
|
-
IS_PROCESSING_LOCAL_CHANGES.set(editor, prev)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
export function isChangingLocally(editor: Editor): boolean | undefined {
|
|
24
17
|
return IS_PROCESSING_LOCAL_CHANGES.get(editor)
|
|
25
18
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import {createContext, useContext} from 'react'
|
|
2
|
-
|
|
3
|
-
import {type EditorSelection} from '../../types/editor'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A React context for sharing the editor selection.
|
|
7
|
-
*/
|
|
8
|
-
export const PortableTextEditorSelectionContext = createContext<EditorSelection | null>(null)
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get the current editor selection from the React context.
|
|
12
|
-
*/
|
|
13
|
-
export const usePortableTextEditorSelection = (): EditorSelection => {
|
|
14
|
-
const selection = useContext(PortableTextEditorSelectionContext)
|
|
15
|
-
|
|
16
|
-
if (selection === undefined) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`The \`usePortableTextEditorSelection\` hook must be used inside the <PortableTextEditor> component's context.`,
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
return selection
|
|
22
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {type PortableTextBlock} from '@sanity/types'
|
|
2
|
-
import {createContext, useContext} from 'react'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A React context for sharing the editor value.
|
|
6
|
-
*/
|
|
7
|
-
export const PortableTextEditorValueContext = createContext<PortableTextBlock[] | undefined>(
|
|
8
|
-
undefined,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get the current editor value from the React context.
|
|
13
|
-
*/
|
|
14
|
-
export const usePortableTextEditorValue = () => {
|
|
15
|
-
return useContext(PortableTextEditorValueContext)
|
|
16
|
-
}
|
package/src/utils/bufferUntil.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import {defer, EMPTY, type Observable, of, type OperatorFunction, switchMap, tap} from 'rxjs'
|
|
2
|
-
|
|
3
|
-
export function bufferUntil<T>(
|
|
4
|
-
emitWhen: (currentBuffer: T[]) => boolean,
|
|
5
|
-
): OperatorFunction<T, T[]> {
|
|
6
|
-
return (source: Observable<T>) =>
|
|
7
|
-
defer(() => {
|
|
8
|
-
let buffer: T[] = [] // custom buffer
|
|
9
|
-
return source.pipe(
|
|
10
|
-
tap((v) => buffer.push(v)), // add values to buffer
|
|
11
|
-
switchMap(() => (emitWhen(buffer) ? of(buffer) : EMPTY)), // emit the buffer when the condition is met
|
|
12
|
-
tap(() => (buffer = [])), // clear the buffer
|
|
13
|
-
)
|
|
14
|
-
})
|
|
15
|
-
}
|