UNPKG

@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
"use strict"; 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;