@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,304 @@
1
+ /* eslint-disable max-statements */
2
+ /* eslint-disable complexity */
3
+ import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types'
4
+ import {isHotkey} from 'is-hotkey-esm'
5
+ import {type KeyboardEvent} from 'react'
6
+ import {Editor, Node, Path, Range, Transforms} from 'slate'
7
+ import {type ReactEditor} from 'slate-react'
8
+
9
+ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
10
+ import {type HotkeyOptions} from '../../types/options'
11
+ import {type SlateTextBlock, type VoidElement} from '../../types/slate'
12
+ import {debugWithName} from '../../utils/debug'
13
+ import {type PortableTextEditor} from '../PortableTextEditor'
14
+
15
+ const debug = debugWithName('plugin:withHotKeys')
16
+
17
+ const DEFAULT_HOTKEYS: HotkeyOptions = {
18
+ marks: {
19
+ 'mod+b': 'strong',
20
+ 'mod+i': 'em',
21
+ 'mod+u': 'underline',
22
+ "mod+'": 'code',
23
+ },
24
+ custom: {},
25
+ }
26
+
27
+ /**
28
+ * This plugin takes care of all hotkeys in the editor
29
+ *
30
+ */
31
+ export function createWithHotkeys(
32
+ types: PortableTextMemberSchemaTypes,
33
+ portableTextEditor: PortableTextEditor,
34
+ hotkeysFromOptions?: HotkeyOptions,
35
+ ): (editor: PortableTextSlateEditor & ReactEditor) => any {
36
+ const reservedHotkeys = ['enter', 'tab', 'shift', 'delete', 'end']
37
+ const activeHotkeys = hotkeysFromOptions || DEFAULT_HOTKEYS // TODO: Merge where possible? A union?
38
+ return function withHotKeys(editor: PortableTextSlateEditor & ReactEditor) {
39
+ editor.pteWithHotKeys = (event: KeyboardEvent<HTMLDivElement>): void => {
40
+ // Wire up custom marks hotkeys
41
+ Object.keys(activeHotkeys).forEach((cat) => {
42
+ if (cat === 'marks') {
43
+ // eslint-disable-next-line guard-for-in
44
+ for (const hotkey in activeHotkeys[cat]) {
45
+ if (reservedHotkeys.includes(hotkey)) {
46
+ throw new Error(`The hotkey ${hotkey} is reserved!`)
47
+ }
48
+ if (isHotkey(hotkey, event.nativeEvent)) {
49
+ event.preventDefault()
50
+ const possibleMark = activeHotkeys[cat]
51
+ if (possibleMark) {
52
+ const mark = possibleMark[hotkey]
53
+ debug(`HotKey ${hotkey} to toggle ${mark}`)
54
+ editor.pteToggleMark(mark)
55
+ }
56
+ }
57
+ }
58
+ }
59
+ if (cat === 'custom') {
60
+ // eslint-disable-next-line guard-for-in
61
+ for (const hotkey in activeHotkeys[cat]) {
62
+ if (reservedHotkeys.includes(hotkey)) {
63
+ throw new Error(`The hotkey ${hotkey} is reserved!`)
64
+ }
65
+ if (isHotkey(hotkey, event.nativeEvent)) {
66
+ const possibleCommand = activeHotkeys[cat]
67
+ if (possibleCommand) {
68
+ const command = possibleCommand[hotkey]
69
+ command(event, portableTextEditor)
70
+ }
71
+ }
72
+ }
73
+ }
74
+ })
75
+
76
+ const isEnter = isHotkey('enter', event.nativeEvent)
77
+ const isTab = isHotkey('tab', event.nativeEvent)
78
+ const isShiftEnter = isHotkey('shift+enter', event.nativeEvent)
79
+ const isShiftTab = isHotkey('shift+tab', event.nativeEvent)
80
+ const isBackspace = isHotkey('backspace', event.nativeEvent)
81
+ const isDelete = isHotkey('delete', event.nativeEvent)
82
+ const isArrowDown = isHotkey('down', event.nativeEvent)
83
+ const isArrowUp = isHotkey('up', event.nativeEvent)
84
+
85
+ // Check if the user is in a void block, in that case, add an empty text block below if there is no next block
86
+ if (isArrowDown && editor.selection) {
87
+ const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
88
+ | SlateTextBlock
89
+ | VoidElement
90
+
91
+ if (focusBlock && Editor.isVoid(editor, focusBlock)) {
92
+ const nextPath = Path.next(editor.selection.focus.path.slice(0, 1))
93
+ const nextBlock = Node.has(editor, nextPath)
94
+ if (!nextBlock) {
95
+ Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: nextPath})
96
+ editor.onChange()
97
+ return
98
+ }
99
+ }
100
+ }
101
+ if (isArrowUp && editor.selection) {
102
+ const isFirstBlock = editor.selection.focus.path[0] === 0
103
+ const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
104
+ | SlateTextBlock
105
+ | VoidElement
106
+
107
+ if (isFirstBlock && focusBlock && Editor.isVoid(editor, focusBlock)) {
108
+ Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: [0]})
109
+ Transforms.select(editor, {path: [0, 0], offset: 0})
110
+ editor.onChange()
111
+ return
112
+ }
113
+ }
114
+ if (
115
+ isBackspace &&
116
+ editor.selection &&
117
+ editor.selection.focus.path[0] === 0 &&
118
+ Range.isCollapsed(editor.selection)
119
+ ) {
120
+ // If the block is text and we have a next block below, remove the current block
121
+ const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
122
+ | SlateTextBlock
123
+ | VoidElement
124
+ const nextPath = Path.next(editor.selection.focus.path.slice(0, 1))
125
+ const nextBlock = Node.has(editor, nextPath)
126
+ const isTextBlock = isPortableTextTextBlock(focusBlock)
127
+ const isEmptyFocusBlock =
128
+ isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === ''
129
+
130
+ if (nextBlock && isTextBlock && isEmptyFocusBlock) {
131
+ // Remove current block
132
+ event.preventDefault()
133
+ event.stopPropagation()
134
+ Transforms.removeNodes(editor, {match: (n) => n === focusBlock})
135
+ editor.onChange()
136
+ return
137
+ }
138
+ }
139
+ // Disallow deleting void blocks by backspace from another line.
140
+ // Otherwise it's so easy to delete the void block above when trying to delete text on
141
+ // the line below or above
142
+ if (
143
+ isBackspace &&
144
+ editor.selection &&
145
+ editor.selection.focus.path[0] > 0 &&
146
+ Range.isCollapsed(editor.selection)
147
+ ) {
148
+ const prevPath = Path.previous(editor.selection.focus.path.slice(0, 1))
149
+ const prevBlock = Node.descendant(editor, prevPath) as SlateTextBlock | VoidElement
150
+ const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1))
151
+ if (
152
+ prevBlock &&
153
+ focusBlock &&
154
+ Editor.isVoid(editor, prevBlock) &&
155
+ editor.selection.focus.offset === 0
156
+ ) {
157
+ debug('Preventing deleting void block above')
158
+ event.preventDefault()
159
+ event.stopPropagation()
160
+
161
+ const isTextBlock = isPortableTextTextBlock(focusBlock)
162
+ const isEmptyFocusBlock =
163
+ isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === ''
164
+
165
+ // If this is a not an text block or it is empty, simply remove it
166
+ if (!isTextBlock || isEmptyFocusBlock) {
167
+ Transforms.removeNodes(editor, {match: (n) => n === focusBlock})
168
+ Transforms.select(editor, prevPath)
169
+
170
+ editor.onChange()
171
+ return
172
+ }
173
+
174
+ // If the focused block is a text node but it isn't empty, focus on the previous block
175
+ if (isTextBlock && !isEmptyFocusBlock) {
176
+ Transforms.select(editor, prevPath)
177
+
178
+ editor.onChange()
179
+ return
180
+ }
181
+
182
+ return
183
+ }
184
+ }
185
+ if (
186
+ isDelete &&
187
+ editor.selection &&
188
+ editor.selection.focus.offset === 0 &&
189
+ Range.isCollapsed(editor.selection) &&
190
+ editor.children[editor.selection.focus.path[0] + 1]
191
+ ) {
192
+ const nextBlock = Node.descendant(
193
+ editor,
194
+ Path.next(editor.selection.focus.path.slice(0, 1)),
195
+ ) as SlateTextBlock | VoidElement
196
+ const focusBlockPath = editor.selection.focus.path.slice(0, 1)
197
+ const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
198
+
199
+ if (
200
+ nextBlock &&
201
+ focusBlock &&
202
+ !Editor.isVoid(editor, focusBlock) &&
203
+ Editor.isVoid(editor, nextBlock)
204
+ ) {
205
+ debug('Preventing deleting void block below')
206
+ event.preventDefault()
207
+ event.stopPropagation()
208
+ Transforms.removeNodes(editor, {match: (n) => n === focusBlock})
209
+ Transforms.select(editor, focusBlockPath)
210
+ editor.onChange()
211
+ return
212
+ }
213
+ }
214
+
215
+ // Tab for lists
216
+ // Only steal tab when we are on a plain text span or we are at the start of the line (fallback if the whole block is annotated or contains a single inline object)
217
+ // Otherwise tab is reserved for accessability for buttons etc.
218
+ if ((isTab || isShiftTab) && editor.selection) {
219
+ const [focusChild] = Editor.node(editor, editor.selection.focus, {depth: 2})
220
+ const [focusBlock] = isPortableTextSpan(focusChild)
221
+ ? Editor.node(editor, editor.selection.focus, {depth: 1})
222
+ : []
223
+ const hasAnnotationFocus =
224
+ focusChild &&
225
+ isPortableTextTextBlock(focusBlock) &&
226
+ isPortableTextSpan(focusChild) &&
227
+ (focusChild.marks || ([] as string[])).filter((m) =>
228
+ (focusBlock.markDefs || []).map((def) => def._key).includes(m),
229
+ ).length > 0
230
+ const [start] = Range.edges(editor.selection)
231
+ const atStartOfNode = Editor.isStart(editor, start, start.path)
232
+
233
+ if (
234
+ focusChild &&
235
+ isPortableTextSpan(focusChild) &&
236
+ (!hasAnnotationFocus || atStartOfNode) &&
237
+ editor.pteIncrementBlockLevels(isShiftTab)
238
+ ) {
239
+ event.preventDefault()
240
+ }
241
+ }
242
+
243
+ // Deal with enter key combos
244
+ if (isEnter && !isShiftEnter && editor.selection) {
245
+ const focusBlockPath = editor.selection.focus.path.slice(0, 1)
246
+ const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
247
+
248
+ // List item enter key
249
+ if (editor.isListBlock(focusBlock)) {
250
+ if (editor.pteEndList()) {
251
+ event.preventDefault()
252
+ }
253
+ return
254
+ }
255
+
256
+ // Enter from another style than the first (default one)
257
+ if (
258
+ editor.isTextBlock(focusBlock) &&
259
+ focusBlock.style &&
260
+ focusBlock.style !== types.styles[0].value
261
+ ) {
262
+ const [, end] = Range.edges(editor.selection)
263
+ const endAtEndOfNode = Editor.isEnd(editor, end, end.path)
264
+ if (endAtEndOfNode) {
265
+ Editor.insertNode(editor, editor.pteCreateEmptyBlock())
266
+ event.preventDefault()
267
+ editor.onChange()
268
+ return
269
+ }
270
+ }
271
+ // Block object enter key
272
+ if (focusBlock && Editor.isVoid(editor, focusBlock)) {
273
+ Editor.insertNode(editor, editor.pteCreateEmptyBlock())
274
+ event.preventDefault()
275
+ editor.onChange()
276
+ return
277
+ }
278
+ // Default enter key behavior
279
+ event.preventDefault()
280
+ editor.insertBreak()
281
+ editor.onChange()
282
+ }
283
+
284
+ // Soft line breaks
285
+ if (isShiftEnter) {
286
+ event.preventDefault()
287
+ editor.insertText('\n')
288
+ return
289
+ }
290
+
291
+ // Undo/redo
292
+ if (isHotkey('mod+z', event.nativeEvent)) {
293
+ event.preventDefault()
294
+ editor.undo()
295
+ return
296
+ }
297
+ if (isHotkey('mod+y', event.nativeEvent) || isHotkey('mod+shift+z', event.nativeEvent)) {
298
+ event.preventDefault()
299
+ editor.redo()
300
+ }
301
+ }
302
+ return editor
303
+ }
304
+ }
@@ -0,0 +1,45 @@
1
+ import {Editor, Node, Path, Range, Transforms} from 'slate'
2
+
3
+ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
4
+ import {type SlateTextBlock, type VoidElement} from '../../types/slate'
5
+ import {isEqualToEmptyEditor} from '../../utils/values'
6
+
7
+ /**
8
+ * Changes default behavior of insertBreak to insert a new block instead of splitting current when the cursor is at the
9
+ * start of the block.
10
+ */
11
+ export function createWithInsertBreak(
12
+ types: PortableTextMemberSchemaTypes,
13
+ ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
14
+ return function withInsertBreak(editor: PortableTextSlateEditor): PortableTextSlateEditor {
15
+ const {insertBreak} = editor
16
+
17
+ editor.insertBreak = () => {
18
+ if (editor.selection) {
19
+ const focusBlockPath = editor.selection.focus.path.slice(0, 1)
20
+ const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
21
+
22
+ if (editor.isTextBlock(focusBlock)) {
23
+ // Enter from another style than the first (default one)
24
+ const [, end] = Range.edges(editor.selection)
25
+ // If it's at the start of block, we want to preserve the current block key and insert a new one in the current position instead of splitting the node.
26
+ const isEndAtStartOfNode = Editor.isStart(editor, end, end.path)
27
+ const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types)
28
+ if (isEndAtStartOfNode && !isEmptyTextBlock) {
29
+ Editor.insertNode(editor, editor.pteCreateEmptyBlock())
30
+ const [nextBlockPath] = Path.next(focusBlockPath)
31
+ Transforms.select(editor, {
32
+ anchor: {path: [nextBlockPath, 0], offset: 0},
33
+ focus: {path: [nextBlockPath, 0], offset: 0},
34
+ })
35
+
36
+ editor.onChange()
37
+ return
38
+ }
39
+ }
40
+ }
41
+ insertBreak()
42
+ }
43
+ return editor
44
+ }
45
+ }