UNPKG

masonry-snap-grid-layout

Version:

A performant, responsive masonry layout library with smooth animations, dynamic columns, and zero dependencies.

174 lines (172 loc) 5.5 kB
// src/react.tsx import { useEffect, useRef, forwardRef } from "react"; import ReactDOM from "react-dom/client"; // src/MasonrySnapGridLayout.ts var MasonrySnapGridLayout = class { /** * Creates a new MasonrySnapGridLayout instance. * * @param container - The HTML element that will act as the grid container. * @param options - Partial configuration object for layout behavior and rendering. */ constructor(container, options) { /** A reference to all grid item elements currently rendered. */ this.items = []; /** Tracks the current heights of each column to position new items. */ this.columnHeights = []; /** Stores requestAnimationFrame ID for layout updates to prevent redundant calls. */ this.rafId = null; this.container = container; this.options = { gutter: 16, minColWidth: 250, animate: true, transitionDuration: 400, classNames: { container: "masonry-snap-grid-container", item: "masonry-snap-grid-item" }, ...options }; this.container.classList.add(this.options.classNames.container || ""); this.renderItems(); this.setupResizeObserver(); } /** * Renders the provided items into the container. * Clears previous items and re-builds DOM structure. */ renderItems() { this.items.forEach((item) => item.remove()); this.items = []; const fragment = document.createDocumentFragment(); this.options.items.forEach((itemData) => { const itemElement = this.options.renderItem(itemData); itemElement.classList.add(this.options.classNames.item || ""); fragment.appendChild(itemElement); this.items.push(itemElement); }); this.container.appendChild(fragment); this.updateLayout(); } /** * Sets up a ResizeObserver to re-calculate layout when container size changes. */ setupResizeObserver() { this.resizeObserver = new ResizeObserver(() => { if (this.rafId) cancelAnimationFrame(this.rafId); this.rafId = requestAnimationFrame(() => this.updateLayout()); }); this.resizeObserver.observe(this.container); } /** * Calculates item positions and updates their transforms. * Also adjusts the container height to fit all items. */ updateLayout() { const { gutter, minColWidth, animate, transitionDuration } = this.options; const containerWidth = this.container.clientWidth; const columns = Math.max(1, Math.floor((containerWidth + gutter) / (minColWidth + gutter))); const colWidth = (containerWidth - (columns - 1) * gutter) / columns; this.columnHeights = new Array(columns).fill(0); this.items.forEach((item) => { const height = item.offsetHeight; const minCol = this.findShortestColumn(); const x = minCol * (colWidth + gutter); const y = this.columnHeights[minCol]; item.style.width = `${colWidth}px`; item.style.transform = `translate3d(${x}px, ${y}px, 0)`; item.style.transition = animate ? `transform ${transitionDuration}ms ease` : "none"; this.columnHeights[minCol] += height + gutter; }); const maxHeight = Math.max(...this.columnHeights); this.container.style.height = `${maxHeight}px`; } /** * Finds the index of the column with the smallest total height. * * @returns Index of the shortest column. */ findShortestColumn() { return this.columnHeights.indexOf(Math.min(...this.columnHeights)); } /** * Replaces current items with a new set and re-renders the layout. * * @param newItems - New set of data items to render. */ updateItems(newItems) { this.options.items = newItems; this.renderItems(); } /** * Cleans up event listeners, observers, and DOM modifications. * This should be called before discarding the instance. */ destroy() { this.resizeObserver?.disconnect(); if (this.rafId) cancelAnimationFrame(this.rafId); this.container.innerHTML = ""; this.container.removeAttribute("style"); this.container.classList.remove(this.options.classNames.container || ""); } }; // src/react.tsx import { jsx } from "react/jsx-runtime"; var MasonrySnapGridInner = ({ items, renderItem, className, style, ...options }, ref) => { const containerRef = useRef(null); const masonryRef = useRef(null); const rootsRef = useRef(/* @__PURE__ */ new Map()); useEffect(() => { if (!containerRef.current) return; masonryRef.current = new MasonrySnapGridLayout(containerRef.current, { ...options, items, renderItem: (item) => { const div = document.createElement("div"); const root = ReactDOM.createRoot(div); root.render(renderItem(item)); rootsRef.current.set(div, root); return div; } }); return () => { rootsRef.current.forEach((root, div) => { root.unmount(); div.remove(); }); rootsRef.current.clear(); masonryRef.current?.destroy(); masonryRef.current = null; }; }, [options, renderItem]); useEffect(() => { if (masonryRef.current) { masonryRef.current.updateItems(items); } }, [items]); return /* @__PURE__ */ jsx( "div", { ref: containerRef, className, style: { position: "relative", width: "100%", ...style } } ); }; var MasonrySnapGrid = forwardRef(MasonrySnapGridInner); var react_default = MasonrySnapGrid; export { react_default as default }; //# sourceMappingURL=react.js.map