masonic
Version:
<hr> <div align="center"> <h1 align="center"> 🧱 masonic </h1>
230 lines (193 loc) • 7.91 kB
JavaScript
import * as React from "react";
import { createIntervalTree } from "./interval-tree";
/**
* This hook creates the grid cell positioner and cache required by `useMasonry()`. This is
* the meat of the grid's layout algorithm, determining which cells to render at a given scroll
* position, as well as where to place new items in the grid.
*
* @param options - Properties that determine the number of columns in the grid, as well
* as their widths.
* @param options.columnWidth
* @param options.width
* @param deps - This hook will create a new positioner, clearing all existing cached positions,
* whenever the dependencies in this list change.
* @param options.columnGutter
* @param options.rowGutter
* @param options.columnCount
* @param options.maxColumnCount
*/
export function usePositioner(_ref, deps) {
let {
width,
columnWidth = 200,
columnGutter = 0,
rowGutter,
columnCount,
maxColumnCount
} = _ref;
if (deps === void 0) {
deps = emptyArr;
}
const initPositioner = () => {
const [computedColumnWidth, computedColumnCount] = getColumns(width, columnWidth, columnGutter, columnCount, maxColumnCount);
return createPositioner(computedColumnCount, computedColumnWidth, columnGutter, rowGutter !== null && rowGutter !== void 0 ? rowGutter : columnGutter);
};
const positionerRef = React.useRef();
if (positionerRef.current === undefined) positionerRef.current = initPositioner();
const prevDeps = React.useRef(deps);
const opts = [width, columnWidth, columnGutter, rowGutter, columnCount, maxColumnCount];
const prevOpts = React.useRef(opts);
const optsChanged = !opts.every((item, i) => prevOpts.current[i] === item);
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
if (deps.length !== prevDeps.current.length) {
throw new Error("usePositioner(): The length of your dependencies array changed.");
}
} // Create a new positioner when the dependencies or sizes change
// Thanks to https://github.com/khmm12 for pointing this out
// https://github.com/jaredLunde/masonic/pull/41
if (optsChanged || !deps.every((item, i) => prevDeps.current[i] === item)) {
const prevPositioner = positionerRef.current;
const positioner = initPositioner();
prevDeps.current = deps;
prevOpts.current = opts;
if (optsChanged) {
const cacheSize = prevPositioner.size();
for (let index = 0; index < cacheSize; index++) {
const pos = prevPositioner.get(index);
positioner.set(index, pos !== void 0 ? pos.height : 0);
}
}
positionerRef.current = positioner;
}
return positionerRef.current;
}
/**
* Creates a cell positioner for the `useMasonry()` hook. The `usePositioner()` hook uses
* this utility under the hood.
*
* @param columnCount - The number of columns in the grid
* @param columnWidth - The width of each column in the grid
* @param columnGutter - The amount of horizontal space between columns in pixels.
* @param rowGutter - The amount of vertical space between cells within a column in pixels (falls back
* to `columnGutter`).
*/
export const createPositioner = function (columnCount, columnWidth, columnGutter, rowGutter) {
if (columnGutter === void 0) {
columnGutter = 0;
}
if (rowGutter === void 0) {
rowGutter = columnGutter;
}
// O(log(n)) lookup of cells to render for a given viewport size
// Store tops and bottoms of each cell for fast intersection lookup.
const intervalTree = createIntervalTree(); // Track the height of each column.
// Layout algorithm below always inserts into the shortest column.
const columnHeights = new Array(columnCount); // Used for O(1) item access
const items = []; // Tracks the item indexes within an individual column
const columnItems = new Array(columnCount);
for (let i = 0; i < columnCount; i++) {
columnHeights[i] = 0;
columnItems[i] = [];
}
return {
columnCount,
columnWidth,
set: function (index, height) {
if (height === void 0) {
height = 0;
}
let column = 0; // finds the shortest column and uses it
for (let i = 1; i < columnHeights.length; i++) {
if (columnHeights[i] < columnHeights[column]) column = i;
}
const top = columnHeights[column] || 0;
columnHeights[column] = top + height + rowGutter;
columnItems[column].push(index);
items[index] = {
left: column * (columnWidth + columnGutter),
top,
height,
column
};
intervalTree.insert(top, top + height, index);
},
get: index => items[index],
// This only updates items in the specific columns that have changed, on and after the
// specific items that have changed
update: updates => {
const columns = new Array(columnCount);
let i = 0,
j = 0; // determines which columns have items that changed, as well as the minimum index
// changed in that column, as all items after that index will have their positions
// affected by the change
for (; i < updates.length - 1; i++) {
const index = updates[i];
const item = items[index];
item.height = updates[++i];
intervalTree.remove(index);
intervalTree.insert(item.top, item.top + item.height, index);
columns[item.column] = columns[item.column] === void 0 ? index : Math.min(index, columns[item.column]);
}
for (i = 0; i < columns.length; i++) {
// bails out if the column didn't change
if (columns[i] === void 0) continue;
const itemsInColumn = columnItems[i]; // the index order is sorted with certainty so binary search is a great solution
// here as opposed to Array.indexOf()
const startIndex = binarySearch(itemsInColumn, columns[i]);
const index = columnItems[i][startIndex];
const startItem = items[index];
columnHeights[i] = startItem.top + startItem.height + rowGutter;
for (j = startIndex + 1; j < itemsInColumn.length; j++) {
const index = itemsInColumn[j];
const item = items[index];
item.top = columnHeights[i];
columnHeights[i] = item.top + item.height + rowGutter;
intervalTree.remove(index);
intervalTree.insert(item.top, item.top + item.height, index);
}
}
},
// Render all cells visible within the viewport range defined.
range: (lo, hi, renderCallback) => intervalTree.search(lo, hi, (index, top) => renderCallback(index, items[index].left, top)),
estimateHeight: (itemCount, defaultItemHeight) => {
const tallestColumn = Math.max(0, Math.max.apply(null, columnHeights));
return itemCount === intervalTree.size ? tallestColumn : tallestColumn + Math.ceil((itemCount - intervalTree.size) / columnCount) * defaultItemHeight;
},
shortestColumn: () => {
if (columnHeights.length > 1) return Math.min.apply(null, columnHeights);
return columnHeights[0] || 0;
},
size() {
return intervalTree.size;
},
all() {
return items;
}
};
};
/* istanbul ignore next */
const binarySearch = (a, y) => {
let l = 0;
let h = a.length - 1;
while (l <= h) {
const m = l + h >>> 1;
const x = a[m];
if (x === y) return m;else if (x <= y) l = m + 1;else h = m - 1;
}
return -1;
};
const getColumns = function (width, minimumWidth, gutter, columnCount, maxColumnCount) {
if (width === void 0) {
width = 0;
}
if (minimumWidth === void 0) {
minimumWidth = 0;
}
if (gutter === void 0) {
gutter = 8;
}
columnCount = columnCount || Math.min(Math.floor((width + gutter) / (minimumWidth + gutter)), maxColumnCount || Infinity) || 1;
const columnWidth = Math.floor((width - gutter * (columnCount - 1)) / columnCount);
return [columnWidth, columnCount];
};
const emptyArr = [];