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