@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,116 @@
1
+ import {jest} from '@jest/globals'
2
+ import {Schema} from '@sanity/schema'
3
+ import {defineArrayMember, defineField} from '@sanity/types'
4
+ import {type ForwardedRef, forwardRef, useCallback, useEffect, useMemo} from 'react'
5
+
6
+ import {
7
+ PortableTextEditable,
8
+ type PortableTextEditableProps,
9
+ PortableTextEditor,
10
+ type PortableTextEditorProps,
11
+ } from '../../index'
12
+
13
+ const imageType = defineField({
14
+ type: 'image',
15
+ name: 'blockImage',
16
+ })
17
+
18
+ const someObject = defineField({
19
+ type: 'object',
20
+ name: 'someObject',
21
+ fields: [{type: 'string', name: 'color'}],
22
+ })
23
+
24
+ const blockType = defineField({
25
+ type: 'block',
26
+ name: 'myTestBlockType',
27
+ styles: [
28
+ {title: 'Normal', value: 'normal'},
29
+ {title: 'H1', value: 'h1'},
30
+ {title: 'H2', value: 'h2'},
31
+ {title: 'H3', value: 'h3'},
32
+ {title: 'H4', value: 'h4'},
33
+ {title: 'H5', value: 'h5'},
34
+ {title: 'H6', value: 'h6'},
35
+ {title: 'Quote', value: 'blockquote'},
36
+ ],
37
+ of: [someObject, imageType],
38
+ })
39
+
40
+ const portableTextType = defineArrayMember({
41
+ type: 'array',
42
+ name: 'body',
43
+ of: [blockType, someObject],
44
+ })
45
+
46
+ const colorAndLink = defineArrayMember({
47
+ type: 'array',
48
+ name: 'colorAndLink',
49
+ of: [
50
+ {
51
+ ...blockType,
52
+ marks: {
53
+ annotations: [
54
+ {
55
+ name: 'link',
56
+ type: 'object',
57
+ fields: [{type: 'string', name: 'color'}],
58
+ },
59
+ {
60
+ name: 'color',
61
+ type: 'object',
62
+ fields: [{type: 'string', name: 'color'}],
63
+ },
64
+ ],
65
+ },
66
+ },
67
+ ],
68
+ })
69
+
70
+ const schema = Schema.compile({
71
+ name: 'test',
72
+ types: [portableTextType, colorAndLink],
73
+ })
74
+
75
+ let key = 0
76
+
77
+ export const PortableTextEditorTester = forwardRef(function PortableTextEditorTester(
78
+ props: Partial<Omit<PortableTextEditorProps, 'type' | 'onChange' | 'value'>> & {
79
+ onChange?: PortableTextEditorProps['onChange']
80
+ rangeDecorations?: PortableTextEditableProps['rangeDecorations']
81
+ renderPlaceholder?: PortableTextEditableProps['renderPlaceholder']
82
+ schemaType: PortableTextEditorProps['schemaType']
83
+ selection?: PortableTextEditableProps['selection']
84
+ value?: PortableTextEditorProps['value']
85
+ },
86
+ ref: ForwardedRef<PortableTextEditor>,
87
+ ) {
88
+ useEffect(() => {
89
+ key = 0
90
+ }, [])
91
+ const _keyGenerator = useCallback(() => {
92
+ key++
93
+ return `${key}`
94
+ }, [])
95
+ const onChange = useMemo(() => props.onChange || jest.fn(), [props.onChange])
96
+ return (
97
+ <PortableTextEditor
98
+ schemaType={props.schemaType}
99
+ onChange={onChange}
100
+ value={props.value || undefined}
101
+ keyGenerator={_keyGenerator}
102
+ ref={ref}
103
+ >
104
+ <PortableTextEditable
105
+ selection={props.selection || undefined}
106
+ rangeDecorations={props.rangeDecorations}
107
+ renderPlaceholder={props.renderPlaceholder}
108
+ aria-describedby="desc_foo"
109
+ />
110
+ </PortableTextEditor>
111
+ )
112
+ })
113
+
114
+ export const schemaType = schema.get('body')
115
+
116
+ export const schemaTypeWithColorAndLink = schema.get('colorAndLink')
@@ -0,0 +1,115 @@
1
+ import {describe, expect, it, jest} from '@jest/globals'
2
+ /* eslint-disable no-irregular-whitespace */
3
+ import {type PortableTextBlock} from '@sanity/types'
4
+ import {render, waitFor} from '@testing-library/react'
5
+ import {createRef, type ReactNode, type RefObject} from 'react'
6
+
7
+ import {type RangeDecoration} from '../..'
8
+ import {type PortableTextEditor} from '../PortableTextEditor'
9
+ import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester'
10
+
11
+ const helloBlock: PortableTextBlock = {
12
+ _key: '123',
13
+ _type: 'myTestBlockType',
14
+ markDefs: [],
15
+ children: [{_key: '567', _type: 'span', text: 'Hello', marks: []}],
16
+ }
17
+
18
+ let rangeDecorationIteration = 0
19
+
20
+ const RangeDecorationTestComponent = ({children}: {children?: ReactNode}) => {
21
+ rangeDecorationIteration++
22
+ return <span data-testid="range-decoration">{children}</span>
23
+ }
24
+
25
+ describe('RangeDecorations', () => {
26
+ it.only('only render range decorations as necessary', async () => {
27
+ const editorRef: RefObject<PortableTextEditor> = createRef()
28
+ const onChange = jest.fn()
29
+ const value = [helloBlock]
30
+ let rangeDecorations: RangeDecoration[] = [
31
+ {
32
+ component: RangeDecorationTestComponent,
33
+ selection: {
34
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
35
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
36
+ },
37
+ },
38
+ ]
39
+ const {rerender} = render(
40
+ <PortableTextEditorTester
41
+ onChange={onChange}
42
+ rangeDecorations={rangeDecorations}
43
+ ref={editorRef}
44
+ schemaType={schemaType}
45
+ value={value}
46
+ />,
47
+ )
48
+ await waitFor(() => {
49
+ expect([rangeDecorationIteration, 'initial']).toEqual([0, 'initial'])
50
+ })
51
+ // Re-render with the same range decorations
52
+ rerender(
53
+ <PortableTextEditorTester
54
+ onChange={onChange}
55
+ rangeDecorations={rangeDecorations}
56
+ ref={editorRef}
57
+ schemaType={schemaType}
58
+ value={value}
59
+ />,
60
+ )
61
+ await waitFor(() => {
62
+ expect([rangeDecorationIteration, 'initial']).toEqual([0, 'initial'])
63
+ })
64
+ // Update the range decorations, a new object with identical values
65
+ rangeDecorations = [
66
+ {
67
+ component: RangeDecorationTestComponent,
68
+ selection: {
69
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
70
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
71
+ },
72
+ },
73
+ ]
74
+ rerender(
75
+ <PortableTextEditorTester
76
+ onChange={onChange}
77
+ rangeDecorations={rangeDecorations}
78
+ ref={editorRef}
79
+ schemaType={schemaType}
80
+ value={value}
81
+ />,
82
+ )
83
+ await waitFor(() => {
84
+ expect([rangeDecorationIteration, 'updated-with-equal-values']).toEqual([
85
+ 0,
86
+ 'updated-with-equal-values',
87
+ ])
88
+ })
89
+ // Update the range decorations with a new offset
90
+ rangeDecorations = [
91
+ {
92
+ component: RangeDecorationTestComponent,
93
+ selection: {
94
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
95
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 4},
96
+ },
97
+ },
98
+ ]
99
+ rerender(
100
+ <PortableTextEditorTester
101
+ onChange={onChange}
102
+ rangeDecorations={rangeDecorations}
103
+ ref={editorRef}
104
+ schemaType={schemaType}
105
+ value={value}
106
+ />,
107
+ )
108
+ await waitFor(() => {
109
+ expect([rangeDecorationIteration, 'updated-with-different']).toEqual([
110
+ 1,
111
+ 'updated-with-different',
112
+ ])
113
+ })
114
+ })
115
+ })
@@ -0,0 +1,218 @@
1
+ import {describe, expect, it, jest} from '@jest/globals'
2
+ import {fireEvent, render, waitFor} from '@testing-library/react'
3
+ import {createRef, type RefObject} from 'react'
4
+
5
+ import {PortableTextEditor} from '../PortableTextEditor'
6
+ import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester'
7
+ import {getEditableElement} from './utils'
8
+
9
+ describe('adds empty text block if its needed', () => {
10
+ const newBlock = {
11
+ _type: 'myTestBlockType',
12
+ _key: '3',
13
+ style: 'normal',
14
+ markDefs: [],
15
+ children: [
16
+ {
17
+ _type: 'span',
18
+ _key: '2',
19
+ text: '',
20
+ marks: [],
21
+ },
22
+ ],
23
+ }
24
+ it('adds a new block at the bottom, when clicking on the portable text editor, because the only block is void and user is focused on that one', async () => {
25
+ const initialValue = [
26
+ {
27
+ _key: 'b',
28
+ _type: 'someObject',
29
+ },
30
+ ]
31
+
32
+ const initialSelection = {
33
+ focus: {path: [{_key: 'b'}], offset: 0},
34
+ anchor: {path: [{_key: 'b'}], offset: 0},
35
+ }
36
+
37
+ const editorRef: RefObject<PortableTextEditor> = createRef()
38
+ const onChange = jest.fn()
39
+ const component = render(
40
+ <PortableTextEditorTester
41
+ onChange={onChange}
42
+ ref={editorRef}
43
+ schemaType={schemaType}
44
+ value={initialValue}
45
+ />,
46
+ )
47
+ const element = await getEditableElement(component)
48
+
49
+ const editor = editorRef.current
50
+ const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
51
+ await waitFor(async () => {
52
+ if (editor && inlineType && element) {
53
+ PortableTextEditor.focus(editor)
54
+ PortableTextEditor.select(editor, initialSelection)
55
+ fireEvent.click(element)
56
+ expect(PortableTextEditor.getValue(editor)).toEqual([initialValue[0], newBlock])
57
+ }
58
+ })
59
+ })
60
+ it('should not add blocks if the last element is a text block', async () => {
61
+ const initialValue = [
62
+ {
63
+ _key: 'b',
64
+ _type: 'someObject',
65
+ },
66
+ {
67
+ _type: 'myTestBlockType',
68
+ _key: '3',
69
+ style: 'normal',
70
+ markDefs: [],
71
+ children: [
72
+ {
73
+ _type: 'span',
74
+ _key: '2',
75
+ text: '',
76
+ marks: [],
77
+ },
78
+ ],
79
+ },
80
+ ]
81
+
82
+ const initialSelection = {
83
+ focus: {path: [{_key: 'b'}], offset: 0},
84
+ anchor: {path: [{_key: 'b'}], offset: 0},
85
+ }
86
+
87
+ const editorRef: RefObject<PortableTextEditor> = createRef()
88
+ const onChange = jest.fn()
89
+ const component = render(
90
+ <PortableTextEditorTester
91
+ onChange={onChange}
92
+ ref={editorRef}
93
+ schemaType={schemaType}
94
+ value={initialValue}
95
+ />,
96
+ )
97
+ const element = await getEditableElement(component)
98
+
99
+ const editor = editorRef.current
100
+ const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
101
+ await waitFor(async () => {
102
+ if (editor && inlineType && element) {
103
+ PortableTextEditor.focus(editor)
104
+ PortableTextEditor.select(editor, initialSelection)
105
+ fireEvent.click(element)
106
+ expect(PortableTextEditor.getValue(editor)).toEqual(initialValue)
107
+ }
108
+ })
109
+ })
110
+ it('should not add blocks if the last element is void, but its not focused on that one', async () => {
111
+ const initialValue = [
112
+ {
113
+ _key: 'a',
114
+ _type: 'someObject',
115
+ },
116
+ {
117
+ _type: 'myTestBlockType',
118
+ _key: 'b',
119
+ style: 'normal',
120
+ markDefs: [],
121
+ children: [
122
+ {
123
+ _type: 'span',
124
+ _key: 'b1',
125
+ text: '',
126
+ marks: [],
127
+ },
128
+ ],
129
+ },
130
+ {
131
+ _key: 'c',
132
+ _type: 'someObject',
133
+ },
134
+ ]
135
+
136
+ const initialSelection = {
137
+ focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2},
138
+ anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2},
139
+ }
140
+
141
+ const editorRef: RefObject<PortableTextEditor> = createRef()
142
+ const onChange = jest.fn()
143
+ const component = render(
144
+ <PortableTextEditorTester
145
+ onChange={onChange}
146
+ ref={editorRef}
147
+ schemaType={schemaType}
148
+ value={initialValue}
149
+ />,
150
+ )
151
+ const element = await getEditableElement(component)
152
+
153
+ const editor = editorRef.current
154
+ const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
155
+ await waitFor(async () => {
156
+ if (editor && inlineType && element) {
157
+ PortableTextEditor.focus(editor)
158
+ PortableTextEditor.select(editor, initialSelection)
159
+ fireEvent.click(element)
160
+ expect(PortableTextEditor.getValue(editor)).toEqual(initialValue)
161
+ }
162
+ })
163
+ })
164
+ it('should not add blocks if the last element is void, and its focused on that one when clicking', async () => {
165
+ const initialValue = [
166
+ {
167
+ _key: 'a',
168
+ _type: 'someObject',
169
+ },
170
+ {
171
+ _type: 'myTestBlockType',
172
+ _key: 'b',
173
+ style: 'normal',
174
+ markDefs: [],
175
+ children: [
176
+ {
177
+ _type: 'span',
178
+ _key: 'b1',
179
+ text: '',
180
+ marks: [],
181
+ },
182
+ ],
183
+ },
184
+ {
185
+ _key: 'c',
186
+ _type: 'someObject',
187
+ },
188
+ ]
189
+
190
+ const initialSelection = {
191
+ focus: {path: [{_key: 'c'}], offset: 0},
192
+ anchor: {path: [{_key: 'c'}], offset: 0},
193
+ }
194
+
195
+ const editorRef: RefObject<PortableTextEditor> = createRef()
196
+ const onChange = jest.fn()
197
+ const component = render(
198
+ <PortableTextEditorTester
199
+ onChange={onChange}
200
+ ref={editorRef}
201
+ schemaType={schemaType}
202
+ value={initialValue}
203
+ />,
204
+ )
205
+ const element = await getEditableElement(component)
206
+
207
+ const editor = editorRef.current
208
+ const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
209
+ await waitFor(async () => {
210
+ if (editor && inlineType && element) {
211
+ PortableTextEditor.focus(editor)
212
+ PortableTextEditor.select(editor, initialSelection)
213
+ fireEvent.click(element)
214
+ expect(PortableTextEditor.getValue(editor)).toEqual(initialValue.concat(newBlock))
215
+ }
216
+ })
217
+ })
218
+ })