@portabletext/editor 1.0.8 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -97,7 +97,6 @@
97
97
  "node-ipc": "npm:@node-ipc/compat@9.2.5",
98
98
  "react": "^18.3.1",
99
99
  "react-dom": "^18.3.1",
100
- "rimraf": "^3.0.2",
101
100
  "rxjs": "^7.8.1",
102
101
  "styled-components": "^6.1.11",
103
102
  "tsx": "^4.15.7",
@@ -123,7 +122,7 @@
123
122
  "build": "pkg-utils build --strict --check --clean",
124
123
  "check:lint": "eslint .",
125
124
  "check:types": "tsc --project tsconfig.lib.json",
126
- "clean": "rimraf lib",
125
+ "clean": "del .turbo && del lib && del node_modules",
127
126
  "dev": "pkg-utils watch",
128
127
  "dev:e2e-server": "cd ./e2e-tests/ && tsx serve",
129
128
  "lint:fix": "eslint . --fix",
@@ -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(EMPTY_DECORATIONS_STATE)
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 EMPTY_DECORATIONS_STATE
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 EMPTY_DECORATIONS_STATE
639
+ return []
643
640
  },
644
641
  [slateEditor, schemaTypes, rangeDecorationState],
645
642
  )
@@ -26,7 +26,13 @@ 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 {defaultKeyGenerator} from './hooks/usePortableTextEditorKeyGenerator'
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
 
@@ -143,6 +149,14 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
143
149
  this.editable = {...this.editable, ...editable}
144
150
  }
145
151
 
152
+ private getValue = () => {
153
+ if (this.editable) {
154
+ return this.editable.getValue()
155
+ }
156
+
157
+ return undefined
158
+ }
159
+
146
160
  render() {
147
161
  const {onChange, value, children, patches$, incomingPatches$} = this.props
148
162
  const {change$} = this
@@ -163,16 +177,21 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
163
177
  portableTextEditor={this}
164
178
  readOnly={readOnly}
165
179
  >
166
- <Synchronizer
167
- change$={change$}
168
- keyGenerator={keyGenerator}
169
- onChange={onChange}
170
- portableTextEditor={this}
171
- readOnly={readOnly}
172
- value={value}
173
- >
174
- {children}
175
- </Synchronizer>
180
+ <PortableTextEditorKeyGeneratorContext.Provider value={keyGenerator}>
181
+ <PortableTextEditorContext.Provider value={this}>
182
+ <PortableTextEditorReadOnlyContext.Provider value={readOnly}>
183
+ <PortableTextEditorSelectionProvider change$={change$}>
184
+ <Synchronizer
185
+ change$={change$}
186
+ getValue={this.getValue}
187
+ onChange={onChange}
188
+ value={value}
189
+ />
190
+ {children}
191
+ </PortableTextEditorSelectionProvider>
192
+ </PortableTextEditorReadOnlyContext.Provider>
193
+ </PortableTextEditorContext.Provider>
194
+ </PortableTextEditorKeyGeneratorContext.Provider>
176
195
  </SlateContainer>
177
196
  )
178
197
  }
@@ -46,7 +46,7 @@ describe('RangeDecorations', () => {
46
46
  />,
47
47
  )
48
48
  await waitFor(() => {
49
- expect([rangeDecorationIteration, 'initial']).toEqual([0, 'initial'])
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([0, 'initial'])
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
- 0,
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
- 1,
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, type EditorSelection} from '../../types/editor'
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 {PortableTextEditorContext} from '../hooks/usePortableTextEditor'
20
- import {PortableTextEditorKeyGeneratorContext} from '../hooks/usePortableTextEditorKeyGenerator'
21
- import {PortableTextEditorSelectionContext} from '../hooks/usePortableTextEditorSelection'
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 extends PropsWithChildren {
26
+ export interface SynchronizerProps {
38
27
  change$: EditorChanges
39
- portableTextEditor: PortableTextEditor
40
- keyGenerator: () => string
28
+ getValue: () => Array<PortableTextBlock> | undefined
41
29
  onChange: (change: EditorChange) => void
42
- readOnly: boolean
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 {change$, portableTextEditor, onChange, keyGenerator, readOnly, value} = props
52
- const [selection, setSelection] = useState<EditorSelection>(null)
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 = PortableTextEditor.getValue(portableTextEditor)
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, portableTextEditor, change$])
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
  }
@@ -0,0 +1,62 @@
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
+ * Get the current editor selection from the React context.
13
+ */
14
+ export const usePortableTextEditorSelection = (): EditorSelection => {
15
+ const selection = useContext(PortableTextEditorSelectionContext)
16
+
17
+ if (selection === undefined) {
18
+ throw new Error(
19
+ `The \`usePortableTextEditorSelection\` hook must be used inside the <PortableTextEditor> component's context.`,
20
+ )
21
+ }
22
+ return selection
23
+ }
24
+ const debug = debugWithName('component:PortableTextEditor:SelectionProvider')
25
+ const debugVerbose = debug.enabled && false
26
+
27
+ /**
28
+ * @internal
29
+ */
30
+ export function PortableTextEditorSelectionProvider(
31
+ props: React.PropsWithChildren<{
32
+ change$: EditorChanges
33
+ }>,
34
+ ) {
35
+ const {change$} = props
36
+ const [selection, setSelection] = useState<EditorSelection>(null)
37
+
38
+ // Subscribe to, and handle changes from the editor
39
+ useEffect(() => {
40
+ debug('Subscribing to selection changes$')
41
+ const subscription = change$.subscribe((next): void => {
42
+ if (next.type === 'selection') {
43
+ // Set the selection state in a transition, we don't need the state immediately.
44
+ startTransition(() => {
45
+ if (debugVerbose) debug('Setting selection')
46
+ setSelection(next.selection)
47
+ })
48
+ }
49
+ })
50
+
51
+ return () => {
52
+ debug('Unsubscribing to selection changes$')
53
+ subscription.unsubscribe()
54
+ }
55
+ }, [change$])
56
+
57
+ return (
58
+ <PortableTextEditorSelectionContext.Provider value={selection}>
59
+ {props.children}
60
+ </PortableTextEditorSelectionContext.Provider>
61
+ )
62
+ }
@@ -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
- }