@zapperwing/pinterest-view
Version:
A Pinterest-style grid layout component for React.js with responsive design, dynamic content support, and bulletproof virtualization
981 lines (931 loc) • 43.1 kB
JavaScript
;
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = exports.GridInline = void 0;
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactSizeme = _interopRequireDefault(require("react-sizeme"));
var _computeLayout = _interopRequireWildcard(require("../utils/computeLayout"));
var _excluded = ["itemKey", "component", "rect", "style", "rtl", "children", "onHeightChange"]; // src/components/StackGrid.js
/* eslint-disable max-classes-per-file */
/* eslint-disable react/default-props-match-prop-types */
/* eslint-disable react/no-unused-prop-types */
/* eslint-disable react/jsx-filename-extension */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-param-reassign */
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); }
function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
var isNumber = function isNumber(v) {
return typeof v === 'number' && Number.isFinite(v);
};
var isPercentageNumber = function isPercentageNumber(v) {
return typeof v === 'string' && /^\d+(\.\d+)?%$/.test(v);
};
var getColumnConfig = function getColumnConfig(containerWidth, columnWidth, gutterWidth) {
if (isNumber(columnWidth)) {
// Keep the column width fixed, only calculate number of columns
var columnCount = Math.floor((containerWidth + gutterWidth) / (columnWidth + gutterWidth));
return {
columnCount: Math.max(1, columnCount),
columnWidth: columnWidth
};
}
if (isPercentageNumber(columnWidth)) {
var percentage = parseFloat(columnWidth) / 100;
var _columnCount = Math.floor(1 / percentage);
var actualColumnWidth = (containerWidth - (_columnCount - 1) * gutterWidth) / _columnCount;
return {
columnCount: _columnCount,
columnWidth: actualColumnWidth
};
}
throw new Error('columnWidth must be a number or percentage string');
};
// Optimized column finding - O(n) instead of O(n*m)
var getShortestColumn = function getShortestColumn(heights) {
var minIndex = 0;
var minHeight = heights[0];
for (var i = 1; i < heights.length; i += 1) {
if (heights[i] < minHeight) {
minHeight = heights[i];
minIndex = i;
}
}
return minIndex;
};
var GridItem = /*#__PURE__*/_react["default"].memo(/*#__PURE__*/_react["default"].forwardRef(function (_ref, _ref2) {
var itemKey = _ref.itemKey,
Element = _ref.component,
_ref$rect = _ref.rect,
rect = _ref$rect === void 0 ? {
top: 0,
left: 0,
width: 0,
height: 0
} : _ref$rect,
style = _ref.style,
rtl = _ref.rtl,
children = _ref.children,
onHeightChange = _ref.onHeightChange,
rest = _objectWithoutProperties(_ref, _excluded);
var itemRef = _react["default"].useRef(null);
_react["default"].useEffect(function () {
if (!itemRef.current || typeof onHeightChange !== 'function') return;
// Debounced height update to prevent cascade of updates
var timeoutId;
var updateHeight = function updateHeight() {
clearTimeout(timeoutId);
timeoutId = setTimeout(function () {
if (!itemRef.current) return;
var _itemRef$current$getB = itemRef.current.getBoundingClientRect(),
height = _itemRef$current$getB.height;
onHeightChange(height);
}, 100); // Debounce by 100ms
};
// Initial height
updateHeight();
// ResizeObserver for height changes - use contentRect to avoid reflow
var ro = new ResizeObserver(function (entries) {
var entry = entries[0];
if (entry) {
var height = entry.contentRect.height;
onHeightChange(height);
}
});
ro.observe(itemRef.current);
return function () {
clearTimeout(timeoutId);
ro.disconnect();
};
}, [onHeightChange, itemKey]);
var itemStyle = _objectSpread(_objectSpread({}, style), {}, {
position: 'absolute',
top: rect.top,
left: rtl ? 'auto' : rect.left,
right: rtl ? rect.left : 'auto',
width: rect.width,
zIndex: 1,
transition: 'none',
// REMOVE all transitions for static layout
contain: 'layout style',
willChange: 'auto'
});
return /*#__PURE__*/_react["default"].createElement(Element, _extends({}, rest, {
ref: function ref(node) {
if (typeof _ref2 === 'function') _ref2(node);else if (_ref2) _ref2.current = node;
itemRef.current = node;
},
className: "grid-item",
style: itemStyle
}), children);
}), function (prevProps, nextProps) {
return prevProps.itemKey === nextProps.itemKey && prevProps.rect.top === nextProps.rect.top && prevProps.rect.left === nextProps.rect.left && prevProps.rect.width === nextProps.rect.width && prevProps.rect.height === nextProps.rect.height && prevProps.rtl === nextProps.rtl && prevProps.children === nextProps.children;
});
GridItem.displayName = 'GridItem';
GridItem.propTypes = {
itemKey: _propTypes["default"].string,
component: _propTypes["default"].string,
rect: _propTypes["default"].shape({
top: _propTypes["default"].number,
left: _propTypes["default"].number,
width: _propTypes["default"].number,
height: _propTypes["default"].number
}),
style: _propTypes["default"].shape({}),
rtl: _propTypes["default"].bool,
children: _propTypes["default"].node,
onHeightChange: _propTypes["default"].func
};
GridItem.defaultProps = {
itemKey: '',
component: 'div',
rect: {
top: 0,
left: 0,
width: 0,
height: 0
},
style: {},
rtl: false,
children: null,
onHeightChange: null
};
var GridInlinePropTypes = {
className: _propTypes["default"].string,
style: _propTypes["default"].shape({}),
component: _propTypes["default"].string,
itemComponent: _propTypes["default"].string,
children: _propTypes["default"].node,
rtl: _propTypes["default"].bool,
onLayout: _propTypes["default"].func,
gridRef: _propTypes["default"].func,
size: _propTypes["default"].shape({
width: _propTypes["default"].number,
height: _propTypes["default"].number,
registerRef: _propTypes["default"].func,
unregisterRef: _propTypes["default"].func
}),
columnWidth: _propTypes["default"].oneOfType([_propTypes["default"].number, _propTypes["default"].string]),
gutterWidth: _propTypes["default"].number,
gutterHeight: _propTypes["default"].number,
virtualized: _propTypes["default"].bool,
debug: _propTypes["default"].bool,
virtualizationBuffer: _propTypes["default"].number,
scrollContainer: _propTypes["default"].instanceOf(HTMLElement)
};
var GridInlineDefaultProps = {
className: '',
style: {},
component: 'div',
itemComponent: 'div',
children: null,
rtl: false,
onLayout: function onLayout() {},
gridRef: null,
size: null,
columnWidth: 150,
gutterWidth: 5,
gutterHeight: 5,
virtualized: false,
debug: true,
virtualizationBuffer: 800,
scrollContainer: null
};
var GridInline = exports.GridInline = /*#__PURE__*/function (_Component) {
function GridInline(props) {
var _this;
_classCallCheck(this, GridInline);
_this = _callSuper(this, GridInline, [props]);
// Throttled debug logging
_defineProperty(_this, "debugLog", function (message) {
var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
var force = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
var debug = _this.props.debug;
if (!debug) return;
var now = Date.now();
var timeSinceLastLog = now - _this.lastLogTime;
// Throttle logs to prevent console spam, but allow forced logs
if (!force && timeSinceLastLog < 1000) return;
_this.lastLogTime = now;
if (data) {
console.log("[StackGrid] ".concat(message), data);
} else {
console.log("[StackGrid] ".concat(message));
}
});
_defineProperty(_this, "handleScroll", function () {
var virtualized = _this.props.virtualized;
if (!_this.mounted || !virtualized || !_this.scroller) return;
// Use requestAnimationFrame for better performance
if (_this.scrollRAF) return;
_this.scrollRAF = requestAnimationFrame(function () {
// Get scroll position from the appropriate container
var scrollTop = _this.scroller === window ? window.pageYOffset || document.documentElement.scrollTop : _this.scroller.scrollTop;
var currentScrollTop = _this.state.scrollTop;
// Always update scroll position for proper virtualization
if (Math.abs(scrollTop - currentScrollTop) > 10) {
// Reduced threshold for better responsiveness
_this.debugLog('Scroll position changed', {
from: currentScrollTop,
to: scrollTop,
delta: scrollTop - currentScrollTop,
scroller: _this.scroller === window ? 'window' : 'custom container'
});
_this.setState(function (prevState) {
return _objectSpread(_objectSpread({}, prevState), {}, {
scrollTop: scrollTop
});
});
}
_this.scrollRAF = null;
});
});
_defineProperty(_this, "handleResize", function () {
if (!_this.mounted) return;
_this.debugLog('Window resize detected');
_this.layout();
});
// Layout method for manual control
_defineProperty(_this, "layout", function () {
var _this$props = _this.props,
children = _this$props.children,
columnWidth = _this$props.columnWidth,
gutterWidth = _this$props.gutterWidth,
gutterHeight = _this$props.gutterHeight,
size = _this$props.size;
var containerWidth = size === null || size === void 0 ? void 0 : size.width;
// Fallback: try to get width from DOM if react-sizeme failed
if (!containerWidth || containerWidth <= 0) {
if (_this.containerRef.current) {
containerWidth = _this.containerRef.current.clientWidth;
console.log('[StackGrid] Using DOM fallback width:', containerWidth);
}
}
if (!containerWidth || containerWidth <= 0) {
console.log('[StackGrid] No container width available, skipping layout');
return;
}
var validChildren = _react["default"].Children.toArray(children).filter(_react.isValidElement);
if (validChildren.length === 0) {
_this.rectsMap.clear();
_this.setState(function (prevState) {
return _objectSpread(_objectSpread({}, prevState), {}, {
rects: [],
height: 0
});
});
return;
}
try {
var _getColumnConfig = getColumnConfig(containerWidth, columnWidth, gutterWidth),
columnCount = _getColumnConfig.columnCount,
actualColumnWidth = _getColumnConfig.columnWidth;
var config = {
columnCount: columnCount,
columnWidth: actualColumnWidth,
gutterWidth: gutterWidth,
gutterHeight: gutterHeight
};
var keys = validChildren.map(function (child) {
return child.key;
});
var rectsObj = (0, _computeLayout["default"])(keys, _this.heightCache, config);
_this.rectsMap = new Map(Object.entries(rectsObj));
var height = (0, _computeLayout.computeContainerHeight)(rectsObj, config);
_this.debugLog('Layout computed', {
items: validChildren.length,
columns: columnCount,
height: height
});
_this.setState(function (prevState) {
return _objectSpread(_objectSpread({}, prevState), {}, {
rects: keys.map(function (key) {
return rectsObj[key];
}),
height: height
});
}, function () {
var onLayout = _this.props.onLayout;
if (typeof onLayout === 'function') {
onLayout({
height: height
});
}
});
} catch (error) {
console.error('Layout computation error:', error);
}
});
_defineProperty(_this, "handleHeightChange", function (key, height) {
// Always cache the height
var oldHeight = _this.heightCache.get(key);
_this.heightCache.set(key, height);
// Track that this key has been measured
_this.measuredKeys.add(key);
// Check if this is the first time we're measuring this item
var isInitialMeasurement = oldHeight === undefined;
// During measurement phase, only collect heights, don't trigger layout updates
if (_this.state.measurementPhase) {
if (isInitialMeasurement) {
_this.debugLog('Measurement phase - height measured', {
key: key,
height: height,
measuredKeysCount: _this.measuredKeys.size
});
}
// Always check for measurement completion, not just on initial measurements
_this.checkMeasurementCompletion();
return; // Don't do anything else during measurement phase
}
// After measurement phase, only trigger layout updates for actual height changes
if (oldHeight !== height && !isInitialMeasurement) {
_this.debugLog('Item height changed (post-measurement)', {
key: key,
from: oldHeight,
to: height
});
// Debounce layout updates to prevent thrashing
if (_this.layoutRequestId) {
cancelAnimationFrame(_this.layoutRequestId);
}
_this.layoutRequestId = requestAnimationFrame(function () {
_this.updateLayoutForHeightChange(key, oldHeight, height);
_this.layoutRequestId = null;
});
}
});
_defineProperty(_this, "checkMeasurementCompletion", function () {
// Check if we have measured all current items
var children = _this.props.children;
var validChildren = _react["default"].Children.toArray(children).filter(_react.isValidElement);
var currentKeys = new Set(validChildren.map(function (child) {
return child.key;
}));
// Only count keys that are still in the current children
var measuredCurrentKeys = Array.from(_this.measuredKeys).filter(function (key) {
return currentKeys.has(key);
});
_this.debugLog('Measurement progress', {
measuredKeys: Array.from(_this.measuredKeys),
currentKeys: Array.from(currentKeys),
measuredCurrentKeys: measuredCurrentKeys,
measuredCurrentCount: measuredCurrentKeys.length,
totalCurrentCount: validChildren.length
});
if (measuredCurrentKeys.length >= validChildren.length) {
// All current items measured - transition to virtualized phase
_this.debugLog('All current items measured, transitioning to virtualized phase');
_this.finalizeMeasurementPhase();
}
});
_defineProperty(_this, "finalizeMeasurementPhase", function () {
// Clear measurement timeout
if (_this.measurementTimeout) {
clearTimeout(_this.measurementTimeout);
_this.measurementTimeout = null;
}
// Calculate the final layout with all measured heights
_this.layout();
// Transition to virtualized phase
_this.setState(function (prevState) {
return _objectSpread(_objectSpread({}, prevState), {}, {
measurementPhase: false,
allItemsMeasured: true
});
}, function () {
_this.debugLog('Measurement phase complete, virtualization active');
});
});
_defineProperty(_this, "updateLayoutForHeightChange", function (changedKey, oldHeight, newHeight) {
if (!_this.mounted) return;
var _this$props2 = _this.props,
children = _this$props2.children,
columnWidth = _this$props2.columnWidth,
gutterWidth = _this$props2.gutterWidth,
gutterHeight = _this$props2.gutterHeight,
size = _this$props2.size;
var containerWidth = size === null || size === void 0 ? void 0 : size.width;
// Fallback: try to get width from DOM if react-sizeme failed
if (!containerWidth || containerWidth <= 0) {
if (_this.containerRef.current) {
containerWidth = _this.containerRef.current.clientWidth;
}
}
if (!containerWidth || containerWidth <= 0) return;
var validChildren = _react["default"].Children.toArray(children).filter(_react.isValidElement);
var changedIndex = validChildren.findIndex(function (child) {
return child.key === changedKey;
});
if (changedIndex === -1) return;
try {
var _getColumnConfig2 = getColumnConfig(containerWidth, columnWidth, gutterWidth),
columnCount = _getColumnConfig2.columnCount,
actualColumnWidth = _getColumnConfig2.columnWidth;
// Recalculate layout from the changed item onwards
var columnHeights = new Array(columnCount).fill(0);
var rects = [];
// First pass: calculate positions up to the changed item
for (var i = 0; i < changedIndex; i += 1) {
var child = validChildren[i];
var itemHeight = _this.heightCache.get(child.key) || 200;
// Find shortest column
var shortestColumnIndex = getShortestColumn(columnHeights);
var left = shortestColumnIndex * (actualColumnWidth + gutterWidth);
var top = columnHeights[shortestColumnIndex];
columnHeights[shortestColumnIndex] = top + itemHeight + gutterHeight;
rects.push({
top: top,
left: left,
width: actualColumnWidth,
height: itemHeight
});
}
// Second pass: recalculate from changed item onwards
for (var _i = changedIndex; _i < validChildren.length; _i += 1) {
var _child = validChildren[_i];
var _itemHeight = _this.heightCache.get(_child.key) || 200;
// Find shortest column
var _shortestColumnIndex = getShortestColumn(columnHeights);
var _left = _shortestColumnIndex * (actualColumnWidth + gutterWidth);
var _top = columnHeights[_shortestColumnIndex];
columnHeights[_shortestColumnIndex] = _top + _itemHeight + gutterHeight;
rects.push({
top: _top,
left: _left,
width: actualColumnWidth,
height: _itemHeight
});
}
var height = Math.max.apply(Math, _toConsumableArray(columnHeights)) - gutterHeight;
_this.debugLog('Layout updated for height change', {
changedKey: changedKey,
oldHeight: oldHeight,
newHeight: newHeight,
totalHeight: height
});
_this.setState(function (prevState) {
return _objectSpread(_objectSpread({}, prevState), {}, {
rects: rects,
height: height
});
}, function () {
var onLayout = _this.props.onLayout;
if (typeof onLayout === 'function') {
onLayout({
height: height
});
}
});
} catch (error) {
console.error('Layout update error:', error);
}
});
_this.state = {
rects: [],
height: 0,
scrollTop: 0,
measurementPhase: true,
// Start in measurement phase
allItemsMeasured: false // Track when all items are measured
};
_this.containerRef = /*#__PURE__*/_react["default"].createRef();
_this.measurementContainerRef = /*#__PURE__*/_react["default"].createRef(); // Hidden container for measurements
_this.heightCache = new Map();
_this.columnAssignments = new Map();
_this.mounted = false;
_this.layoutRequestId = null;
_this.scrollRAF = null;
_this.lastLogTime = 0;
_this.scroller = props.scrollContainer || window;
// Layout system
_this.rectsMap = new Map();
_this.measuredKeys = new Set();
_this.measurementTimeout = null; // Timeout fallback for measurement phase
return _this;
}
_inherits(GridInline, _Component);
return _createClass(GridInline, [{
key: "componentDidMount",
value: function componentDidMount() {
var _size$registerRef,
_this2 = this;
this.mounted = true;
var _this$props3 = this.props,
size = _this$props3.size,
gridRef = _this$props3.gridRef,
virtualized = _this$props3.virtualized,
columnWidth = _this$props3.columnWidth,
children = _this$props3.children,
scrollContainer = _this$props3.scrollContainer;
size === null || size === void 0 || (_size$registerRef = size.registerRef) === null || _size$registerRef === void 0 || _size$registerRef.call(size, this);
gridRef === null || gridRef === void 0 || gridRef(this);
// Update scroller if it changed
this.scroller = scrollContainer || window;
this.debugLog('Component mounted', {
virtualized: virtualized,
columnWidth: columnWidth,
childrenCount: _react["default"].Children.count(children),
scrollContainer: scrollContainer ? 'custom' : 'window'
}, true);
// Listen to scroll events only if virtualized
if (virtualized) {
this.scroller.addEventListener('scroll', this.handleScroll, {
passive: true
});
this.debugLog('Scroll listener attached (virtualized mode)', {
scroller: this.scroller === window ? 'window' : 'custom container'
});
}
window.addEventListener('resize', this.handleResize, {
passive: true
});
// Set up ResizeObserver fallback for when react-sizeme fails
if (this.containerRef.current) {
this.resizeObserver = new ResizeObserver(function (entries) {
var entry = entries[0];
if (entry && _this2.mounted) {
var width = entry.contentRect.width;
if (width > 0 && (!_this2.props.size || !_this2.props.size.width)) {
console.log('[StackGrid] ResizeObserver fallback: width =', width);
// Force a layout update with the measured width
_this2.forceUpdate();
}
}
});
this.resizeObserver.observe(this.containerRef.current);
}
// Initial layout using new system
this.layout();
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
var _prevProps$size,
_this3 = this;
var _this$props4 = this.props,
children = _this$props4.children,
size = _this$props4.size,
columnWidth = _this$props4.columnWidth,
gutterWidth = _this$props4.gutterWidth,
gutterHeight = _this$props4.gutterHeight,
scrollContainer = _this$props4.scrollContainer,
virtualized = _this$props4.virtualized;
var childrenChanged = prevProps.children !== children;
var sizeChanged = ((_prevProps$size = prevProps.size) === null || _prevProps$size === void 0 ? void 0 : _prevProps$size.width) !== (size === null || size === void 0 ? void 0 : size.width);
var layoutPropsChanged = prevProps.columnWidth !== columnWidth || prevProps.gutterWidth !== gutterWidth || prevProps.gutterHeight !== gutterHeight;
var scrollContainerChanged = prevProps.scrollContainer !== scrollContainer;
var virtualizedChanged = prevProps.virtualized !== virtualized;
// If the scroll container or virtualization state has changed, we need to update listeners.
if ((scrollContainerChanged || virtualizedChanged) && this.mounted) {
this.debugLog('Updating scroll listeners', {
containerChanged: scrollContainerChanged,
virtualizedChanged: virtualizedChanged
});
// 1. Always try to remove the listener from the OLD scroller,
// but only if virtualization was previously enabled.
if (prevProps.virtualized) {
var oldScroller = prevProps.scrollContainer || window;
oldScroller.removeEventListener('scroll', this.handleScroll);
this.debugLog('Removed scroll listener from', {
scroller: oldScroller === window ? 'window' : 'old custom container'
});
}
// 2. Update the internal scroller reference to the NEW one.
this.scroller = scrollContainer || window;
// 3. Always try to add the listener to the NEW scroller,
// but only if virtualization is currently enabled.
if (virtualized) {
this.scroller.addEventListener('scroll', this.handleScroll, {
passive: true
});
this.debugLog('Added scroll listener to', {
scroller: this.scroller === window ? 'window' : 'new custom container'
});
}
}
// Clean up height cache for removed children
if (childrenChanged) {
var currentKeys = new Set(_react["default"].Children.toArray(children).filter(_react.isValidElement).map(function (child) {
return child.key;
}));
this.heightCache.forEach(function (value, key) {
if (!currentKeys.has(key)) {
_this3.heightCache["delete"](key);
_this3.columnAssignments["delete"](key);
_this3.measuredKeys["delete"](key);
}
});
// Reset measurement state when children change
this.setState(function (prevState) {
return _objectSpread(_objectSpread({}, prevState), {}, {
measurementPhase: true,
allItemsMeasured: false
});
});
this.measuredKeys.clear();
// Clear any existing measurement timeout
if (this.measurementTimeout) {
clearTimeout(this.measurementTimeout);
this.measurementTimeout = null;
}
// Set a timeout fallback to prevent getting stuck in measurement phase
this.measurementTimeout = setTimeout(function () {
if (_this3.state.measurementPhase && _this3.mounted) {
_this3.debugLog('Measurement timeout reached, forcing transition to virtualized phase');
_this3.finalizeMeasurementPhase();
}
}, 5000); // 5 second timeout
}
// Only do layout updates if not in measurement phase
var measurementPhase = this.state.measurementPhase;
if ((childrenChanged || sizeChanged || layoutPropsChanged) && !measurementPhase) {
var _prevProps$size2;
this.debugLog('Layout update triggered', {
childrenChanged: childrenChanged,
sizeChanged: sizeChanged ? "".concat((_prevProps$size2 = prevProps.size) === null || _prevProps$size2 === void 0 ? void 0 : _prevProps$size2.width, " \u2192 ").concat(size === null || size === void 0 ? void 0 : size.width) : false,
layoutPropsChanged: layoutPropsChanged,
newChildrenCount: _react["default"].Children.count(children)
});
this.layout();
}
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
var _size$unregisterRef;
this.mounted = false;
var size = this.props.size;
size === null || size === void 0 || (_size$unregisterRef = size.unregisterRef) === null || _size$unregisterRef === void 0 || _size$unregisterRef.call(size, this);
this.debugLog('Component unmounting', null, true);
// Always try to remove the scroll listener if a scroller was ever set
if (this.scroller) {
this.scroller.removeEventListener('scroll', this.handleScroll);
}
window.removeEventListener('resize', this.handleResize);
// Clean up ResizeObserver
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.layoutRequestId) {
cancelAnimationFrame(this.layoutRequestId);
}
if (this.scrollRAF) {
cancelAnimationFrame(this.scrollRAF);
}
// Clear measurement timeout
if (this.measurementTimeout) {
clearTimeout(this.measurementTimeout);
this.measurementTimeout = null;
}
}
}, {
key: "render",
value: function render() {
var _this4 = this;
var _this$props5 = this.props,
className = _this$props5.className,
style = _this$props5.style,
ElementType = _this$props5.component,
itemComponent = _this$props5.itemComponent,
children = _this$props5.children,
rtl = _this$props5.rtl,
virtualized = _this$props5.virtualized;
var _this$state = this.state,
rects = _this$state.rects,
height = _this$state.height,
scrollTop = _this$state.scrollTop,
measurementPhase = _this$state.measurementPhase,
allItemsMeasured = _this$state.allItemsMeasured;
var validChildren = _react["default"].Children.toArray(children).filter(_react.isValidElement);
// Always render the measurement container in the background (hidden)
var measurementStyle = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
width: '100%',
visibility: 'hidden',
pointerEvents: 'none'
};
var measurementItems = validChildren.map(function (child) {
return /*#__PURE__*/_react["default"].createElement(GridItem, {
key: "measure-".concat(child.key),
itemKey: child.key,
component: itemComponent,
rect: {
top: 0,
left: 0,
width: 300,
height: 200
},
rtl: rtl,
onHeightChange: function onHeightChange(itemHeight) {
return _this4.handleHeightChange(child.key, itemHeight);
}
}, child);
});
// Check if all items are already measured (this can happen when items are removed)
if (measurementPhase && validChildren.length > 0) {
var currentKeys = new Set(validChildren.map(function (child) {
return child.key;
}));
var measuredCurrentKeys = Array.from(this.measuredKeys).filter(function (key) {
return currentKeys.has(key);
});
if (measuredCurrentKeys.length >= validChildren.length) {
// Use setTimeout to avoid calling setState during render
setTimeout(function () {
if (_this4.state.measurementPhase && _this4.mounted) {
_this4.debugLog('All items already measured, transitioning immediately');
_this4.finalizeMeasurementPhase();
}
}, 0);
}
}
// Main grid container
var containerStyle = _objectSpread({
position: 'relative',
height: measurementPhase ? height || '100vh' : height
}, style);
var renderedItems = validChildren;
var virtualizedCount = 0;
// Only virtualize if we have measured all items and virtualization is enabled
if (virtualized && this.scroller && allItemsMeasured) {
var _this$props$virtualiz = this.props.virtualizationBuffer,
virtualizationBuffer = _this$props$virtualiz === void 0 ? 200 : _this$props$virtualiz;
var viewportHeight = this.scroller === window ? window.innerHeight : this.scroller.clientHeight;
var visibleTop = scrollTop - virtualizationBuffer;
var visibleBottom = scrollTop + viewportHeight + virtualizationBuffer;
var beforeCount = renderedItems.length;
renderedItems = validChildren.filter(function (child, index) {
var rect = rects[index];
if (!rect) return false;
var itemTop = rect.top;
var itemBottom = itemTop + rect.height;
return itemBottom >= visibleTop && itemTop <= visibleBottom;
});
virtualizedCount = beforeCount - renderedItems.length;
if (virtualizedCount > 0) {
this.debugLog('Virtualization active', {
total: validChildren.length,
rendered: renderedItems.length,
hidden: virtualizedCount,
scrollTop: scrollTop,
visibleRange: [visibleTop, visibleBottom],
viewportHeight: viewportHeight
});
}
}
var gridItems = renderedItems.map(function (child) {
var originalIndex = validChildren.indexOf(child);
var rect = rects[originalIndex];
// During measurement phase, show items with estimated positions or previous positions
if (measurementPhase) {
// Use previous rect if available, otherwise use a default
var displayRect = rect || {
top: 0,
left: 0,
width: 300,
height: 200
};
return /*#__PURE__*/_react["default"].createElement(GridItem, {
key: child.key,
itemKey: child.key,
component: itemComponent,
rect: displayRect,
rtl: rtl,
onHeightChange: function onHeightChange(itemHeight) {
return _this4.handleHeightChange(child.key, itemHeight);
}
}, child);
}
// Only render items that have calculated positions after measurement
if (!rect) return null;
return /*#__PURE__*/_react["default"].createElement(GridItem, {
key: child.key,
itemKey: child.key,
component: itemComponent,
rect: rect,
rtl: rtl,
onHeightChange: function onHeightChange(itemHeight) {
return _this4.handleHeightChange(child.key, itemHeight);
}
}, child);
});
return /*#__PURE__*/_react["default"].createElement("div", null, /*#__PURE__*/_react["default"].createElement("div", {
ref: this.measurementContainerRef,
style: measurementStyle
}, measurementItems), /*#__PURE__*/_react["default"].createElement(ElementType, {
className: className,
style: containerStyle,
ref: this.containerRef
}, gridItems));
}
}]);
}(_react.Component); // Move propTypes and defaultProps outside the class
GridInline.propTypes = GridInlinePropTypes;
GridInline.defaultProps = GridInlineDefaultProps;
// --- Ref forwarding adapter for react-sizeme HOC ---
var SizedGrid = (0, _reactSizeme["default"])({
monitorHeight: false,
monitorWidth: true,
refreshMode: 'debounce',
refreshRate: 16,
noPlaceholder: true
})(GridInline);
var StackGridPropTypes = {
children: _propTypes["default"].node,
className: _propTypes["default"].string,
style: _propTypes["default"].shape({}),
gridRef: _propTypes["default"].func,
component: _propTypes["default"].string,
itemComponent: _propTypes["default"].string,
columnWidth: _propTypes["default"].oneOfType([_propTypes["default"].number, _propTypes["default"].string]).isRequired,
gutterWidth: _propTypes["default"].number,
gutterHeight: _propTypes["default"].number,
onLayout: _propTypes["default"].func,
rtl: _propTypes["default"].bool,
virtualized: _propTypes["default"].bool,
debug: _propTypes["default"].bool,
size: _propTypes["default"].shape({
width: _propTypes["default"].number,
height: _propTypes["default"].number,
registerRef: _propTypes["default"].func,
unregisterRef: _propTypes["default"].func
}),
scrollContainer: _propTypes["default"].instanceOf(HTMLElement)
};
var StackGridDefaultProps = {
children: null,
className: '',
style: {},
gridRef: null,
component: 'div',
itemComponent: 'div',
gutterWidth: 5,
gutterHeight: 5,
onLayout: null,
rtl: false,
virtualized: false,
debug: false,
size: null,
scrollContainer: null
};
var StackGrid = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
var inner = (0, _react.useRef)(null);
(0, _react.useImperativeHandle)(ref, function () {
if (inner.current) {
// Alias updateLayout for test compatibility
inner.current.updateLayout = inner.current.layout.bind(inner.current);
}
return inner.current;
}, []);
var handleGridRef = function handleGridRef(inst) {
console.log('[StackGrid] handleGridRef called with:', inst);
inner.current = inst;
// Also call the original gridRef prop if provided
if (props.gridRef) {
console.log('[StackGrid] Calling original gridRef prop');
props.gridRef(inst);
}
};
console.log('[StackGrid] Rendering StackGrid with props:', {
hasGridRef: !!props.gridRef,
hasRef: !!ref,
childrenCount: _react["default"].Children.count(props.children)
});
// Use react-sizeme HOC with better configuration
return /*#__PURE__*/_react["default"].createElement(SizedGrid, _extends({}, props, {
gridRef: handleGridRef
}));
});
// Move propTypes and defaultProps outside the class
StackGrid.propTypes = StackGridPropTypes;
StackGrid.defaultProps = StackGridDefaultProps;
var _default = exports["default"] = StackGrid;