@react-stately/layout 4.1.1 → 4.2.1
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/dist/GridLayout.main.js +131 -63
- package/dist/GridLayout.main.js.map +1 -1
- package/dist/GridLayout.mjs +131 -63
- package/dist/GridLayout.module.js +131 -63
- package/dist/GridLayout.module.js.map +1 -1
- package/dist/ListLayout.main.js +62 -21
- package/dist/ListLayout.main.js.map +1 -1
- package/dist/ListLayout.mjs +63 -22
- package/dist/ListLayout.module.js +63 -22
- package/dist/ListLayout.module.js.map +1 -1
- package/dist/TableLayout.main.js +35 -23
- package/dist/TableLayout.main.js.map +1 -1
- package/dist/TableLayout.mjs +36 -24
- package/dist/TableLayout.module.js +36 -24
- package/dist/TableLayout.module.js.map +1 -1
- package/dist/WaterfallLayout.main.js +212 -0
- package/dist/WaterfallLayout.main.js.map +1 -0
- package/dist/WaterfallLayout.mjs +207 -0
- package/dist/WaterfallLayout.module.js +207 -0
- package/dist/WaterfallLayout.module.js.map +1 -0
- package/dist/import.mjs +3 -1
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -1
- package/dist/module.js +3 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +102 -29
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -9
- package/src/GridLayout.ts +194 -89
- package/src/ListLayout.ts +105 -36
- package/src/TableLayout.ts +44 -27
- package/src/WaterfallLayout.ts +302 -0
- package/src/index.ts +2 -0
package/src/GridLayout.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DropTarget, DropTargetDelegate, ItemDropTarget, Key, Node} from '@react-types/shared';
|
|
14
|
-
import {Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer';
|
|
14
|
+
import {InvalidationContext, Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer';
|
|
15
15
|
|
|
16
16
|
export interface GridLayoutOptions {
|
|
17
17
|
/**
|
|
@@ -24,6 +24,13 @@ export interface GridLayoutOptions {
|
|
|
24
24
|
* @default Infinity
|
|
25
25
|
*/
|
|
26
26
|
maxItemSize?: Size,
|
|
27
|
+
/**
|
|
28
|
+
* Whether to preserve the aspect ratio of the `minItemSize`.
|
|
29
|
+
* By default, grid rows may have variable heights. When `preserveAspectRatio`
|
|
30
|
+
* is true, all rows will have equal heights.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
preserveAspectRatio?: boolean,
|
|
27
34
|
/**
|
|
28
35
|
* The minimum space required between items.
|
|
29
36
|
* @default 18 x 18
|
|
@@ -41,129 +48,227 @@ export interface GridLayoutOptions {
|
|
|
41
48
|
dropIndicatorThickness?: number
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const DEFAULT_OPTIONS = {
|
|
52
|
+
minItemSize: new Size(200, 200),
|
|
53
|
+
maxItemSize: new Size(Infinity, Infinity),
|
|
54
|
+
preserveAspectRatio: false,
|
|
55
|
+
minSpace: new Size(18, 18),
|
|
56
|
+
maxColumns: Infinity,
|
|
57
|
+
dropIndicatorThickness: 2
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* GridLayout is a virtualizer Layout implementation
|
|
62
|
+
* that arranges its items in a grid.
|
|
63
|
+
* The items are sized between a minimum and maximum size
|
|
64
|
+
* depending on the width of the container.
|
|
65
|
+
*/
|
|
66
|
+
export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> extends Layout<Node<T>, O> implements DropTargetDelegate {
|
|
67
|
+
protected gap: Size = DEFAULT_OPTIONS.minSpace;
|
|
68
|
+
protected dropIndicatorThickness = 2;
|
|
51
69
|
protected numColumns: number = 0;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
private contentSize: Size = new Size();
|
|
71
|
+
private layoutInfos: Map<Key, LayoutInfo> = new Map();
|
|
72
|
+
|
|
73
|
+
shouldInvalidateLayoutOptions(newOptions: O, oldOptions: O): boolean {
|
|
74
|
+
return newOptions.maxColumns !== oldOptions.maxColumns
|
|
75
|
+
|| newOptions.dropIndicatorThickness !== oldOptions.dropIndicatorThickness
|
|
76
|
+
|| newOptions.preserveAspectRatio !== oldOptions.preserveAspectRatio
|
|
77
|
+
|| (!(newOptions.minItemSize || DEFAULT_OPTIONS.minItemSize).equals(oldOptions.minItemSize || DEFAULT_OPTIONS.minItemSize))
|
|
78
|
+
|| (!(newOptions.maxItemSize || DEFAULT_OPTIONS.maxItemSize).equals(oldOptions.maxItemSize || DEFAULT_OPTIONS.maxItemSize))
|
|
79
|
+
|| (!(newOptions.minSpace || DEFAULT_OPTIONS.minSpace).equals(oldOptions.minSpace || DEFAULT_OPTIONS.minSpace));
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
update(): void {
|
|
82
|
+
update(invalidationContext: InvalidationContext<O>): void {
|
|
83
|
+
let {
|
|
84
|
+
minItemSize = DEFAULT_OPTIONS.minItemSize,
|
|
85
|
+
maxItemSize = DEFAULT_OPTIONS.maxItemSize,
|
|
86
|
+
preserveAspectRatio = DEFAULT_OPTIONS.preserveAspectRatio,
|
|
87
|
+
minSpace = DEFAULT_OPTIONS.minSpace,
|
|
88
|
+
maxColumns = DEFAULT_OPTIONS.maxColumns,
|
|
89
|
+
dropIndicatorThickness = DEFAULT_OPTIONS.dropIndicatorThickness
|
|
90
|
+
} = invalidationContext.layoutOptions || {};
|
|
91
|
+
this.dropIndicatorThickness = dropIndicatorThickness;
|
|
92
|
+
|
|
65
93
|
let visibleWidth = this.virtualizer!.visibleRect.width;
|
|
66
94
|
|
|
67
95
|
// The max item width is always the entire viewport.
|
|
68
96
|
// If the max item height is infinity, scale in proportion to the max width.
|
|
69
|
-
let maxItemWidth = Math.min(
|
|
70
|
-
let maxItemHeight = Number.isFinite(
|
|
71
|
-
?
|
|
72
|
-
: Math.floor((
|
|
97
|
+
let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
|
|
98
|
+
let maxItemHeight = Number.isFinite(maxItemSize.height)
|
|
99
|
+
? maxItemSize.height
|
|
100
|
+
: Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);
|
|
73
101
|
|
|
74
102
|
// Compute the number of rows and columns needed to display the content
|
|
75
|
-
let columns = Math.floor(visibleWidth / (
|
|
76
|
-
|
|
103
|
+
let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
|
|
104
|
+
let numColumns = Math.max(1, Math.min(maxColumns, columns));
|
|
105
|
+
this.numColumns = numColumns;
|
|
77
106
|
|
|
78
107
|
// Compute the available width (minus the space between items)
|
|
79
|
-
let width = visibleWidth - (
|
|
108
|
+
let width = visibleWidth - (minSpace.width * Math.max(0, numColumns));
|
|
80
109
|
|
|
81
110
|
// Compute the item width based on the space available
|
|
82
|
-
let itemWidth = Math.floor(width /
|
|
83
|
-
itemWidth = Math.max(
|
|
111
|
+
let itemWidth = Math.floor(width / numColumns);
|
|
112
|
+
itemWidth = Math.max(minItemSize.width, Math.min(maxItemWidth, itemWidth));
|
|
84
113
|
|
|
85
114
|
// Compute the item height, which is proportional to the item width
|
|
86
|
-
let t = ((itemWidth -
|
|
87
|
-
let itemHeight =
|
|
88
|
-
itemHeight = Math.max(
|
|
89
|
-
this.itemSize = new Size(itemWidth, itemHeight);
|
|
115
|
+
let t = ((itemWidth - minItemSize.width) / Math.max(1, maxItemWidth - minItemSize.width));
|
|
116
|
+
let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t);
|
|
117
|
+
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));
|
|
90
118
|
|
|
91
119
|
// Compute the horizontal spacing and content height
|
|
92
|
-
|
|
120
|
+
let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
|
|
121
|
+
this.gap = new Size(horizontalSpacing, minSpace.height);
|
|
93
122
|
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
123
|
+
let rows = Math.ceil(this.virtualizer!.collection.size / numColumns);
|
|
124
|
+
let iterator = this.virtualizer!.collection[Symbol.iterator]();
|
|
125
|
+
let y = rows > 0 ? minSpace.height : 0;
|
|
126
|
+
let newLayoutInfos = new Map();
|
|
127
|
+
let skeleton: Node<T> | null = null;
|
|
128
|
+
let skeletonCount = 0;
|
|
129
|
+
for (let row = 0; row < rows; row++) {
|
|
130
|
+
let maxHeight = 0;
|
|
131
|
+
let rowLayoutInfos: LayoutInfo[] = [];
|
|
132
|
+
for (let col = 0; col < numColumns; col++) {
|
|
133
|
+
// Repeat skeleton until the end of the current row.
|
|
134
|
+
let node = skeleton || iterator.next().value;
|
|
135
|
+
if (!node) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
99
138
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
if (node.type === 'skeleton') {
|
|
140
|
+
skeleton = node;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let key = skeleton ? `${skeleton.key}-${skeletonCount++}` : node.key;
|
|
144
|
+
let oldLayoutInfo = this.layoutInfos.get(key);
|
|
145
|
+
let content = node;
|
|
146
|
+
if (skeleton) {
|
|
147
|
+
content = oldLayoutInfo && oldLayoutInfo.content.key === key ? oldLayoutInfo.content : {...skeleton, key};
|
|
148
|
+
}
|
|
149
|
+
let x = horizontalSpacing + col * (itemWidth + horizontalSpacing);
|
|
150
|
+
let height = itemHeight;
|
|
151
|
+
let estimatedSize = !preserveAspectRatio;
|
|
152
|
+
if (oldLayoutInfo && estimatedSize) {
|
|
153
|
+
height = oldLayoutInfo.rect.height;
|
|
154
|
+
estimatedSize = invalidationContext.layoutOptionsChanged || invalidationContext.sizeChanged || oldLayoutInfo.estimatedSize || (oldLayoutInfo.content !== content);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let rect = new Rect(x, y, itemWidth, height);
|
|
158
|
+
let layoutInfo = new LayoutInfo(node.type, key, rect);
|
|
159
|
+
layoutInfo.estimatedSize = estimatedSize;
|
|
160
|
+
layoutInfo.allowOverflow = true;
|
|
161
|
+
layoutInfo.content = content;
|
|
162
|
+
newLayoutInfos.set(key, layoutInfo);
|
|
163
|
+
rowLayoutInfos.push(layoutInfo);
|
|
164
|
+
|
|
165
|
+
maxHeight = Math.max(maxHeight, layoutInfo.rect.height);
|
|
109
166
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
167
|
+
|
|
168
|
+
for (let layoutInfo of rowLayoutInfos) {
|
|
169
|
+
layoutInfo.rect.height = maxHeight;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
y += maxHeight + minSpace.height;
|
|
173
|
+
|
|
174
|
+
// Keep adding skeleton rows until we fill the viewport
|
|
175
|
+
if (skeleton && row === rows - 1 && y < this.virtualizer!.visibleRect.height) {
|
|
176
|
+
rows++;
|
|
119
177
|
}
|
|
120
178
|
}
|
|
121
|
-
|
|
122
|
-
|
|
179
|
+
|
|
180
|
+
this.layoutInfos = newLayoutInfos;
|
|
181
|
+
this.contentSize = new Size(this.virtualizer!.visibleRect.width, y);
|
|
123
182
|
}
|
|
124
183
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let itemWidth = this.itemSize.width + this.horizontalSpacing;
|
|
128
|
-
return Math.max(0,
|
|
129
|
-
Math.min(
|
|
130
|
-
this.virtualizer!.collection.size - 1,
|
|
131
|
-
Math.floor(y / itemHeight) * this.numColumns + Math.floor((x - this.horizontalSpacing) / itemWidth)
|
|
132
|
-
)
|
|
133
|
-
);
|
|
184
|
+
getLayoutInfo(key: Key): LayoutInfo {
|
|
185
|
+
return this.layoutInfos.get(key)!;
|
|
134
186
|
}
|
|
135
187
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return node ? this.layoutInfos[node.index] : null;
|
|
188
|
+
getContentSize(): Size {
|
|
189
|
+
return this.contentSize;
|
|
139
190
|
}
|
|
140
191
|
|
|
141
|
-
|
|
142
|
-
let
|
|
143
|
-
let
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return
|
|
192
|
+
getVisibleLayoutInfos(rect: Rect): LayoutInfo[] {
|
|
193
|
+
let layoutInfos: LayoutInfo[] = [];
|
|
194
|
+
for (let layoutInfo of this.layoutInfos.values()) {
|
|
195
|
+
if (layoutInfo.rect.intersects(rect) || this.virtualizer!.isPersistedKey(layoutInfo.key)) {
|
|
196
|
+
layoutInfos.push(layoutInfo);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return layoutInfos;
|
|
149
200
|
}
|
|
150
201
|
|
|
151
|
-
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
updateItemSize(key: Key, size: Size) {
|
|
203
|
+
let layoutInfo = this.layoutInfos.get(key);
|
|
204
|
+
if (!size || !layoutInfo) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (size.height !== layoutInfo.rect.height) {
|
|
209
|
+
let newLayoutInfo = layoutInfo.copy();
|
|
210
|
+
newLayoutInfo.rect.height = size.height;
|
|
211
|
+
newLayoutInfo.estimatedSize = false;
|
|
212
|
+
this.layoutInfos.set(key, newLayoutInfo);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false;
|
|
155
217
|
}
|
|
156
218
|
|
|
157
219
|
getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget {
|
|
158
|
-
if (this.layoutInfos.
|
|
220
|
+
if (this.layoutInfos.size === 0) {
|
|
159
221
|
return {type: 'root'};
|
|
160
222
|
}
|
|
161
223
|
|
|
162
224
|
x += this.virtualizer!.visibleRect.x;
|
|
163
225
|
y += this.virtualizer!.visibleRect.y;
|
|
164
|
-
let index = this.getIndexAtPoint(x, y);
|
|
165
226
|
|
|
166
|
-
|
|
227
|
+
// Find the closest item within on either side of the point using the gap width.
|
|
228
|
+
let key: Key | null = null;
|
|
229
|
+
if (this.numColumns === 1) {
|
|
230
|
+
let searchRect = new Rect(x, Math.max(0, y - this.gap.height), 1, this.gap.height * 2);
|
|
231
|
+
let candidates = this.getVisibleLayoutInfos(searchRect);
|
|
232
|
+
let minDistance = Infinity;
|
|
233
|
+
for (let candidate of candidates) {
|
|
234
|
+
// Ignore items outside the search rect, e.g. persisted keys.
|
|
235
|
+
if (!candidate.rect.intersects(searchRect)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let yDist = Math.abs(candidate.rect.y - y);
|
|
240
|
+
let maxYDist = Math.abs(candidate.rect.maxY - y);
|
|
241
|
+
let dist = Math.min(yDist, maxYDist);
|
|
242
|
+
if (dist < minDistance) {
|
|
243
|
+
minDistance = dist;
|
|
244
|
+
key = candidate.key;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
let searchRect = new Rect(Math.max(0, x - this.gap.width), y, this.gap.width * 2, 1);
|
|
249
|
+
let candidates = this.getVisibleLayoutInfos(searchRect);
|
|
250
|
+
let minDistance = Infinity;
|
|
251
|
+
for (let candidate of candidates) {
|
|
252
|
+
// Ignore items outside the search rect, e.g. persisted keys.
|
|
253
|
+
if (!candidate.rect.intersects(searchRect)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let xDist = Math.abs(candidate.rect.x - x);
|
|
258
|
+
let maxXDist = Math.abs(candidate.rect.maxX - x);
|
|
259
|
+
let dist = Math.min(xDist, maxXDist);
|
|
260
|
+
if (dist < minDistance) {
|
|
261
|
+
minDistance = dist;
|
|
262
|
+
key = candidate.key;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let layoutInfo = key != null ? this.getLayoutInfo(key) : null;
|
|
268
|
+
if (!layoutInfo) {
|
|
269
|
+
return {type: 'root'};
|
|
270
|
+
}
|
|
271
|
+
|
|
167
272
|
let target: DropTarget = {
|
|
168
273
|
type: 'item',
|
|
169
274
|
key: layoutInfo.key,
|
|
@@ -202,16 +307,16 @@ export class GridLayout<T, O = any> extends Layout<Node<T>, O> implements DropTa
|
|
|
202
307
|
rect = new Rect(
|
|
203
308
|
layoutInfo.rect.x,
|
|
204
309
|
target.dropPosition === 'before'
|
|
205
|
-
? layoutInfo.rect.y - this.
|
|
206
|
-
: layoutInfo.rect.maxY + this.
|
|
310
|
+
? layoutInfo.rect.y - this.gap.height / 2 - this.dropIndicatorThickness / 2
|
|
311
|
+
: layoutInfo.rect.maxY + this.gap.height / 2 - this.dropIndicatorThickness / 2,
|
|
207
312
|
layoutInfo.rect.width,
|
|
208
313
|
this.dropIndicatorThickness
|
|
209
314
|
);
|
|
210
315
|
} else {
|
|
211
316
|
rect = new Rect(
|
|
212
317
|
target.dropPosition === 'before'
|
|
213
|
-
? layoutInfo.rect.x - this.
|
|
214
|
-
: layoutInfo.rect.maxX + this.
|
|
318
|
+
? layoutInfo.rect.x - this.gap.width / 2 - this.dropIndicatorThickness / 2
|
|
319
|
+
: layoutInfo.rect.maxX + this.gap.width / 2 - this.dropIndicatorThickness / 2,
|
|
215
320
|
layoutInfo.rect.y,
|
|
216
321
|
this.dropIndicatorThickness,
|
|
217
322
|
layoutInfo.rect.height
|