smart-masonry-grid 0.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/dist/index.cjs ADDED
@@ -0,0 +1,694 @@
1
+ 'use strict';
2
+
3
+ // src/layout.ts
4
+ function computeLayout(input) {
5
+ const { items, containerWidth, columnCount, gap } = input;
6
+ if (columnCount <= 0 || containerWidth <= 0 || items.length === 0) {
7
+ return { positions: [], columnHeights: [], totalHeight: 0 };
8
+ }
9
+ const columnWidth = (containerWidth - (columnCount - 1) * gap) / columnCount;
10
+ const columnHeights = new Float64Array(columnCount);
11
+ const positions = new Array(items.length);
12
+ for (let i = 0; i < items.length; i++) {
13
+ const item = items[i];
14
+ let shortestCol = 0;
15
+ let shortestHeight = columnHeights[0];
16
+ for (let c = 1; c < columnCount; c++) {
17
+ if (columnHeights[c] < shortestHeight) {
18
+ shortestHeight = columnHeights[c];
19
+ shortestCol = c;
20
+ }
21
+ }
22
+ const left = shortestCol * (columnWidth + gap);
23
+ const top = columnHeights[shortestCol];
24
+ positions[i] = {
25
+ id: item.id,
26
+ index: item.index,
27
+ top,
28
+ left,
29
+ width: columnWidth,
30
+ height: item.height,
31
+ column: shortestCol
32
+ };
33
+ columnHeights[shortestCol] = top + item.height + gap;
34
+ }
35
+ let maxHeight = 0;
36
+ for (let c = 0; c < columnCount; c++) {
37
+ if (columnHeights[c] > maxHeight) {
38
+ maxHeight = columnHeights[c];
39
+ }
40
+ }
41
+ const totalHeight = maxHeight > 0 ? maxHeight - gap : 0;
42
+ return {
43
+ positions,
44
+ columnHeights: Array.from(columnHeights),
45
+ totalHeight
46
+ };
47
+ }
48
+ function resolveColumnCount(containerWidth, strategy, gap) {
49
+ if (strategy.type === "fixed") {
50
+ return Math.max(1, strategy.count);
51
+ }
52
+ if (strategy.type === "responsive") {
53
+ return resolveResponsiveColumns(containerWidth, strategy.breakpoints);
54
+ }
55
+ const count = Math.floor((containerWidth + gap) / (strategy.minColumnWidth + gap));
56
+ return Math.max(1, count);
57
+ }
58
+ function resolveResponsiveColumns(containerWidth, breakpoints) {
59
+ const entries = Object.keys(breakpoints).map((w) => [Number(w), breakpoints[Number(w)]]).sort((a, b) => b[0] - a[0]);
60
+ for (const [minWidth, cols] of entries) {
61
+ if (containerWidth >= minWidth) {
62
+ return Math.max(1, cols);
63
+ }
64
+ }
65
+ return entries.length > 0 ? Math.max(1, entries[entries.length - 1][1]) : 1;
66
+ }
67
+
68
+ // src/virtualizer.ts
69
+ var DEFAULT_CONFIG = {
70
+ overscan: 600,
71
+ estimatedItemHeight: 300
72
+ };
73
+ var Virtualizer = class {
74
+ constructor(config, onRenderItem, onRecycleItem) {
75
+ this.positions = [];
76
+ this.sortedByTop = [];
77
+ this.renderedIndices = /* @__PURE__ */ new Set();
78
+ this.heightEstimates = /* @__PURE__ */ new Map();
79
+ this.scrollTop = 0;
80
+ this.viewportHeight = 0;
81
+ this.totalHeight = 0;
82
+ this.maxMeasuredHeight = 0;
83
+ this.scrollRafId = null;
84
+ this.config = { ...DEFAULT_CONFIG, ...config };
85
+ this.onRenderItem = onRenderItem;
86
+ this.onRecycleItem = onRecycleItem;
87
+ }
88
+ /** Update positions after layout recomputation */
89
+ updatePositions(positions, totalHeight) {
90
+ this.positions = positions;
91
+ this.totalHeight = totalHeight;
92
+ this.rebuildSortedIndex();
93
+ this.update();
94
+ }
95
+ rebuildSortedIndex() {
96
+ this.sortedByTop = this.positions.map((_, i) => i);
97
+ this.sortedByTop.sort((a, b) => this.positions[a].top - this.positions[b].top);
98
+ }
99
+ /** Handle scroll event (throttled via rAF) */
100
+ onScroll(scrollTop, viewportHeight) {
101
+ this.scrollTop = scrollTop;
102
+ this.viewportHeight = viewportHeight;
103
+ if (this.scrollRafId !== null) return;
104
+ this.scrollRafId = requestAnimationFrame(() => {
105
+ this.scrollRafId = null;
106
+ this.update();
107
+ });
108
+ }
109
+ /** Core: diff visible set and mount/unmount items */
110
+ update() {
111
+ const { overscan } = this.config;
112
+ const rangeTop = this.scrollTop - overscan;
113
+ const rangeBottom = this.scrollTop + this.viewportHeight + overscan;
114
+ const visibleIndices = this.findVisibleIndices(rangeTop, rangeBottom);
115
+ const visibleSet = new Set(visibleIndices);
116
+ for (const idx of this.renderedIndices) {
117
+ if (!visibleSet.has(idx)) {
118
+ this.onRecycleItem(idx);
119
+ }
120
+ }
121
+ for (const idx of visibleIndices) {
122
+ if (!this.renderedIndices.has(idx)) {
123
+ const position = this.positions[idx];
124
+ if (position) {
125
+ this.onRenderItem(idx, position);
126
+ }
127
+ }
128
+ }
129
+ this.renderedIndices = visibleSet;
130
+ }
131
+ /**
132
+ * Binary search to find items in [rangeTop, rangeBottom].
133
+ * An item is visible if: item.top + item.height > rangeTop AND item.top < rangeBottom
134
+ */
135
+ findVisibleIndices(rangeTop, rangeBottom) {
136
+ const sorted = this.sortedByTop;
137
+ const positions = this.positions;
138
+ if (sorted.length === 0) return [];
139
+ const searchTop = rangeTop - Math.max(this.maxMeasuredHeight, this.config.estimatedItemHeight);
140
+ let lo = 0;
141
+ let hi = sorted.length;
142
+ while (lo < hi) {
143
+ const mid = lo + hi >>> 1;
144
+ if (positions[sorted[mid]].top < searchTop) {
145
+ lo = mid + 1;
146
+ } else {
147
+ hi = mid;
148
+ }
149
+ }
150
+ const result = [];
151
+ for (let i = lo; i < sorted.length; i++) {
152
+ const idx = sorted[i];
153
+ const pos = positions[idx];
154
+ if (pos.top >= rangeBottom) break;
155
+ if (pos.top + pos.height > rangeTop) {
156
+ result.push(idx);
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+ /** Record a measured height */
162
+ recordMeasurement(id, height) {
163
+ this.heightEstimates.set(id, height);
164
+ if (height > this.maxMeasuredHeight) {
165
+ this.maxMeasuredHeight = height;
166
+ }
167
+ }
168
+ /** Get current visible range */
169
+ getVisibleRange() {
170
+ if (this.renderedIndices.size === 0) {
171
+ return { startIndex: 0, endIndex: 0 };
172
+ }
173
+ let min = Infinity;
174
+ let max = -Infinity;
175
+ for (const idx of this.renderedIndices) {
176
+ if (idx < min) min = idx;
177
+ if (idx > max) max = idx;
178
+ }
179
+ return { startIndex: min, endIndex: max };
180
+ }
181
+ getTotalHeight() {
182
+ return this.totalHeight;
183
+ }
184
+ destroy() {
185
+ if (this.scrollRafId !== null) {
186
+ cancelAnimationFrame(this.scrollRafId);
187
+ }
188
+ this.renderedIndices.clear();
189
+ this.heightEstimates.clear();
190
+ this.positions = [];
191
+ this.sortedByTop = [];
192
+ }
193
+ };
194
+
195
+ // src/observers.ts
196
+ var ObserverManager = class {
197
+ constructor(callbacks) {
198
+ this.containerObserver = null;
199
+ this.itemObserver = null;
200
+ this.elementToId = /* @__PURE__ */ new WeakMap();
201
+ this.pendingItemResizes = /* @__PURE__ */ new Map();
202
+ this.batchScheduled = false;
203
+ this.callbacks = callbacks;
204
+ }
205
+ /** Observe container for width changes */
206
+ observeContainer(container) {
207
+ this.containerObserver = new ResizeObserver((entries) => {
208
+ for (const entry of entries) {
209
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
210
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
211
+ this.callbacks.onContainerResize(width, height);
212
+ }
213
+ });
214
+ this.containerObserver.observe(container);
215
+ }
216
+ /** Initialize the shared item observer (lazy) */
217
+ initItemObserver() {
218
+ this.itemObserver = new ResizeObserver((entries) => {
219
+ for (const entry of entries) {
220
+ const element = entry.target;
221
+ const id = this.elementToId.get(element);
222
+ if (id === void 0) continue;
223
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
224
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
225
+ this.pendingItemResizes.set(id, { element, width, height });
226
+ }
227
+ if (!this.batchScheduled) {
228
+ this.batchScheduled = true;
229
+ queueMicrotask(() => {
230
+ this.batchScheduled = false;
231
+ const batch = new Map(this.pendingItemResizes);
232
+ this.pendingItemResizes.clear();
233
+ for (const [id, { element, width, height }] of batch) {
234
+ this.callbacks.onItemResize(id, element, width, height);
235
+ }
236
+ });
237
+ }
238
+ });
239
+ }
240
+ /** Observe a single item element */
241
+ observeItem(element, id) {
242
+ if (!this.itemObserver) this.initItemObserver();
243
+ this.elementToId.set(element, id);
244
+ this.itemObserver.observe(element);
245
+ }
246
+ /** Stop observing a single item element */
247
+ unobserveItem(element) {
248
+ this.elementToId.delete(element);
249
+ this.itemObserver?.unobserve(element);
250
+ }
251
+ /** Clean up everything */
252
+ destroy() {
253
+ this.containerObserver?.disconnect();
254
+ this.itemObserver?.disconnect();
255
+ this.containerObserver = null;
256
+ this.itemObserver = null;
257
+ this.elementToId = /* @__PURE__ */ new WeakMap();
258
+ this.pendingItemResizes.clear();
259
+ }
260
+ };
261
+
262
+ // src/styles.ts
263
+ var STYLE_ID = "smart-masonry-grid-styles";
264
+ function injectBaseStyles(prefix = "smg") {
265
+ if (typeof document === "undefined") return;
266
+ if (document.getElementById(STYLE_ID)) return;
267
+ const style = document.createElement("style");
268
+ style.id = STYLE_ID;
269
+ style.textContent = `
270
+ .${prefix}-container--ssr {
271
+ column-gap: var(--${prefix}-gap, 16px);
272
+ column-count: var(--${prefix}-columns, 3);
273
+ }
274
+ .${prefix}-container--ssr > .${prefix}-item {
275
+ break-inside: avoid;
276
+ margin-bottom: var(--${prefix}-gap, 16px);
277
+ }
278
+ .${prefix}-container--js {
279
+ position: relative;
280
+ overflow: hidden;
281
+ }
282
+ .${prefix}-container--js > .${prefix}-item {
283
+ position: absolute;
284
+ top: 0;
285
+ left: 0;
286
+ will-change: transform;
287
+ }
288
+ .${prefix}-container--transitioning > .${prefix}-item {
289
+ transition: transform 0.2s ease-out, opacity 0.2s ease-out;
290
+ }
291
+ `;
292
+ document.head.appendChild(style);
293
+ }
294
+ function getSSRStyles(prefix = "smg", columns = 3, gap = 16) {
295
+ return `.${prefix}-container--ssr{column-gap:${gap}px;column-count:${columns}}.${prefix}-container--ssr>.${prefix}-item{break-inside:avoid;margin-bottom:${gap}px}`;
296
+ }
297
+
298
+ // src/utils.ts
299
+ var idCounter = 0;
300
+ function generateId() {
301
+ return `smg-${++idCounter}-${Math.random().toString(36).slice(2, 8)}`;
302
+ }
303
+ function debounce(fn, delayMs) {
304
+ let timeoutId = null;
305
+ const debounced = function(...args) {
306
+ if (timeoutId !== null) clearTimeout(timeoutId);
307
+ timeoutId = setTimeout(() => {
308
+ timeoutId = null;
309
+ fn.apply(this, args);
310
+ }, delayMs);
311
+ };
312
+ debounced.cancel = () => {
313
+ if (timeoutId !== null) {
314
+ clearTimeout(timeoutId);
315
+ timeoutId = null;
316
+ }
317
+ };
318
+ return debounced;
319
+ }
320
+
321
+ // src/masonry.ts
322
+ function resolveOptions(options) {
323
+ const gap = options.gap ?? 16;
324
+ let columns;
325
+ if (typeof options.columns === "number") {
326
+ columns = { type: "fixed", count: options.columns };
327
+ } else if (options.columns) {
328
+ columns = options.columns;
329
+ } else {
330
+ columns = { type: "auto", minColumnWidth: 250 };
331
+ }
332
+ return {
333
+ columns,
334
+ gap,
335
+ virtualize: options.virtualize ?? true,
336
+ virtualizer: {
337
+ overscan: options.virtualizer?.overscan ?? 600,
338
+ estimatedItemHeight: options.virtualizer?.estimatedItemHeight ?? 300
339
+ },
340
+ renderItem: options.renderItem ?? null,
341
+ totalItems: options.totalItems ?? 0,
342
+ ssrFallback: options.ssrFallback ?? false,
343
+ classPrefix: options.classPrefix ?? "smg",
344
+ resizeDebounceMs: options.resizeDebounceMs ?? 100
345
+ };
346
+ }
347
+ var MasonryGrid = class {
348
+ constructor(container, options = {}) {
349
+ // State
350
+ this.items = [];
351
+ this.itemMap = /* @__PURE__ */ new Map();
352
+ this.elementMap = /* @__PURE__ */ new Map();
353
+ this.indexToElement = /* @__PURE__ */ new Map();
354
+ this.currentLayout = null;
355
+ this.containerWidth = 0;
356
+ this.columnCount = 0;
357
+ this.isDestroyed = false;
358
+ // Subsystems
359
+ this.virtualizer = null;
360
+ // Events
361
+ this.listeners = /* @__PURE__ */ new Map();
362
+ this.boundScrollHandler = null;
363
+ this.container = container;
364
+ this.opts = resolveOptions(options);
365
+ this.prefix = this.opts.classPrefix;
366
+ injectBaseStyles(this.prefix);
367
+ this.observers = new ObserverManager({
368
+ onContainerResize: (width) => this.handleContainerResize(width),
369
+ onItemResize: (id, _element, _width, height) => this.handleItemResize(id, height)
370
+ });
371
+ this.debouncedRelayout = debounce(() => {
372
+ this.relayout();
373
+ this.applyPositions();
374
+ }, this.opts.resizeDebounceMs);
375
+ this.scrollContainer = this.findScrollParent(container) ?? window;
376
+ this.init();
377
+ }
378
+ // ==================== INITIALIZATION ====================
379
+ init() {
380
+ const isSSR = this.container.classList.contains(
381
+ `${this.prefix}-container--ssr`
382
+ );
383
+ this.observers.observeContainer(this.container);
384
+ this.containerWidth = this.container.offsetWidth;
385
+ this.columnCount = resolveColumnCount(
386
+ this.containerWidth,
387
+ this.opts.columns,
388
+ this.opts.gap
389
+ );
390
+ if (isSSR) {
391
+ this.hydrateFromSSR();
392
+ } else if (this.opts.virtualize && this.opts.renderItem && this.opts.totalItems > 0) {
393
+ this.container.classList.add(`${this.prefix}-container--js`);
394
+ this.initVirtualized();
395
+ } else {
396
+ this.container.classList.add(`${this.prefix}-container--js`);
397
+ this.initFromExistingChildren();
398
+ }
399
+ }
400
+ hydrateFromSSR() {
401
+ const children = Array.from(this.container.children);
402
+ this.collectItems(children);
403
+ this.relayout();
404
+ requestAnimationFrame(() => {
405
+ this.container.classList.add(`${this.prefix}-container--transitioning`);
406
+ this.container.classList.remove(`${this.prefix}-container--ssr`);
407
+ this.container.classList.add(`${this.prefix}-container--js`);
408
+ this.applyPositions();
409
+ setTimeout(() => {
410
+ this.container.classList.remove(
411
+ `${this.prefix}-container--transitioning`
412
+ );
413
+ }, 250);
414
+ });
415
+ }
416
+ initFromExistingChildren() {
417
+ const children = Array.from(this.container.children);
418
+ if (children.length > 0) {
419
+ this.collectItems(children);
420
+ this.relayout();
421
+ this.applyPositions();
422
+ }
423
+ }
424
+ initVirtualized() {
425
+ this.virtualizer = new Virtualizer(
426
+ this.opts.virtualizer,
427
+ (index, position) => this.renderVirtualItem(index, position),
428
+ (index) => this.recycleVirtualItem(index)
429
+ );
430
+ const estimatedHeight = this.opts.virtualizer.estimatedItemHeight;
431
+ for (let i = 0; i < this.opts.totalItems; i++) {
432
+ this.items.push({
433
+ id: i,
434
+ index: i,
435
+ element: null,
436
+ height: estimatedHeight,
437
+ measured: false
438
+ });
439
+ }
440
+ this.relayout();
441
+ this.bindScrollListener();
442
+ }
443
+ // ==================== LAYOUT ====================
444
+ relayout() {
445
+ if (this.isDestroyed) return;
446
+ const layoutItems = this.items.map((item) => ({
447
+ id: item.id,
448
+ index: item.index,
449
+ height: item.height
450
+ }));
451
+ this.currentLayout = computeLayout({
452
+ items: layoutItems,
453
+ containerWidth: this.containerWidth,
454
+ columnCount: this.columnCount,
455
+ gap: this.opts.gap
456
+ });
457
+ this.container.style.height = `${this.currentLayout.totalHeight}px`;
458
+ if (this.virtualizer) {
459
+ this.virtualizer.updatePositions(
460
+ this.currentLayout.positions,
461
+ this.currentLayout.totalHeight
462
+ );
463
+ }
464
+ this.emit("layout", this.currentLayout);
465
+ }
466
+ applyPositions() {
467
+ if (!this.currentLayout) return;
468
+ for (const pos of this.currentLayout.positions) {
469
+ const element = this.elementMap.get(pos.id);
470
+ if (!element) continue;
471
+ element.style.transform = `translate3d(${pos.left}px, ${pos.top}px, 0)`;
472
+ element.style.width = `${pos.width}px`;
473
+ }
474
+ }
475
+ // ==================== VIRTUAL ITEM LIFECYCLE ====================
476
+ renderVirtualItem(index, position) {
477
+ let element = this.indexToElement.get(index);
478
+ if (!element) {
479
+ element = this.opts.renderItem(index);
480
+ element.classList.add(`${this.prefix}-item`);
481
+ this.container.appendChild(element);
482
+ this.indexToElement.set(index, element);
483
+ this.elementMap.set(position.id, element);
484
+ this.observers.observeItem(element, position.id);
485
+ }
486
+ element.style.transform = `translate3d(${position.left}px, ${position.top}px, 0)`;
487
+ element.style.width = `${position.width}px`;
488
+ element.style.display = "";
489
+ requestAnimationFrame(() => {
490
+ if (this.isDestroyed || !element) return;
491
+ const measuredHeight = element.offsetHeight;
492
+ const item = this.items[index];
493
+ if (item && !item.measured && Math.abs(item.height - measuredHeight) > 1) {
494
+ item.height = measuredHeight;
495
+ item.measured = true;
496
+ this.virtualizer?.recordMeasurement(position.id, measuredHeight);
497
+ this.relayout();
498
+ }
499
+ });
500
+ }
501
+ recycleVirtualItem(index) {
502
+ const element = this.indexToElement.get(index);
503
+ if (element) {
504
+ element.style.display = "none";
505
+ this.observers.unobserveItem(element);
506
+ }
507
+ }
508
+ // ==================== OBSERVER HANDLERS ====================
509
+ handleContainerResize(width) {
510
+ if (Math.abs(width - this.containerWidth) < 1) return;
511
+ this.containerWidth = width;
512
+ this.columnCount = resolveColumnCount(
513
+ width,
514
+ this.opts.columns,
515
+ this.opts.gap
516
+ );
517
+ this.debouncedRelayout();
518
+ this.emit("resize", width, this.columnCount);
519
+ }
520
+ handleItemResize(id, newHeight) {
521
+ const item = this.itemMap.get(id);
522
+ if (!item) return;
523
+ const oldHeight = item.height;
524
+ if (Math.abs(oldHeight - newHeight) < 1) return;
525
+ item.height = newHeight;
526
+ item.measured = true;
527
+ this.emit("itemResize", id, oldHeight, newHeight);
528
+ this.debouncedRelayout();
529
+ }
530
+ // ==================== SCROLL ====================
531
+ bindScrollListener() {
532
+ this.boundScrollHandler = () => {
533
+ const scrollTop = this.scrollContainer === window ? window.scrollY : this.scrollContainer.scrollTop;
534
+ const viewportHeight = this.scrollContainer === window ? window.innerHeight : this.scrollContainer.clientHeight;
535
+ this.virtualizer?.onScroll(scrollTop, viewportHeight);
536
+ const visibleRange = this.virtualizer?.getVisibleRange() ?? {
537
+ startIndex: 0,
538
+ endIndex: 0
539
+ };
540
+ this.emit("scroll", scrollTop, visibleRange);
541
+ };
542
+ this.scrollContainer.addEventListener("scroll", this.boundScrollHandler, {
543
+ passive: true
544
+ });
545
+ this.boundScrollHandler();
546
+ }
547
+ // ==================== PUBLIC API ====================
548
+ /** Add items to the end of the grid */
549
+ append(elements) {
550
+ const startIndex = this.items.length;
551
+ this.collectItems(elements, startIndex);
552
+ this.relayout();
553
+ this.applyPositions();
554
+ }
555
+ /** Add items to the beginning of the grid */
556
+ prepend(elements) {
557
+ const newItems = [];
558
+ elements.forEach((el, i) => {
559
+ const id = generateId();
560
+ el.classList.add(`${this.prefix}-item`);
561
+ this.container.prepend(el);
562
+ const height = el.offsetHeight || this.opts.virtualizer.estimatedItemHeight;
563
+ const item = {
564
+ id,
565
+ index: i,
566
+ element: el,
567
+ height,
568
+ measured: el.offsetHeight > 0
569
+ };
570
+ newItems.push(item);
571
+ this.itemMap.set(id, item);
572
+ this.elementMap.set(id, el);
573
+ this.observers.observeItem(el, id);
574
+ });
575
+ this.items.forEach((item) => {
576
+ item.index += elements.length;
577
+ });
578
+ this.items = [...newItems, ...this.items];
579
+ this.relayout();
580
+ this.applyPositions();
581
+ }
582
+ /** Remove an item by ID */
583
+ remove(id) {
584
+ const item = this.itemMap.get(id);
585
+ if (!item) return;
586
+ const element = this.elementMap.get(id);
587
+ if (element) {
588
+ this.observers.unobserveItem(element);
589
+ element.remove();
590
+ }
591
+ this.items = this.items.filter((i) => i.id !== id);
592
+ this.items.forEach((item2, i) => {
593
+ item2.index = i;
594
+ });
595
+ this.itemMap.delete(id);
596
+ this.elementMap.delete(id);
597
+ this.relayout();
598
+ this.applyPositions();
599
+ }
600
+ /** Force a full relayout */
601
+ refresh() {
602
+ for (const item of this.items) {
603
+ const el = this.elementMap.get(item.id);
604
+ if (el) {
605
+ item.height = el.offsetHeight;
606
+ item.measured = true;
607
+ }
608
+ }
609
+ this.relayout();
610
+ this.applyPositions();
611
+ }
612
+ /** Get current layout data */
613
+ getLayout() {
614
+ return this.currentLayout;
615
+ }
616
+ /** Get the number of columns */
617
+ getColumnCount() {
618
+ return this.columnCount;
619
+ }
620
+ /** Subscribe to events. Returns an unsubscribe function. */
621
+ on(event, callback) {
622
+ if (!this.listeners.has(event)) {
623
+ this.listeners.set(event, /* @__PURE__ */ new Set());
624
+ }
625
+ this.listeners.get(event).add(callback);
626
+ return () => {
627
+ this.listeners.get(event)?.delete(callback);
628
+ };
629
+ }
630
+ emit(event, ...args) {
631
+ const callbacks = this.listeners.get(event);
632
+ if (!callbacks) return;
633
+ for (const cb of callbacks) {
634
+ cb(...args);
635
+ }
636
+ }
637
+ /** Tear down everything */
638
+ destroy() {
639
+ this.isDestroyed = true;
640
+ this.emit("destroy");
641
+ this.observers.destroy();
642
+ this.virtualizer?.destroy();
643
+ this.debouncedRelayout.cancel();
644
+ if (this.boundScrollHandler) {
645
+ this.scrollContainer.removeEventListener("scroll", this.boundScrollHandler);
646
+ }
647
+ this.container.classList.remove(`${this.prefix}-container--js`);
648
+ this.container.style.height = "";
649
+ for (const [, element] of this.elementMap) {
650
+ element.style.transform = "";
651
+ element.style.width = "";
652
+ }
653
+ this.items = [];
654
+ this.itemMap.clear();
655
+ this.elementMap.clear();
656
+ this.indexToElement.clear();
657
+ this.listeners.clear();
658
+ }
659
+ // ==================== HELPERS ====================
660
+ collectItems(elements, startIndex = 0) {
661
+ elements.forEach((el, i) => {
662
+ const id = el.dataset.masonryId ?? generateId();
663
+ el.classList.add(`${this.prefix}-item`);
664
+ const height = el.offsetHeight || this.opts.virtualizer.estimatedItemHeight;
665
+ const item = {
666
+ id,
667
+ index: startIndex + i,
668
+ element: el,
669
+ height,
670
+ measured: el.offsetHeight > 0
671
+ };
672
+ this.items.push(item);
673
+ this.itemMap.set(id, item);
674
+ this.elementMap.set(id, el);
675
+ this.observers.observeItem(el, id);
676
+ });
677
+ }
678
+ findScrollParent(element) {
679
+ let parent = element.parentElement;
680
+ while (parent) {
681
+ const overflow = getComputedStyle(parent).overflowY;
682
+ if (overflow === "auto" || overflow === "scroll") return parent;
683
+ parent = parent.parentElement;
684
+ }
685
+ return null;
686
+ }
687
+ };
688
+
689
+ exports.MasonryGrid = MasonryGrid;
690
+ exports.computeLayout = computeLayout;
691
+ exports.getSSRStyles = getSSRStyles;
692
+ exports.resolveColumnCount = resolveColumnCount;
693
+ //# sourceMappingURL=index.cjs.map
694
+ //# sourceMappingURL=index.cjs.map