@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,372 @@
1
+ /* eslint-disable max-nested-callbacks */
2
+ import {type PortableTextBlock} from '@sanity/types'
3
+ import {debounce, isEqual} from 'lodash'
4
+ import {useCallback, useMemo, useRef} from 'react'
5
+ import {type Descendant, Editor, type Node, Text, Transforms} from 'slate'
6
+ import {useSlate} from 'slate-react'
7
+
8
+ import {type EditorChange, type PortableTextSlateEditor} from '../../types/editor'
9
+ import {debugWithName} from '../../utils/debug'
10
+ import {validateValue} from '../../utils/validateValue'
11
+ import {toSlateValue, VOID_CHILD_KEY} from '../../utils/values'
12
+ import {isChangingLocally, isChangingRemotely, withRemoteChanges} from '../../utils/withChanges'
13
+ import {withoutPatching} from '../../utils/withoutPatching'
14
+ import {withPreserveKeys} from '../../utils/withPreserveKeys'
15
+ import {withoutSaving} from '../plugins/createWithUndoRedo'
16
+ import {type PortableTextEditor} from '../PortableTextEditor'
17
+
18
+ const debug = debugWithName('hook:useSyncValue')
19
+
20
+ /**
21
+ * @internal
22
+ */
23
+ export interface UseSyncValueProps {
24
+ keyGenerator: () => string
25
+ onChange: (change: EditorChange) => void
26
+ portableTextEditor: PortableTextEditor
27
+ readOnly: boolean
28
+ }
29
+
30
+ const CURRENT_VALUE = new WeakMap<PortableTextEditor, PortableTextBlock[] | undefined>()
31
+
32
+ /**
33
+ * Sync value with the editor state
34
+ *
35
+ * Normally nothing here should apply, and the editor and the real world are perfectly aligned.
36
+ *
37
+ * Inconsistencies could happen though, so we need to check the editor state when the value changes.
38
+ *
39
+ * For performance reasons, it makes sense to also do the content validation here, as we already
40
+ * iterate over the value and can validate only the new content that is actually changed.
41
+ *
42
+ * @internal
43
+ */
44
+ export function useSyncValue(
45
+ props: UseSyncValueProps,
46
+ ): (value: PortableTextBlock[] | undefined, userCallbackFn?: () => void) => void {
47
+ const {portableTextEditor, readOnly, keyGenerator} = props
48
+ const {change$, schemaTypes} = portableTextEditor
49
+ const previousValue = useRef<PortableTextBlock[] | undefined>()
50
+ const slateEditor = useSlate()
51
+ const updateValueFunctionRef = useRef<(value: PortableTextBlock[] | undefined) => void>()
52
+
53
+ const updateFromCurrentValue = useCallback(() => {
54
+ const currentValue = CURRENT_VALUE.get(portableTextEditor)
55
+ if (previousValue.current === currentValue) {
56
+ debug('Value is the same object as previous, not need to sync')
57
+ return
58
+ }
59
+ if (updateValueFunctionRef.current && currentValue) {
60
+ debug('Updating the value debounced')
61
+ updateValueFunctionRef.current(currentValue)
62
+ }
63
+ }, [portableTextEditor])
64
+ const updateValueDebounced = useMemo(
65
+ () => debounce(updateFromCurrentValue, 1000, {trailing: true, leading: false}),
66
+ [updateFromCurrentValue],
67
+ )
68
+
69
+ return useMemo(() => {
70
+ const updateFunction = (value: PortableTextBlock[] | undefined) => {
71
+ CURRENT_VALUE.set(portableTextEditor, value)
72
+ const isProcessingLocalChanges = isChangingLocally(slateEditor)
73
+ const isProcessingRemoteChanges = isChangingRemotely(slateEditor)
74
+ if (!readOnly) {
75
+ if (isProcessingLocalChanges) {
76
+ debug('Has local changes, not syncing value right now')
77
+ updateValueDebounced()
78
+ return
79
+ }
80
+ if (isProcessingRemoteChanges) {
81
+ debug('Has remote changes, not syncing value right now')
82
+ updateValueDebounced()
83
+ return
84
+ }
85
+ }
86
+
87
+ let isChanged = false
88
+ let isValid = true
89
+
90
+ const hadSelection = !!slateEditor.selection
91
+
92
+ // If empty value, remove everything in the editor and insert a placeholder block
93
+ if (!value || value.length === 0) {
94
+ debug('Value is empty')
95
+ Editor.withoutNormalizing(slateEditor, () => {
96
+ withoutSaving(slateEditor, () => {
97
+ withoutPatching(slateEditor, () => {
98
+ if (hadSelection) {
99
+ Transforms.deselect(slateEditor)
100
+ }
101
+ const childrenLength = slateEditor.children.length
102
+ slateEditor.children.forEach((_, index) => {
103
+ Transforms.removeNodes(slateEditor, {
104
+ at: [childrenLength - 1 - index],
105
+ })
106
+ })
107
+ Transforms.insertNodes(slateEditor, slateEditor.pteCreateEmptyBlock(), {at: [0]})
108
+ // Add a new selection in the top of the document
109
+ if (hadSelection) {
110
+ Transforms.select(slateEditor, [0, 0])
111
+ }
112
+ })
113
+ })
114
+ })
115
+ isChanged = true
116
+ }
117
+ // Remove, replace or add nodes according to what is changed.
118
+ if (value && value.length > 0) {
119
+ const slateValueFromProps = toSlateValue(value, {
120
+ schemaTypes,
121
+ })
122
+ Editor.withoutNormalizing(slateEditor, () => {
123
+ withRemoteChanges(slateEditor, () => {
124
+ withoutSaving(slateEditor, () => {
125
+ withoutPatching(slateEditor, () => {
126
+ const childrenLength = slateEditor.children.length
127
+ // Remove blocks that have become superfluous
128
+ if (slateValueFromProps.length < childrenLength) {
129
+ for (let i = childrenLength - 1; i > slateValueFromProps.length - 1; i--) {
130
+ Transforms.removeNodes(slateEditor, {
131
+ at: [i],
132
+ })
133
+ }
134
+ isChanged = true
135
+ }
136
+ // Go through all of the blocks and see if they need to be updated
137
+ slateValueFromProps.forEach((currentBlock, currentBlockIndex) => {
138
+ const oldBlock = slateEditor.children[currentBlockIndex]
139
+ const hasChanges = oldBlock && !isEqual(currentBlock, oldBlock)
140
+ if (hasChanges && isValid) {
141
+ const validationValue = [value[currentBlockIndex]]
142
+ const validation = validateValue(validationValue, schemaTypes, keyGenerator)
143
+ // Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed)
144
+ if (
145
+ !validation.valid &&
146
+ validation.resolution?.autoResolve &&
147
+ validation.resolution?.patches.length > 0
148
+ ) {
149
+ // Only apply auto resolution if the value has been populated before and is different from the last one.
150
+ if (!readOnly && previousValue.current && previousValue.current !== value) {
151
+ // Give a console warning about the fact that it did an auto resolution
152
+ console.warn(
153
+ `${validation.resolution.action} for block with _key '${validationValue[0]._key}'. ${validation.resolution?.description}`,
154
+ )
155
+ validation.resolution.patches.forEach((patch) => {
156
+ change$.next({type: 'patch', patch})
157
+ })
158
+ }
159
+ }
160
+ if (validation.valid || validation.resolution?.autoResolve) {
161
+ if (oldBlock._key === currentBlock._key) {
162
+ if (debug.enabled) debug('Updating block', oldBlock, currentBlock)
163
+ _updateBlock(slateEditor, currentBlock, oldBlock, currentBlockIndex)
164
+ } else {
165
+ if (debug.enabled) debug('Replacing block', oldBlock, currentBlock)
166
+ _replaceBlock(slateEditor, currentBlock, currentBlockIndex)
167
+ }
168
+ isChanged = true
169
+ } else {
170
+ change$.next({
171
+ type: 'invalidValue',
172
+ resolution: validation.resolution,
173
+ value,
174
+ })
175
+ isValid = false
176
+ }
177
+ }
178
+ if (!oldBlock && isValid) {
179
+ const validationValue = [value[currentBlockIndex]]
180
+ const validation = validateValue(validationValue, schemaTypes, keyGenerator)
181
+ if (debug.enabled)
182
+ debug(
183
+ 'Validating and inserting new block in the end of the value',
184
+ currentBlock,
185
+ )
186
+ if (validation.valid || validation.resolution?.autoResolve) {
187
+ withPreserveKeys(slateEditor, () => {
188
+ Transforms.insertNodes(slateEditor, currentBlock, {
189
+ at: [currentBlockIndex],
190
+ })
191
+ })
192
+ } else {
193
+ debug('Invalid', validation)
194
+ change$.next({
195
+ type: 'invalidValue',
196
+ resolution: validation.resolution,
197
+ value,
198
+ })
199
+ isValid = false
200
+ }
201
+ }
202
+ })
203
+ })
204
+ })
205
+ })
206
+ })
207
+ }
208
+
209
+ if (!isValid) {
210
+ debug('Invalid value, returning')
211
+ return
212
+ }
213
+ if (isChanged) {
214
+ debug('Server value changed, syncing editor')
215
+ try {
216
+ slateEditor.onChange()
217
+ } catch (err) {
218
+ console.error(err)
219
+ change$.next({
220
+ type: 'invalidValue',
221
+ resolution: null,
222
+ value,
223
+ })
224
+ return
225
+ }
226
+ if (hadSelection && !slateEditor.selection) {
227
+ Transforms.select(slateEditor, {
228
+ anchor: {path: [0, 0], offset: 0},
229
+ focus: {path: [0, 0], offset: 0},
230
+ })
231
+ slateEditor.onChange()
232
+ }
233
+ change$.next({type: 'value', value})
234
+ } else {
235
+ debug('Server value and editor value is equal, no need to sync.')
236
+ }
237
+ previousValue.current = value
238
+ }
239
+ updateValueFunctionRef.current = updateFunction
240
+ return updateFunction
241
+ }, [
242
+ change$,
243
+ keyGenerator,
244
+ portableTextEditor,
245
+ readOnly,
246
+ schemaTypes,
247
+ slateEditor,
248
+ updateValueDebounced,
249
+ ])
250
+ }
251
+
252
+ /**
253
+ * This code is moved out of the above algorithm to keep complexity down.
254
+ * @internal
255
+ */
256
+ function _replaceBlock(
257
+ slateEditor: PortableTextSlateEditor,
258
+ currentBlock: Descendant,
259
+ currentBlockIndex: number,
260
+ ) {
261
+ // While replacing the block and the current selection focus is on the replaced block,
262
+ // temporarily deselect the editor then optimistically try to restore the selection afterwards.
263
+ const currentSelection = slateEditor.selection
264
+ const selectionFocusOnBlock =
265
+ currentSelection && currentSelection.focus.path[0] === currentBlockIndex
266
+ if (selectionFocusOnBlock) {
267
+ Transforms.deselect(slateEditor)
268
+ }
269
+ Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]})
270
+ withPreserveKeys(slateEditor, () => {
271
+ Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
272
+ })
273
+ slateEditor.onChange()
274
+ if (selectionFocusOnBlock) {
275
+ Transforms.select(slateEditor, currentSelection)
276
+ }
277
+ }
278
+
279
+ /**
280
+ * This code is moved out of the above algorithm to keep complexity down.
281
+ * @internal
282
+ */
283
+ function _updateBlock(
284
+ slateEditor: PortableTextSlateEditor,
285
+ currentBlock: Descendant,
286
+ oldBlock: Descendant,
287
+ currentBlockIndex: number,
288
+ ) {
289
+ // Update the root props on the block
290
+ Transforms.setNodes(slateEditor, currentBlock as Partial<Node>, {
291
+ at: [currentBlockIndex],
292
+ })
293
+ // Text block's need to have their children updated as well (setNode does not target a node's children)
294
+ if (slateEditor.isTextBlock(currentBlock) && slateEditor.isTextBlock(oldBlock)) {
295
+ const oldBlockChildrenLength = oldBlock.children.length
296
+ if (currentBlock.children.length < oldBlockChildrenLength) {
297
+ // Remove any children that have become superfluous
298
+ Array.from(Array(oldBlockChildrenLength - currentBlock.children.length)).forEach(
299
+ (_, index) => {
300
+ const childIndex = oldBlockChildrenLength - 1 - index
301
+ if (childIndex > 0) {
302
+ debug('Removing child')
303
+ Transforms.removeNodes(slateEditor, {
304
+ at: [currentBlockIndex, childIndex],
305
+ })
306
+ }
307
+ },
308
+ )
309
+ }
310
+ currentBlock.children.forEach((currentBlockChild, currentBlockChildIndex) => {
311
+ const oldBlockChild = oldBlock.children[currentBlockChildIndex]
312
+ const isChildChanged = !isEqual(currentBlockChild, oldBlockChild)
313
+ const isTextChanged = !isEqual(currentBlockChild.text, oldBlockChild?.text)
314
+ const path = [currentBlockIndex, currentBlockChildIndex]
315
+ if (isChildChanged) {
316
+ // Update if this is the same child
317
+ if (currentBlockChild._key === oldBlockChild?._key) {
318
+ debug('Updating changed child', currentBlockChild, oldBlockChild)
319
+ Transforms.setNodes(slateEditor, currentBlockChild as Partial<Node>, {
320
+ at: path,
321
+ })
322
+ const isSpanNode =
323
+ Text.isText(currentBlockChild) &&
324
+ currentBlockChild._type === 'span' &&
325
+ Text.isText(oldBlockChild) &&
326
+ oldBlockChild._type === 'span'
327
+ if (isSpanNode && isTextChanged) {
328
+ Transforms.delete(slateEditor, {
329
+ at: {focus: {path, offset: 0}, anchor: {path, offset: oldBlockChild.text.length}},
330
+ })
331
+ Transforms.insertText(slateEditor, currentBlockChild.text, {
332
+ at: path,
333
+ })
334
+ slateEditor.onChange()
335
+ } else if (!isSpanNode) {
336
+ // If it's a inline block, also update the void text node key
337
+ debug('Updating changed inline object child', currentBlockChild)
338
+ Transforms.setNodes(
339
+ slateEditor,
340
+ {_key: VOID_CHILD_KEY},
341
+ {
342
+ at: [...path, 0],
343
+ voids: true,
344
+ },
345
+ )
346
+ }
347
+ // Replace the child if _key's are different
348
+ } else if (oldBlockChild) {
349
+ debug('Replacing child', currentBlockChild)
350
+ Transforms.removeNodes(slateEditor, {
351
+ at: [currentBlockIndex, currentBlockChildIndex],
352
+ })
353
+ withPreserveKeys(slateEditor, () => {
354
+ Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
355
+ at: [currentBlockIndex, currentBlockChildIndex],
356
+ })
357
+ })
358
+ slateEditor.onChange()
359
+ // Insert it if it didn't exist before
360
+ } else if (!oldBlockChild) {
361
+ debug('Inserting new child', currentBlockChild)
362
+ withPreserveKeys(slateEditor, () => {
363
+ Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
364
+ at: [currentBlockIndex, currentBlockChildIndex],
365
+ })
366
+ slateEditor.onChange()
367
+ })
368
+ }
369
+ }
370
+ })
371
+ }
372
+ }
@@ -0,0 +1,16 @@
1
+ import {type PortableTextObject} from '@sanity/types'
2
+ import {type ReactNode, useCallback} from 'react'
3
+
4
+ type Props = {
5
+ annotation: PortableTextObject
6
+ children: ReactNode
7
+ }
8
+ export function DefaultAnnotation(props: Props) {
9
+ // eslint-disable-next-line no-alert
10
+ const handleClick = useCallback(() => alert(JSON.stringify(props.annotation)), [props.annotation])
11
+ return (
12
+ <span style={{color: 'blue'}} onClick={handleClick}>
13
+ {props.children}
14
+ </span>
15
+ )
16
+ }
@@ -0,0 +1,15 @@
1
+ import {type PortableTextBlock, type PortableTextChild} from '@sanity/types'
2
+
3
+ type Props = {
4
+ value: PortableTextBlock | PortableTextChild
5
+ }
6
+
7
+ const DefaultObject = (props: Props): JSX.Element => {
8
+ return (
9
+ <div>
10
+ <pre>{JSON.stringify(props.value, null, 2)}</pre>
11
+ </div>
12
+ )
13
+ }
14
+
15
+ export default DefaultObject
@@ -0,0 +1,189 @@
1
+ import {styled} from 'styled-components'
2
+
3
+ export const DefaultBlockObject = styled.div<{selected: boolean}>`
4
+ user-select: none;
5
+ border: ${(props) => {
6
+ if (props.selected) {
7
+ return '1px solid blue'
8
+ }
9
+ return '1px solid transparent'
10
+ }};
11
+ `
12
+
13
+ export const DefaultInlineObject = styled.span<{selected: boolean}>`
14
+ background: #999;
15
+ border: ${(props) => {
16
+ if (props.selected) {
17
+ return '1px solid blue'
18
+ }
19
+ return '1px solid transparent'
20
+ }};
21
+ `
22
+
23
+ type ListItemProps = {listLevel: number; listStyle: string}
24
+
25
+ export const DefaultListItem = styled.div<ListItemProps>`
26
+ &.pt-list-item {
27
+ width: fit-content;
28
+ position: relative;
29
+ display: block;
30
+
31
+ /* Important 'transform' in order to force refresh the ::before and ::after rules
32
+ in Webkit: https://stackoverflow.com/a/21947628/831480
33
+ */
34
+ transform: translateZ(0);
35
+ margin-left: ${(props: ListItemProps) => getLeftPositionForListLevel(props.listLevel)};
36
+ }
37
+ &.pt-list-item > .pt-list-item-inner {
38
+ display: flex;
39
+ margin: 0;
40
+ padding: 0;
41
+ &:before {
42
+ justify-content: flex-start;
43
+ vertical-align: top;
44
+ }
45
+ }
46
+ &.pt-list-item-bullet > .pt-list-item-inner:before {
47
+ content: '${(props: ListItemProps) =>
48
+ getContentForListLevelAndStyle(props.listLevel, props.listStyle)}';
49
+ font-size: 0.4375rem; /* 7px */
50
+ line-height: 1.5rem; /* Same as body text */
51
+ /* Optical alignment */
52
+ position: relative;
53
+ }
54
+ }
55
+ &.pt-list-item-bullet > .pt-list-item-inner {
56
+ &:before {
57
+ min-width: 1.5rem; /* Make sure space between bullet and text never shrinks */
58
+ }
59
+ }
60
+ &.pt-list-item-number {
61
+ counter-increment: ${(props: {listLevel: number}) =>
62
+ getCounterIncrementForListLevel(props.listLevel)};
63
+ counter-reset: ${(props: {listLevel: number}) => getCounterResetForListLevel(props.listLevel)};
64
+ }
65
+ & + :not(.pt-list-item-number) {
66
+ counter-reset: listItemNumber;
67
+ }
68
+ &.pt-list-item-number > .pt-list-item-inner:before {
69
+ content: ${(props) => getCounterContentForListLevel(props.listLevel)};
70
+ min-width: 1.5rem; /* Make sure space between number and text never shrinks */
71
+ /* Optical alignment */
72
+ position: relative;
73
+ top: 1px;
74
+ }
75
+ `
76
+
77
+ export const DefaultListItemInner = styled.div``
78
+
79
+ function getLeftPositionForListLevel(level: number) {
80
+ switch (Number(level)) {
81
+ case 1:
82
+ return '1.5em'
83
+ case 2:
84
+ return '3em'
85
+ case 3:
86
+ return '4.5em'
87
+ case 4:
88
+ return '6em'
89
+ case 5:
90
+ return '7.5em'
91
+ case 6:
92
+ return '9em'
93
+ case 7:
94
+ return '10.5em'
95
+ case 8:
96
+ return '12em'
97
+ case 9:
98
+ return '13.5em'
99
+ case 10:
100
+ return '15em'
101
+ default:
102
+ return '0em'
103
+ }
104
+ }
105
+
106
+ const bullets = ['●', '○', '■']
107
+
108
+ function getContentForListLevelAndStyle(level: number, style: string) {
109
+ const normalizedLevel = (level - 1) % 3
110
+ if (style === 'bullet') {
111
+ return bullets[normalizedLevel]
112
+ }
113
+ return '*'
114
+ }
115
+
116
+ function getCounterIncrementForListLevel(level: number) {
117
+ switch (level) {
118
+ case 1:
119
+ return 'listItemNumber'
120
+ case 2:
121
+ return 'listItemAlpha'
122
+ case 3:
123
+ return 'listItemRoman'
124
+ case 4:
125
+ return 'listItemNumberNext'
126
+ case 5:
127
+ return 'listItemLetterNext'
128
+ case 6:
129
+ return 'listItemRomanNext'
130
+ case 7:
131
+ return 'listItemNumberNextNext'
132
+ case 8:
133
+ return 'listItemAlphaNextNext'
134
+ case 9:
135
+ return 'listItemRomanNextNext'
136
+ default:
137
+ return 'listItemNumberNextNextNext'
138
+ }
139
+ }
140
+
141
+ function getCounterResetForListLevel(level: number) {
142
+ switch (level) {
143
+ case 1:
144
+ return 'listItemAlpha'
145
+ case 2:
146
+ return 'listItemRoman'
147
+ case 3:
148
+ return 'listItemNumberNext'
149
+ case 4:
150
+ return 'listItemLetterNext'
151
+ case 5:
152
+ return 'listItemRomanNext'
153
+ case 6:
154
+ return 'listItemNumberNextNext'
155
+ case 7:
156
+ return 'listItemAlphaNextNext'
157
+ case 8:
158
+ return 'listItemRomanNextNext'
159
+ case 9:
160
+ return 'listItemNumberNextNextNext'
161
+ default:
162
+ return 'listItemNumberNextNextNext'
163
+ }
164
+ }
165
+
166
+ function getCounterContentForListLevel(level: number) {
167
+ switch (level) {
168
+ case 1:
169
+ return `counter(listItemNumber) '. '`
170
+ case 2:
171
+ return `counter(listItemAlpha, lower-alpha) '. '`
172
+ case 3:
173
+ return `counter(listItemRoman, lower-roman) '. '`
174
+ case 4:
175
+ return `counter(listItemNumberNext) '. '`
176
+ case 5:
177
+ return `counter(listItemLetterNext, lower-alpha) '. '`
178
+ case 6:
179
+ return `counter(listItemRomanNext, lower-roman) '. '`
180
+ case 7:
181
+ return `counter(listItemNumberNextNext) '. '`
182
+ case 8:
183
+ return `counter(listItemAlphaNextNext, lower-alpha) '. '`
184
+ case 9:
185
+ return `counter(listItemRomanNextNext, lower-roman) '. '`
186
+ default:
187
+ return `counter(listItemNumberNextNextNext) '. '`
188
+ }
189
+ }