@gtkx/components 1.0.0-rc.4 → 1.1.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.
- package/README.md +5 -5
- package/dist/column-view.d.ts +2 -2
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +14 -12
- package/dist/column-view.js.map +1 -1
- package/dist/grid-view.js +1 -1
- package/dist/grid-view.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +31 -23
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +168 -77
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-index.d.ts +8 -11
- package/dist/internal/collection-index.d.ts.map +1 -1
- package/dist/internal/collection-index.js +53 -73
- package/dist/internal/collection-index.js.map +1 -1
- package/dist/internal/collection-model.d.ts +24 -4
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/collection-model.js +247 -96
- package/dist/internal/collection-model.js.map +1 -1
- package/dist/internal/collection.d.ts +8 -10
- package/dist/internal/collection.d.ts.map +1 -1
- package/dist/internal/collection.js +38 -48
- package/dist/internal/collection.js.map +1 -1
- package/dist/internal/controlled-sync.d.ts +11 -0
- package/dist/internal/controlled-sync.d.ts.map +1 -0
- package/dist/internal/controlled-sync.js +19 -0
- package/dist/internal/controlled-sync.js.map +1 -0
- package/dist/internal/drop-down.d.ts.map +1 -1
- package/dist/internal/drop-down.js +27 -15
- package/dist/internal/drop-down.js.map +1 -1
- package/dist/internal/expansion.d.ts +3 -2
- package/dist/internal/expansion.d.ts.map +1 -1
- package/dist/internal/expansion.js +73 -48
- package/dist/internal/expansion.js.map +1 -1
- package/dist/internal/keys.d.ts +5 -0
- package/dist/internal/keys.d.ts.map +1 -0
- package/dist/internal/keys.js +13 -0
- package/dist/internal/keys.js.map +1 -0
- package/dist/internal/selection.d.ts +3 -2
- package/dist/internal/selection.d.ts.map +1 -1
- package/dist/internal/selection.js +68 -35
- package/dist/internal/selection.js.map +1 -1
- package/dist/internal/slots.d.ts +10 -0
- package/dist/internal/slots.d.ts.map +1 -0
- package/dist/internal/slots.js +43 -0
- package/dist/internal/slots.js.map +1 -0
- package/dist/internal/tree-expansion.d.ts +19 -0
- package/dist/internal/tree-expansion.d.ts.map +1 -0
- package/dist/internal/tree-expansion.js +72 -0
- package/dist/internal/tree-expansion.js.map +1 -0
- package/dist/internal/tree-order.d.ts +26 -0
- package/dist/internal/tree-order.d.ts.map +1 -0
- package/dist/internal/tree-order.js +109 -0
- package/dist/internal/tree-order.js.map +1 -0
- package/dist/internal/use-collection.d.ts +1 -1
- package/dist/internal/use-collection.d.ts.map +1 -1
- package/dist/internal/use-collection.js +4 -4
- package/dist/internal/use-collection.js.map +1 -1
- package/dist/list-view.d.ts.map +1 -1
- package/dist/list-view.js +6 -6
- package/dist/list-view.js.map +1 -1
- package/dist/types.d.ts +72 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +17 -9
- package/src/column-view.tsx +28 -25
- package/src/grid-view.tsx +1 -1
- package/src/index.ts +3 -0
- package/src/internal/cells.tsx +339 -117
- package/src/internal/collection-index.ts +68 -92
- package/src/internal/collection-model.ts +327 -112
- package/src/internal/collection.ts +45 -65
- package/src/internal/controlled-sync.ts +34 -0
- package/src/internal/drop-down.tsx +41 -20
- package/src/internal/expansion.ts +95 -70
- package/src/internal/keys.ts +18 -0
- package/src/internal/selection.tsx +100 -51
- package/src/internal/slots.ts +67 -0
- package/src/internal/tree-expansion.ts +115 -0
- package/src/internal/tree-order.ts +208 -0
- package/src/internal/use-collection.tsx +5 -5
- package/src/list-view.tsx +14 -10
- package/src/types.ts +78 -20
|
@@ -1,104 +1,84 @@
|
|
|
1
|
-
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
-
import type * as Gtk from "@gtkx/gi/gtk";
|
|
3
1
|
import type { ListItem } from "../types.js";
|
|
4
2
|
import type { CollectionIndex } from "./collection-index.js";
|
|
5
|
-
import type { CollectionModel } from "./collection-model.js";
|
|
6
|
-
import {
|
|
3
|
+
import type { CollectionModel, SlotRef } from "./collection-model.js";
|
|
4
|
+
import { slotPathAt, slotRefFor } from "./collection-model.js";
|
|
5
|
+
import { findPositions } from "./tree-order.js";
|
|
7
6
|
|
|
8
|
-
type Collection = {
|
|
7
|
+
type Collection = Pick<CollectionModel, "model" | "expansion" | "rowAt"> & {
|
|
9
8
|
isTree: boolean;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
itemFor: (id: string) => ListItem | undefined;
|
|
13
|
-
sectionFor: (id: string) => unknown;
|
|
9
|
+
itemAt: (ref: SlotRef) => ListItem | undefined;
|
|
10
|
+
sectionFor: (levelPath: string) => unknown;
|
|
14
11
|
idAt: (position: number) => string | null;
|
|
12
|
+
pathAt: (position: number) => string | null;
|
|
15
13
|
positionFor: (id: string) => number;
|
|
16
14
|
positionsFor: (ids: string[]) => number[];
|
|
17
15
|
};
|
|
18
16
|
|
|
19
|
-
const
|
|
17
|
+
const NO_POSITIONS: number[] = [];
|
|
20
18
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
visit: (row: Gtk.TreeListRow, id: string | null) => void,
|
|
25
|
-
): void {
|
|
26
|
-
const row = tree.getRow(position);
|
|
27
|
-
|
|
28
|
-
if (row === null) {
|
|
29
|
-
return;
|
|
19
|
+
function positionsFor(collectionModel: CollectionModel, ids: string[]): number[] {
|
|
20
|
+
if (ids.length === 0) {
|
|
21
|
+
return NO_POSITIONS;
|
|
30
22
|
}
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
}
|
|
24
|
+
const { expansion } = collectionModel;
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
for (let position = 0; position < tree.getNItems(); position++) {
|
|
37
|
-
visitRow(tree, position, visit);
|
|
38
|
-
}
|
|
26
|
+
return findPositions(expansion.index, expansion.slots, new Set(ids));
|
|
39
27
|
}
|
|
40
28
|
|
|
41
|
-
function
|
|
42
|
-
const
|
|
29
|
+
function positionFor(collectionModel: CollectionModel, id: string): number {
|
|
30
|
+
const [first = -1] = positionsFor(collectionModel, [id]);
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
out.push(position);
|
|
46
|
-
}
|
|
32
|
+
return first;
|
|
47
33
|
}
|
|
48
34
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const count = tree.getNItems();
|
|
53
|
-
|
|
54
|
-
for (let position = 0; position < count; position++) {
|
|
55
|
-
collectWanted(tree.getItem(position), position, wanted, positions);
|
|
35
|
+
function refAt(collectionModel: CollectionModel, position: number): SlotRef | null {
|
|
36
|
+
if (position < 0) {
|
|
37
|
+
return null;
|
|
56
38
|
}
|
|
57
39
|
|
|
58
|
-
return
|
|
40
|
+
return slotRefFor(collectionModel.model.getItem(position));
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (const id of ids) {
|
|
65
|
-
const position = index.positionFor(id);
|
|
43
|
+
function itemAt(index: CollectionIndex, ref: SlotRef): ListItem | undefined {
|
|
44
|
+
return index.itemAt(ref.store.level.path, ref.slot);
|
|
45
|
+
}
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
}
|
|
47
|
+
function idAt(collectionModel: CollectionModel, index: CollectionIndex, position: number): string | null {
|
|
48
|
+
const ref = refAt(collectionModel, position);
|
|
71
49
|
|
|
72
|
-
return
|
|
50
|
+
return ref === null ? null : (itemAt(index, ref)?.id ?? null);
|
|
73
51
|
}
|
|
74
52
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
return indexPositions(index, ids);
|
|
78
|
-
}
|
|
53
|
+
function pathAt(collectionModel: CollectionModel, position: number): string | null {
|
|
54
|
+
const ref = refAt(collectionModel, position);
|
|
79
55
|
|
|
80
|
-
|
|
56
|
+
if (ref === null) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
81
59
|
|
|
82
|
-
return
|
|
60
|
+
return slotPathAt(ref.store, ref.slot);
|
|
83
61
|
}
|
|
84
62
|
|
|
85
|
-
function
|
|
86
|
-
const
|
|
63
|
+
function isCollectionIdle(collection: Collection): boolean {
|
|
64
|
+
const { expansion } = collection;
|
|
87
65
|
|
|
88
|
-
return
|
|
66
|
+
return !expansion.isApplying && !expansion.isSyncing;
|
|
89
67
|
}
|
|
90
68
|
|
|
91
|
-
function createCollection(
|
|
69
|
+
function createCollection(collectionModel: CollectionModel, index: CollectionIndex): Collection {
|
|
92
70
|
return {
|
|
71
|
+
model: collectionModel.model,
|
|
72
|
+
expansion: collectionModel.expansion,
|
|
93
73
|
isTree: index.isTree,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
itemFor: index.itemFor,
|
|
74
|
+
rowAt: collectionModel.rowAt,
|
|
75
|
+
itemAt: (ref) => itemAt(index, ref),
|
|
97
76
|
sectionFor: index.sectionFor,
|
|
98
|
-
idAt: (position) =>
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
idAt: (position) => idAt(collectionModel, index, position),
|
|
78
|
+
pathAt: (position) => pathAt(collectionModel, position),
|
|
79
|
+
positionFor: (id) => positionFor(collectionModel, id),
|
|
80
|
+
positionsFor: (ids) => positionsFor(collectionModel, ids),
|
|
101
81
|
};
|
|
102
82
|
}
|
|
103
83
|
|
|
104
|
-
export { createCollection,
|
|
84
|
+
export { createCollection, isCollectionIdle, type Collection };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { startTransition, useEffectEvent, useLayoutEffect, useState } from "react";
|
|
2
|
+
import type { Collection } from "./collection.js";
|
|
3
|
+
|
|
4
|
+
type ControlledIds = string[] | null | undefined;
|
|
5
|
+
|
|
6
|
+
type ControlledSyncOptions = {
|
|
7
|
+
ids: ControlledIds;
|
|
8
|
+
collection: Collection;
|
|
9
|
+
widget?: object | null | undefined;
|
|
10
|
+
apply: (ids: ControlledIds) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function useControlledSync(options: ControlledSyncOptions): () => void {
|
|
14
|
+
const { ids, collection, widget } = options;
|
|
15
|
+
const [drift, setDrift] = useState(0);
|
|
16
|
+
|
|
17
|
+
const markDrift = (): void => {
|
|
18
|
+
startTransition(() => {
|
|
19
|
+
setDrift((count) => count + 1);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const apply = useEffectEvent((next: ControlledIds): void => {
|
|
24
|
+
options.apply(next);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
apply(ids);
|
|
29
|
+
}, [widget, collection, ids, drift]);
|
|
30
|
+
|
|
31
|
+
return markDrift;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { useControlledSync };
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
2
|
import type { ElementType, ReactNode, Ref } from "react";
|
|
3
3
|
import { GtkLabel, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
|
|
4
|
-
import { applyWrite } from "@gtkx/react/internal";
|
|
5
4
|
import { omit } from "@gtkx/utils";
|
|
6
|
-
import { useEffectEvent, useLayoutEffect, useRef } from "react";
|
|
5
|
+
import { useEffectEvent, useLayoutEffect, useRef, useState } from "react";
|
|
7
6
|
import type { DropDownOwnProps, ListItemRenderArgs, ListItemRenderer } from "../types.js";
|
|
8
7
|
import type { Collection } from "./collection.js";
|
|
9
|
-
import {
|
|
8
|
+
import { ItemPortals, useItemCells, useSectionHeader } from "./cells.js";
|
|
10
9
|
import { useCollectionData } from "./use-collection.js";
|
|
11
10
|
import { useWidgetRef } from "./use-widget-ref.js";
|
|
12
11
|
|
|
@@ -24,6 +23,13 @@ type SelectionOptions = {
|
|
|
24
23
|
};
|
|
25
24
|
|
|
26
25
|
type NotifySelectedHandler = NonNullable<DropDownBaseProps["onNotifySelected"]>;
|
|
26
|
+
type ApplyState = { isApplying: boolean };
|
|
27
|
+
|
|
28
|
+
type NotifyContext = {
|
|
29
|
+
options: SelectionOptions;
|
|
30
|
+
known: { current: string | null };
|
|
31
|
+
state: ApplyState;
|
|
32
|
+
};
|
|
27
33
|
|
|
28
34
|
type KnownSelection = {
|
|
29
35
|
known: { current: string | null };
|
|
@@ -43,6 +49,10 @@ const DROP_DOWN_PROPS: string[] = [
|
|
|
43
49
|
"ref",
|
|
44
50
|
];
|
|
45
51
|
|
|
52
|
+
function newApplyState(): ApplyState {
|
|
53
|
+
return { isApplying: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
const describeValue = (value: unknown): string => {
|
|
47
57
|
if (typeof value === "string") {
|
|
48
58
|
return value;
|
|
@@ -61,9 +71,6 @@ const defaultItemContent = (value: unknown): ReactNode =>
|
|
|
61
71
|
const defaultRenderItem = ({ item }: ListItemRenderArgs<unknown>): ReactNode => defaultItemContent(item);
|
|
62
72
|
const faceRenderer = (props: DropDownBaseProps): ListItemRenderer<never> => props.renderItem ?? defaultRenderItem;
|
|
63
73
|
|
|
64
|
-
const listRenderer = (props: DropDownBaseProps): ListItemRenderer<never> =>
|
|
65
|
-
props.renderListItem ?? props.renderItem ?? defaultRenderItem;
|
|
66
|
-
|
|
67
74
|
const resolvePosition = (
|
|
68
75
|
widget: SelectableWidget,
|
|
69
76
|
collection: Collection,
|
|
@@ -110,16 +117,32 @@ const reportSelectedNotify = (options: SelectionOptions, tracker: KnownSelection
|
|
|
110
117
|
reportKnownSelection(tracker, collection.idAt(widget.getSelected()));
|
|
111
118
|
};
|
|
112
119
|
|
|
113
|
-
const applySelectedPosition = (widget: SelectableWidget, position: number): void => {
|
|
114
|
-
|
|
120
|
+
const applySelectedPosition = (widget: SelectableWidget, position: number, state: ApplyState): void => {
|
|
121
|
+
state.isApplying = true;
|
|
122
|
+
|
|
123
|
+
try {
|
|
115
124
|
widget.selected = position;
|
|
116
|
-
}
|
|
125
|
+
} finally {
|
|
126
|
+
state.isApplying = false;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const dispatchSelectedNotify = (context: NotifyContext, value: number | null, self: SelectableWidget): void => {
|
|
131
|
+
const { options, known, state } = context;
|
|
132
|
+
|
|
133
|
+
if (state.isApplying) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
reportSelectedNotify(options, { known, onSelectionChanged: options.props.onSelectionChanged });
|
|
138
|
+
options.props.onNotifySelected?.(value, self);
|
|
117
139
|
};
|
|
118
140
|
|
|
119
141
|
const useDropDownSelection = (options: SelectionOptions): NotifySelectedHandler => {
|
|
120
142
|
const { widget, collection } = options;
|
|
121
143
|
const { selectedId } = options.props;
|
|
122
144
|
const known = useRef<string | null>(null);
|
|
145
|
+
const [applyState] = useState<ApplyState>(newApplyState);
|
|
123
146
|
|
|
124
147
|
const syncKnownSelection = useEffectEvent((position: number): void => {
|
|
125
148
|
const effectiveId = collection.idAt(position);
|
|
@@ -140,13 +163,12 @@ const useDropDownSelection = (options: SelectionOptions): NotifySelectedHandler
|
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
const position = resolvePosition(widget, collection, selectedId);
|
|
143
|
-
applySelectedPosition(widget, position);
|
|
166
|
+
applySelectedPosition(widget, position, applyState);
|
|
144
167
|
syncKnownSelection(position);
|
|
145
|
-
}, [widget, collection, selectedId]);
|
|
168
|
+
}, [widget, collection, selectedId, applyState]);
|
|
146
169
|
|
|
147
170
|
return (value, self) => {
|
|
148
|
-
|
|
149
|
-
options.props.onNotifySelected?.(value, self);
|
|
171
|
+
dispatchSelectedNotify({ options, known, state: applyState }, value, self);
|
|
150
172
|
};
|
|
151
173
|
};
|
|
152
174
|
|
|
@@ -154,12 +176,11 @@ function DropDownBase(props: DropDownBaseProps & { component: ElementType }): Re
|
|
|
154
176
|
const { component: Component, items, sections, renderListItem, renderHeader, ref } = props;
|
|
155
177
|
const rest = omit(props, DROP_DOWN_PROPS);
|
|
156
178
|
const [widget, refCallback] = useWidgetRef<SelectableWidget>(ref);
|
|
157
|
-
const collection = useCollectionData({ items, sections });
|
|
179
|
+
const collection = useCollectionData({ items, sections, isFlat: true });
|
|
158
180
|
const faceCells = useItemCells({ width: -1, height: -1 });
|
|
159
181
|
const listCells = useItemCells({ width: -1, height: -1 });
|
|
160
|
-
const
|
|
182
|
+
const header = useSectionHeader(renderHeader, collection, { width: -1, height: -1 });
|
|
161
183
|
const handleNotifySelected = useDropDownSelection({ widget, collection, props });
|
|
162
|
-
const hasHeader = typeof renderHeader === "function";
|
|
163
184
|
|
|
164
185
|
return (
|
|
165
186
|
<>
|
|
@@ -170,15 +191,15 @@ function DropDownBase(props: DropDownBaseProps & { component: ElementType }): Re
|
|
|
170
191
|
{...(renderListItem != null && {
|
|
171
192
|
listFactory: <GtkSignalListItemFactory {...listCells.handlers} />,
|
|
172
193
|
})}
|
|
173
|
-
{...
|
|
194
|
+
{...header.factoryProps}
|
|
174
195
|
{...rest}
|
|
175
196
|
onNotifySelected={handleNotifySelected}
|
|
176
197
|
/>
|
|
177
|
-
<ItemPortals
|
|
198
|
+
<ItemPortals registry={faceCells} render={faceRenderer(props)} collection={collection} />
|
|
178
199
|
{renderListItem != null && (
|
|
179
|
-
<ItemPortals
|
|
200
|
+
<ItemPortals registry={listCells} render={renderListItem} collection={collection} />
|
|
180
201
|
)}
|
|
181
|
-
{
|
|
202
|
+
{header.portals}
|
|
182
203
|
</>
|
|
183
204
|
);
|
|
184
205
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { RefObject } from "react";
|
|
3
|
-
import { useEffectEvent, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import { useState } from "react";
|
|
4
2
|
import type { Collection } from "./collection.js";
|
|
5
|
-
import {
|
|
3
|
+
import type { TreeExpansion } from "./tree-expansion.js";
|
|
4
|
+
import type { VisibleOrder, WalkOptions } from "./tree-order.js";
|
|
5
|
+
import { isCollectionIdle } from "./collection.js";
|
|
6
|
+
import { useControlledSync } from "./controlled-sync.js";
|
|
7
|
+
import { joinParts } from "./keys.js";
|
|
8
|
+
import { trackPaths } from "./slots.js";
|
|
9
|
+
import { adoptOrder, markExpanded, orderFor } from "./tree-expansion.js";
|
|
10
|
+
import { expandedPathsFor, walkVisible } from "./tree-order.js";
|
|
6
11
|
|
|
7
12
|
type ExpansionOptions = {
|
|
8
13
|
collection: Collection;
|
|
@@ -10,113 +15,133 @@ type ExpansionOptions = {
|
|
|
10
15
|
onExpandedChange?: ((ids: string[]) => void) | null | undefined;
|
|
11
16
|
};
|
|
12
17
|
|
|
18
|
+
type ItemsChange = {
|
|
19
|
+
position: number;
|
|
20
|
+
removed: number;
|
|
21
|
+
added: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ItemsChangeHandler = (position: number, removed: number, added: number) => void;
|
|
13
25
|
type LastExpansion = { key: string };
|
|
14
26
|
|
|
27
|
+
type ExpansionContext = {
|
|
28
|
+
collection: Collection;
|
|
29
|
+
last: LastExpansion;
|
|
30
|
+
onExpandedChange?: ((ids: string[]) => void) | null | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
15
33
|
function newLastExpansion(): LastExpansion {
|
|
16
34
|
return { key: "" };
|
|
17
35
|
}
|
|
18
36
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
ids.push(id);
|
|
22
|
-
}
|
|
37
|
+
function hasSameExpansion(expanded: Set<string>, wanted: Set<string>): boolean {
|
|
38
|
+
return expanded.size === wanted.size && wanted.isSubsetOf(expanded);
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
function toggleOptions(collection: Collection, wanted: Set<string>): WalkOptions {
|
|
42
|
+
const { expansion } = collection;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
index: expansion.index,
|
|
46
|
+
slots: trackPaths(wanted),
|
|
47
|
+
marks: trackPaths(wanted.symmetricDifference(expansion.expanded)),
|
|
48
|
+
visit: (row) => {
|
|
49
|
+
if (row.isMarked) {
|
|
50
|
+
collection.rowAt(row.position)?.setExpanded(row.isOpen);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
31
54
|
}
|
|
32
55
|
|
|
33
|
-
function
|
|
34
|
-
|
|
56
|
+
function walkApplying(expansion: TreeExpansion, options: WalkOptions): VisibleOrder {
|
|
57
|
+
expansion.isApplying = true;
|
|
35
58
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
59
|
+
try {
|
|
60
|
+
return walkVisible(options);
|
|
61
|
+
} finally {
|
|
62
|
+
expansion.isApplying = false;
|
|
63
|
+
}
|
|
39
64
|
}
|
|
40
65
|
|
|
41
|
-
function
|
|
42
|
-
collection
|
|
43
|
-
|
|
44
|
-
onExpandedChange: ((ids: string[]) => void) | null | undefined,
|
|
45
|
-
): void {
|
|
46
|
-
const tree = collection.treeModel();
|
|
66
|
+
function applyWanted(collection: Collection, expandedIds: string[]): void {
|
|
67
|
+
const { expansion } = collection;
|
|
68
|
+
const wanted = expandedPathsFor(expansion.index, new Set(expandedIds));
|
|
47
69
|
|
|
48
|
-
if (
|
|
70
|
+
if (hasSameExpansion(expansion.expanded, wanted)) {
|
|
49
71
|
return;
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
eachRow(tree, (row, id) => {
|
|
55
|
-
collectExpanded(row, id, ids);
|
|
56
|
-
});
|
|
74
|
+
adoptOrder(expansion, walkApplying(expansion, toggleOptions(collection, wanted)));
|
|
75
|
+
}
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
function reportExpansion(context: ExpansionContext): void {
|
|
78
|
+
const { expandedIds, expandedPaths } = orderFor(context.collection.expansion);
|
|
79
|
+
const key = joinParts(expandedPaths);
|
|
59
80
|
|
|
60
|
-
if (last.key === key) {
|
|
81
|
+
if (context.last.key === key) {
|
|
61
82
|
return;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
last.key = key;
|
|
65
|
-
onExpandedChange?.(
|
|
85
|
+
context.last.key = key;
|
|
86
|
+
context.onExpandedChange?.([...expandedIds]);
|
|
66
87
|
}
|
|
67
88
|
|
|
68
|
-
function
|
|
69
|
-
collection
|
|
70
|
-
last: LastExpansion,
|
|
71
|
-
expanding: RefObject<boolean>,
|
|
72
|
-
onExpandedChange: ((ids: string[]) => void) | null | undefined,
|
|
73
|
-
): void {
|
|
74
|
-
if (expanding.current) {
|
|
89
|
+
function applyControlledExpansion(context: ExpansionContext, expandedIds: string[] | null | undefined): void {
|
|
90
|
+
if (!context.collection.isTree) {
|
|
75
91
|
return;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
|
|
94
|
+
applyWanted(context.collection, expandedIds ?? []);
|
|
95
|
+
reportExpansion(context);
|
|
79
96
|
}
|
|
80
97
|
|
|
81
|
-
function
|
|
82
|
-
collection
|
|
83
|
-
|
|
84
|
-
expanding: RefObject<boolean>,
|
|
85
|
-
report: () => void,
|
|
86
|
-
): void {
|
|
87
|
-
const tree = collection.treeModel();
|
|
98
|
+
function isExpansionIdle(collection: Collection): boolean {
|
|
99
|
+
return collection.isTree && isCollectionIdle(collection);
|
|
100
|
+
}
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
function didRowDrift(collection: Collection, change: ItemsChange): boolean {
|
|
103
|
+
const path = collection.pathAt(change.position - 1);
|
|
104
|
+
const isExpanded = change.removed === 0 && change.added > 0;
|
|
105
|
+
const isCollapsed = change.added === 0 && change.removed > 0;
|
|
106
|
+
|
|
107
|
+
if (path === null || isExpanded === isCollapsed) {
|
|
108
|
+
return false;
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
|
|
111
|
+
markExpanded(collection.expansion, path, isExpanded);
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function observeExpansion(context: ExpansionContext, change: ItemsChange, onDrift: () => void): void {
|
|
117
|
+
if (!isExpansionIdle(context.collection)) {
|
|
118
|
+
return;
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
const didDrift = didRowDrift(context.collection, change);
|
|
122
|
+
reportExpansion(context);
|
|
123
|
+
|
|
124
|
+
if (didDrift) {
|
|
125
|
+
onDrift();
|
|
126
|
+
}
|
|
102
127
|
}
|
|
103
128
|
|
|
104
|
-
function useExpansion(options: ExpansionOptions):
|
|
129
|
+
function useExpansion(options: ExpansionOptions): ItemsChangeHandler {
|
|
105
130
|
const { collection, expandedIds, onExpandedChange } = options;
|
|
106
131
|
const [last] = useState<LastExpansion>(newLastExpansion);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
132
|
+
const context: ExpansionContext = { collection, last, onExpandedChange };
|
|
133
|
+
|
|
134
|
+
const markDrift = useControlledSync({
|
|
135
|
+
ids: expandedIds,
|
|
136
|
+
collection,
|
|
137
|
+
apply: (ids) => {
|
|
138
|
+
applyControlledExpansion(context, ids);
|
|
139
|
+
},
|
|
111
140
|
});
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}, [collection, expandedIds]);
|
|
116
|
-
|
|
117
|
-
return () => {
|
|
118
|
-
reportWhenIdle(collection, last, expanding, onExpandedChange);
|
|
142
|
+
return (position, removed, added) => {
|
|
143
|
+
observeExpansion(context, { position, removed, added }, markDrift);
|
|
119
144
|
};
|
|
120
145
|
}
|
|
121
146
|
|
|
122
|
-
export { useExpansion, type
|
|
147
|
+
export { useExpansion, type ItemsChangeHandler };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const LENGTH_SEPARATOR = "\u{1}";
|
|
2
|
+
|
|
3
|
+
const encodePart = (value: string): string => `${String(value.length)}${LENGTH_SEPARATOR}${value}`;
|
|
4
|
+
const joinParts = (values: string[]): string => values.map((value) => encodePart(value)).join("");
|
|
5
|
+
|
|
6
|
+
const decodePartAt = (value: string, offset: number): string | null => {
|
|
7
|
+
const separator = value.indexOf(LENGTH_SEPARATOR, offset);
|
|
8
|
+
|
|
9
|
+
if (separator === -1) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const length = Number(value.slice(offset, separator));
|
|
14
|
+
|
|
15
|
+
return value.slice(separator + 1, separator + 1 + length);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { decodePartAt, encodePart, joinParts };
|