@portabletext/editor 1.48.14 → 1.49.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 (48) hide show
  1. package/lib/_chunks-cjs/editor-provider.cjs +117 -34
  2. package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.slice-blocks.cjs +2 -0
  4. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  5. package/lib/_chunks-es/editor-provider.js +117 -34
  6. package/lib/_chunks-es/editor-provider.js.map +1 -1
  7. package/lib/_chunks-es/util.slice-blocks.js +2 -0
  8. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  9. package/lib/behaviors/index.d.cts +222 -208
  10. package/lib/behaviors/index.d.ts +222 -208
  11. package/lib/index.cjs +346 -257
  12. package/lib/index.cjs.map +1 -1
  13. package/lib/index.d.cts +222 -216
  14. package/lib/index.d.ts +222 -216
  15. package/lib/index.js +353 -264
  16. package/lib/index.js.map +1 -1
  17. package/lib/plugins/index.d.cts +222 -216
  18. package/lib/plugins/index.d.ts +222 -216
  19. package/lib/selectors/index.d.cts +222 -208
  20. package/lib/selectors/index.d.ts +222 -208
  21. package/lib/utils/index.d.cts +222 -208
  22. package/lib/utils/index.d.ts +222 -208
  23. package/package.json +1 -1
  24. package/src/behaviors/behavior.config.ts +7 -0
  25. package/src/behaviors/behavior.core.block-element.ts +108 -0
  26. package/src/behaviors/behavior.core.ts +6 -2
  27. package/src/converters/converter.portable-text.ts +4 -1
  28. package/src/converters/converter.text-html.ts +4 -1
  29. package/src/converters/converter.text-plain.ts +4 -1
  30. package/src/editor/Editable.tsx +2 -4
  31. package/src/editor/__tests__/PortableTextEditor.test.tsx +6 -0
  32. package/src/editor/components/Leaf.tsx +8 -1
  33. package/src/editor/components/render-block-object.tsx +90 -0
  34. package/src/editor/components/render-default-object.tsx +21 -0
  35. package/src/editor/components/render-element.tsx +140 -0
  36. package/src/editor/components/render-inline-object.tsx +93 -0
  37. package/src/editor/components/render-text-block.tsx +148 -0
  38. package/src/editor/components/use-core-block-element-behaviors.ts +39 -0
  39. package/src/editor/create-editor.ts +17 -5
  40. package/src/editor/editor-machine.ts +21 -18
  41. package/src/internal-utils/parse-blocks.ts +2 -2
  42. package/src/internal-utils/slate-utils.ts +1 -1
  43. package/src/priority/priority.core.ts +3 -0
  44. package/src/priority/priority.sort.test.ts +319 -0
  45. package/src/priority/priority.sort.ts +121 -0
  46. package/src/priority/priority.types.ts +24 -0
  47. package/src/editor/components/DefaultObject.tsx +0 -21
  48. package/src/editor/components/Element.tsx +0 -435
@@ -0,0 +1,148 @@
1
+ import type {PortableTextTextBlock} from '@sanity/types'
2
+ import {useSelector} from '@xstate/react'
3
+ import {useContext, useRef, useState, type ReactElement} from 'react'
4
+ import {Range, type Element as SlateElement} from 'slate'
5
+ import {useSelected, useSlateStatic, type RenderElementProps} from 'slate-react'
6
+ import type {EventPositionBlock} from '../../internal-utils/event-position'
7
+ import type {
8
+ RenderBlockFunction,
9
+ RenderListItemFunction,
10
+ RenderStyleFunction,
11
+ } from '../../types/editor'
12
+ import {EditorActorContext} from '../editor-actor-context'
13
+ import {DropIndicator} from './drop-indicator'
14
+ import {useCoreBlockElementBehaviors} from './use-core-block-element-behaviors'
15
+
16
+ export function RenderTextBlock(props: {
17
+ attributes: RenderElementProps['attributes']
18
+ children: ReactElement
19
+ element: SlateElement
20
+ readOnly: boolean
21
+ renderBlock?: RenderBlockFunction
22
+ renderListItem?: RenderListItemFunction
23
+ renderStyle?: RenderStyleFunction
24
+ spellCheck?: boolean
25
+ textBlock: PortableTextTextBlock
26
+ }) {
27
+ const [dragPositionBlock, setDragPositionBlock] =
28
+ useState<EventPositionBlock>()
29
+ const blockRef = useRef<HTMLDivElement>(null)
30
+
31
+ const slateEditor = useSlateStatic()
32
+ const selected = useSelected()
33
+
34
+ const editorActor = useContext(EditorActorContext)
35
+
36
+ useCoreBlockElementBehaviors({
37
+ key: props.element._key,
38
+ onSetDragPositionBlock: setDragPositionBlock,
39
+ })
40
+
41
+ const legacySchema = useSelector(editorActor, (s) =>
42
+ s.context.getLegacySchema(),
43
+ )
44
+
45
+ const focused =
46
+ selected &&
47
+ slateEditor.selection !== null &&
48
+ Range.isCollapsed(slateEditor.selection)
49
+
50
+ let children = props.children
51
+
52
+ const legacyBlockSchemaType = legacySchema.block
53
+
54
+ if (props.renderStyle && props.textBlock.style) {
55
+ const legacyStyleSchemaType =
56
+ props.textBlock.style !== undefined
57
+ ? legacySchema.styles.find(
58
+ (style) => style.value === props.textBlock.style,
59
+ )
60
+ : undefined
61
+
62
+ if (legacyStyleSchemaType) {
63
+ children = props.renderStyle({
64
+ block: props.textBlock,
65
+ children,
66
+ editorElementRef: blockRef,
67
+ focused,
68
+ path: [{_key: props.textBlock._key}],
69
+ schemaType: legacyStyleSchemaType,
70
+ selected,
71
+ value: props.textBlock.style,
72
+ })
73
+ } else {
74
+ console.error(
75
+ `Unable to find Schema type for text block style ${props.textBlock.style}`,
76
+ )
77
+ }
78
+ }
79
+
80
+ if (props.renderListItem && props.textBlock.listItem) {
81
+ const legacyListItemSchemaType = legacySchema.lists.find(
82
+ (list) => list.value === props.textBlock.listItem,
83
+ )
84
+
85
+ if (legacyListItemSchemaType) {
86
+ children = props.renderListItem({
87
+ block: props.textBlock,
88
+ children,
89
+ editorElementRef: blockRef,
90
+ focused,
91
+ level: props.textBlock.level ?? 1,
92
+ path: [{_key: props.textBlock._key}],
93
+ selected,
94
+ value: props.textBlock.listItem,
95
+ schemaType: legacyListItemSchemaType,
96
+ })
97
+ } else {
98
+ console.error(
99
+ `Unable to find Schema type for text block list item ${props.textBlock.listItem}`,
100
+ )
101
+ }
102
+ }
103
+
104
+ return (
105
+ <div
106
+ key={props.element._key}
107
+ {...props.attributes}
108
+ className={[
109
+ 'pt-block',
110
+ 'pt-text-block',
111
+ ...(props.textBlock.style
112
+ ? [`pt-text-block-style-${props.textBlock.style}`]
113
+ : []),
114
+ ...(props.textBlock.listItem
115
+ ? [
116
+ 'pt-list-item',
117
+ `pt-list-item-${props.textBlock.listItem}`,
118
+ `pt-list-item-level-${props.textBlock.level ?? 1}`,
119
+ ]
120
+ : []),
121
+ ].join(' ')}
122
+ spellCheck={props.spellCheck}
123
+ data-block-key={props.textBlock._key}
124
+ data-block-name={props.textBlock._type}
125
+ data-block-type="text"
126
+ >
127
+ {dragPositionBlock === 'start' ? <DropIndicator /> : null}
128
+ <div ref={blockRef}>
129
+ {props.renderBlock
130
+ ? props.renderBlock({
131
+ children,
132
+ editorElementRef: blockRef,
133
+ focused,
134
+ level: props.textBlock.level,
135
+ listItem: props.textBlock.listItem,
136
+ path: [{_key: props.textBlock._key}],
137
+ selected,
138
+ schemaType: legacyBlockSchemaType,
139
+ style: props.textBlock.style,
140
+ type: legacyBlockSchemaType,
141
+ value: props.textBlock,
142
+ })
143
+ : props.children}
144
+ </div>
145
+ {dragPositionBlock === 'end' ? <DropIndicator /> : null}
146
+ </div>
147
+ )
148
+ }
@@ -0,0 +1,39 @@
1
+ import {useContext, useEffect} from 'react'
2
+ import {createCoreBlockElementBehaviorsConfig} from '../../behaviors/behavior.core.block-element'
3
+ import type {EventPositionBlock} from '../../internal-utils/event-position'
4
+ import {EditorActorContext} from '../editor-actor-context'
5
+
6
+ export function useCoreBlockElementBehaviors({
7
+ key,
8
+ onSetDragPositionBlock,
9
+ }: {
10
+ key: string
11
+ onSetDragPositionBlock: (
12
+ eventPositionBlock: EventPositionBlock | undefined,
13
+ ) => void
14
+ }) {
15
+ const editorActor = useContext(EditorActorContext)
16
+
17
+ useEffect(() => {
18
+ const behaviorConfigs = createCoreBlockElementBehaviorsConfig({
19
+ key,
20
+ onSetDragPositionBlock,
21
+ })
22
+
23
+ for (const behaviorConfig of behaviorConfigs) {
24
+ editorActor.send({
25
+ type: 'add behavior',
26
+ behaviorConfig,
27
+ })
28
+ }
29
+
30
+ return () => {
31
+ for (const behaviorConfig of behaviorConfigs) {
32
+ editorActor.send({
33
+ type: 'remove behavior',
34
+ behaviorConfig,
35
+ })
36
+ }
37
+ }
38
+ }, [editorActor, key, onSetDragPositionBlock])
39
+ }
@@ -8,6 +8,8 @@ import type {Behavior} from '../behaviors/behavior.types.behavior'
8
8
  import type {ExternalBehaviorEvent} from '../behaviors/behavior.types.event'
9
9
  import {createCoreConverters} from '../converters/converters.core'
10
10
  import {compileType} from '../internal-utils/schema'
11
+ import {corePriority} from '../priority/priority.core'
12
+ import {createEditorPriority} from '../priority/priority.types'
11
13
  import type {EditableAPI} from '../types/editor'
12
14
  import {createSlateEditor, type SlateEditor} from './create-slate-editor'
13
15
  import type {
@@ -117,23 +119,33 @@ export function createInternalEditor(editorActor: EditorActor): InternalEditor {
117
119
  editorActorSnapshot: editorActor.getSnapshot(),
118
120
  slateEditorInstance: slateEditor.instance,
119
121
  }),
120
- registerBehavior: (config) => {
122
+ registerBehavior: (behaviorConfig) => {
123
+ const priority = createEditorPriority({
124
+ name: 'custom',
125
+ reference: {
126
+ priority: corePriority,
127
+ importance: 'higher',
128
+ },
129
+ })
130
+ const behaviorConfigWithPriority = {
131
+ ...behaviorConfig,
132
+ priority,
133
+ }
134
+
121
135
  editorActor.send({
122
136
  type: 'add behavior',
123
- behavior: config.behavior,
137
+ behaviorConfig: behaviorConfigWithPriority,
124
138
  })
125
139
 
126
140
  return () => {
127
141
  editorActor.send({
128
142
  type: 'remove behavior',
129
- behavior: config.behavior,
143
+ behaviorConfig: behaviorConfigWithPriority,
130
144
  })
131
145
  }
132
146
  },
133
147
  send: (event) => {
134
148
  switch (event.type) {
135
- case 'add behavior':
136
- case 'remove behavior':
137
149
  case 'update key generator':
138
150
  case 'update readOnly':
139
151
  case 'patches':
@@ -11,12 +11,13 @@ import {
11
11
  setup,
12
12
  type ActorRefFrom,
13
13
  } from 'xstate'
14
- import {coreBehaviors} from '../behaviors/behavior.core'
14
+ import type {BehaviorConfig} from '../behaviors/behavior.config'
15
+ import {coreBehaviorsConfig} from '../behaviors/behavior.core'
15
16
  import {performEvent} from '../behaviors/behavior.perform-event'
16
- import type {Behavior} from '../behaviors/behavior.types.behavior'
17
17
  import type {BehaviorEvent} from '../behaviors/behavior.types.event'
18
18
  import type {Converter} from '../converters/converter.types'
19
19
  import type {EventPosition} from '../internal-utils/event-position'
20
+ import {sortByPriority} from '../priority/priority.sort'
20
21
  import type {NamespaceEvent} from '../type-utils'
21
22
  import type {
22
23
  EditorSelection,
@@ -55,14 +56,6 @@ export type MutationEvent = {
55
56
  * @public
56
57
  */
57
58
  export type ExternalEditorEvent =
58
- | {
59
- type: 'add behavior'
60
- behavior: Behavior
61
- }
62
- | {
63
- type: 'remove behavior'
64
- behavior: Behavior
65
- }
66
59
  | {
67
60
  type: 'update readOnly'
68
61
  readOnly: boolean
@@ -160,6 +153,14 @@ export type HasTag = ReturnType<EditorActor['getSnapshot']>['hasTag']
160
153
  */
161
154
  export type InternalEditorEvent =
162
155
  | ExternalEditorEvent
156
+ | {
157
+ type: 'add behavior'
158
+ behaviorConfig: BehaviorConfig
159
+ }
160
+ | {
161
+ type: 'remove behavior'
162
+ behaviorConfig: BehaviorConfig
163
+ }
163
164
  | {
164
165
  type: 'blur'
165
166
  editor: PortableTextSlateEditor
@@ -213,7 +214,7 @@ export type InternalEditorEmittedEvent =
213
214
  export const editorMachine = setup({
214
215
  types: {
215
216
  context: {} as {
216
- behaviors: Set<Behavior>
217
+ behaviors: Set<BehaviorConfig>
217
218
  converters: Set<Converter>
218
219
  getLegacySchema: () => PortableTextMemberSchemaTypes
219
220
  keyGenerator: () => string
@@ -248,14 +249,14 @@ export const editorMachine = setup({
248
249
  behaviors: ({context, event}) => {
249
250
  assertEvent(event, 'add behavior')
250
251
 
251
- return new Set([...context.behaviors, event.behavior])
252
+ return new Set([...context.behaviors, event.behaviorConfig])
252
253
  },
253
254
  }),
254
255
  'remove behavior from context': assign({
255
256
  behaviors: ({context, event}) => {
256
257
  assertEvent(event, 'remove behavior')
257
258
 
258
- context.behaviors.delete(event.behavior)
259
+ context.behaviors.delete(event.behaviorConfig)
259
260
 
260
261
  return new Set([...context.behaviors])
261
262
  },
@@ -342,13 +343,15 @@ export const editorMachine = setup({
342
343
  assertEvent(event, ['behavior event'])
343
344
 
344
345
  try {
346
+ const behaviors = sortByPriority([
347
+ ...context.behaviors.values(),
348
+ ...coreBehaviorsConfig,
349
+ ]).map((config) => config.behavior)
350
+
345
351
  performEvent({
346
352
  mode: 'raise',
347
- behaviors: [...context.behaviors.values(), ...coreBehaviors],
348
- remainingEventBehaviors: [
349
- ...context.behaviors.values(),
350
- ...coreBehaviors,
351
- ],
353
+ behaviors,
354
+ remainingEventBehaviors: behaviors,
352
355
  event: event.behaviorEvent,
353
356
  editor: event.editor,
354
357
  keyGenerator: context.keyGenerator,
@@ -49,7 +49,7 @@ export function parseBlock({
49
49
  )
50
50
  }
51
51
 
52
- function parseBlockObject({
52
+ export function parseBlockObject({
53
53
  blockObject,
54
54
  context,
55
55
  options,
@@ -104,7 +104,7 @@ export function isTextBlock(
104
104
  )
105
105
  }
106
106
 
107
- function parseTextBlock({
107
+ export function parseTextBlock({
108
108
  block,
109
109
  context,
110
110
  options,
@@ -73,7 +73,7 @@ export function getSelectionEndBlock({
73
73
  return getPointBlock({editor, point: selectionEndPoint})
74
74
  }
75
75
 
76
- function getPointBlock({
76
+ export function getPointBlock({
77
77
  editor,
78
78
  point,
79
79
  }: {
@@ -0,0 +1,3 @@
1
+ import {createEditorPriority} from './priority.types'
2
+
3
+ export const corePriority = createEditorPriority({name: 'core'})
@@ -0,0 +1,319 @@
1
+ import {describe, expect, test} from 'vitest'
2
+ import {sortByPriority} from './priority.sort'
3
+ import {createEditorPriority} from './priority.types'
4
+
5
+ describe(sortByPriority.name, () => {
6
+ test('empty array', () => {
7
+ const result = sortByPriority([])
8
+ expect(result).toEqual([])
9
+ })
10
+
11
+ test('single item', () => {
12
+ const item = {priority: createEditorPriority({name: 'single'})}
13
+ const result = sortByPriority([item])
14
+ expect(result).toEqual([item])
15
+ })
16
+
17
+ test('two sets of priorities', () => {
18
+ const a = createEditorPriority({name: 'a'})
19
+ const b = createEditorPriority({
20
+ name: 'b',
21
+ reference: {priority: a, importance: 'higher'},
22
+ })
23
+ const b1 = createEditorPriority({
24
+ name: 'b1',
25
+ reference: {priority: b, importance: 'lower'},
26
+ })
27
+ const b2 = createEditorPriority({
28
+ name: 'b2',
29
+ reference: {priority: b, importance: 'lower'},
30
+ })
31
+ const c = createEditorPriority({
32
+ name: 'c',
33
+ reference: {priority: a, importance: 'higher'},
34
+ })
35
+ const c1 = createEditorPriority({
36
+ name: 'c1',
37
+ reference: {priority: c, importance: 'lower'},
38
+ })
39
+ const c2 = createEditorPriority({
40
+ name: 'c2',
41
+ reference: {priority: c, importance: 'lower'},
42
+ })
43
+
44
+ const items = [
45
+ {priority: c1},
46
+ {priority: c2},
47
+ {priority: a},
48
+ {priority: b1},
49
+ {priority: b2},
50
+ {priority: b},
51
+ {priority: c},
52
+ ]
53
+
54
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
55
+ 'b',
56
+ 'c',
57
+ 'b1',
58
+ 'b2',
59
+ 'c1',
60
+ 'c2',
61
+ 'a',
62
+ ])
63
+ })
64
+
65
+ test('sub-priorities', () => {
66
+ const a = createEditorPriority({name: 'a'})
67
+ const b = createEditorPriority({
68
+ name: 'b',
69
+ reference: {priority: a, importance: 'lower'},
70
+ })
71
+ const b1 = createEditorPriority({
72
+ name: 'b1',
73
+ reference: {priority: b, importance: 'lower'},
74
+ })
75
+ const b2 = createEditorPriority({
76
+ name: 'b2',
77
+ reference: {priority: b1, importance: 'higher'},
78
+ })
79
+
80
+ const items = [{priority: b2}, {priority: b1}, {priority: a}]
81
+
82
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
83
+ 'a',
84
+ 'b2',
85
+ 'b1',
86
+ ])
87
+ })
88
+
89
+ test('direct higher reference', () => {
90
+ const a = createEditorPriority({
91
+ name: 'a',
92
+ })
93
+ const b = createEditorPriority({
94
+ name: 'b',
95
+ reference: {priority: a, importance: 'higher'},
96
+ })
97
+
98
+ const items = [{priority: b}, {priority: a}]
99
+
100
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
101
+ 'b',
102
+ 'a',
103
+ ])
104
+ })
105
+
106
+ test('direct lower reference', () => {
107
+ const a = createEditorPriority({name: 'a'})
108
+ const b = createEditorPriority({
109
+ name: 'b',
110
+ reference: {priority: a, importance: 'lower'},
111
+ })
112
+
113
+ const items = [{priority: b}, {priority: a}]
114
+
115
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
116
+ 'a',
117
+ 'b',
118
+ ])
119
+ })
120
+
121
+ test('transitive references', () => {
122
+ const a = createEditorPriority({name: 'a'})
123
+ const b = createEditorPriority({
124
+ name: 'b',
125
+ reference: {priority: a, importance: 'lower'},
126
+ })
127
+ const c = createEditorPriority({
128
+ name: 'c',
129
+ reference: {priority: b, importance: 'higher'},
130
+ })
131
+
132
+ const items = [{priority: c}, {priority: b}, {priority: a}]
133
+
134
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
135
+ 'a',
136
+ 'c',
137
+ 'b',
138
+ ])
139
+ })
140
+
141
+ test('transitive references #2', () => {
142
+ const a = createEditorPriority({name: 'a'})
143
+ const b = createEditorPriority({
144
+ name: 'b',
145
+ reference: {priority: a, importance: 'higher'},
146
+ })
147
+ const c = createEditorPriority({
148
+ name: 'c',
149
+ reference: {priority: b, importance: 'lower'},
150
+ })
151
+
152
+ const items = [{priority: c}, {priority: b}, {priority: a}]
153
+
154
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
155
+ 'b',
156
+ 'c',
157
+ 'a',
158
+ ])
159
+ })
160
+
161
+ test('references to missing priorities', () => {
162
+ const a = createEditorPriority({name: 'a'})
163
+ const b = createEditorPriority({
164
+ name: 'b',
165
+ reference: {priority: a, importance: 'lower'},
166
+ })
167
+ const c = createEditorPriority({
168
+ name: 'c',
169
+ reference: {priority: b, importance: 'higher'},
170
+ })
171
+ const items = [{priority: a}, {priority: c}]
172
+
173
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
174
+ 'a',
175
+ 'c',
176
+ ])
177
+ })
178
+
179
+ test('references to missing priorities #2', () => {
180
+ const a = createEditorPriority({name: 'a'})
181
+ const b = createEditorPriority({
182
+ name: 'b',
183
+ reference: {priority: a, importance: 'higher'},
184
+ })
185
+ const c = createEditorPriority({
186
+ name: 'c',
187
+ reference: {priority: b, importance: 'lower'},
188
+ })
189
+ const items = [{priority: a}, {priority: c}]
190
+
191
+ expect(sortByPriority(items).map((item) => item.priority.name)).toEqual([
192
+ 'c',
193
+ 'a',
194
+ ])
195
+ })
196
+
197
+ test('complex reference chains', () => {
198
+ const a = createEditorPriority({
199
+ name: 'a',
200
+ })
201
+ const b = createEditorPriority({
202
+ name: 'b',
203
+ reference: {priority: a, importance: 'lower'},
204
+ })
205
+ const c = createEditorPriority({
206
+ name: 'c',
207
+ reference: {priority: b, importance: 'higher'},
208
+ })
209
+ const d = createEditorPriority({
210
+ name: 'd',
211
+ reference: {priority: c, importance: 'lower'},
212
+ })
213
+
214
+ const items = [{priority: d}, {priority: c}, {priority: b}, {priority: a}]
215
+
216
+ const result = sortByPriority(items)
217
+ expect(result.map((item) => item.priority.name)).toEqual([
218
+ 'a',
219
+ 'c',
220
+ 'd',
221
+ 'b',
222
+ ])
223
+ })
224
+
225
+ test('complex reference chains #2', () => {
226
+ const a = createEditorPriority({
227
+ name: 'a',
228
+ })
229
+ const b = createEditorPriority({
230
+ name: 'b',
231
+ reference: {priority: a, importance: 'higher'},
232
+ })
233
+ const c = createEditorPriority({
234
+ name: 'c',
235
+ reference: {priority: b, importance: 'lower'},
236
+ })
237
+ const d = createEditorPriority({
238
+ name: 'd',
239
+ reference: {priority: c, importance: 'higher'},
240
+ })
241
+
242
+ const items = [{priority: d}, {priority: c}, {priority: b}, {priority: a}]
243
+
244
+ const result = sortByPriority(items)
245
+ expect(result.map((item) => item.priority.name)).toEqual([
246
+ 'b',
247
+ 'd',
248
+ 'c',
249
+ 'a',
250
+ ])
251
+ })
252
+
253
+ test('multiple independent chains', () => {
254
+ const a1 = createEditorPriority({name: 'a1'})
255
+ const a2 = createEditorPriority({
256
+ name: 'a2',
257
+ reference: {priority: a1, importance: 'lower'},
258
+ })
259
+
260
+ const b1 = createEditorPriority({name: 'b1'})
261
+ const b2 = createEditorPriority({
262
+ name: 'b2',
263
+ reference: {priority: b1, importance: 'lower'},
264
+ })
265
+
266
+ const items = [
267
+ {priority: a2},
268
+ {priority: b1},
269
+ {priority: a1},
270
+ {priority: b2},
271
+ ]
272
+
273
+ const result = sortByPriority(items)
274
+ const names = result.map((item) => item.priority.name)
275
+
276
+ // Verify that a1 comes before a2 and b1 comes before b2
277
+ expect(names.indexOf('a1')).toBeLessThan(names.indexOf('a2'))
278
+ expect(names.indexOf('b1')).toBeLessThan(names.indexOf('b2'))
279
+ })
280
+
281
+ test('cyclic references', () => {
282
+ const a = createEditorPriority({name: 'a'})
283
+ const b = createEditorPriority({
284
+ name: 'b',
285
+ reference: {priority: a, importance: 'lower'},
286
+ })
287
+ const c = createEditorPriority({
288
+ name: 'c',
289
+ reference: {priority: b, importance: 'lower'},
290
+ })
291
+ a.reference = {priority: c, importance: 'lower'}
292
+
293
+ const items = [{priority: a}, {priority: b}, {priority: c}]
294
+
295
+ expect(() => sortByPriority(items)).toThrow()
296
+ })
297
+
298
+ test('missing priorities', () => {
299
+ const a = createEditorPriority({name: 'a'})
300
+ const b = createEditorPriority({
301
+ name: 'b',
302
+ reference: {priority: a, importance: 'lower'},
303
+ })
304
+
305
+ const items = [
306
+ {name: 'd'},
307
+ {name: 'c'},
308
+ {priority: a, name: 'a'},
309
+ {priority: b, name: 'b'},
310
+ ]
311
+
312
+ expect(sortByPriority(items).map((item) => item.name)).toEqual([
313
+ 'a',
314
+ 'b',
315
+ 'd',
316
+ 'c',
317
+ ])
318
+ })
319
+ })