vlist 1.9.0 → 2.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.
- package/README.github.md +104 -97
- package/README.md +46 -33
- package/dist/constants.d.ts +11 -6
- package/dist/constants.js +83 -0
- package/dist/core/create.d.ts +10 -0
- package/dist/core/create.js +740 -0
- package/dist/core/dom.d.ts +8 -0
- package/dist/core/dom.js +47 -0
- package/dist/core/hooks.d.ts +16 -0
- package/dist/core/hooks.js +67 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +13 -0
- package/dist/core/pipeline.d.ts +51 -0
- package/dist/core/pipeline.js +307 -0
- package/dist/core/pool.d.ts +9 -0
- package/dist/core/pool.js +42 -0
- package/dist/core/scroll.d.ts +32 -0
- package/dist/core/scroll.js +137 -0
- package/dist/core/sizes.d.ts +8 -0
- package/dist/core/sizes.js +6 -0
- package/dist/core/state.d.ts +47 -0
- package/dist/core/state.js +56 -0
- package/dist/core/types.d.ts +187 -0
- package/dist/core/types.js +7 -0
- package/dist/{builder → core}/velocity.d.ts +1 -1
- package/dist/core/velocity.js +33 -0
- package/dist/events/emitter.js +60 -0
- package/dist/events/index.js +6 -0
- package/dist/index.d.ts +28 -19
- package/dist/index.js +28 -1
- package/dist/internals.d.ts +11 -7
- package/dist/internals.js +60 -1
- package/dist/plugins/a11y/index.d.ts +2 -0
- package/dist/plugins/a11y/index.js +1 -0
- package/dist/plugins/a11y/plugin.d.ts +13 -0
- package/dist/plugins/a11y/plugin.js +259 -0
- package/dist/{features → plugins}/async/index.d.ts +1 -1
- package/dist/plugins/async/index.js +12 -0
- package/dist/{features → plugins}/async/manager.d.ts +5 -1
- package/dist/plugins/async/manager.js +568 -0
- package/dist/plugins/async/placeholder.js +154 -0
- package/dist/plugins/async/plugin.d.ts +48 -0
- package/dist/plugins/async/plugin.js +311 -0
- package/dist/plugins/async/sparse.js +540 -0
- package/dist/plugins/autosize/index.d.ts +5 -0
- package/dist/plugins/autosize/index.js +4 -0
- package/dist/plugins/autosize/plugin.d.ts +19 -0
- package/dist/plugins/autosize/plugin.js +185 -0
- package/dist/plugins/grid/index.d.ts +7 -0
- package/dist/plugins/grid/index.js +5 -0
- package/dist/plugins/grid/layout.js +275 -0
- package/dist/plugins/grid/plugin.d.ts +23 -0
- package/dist/plugins/grid/plugin.js +347 -0
- package/dist/plugins/grid/renderer.js +525 -0
- package/dist/plugins/grid/types.js +11 -0
- package/dist/plugins/groups/async-bridge.js +246 -0
- package/dist/{features → plugins}/groups/index.d.ts +1 -1
- package/dist/plugins/groups/index.js +13 -0
- package/dist/plugins/groups/layout.js +294 -0
- package/dist/plugins/groups/plugin.d.ts +22 -0
- package/dist/plugins/groups/plugin.js +571 -0
- package/dist/plugins/groups/sticky.js +255 -0
- package/dist/plugins/groups/types.js +12 -0
- package/dist/plugins/masonry/index.d.ts +8 -0
- package/dist/plugins/masonry/index.js +6 -0
- package/dist/plugins/masonry/layout.js +261 -0
- package/dist/plugins/masonry/plugin.d.ts +32 -0
- package/dist/plugins/masonry/plugin.js +381 -0
- package/dist/plugins/masonry/renderer.js +354 -0
- package/dist/plugins/masonry/types.js +9 -0
- package/dist/plugins/page/index.d.ts +5 -0
- package/dist/plugins/page/index.js +5 -0
- package/dist/plugins/page/plugin.d.ts +21 -0
- package/dist/plugins/page/plugin.js +166 -0
- package/dist/plugins/scale/index.d.ts +5 -0
- package/dist/plugins/scale/index.js +4 -0
- package/dist/plugins/scale/plugin.d.ts +24 -0
- package/dist/plugins/scale/plugin.js +507 -0
- package/dist/plugins/scrollbar/controller.js +574 -0
- package/dist/plugins/scrollbar/index.d.ts +7 -0
- package/dist/plugins/scrollbar/index.js +6 -0
- package/dist/plugins/scrollbar/plugin.d.ts +20 -0
- package/dist/plugins/scrollbar/plugin.js +93 -0
- package/dist/plugins/scrollbar/scrollbar.js +556 -0
- package/dist/plugins/selection/index.d.ts +6 -0
- package/dist/plugins/selection/index.js +7 -0
- package/dist/plugins/selection/plugin.d.ts +16 -0
- package/dist/plugins/selection/plugin.js +601 -0
- package/dist/{features → plugins}/selection/state.d.ts +8 -0
- package/dist/plugins/selection/state.js +332 -0
- package/dist/plugins/snapshots/index.d.ts +5 -0
- package/dist/plugins/snapshots/index.js +5 -0
- package/dist/plugins/snapshots/plugin.d.ts +17 -0
- package/dist/plugins/snapshots/plugin.js +301 -0
- package/dist/plugins/sortable/index.d.ts +6 -0
- package/dist/plugins/sortable/index.js +6 -0
- package/dist/plugins/sortable/plugin.d.ts +34 -0
- package/dist/plugins/sortable/plugin.js +753 -0
- package/dist/plugins/table/header.js +501 -0
- package/dist/{features → plugins}/table/index.d.ts +1 -1
- package/dist/plugins/table/index.js +12 -0
- package/dist/plugins/table/layout.js +211 -0
- package/dist/plugins/table/plugin.d.ts +20 -0
- package/dist/plugins/table/plugin.js +391 -0
- package/dist/plugins/table/renderer.js +625 -0
- package/dist/plugins/table/types.js +12 -0
- package/dist/plugins/transition/index.d.ts +5 -0
- package/dist/plugins/transition/index.js +5 -0
- package/dist/plugins/transition/plugin.d.ts +22 -0
- package/dist/plugins/transition/plugin.js +405 -0
- package/dist/rendering/aria.js +23 -0
- package/dist/rendering/index.js +18 -0
- package/dist/rendering/measured.js +98 -0
- package/dist/rendering/renderer.js +586 -0
- package/dist/rendering/scale.js +267 -0
- package/dist/rendering/scroll.js +71 -0
- package/dist/rendering/sizes.js +193 -0
- package/dist/rendering/sort.js +65 -0
- package/dist/rendering/viewport.js +268 -0
- package/dist/size.json +1 -1
- package/dist/types.js +5 -0
- package/dist/utils/padding.d.ts +2 -4
- package/dist/utils/padding.js +49 -0
- package/dist/utils/stats.js +124 -0
- package/dist/vlist-grid.css +1 -1
- package/dist/vlist-masonry.css +1 -1
- package/dist/vlist-table.css +1 -1
- package/dist/vlist.css +1 -1
- package/package.json +9 -4
- package/dist/builder/a11y.d.ts +0 -16
- package/dist/builder/api.d.ts +0 -21
- package/dist/builder/context.d.ts +0 -36
- package/dist/builder/core.d.ts +0 -16
- package/dist/builder/data.d.ts +0 -71
- package/dist/builder/dom.d.ts +0 -15
- package/dist/builder/index.d.ts +0 -25
- package/dist/builder/materialize.d.ts +0 -166
- package/dist/builder/pool.d.ts +0 -10
- package/dist/builder/range.d.ts +0 -10
- package/dist/builder/scroll.d.ts +0 -24
- package/dist/builder/types.d.ts +0 -512
- package/dist/features/async/feature.d.ts +0 -72
- package/dist/features/autosize/feature.d.ts +0 -34
- package/dist/features/autosize/index.d.ts +0 -2
- package/dist/features/grid/feature.d.ts +0 -48
- package/dist/features/grid/index.d.ts +0 -9
- package/dist/features/groups/feature.d.ts +0 -75
- package/dist/features/masonry/feature.d.ts +0 -45
- package/dist/features/masonry/index.d.ts +0 -9
- package/dist/features/page/feature.d.ts +0 -109
- package/dist/features/page/index.d.ts +0 -9
- package/dist/features/scale/feature.d.ts +0 -42
- package/dist/features/scale/index.d.ts +0 -10
- package/dist/features/scrollbar/feature.d.ts +0 -81
- package/dist/features/scrollbar/index.d.ts +0 -8
- package/dist/features/selection/feature.d.ts +0 -91
- package/dist/features/selection/index.d.ts +0 -7
- package/dist/features/snapshots/feature.d.ts +0 -79
- package/dist/features/snapshots/index.d.ts +0 -9
- package/dist/features/sortable/feature.d.ts +0 -101
- package/dist/features/sortable/index.d.ts +0 -6
- package/dist/features/table/feature.d.ts +0 -67
- package/dist/features/transition/feature.d.ts +0 -30
- package/dist/features/transition/index.d.ts +0 -9
- /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
- /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
- /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
- /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/grid/types.d.ts +0 -0
- /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
- /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
- /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
- /package/dist/{features → plugins}/groups/types.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
- /package/dist/{features → plugins}/table/header.d.ts +0 -0
- /package/dist/{features → plugins}/table/layout.d.ts +0 -0
- /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/table/types.d.ts +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Sticky Header
|
|
3
|
+
*
|
|
4
|
+
* Manages a floating header that sticks to the viewport edge and transitions
|
|
5
|
+
* smoothly when the next group's header approaches (push-out effect).
|
|
6
|
+
*
|
|
7
|
+
* Two permanent slot elements are recycled — content is swapped via a
|
|
8
|
+
* caller-provided `renderInto` callback, keeping the sticky header
|
|
9
|
+
* template-agnostic (same pattern as item rendering).
|
|
10
|
+
*
|
|
11
|
+
* Header offsets and sizes are pre-cached into flat arrays on rebuild,
|
|
12
|
+
* keeping the per-tick scroll handler free of function calls.
|
|
13
|
+
*
|
|
14
|
+
* .vlist-sticky-header (position: relative, overflow: hidden)
|
|
15
|
+
* ├── .sticky-group (active slot — translated during push)
|
|
16
|
+
* └── .sticky-group (standby slot — translated during push)
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Create the sticky header container element upfront (before data arrives)
|
|
20
|
+
* so the DOM structure is stable and doesn't cause a visual shift.
|
|
21
|
+
*/
|
|
22
|
+
export const createStickyContainer = (root, classPrefix, horizontal, headerHeight, stickyOffset = 0) => {
|
|
23
|
+
const mainProp = horizontal ? "width" : "height";
|
|
24
|
+
const container = document.createElement("div");
|
|
25
|
+
container.className = `${classPrefix}-sticky-header`;
|
|
26
|
+
container.setAttribute("role", "presentation");
|
|
27
|
+
container.setAttribute("aria-hidden", "true");
|
|
28
|
+
container.style.cssText =
|
|
29
|
+
`position:relative;z-index:5;pointer-events:none;overflow:hidden;` +
|
|
30
|
+
(horizontal
|
|
31
|
+
? `top:0;bottom:0;left:${stickyOffset || 0}px`
|
|
32
|
+
: `top:${stickyOffset || 0}px`);
|
|
33
|
+
container.style[mainProp] = `${headerHeight}px`;
|
|
34
|
+
root.insertBefore(container, root.firstChild);
|
|
35
|
+
return container;
|
|
36
|
+
};
|
|
37
|
+
export const createStickyHeader = (root, layout, sizeCache, renderInto, classPrefix, horizontal = false, stickyOffset = 0, getCompressionRatio, existingContainer) => {
|
|
38
|
+
// Orientation helpers — resolved once
|
|
39
|
+
const mainProp = horizontal ? "width" : "height";
|
|
40
|
+
const crossProp = horizontal ? "height" : "width";
|
|
41
|
+
const translateAxis = horizontal ? "X" : "Y";
|
|
42
|
+
const setMain = (el, px) => {
|
|
43
|
+
el.style[mainProp] = `${px}px`;
|
|
44
|
+
};
|
|
45
|
+
// DOM setup — reuse pre-created container if provided
|
|
46
|
+
const container = existingContainer ?? createStickyContainer(root, classPrefix, horizontal, 0, stickyOffset);
|
|
47
|
+
const mkSlot = () => {
|
|
48
|
+
const s = document.createElement("div");
|
|
49
|
+
s.className = "sticky-group";
|
|
50
|
+
s.style.position = "absolute";
|
|
51
|
+
s.style.willChange = "transform";
|
|
52
|
+
s.style[crossProp] = "100%";
|
|
53
|
+
return s;
|
|
54
|
+
};
|
|
55
|
+
const slotA = mkSlot();
|
|
56
|
+
const slotB = mkSlot();
|
|
57
|
+
container.append(slotA, slotB);
|
|
58
|
+
// Slot references — swap roles after each completed transition
|
|
59
|
+
let active = slotA;
|
|
60
|
+
let standby = slotB;
|
|
61
|
+
// Pre-cached arrays — rebuilt in cacheGroups(), read on every scroll tick
|
|
62
|
+
// offsets & vSizes are in virtual (scroll) space for binary search / push detection.
|
|
63
|
+
// sizes are in actual pixels for DOM rendering.
|
|
64
|
+
let groups = layout.groups;
|
|
65
|
+
let offsets = [];
|
|
66
|
+
let sizes = [];
|
|
67
|
+
let vSizes = [];
|
|
68
|
+
let groupCount = 0;
|
|
69
|
+
const cacheGroups = () => {
|
|
70
|
+
groups = layout.groups;
|
|
71
|
+
groupCount = groups.length;
|
|
72
|
+
offsets = new Array(groupCount);
|
|
73
|
+
sizes = new Array(groupCount);
|
|
74
|
+
vSizes = new Array(groupCount);
|
|
75
|
+
const ratio = getCompressionRatio ? getCompressionRatio() : 1;
|
|
76
|
+
for (let i = 0; i < groupCount; i++) {
|
|
77
|
+
offsets[i] = sizeCache.getOffset(groups[i].headerLayoutIndex) * ratio;
|
|
78
|
+
sizes[i] = layout.getHeaderHeight(i);
|
|
79
|
+
vSizes[i] = sizes[i] * ratio;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
cacheGroups();
|
|
83
|
+
// Mutable state
|
|
84
|
+
let curGroup = -1;
|
|
85
|
+
let curSize = 0;
|
|
86
|
+
let curVSize = 0;
|
|
87
|
+
let nxtGroup = -1;
|
|
88
|
+
let visible = false;
|
|
89
|
+
let lastOffset = 0;
|
|
90
|
+
let transitioning = false;
|
|
91
|
+
// Slot content — delegate to caller-provided renderInto
|
|
92
|
+
const fill = (slot, gi) => {
|
|
93
|
+
renderInto(slot, gi);
|
|
94
|
+
const sz = sizes[gi];
|
|
95
|
+
setMain(slot, sz);
|
|
96
|
+
return sz;
|
|
97
|
+
};
|
|
98
|
+
const clear = (slot) => {
|
|
99
|
+
slot.replaceChildren();
|
|
100
|
+
slot.style.transform = "";
|
|
101
|
+
};
|
|
102
|
+
// Current group
|
|
103
|
+
const setCurrent = (gi) => {
|
|
104
|
+
if (gi === curGroup)
|
|
105
|
+
return;
|
|
106
|
+
curGroup = gi;
|
|
107
|
+
curSize = 0;
|
|
108
|
+
curVSize = 0;
|
|
109
|
+
if (gi < 0 || gi >= groupCount) {
|
|
110
|
+
clear(active);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
curSize = fill(active, gi);
|
|
114
|
+
curVSize = vSizes[gi];
|
|
115
|
+
setMain(container, curSize);
|
|
116
|
+
active.style.transform = "";
|
|
117
|
+
};
|
|
118
|
+
// Push transition
|
|
119
|
+
const applyPush = (offset) => {
|
|
120
|
+
if (offset === lastOffset)
|
|
121
|
+
return;
|
|
122
|
+
lastOffset = offset;
|
|
123
|
+
const r = Math.round(offset);
|
|
124
|
+
active.style.transform = `translate${translateAxis}(${r}px)`;
|
|
125
|
+
standby.style.transform = `translate${translateAxis}(${r + curSize}px)`;
|
|
126
|
+
};
|
|
127
|
+
const resetTransforms = () => {
|
|
128
|
+
lastOffset = 0;
|
|
129
|
+
active.style.transform = "";
|
|
130
|
+
standby.style.transform = "";
|
|
131
|
+
};
|
|
132
|
+
const complete = () => {
|
|
133
|
+
if (!transitioning)
|
|
134
|
+
return;
|
|
135
|
+
const prev = active;
|
|
136
|
+
active = standby;
|
|
137
|
+
standby = prev;
|
|
138
|
+
curGroup = nxtGroup;
|
|
139
|
+
curSize = curGroup >= 0 ? sizes[curGroup] : 0;
|
|
140
|
+
curVSize = curGroup >= 0 ? vSizes[curGroup] : 0;
|
|
141
|
+
setMain(container, curSize);
|
|
142
|
+
clear(standby);
|
|
143
|
+
nxtGroup = -1;
|
|
144
|
+
transitioning = false;
|
|
145
|
+
resetTransforms();
|
|
146
|
+
};
|
|
147
|
+
const cancel = () => {
|
|
148
|
+
if (!transitioning)
|
|
149
|
+
return;
|
|
150
|
+
clear(standby);
|
|
151
|
+
nxtGroup = -1;
|
|
152
|
+
transitioning = false;
|
|
153
|
+
resetTransforms();
|
|
154
|
+
};
|
|
155
|
+
// Visibility — when a pre-created container is reused, use visibility
|
|
156
|
+
// instead of display to preserve layout (the viewport is sized with
|
|
157
|
+
// calc(100% - headerHeight) and relies on the container occupying space).
|
|
158
|
+
const preserveLayout = !!existingContainer;
|
|
159
|
+
const show = () => {
|
|
160
|
+
if (visible)
|
|
161
|
+
return;
|
|
162
|
+
visible = true;
|
|
163
|
+
if (preserveLayout)
|
|
164
|
+
container.style.visibility = "";
|
|
165
|
+
else
|
|
166
|
+
container.style.display = "";
|
|
167
|
+
};
|
|
168
|
+
const hide = () => {
|
|
169
|
+
if (!visible)
|
|
170
|
+
return;
|
|
171
|
+
visible = false;
|
|
172
|
+
if (preserveLayout)
|
|
173
|
+
container.style.visibility = "hidden";
|
|
174
|
+
else
|
|
175
|
+
container.style.display = "none";
|
|
176
|
+
clear(active);
|
|
177
|
+
curGroup = -1;
|
|
178
|
+
curSize = 0;
|
|
179
|
+
curVSize = 0;
|
|
180
|
+
cancel();
|
|
181
|
+
};
|
|
182
|
+
// Scroll handler — hot path
|
|
183
|
+
const update = (scroll) => {
|
|
184
|
+
if (groupCount === 0) {
|
|
185
|
+
hide();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (scroll < offsets[0]) {
|
|
189
|
+
hide();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Binary search — pure array reads, no function calls
|
|
193
|
+
// offsets and vSizes are in virtual (scroll) space
|
|
194
|
+
let lo = 0, hi = groupCount - 1;
|
|
195
|
+
while (lo < hi) {
|
|
196
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
197
|
+
if (offsets[mid] + vSizes[mid] <= scroll)
|
|
198
|
+
lo = mid;
|
|
199
|
+
else
|
|
200
|
+
hi = mid - 1;
|
|
201
|
+
}
|
|
202
|
+
if (!visible)
|
|
203
|
+
show();
|
|
204
|
+
setCurrent(lo);
|
|
205
|
+
// Push transition check — dist is in virtual space,
|
|
206
|
+
// scaled to actual pixels for DOM transforms
|
|
207
|
+
const nxt = lo + 1;
|
|
208
|
+
if (nxt < groupCount) {
|
|
209
|
+
const dist = offsets[nxt] - scroll;
|
|
210
|
+
if (dist <= 0 && dist > -curVSize) {
|
|
211
|
+
if (nxtGroup !== nxt || !transitioning) {
|
|
212
|
+
nxtGroup = nxt;
|
|
213
|
+
fill(standby, nxt);
|
|
214
|
+
transitioning = true;
|
|
215
|
+
}
|
|
216
|
+
const pxDist = curVSize > 0 ? (dist / curVSize) * curSize : dist;
|
|
217
|
+
applyPush(pxDist);
|
|
218
|
+
}
|
|
219
|
+
else if (dist <= -curVSize) {
|
|
220
|
+
if (transitioning)
|
|
221
|
+
complete();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (transitioning)
|
|
225
|
+
cancel();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (transitioning)
|
|
230
|
+
cancel();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const refresh = () => {
|
|
234
|
+
cacheGroups();
|
|
235
|
+
const prev = curGroup;
|
|
236
|
+
curGroup = -1;
|
|
237
|
+
curSize = 0;
|
|
238
|
+
curVSize = 0;
|
|
239
|
+
if (prev >= 0)
|
|
240
|
+
setCurrent(prev);
|
|
241
|
+
};
|
|
242
|
+
const destroy = () => {
|
|
243
|
+
container.remove();
|
|
244
|
+
curGroup = -1;
|
|
245
|
+
curSize = 0;
|
|
246
|
+
curVSize = 0;
|
|
247
|
+
nxtGroup = -1;
|
|
248
|
+
visible = false;
|
|
249
|
+
transitioning = false;
|
|
250
|
+
};
|
|
251
|
+
if (!existingContainer) {
|
|
252
|
+
container.style.display = "none";
|
|
253
|
+
}
|
|
254
|
+
return { update, refresh, show, hide, destroy };
|
|
255
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Group Types
|
|
3
|
+
* Types for sticky headers and grouped lists
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Type guard: check if an item is a group header pseudo-item
|
|
7
|
+
*/
|
|
8
|
+
export const isGroupHeader = (item) => {
|
|
9
|
+
return (item !== null &&
|
|
10
|
+
typeof item === "object" &&
|
|
11
|
+
item.__groupHeader === true);
|
|
12
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Masonry Plugin
|
|
3
|
+
*/
|
|
4
|
+
export { masonry, type MasonryPluginConfig, type MasonryContext } from "./plugin";
|
|
5
|
+
export { createMasonryLayout } from "./layout";
|
|
6
|
+
export { createMasonryRenderer, type MasonryRenderer, type GetItemFn } from "./renderer";
|
|
7
|
+
export type { MasonryConfig, MasonryLayout, ItemPlacement } from "./types";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Masonry Layout
|
|
3
|
+
* Shortest-lane placement algorithm for masonry/Pinterest-style layouts.
|
|
4
|
+
*
|
|
5
|
+
* Algorithm:
|
|
6
|
+
* 1. Track the size (height/width) of each lane (column/row)
|
|
7
|
+
* 2. For each item:
|
|
8
|
+
* - Find the shortest lane
|
|
9
|
+
* - Place item at the end of that lane
|
|
10
|
+
* - Update lane size
|
|
11
|
+
* 3. Cache all item positions for O(1) lookup during rendering
|
|
12
|
+
*
|
|
13
|
+
* Complexity:
|
|
14
|
+
* - Layout calculation: O(n) where n = total items
|
|
15
|
+
* - Position lookup: O(1) using cached placements
|
|
16
|
+
* - Visibility check: O(k * log(n/k)) using per-lane binary search
|
|
17
|
+
* where k = columns, n = total items
|
|
18
|
+
*/
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Factory
|
|
21
|
+
// =============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Create a MasonryLayout instance.
|
|
24
|
+
*
|
|
25
|
+
* @param config - Masonry configuration (columns, gap, containerSize)
|
|
26
|
+
* @returns MasonryLayout with placement algorithm
|
|
27
|
+
*/
|
|
28
|
+
export const createMasonryLayout = (config) => {
|
|
29
|
+
let columns = Math.max(1, Math.floor(config.columns));
|
|
30
|
+
let gap = config.gap ?? 0;
|
|
31
|
+
let containerSize = config.containerSize;
|
|
32
|
+
// ── Cached derived values ──
|
|
33
|
+
// Recomputed when columns, gap, or containerSize change.
|
|
34
|
+
let crossAxisSize = 0;
|
|
35
|
+
let laneOffsets = [];
|
|
36
|
+
const recomputeDerived = () => {
|
|
37
|
+
const totalGap = (columns - 1) * gap;
|
|
38
|
+
crossAxisSize = Math.max(0, (containerSize - totalGap) / columns);
|
|
39
|
+
laneOffsets = new Array(columns);
|
|
40
|
+
const stride = crossAxisSize + gap;
|
|
41
|
+
for (let i = 0; i < columns; i++) {
|
|
42
|
+
laneOffsets[i] = i * stride;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// Initial computation
|
|
46
|
+
recomputeDerived();
|
|
47
|
+
// ── Per-lane placement indices (for binary search in getVisibleItems) ──
|
|
48
|
+
// Built by calculateLayout, consumed by getVisibleItems.
|
|
49
|
+
let lanePlacements = [];
|
|
50
|
+
// ── Cached total size from last calculateLayout ──
|
|
51
|
+
let cachedTotalSize = 0;
|
|
52
|
+
// ── Pooled visible-items array ──
|
|
53
|
+
// Reused by getVisibleItems to avoid allocation per scroll frame.
|
|
54
|
+
// Single-consumer contract: caller must finish reading before the next call.
|
|
55
|
+
let visiblePool = [];
|
|
56
|
+
/**
|
|
57
|
+
* Calculate layout for all items using shortest-lane algorithm.
|
|
58
|
+
* Returns array of item placements with cached positions.
|
|
59
|
+
*
|
|
60
|
+
* Also caches:
|
|
61
|
+
* - Total size (tallest lane) — retrievable via getTotalSize([])
|
|
62
|
+
* - Per-lane placement index lists — used by getVisibleItems for binary search
|
|
63
|
+
*/
|
|
64
|
+
const calculateLayout = (totalItems, getSizeForItem) => {
|
|
65
|
+
if (totalItems <= 0) {
|
|
66
|
+
cachedTotalSize = 0;
|
|
67
|
+
lanePlacements = [];
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
// Initialize lane sizes (accumulated height/width for each column/row)
|
|
71
|
+
const laneSizes = new Array(columns).fill(0);
|
|
72
|
+
// Per-lane placement indices (for binary search later)
|
|
73
|
+
const lanes = new Array(columns);
|
|
74
|
+
for (let c = 0; c < columns; c++) {
|
|
75
|
+
lanes[c] = [];
|
|
76
|
+
}
|
|
77
|
+
// Array to store all item placements
|
|
78
|
+
const placements = new Array(totalItems);
|
|
79
|
+
// Place each item in the shortest lane
|
|
80
|
+
for (let i = 0; i < totalItems; i++) {
|
|
81
|
+
// Find shortest lane
|
|
82
|
+
let lane = 0;
|
|
83
|
+
let shortestSize = laneSizes[0];
|
|
84
|
+
for (let c = 1; c < columns; c++) {
|
|
85
|
+
const currentSize = laneSizes[c];
|
|
86
|
+
if (currentSize < shortestSize) {
|
|
87
|
+
shortestSize = currentSize;
|
|
88
|
+
lane = c;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Get item size in main axis
|
|
92
|
+
const itemSize = getSizeForItem(i);
|
|
93
|
+
// Position: use precomputed lane offset, current lane accumulator
|
|
94
|
+
const mainOffset = laneSizes[lane];
|
|
95
|
+
// Create placement (flat structure — no nested position object)
|
|
96
|
+
placements[i] = {
|
|
97
|
+
index: i,
|
|
98
|
+
x: laneOffsets[lane],
|
|
99
|
+
y: mainOffset,
|
|
100
|
+
lane,
|
|
101
|
+
size: itemSize,
|
|
102
|
+
crossSize: crossAxisSize,
|
|
103
|
+
};
|
|
104
|
+
// Track placement index per lane
|
|
105
|
+
lanes[lane].push(i);
|
|
106
|
+
// Update lane size (add item size + gap)
|
|
107
|
+
laneSizes[lane] = mainOffset + itemSize + gap;
|
|
108
|
+
}
|
|
109
|
+
// Cache per-lane placements for binary search
|
|
110
|
+
lanePlacements = lanes;
|
|
111
|
+
// Cache total size (max lane extent, minus trailing gap)
|
|
112
|
+
let maxExtent = 0;
|
|
113
|
+
for (let c = 0; c < columns; c++) {
|
|
114
|
+
const laneSize = laneSizes[c];
|
|
115
|
+
// laneSizes includes a trailing gap after the last item — subtract it
|
|
116
|
+
// unless the lane is empty (size === 0)
|
|
117
|
+
const extent = laneSize > 0 ? laneSize - gap : 0;
|
|
118
|
+
if (extent > maxExtent)
|
|
119
|
+
maxExtent = extent;
|
|
120
|
+
}
|
|
121
|
+
cachedTotalSize = maxExtent;
|
|
122
|
+
return placements;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Get total size in main axis (tallest column height or widest row width).
|
|
126
|
+
* This determines the total scroll size.
|
|
127
|
+
*
|
|
128
|
+
* When called with the result of calculateLayout, returns the cached value
|
|
129
|
+
* computed during layout (O(1)). Falls back to recomputation for external
|
|
130
|
+
* placement arrays.
|
|
131
|
+
*/
|
|
132
|
+
const getTotalSize = (placements) => {
|
|
133
|
+
// Fast path: return cached value when called with the layout result
|
|
134
|
+
// (the common case — feature.ts always passes cachedPlacements)
|
|
135
|
+
if (placements.length === 0)
|
|
136
|
+
return cachedTotalSize;
|
|
137
|
+
// If the first placement matches our cached data, use cached total
|
|
138
|
+
// This avoids the O(n) scan in the normal flow
|
|
139
|
+
if (cachedTotalSize)
|
|
140
|
+
return cachedTotalSize;
|
|
141
|
+
// Fallback: recompute (only for externally-constructed placements)
|
|
142
|
+
const laneSizes = new Array(columns).fill(0);
|
|
143
|
+
for (const placement of placements) {
|
|
144
|
+
const extent = placement.y + placement.size;
|
|
145
|
+
const currentSize = laneSizes[placement.lane];
|
|
146
|
+
if (extent > currentSize) {
|
|
147
|
+
laneSizes[placement.lane] = extent;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let max = 0;
|
|
151
|
+
for (let c = 0; c < columns; c++) {
|
|
152
|
+
if (laneSizes[c] > max)
|
|
153
|
+
max = laneSizes[c];
|
|
154
|
+
}
|
|
155
|
+
return max;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Get items visible within the given main-axis range.
|
|
159
|
+
* Uses per-lane binary search for O(k * log(n/k)) performance
|
|
160
|
+
* where k = columns and n = total items.
|
|
161
|
+
*
|
|
162
|
+
* Within each lane, items are sorted by Y position (guaranteed by the
|
|
163
|
+
* shortest-lane algorithm). We binary search each lane to find the first
|
|
164
|
+
* item whose bottom edge enters the viewport and the last item whose
|
|
165
|
+
* top edge is before the viewport end.
|
|
166
|
+
*/
|
|
167
|
+
const getVisibleItems = (placements, mainAxisStart, mainAxisEnd) => {
|
|
168
|
+
if (placements.length === 0 || mainAxisEnd <= mainAxisStart) {
|
|
169
|
+
visiblePool.length = 0;
|
|
170
|
+
return visiblePool;
|
|
171
|
+
}
|
|
172
|
+
// If per-lane data isn't available (external placements), fall back to linear scan
|
|
173
|
+
if (lanePlacements.length === 0 || lanePlacements.length !== columns) {
|
|
174
|
+
return getVisibleItemsLinear(placements, mainAxisStart, mainAxisEnd);
|
|
175
|
+
}
|
|
176
|
+
// Reuse pooled array — reset length to 0 (no allocation)
|
|
177
|
+
visiblePool.length = 0;
|
|
178
|
+
for (let c = 0; c < columns; c++) {
|
|
179
|
+
const laneIndices = lanePlacements[c];
|
|
180
|
+
const laneLen = laneIndices.length;
|
|
181
|
+
if (!laneLen)
|
|
182
|
+
continue;
|
|
183
|
+
// Binary search: find first item in this lane where itemEnd > mainAxisStart
|
|
184
|
+
// i.e., the item's bottom edge is past the viewport top
|
|
185
|
+
let lo = 0;
|
|
186
|
+
let hi = laneLen;
|
|
187
|
+
while (lo < hi) {
|
|
188
|
+
const mid = (lo + hi) >>> 1;
|
|
189
|
+
const p = placements[laneIndices[mid]];
|
|
190
|
+
if (p.y + p.size <= mainAxisStart) {
|
|
191
|
+
lo = mid + 1;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
hi = mid;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Collect visible items from this lane starting at `lo`
|
|
198
|
+
for (let j = lo; j < laneLen; j++) {
|
|
199
|
+
const p = placements[laneIndices[j]];
|
|
200
|
+
// Once the item's top edge is past the viewport bottom, stop
|
|
201
|
+
if (p.y >= mainAxisEnd)
|
|
202
|
+
break;
|
|
203
|
+
visiblePool.push(p);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return visiblePool;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Linear fallback for getVisibleItems when per-lane data is unavailable.
|
|
210
|
+
* Used only for externally-constructed placement arrays.
|
|
211
|
+
*/
|
|
212
|
+
const getVisibleItemsLinear = (placements, mainAxisStart, mainAxisEnd) => {
|
|
213
|
+
visiblePool.length = 0;
|
|
214
|
+
for (const placement of placements) {
|
|
215
|
+
const itemEnd = placement.y + placement.size;
|
|
216
|
+
if (itemEnd > mainAxisStart && placement.y < mainAxisEnd) {
|
|
217
|
+
visiblePool.push(placement);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return visiblePool;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Update masonry configuration without recreating the layout.
|
|
224
|
+
*/
|
|
225
|
+
const updateConfig = (newConfig) => {
|
|
226
|
+
let changed = false;
|
|
227
|
+
if (newConfig.columns !== undefined) {
|
|
228
|
+
const newCols = Math.max(1, Math.floor(newConfig.columns));
|
|
229
|
+
if (newCols !== columns) {
|
|
230
|
+
columns = newCols;
|
|
231
|
+
changed = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (newConfig.gap !== undefined && newConfig.gap !== gap) {
|
|
235
|
+
gap = newConfig.gap;
|
|
236
|
+
changed = true;
|
|
237
|
+
}
|
|
238
|
+
if (newConfig.containerSize !== undefined && newConfig.containerSize !== containerSize) {
|
|
239
|
+
containerSize = newConfig.containerSize;
|
|
240
|
+
changed = true;
|
|
241
|
+
}
|
|
242
|
+
if (changed) {
|
|
243
|
+
recomputeDerived();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
get columns() {
|
|
248
|
+
return columns;
|
|
249
|
+
},
|
|
250
|
+
get gap() {
|
|
251
|
+
return gap;
|
|
252
|
+
},
|
|
253
|
+
get containerSize() {
|
|
254
|
+
return containerSize;
|
|
255
|
+
},
|
|
256
|
+
update: updateConfig,
|
|
257
|
+
calculateLayout,
|
|
258
|
+
getTotalSize,
|
|
259
|
+
getVisibleItems,
|
|
260
|
+
};
|
|
261
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Masonry Plugin
|
|
3
|
+
*
|
|
4
|
+
* Switches from list layout to masonry/Pinterest-style layout with
|
|
5
|
+
* shortest-lane placement. Each item is positioned in the shortest
|
|
6
|
+
* column, creating an organic packed layout.
|
|
7
|
+
*
|
|
8
|
+
* Priority 10 — runs before selection (50) so layout is ready.
|
|
9
|
+
*
|
|
10
|
+
* Restrictions:
|
|
11
|
+
* - Cannot be combined with grid or table plugins
|
|
12
|
+
* - Cannot be combined with reverse: true
|
|
13
|
+
*/
|
|
14
|
+
import type { VListItem } from "../../types";
|
|
15
|
+
import type { VListPlugin } from "../../core/types";
|
|
16
|
+
export interface MasonryPluginConfig {
|
|
17
|
+
columns: number;
|
|
18
|
+
gap?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Optional size function for responsive sizing relative to column width.
|
|
21
|
+
* If omitted, reads sizes from the main config's item.height/width.
|
|
22
|
+
*/
|
|
23
|
+
size?: (index: number, context: MasonryContext) => number;
|
|
24
|
+
}
|
|
25
|
+
export interface MasonryContext {
|
|
26
|
+
columnWidth: number;
|
|
27
|
+
columns: number;
|
|
28
|
+
gap: number;
|
|
29
|
+
containerWidth: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function masonry<T extends VListItem = VListItem>(config: MasonryPluginConfig): VListPlugin<T>;
|
|
32
|
+
//# sourceMappingURL=plugin.d.ts.map
|