masonry-snap-grid-layout 1.0.16 → 1.0.17

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 CHANGED
@@ -10,7 +10,7 @@ A **performant, SSR-friendly** masonry grid layout library with smooth animation
10
10
 
11
11
  ---
12
12
 
13
- ## ✨ What's New (v1.0.16)
13
+ ## ✨ What's New (v1.0.17)
14
14
  ✅ **SSR-Ready Rendering** — On the server, items are rendered as plain HTML so your grid is SEO-friendly and instantly visible.
15
15
  ✅ **Hydration Takeover** — On the client, the library recalculates and animates the masonry layout after hydration.
16
16
  ✅ **Zero Dependencies** — Written in TypeScript, works with React and Vanilla JS.
package/dist/esm/react.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import {
3
3
  useEffect,
4
4
  useRef,
5
- forwardRef
5
+ forwardRef,
6
+ useCallback
6
7
  } from "react";
7
8
  import ReactDOM from "react-dom/client";
8
9
 
@@ -253,7 +254,7 @@ function waitForImages(el, timeout = 1e3) {
253
254
  const finish = () => {
254
255
  if (called) return;
255
256
  called = true;
256
- requestAnimationFrame(() => resolve());
257
+ resolve();
257
258
  };
258
259
  const onLoadOrError = () => {
259
260
  remaining -= 1;
@@ -267,7 +268,7 @@ function waitForImages(el, timeout = 1e3) {
267
268
  img.addEventListener("error", onLoadOrError, { once: true });
268
269
  }
269
270
  });
270
- setTimeout(() => finish(), timeout);
271
+ setTimeout(finish, timeout);
271
272
  });
272
273
  }
273
274
  var MasonrySnapGridInner = ({
@@ -280,68 +281,92 @@ var MasonrySnapGridInner = ({
280
281
  const containerRef = useRef(null);
281
282
  const masonryRef = useRef(null);
282
283
  const rootsRef = useRef(/* @__PURE__ */ new Map());
283
- const serverRenderedItems = /* @__PURE__ */ jsx(Fragment, { children: items.map((item, idx) => (
284
- // Use inline-block to let items flow before masonry takes over.
285
- // Consumers can override with their own classes/styles.
286
- /* @__PURE__ */ jsx("div", { style: { display: "inline-block", verticalAlign: "top" }, children: renderItem(item) }, idx)
287
- )) });
284
+ const isMountedRef = useRef(true);
285
+ const latestRef = useRef(ref);
286
+ latestRef.current = ref;
287
+ const updateForwardedRef = useCallback((instance) => {
288
+ if (latestRef.current) {
289
+ if (typeof latestRef.current === "function") {
290
+ latestRef.current(instance);
291
+ } else {
292
+ latestRef.current.current = instance;
293
+ }
294
+ }
295
+ }, []);
296
+ const serverRenderedItems = /* @__PURE__ */ jsx(Fragment, { children: items.map((item, idx) => /* @__PURE__ */ jsx("div", { style: { display: "inline-block", verticalAlign: "top" }, children: renderItem(item) }, idx)) });
288
297
  useEffect(() => {
289
- if (!containerRef.current) return;
298
+ isMountedRef.current = true;
290
299
  const container = containerRef.current;
300
+ if (!container) return;
301
+ const serverContent = container.cloneNode(true);
291
302
  container.innerHTML = "";
292
- masonryRef.current = new MasonrySnapGridLayout(container, {
293
- ...options,
294
- items,
295
- renderItem: (item) => {
296
- const div = document.createElement("div");
297
- div.style.willChange = "transform, height";
298
- const root = ReactDOM.createRoot(div);
299
- root.render(renderItem(item));
300
- rootsRef.current.set(div, root);
301
- return div;
302
- }
303
- });
303
+ try {
304
+ masonryRef.current = new MasonrySnapGridLayout(container, {
305
+ ...options,
306
+ items,
307
+ renderItem: (item) => {
308
+ const div = document.createElement("div");
309
+ div.style.willChange = "transform, height";
310
+ const root = ReactDOM.createRoot(div);
311
+ root.render(renderItem(item));
312
+ rootsRef.current.set(div, root);
313
+ return div;
314
+ }
315
+ });
316
+ updateForwardedRef({ layout: masonryRef.current });
317
+ } catch (error) {
318
+ console.error("Masonry initialization failed:", error);
319
+ container.replaceWith(serverContent);
320
+ return;
321
+ }
304
322
  let rafId = null;
305
323
  let cancelled = false;
306
324
  const doInitialLayout = async () => {
307
- await new Promise((r) => {
308
- rafId = requestAnimationFrame(() => r());
309
- });
310
- await waitForImages(container, 1e3);
311
- if (cancelled) return;
312
325
  try {
326
+ await new Promise((r) => {
327
+ rafId = requestAnimationFrame(() => r());
328
+ });
329
+ if (cancelled || !isMountedRef.current) return;
330
+ await waitForImages(container, 1e3);
331
+ if (cancelled || !isMountedRef.current) return;
313
332
  masonryRef.current?.updateItems(items);
314
- } catch (e) {
333
+ } catch (error) {
334
+ console.error("Initial layout failed:", error);
315
335
  }
316
336
  };
317
337
  doInitialLayout();
318
338
  return () => {
319
- if (rafId != null) cancelAnimationFrame(rafId);
339
+ isMountedRef.current = false;
320
340
  cancelled = true;
321
- rootsRef.current.forEach((root, div) => {
341
+ if (rafId) cancelAnimationFrame(rafId);
342
+ rootsRef.current.forEach((root, el) => {
322
343
  try {
323
344
  root.unmount();
324
- } catch (err) {
345
+ el.remove();
346
+ } catch (error) {
347
+ console.warn("Error during unmount:", error);
325
348
  }
326
- if (div.parentNode) div.remove();
327
349
  });
328
350
  rootsRef.current.clear();
329
351
  try {
330
352
  masonryRef.current?.destroy();
331
- } catch (err) {
353
+ } catch (error) {
354
+ console.warn("Error during masonry cleanup:", error);
332
355
  }
333
356
  masonryRef.current = null;
357
+ updateForwardedRef(null);
334
358
  };
335
- }, [options, renderItem]);
359
+ }, [options, renderItem, updateForwardedRef]);
336
360
  useEffect(() => {
337
- if (masonryRef.current) {
338
- requestAnimationFrame(() => {
339
- try {
340
- masonryRef.current?.updateItems(items);
341
- } catch (e) {
342
- }
343
- });
344
- }
361
+ if (!masonryRef.current) return;
362
+ const rafId = requestAnimationFrame(() => {
363
+ try {
364
+ masonryRef.current?.updateItems(items);
365
+ } catch (error) {
366
+ console.error("Items update failed:", error);
367
+ }
368
+ });
369
+ return () => cancelAnimationFrame(rafId);
345
370
  }, [items]);
346
371
  return /* @__PURE__ */ jsx(
347
372
  "div",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react.tsx","../../src/MasonrySnapGridLayout.ts"],"sourcesContent":["import React, {\n useEffect,\n useRef,\n forwardRef,\n} from 'react';\nimport ReactDOM from 'react-dom/client';\nimport MasonrySnapGridLayout from './MasonrySnapGridLayout';\nimport { MasonrySnapGridLayoutOptions, MasonrySnapGridRef } from './types';\n\n/**\n * Props for the MasonrySnapGrid React wrapper.\n *\n * @template T - Type of items in the masonry grid.\n */\ninterface MasonrySnapGridProps<T>\n extends Omit<MasonrySnapGridLayoutOptions<T>, 'items' | 'renderItem'> {\n /** The data items to render into the masonry grid. */\n items: T[];\n\n /** Renders a single data item into a React node. */\n renderItem: (item: T) => React.ReactNode;\n\n /** Optional container class name. */\n className?: string;\n\n /** Optional inline styles for the container. */\n style?: React.CSSProperties;\n}\n\n/**\n * Helper: wait for all <img> inside \"el\" to load (or error) with a timeout fallback.\n * Returns a promise that resolves when all images are either loaded or the timeout hits.\n */\nfunction waitForImages(el: HTMLElement, timeout = 1000): Promise<void> {\n return new Promise((resolve) => {\n if (!el) return resolve();\n\n const images = Array.from(el.querySelectorAll('img'));\n if (images.length === 0) return resolve();\n\n let remaining = images.length;\n let called = false;\n\n const finish = () => {\n if (called) return;\n called = true;\n // small microtask delay to ensure layout has applied\n requestAnimationFrame(() => resolve());\n };\n\n const onLoadOrError = () => {\n remaining -= 1;\n if (remaining <= 0) finish();\n };\n\n images.forEach((img) => {\n if (img.complete) {\n // already finished (loaded or errored)\n onLoadOrError();\n } else {\n img.addEventListener('load', onLoadOrError, { once: true });\n img.addEventListener('error', onLoadOrError, { once: true });\n }\n });\n\n // Timeout fallback — in case images hang or take too long\n setTimeout(() => finish(), timeout);\n });\n}\n\n/**\n * React wrapper for MasonrySnapGridLayout that supports SSR-friendly rendering.\n *\n * SSR Strategy:\n * - On the server: render all items normally with React → HTML is SEO-friendly & visible without JS.\n * - On the client: after hydration, remove the static HTML and let MasonrySnapGridLayout take over.\n *\n * Fixes:\n * - Delay initial layout until after paint (rAF) so measurements are correct.\n * - Wait for images to load (with a timeout fallback) before triggering layout.\n */\nconst MasonrySnapGridInner = <T,>(\n {\n items,\n renderItem,\n className,\n style,\n ...options\n }: MasonrySnapGridProps<T>,\n ref: React.ForwardedRef<MasonrySnapGridRef>\n) => {\n /** Ref to the outer container where the masonry layout will be applied. */\n const containerRef = useRef<HTMLDivElement>(null);\n\n /** Ref to hold the underlying non-React Masonry layout instance. */\n const masonryRef = useRef<MasonrySnapGridLayout<T> | null>(null);\n\n /**\n * Stores references to all mounted React roots.\n * - Needed because we're rendering React components into DOM nodes created manually.\n * - Helps us unmount cleanly when the component unmounts.\n */\n const rootsRef = useRef<Map<HTMLElement, ReactDOM.Root>>(new Map());\n\n /**\n * Server-side / initial render:\n * We render items as plain HTML so they are visible for SSR & SEO.\n * Styling here should approximate the final look to minimize CLS.\n */\n const serverRenderedItems = (\n <>\n {items.map((item, idx) => (\n // Use inline-block to let items flow before masonry takes over.\n // Consumers can override with their own classes/styles.\n <div key={idx} style={{ display: 'inline-block', verticalAlign: 'top' }}>\n {renderItem(item)}\n </div>\n ))}\n </>\n );\n\n /**\n * Client takeover effect:\n * - Remove SSR content\n * - Initialize the Masonry instance (but delay the *first layout* until after paint/images)\n */\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n // Clear any SSR static HTML so Masonry can manage DOM properly.\n container.innerHTML = '';\n\n // Create the masonry instance. We pass a renderItem factory that creates\n // DOM nodes and mounts React roots into them. The masonry implementation\n // is expected to insert these returned elements into the layout.\n masonryRef.current = new MasonrySnapGridLayout(container, {\n ...options,\n items,\n renderItem: (item) => {\n const div = document.createElement('div');\n // Optional: set a sensible default style class to avoid flash\n div.style.willChange = 'transform, height';\n const root = ReactDOM.createRoot(div);\n root.render(renderItem(item));\n rootsRef.current.set(div, root);\n return div;\n },\n });\n\n let rafId: number | null = null;\n let cancelled = false;\n\n // Wait for a paint frame, then wait for images within the container, then trigger layout.\n // This prevents the \"stacked\" initial state where items have zero measured height.\n const doInitialLayout = async () => {\n // Ensure at least one paint has happened so layout/measurements are meaningful.\n await new Promise<void>((r) => {\n rafId = requestAnimationFrame(() => r());\n });\n\n // Wait for images inside container (if any) to finish loading or timeout\n await waitForImages(container, 1000);\n\n if (cancelled) return;\n\n // Trigger initial layout. Use updateItems to be conservative and generic.\n // If your masonry has a dedicated `layout()` method, call that instead.\n try {\n masonryRef.current?.updateItems(items);\n } catch (e) {\n // Defensive: if updateItems isn't implemented or throws, ignore to avoid crash.\n // You may want to surface this in dev mode.\n // console.warn('Masonry initial layout failed', e);\n }\n };\n\n // Kick off the layout sequence\n doInitialLayout();\n\n return () => {\n // cancel RAF if pending\n if (rafId != null) cancelAnimationFrame(rafId);\n cancelled = true;\n\n // Unmount all React roots & remove their DOM nodes\n rootsRef.current.forEach((root, div) => {\n try {\n root.unmount();\n } catch (err) {\n // ignore\n }\n // remove from DOM if still there\n if (div.parentNode) div.remove();\n });\n rootsRef.current.clear();\n\n // Destroy the masonry instance\n try {\n masonryRef.current?.destroy();\n } catch (err) {\n // ignore if destroy not present or errors\n }\n masonryRef.current = null;\n };\n // NOTE: We intentionally don't include `items` in this dependency array because:\n // - this effect is the \"takeover\" after initial hydration; re-initializing masonry\n // on every items change would be expensive.\n // - updates to `items` are handled by the separate `useEffect` below.\n // We keep `options` & `renderItem` because changing these should re-init the library.\n }, [options, renderItem]);\n\n /**\n * When items change, ask the masonry instance to update.\n * This avoids a full re-initialization and is much faster.\n */\n useEffect(() => {\n if (masonryRef.current) {\n // Defensive: call updateItems inside RAF so layout happens after paint\n // and measurements are consistent.\n requestAnimationFrame(() => {\n try {\n masonryRef.current?.updateItems(items);\n } catch (e) {\n // swallow errors to avoid crashing the app\n }\n });\n }\n }, [items]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ position: 'relative', width: '100%', ...style }}\n >\n {/* Static SSR-friendly markup that will be removed during hydrate takeover */}\n {serverRenderedItems}\n </div>\n );\n};\n\n/**\n * ForwardRef wrapper so parent components can access MasonrySnapGrid methods.\n */\nconst MasonrySnapGrid = forwardRef(MasonrySnapGridInner) as <T>(\n props: MasonrySnapGridProps<T> & { ref?: React.ForwardedRef<MasonrySnapGridRef> }\n) => ReturnType<typeof MasonrySnapGridInner>;\n\nexport default MasonrySnapGrid;\n","import { MasonrySnapGridLayoutOptions } from './types';\n\nexport default class MasonrySnapGridLayout<T = any> {\n // Main container for the grid\n private readonly container: HTMLElement;\n // Normalized config options with defaults applied\n private readonly options: Required<MasonrySnapGridLayoutOptions<T>>;\n // Active DOM elements currently in the layout\n private items: HTMLElement[] = [];\n // Running height for each column (used for placement calculations)\n private columnHeights: number[] = [];\n // Resize observer to detect container width changes\n private resizeObserver: ResizeObserver | undefined;\n // Tracks a pending animation frame request for layout updates\n private rafId: number | null = null;\n // Cache last measured container width to avoid unnecessary relayouts\n private lastContainerWidth = 0;\n // Pool of DOM elements for recycling between renders (avoids costly re-creation)\n private itemPool: HTMLElement[] = [];\n // Flag to prevent operations after destruction\n private isDestroyed = false;\n\n constructor(container: HTMLElement, options: MasonrySnapGridLayoutOptions<T>) {\n if (!container) {\n throw new Error('Container element is required');\n }\n\n this.container = container;\n // Merge user-provided options with defaults\n this.options = {\n gutter: 16,\n minColWidth: 250,\n animate: true,\n transitionDuration: 400,\n classNames: {\n container: 'masonry-snap-grid-container',\n item: 'masonry-snap-grid-item',\n },\n ...options,\n };\n\n this.init();\n }\n\n /**\n * Initialize layout: applies base classes, renders initial items,\n * and sets up resize monitoring.\n */\n private init(): void {\n if (this.isDestroyed) return;\n\n this.container.classList.add(this.options.classNames.container || '');\n this.renderItems();\n this.setupResizeObserver();\n }\n\n /**\n * Renders items into the container using a pooled DOM strategy:\n * - Avoids DOM churn by reusing elements where possible\n * - Only creates new nodes when needed\n * - Removes unused pool items when shrinking\n */\n private renderItems(): void {\n if (this.isDestroyed) return;\n\n // Remove orphaned elements from the DOM\n this.items.forEach(item => {\n if (!this.options.items.some((_, i) => this.itemPool[i] === item)) {\n item.remove();\n }\n });\n\n this.items = [];\n this.columnHeights = [];\n\n // Use a fragment for batch DOM insertion (better performance)\n const fragment = document.createDocumentFragment();\n this.options.items.forEach((itemData, index) => {\n let itemElement = this.itemPool[index];\n\n if (!itemElement) {\n itemElement = document.createElement('div');\n itemElement.classList.add(this.options.classNames.item || '');\n this.itemPool[index] = itemElement;\n }\n\n // Render content via provided renderItem function\n const content = this.options.renderItem(itemData);\n if (typeof content === 'string') {\n itemElement.innerHTML = content;\n } else if (content instanceof Node) {\n itemElement.innerHTML = '';\n itemElement.appendChild(content);\n }\n\n fragment.appendChild(itemElement);\n this.items.push(itemElement);\n });\n\n // Trim excess pooled items\n while (this.itemPool.length > this.options.items.length) {\n const item = this.itemPool.pop()!;\n item.remove();\n }\n\n this.container.appendChild(fragment);\n this.updateLayout();\n }\n\n /**\n * Sets up a ResizeObserver on the container to trigger re-layout\n * when width changes — throttled to animation frames for performance.\n */\n private setupResizeObserver(): void {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => {\n const newWidth = this.container.clientWidth;\n if (newWidth !== this.lastContainerWidth) {\n this.lastContainerWidth = newWidth;\n this.updateLayout();\n }\n });\n });\n\n this.resizeObserver.observe(this.container);\n }\n\n /**\n * Core layout function:\n * - Calculates number of columns based on container width & min column width\n * - Measures all items to avoid forced reflows during positioning\n * - Positions items in the shortest column to maintain balance\n */\n private updateLayout(): void {\n if (this.isDestroyed || !this.container.isConnected) return;\n\n try {\n const { gutter, minColWidth, animate, transitionDuration } = this.options;\n const containerWidth = this.container.clientWidth;\n\n // Avoid layout if container is hidden or collapsed\n if (containerWidth <= 0) {\n this.container.style.height = '0';\n return;\n }\n\n // Determine column count and width\n const columns = Math.max(1, Math.floor((containerWidth + gutter) / (minColWidth + gutter)));\n const colWidth = (containerWidth - (columns - 1) * gutter) / columns;\n\n // Reset tracking for column heights\n this.columnHeights = new Array(columns).fill(0);\n\n // Measure all items with the new column width before positioning\n const itemHeights = this.measureItems(colWidth);\n\n // Place each item in the shortest available column\n this.positionItems(colWidth, gutter, animate, transitionDuration, itemHeights);\n\n // Adjust container height to fit the tallest column\n this.setContainerHeight(gutter);\n } catch (error) {\n console.error('Masonry layout failed:', error);\n // Fallback: simple vertical stacking\n this.applyFallbackLayout();\n }\n }\n\n /**\n * Measures item heights without affecting layout:\n * - Temporarily forces block layout for accurate measurement\n * - Restores original styles after measuring\n */\n private measureItems(colWidth: number): number[] {\n return this.items.map(item => {\n const originalStyles = {\n display: item.style.display,\n visibility: item.style.visibility,\n position: item.style.position,\n width: item.style.width\n };\n\n item.style.display = 'block';\n item.style.visibility = 'hidden';\n item.style.position = 'absolute';\n item.style.width = `${colWidth}px`;\n\n const height = item.offsetHeight;\n\n Object.assign(item.style, originalStyles);\n return height;\n });\n }\n\n /**\n * Positions items column-by-column:\n * - Chooses the shortest column for each item to maintain balance\n * - Uses transform for GPU-accelerated positioning\n */\n private positionItems(\n colWidth: number,\n gutter: number,\n animate: boolean,\n transitionDuration: number,\n itemHeights: number[]\n ): void {\n this.items.forEach((item, index) => {\n const height = itemHeights[index];\n const minCol = this.findShortestColumn();\n const x = minCol * (colWidth + gutter);\n const y = this.columnHeights[minCol];\n\n item.style.width = `${colWidth}px`;\n item.style.transform = `translate3d(${x}px, ${y}px, 0)`;\n item.style.transition = animate\n ? `transform ${transitionDuration}ms ease`\n : 'none';\n item.style.willChange = 'transform';\n\n this.columnHeights[minCol] += height + gutter;\n });\n }\n\n /**\n * Sets the container height to match the tallest column\n * while subtracting trailing gutter space for a clean edge.\n */\n private setContainerHeight(gutter: number): void {\n const maxHeight = Math.max(0, ...this.columnHeights);\n const containerHeight = maxHeight > 0 ? maxHeight - gutter : 0;\n this.container.style.height = `${containerHeight}px`;\n }\n\n /**\n * Simple fallback layout in case the Masonry calculation fails:\n * stacks items vertically in one column.\n */\n private applyFallbackLayout(): void {\n let top = 0;\n this.items.forEach(item => {\n item.style.transform = `translate3d(0, ${top}px, 0)`;\n top += item.offsetHeight + this.options.gutter;\n });\n this.container.style.height = `${top - this.options.gutter}px`;\n }\n\n /**\n * Finds the column with the least accumulated height.\n */\n private findShortestColumn(): number {\n let minIndex = 0;\n let minHeight = Infinity;\n\n this.columnHeights.forEach((height, index) => {\n if (height < minHeight) {\n minHeight = height;\n minIndex = index;\n }\n });\n\n return minIndex;\n }\n\n /**\n * Public method to replace current items and trigger a full re-render.\n */\n public updateItems(newItems: T[]): void {\n if (this.isDestroyed) return;\n this.options.items = newItems;\n this.renderItems();\n }\n\n /**\n * Cleanly tears down the layout:\n * - Stops observing size changes\n * - Cancels pending animation frames\n * - Clears DOM references and resets container\n */\n public destroy(): void {\n if (this.isDestroyed) return;\n\n this.isDestroyed = true;\n\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n\n this.container.innerHTML = '';\n this.container.removeAttribute('style');\n this.container.classList.remove(this.options.classNames.container || '');\n\n this.items = [];\n this.columnHeights = [];\n this.itemPool = [];\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react.tsx","../../src/MasonrySnapGridLayout.ts"],"sourcesContent":["import React, {\n useEffect,\n useRef,\n forwardRef,\n useCallback,\n} from 'react';\nimport ReactDOM from 'react-dom/client';\nimport MasonrySnapGridLayout from './MasonrySnapGridLayout';\nimport { MasonrySnapGridLayoutOptions, MasonrySnapGridRef } from './types';\n\n/**\n * Props for the MasonrySnapGrid React wrapper.\n * Generic <T> allows layout to work with any item type.\n */\ninterface MasonrySnapGridProps<T>\n extends Omit<MasonrySnapGridLayoutOptions<T>, 'items' | 'renderItem'> {\n items: T[];\n renderItem: (item: T) => React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Utility — Waits until all <img> elements inside a given container\n * have either loaded or errored, or until the timeout expires.\n * This ensures layout measurements are based on final image sizes.\n */\nfunction waitForImages(el: HTMLElement, timeout = 1000): Promise<void> {\n return new Promise((resolve) => {\n if (!el) return resolve();\n\n const images = Array.from(el.querySelectorAll('img'));\n if (images.length === 0) return resolve();\n\n let remaining = images.length;\n let called = false;\n\n const finish = () => {\n if (called) return;\n called = true;\n resolve();\n };\n\n const onLoadOrError = () => {\n remaining -= 1;\n if (remaining <= 0) finish();\n };\n\n images.forEach((img) => {\n if (img.complete) {\n onLoadOrError();\n } else {\n img.addEventListener('load', onLoadOrError, { once: true });\n img.addEventListener('error', onLoadOrError, { once: true });\n }\n });\n\n setTimeout(finish, timeout);\n });\n}\n\n/**\n * React wrapper for MasonrySnapGridLayout\n * ---------------------------------------\n * Handles SSR → CSR transition, forwards ref to parent,\n * and manages async layout initialization after image load.\n */\nconst MasonrySnapGridInner = <T,>(\n {\n items,\n renderItem,\n className,\n style,\n ...options\n }: MasonrySnapGridProps<T>,\n ref: React.ForwardedRef<MasonrySnapGridRef>\n) => {\n // DOM container where the masonry will live\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Underlying vanilla masonry instance\n const masonryRef = useRef<MasonrySnapGridLayout<T> | null>(null);\n\n // Track all React roots rendered inside masonry slots (for cleanup)\n const rootsRef = useRef<Map<HTMLElement, ReactDOM.Root>>(new Map());\n\n // Tracks component mount state to prevent async leaks\n const isMountedRef = useRef(true);\n\n // Latest ref passed from parent (keeps forwardRef stable across renders)\n const latestRef = useRef(ref);\n latestRef.current = ref;\n\n /**\n * Forward ref handling — ensures both function refs\n * and object refs from the parent receive the correct instance.\n */\n const updateForwardedRef = useCallback((instance: MasonrySnapGridRef | null) => {\n if (latestRef.current) {\n if (typeof latestRef.current === 'function') {\n latestRef.current(instance);\n } else {\n latestRef.current.current = instance;\n }\n }\n }, []);\n\n /**\n * Server-rendered placeholder items.\n * Rendered inline so that:\n * - SEO crawlers see real content\n * - No layout shift before hydration\n * - Screen readers have immediate access\n */\n const serverRenderedItems = (\n <>\n {items.map((item, idx) => (\n <div key={idx} style={{ display: 'inline-block', verticalAlign: 'top' }}>\n {renderItem(item)}\n </div>\n ))}\n </>\n );\n\n /**\n * Effect: Client takeover after hydration\n * ----------------------------------------\n * Steps:\n * 1. Clear server-rendered HTML\n * 2. Create and initialize masonry layout\n * 3. Wait for next paint + images load before first layout pass\n */\n useEffect(() => {\n isMountedRef.current = true;\n const container = containerRef.current;\n if (!container) return;\n\n // Save original SSR content in case init fails\n const serverContent = container.cloneNode(true) as HTMLElement;\n container.innerHTML = '';\n\n try {\n // Create Masonry instance with React-powered renderItem\n masonryRef.current = new MasonrySnapGridLayout(container, {\n ...options,\n items,\n renderItem: (item) => {\n const div = document.createElement('div');\n div.style.willChange = 'transform, height'; // Hint for smoother animations\n const root = ReactDOM.createRoot(div);\n root.render(renderItem(item));\n rootsRef.current.set(div, root);\n return div;\n },\n });\n\n // Expose instance to parent via forwarded ref\n updateForwardedRef({ layout: masonryRef.current });\n\n } catch (error) {\n console.error('Masonry initialization failed:', error);\n container.replaceWith(serverContent); // Fallback to SSR markup\n return;\n }\n\n let rafId: number | null = null;\n let cancelled = false;\n\n const doInitialLayout = async () => {\n try {\n // Ensure browser has painted initial DOM\n await new Promise<void>((r) => {\n rafId = requestAnimationFrame(() => r());\n });\n\n if (cancelled || !isMountedRef.current) return;\n\n // Wait for images so item heights are final\n await waitForImages(container, 1000);\n\n if (cancelled || !isMountedRef.current) return;\n\n // Trigger initial layout pass\n masonryRef.current?.updateItems(items);\n } catch (error) {\n console.error('Initial layout failed:', error);\n }\n };\n\n doInitialLayout();\n\n return () => {\n // Cleanup on unmount\n isMountedRef.current = false;\n cancelled = true;\n\n if (rafId) cancelAnimationFrame(rafId);\n\n // Unmount React roots inside masonry slots\n rootsRef.current.forEach((root, el) => {\n try {\n root.unmount();\n el.remove();\n } catch (error) {\n console.warn('Error during unmount:', error);\n }\n });\n rootsRef.current.clear();\n\n // Destroy masonry instance\n try {\n masonryRef.current?.destroy();\n } catch (error) {\n console.warn('Error during masonry cleanup:', error);\n }\n masonryRef.current = null;\n\n // Reset forwarded ref\n updateForwardedRef(null);\n };\n }, [options, renderItem, updateForwardedRef]);\n\n /**\n * Effect: Handle updates when `items` changes\n * --------------------------------------------\n * Avoids full re-init by just telling masonry to refresh layout.\n * Uses rAF to batch updates for better performance.\n */\n useEffect(() => {\n if (!masonryRef.current) return;\n\n const rafId = requestAnimationFrame(() => {\n try {\n masonryRef.current?.updateItems(items);\n } catch (error) {\n console.error('Items update failed:', error);\n }\n });\n\n return () => cancelAnimationFrame(rafId);\n }, [items]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ position: 'relative', width: '100%', ...style }}\n >\n {serverRenderedItems}\n </div>\n );\n};\n\n/**\n * ForwardRef wrapper so parent components can call layout methods.\n */\nconst MasonrySnapGrid = forwardRef(MasonrySnapGridInner) as <T>(\n props: MasonrySnapGridProps<T> & { ref?: React.ForwardedRef<MasonrySnapGridRef> }\n) => ReturnType<typeof MasonrySnapGridInner>;\n\nexport default MasonrySnapGrid;\n","import { MasonrySnapGridLayoutOptions } from './types';\n\nexport default class MasonrySnapGridLayout<T = any> {\n // Main container for the grid\n private readonly container: HTMLElement;\n // Normalized config options with defaults applied\n private readonly options: Required<MasonrySnapGridLayoutOptions<T>>;\n // Active DOM elements currently in the layout\n private items: HTMLElement[] = [];\n // Running height for each column (used for placement calculations)\n private columnHeights: number[] = [];\n // Resize observer to detect container width changes\n private resizeObserver: ResizeObserver | undefined;\n // Tracks a pending animation frame request for layout updates\n private rafId: number | null = null;\n // Cache last measured container width to avoid unnecessary relayouts\n private lastContainerWidth = 0;\n // Pool of DOM elements for recycling between renders (avoids costly re-creation)\n private itemPool: HTMLElement[] = [];\n // Flag to prevent operations after destruction\n private isDestroyed = false;\n\n constructor(container: HTMLElement, options: MasonrySnapGridLayoutOptions<T>) {\n if (!container) {\n throw new Error('Container element is required');\n }\n\n this.container = container;\n // Merge user-provided options with defaults\n this.options = {\n gutter: 16,\n minColWidth: 250,\n animate: true,\n transitionDuration: 400,\n classNames: {\n container: 'masonry-snap-grid-container',\n item: 'masonry-snap-grid-item',\n },\n ...options,\n };\n\n this.init();\n }\n\n /**\n * Initialize layout: applies base classes, renders initial items,\n * and sets up resize monitoring.\n */\n private init(): void {\n if (this.isDestroyed) return;\n\n this.container.classList.add(this.options.classNames.container || '');\n this.renderItems();\n this.setupResizeObserver();\n }\n\n /**\n * Renders items into the container using a pooled DOM strategy:\n * - Avoids DOM churn by reusing elements where possible\n * - Only creates new nodes when needed\n * - Removes unused pool items when shrinking\n */\n private renderItems(): void {\n if (this.isDestroyed) return;\n\n // Remove orphaned elements from the DOM\n this.items.forEach(item => {\n if (!this.options.items.some((_, i) => this.itemPool[i] === item)) {\n item.remove();\n }\n });\n\n this.items = [];\n this.columnHeights = [];\n\n // Use a fragment for batch DOM insertion (better performance)\n const fragment = document.createDocumentFragment();\n this.options.items.forEach((itemData, index) => {\n let itemElement = this.itemPool[index];\n\n if (!itemElement) {\n itemElement = document.createElement('div');\n itemElement.classList.add(this.options.classNames.item || '');\n this.itemPool[index] = itemElement;\n }\n\n // Render content via provided renderItem function\n const content = this.options.renderItem(itemData);\n if (typeof content === 'string') {\n itemElement.innerHTML = content;\n } else if (content instanceof Node) {\n itemElement.innerHTML = '';\n itemElement.appendChild(content);\n }\n\n fragment.appendChild(itemElement);\n this.items.push(itemElement);\n });\n\n // Trim excess pooled items\n while (this.itemPool.length > this.options.items.length) {\n const item = this.itemPool.pop()!;\n item.remove();\n }\n\n this.container.appendChild(fragment);\n this.updateLayout();\n }\n\n /**\n * Sets up a ResizeObserver on the container to trigger re-layout\n * when width changes — throttled to animation frames for performance.\n */\n private setupResizeObserver(): void {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => {\n const newWidth = this.container.clientWidth;\n if (newWidth !== this.lastContainerWidth) {\n this.lastContainerWidth = newWidth;\n this.updateLayout();\n }\n });\n });\n\n this.resizeObserver.observe(this.container);\n }\n\n /**\n * Core layout function:\n * - Calculates number of columns based on container width & min column width\n * - Measures all items to avoid forced reflows during positioning\n * - Positions items in the shortest column to maintain balance\n */\n private updateLayout(): void {\n if (this.isDestroyed || !this.container.isConnected) return;\n\n try {\n const { gutter, minColWidth, animate, transitionDuration } = this.options;\n const containerWidth = this.container.clientWidth;\n\n // Avoid layout if container is hidden or collapsed\n if (containerWidth <= 0) {\n this.container.style.height = '0';\n return;\n }\n\n // Determine column count and width\n const columns = Math.max(1, Math.floor((containerWidth + gutter) / (minColWidth + gutter)));\n const colWidth = (containerWidth - (columns - 1) * gutter) / columns;\n\n // Reset tracking for column heights\n this.columnHeights = new Array(columns).fill(0);\n\n // Measure all items with the new column width before positioning\n const itemHeights = this.measureItems(colWidth);\n\n // Place each item in the shortest available column\n this.positionItems(colWidth, gutter, animate, transitionDuration, itemHeights);\n\n // Adjust container height to fit the tallest column\n this.setContainerHeight(gutter);\n } catch (error) {\n console.error('Masonry layout failed:', error);\n // Fallback: simple vertical stacking\n this.applyFallbackLayout();\n }\n }\n\n /**\n * Measures item heights without affecting layout:\n * - Temporarily forces block layout for accurate measurement\n * - Restores original styles after measuring\n */\n private measureItems(colWidth: number): number[] {\n return this.items.map(item => {\n const originalStyles = {\n display: item.style.display,\n visibility: item.style.visibility,\n position: item.style.position,\n width: item.style.width\n };\n\n item.style.display = 'block';\n item.style.visibility = 'hidden';\n item.style.position = 'absolute';\n item.style.width = `${colWidth}px`;\n\n const height = item.offsetHeight;\n\n Object.assign(item.style, originalStyles);\n return height;\n });\n }\n\n /**\n * Positions items column-by-column:\n * - Chooses the shortest column for each item to maintain balance\n * - Uses transform for GPU-accelerated positioning\n */\n private positionItems(\n colWidth: number,\n gutter: number,\n animate: boolean,\n transitionDuration: number,\n itemHeights: number[]\n ): void {\n this.items.forEach((item, index) => {\n const height = itemHeights[index];\n const minCol = this.findShortestColumn();\n const x = minCol * (colWidth + gutter);\n const y = this.columnHeights[minCol];\n\n item.style.width = `${colWidth}px`;\n item.style.transform = `translate3d(${x}px, ${y}px, 0)`;\n item.style.transition = animate\n ? `transform ${transitionDuration}ms ease`\n : 'none';\n item.style.willChange = 'transform';\n\n this.columnHeights[minCol] += height + gutter;\n });\n }\n\n /**\n * Sets the container height to match the tallest column\n * while subtracting trailing gutter space for a clean edge.\n */\n private setContainerHeight(gutter: number): void {\n const maxHeight = Math.max(0, ...this.columnHeights);\n const containerHeight = maxHeight > 0 ? maxHeight - gutter : 0;\n this.container.style.height = `${containerHeight}px`;\n }\n\n /**\n * Simple fallback layout in case the Masonry calculation fails:\n * stacks items vertically in one column.\n */\n private applyFallbackLayout(): void {\n let top = 0;\n this.items.forEach(item => {\n item.style.transform = `translate3d(0, ${top}px, 0)`;\n top += item.offsetHeight + this.options.gutter;\n });\n this.container.style.height = `${top - this.options.gutter}px`;\n }\n\n /**\n * Finds the column with the least accumulated height.\n */\n private findShortestColumn(): number {\n let minIndex = 0;\n let minHeight = Infinity;\n\n this.columnHeights.forEach((height, index) => {\n if (height < minHeight) {\n minHeight = height;\n minIndex = index;\n }\n });\n\n return minIndex;\n }\n\n /**\n * Public method to replace current items and trigger a full re-render.\n */\n public updateItems(newItems: T[]): void {\n if (this.isDestroyed) return;\n this.options.items = newItems;\n this.renderItems();\n }\n\n /**\n * Cleanly tears down the layout:\n * - Stops observing size changes\n * - Cancels pending animation frames\n * - Clears DOM references and resets container\n */\n public destroy(): void {\n if (this.isDestroyed) return;\n\n this.isDestroyed = true;\n\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n\n this.container.innerHTML = '';\n this.container.removeAttribute('style');\n this.container.classList.remove(this.options.classNames.container || '');\n\n this.items = [];\n this.columnHeights = [];\n this.itemPool = [];\n }\n}\n"],"mappings":"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","names":[]}
package/dist/react.d.cts CHANGED
@@ -4,33 +4,23 @@ import { a as MasonrySnapGridLayoutOptions, b as MasonrySnapGridRef } from './Ma
4
4
 
5
5
  /**
6
6
  * Props for the MasonrySnapGrid React wrapper.
7
- *
8
- * @template T - Type of items in the masonry grid.
7
+ * Generic <T> allows layout to work with any item type.
9
8
  */
10
9
  interface MasonrySnapGridProps<T> extends Omit<MasonrySnapGridLayoutOptions<T>, 'items' | 'renderItem'> {
11
- /** The data items to render into the masonry grid. */
12
10
  items: T[];
13
- /** Renders a single data item into a React node. */
14
11
  renderItem: (item: T) => React.ReactNode;
15
- /** Optional container class name. */
16
12
  className?: string;
17
- /** Optional inline styles for the container. */
18
13
  style?: React.CSSProperties;
19
14
  }
20
15
  /**
21
- * React wrapper for MasonrySnapGridLayout that supports SSR-friendly rendering.
22
- *
23
- * SSR Strategy:
24
- * - On the server: render all items normally with React → HTML is SEO-friendly & visible without JS.
25
- * - On the client: after hydration, remove the static HTML and let MasonrySnapGridLayout take over.
26
- *
27
- * Fixes:
28
- * - Delay initial layout until after paint (rAF) so measurements are correct.
29
- * - Wait for images to load (with a timeout fallback) before triggering layout.
16
+ * React wrapper for MasonrySnapGridLayout
17
+ * ---------------------------------------
18
+ * Handles SSR → CSR transition, forwards ref to parent,
19
+ * and manages async layout initialization after image load.
30
20
  */
31
21
  declare const MasonrySnapGridInner: <T>({ items, renderItem, className, style, ...options }: MasonrySnapGridProps<T>, ref: React.ForwardedRef<MasonrySnapGridRef>) => react_jsx_runtime.JSX.Element;
32
22
  /**
33
- * ForwardRef wrapper so parent components can access MasonrySnapGrid methods.
23
+ * ForwardRef wrapper so parent components can call layout methods.
34
24
  */
35
25
  declare const MasonrySnapGrid: <T>(props: MasonrySnapGridProps<T> & {
36
26
  ref?: React.ForwardedRef<MasonrySnapGridRef>;
package/dist/react.d.ts CHANGED
@@ -4,33 +4,23 @@ import { a as MasonrySnapGridLayoutOptions, b as MasonrySnapGridRef } from './Ma
4
4
 
5
5
  /**
6
6
  * Props for the MasonrySnapGrid React wrapper.
7
- *
8
- * @template T - Type of items in the masonry grid.
7
+ * Generic <T> allows layout to work with any item type.
9
8
  */
10
9
  interface MasonrySnapGridProps<T> extends Omit<MasonrySnapGridLayoutOptions<T>, 'items' | 'renderItem'> {
11
- /** The data items to render into the masonry grid. */
12
10
  items: T[];
13
- /** Renders a single data item into a React node. */
14
11
  renderItem: (item: T) => React.ReactNode;
15
- /** Optional container class name. */
16
12
  className?: string;
17
- /** Optional inline styles for the container. */
18
13
  style?: React.CSSProperties;
19
14
  }
20
15
  /**
21
- * React wrapper for MasonrySnapGridLayout that supports SSR-friendly rendering.
22
- *
23
- * SSR Strategy:
24
- * - On the server: render all items normally with React → HTML is SEO-friendly & visible without JS.
25
- * - On the client: after hydration, remove the static HTML and let MasonrySnapGridLayout take over.
26
- *
27
- * Fixes:
28
- * - Delay initial layout until after paint (rAF) so measurements are correct.
29
- * - Wait for images to load (with a timeout fallback) before triggering layout.
16
+ * React wrapper for MasonrySnapGridLayout
17
+ * ---------------------------------------
18
+ * Handles SSR → CSR transition, forwards ref to parent,
19
+ * and manages async layout initialization after image load.
30
20
  */
31
21
  declare const MasonrySnapGridInner: <T>({ items, renderItem, className, style, ...options }: MasonrySnapGridProps<T>, ref: React.ForwardedRef<MasonrySnapGridRef>) => react_jsx_runtime.JSX.Element;
32
22
  /**
33
- * ForwardRef wrapper so parent components can access MasonrySnapGrid methods.
23
+ * ForwardRef wrapper so parent components can call layout methods.
34
24
  */
35
25
  declare const MasonrySnapGrid: <T>(props: MasonrySnapGridProps<T> & {
36
26
  ref?: React.ForwardedRef<MasonrySnapGridRef>;
package/dist/react.js CHANGED
@@ -283,7 +283,7 @@ function waitForImages(el, timeout = 1e3) {
283
283
  const finish = () => {
284
284
  if (called) return;
285
285
  called = true;
286
- requestAnimationFrame(() => resolve());
286
+ resolve();
287
287
  };
288
288
  const onLoadOrError = () => {
289
289
  remaining -= 1;
@@ -297,7 +297,7 @@ function waitForImages(el, timeout = 1e3) {
297
297
  img.addEventListener("error", onLoadOrError, { once: true });
298
298
  }
299
299
  });
300
- setTimeout(() => finish(), timeout);
300
+ setTimeout(finish, timeout);
301
301
  });
302
302
  }
303
303
  var MasonrySnapGridInner = ({
@@ -310,68 +310,92 @@ var MasonrySnapGridInner = ({
310
310
  const containerRef = (0, import_react.useRef)(null);
311
311
  const masonryRef = (0, import_react.useRef)(null);
312
312
  const rootsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
313
- const serverRenderedItems = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: items.map((item, idx) => (
314
- // Use inline-block to let items flow before masonry takes over.
315
- // Consumers can override with their own classes/styles.
316
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "inline-block", verticalAlign: "top" }, children: renderItem(item) }, idx)
317
- )) });
313
+ const isMountedRef = (0, import_react.useRef)(true);
314
+ const latestRef = (0, import_react.useRef)(ref);
315
+ latestRef.current = ref;
316
+ const updateForwardedRef = (0, import_react.useCallback)((instance) => {
317
+ if (latestRef.current) {
318
+ if (typeof latestRef.current === "function") {
319
+ latestRef.current(instance);
320
+ } else {
321
+ latestRef.current.current = instance;
322
+ }
323
+ }
324
+ }, []);
325
+ const serverRenderedItems = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: items.map((item, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "inline-block", verticalAlign: "top" }, children: renderItem(item) }, idx)) });
318
326
  (0, import_react.useEffect)(() => {
319
- if (!containerRef.current) return;
327
+ isMountedRef.current = true;
320
328
  const container = containerRef.current;
329
+ if (!container) return;
330
+ const serverContent = container.cloneNode(true);
321
331
  container.innerHTML = "";
322
- masonryRef.current = new MasonrySnapGridLayout(container, {
323
- ...options,
324
- items,
325
- renderItem: (item) => {
326
- const div = document.createElement("div");
327
- div.style.willChange = "transform, height";
328
- const root = import_client.default.createRoot(div);
329
- root.render(renderItem(item));
330
- rootsRef.current.set(div, root);
331
- return div;
332
- }
333
- });
332
+ try {
333
+ masonryRef.current = new MasonrySnapGridLayout(container, {
334
+ ...options,
335
+ items,
336
+ renderItem: (item) => {
337
+ const div = document.createElement("div");
338
+ div.style.willChange = "transform, height";
339
+ const root = import_client.default.createRoot(div);
340
+ root.render(renderItem(item));
341
+ rootsRef.current.set(div, root);
342
+ return div;
343
+ }
344
+ });
345
+ updateForwardedRef({ layout: masonryRef.current });
346
+ } catch (error) {
347
+ console.error("Masonry initialization failed:", error);
348
+ container.replaceWith(serverContent);
349
+ return;
350
+ }
334
351
  let rafId = null;
335
352
  let cancelled = false;
336
353
  const doInitialLayout = async () => {
337
- await new Promise((r) => {
338
- rafId = requestAnimationFrame(() => r());
339
- });
340
- await waitForImages(container, 1e3);
341
- if (cancelled) return;
342
354
  try {
355
+ await new Promise((r) => {
356
+ rafId = requestAnimationFrame(() => r());
357
+ });
358
+ if (cancelled || !isMountedRef.current) return;
359
+ await waitForImages(container, 1e3);
360
+ if (cancelled || !isMountedRef.current) return;
343
361
  masonryRef.current?.updateItems(items);
344
- } catch (e) {
362
+ } catch (error) {
363
+ console.error("Initial layout failed:", error);
345
364
  }
346
365
  };
347
366
  doInitialLayout();
348
367
  return () => {
349
- if (rafId != null) cancelAnimationFrame(rafId);
368
+ isMountedRef.current = false;
350
369
  cancelled = true;
351
- rootsRef.current.forEach((root, div) => {
370
+ if (rafId) cancelAnimationFrame(rafId);
371
+ rootsRef.current.forEach((root, el) => {
352
372
  try {
353
373
  root.unmount();
354
- } catch (err) {
374
+ el.remove();
375
+ } catch (error) {
376
+ console.warn("Error during unmount:", error);
355
377
  }
356
- if (div.parentNode) div.remove();
357
378
  });
358
379
  rootsRef.current.clear();
359
380
  try {
360
381
  masonryRef.current?.destroy();
361
- } catch (err) {
382
+ } catch (error) {
383
+ console.warn("Error during masonry cleanup:", error);
362
384
  }
363
385
  masonryRef.current = null;
386
+ updateForwardedRef(null);
364
387
  };
365
- }, [options, renderItem]);
388
+ }, [options, renderItem, updateForwardedRef]);
366
389
  (0, import_react.useEffect)(() => {
367
- if (masonryRef.current) {
368
- requestAnimationFrame(() => {
369
- try {
370
- masonryRef.current?.updateItems(items);
371
- } catch (e) {
372
- }
373
- });
374
- }
390
+ if (!masonryRef.current) return;
391
+ const rafId = requestAnimationFrame(() => {
392
+ try {
393
+ masonryRef.current?.updateItems(items);
394
+ } catch (error) {
395
+ console.error("Items update failed:", error);
396
+ }
397
+ });
398
+ return () => cancelAnimationFrame(rafId);
375
399
  }, [items]);
376
400
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
377
401
  "div",
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react.tsx","../src/MasonrySnapGridLayout.ts"],"sourcesContent":["import React, {\n useEffect,\n useRef,\n forwardRef,\n} from 'react';\nimport ReactDOM from 'react-dom/client';\nimport MasonrySnapGridLayout from './MasonrySnapGridLayout';\nimport { MasonrySnapGridLayoutOptions, MasonrySnapGridRef } from './types';\n\n/**\n * Props for the MasonrySnapGrid React wrapper.\n *\n * @template T - Type of items in the masonry grid.\n */\ninterface MasonrySnapGridProps<T>\n extends Omit<MasonrySnapGridLayoutOptions<T>, 'items' | 'renderItem'> {\n /** The data items to render into the masonry grid. */\n items: T[];\n\n /** Renders a single data item into a React node. */\n renderItem: (item: T) => React.ReactNode;\n\n /** Optional container class name. */\n className?: string;\n\n /** Optional inline styles for the container. */\n style?: React.CSSProperties;\n}\n\n/**\n * Helper: wait for all <img> inside \"el\" to load (or error) with a timeout fallback.\n * Returns a promise that resolves when all images are either loaded or the timeout hits.\n */\nfunction waitForImages(el: HTMLElement, timeout = 1000): Promise<void> {\n return new Promise((resolve) => {\n if (!el) return resolve();\n\n const images = Array.from(el.querySelectorAll('img'));\n if (images.length === 0) return resolve();\n\n let remaining = images.length;\n let called = false;\n\n const finish = () => {\n if (called) return;\n called = true;\n // small microtask delay to ensure layout has applied\n requestAnimationFrame(() => resolve());\n };\n\n const onLoadOrError = () => {\n remaining -= 1;\n if (remaining <= 0) finish();\n };\n\n images.forEach((img) => {\n if (img.complete) {\n // already finished (loaded or errored)\n onLoadOrError();\n } else {\n img.addEventListener('load', onLoadOrError, { once: true });\n img.addEventListener('error', onLoadOrError, { once: true });\n }\n });\n\n // Timeout fallback — in case images hang or take too long\n setTimeout(() => finish(), timeout);\n });\n}\n\n/**\n * React wrapper for MasonrySnapGridLayout that supports SSR-friendly rendering.\n *\n * SSR Strategy:\n * - On the server: render all items normally with React → HTML is SEO-friendly & visible without JS.\n * - On the client: after hydration, remove the static HTML and let MasonrySnapGridLayout take over.\n *\n * Fixes:\n * - Delay initial layout until after paint (rAF) so measurements are correct.\n * - Wait for images to load (with a timeout fallback) before triggering layout.\n */\nconst MasonrySnapGridInner = <T,>(\n {\n items,\n renderItem,\n className,\n style,\n ...options\n }: MasonrySnapGridProps<T>,\n ref: React.ForwardedRef<MasonrySnapGridRef>\n) => {\n /** Ref to the outer container where the masonry layout will be applied. */\n const containerRef = useRef<HTMLDivElement>(null);\n\n /** Ref to hold the underlying non-React Masonry layout instance. */\n const masonryRef = useRef<MasonrySnapGridLayout<T> | null>(null);\n\n /**\n * Stores references to all mounted React roots.\n * - Needed because we're rendering React components into DOM nodes created manually.\n * - Helps us unmount cleanly when the component unmounts.\n */\n const rootsRef = useRef<Map<HTMLElement, ReactDOM.Root>>(new Map());\n\n /**\n * Server-side / initial render:\n * We render items as plain HTML so they are visible for SSR & SEO.\n * Styling here should approximate the final look to minimize CLS.\n */\n const serverRenderedItems = (\n <>\n {items.map((item, idx) => (\n // Use inline-block to let items flow before masonry takes over.\n // Consumers can override with their own classes/styles.\n <div key={idx} style={{ display: 'inline-block', verticalAlign: 'top' }}>\n {renderItem(item)}\n </div>\n ))}\n </>\n );\n\n /**\n * Client takeover effect:\n * - Remove SSR content\n * - Initialize the Masonry instance (but delay the *first layout* until after paint/images)\n */\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n // Clear any SSR static HTML so Masonry can manage DOM properly.\n container.innerHTML = '';\n\n // Create the masonry instance. We pass a renderItem factory that creates\n // DOM nodes and mounts React roots into them. The masonry implementation\n // is expected to insert these returned elements into the layout.\n masonryRef.current = new MasonrySnapGridLayout(container, {\n ...options,\n items,\n renderItem: (item) => {\n const div = document.createElement('div');\n // Optional: set a sensible default style class to avoid flash\n div.style.willChange = 'transform, height';\n const root = ReactDOM.createRoot(div);\n root.render(renderItem(item));\n rootsRef.current.set(div, root);\n return div;\n },\n });\n\n let rafId: number | null = null;\n let cancelled = false;\n\n // Wait for a paint frame, then wait for images within the container, then trigger layout.\n // This prevents the \"stacked\" initial state where items have zero measured height.\n const doInitialLayout = async () => {\n // Ensure at least one paint has happened so layout/measurements are meaningful.\n await new Promise<void>((r) => {\n rafId = requestAnimationFrame(() => r());\n });\n\n // Wait for images inside container (if any) to finish loading or timeout\n await waitForImages(container, 1000);\n\n if (cancelled) return;\n\n // Trigger initial layout. Use updateItems to be conservative and generic.\n // If your masonry has a dedicated `layout()` method, call that instead.\n try {\n masonryRef.current?.updateItems(items);\n } catch (e) {\n // Defensive: if updateItems isn't implemented or throws, ignore to avoid crash.\n // You may want to surface this in dev mode.\n // console.warn('Masonry initial layout failed', e);\n }\n };\n\n // Kick off the layout sequence\n doInitialLayout();\n\n return () => {\n // cancel RAF if pending\n if (rafId != null) cancelAnimationFrame(rafId);\n cancelled = true;\n\n // Unmount all React roots & remove their DOM nodes\n rootsRef.current.forEach((root, div) => {\n try {\n root.unmount();\n } catch (err) {\n // ignore\n }\n // remove from DOM if still there\n if (div.parentNode) div.remove();\n });\n rootsRef.current.clear();\n\n // Destroy the masonry instance\n try {\n masonryRef.current?.destroy();\n } catch (err) {\n // ignore if destroy not present or errors\n }\n masonryRef.current = null;\n };\n // NOTE: We intentionally don't include `items` in this dependency array because:\n // - this effect is the \"takeover\" after initial hydration; re-initializing masonry\n // on every items change would be expensive.\n // - updates to `items` are handled by the separate `useEffect` below.\n // We keep `options` & `renderItem` because changing these should re-init the library.\n }, [options, renderItem]);\n\n /**\n * When items change, ask the masonry instance to update.\n * This avoids a full re-initialization and is much faster.\n */\n useEffect(() => {\n if (masonryRef.current) {\n // Defensive: call updateItems inside RAF so layout happens after paint\n // and measurements are consistent.\n requestAnimationFrame(() => {\n try {\n masonryRef.current?.updateItems(items);\n } catch (e) {\n // swallow errors to avoid crashing the app\n }\n });\n }\n }, [items]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ position: 'relative', width: '100%', ...style }}\n >\n {/* Static SSR-friendly markup that will be removed during hydrate takeover */}\n {serverRenderedItems}\n </div>\n );\n};\n\n/**\n * ForwardRef wrapper so parent components can access MasonrySnapGrid methods.\n */\nconst MasonrySnapGrid = forwardRef(MasonrySnapGridInner) as <T>(\n props: MasonrySnapGridProps<T> & { ref?: React.ForwardedRef<MasonrySnapGridRef> }\n) => ReturnType<typeof MasonrySnapGridInner>;\n\nexport default MasonrySnapGrid;\n","import { MasonrySnapGridLayoutOptions } from './types';\n\nexport default class MasonrySnapGridLayout<T = any> {\n // Main container for the grid\n private readonly container: HTMLElement;\n // Normalized config options with defaults applied\n private readonly options: Required<MasonrySnapGridLayoutOptions<T>>;\n // Active DOM elements currently in the layout\n private items: HTMLElement[] = [];\n // Running height for each column (used for placement calculations)\n private columnHeights: number[] = [];\n // Resize observer to detect container width changes\n private resizeObserver: ResizeObserver | undefined;\n // Tracks a pending animation frame request for layout updates\n private rafId: number | null = null;\n // Cache last measured container width to avoid unnecessary relayouts\n private lastContainerWidth = 0;\n // Pool of DOM elements for recycling between renders (avoids costly re-creation)\n private itemPool: HTMLElement[] = [];\n // Flag to prevent operations after destruction\n private isDestroyed = false;\n\n constructor(container: HTMLElement, options: MasonrySnapGridLayoutOptions<T>) {\n if (!container) {\n throw new Error('Container element is required');\n }\n\n this.container = container;\n // Merge user-provided options with defaults\n this.options = {\n gutter: 16,\n minColWidth: 250,\n animate: true,\n transitionDuration: 400,\n classNames: {\n container: 'masonry-snap-grid-container',\n item: 'masonry-snap-grid-item',\n },\n ...options,\n };\n\n this.init();\n }\n\n /**\n * Initialize layout: applies base classes, renders initial items,\n * and sets up resize monitoring.\n */\n private init(): void {\n if (this.isDestroyed) return;\n\n this.container.classList.add(this.options.classNames.container || '');\n this.renderItems();\n this.setupResizeObserver();\n }\n\n /**\n * Renders items into the container using a pooled DOM strategy:\n * - Avoids DOM churn by reusing elements where possible\n * - Only creates new nodes when needed\n * - Removes unused pool items when shrinking\n */\n private renderItems(): void {\n if (this.isDestroyed) return;\n\n // Remove orphaned elements from the DOM\n this.items.forEach(item => {\n if (!this.options.items.some((_, i) => this.itemPool[i] === item)) {\n item.remove();\n }\n });\n\n this.items = [];\n this.columnHeights = [];\n\n // Use a fragment for batch DOM insertion (better performance)\n const fragment = document.createDocumentFragment();\n this.options.items.forEach((itemData, index) => {\n let itemElement = this.itemPool[index];\n\n if (!itemElement) {\n itemElement = document.createElement('div');\n itemElement.classList.add(this.options.classNames.item || '');\n this.itemPool[index] = itemElement;\n }\n\n // Render content via provided renderItem function\n const content = this.options.renderItem(itemData);\n if (typeof content === 'string') {\n itemElement.innerHTML = content;\n } else if (content instanceof Node) {\n itemElement.innerHTML = '';\n itemElement.appendChild(content);\n }\n\n fragment.appendChild(itemElement);\n this.items.push(itemElement);\n });\n\n // Trim excess pooled items\n while (this.itemPool.length > this.options.items.length) {\n const item = this.itemPool.pop()!;\n item.remove();\n }\n\n this.container.appendChild(fragment);\n this.updateLayout();\n }\n\n /**\n * Sets up a ResizeObserver on the container to trigger re-layout\n * when width changes — throttled to animation frames for performance.\n */\n private setupResizeObserver(): void {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => {\n const newWidth = this.container.clientWidth;\n if (newWidth !== this.lastContainerWidth) {\n this.lastContainerWidth = newWidth;\n this.updateLayout();\n }\n });\n });\n\n this.resizeObserver.observe(this.container);\n }\n\n /**\n * Core layout function:\n * - Calculates number of columns based on container width & min column width\n * - Measures all items to avoid forced reflows during positioning\n * - Positions items in the shortest column to maintain balance\n */\n private updateLayout(): void {\n if (this.isDestroyed || !this.container.isConnected) return;\n\n try {\n const { gutter, minColWidth, animate, transitionDuration } = this.options;\n const containerWidth = this.container.clientWidth;\n\n // Avoid layout if container is hidden or collapsed\n if (containerWidth <= 0) {\n this.container.style.height = '0';\n return;\n }\n\n // Determine column count and width\n const columns = Math.max(1, Math.floor((containerWidth + gutter) / (minColWidth + gutter)));\n const colWidth = (containerWidth - (columns - 1) * gutter) / columns;\n\n // Reset tracking for column heights\n this.columnHeights = new Array(columns).fill(0);\n\n // Measure all items with the new column width before positioning\n const itemHeights = this.measureItems(colWidth);\n\n // Place each item in the shortest available column\n this.positionItems(colWidth, gutter, animate, transitionDuration, itemHeights);\n\n // Adjust container height to fit the tallest column\n this.setContainerHeight(gutter);\n } catch (error) {\n console.error('Masonry layout failed:', error);\n // Fallback: simple vertical stacking\n this.applyFallbackLayout();\n }\n }\n\n /**\n * Measures item heights without affecting layout:\n * - Temporarily forces block layout for accurate measurement\n * - Restores original styles after measuring\n */\n private measureItems(colWidth: number): number[] {\n return this.items.map(item => {\n const originalStyles = {\n display: item.style.display,\n visibility: item.style.visibility,\n position: item.style.position,\n width: item.style.width\n };\n\n item.style.display = 'block';\n item.style.visibility = 'hidden';\n item.style.position = 'absolute';\n item.style.width = `${colWidth}px`;\n\n const height = item.offsetHeight;\n\n Object.assign(item.style, originalStyles);\n return height;\n });\n }\n\n /**\n * Positions items column-by-column:\n * - Chooses the shortest column for each item to maintain balance\n * - Uses transform for GPU-accelerated positioning\n */\n private positionItems(\n colWidth: number,\n gutter: number,\n animate: boolean,\n transitionDuration: number,\n itemHeights: number[]\n ): void {\n this.items.forEach((item, index) => {\n const height = itemHeights[index];\n const minCol = this.findShortestColumn();\n const x = minCol * (colWidth + gutter);\n const y = this.columnHeights[minCol];\n\n item.style.width = `${colWidth}px`;\n item.style.transform = `translate3d(${x}px, ${y}px, 0)`;\n item.style.transition = animate\n ? `transform ${transitionDuration}ms ease`\n : 'none';\n item.style.willChange = 'transform';\n\n this.columnHeights[minCol] += height + gutter;\n });\n }\n\n /**\n * Sets the container height to match the tallest column\n * while subtracting trailing gutter space for a clean edge.\n */\n private setContainerHeight(gutter: number): void {\n const maxHeight = Math.max(0, ...this.columnHeights);\n const containerHeight = maxHeight > 0 ? maxHeight - gutter : 0;\n this.container.style.height = `${containerHeight}px`;\n }\n\n /**\n * Simple fallback layout in case the Masonry calculation fails:\n * stacks items vertically in one column.\n */\n private applyFallbackLayout(): void {\n let top = 0;\n this.items.forEach(item => {\n item.style.transform = `translate3d(0, ${top}px, 0)`;\n top += item.offsetHeight + this.options.gutter;\n });\n this.container.style.height = `${top - this.options.gutter}px`;\n }\n\n /**\n * Finds the column with the least accumulated height.\n */\n private findShortestColumn(): number {\n let minIndex = 0;\n let minHeight = Infinity;\n\n this.columnHeights.forEach((height, index) => {\n if (height < minHeight) {\n minHeight = height;\n minIndex = index;\n }\n });\n\n return minIndex;\n }\n\n /**\n * Public method to replace current items and trigger a full re-render.\n */\n public updateItems(newItems: T[]): void {\n if (this.isDestroyed) return;\n this.options.items = newItems;\n this.renderItems();\n }\n\n /**\n * Cleanly tears down the layout:\n * - Stops observing size changes\n * - Cancels pending animation frames\n * - Clears DOM references and resets container\n */\n public destroy(): void {\n if (this.isDestroyed) return;\n\n this.isDestroyed = true;\n\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n\n this.container.innerHTML = '';\n this.container.removeAttribute('style');\n this.container.classList.remove(this.options.classNames.container || '');\n\n this.items = [];\n this.columnHeights = [];\n this.itemPool = [];\n }\n}\n"],"mappings":"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","names":["ReactDOM"]}
1
+ {"version":3,"sources":["../src/react.tsx","../src/MasonrySnapGridLayout.ts"],"sourcesContent":["import React, {\n useEffect,\n useRef,\n forwardRef,\n useCallback,\n} from 'react';\nimport ReactDOM from 'react-dom/client';\nimport MasonrySnapGridLayout from './MasonrySnapGridLayout';\nimport { MasonrySnapGridLayoutOptions, MasonrySnapGridRef } from './types';\n\n/**\n * Props for the MasonrySnapGrid React wrapper.\n * Generic <T> allows layout to work with any item type.\n */\ninterface MasonrySnapGridProps<T>\n extends Omit<MasonrySnapGridLayoutOptions<T>, 'items' | 'renderItem'> {\n items: T[];\n renderItem: (item: T) => React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Utility — Waits until all <img> elements inside a given container\n * have either loaded or errored, or until the timeout expires.\n * This ensures layout measurements are based on final image sizes.\n */\nfunction waitForImages(el: HTMLElement, timeout = 1000): Promise<void> {\n return new Promise((resolve) => {\n if (!el) return resolve();\n\n const images = Array.from(el.querySelectorAll('img'));\n if (images.length === 0) return resolve();\n\n let remaining = images.length;\n let called = false;\n\n const finish = () => {\n if (called) return;\n called = true;\n resolve();\n };\n\n const onLoadOrError = () => {\n remaining -= 1;\n if (remaining <= 0) finish();\n };\n\n images.forEach((img) => {\n if (img.complete) {\n onLoadOrError();\n } else {\n img.addEventListener('load', onLoadOrError, { once: true });\n img.addEventListener('error', onLoadOrError, { once: true });\n }\n });\n\n setTimeout(finish, timeout);\n });\n}\n\n/**\n * React wrapper for MasonrySnapGridLayout\n * ---------------------------------------\n * Handles SSR → CSR transition, forwards ref to parent,\n * and manages async layout initialization after image load.\n */\nconst MasonrySnapGridInner = <T,>(\n {\n items,\n renderItem,\n className,\n style,\n ...options\n }: MasonrySnapGridProps<T>,\n ref: React.ForwardedRef<MasonrySnapGridRef>\n) => {\n // DOM container where the masonry will live\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Underlying vanilla masonry instance\n const masonryRef = useRef<MasonrySnapGridLayout<T> | null>(null);\n\n // Track all React roots rendered inside masonry slots (for cleanup)\n const rootsRef = useRef<Map<HTMLElement, ReactDOM.Root>>(new Map());\n\n // Tracks component mount state to prevent async leaks\n const isMountedRef = useRef(true);\n\n // Latest ref passed from parent (keeps forwardRef stable across renders)\n const latestRef = useRef(ref);\n latestRef.current = ref;\n\n /**\n * Forward ref handling — ensures both function refs\n * and object refs from the parent receive the correct instance.\n */\n const updateForwardedRef = useCallback((instance: MasonrySnapGridRef | null) => {\n if (latestRef.current) {\n if (typeof latestRef.current === 'function') {\n latestRef.current(instance);\n } else {\n latestRef.current.current = instance;\n }\n }\n }, []);\n\n /**\n * Server-rendered placeholder items.\n * Rendered inline so that:\n * - SEO crawlers see real content\n * - No layout shift before hydration\n * - Screen readers have immediate access\n */\n const serverRenderedItems = (\n <>\n {items.map((item, idx) => (\n <div key={idx} style={{ display: 'inline-block', verticalAlign: 'top' }}>\n {renderItem(item)}\n </div>\n ))}\n </>\n );\n\n /**\n * Effect: Client takeover after hydration\n * ----------------------------------------\n * Steps:\n * 1. Clear server-rendered HTML\n * 2. Create and initialize masonry layout\n * 3. Wait for next paint + images load before first layout pass\n */\n useEffect(() => {\n isMountedRef.current = true;\n const container = containerRef.current;\n if (!container) return;\n\n // Save original SSR content in case init fails\n const serverContent = container.cloneNode(true) as HTMLElement;\n container.innerHTML = '';\n\n try {\n // Create Masonry instance with React-powered renderItem\n masonryRef.current = new MasonrySnapGridLayout(container, {\n ...options,\n items,\n renderItem: (item) => {\n const div = document.createElement('div');\n div.style.willChange = 'transform, height'; // Hint for smoother animations\n const root = ReactDOM.createRoot(div);\n root.render(renderItem(item));\n rootsRef.current.set(div, root);\n return div;\n },\n });\n\n // Expose instance to parent via forwarded ref\n updateForwardedRef({ layout: masonryRef.current });\n\n } catch (error) {\n console.error('Masonry initialization failed:', error);\n container.replaceWith(serverContent); // Fallback to SSR markup\n return;\n }\n\n let rafId: number | null = null;\n let cancelled = false;\n\n const doInitialLayout = async () => {\n try {\n // Ensure browser has painted initial DOM\n await new Promise<void>((r) => {\n rafId = requestAnimationFrame(() => r());\n });\n\n if (cancelled || !isMountedRef.current) return;\n\n // Wait for images so item heights are final\n await waitForImages(container, 1000);\n\n if (cancelled || !isMountedRef.current) return;\n\n // Trigger initial layout pass\n masonryRef.current?.updateItems(items);\n } catch (error) {\n console.error('Initial layout failed:', error);\n }\n };\n\n doInitialLayout();\n\n return () => {\n // Cleanup on unmount\n isMountedRef.current = false;\n cancelled = true;\n\n if (rafId) cancelAnimationFrame(rafId);\n\n // Unmount React roots inside masonry slots\n rootsRef.current.forEach((root, el) => {\n try {\n root.unmount();\n el.remove();\n } catch (error) {\n console.warn('Error during unmount:', error);\n }\n });\n rootsRef.current.clear();\n\n // Destroy masonry instance\n try {\n masonryRef.current?.destroy();\n } catch (error) {\n console.warn('Error during masonry cleanup:', error);\n }\n masonryRef.current = null;\n\n // Reset forwarded ref\n updateForwardedRef(null);\n };\n }, [options, renderItem, updateForwardedRef]);\n\n /**\n * Effect: Handle updates when `items` changes\n * --------------------------------------------\n * Avoids full re-init by just telling masonry to refresh layout.\n * Uses rAF to batch updates for better performance.\n */\n useEffect(() => {\n if (!masonryRef.current) return;\n\n const rafId = requestAnimationFrame(() => {\n try {\n masonryRef.current?.updateItems(items);\n } catch (error) {\n console.error('Items update failed:', error);\n }\n });\n\n return () => cancelAnimationFrame(rafId);\n }, [items]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ position: 'relative', width: '100%', ...style }}\n >\n {serverRenderedItems}\n </div>\n );\n};\n\n/**\n * ForwardRef wrapper so parent components can call layout methods.\n */\nconst MasonrySnapGrid = forwardRef(MasonrySnapGridInner) as <T>(\n props: MasonrySnapGridProps<T> & { ref?: React.ForwardedRef<MasonrySnapGridRef> }\n) => ReturnType<typeof MasonrySnapGridInner>;\n\nexport default MasonrySnapGrid;\n","import { MasonrySnapGridLayoutOptions } from './types';\n\nexport default class MasonrySnapGridLayout<T = any> {\n // Main container for the grid\n private readonly container: HTMLElement;\n // Normalized config options with defaults applied\n private readonly options: Required<MasonrySnapGridLayoutOptions<T>>;\n // Active DOM elements currently in the layout\n private items: HTMLElement[] = [];\n // Running height for each column (used for placement calculations)\n private columnHeights: number[] = [];\n // Resize observer to detect container width changes\n private resizeObserver: ResizeObserver | undefined;\n // Tracks a pending animation frame request for layout updates\n private rafId: number | null = null;\n // Cache last measured container width to avoid unnecessary relayouts\n private lastContainerWidth = 0;\n // Pool of DOM elements for recycling between renders (avoids costly re-creation)\n private itemPool: HTMLElement[] = [];\n // Flag to prevent operations after destruction\n private isDestroyed = false;\n\n constructor(container: HTMLElement, options: MasonrySnapGridLayoutOptions<T>) {\n if (!container) {\n throw new Error('Container element is required');\n }\n\n this.container = container;\n // Merge user-provided options with defaults\n this.options = {\n gutter: 16,\n minColWidth: 250,\n animate: true,\n transitionDuration: 400,\n classNames: {\n container: 'masonry-snap-grid-container',\n item: 'masonry-snap-grid-item',\n },\n ...options,\n };\n\n this.init();\n }\n\n /**\n * Initialize layout: applies base classes, renders initial items,\n * and sets up resize monitoring.\n */\n private init(): void {\n if (this.isDestroyed) return;\n\n this.container.classList.add(this.options.classNames.container || '');\n this.renderItems();\n this.setupResizeObserver();\n }\n\n /**\n * Renders items into the container using a pooled DOM strategy:\n * - Avoids DOM churn by reusing elements where possible\n * - Only creates new nodes when needed\n * - Removes unused pool items when shrinking\n */\n private renderItems(): void {\n if (this.isDestroyed) return;\n\n // Remove orphaned elements from the DOM\n this.items.forEach(item => {\n if (!this.options.items.some((_, i) => this.itemPool[i] === item)) {\n item.remove();\n }\n });\n\n this.items = [];\n this.columnHeights = [];\n\n // Use a fragment for batch DOM insertion (better performance)\n const fragment = document.createDocumentFragment();\n this.options.items.forEach((itemData, index) => {\n let itemElement = this.itemPool[index];\n\n if (!itemElement) {\n itemElement = document.createElement('div');\n itemElement.classList.add(this.options.classNames.item || '');\n this.itemPool[index] = itemElement;\n }\n\n // Render content via provided renderItem function\n const content = this.options.renderItem(itemData);\n if (typeof content === 'string') {\n itemElement.innerHTML = content;\n } else if (content instanceof Node) {\n itemElement.innerHTML = '';\n itemElement.appendChild(content);\n }\n\n fragment.appendChild(itemElement);\n this.items.push(itemElement);\n });\n\n // Trim excess pooled items\n while (this.itemPool.length > this.options.items.length) {\n const item = this.itemPool.pop()!;\n item.remove();\n }\n\n this.container.appendChild(fragment);\n this.updateLayout();\n }\n\n /**\n * Sets up a ResizeObserver on the container to trigger re-layout\n * when width changes — throttled to animation frames for performance.\n */\n private setupResizeObserver(): void {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => {\n const newWidth = this.container.clientWidth;\n if (newWidth !== this.lastContainerWidth) {\n this.lastContainerWidth = newWidth;\n this.updateLayout();\n }\n });\n });\n\n this.resizeObserver.observe(this.container);\n }\n\n /**\n * Core layout function:\n * - Calculates number of columns based on container width & min column width\n * - Measures all items to avoid forced reflows during positioning\n * - Positions items in the shortest column to maintain balance\n */\n private updateLayout(): void {\n if (this.isDestroyed || !this.container.isConnected) return;\n\n try {\n const { gutter, minColWidth, animate, transitionDuration } = this.options;\n const containerWidth = this.container.clientWidth;\n\n // Avoid layout if container is hidden or collapsed\n if (containerWidth <= 0) {\n this.container.style.height = '0';\n return;\n }\n\n // Determine column count and width\n const columns = Math.max(1, Math.floor((containerWidth + gutter) / (minColWidth + gutter)));\n const colWidth = (containerWidth - (columns - 1) * gutter) / columns;\n\n // Reset tracking for column heights\n this.columnHeights = new Array(columns).fill(0);\n\n // Measure all items with the new column width before positioning\n const itemHeights = this.measureItems(colWidth);\n\n // Place each item in the shortest available column\n this.positionItems(colWidth, gutter, animate, transitionDuration, itemHeights);\n\n // Adjust container height to fit the tallest column\n this.setContainerHeight(gutter);\n } catch (error) {\n console.error('Masonry layout failed:', error);\n // Fallback: simple vertical stacking\n this.applyFallbackLayout();\n }\n }\n\n /**\n * Measures item heights without affecting layout:\n * - Temporarily forces block layout for accurate measurement\n * - Restores original styles after measuring\n */\n private measureItems(colWidth: number): number[] {\n return this.items.map(item => {\n const originalStyles = {\n display: item.style.display,\n visibility: item.style.visibility,\n position: item.style.position,\n width: item.style.width\n };\n\n item.style.display = 'block';\n item.style.visibility = 'hidden';\n item.style.position = 'absolute';\n item.style.width = `${colWidth}px`;\n\n const height = item.offsetHeight;\n\n Object.assign(item.style, originalStyles);\n return height;\n });\n }\n\n /**\n * Positions items column-by-column:\n * - Chooses the shortest column for each item to maintain balance\n * - Uses transform for GPU-accelerated positioning\n */\n private positionItems(\n colWidth: number,\n gutter: number,\n animate: boolean,\n transitionDuration: number,\n itemHeights: number[]\n ): void {\n this.items.forEach((item, index) => {\n const height = itemHeights[index];\n const minCol = this.findShortestColumn();\n const x = minCol * (colWidth + gutter);\n const y = this.columnHeights[minCol];\n\n item.style.width = `${colWidth}px`;\n item.style.transform = `translate3d(${x}px, ${y}px, 0)`;\n item.style.transition = animate\n ? `transform ${transitionDuration}ms ease`\n : 'none';\n item.style.willChange = 'transform';\n\n this.columnHeights[minCol] += height + gutter;\n });\n }\n\n /**\n * Sets the container height to match the tallest column\n * while subtracting trailing gutter space for a clean edge.\n */\n private setContainerHeight(gutter: number): void {\n const maxHeight = Math.max(0, ...this.columnHeights);\n const containerHeight = maxHeight > 0 ? maxHeight - gutter : 0;\n this.container.style.height = `${containerHeight}px`;\n }\n\n /**\n * Simple fallback layout in case the Masonry calculation fails:\n * stacks items vertically in one column.\n */\n private applyFallbackLayout(): void {\n let top = 0;\n this.items.forEach(item => {\n item.style.transform = `translate3d(0, ${top}px, 0)`;\n top += item.offsetHeight + this.options.gutter;\n });\n this.container.style.height = `${top - this.options.gutter}px`;\n }\n\n /**\n * Finds the column with the least accumulated height.\n */\n private findShortestColumn(): number {\n let minIndex = 0;\n let minHeight = Infinity;\n\n this.columnHeights.forEach((height, index) => {\n if (height < minHeight) {\n minHeight = height;\n minIndex = index;\n }\n });\n\n return minIndex;\n }\n\n /**\n * Public method to replace current items and trigger a full re-render.\n */\n public updateItems(newItems: T[]): void {\n if (this.isDestroyed) return;\n this.options.items = newItems;\n this.renderItems();\n }\n\n /**\n * Cleanly tears down the layout:\n * - Stops observing size changes\n * - Cancels pending animation frames\n * - Clears DOM references and resets container\n */\n public destroy(): void {\n if (this.isDestroyed) return;\n\n this.isDestroyed = true;\n\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n\n this.container.innerHTML = '';\n this.container.removeAttribute('style');\n this.container.classList.remove(this.options.classNames.container || '');\n\n this.items = [];\n this.columnHeights = [];\n this.itemPool = [];\n }\n}\n"],"mappings":"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","names":["ReactDOM"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masonry-snap-grid-layout",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "A performant, responsive masonry layout library with smooth animations, dynamic columns, and zero dependencies.",
5
5
  "keywords": [
6
6
  "masonry",