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
JavaScript
// 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