@gtkx/components 0.21.0 → 1.0.0-rc.2

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 (192) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +173 -0
  3. package/dist/adw/index.d.ts +3 -0
  4. package/dist/adw/index.d.ts.map +1 -0
  5. package/dist/adw/index.js +2 -0
  6. package/dist/adw/index.js.map +1 -0
  7. package/dist/adw/toast.d.ts +10 -0
  8. package/dist/adw/toast.d.ts.map +1 -0
  9. package/dist/adw/toast.js +51 -0
  10. package/dist/adw/toast.js.map +1 -0
  11. package/dist/adw/types.d.ts +28 -0
  12. package/dist/adw/types.d.ts.map +1 -0
  13. package/dist/adw/types.js +2 -0
  14. package/dist/adw/types.js.map +1 -0
  15. package/dist/column-view.d.ts +6 -14
  16. package/dist/column-view.d.ts.map +1 -1
  17. package/dist/column-view.js +98 -100
  18. package/dist/column-view.js.map +1 -1
  19. package/dist/drop-down.d.ts +8 -37
  20. package/dist/drop-down.d.ts.map +1 -1
  21. package/dist/drop-down.js +109 -118
  22. package/dist/drop-down.js.map +1 -1
  23. package/dist/grid-view.d.ts +4 -11
  24. package/dist/grid-view.d.ts.map +1 -1
  25. package/dist/grid-view.js +25 -19
  26. package/dist/grid-view.js.map +1 -1
  27. package/dist/index.d.ts +6 -18
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -14
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal/cells.d.ts +50 -0
  32. package/dist/internal/cells.d.ts.map +1 -0
  33. package/dist/internal/cells.js +185 -0
  34. package/dist/internal/cells.js.map +1 -0
  35. package/dist/internal/collection-index.d.ts +21 -0
  36. package/dist/internal/collection-index.d.ts.map +1 -0
  37. package/dist/internal/collection-index.js +106 -0
  38. package/dist/internal/collection-index.js.map +1 -0
  39. package/dist/internal/collection-model.d.ts +12 -0
  40. package/dist/internal/collection-model.d.ts.map +1 -0
  41. package/dist/internal/collection-model.js +164 -0
  42. package/dist/internal/collection-model.js.map +1 -0
  43. package/dist/internal/collection.d.ts +18 -0
  44. package/dist/internal/collection.d.ts.map +1 -0
  45. package/dist/internal/collection.js +64 -0
  46. package/dist/internal/collection.js.map +1 -0
  47. package/dist/internal/expansion.d.ts +9 -0
  48. package/dist/internal/expansion.d.ts.map +1 -0
  49. package/dist/internal/expansion.js +75 -0
  50. package/dist/internal/expansion.js.map +1 -0
  51. package/dist/internal/selection.d.ts +12 -0
  52. package/dist/internal/selection.d.ts.map +1 -0
  53. package/dist/internal/selection.js +99 -0
  54. package/dist/internal/selection.js.map +1 -0
  55. package/dist/internal/use-collection.d.ts +18 -0
  56. package/dist/internal/use-collection.d.ts.map +1 -0
  57. package/dist/internal/use-collection.js +42 -0
  58. package/dist/internal/use-collection.js.map +1 -0
  59. package/dist/internal/use-widget-ref.d.ts +4 -0
  60. package/dist/internal/use-widget-ref.d.ts.map +1 -0
  61. package/dist/internal/use-widget-ref.js +8 -0
  62. package/dist/internal/use-widget-ref.js.map +1 -0
  63. package/dist/list-view.d.ts +4 -11
  64. package/dist/list-view.d.ts.map +1 -1
  65. package/dist/list-view.js +32 -28
  66. package/dist/list-view.js.map +1 -1
  67. package/dist/size-group.d.ts +12 -0
  68. package/dist/size-group.d.ts.map +1 -0
  69. package/dist/size-group.js +41 -0
  70. package/dist/size-group.js.map +1 -0
  71. package/dist/types.d.ts +104 -161
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js.map +1 -1
  74. package/package.json +62 -52
  75. package/src/adw/index.ts +2 -0
  76. package/src/adw/toast.tsx +74 -0
  77. package/src/adw/types.ts +32 -0
  78. package/src/column-view.tsx +184 -184
  79. package/src/drop-down.tsx +173 -227
  80. package/src/grid-view.tsx +29 -65
  81. package/src/index.ts +16 -25
  82. package/src/internal/cells.tsx +345 -0
  83. package/src/internal/collection-index.ts +168 -0
  84. package/src/internal/collection-model.ts +246 -0
  85. package/src/internal/collection.ts +104 -0
  86. package/src/internal/expansion.ts +123 -0
  87. package/src/internal/selection.tsx +159 -0
  88. package/src/internal/use-collection.tsx +70 -0
  89. package/src/internal/use-widget-ref.ts +11 -0
  90. package/src/list-view.tsx +45 -78
  91. package/src/size-group.tsx +77 -0
  92. package/src/types.ts +171 -191
  93. package/dist/collection-view.d.ts +0 -30
  94. package/dist/collection-view.d.ts.map +0 -1
  95. package/dist/collection-view.js +0 -52
  96. package/dist/collection-view.js.map +0 -1
  97. package/dist/column-view-column.d.ts +0 -17
  98. package/dist/column-view-column.d.ts.map +0 -1
  99. package/dist/column-view-column.js +0 -51
  100. package/dist/column-view-column.js.map +0 -1
  101. package/dist/constraint-layout-apply.d.ts +0 -6
  102. package/dist/constraint-layout-apply.d.ts.map +0 -1
  103. package/dist/constraint-layout-apply.js +0 -107
  104. package/dist/constraint-layout-apply.js.map +0 -1
  105. package/dist/constraint-layout.d.ts +0 -13
  106. package/dist/constraint-layout.d.ts.map +0 -1
  107. package/dist/constraint-layout.js +0 -85
  108. package/dist/constraint-layout.js.map +0 -1
  109. package/dist/contexts/column-view-context.d.ts +0 -17
  110. package/dist/contexts/column-view-context.d.ts.map +0 -1
  111. package/dist/contexts/column-view-context.js +0 -10
  112. package/dist/contexts/column-view-context.js.map +0 -1
  113. package/dist/hooks/use-drop-down-selection.d.ts +0 -15
  114. package/dist/hooks/use-drop-down-selection.d.ts.map +0 -1
  115. package/dist/hooks/use-drop-down-selection.js +0 -35
  116. package/dist/hooks/use-drop-down-selection.js.map +0 -1
  117. package/dist/hooks/use-header-menu.d.ts +0 -4
  118. package/dist/hooks/use-header-menu.d.ts.map +0 -1
  119. package/dist/hooks/use-header-menu.js +0 -32
  120. package/dist/hooks/use-header-menu.js.map +0 -1
  121. package/dist/hooks/use-list-model.d.ts +0 -20
  122. package/dist/hooks/use-list-model.d.ts.map +0 -1
  123. package/dist/hooks/use-list-model.js +0 -70
  124. package/dist/hooks/use-list-model.js.map +0 -1
  125. package/dist/hooks/use-model-installation.d.ts +0 -9
  126. package/dist/hooks/use-model-installation.d.ts.map +0 -1
  127. package/dist/hooks/use-model-installation.js +0 -20
  128. package/dist/hooks/use-model-installation.js.map +0 -1
  129. package/dist/hooks/use-realized-slots.d.ts +0 -20
  130. package/dist/hooks/use-realized-slots.d.ts.map +0 -1
  131. package/dist/hooks/use-realized-slots.js +0 -66
  132. package/dist/hooks/use-realized-slots.js.map +0 -1
  133. package/dist/hooks/use-selection-model.d.ts +0 -12
  134. package/dist/hooks/use-selection-model.d.ts.map +0 -1
  135. package/dist/hooks/use-selection-model.js +0 -111
  136. package/dist/hooks/use-selection-model.js.map +0 -1
  137. package/dist/hooks/use-sort-handler.d.ts +0 -12
  138. package/dist/hooks/use-sort-handler.d.ts.map +0 -1
  139. package/dist/hooks/use-sort-handler.js +0 -87
  140. package/dist/hooks/use-sort-handler.js.map +0 -1
  141. package/dist/list-portal-host.d.ts +0 -11
  142. package/dist/list-portal-host.d.ts.map +0 -1
  143. package/dist/list-portal-host.js +0 -8
  144. package/dist/list-portal-host.js.map +0 -1
  145. package/dist/list-slot.d.ts +0 -14
  146. package/dist/list-slot.d.ts.map +0 -1
  147. package/dist/list-slot.js +0 -29
  148. package/dist/list-slot.js.map +0 -1
  149. package/dist/menu-items.d.ts +0 -5
  150. package/dist/menu-items.d.ts.map +0 -1
  151. package/dist/menu-items.js +0 -48
  152. package/dist/menu-items.js.map +0 -1
  153. package/dist/menu.d.ts +0 -18
  154. package/dist/menu.d.ts.map +0 -1
  155. package/dist/menu.js +0 -26
  156. package/dist/menu.js.map +0 -1
  157. package/dist/utils/item-resolver.d.ts +0 -28
  158. package/dist/utils/item-resolver.d.ts.map +0 -1
  159. package/dist/utils/item-resolver.js +0 -111
  160. package/dist/utils/item-resolver.js.map +0 -1
  161. package/dist/utils/list-item-flatten.d.ts +0 -32
  162. package/dist/utils/list-item-flatten.d.ts.map +0 -1
  163. package/dist/utils/list-item-flatten.js +0 -88
  164. package/dist/utils/list-item-flatten.js.map +0 -1
  165. package/dist/utils/position-only-model.d.ts +0 -10
  166. package/dist/utils/position-only-model.d.ts.map +0 -1
  167. package/dist/utils/position-only-model.js +0 -83
  168. package/dist/utils/position-only-model.js.map +0 -1
  169. package/dist/utils/realized-slot-store.d.ts +0 -30
  170. package/dist/utils/realized-slot-store.d.ts.map +0 -1
  171. package/dist/utils/realized-slot-store.js +0 -103
  172. package/dist/utils/realized-slot-store.js.map +0 -1
  173. package/src/collection-view.tsx +0 -125
  174. package/src/column-view-column.tsx +0 -73
  175. package/src/constraint-layout-apply.ts +0 -121
  176. package/src/constraint-layout.tsx +0 -130
  177. package/src/contexts/column-view-context.ts +0 -28
  178. package/src/hooks/use-drop-down-selection.ts +0 -53
  179. package/src/hooks/use-header-menu.ts +0 -33
  180. package/src/hooks/use-list-model.ts +0 -145
  181. package/src/hooks/use-model-installation.ts +0 -25
  182. package/src/hooks/use-realized-slots.ts +0 -93
  183. package/src/hooks/use-selection-model.ts +0 -121
  184. package/src/hooks/use-sort-handler.ts +0 -102
  185. package/src/list-portal-host.tsx +0 -27
  186. package/src/list-slot.tsx +0 -56
  187. package/src/menu-items.ts +0 -48
  188. package/src/menu.tsx +0 -34
  189. package/src/utils/item-resolver.ts +0 -143
  190. package/src/utils/list-item-flatten.ts +0 -113
  191. package/src/utils/position-only-model.ts +0 -115
  192. package/src/utils/realized-slot-store.ts +0 -126
@@ -0,0 +1,345 @@
1
+ import type * as GObject from "@gtkx/gi/gobject";
2
+ import type { ReactNode } from "react";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import { GtkTreeExpander } from "@gtkx/jsx/gtk";
5
+ import { createPortal, useProperty } from "@gtkx/react";
6
+ import { memo, useLayoutEffect, useState, useSyncExternalStore } from "react";
7
+ import type { HeaderRenderer, Item, ItemRenderer, RenderItemArgs } from "../types.js";
8
+ import type { Collection } from "./collection.js";
9
+ import { getId } from "./collection-model.js";
10
+
11
+ type CellSize = {
12
+ width: number;
13
+ height: number;
14
+ };
15
+
16
+ type CellHost = GObject.Object & {
17
+ getChild: () => Gtk.Widget | null;
18
+ setChild: (child: Gtk.Widget | null) => void;
19
+ };
20
+
21
+ type CellEntry<C extends CellHost> = {
22
+ key: string;
23
+ cell: C;
24
+ item: GObject.Object | null;
25
+ listeners: Set<() => void>;
26
+ subscribe: (onChange: () => void) => () => void;
27
+ getItem: () => GObject.Object | null;
28
+ };
29
+
30
+ type FactoryHandlers = {
31
+ onSetup: (cell: GObject.Object) => void;
32
+ onBind: (cell: GObject.Object) => void;
33
+ onUnbind: (cell: GObject.Object) => void;
34
+ onTeardown: (cell: GObject.Object) => void;
35
+ };
36
+
37
+ type CellStore<C extends CellHost> = {
38
+ handlers: FactoryHandlers;
39
+ setSize: (size: CellSize) => void;
40
+ subscribe: (onChange: () => void) => () => void;
41
+ getSnapshot: () => CellEntry<C>[];
42
+ };
43
+
44
+ type StoreState<C extends CellHost> = {
45
+ size: CellSize;
46
+ entries: Map<C, CellEntry<C>>;
47
+ snapshot: CellEntry<C>[] | null;
48
+ listeners: Set<() => void>;
49
+ serial: number;
50
+ };
51
+
52
+ type CellGuard<C extends CellHost> = (value: GObject.Object) => value is C;
53
+
54
+ type ItemBodyOptions = {
55
+ item: GObject.Object | null;
56
+ position: number | undefined;
57
+ row: Gtk.TreeListRow | null;
58
+ isRowExpanded: boolean | undefined;
59
+ render: ItemRenderer<never>;
60
+ collection: Collection;
61
+ expandedIds: string[] | null | undefined;
62
+ };
63
+
64
+ type ItemCellProps = {
65
+ entry: CellEntry<Gtk.ListItem>;
66
+ render: ItemRenderer<never>;
67
+ collection: Collection;
68
+ expandedIds: string[] | null | undefined;
69
+ };
70
+
71
+ type ItemPortalsProps = {
72
+ store: CellStore<Gtk.ListItem>;
73
+ render: ItemRenderer<never>;
74
+ collection: Collection;
75
+ expandedIds?: string[] | null | undefined;
76
+ };
77
+
78
+ type HeaderCellProps = {
79
+ entry: CellEntry<Gtk.ListHeader>;
80
+ render: HeaderRenderer<never>;
81
+ collection: Collection;
82
+ };
83
+
84
+ type HeaderPortalsProps = {
85
+ store: CellStore<Gtk.ListHeader>;
86
+ render: HeaderRenderer<never>;
87
+ collection: Collection;
88
+ };
89
+
90
+ const ItemCell = memo(ItemCellImpl);
91
+ const HeaderCell = memo(HeaderCellImpl);
92
+
93
+ const isListItem = (value: GObject.Object): value is Gtk.ListItem => value instanceof Gtk.ListItem;
94
+ const isListHeader = (value: GObject.Object): value is Gtk.ListHeader => value instanceof Gtk.ListHeader;
95
+
96
+ const placeholder = (size: CellSize): Gtk.Widget =>
97
+ new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
98
+
99
+ function notifyEntry<C extends CellHost>(entry: CellEntry<C>): void {
100
+ for (const listener of entry.listeners) {
101
+ listener();
102
+ }
103
+ }
104
+
105
+ function notifyRoster<C extends CellHost>(state: StoreState<C>): void {
106
+ state.snapshot = null;
107
+
108
+ for (const listener of state.listeners) {
109
+ listener();
110
+ }
111
+ }
112
+
113
+ function createEntry<C extends CellHost>(state: StoreState<C>, cell: C): CellEntry<C> {
114
+ state.serial += 1;
115
+
116
+ const entry: CellEntry<C> = {
117
+ key: `gtkx-cell-${String(state.serial)}`,
118
+ cell,
119
+ item: null,
120
+ listeners: new Set(),
121
+ subscribe: (onChange) => {
122
+ entry.listeners.add(onChange);
123
+
124
+ return () => {
125
+ entry.listeners.delete(onChange);
126
+ };
127
+ },
128
+ getItem: () => entry.item,
129
+ };
130
+
131
+ return entry;
132
+ }
133
+
134
+ function setupCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
135
+ cell.setChild(placeholder(state.size));
136
+ state.entries.set(cell, createEntry(state, cell));
137
+ notifyRoster(state);
138
+ }
139
+
140
+ function teardownCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
141
+ if (state.entries.delete(cell)) {
142
+ notifyRoster(state);
143
+ }
144
+ }
145
+
146
+ function writeBinding<C extends CellHost>(state: StoreState<C>, cell: C, item: GObject.Object | null): void {
147
+ const entry = state.entries.get(cell);
148
+
149
+ if (entry === undefined) {
150
+ return;
151
+ }
152
+
153
+ entry.item = item;
154
+ notifyEntry(entry);
155
+ }
156
+
157
+ function boundItem(cell: CellHost): GObject.Object | null {
158
+ if (cell instanceof Gtk.ListItem) {
159
+ return cell.getItem();
160
+ }
161
+
162
+ return cell instanceof Gtk.ListHeader ? cell.getItem() : null;
163
+ }
164
+
165
+ function bindCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
166
+ if (cell.getChild() === null) {
167
+ cell.setChild(placeholder(state.size));
168
+ }
169
+
170
+ writeBinding(state, cell, boundItem(cell));
171
+ }
172
+
173
+ function subscribeRoster<C extends CellHost>(state: StoreState<C>, onChange: () => void): () => void {
174
+ state.listeners.add(onChange);
175
+
176
+ return () => {
177
+ state.listeners.delete(onChange);
178
+ };
179
+ }
180
+
181
+ function rosterSnapshot<C extends CellHost>(state: StoreState<C>): CellEntry<C>[] {
182
+ state.snapshot ??= state.entries.values().toArray();
183
+
184
+ return state.snapshot;
185
+ }
186
+
187
+ function guarded<C extends CellHost>(isCell: CellGuard<C>, run: (cell: C) => void): (cell: GObject.Object) => void {
188
+ return (cell) => {
189
+ if (isCell(cell)) {
190
+ run(cell);
191
+ }
192
+ };
193
+ }
194
+
195
+ function createHandlers<C extends CellHost>(state: StoreState<C>, isCell: CellGuard<C>): FactoryHandlers {
196
+ return {
197
+ onSetup: guarded(isCell, (cell) => {
198
+ setupCell(state, cell);
199
+ }),
200
+ onBind: guarded(isCell, (cell) => {
201
+ bindCell(state, cell);
202
+ }),
203
+ onUnbind: guarded(isCell, (cell) => {
204
+ writeBinding(state, cell, null);
205
+ }),
206
+ onTeardown: guarded(isCell, (cell) => {
207
+ teardownCell(state, cell);
208
+ }),
209
+ };
210
+ }
211
+
212
+ function createCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
213
+ const state: StoreState<C> = {
214
+ size,
215
+ entries: new Map(),
216
+ snapshot: null,
217
+ listeners: new Set(),
218
+ serial: 0,
219
+ };
220
+
221
+ return {
222
+ handlers: createHandlers(state, isCell),
223
+ setSize: (next) => {
224
+ state.size = next;
225
+ },
226
+ subscribe: (onChange) => subscribeRoster(state, onChange),
227
+ getSnapshot: () => rosterSnapshot(state),
228
+ };
229
+ }
230
+
231
+ function useCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
232
+ const [store] = useState(() => createCellStore(isCell, size));
233
+
234
+ useLayoutEffect(() => {
235
+ store.setSize(size);
236
+ });
237
+
238
+ return store;
239
+ }
240
+
241
+ function useItemCells(size: CellSize): CellStore<Gtk.ListItem> {
242
+ return useCellStore(isListItem, size);
243
+ }
244
+
245
+ function useHeaderCells(size: CellSize): CellStore<Gtk.ListHeader> {
246
+ return useCellStore(isListHeader, size);
247
+ }
248
+
249
+ function isRowWanted(options: ItemBodyOptions, item: Item): boolean {
250
+ const { expandedIds } = options;
251
+
252
+ return expandedIds == null ? options.isRowExpanded === true : expandedIds.includes(item.id);
253
+ }
254
+
255
+ function itemArgs(item: Item, options: ItemBodyOptions): RenderItemArgs<unknown> {
256
+ const args: RenderItemArgs<unknown> = { item: item.value, index: options.position ?? 0 };
257
+ const row = options.row;
258
+
259
+ if (row === null) {
260
+ return args;
261
+ }
262
+
263
+ args.depth = row.getDepth();
264
+
265
+ if (row.isExpandable()) {
266
+ args.isExpanded = isRowWanted(options, item);
267
+ }
268
+
269
+ return args;
270
+ }
271
+
272
+ function wrapExpander(item: Item, row: Gtk.TreeListRow | null, content: ReactNode): ReactNode {
273
+ const expander: ReactNode =
274
+ row === null
275
+ ? (
276
+ content
277
+ )
278
+ : (
279
+ <GtkTreeExpander
280
+ listRow={row}
281
+ hideExpander={item.hideExpander ?? false}
282
+ indentForDepth={item.indentForDepth ?? true}
283
+ indentForIcon={item.indentForIcon ?? true}
284
+ >
285
+ {content}
286
+ </GtkTreeExpander>
287
+ );
288
+
289
+ return expander;
290
+ }
291
+
292
+ function itemBody(options: ItemBodyOptions): ReactNode {
293
+ const id = getId(options.item);
294
+ const item = id === null ? undefined : options.collection.itemFor(id);
295
+
296
+ if (item === undefined) {
297
+ return null;
298
+ }
299
+
300
+ const render = options.render as ItemRenderer<unknown>;
301
+
302
+ return wrapExpander(item, options.row, render(itemArgs(item, options)));
303
+ }
304
+
305
+ function ItemCellImpl({ entry, render, collection, expandedIds }: ItemCellProps): ReactNode {
306
+ const item = useSyncExternalStore(entry.subscribe, entry.getItem);
307
+ const position = useProperty(entry.cell, "position");
308
+ const row = item instanceof Gtk.TreeListRow ? item : null;
309
+ const isRowExpanded = useProperty(row, "expanded");
310
+ const body = itemBody({ item, position, row, isRowExpanded, render, collection, expandedIds });
311
+
312
+ return createPortal(body, entry.cell, entry.key);
313
+ }
314
+
315
+ function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
316
+ const item = useSyncExternalStore(entry.subscribe, entry.getItem);
317
+ const id = getId(item);
318
+ const renderHeader = render as HeaderRenderer<unknown>;
319
+ const body = id === null ? null : renderHeader({ section: collection.sectionFor(id) });
320
+
321
+ return createPortal(body, entry.cell, entry.key);
322
+ }
323
+
324
+ function usePortalEntries<C extends CellHost>(store: CellStore<C>): CellEntry<C>[] {
325
+ return useSyncExternalStore(store.subscribe, store.getSnapshot);
326
+ }
327
+
328
+ const ItemPortals = ({ store, render, collection, expandedIds }: ItemPortalsProps): ReactNode =>
329
+ usePortalEntries(store).map((entry) => (
330
+ <ItemCell key={entry.key} entry={entry} render={render} collection={collection} expandedIds={expandedIds} />
331
+ ));
332
+
333
+ const HeaderPortals = ({ store, render, collection }: HeaderPortalsProps): ReactNode =>
334
+ usePortalEntries(store).map((entry) => (
335
+ <HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
336
+ ));
337
+
338
+ export {
339
+ useItemCells,
340
+ useHeaderCells,
341
+ ItemPortals,
342
+ HeaderPortals,
343
+ type CellSize,
344
+ type CellStore,
345
+ };
@@ -0,0 +1,168 @@
1
+ import type { Item, Section } from "../types.js";
2
+
3
+ type Level = {
4
+ key: string;
5
+ ids: string[];
6
+ isExpandable: boolean[];
7
+ };
8
+
9
+ type CollectionIndex = {
10
+ isTree: boolean;
11
+ size: number;
12
+ groups: Level[];
13
+ children: Map<string, Level>;
14
+ has: (id: string) => boolean;
15
+ itemFor: (id: string) => Item | undefined;
16
+ sectionFor: (id: string) => unknown;
17
+ positionFor: (id: string) => number;
18
+ };
19
+
20
+ type IndexState = {
21
+ isTree: boolean;
22
+ groups: Level[];
23
+ children: Map<string, Level>;
24
+ itemsById: Map<string, Item>;
25
+ sections: Section[] | undefined;
26
+ positions: Map<string, number> | null;
27
+ starts: number[] | null;
28
+ };
29
+
30
+ const ROOT_LEVEL_KEY = "";
31
+
32
+ const hasChildren = (item: Item): boolean => item.children !== undefined && item.children.length > 0;
33
+ const childLevelKey = (id: string): string => `child:${id}`;
34
+
35
+ function collectChildren(state: IndexState, level: Level, item: Item): void {
36
+ if (!state.isTree) {
37
+ return;
38
+ }
39
+
40
+ const canExpand = hasChildren(item);
41
+ level.isExpandable.push(canExpand);
42
+
43
+ if (!canExpand) {
44
+ return;
45
+ }
46
+
47
+ const key = childLevelKey(item.id);
48
+ state.children.set(key, collectLevel(state, key, item.children ?? []));
49
+ }
50
+
51
+ function collectLevel(state: IndexState, key: string, items: Item[]): Level {
52
+ const level: Level = { key, ids: [], isExpandable: [] };
53
+
54
+ for (const item of items) {
55
+ level.ids.push(item.id);
56
+ state.itemsById.set(item.id, item);
57
+ collectChildren(state, level, item);
58
+ }
59
+
60
+ return level;
61
+ }
62
+
63
+ function buildPositions(state: IndexState): void {
64
+ const positions: Map<string, number> = new Map();
65
+ const starts: number[] = [];
66
+ let offset = 0;
67
+
68
+ for (const level of state.groups) {
69
+ starts.push(offset);
70
+
71
+ for (const id of level.ids) {
72
+ positions.set(id, offset);
73
+ offset += 1;
74
+ }
75
+ }
76
+
77
+ state.positions = positions;
78
+ state.starts = starts;
79
+ }
80
+
81
+ function positionFor(state: IndexState, id: string): number {
82
+ if (state.isTree) {
83
+ return -1;
84
+ }
85
+
86
+ if (state.positions === null) {
87
+ buildPositions(state);
88
+ }
89
+
90
+ return state.positions?.get(id) ?? -1;
91
+ }
92
+
93
+ function sectionAt(starts: number[], position: number): number {
94
+ let low = 0;
95
+ let high = starts.length - 1;
96
+
97
+ while (low < high) {
98
+ const middle = Math.ceil((low + high) / 2);
99
+
100
+ if ((starts[middle] ?? 0) <= position) {
101
+ low = middle;
102
+ } else {
103
+ high = middle - 1;
104
+ }
105
+ }
106
+
107
+ return low;
108
+ }
109
+
110
+ function sectionFor(state: IndexState, id: string): unknown {
111
+ const sections = state.sections;
112
+ const position = positionFor(state, id);
113
+
114
+ if (sections === undefined || position < 0 || state.starts === null) {
115
+ return undefined;
116
+ }
117
+
118
+ return sections[sectionAt(state.starts, position)]?.value;
119
+ }
120
+
121
+ function buildGroups(state: IndexState, source: Item[], sections: Section[] | undefined): Level[] {
122
+ if (sections === undefined) {
123
+ return [collectLevel(state, ROOT_LEVEL_KEY, source)];
124
+ }
125
+
126
+ return sections.map((section) => collectLevel(state, section.id, section.data));
127
+ }
128
+
129
+ function isTreeSource(source: Item[], sections: Section[] | undefined, isFlat: boolean): boolean {
130
+ if (sections !== undefined || isFlat) {
131
+ return false;
132
+ }
133
+
134
+ return source.some((item) => hasChildren(item));
135
+ }
136
+
137
+ function createCollectionIndex(
138
+ items: Item[] | undefined,
139
+ sections: Section[] | undefined,
140
+ isFlat: boolean,
141
+ ): CollectionIndex {
142
+ const source = items ?? [];
143
+
144
+ const state: IndexState = {
145
+ isTree: isTreeSource(source, sections, isFlat),
146
+ groups: [],
147
+ children: new Map(),
148
+ itemsById: new Map(),
149
+ sections,
150
+ positions: null,
151
+ starts: null,
152
+ };
153
+
154
+ state.groups = buildGroups(state, source, sections);
155
+
156
+ return {
157
+ isTree: state.isTree,
158
+ size: state.itemsById.size,
159
+ groups: state.groups,
160
+ children: state.children,
161
+ has: (id) => state.itemsById.has(id),
162
+ itemFor: (id) => state.itemsById.get(id),
163
+ sectionFor: (id) => sectionFor(state, id),
164
+ positionFor: (id) => positionFor(state, id),
165
+ };
166
+ }
167
+
168
+ export { createCollectionIndex, childLevelKey, ROOT_LEVEL_KEY, type CollectionIndex, type Level };