react-fast-masonry
Version:
A fast masonry infinite-scrolling component using the intersection api
276 lines (275 loc) • 11.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FastMasonry = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
/**
* # react-fast-masonry
*
* A react masonry library featuring infinite-scrolling capabilities using [bricks.js](http://callmecavs.com/bricks.js/) and [react-intersection list](https://github.com/researchgate/react-intersection-list). It's based off [react-masonry-infinite](https://github.com/skoob13/react-masonry-infinite), but uses react-intersection-list instead of react-infinite-scroll for faster infinite scrolling.
*
* Since it's based on bricks.js you will need to set all of your items within the masonry container to be the same width. What makes this different then bricks.js is it allows container-queries on it's sizing options rather than media queries. It also allows you to set custom widths at those container query breakpoints.
*
* ## Installing
*
* ```sh
* npm i --save react-fast-masonry
* ```
*
* ## Usage
*
* First import the library `react-fast-masonry`
*
* ```jsx
* import MasonryLayout from "react-fast-masonry";
* ```
*
* Then use it like so.
*
* ```jsx
* <MasonryLayout
* // This is a required prop this defines how wide your gutters and columns are (required) and optionally provides a way to define your column-width (columnWidth) and container-queries (cq)
* sizes={[
* { columns: 1, gutter: 0, columnWidth: "100%" },
* { cq: 768, columns: 2, gutter: 20, columnWidth: 300 },
* { cq: 1024, columns: 3, gutter: 20, columnWidth: 400 }
* ]}
* items={this.state.items}
* // The columnWidth here comes from the sizes prop up above
* renderItem={({ columnWidth }, index: number, key: any) => (
* <div
* style={{
* width: columnWidth
* }}
* key={key}
* >
* {index}
* </div>
* )}
* loadMore={this.loadMore}
* awaitMore={true}
* pageSize={20}
* className="masonry"
* />
* ```
*
* A full fledged example of the above might is given as the Simple masonry layout in the [storybook](https://johnsonjo4531.github.io/react-fast-masonry/?selectedKind=FastMasonry&selectedStory=Simple%20masonry%20layout&full=0&addons=0&stories=1&panelRight=0).
*
*
* @packageDocumentation
*/
const react_1 = __importStar(require("react"));
const bricks_js_1 = __importDefault(require("bricks.js"));
const react_intersection_list_1 = __importDefault(require("@researchgate/react-intersection-list"));
/** The main Fast Masonry component
*
* @example
* ```tsx
* <MasonryLayout
* sizes={[
* { columns: 1, gutter: 0, columnWidth: "100%" },
* { cq: 768, columns: 2, gutter: 20, columnWidth: 300 },
* { cq: 1024, columns: 3, gutter: 20, columnWidth: 400 }
* ]}
* items={this.state.items}
* renderItem={({ columnWidth }, index: number, key: any) => (
* <div
* style={{
* ...this.state.items[index],
* ...MyMasonry.defaultStyles,
* width: columnWidth
* }}
* key={key}
* >
* {index}
* </div>
* )}
* loadMore={this.loadMore}
* awaitMore={true}
* pageSize={20}
* className="masonry"
* />
* ```
*
* @param props - The functions props
*
* @public
*/
const FastMasonry = (_a) => {
var { items, renderItem, loadMore, onIntersection, className = "", pack = false, packedAttribute = "data-packed", position = true, sizes, style = {
margin: "0 auto"
}, outerStyle = {
width: "100%"
}, outerClassName = "" } = _a, props = __rest(_a, ["items", "renderItem", "loadMore", "onIntersection", "className", "pack", "packedAttribute", "position", "sizes", "style", "outerStyle", "outerClassName"]);
const containerComponent = react_1.default.useRef(null);
const masonryContainer = react_1.default.useRef(null);
const [instance, setInstance] = react_1.default.useState(null);
const [computedSize, setComputedSize] = react_1.default.useState({ columns: 1, gutter: 0, columnWidth: 300 });
const [currentSize, setCurrentSize] = react_1.default.useState({ columns: 1, gutter: 0, columnWidth: 300 });
react_1.default.useLayoutEffect(() => {
if (!containerComponent.current || !sizes)
return;
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
if (!entry.contentRect.width)
continue;
let foundIdx = sizes.findIndex((size, i) => { var _a; return entry.contentRect.width < ((_a = size.cq) !== null && _a !== void 0 ? _a : 0); });
if (foundIdx === -1) {
foundIdx = sizes.length;
}
setComputedSize({
columns: sizes[foundIdx - 1].columns,
gutter: sizes[foundIdx - 1].gutter,
columnWidth: sizes[foundIdx - 1].columnWidth
});
return;
}
});
resizeObserver.observe(containerComponent.current);
return () => {
if (!containerComponent.current)
return;
return resizeObserver.unobserve(containerComponent.current);
};
}, [sizes]);
const handleSentinel = react_1.default.useCallback(() => {
if (masonryContainer.current &&
masonryContainer.current.lastChild &&
masonryContainer.current.lastChild instanceof HTMLElement &&
masonryContainer.current.firstChild &&
masonryContainer.current.firstChild instanceof HTMLElement) {
// bricks.js sizes the container based off the last elements width
masonryContainer.current.lastChild.style.width =
masonryContainer.current.firstChild.style.width;
// we must unpack the sentinel or it will not be repositioned.
masonryContainer.current.lastChild.removeAttribute(packedAttribute);
}
}, [packedAttribute, masonryContainer]);
(0, react_1.useEffect)(() => {
handleSentinel();
}, [handleSentinel]);
(0, react_1.useEffect)(() => {
handleSentinel();
if (items.length === 0 && items.length === 0) {
return;
}
if (items.length === 0 && items.length > 0) {
instance === null || instance === void 0 ? void 0 : instance.pack();
return;
}
if (pack) {
instance === null || instance === void 0 ? void 0 : instance.pack();
}
else {
instance === null || instance === void 0 ? void 0 : instance.update();
}
}, [items, instance, handleSentinel, pack]);
function forcePack() {
if (masonryContainer.current) {
instance === null || instance === void 0 ? void 0 : instance.pack();
}
}
const createNewInstance = react_1.default.useCallback((container) => {
const instance = (0, bricks_js_1.default)({
container,
packed: packedAttribute,
sizes: [computedSize],
position
});
setCurrentSize(computedSize);
instance.pack();
setInstance(instance);
return instance;
}, [computedSize, packedAttribute, position]);
(0, react_1.useEffect)(() => {
if (masonryContainer.current !== null &&
(instance === null ||
computedSize.cq !== currentSize.cq ||
computedSize.columns !== currentSize.columns ||
computedSize.gutter !== currentSize.gutter)) {
createNewInstance(masonryContainer.current);
}
return () => {
if (instance) {
instance.resize(false);
}
};
}, [
instance,
createNewInstance,
computedSize.cq,
computedSize.columns,
computedSize.gutter,
currentSize.cq,
currentSize.columns,
currentSize.gutter
]);
function itemsRenderer(items, ref) {
return ((0, jsx_runtime_1.jsx)("div", Object.assign({ ref: Ref => {
if (!Ref)
return;
containerComponent.current = Ref;
}, className: outerClassName, style: Object.assign({}, outerStyle) }, { children: (0, jsx_runtime_1.jsx)("div", Object.assign({ ref: Ref => {
if (!Ref)
return;
ref(Ref);
masonryContainer.current = Ref;
}, className: className, style: Object.assign({ maxWidth: "100%", margin: "0 auto" }, style) }, { children: items }), void 0) }), void 0));
}
function intersection(...args) {
return __awaiter(this, void 0, void 0, function* () {
onIntersection && onIntersection(...args);
if (masonryContainer &&
masonryContainer.current &&
masonryContainer.current.childNodes &&
masonryContainer.current.childNodes.length >= items.length) {
yield loadMore();
}
forcePack();
});
}
return ((0, jsx_runtime_1.jsx)(react_intersection_list_1.default, Object.assign({ items: items, renderItem: (...args) => renderItem(computedSize, ...args), itemsRenderer: itemsRenderer, onIntersection: intersection }, props), void 0));
};
exports.FastMasonry = FastMasonry;
exports.default = exports.FastMasonry;