UNPKG

masonry-snap-grid-layout

Version:

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

201 lines (198 loc) 7.15 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/react.tsx var react_exports = {}; __export(react_exports, { default: () => react_default }); module.exports = __toCommonJS(react_exports); var import_react = require("react"); var import_client = __toESM(require("react-dom/client"), 1); // 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 var import_jsx_runtime = require("react/jsx-runtime"); var MasonrySnapGridInner = ({ items, renderItem, className, style, ...options }, ref) => { const containerRef = (0, import_react.useRef)(null); const masonryRef = (0, import_react.useRef)(null); const rootsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map()); (0, import_react.useEffect)(() => { if (!containerRef.current) return; masonryRef.current = new MasonrySnapGridLayout(containerRef.current, { ...options, items, renderItem: (item) => { const div = document.createElement("div"); const root = import_client.default.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]); (0, import_react.useEffect)(() => { if (masonryRef.current) { masonryRef.current.updateItems(items); } }, [items]); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { ref: containerRef, className, style: { position: "relative", width: "100%", ...style } } ); }; var MasonrySnapGrid = (0, import_react.forwardRef)(MasonrySnapGridInner); var react_default = MasonrySnapGrid; //# sourceMappingURL=react.js.map