@portabletext/editor 0.0.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. package/src/utils/withoutPatching.ts +14 -0
@@ -0,0 +1,190 @@
1
+ import {type PortableTextBlock} from '@sanity/types'
2
+ import {throttle} from 'lodash'
3
+ import {
4
+ type PropsWithChildren,
5
+ startTransition,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import {Editor} from 'slate'
13
+ import {useSlate} from 'slate-react'
14
+
15
+ import {type EditorChange, type EditorChanges, type EditorSelection} from '../../types/editor'
16
+ import {type Patch} from '../../types/patch'
17
+ import {debugWithName} from '../../utils/debug'
18
+ 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'
24
+ import {useSyncValue} from '../hooks/useSyncValue'
25
+ import {PortableTextEditor} from '../PortableTextEditor'
26
+
27
+ const debug = debugWithName('component:PortableTextEditor:Synchronizer')
28
+ const debugVerbose = debug.enabled && false
29
+
30
+ // The editor will commit changes in a throttled fashion in order
31
+ // not to overload the network and degrade performance while typing.
32
+ const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
33
+
34
+ /**
35
+ * @internal
36
+ */
37
+ export interface SynchronizerProps extends PropsWithChildren {
38
+ change$: EditorChanges
39
+ portableTextEditor: PortableTextEditor
40
+ keyGenerator: () => string
41
+ onChange: (change: EditorChange) => void
42
+ readOnly: boolean
43
+ value: PortableTextBlock[] | undefined
44
+ }
45
+
46
+ /**
47
+ * Synchronizes the server value with the editor, and provides various contexts for the editor state.
48
+ * @internal
49
+ */
50
+ export function Synchronizer(props: SynchronizerProps) {
51
+ const {change$, portableTextEditor, onChange, keyGenerator, readOnly, value} = props
52
+ const [selection, setSelection] = useState<EditorSelection>(null)
53
+ const pendingPatches = useRef<Patch[]>([])
54
+
55
+ const syncValue = useSyncValue({
56
+ keyGenerator,
57
+ onChange,
58
+ portableTextEditor,
59
+ readOnly,
60
+ })
61
+
62
+ const slateEditor = useSlate()
63
+
64
+ useEffect(() => {
65
+ IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
66
+ }, [slateEditor])
67
+
68
+ const onFlushPendingPatches = useCallback(() => {
69
+ if (pendingPatches.current.length > 0) {
70
+ debug('Flushing pending patches')
71
+ if (debugVerbose) {
72
+ debug(`Patches:\n${JSON.stringify(pendingPatches.current, null, 2)}`)
73
+ }
74
+ const snapshot = PortableTextEditor.getValue(portableTextEditor)
75
+ change$.next({type: 'mutation', patches: pendingPatches.current, snapshot})
76
+ pendingPatches.current = []
77
+ }
78
+ IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
79
+ }, [slateEditor, portableTextEditor, change$])
80
+
81
+ const onFlushPendingPatchesThrottled = useMemo(() => {
82
+ return throttle(
83
+ () => {
84
+ // If the editor is normalizing (each operation) it means that it's not in the middle of a bigger transform,
85
+ // and we can flush these changes immediately.
86
+ if (Editor.isNormalizing(slateEditor)) {
87
+ onFlushPendingPatches()
88
+ return
89
+ }
90
+ // If it's in the middle of something, try again.
91
+ onFlushPendingPatchesThrottled()
92
+ },
93
+ FLUSH_PATCHES_THROTTLED_MS,
94
+ {
95
+ leading: false,
96
+ trailing: true,
97
+ },
98
+ )
99
+ }, [onFlushPendingPatches, slateEditor])
100
+
101
+ // Flush pending patches immediately on unmount
102
+ useEffect(() => {
103
+ return () => {
104
+ onFlushPendingPatches()
105
+ }
106
+ }, [onFlushPendingPatches])
107
+
108
+ // Subscribe to, and handle changes from the editor
109
+ useEffect(() => {
110
+ debug('Subscribing to editor changes$')
111
+ const sub = change$.subscribe((next: EditorChange): void => {
112
+ switch (next.type) {
113
+ case 'patch':
114
+ IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true)
115
+ pendingPatches.current.push(next.patch)
116
+ onFlushPendingPatchesThrottled()
117
+ onChange(next)
118
+ 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
+ default:
128
+ onChange(next)
129
+ }
130
+ })
131
+ return () => {
132
+ debug('Unsubscribing to changes$')
133
+ sub.unsubscribe()
134
+ }
135
+ }, [change$, onChange, onFlushPendingPatchesThrottled, slateEditor])
136
+
137
+ // Sync the value when going online
138
+ const handleOnline = useCallback(() => {
139
+ debug('Editor is online, syncing from props.value')
140
+ change$.next({type: 'connection', value: 'online'})
141
+ syncValue(value)
142
+ }, [change$, syncValue, value])
143
+
144
+ const handleOffline = useCallback(() => {
145
+ debug('Editor is offline')
146
+ change$.next({type: 'connection', value: 'offline'})
147
+ }, [change$])
148
+
149
+ // Notify about window online and offline status changes
150
+ useEffect(() => {
151
+ if (portableTextEditor.props.patches$) {
152
+ window.addEventListener('online', handleOnline)
153
+ window.addEventListener('offline', handleOffline)
154
+ }
155
+ return () => {
156
+ if (portableTextEditor.props.patches$) {
157
+ window.removeEventListener('online', handleOnline)
158
+ window.removeEventListener('offline', handleOffline)
159
+ }
160
+ }
161
+ })
162
+
163
+ // This hook must be set up after setting up the subscription above, or it will not pick up validation errors from the useSyncValue hook.
164
+ // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value.
165
+ const isInitialValueFromProps = useRef(true)
166
+ useEffect(() => {
167
+ debug('Value from props changed, syncing new value')
168
+ syncValue(value)
169
+ // Signal that we have our first value, and are ready to roll.
170
+ if (isInitialValueFromProps.current) {
171
+ change$.next({type: 'loading', isLoading: false})
172
+ change$.next({type: 'ready'})
173
+ isInitialValueFromProps.current = false
174
+ }
175
+ }, [change$, syncValue, value])
176
+
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
+ )
190
+ }
@@ -0,0 +1,23 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ import {type PortableTextEditor} from '../PortableTextEditor'
4
+
5
+ /**
6
+ * A React context for sharing the editor object.
7
+ */
8
+ export const PortableTextEditorContext = createContext<PortableTextEditor | null>(null)
9
+
10
+ /**
11
+ * Get the current editor object from the React context.
12
+ */
13
+ export const usePortableTextEditor = (): PortableTextEditor => {
14
+ const editor = useContext(PortableTextEditorContext)
15
+
16
+ if (!editor) {
17
+ throw new Error(
18
+ `The \`usePortableTextEditor\` hook must be used inside the <PortableTextEditor> component's context.`,
19
+ )
20
+ }
21
+
22
+ return editor
23
+ }
@@ -0,0 +1,24 @@
1
+ import {randomKey} from '@sanity/util/content'
2
+ import {createContext, useContext} from 'react'
3
+
4
+ export const defaultKeyGenerator = (): string => randomKey(12)
5
+
6
+ /**
7
+ * A React context for sharing the editor's keyGenerator.
8
+ */
9
+ export const PortableTextEditorKeyGeneratorContext =
10
+ createContext<() => string>(defaultKeyGenerator)
11
+
12
+ /**
13
+ * Get the current editor selection from the React context.
14
+ */
15
+ export const usePortableTextEditorKeyGenerator = (): (() => string) => {
16
+ const keyGenerator = useContext(PortableTextEditorKeyGeneratorContext)
17
+
18
+ if (keyGenerator === undefined) {
19
+ throw new Error(
20
+ `The \`usePortableTextEditorKeyGenerator\` hook must be used inside the <PortableTextEditor> component's context.`,
21
+ )
22
+ }
23
+ return keyGenerator
24
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,20 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ /**
4
+ * A React context for sharing the editor's readOnly status.
5
+ */
6
+ export const PortableTextEditorReadOnlyContext = createContext<boolean>(false)
7
+
8
+ /**
9
+ * Get the current editor selection from the React context.
10
+ */
11
+ export const usePortableTextEditorReadOnlyStatus = (): boolean => {
12
+ const readOnly = useContext(PortableTextEditorReadOnlyContext)
13
+
14
+ if (readOnly === undefined) {
15
+ throw new Error(
16
+ `The \`usePortableTextEditorReadOnly\` hook must be used inside the <PortableTextEditor> component's context.`,
17
+ )
18
+ }
19
+ return readOnly
20
+ }
@@ -0,0 +1,125 @@
1
+ import {describe, expect, it, jest} from '@jest/globals'
2
+ import {render, waitFor} from '@testing-library/react'
3
+ import {createRef, type RefObject} from 'react'
4
+
5
+ import {PortableTextEditorTester, schemaType} from '../__tests__/PortableTextEditorTester'
6
+ import {PortableTextEditor} from '../PortableTextEditor'
7
+
8
+ const initialValue = [
9
+ {
10
+ _key: '77071c3af231',
11
+ _type: 'myTestBlockType',
12
+ children: [
13
+ {
14
+ _key: 'c001f0e92c1f0',
15
+ _type: 'span',
16
+ marks: [],
17
+ text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ',
18
+ },
19
+ ],
20
+ markDefs: [],
21
+ style: 'normal',
22
+ },
23
+ ]
24
+
25
+ describe('useSyncValue', () => {
26
+ it('updates span text', async () => {
27
+ const editorRef: RefObject<PortableTextEditor> = createRef()
28
+ const onChange = jest.fn()
29
+ const syncedValue = [
30
+ {
31
+ _key: '77071c3af231',
32
+ _type: 'myTestBlockType',
33
+ children: [
34
+ {
35
+ _key: 'c001f0e92c1f0',
36
+ _type: 'span',
37
+ marks: [],
38
+ text: 'Lorem my ipsum!',
39
+ },
40
+ ],
41
+ markDefs: [],
42
+ style: 'normal',
43
+ },
44
+ ]
45
+ const {rerender} = render(
46
+ <PortableTextEditorTester
47
+ onChange={onChange}
48
+ ref={editorRef}
49
+ schemaType={schemaType}
50
+ value={initialValue}
51
+ />,
52
+ )
53
+ rerender(
54
+ <PortableTextEditorTester
55
+ onChange={onChange}
56
+ ref={editorRef}
57
+ schemaType={schemaType}
58
+ value={syncedValue}
59
+ />,
60
+ )
61
+ await waitFor(() => {
62
+ if (editorRef.current) {
63
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual(syncedValue)
64
+ }
65
+ })
66
+ })
67
+ it('replaces span nodes with different keys inside the same children array', async () => {
68
+ const editorRef: RefObject<PortableTextEditor> = createRef()
69
+ const onChange = jest.fn()
70
+ const syncedValue = [
71
+ {
72
+ _key: '77071c3af231',
73
+ _type: 'myTestBlockType',
74
+ children: [
75
+ {
76
+ _key: 'c001f0e92c1f0__NEW_KEY_YA!',
77
+ _type: 'span',
78
+ marks: [],
79
+ text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ',
80
+ },
81
+ ],
82
+ markDefs: [],
83
+ style: 'normal',
84
+ },
85
+ ]
86
+ const {rerender} = render(
87
+ <PortableTextEditorTester
88
+ onChange={onChange}
89
+ ref={editorRef}
90
+ schemaType={schemaType}
91
+ value={initialValue}
92
+ />,
93
+ )
94
+ rerender(
95
+ <PortableTextEditorTester
96
+ onChange={onChange}
97
+ ref={editorRef}
98
+ schemaType={schemaType}
99
+ value={syncedValue}
100
+ />,
101
+ )
102
+ await waitFor(() => {
103
+ if (editorRef.current) {
104
+ expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(`
105
+ Array [
106
+ Object {
107
+ "_key": "77071c3af231",
108
+ "_type": "myTestBlockType",
109
+ "children": Array [
110
+ Object {
111
+ "_key": "c001f0e92c1f0__NEW_KEY_YA!",
112
+ "_type": "span",
113
+ "marks": Array [],
114
+ "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ",
115
+ },
116
+ ],
117
+ "markDefs": Array [],
118
+ "style": "normal",
119
+ },
120
+ ]
121
+ `)
122
+ }
123
+ })
124
+ })
125
+ })