diff --git a/src/FillSpace.jsx b/src/FillSpace.jsx index 5262e0e..67cb1c1 100644 --- a/src/FillSpace.jsx +++ b/src/FillSpace.jsx @@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography'; import { alpha } from '@mui/material/styles'; /** - * FillSpace — fills remaining viewport height; optionally paginates `items`. + * FillSpace — fills remaining viewport height; paginates `items`. * * @component FillSpace * @import dashgui/FillSpace @@ -15,26 +15,21 @@ import { alpha } from '@mui/material/styles'; * @prop {React.Ref} [aboveRef] - Element(s) above; re-measure when they resize. * @prop {React.Ref|React.Ref[]} [belowRef] - Footer element(s) below; their height is reserved. * @prop {number} [reserveBottom=0] - Extra px to reserve below the spacer. - * @prop {ReactNode} [measureElement] - Legacy mode: hidden sample row, repeated to fill leftover space. - * @prop {number} [measureGap=0] - Gap (px) between repeated units. - * @prop {(count: number) => void} [onMeasurement] - Legacy mode: called with fitting unit count. * @prop {number} [minHeight=0] - Minimum height in px. - * @prop {number} [minCount=0] - Minimum units that must fit (page scrolls instead of clipping). - * @prop {Array} [items] - Paginated mode: page size is measured from row height, wheel flips pages. - * @prop {React.ComponentType|Function} [renderItem] - Row renderer (component or callback). + * @prop {Array} items - Page size is measured from row height, wheel flips pages. + * @prop {React.ComponentType|Function} renderItem - Row renderer (component or callback). * @prop {number} [minPageSize=10] - Minimum rows per page. * @prop {(page: number) => void} [onPageChange] - Called on page change. * @prop {number} [maxHeight] - Maximum height in px. * @prop {boolean} [debug=false] - Dashed outline for layout debugging. - * @prop {ReactNode} [children] - Legacy mode content rendered on top. * @prop {object} [sx] - Extra styles. */ /** Minimum spacer height so at least `count` units fit (lets the page scroll on small windows). */ -function minHeightFromCount(count, unitHeight, gap) { +function minHeightFromCount(count, unitHeight) { const n = Number(count) || 0; if (n <= 0 || !(unitHeight > 0)) return 0; - return n * unitHeight + Math.max(0, n - 1) * (Number(gap) || 0); + return n * unitHeight; } /** True for class components (e.g. `renderItem={ArtikelMarke}`). Rendered as element, not called. */ @@ -77,18 +72,12 @@ class FillSpace extends Component { // first visible frame is already full-size, no 0 → expanded jump. height: null, unitHeight: 0, - contentHeight: 0, - totalCount: 0, - fillCount: 0, - // Paginated mode (`items`): page size comes from the fit count. - pageSize: Number(props.minPageSize ?? props.minCount) || 10, + // Page size comes from the fit count. + pageSize: Number(props.minPageSize) || 10, currentPage: 1, measured: false, }; - this.lastTotalCount = null; this.ref = null; - this.contentRef = null; - this.unitRef = null; this.probeRef = null; this.paginationRef = null; this.wheelAcc = 0; @@ -112,12 +101,10 @@ class FillSpace extends Component { prevProps.containerRef !== this.props.containerRef || prevProps.reserveBottom !== this.props.reserveBottom || prevProps.minHeight !== this.props.minHeight || - prevProps.minCount !== this.props.minCount || prevProps.minPageSize !== this.props.minPageSize || prevProps.maxHeight !== this.props.maxHeight || - prevProps.measureElement !== this.props.measureElement || - prevProps.measureGap !== this.props.measureGap || - prevProps.items !== this.props.items + prevProps.items !== this.props.items || + prevProps.renderItem !== this.props.renderItem ) { this._observe(); this._measure(); @@ -166,11 +153,10 @@ class FillSpace extends Component { this.observer = new ResizeObserver(() => this._onViewportChange()); // Never observe the spacer's own parent: resizing the spacer would resize the parent. - // Paginated mode also skips contentRef: every flip swaps all rows, which + // Skip content: every flip swaps all rows, which // would fire the observer -> _measure -> setState -> render in a loop. // Unit height is read lazily in _measure, so no observer is needed for it. - const { aboveRef, belowRef, containerRef, items, renderItem } = this.props; - const paginated = Array.isArray(items) && typeof renderItem === 'function'; + const { aboveRef, belowRef, containerRef } = this.props; const aboveList = Array.isArray(aboveRef) ? aboveRef : [aboveRef]; const belowList = Array.isArray(belowRef) ? belowRef : [belowRef]; const containerTarget = containerRef && 'current' in containerRef ? containerRef.current : containerRef; @@ -178,7 +164,6 @@ class FillSpace extends Component { ...aboveList.map((r) => (r && 'current' in r ? r.current : r)), ...belowList.map((r) => (r && 'current' in r ? r.current : r)), containerTarget, - ...(paginated ? [] : [this.contentRef]), this.paginationRef, ]; for (const el of targets) { @@ -199,15 +184,9 @@ class FillSpace extends Component { belowRef, containerRef, reserveBottom = 0, - measureElement, - renderItem, - measureGap = 0, minHeight = 0, - minCount = 0, minPageSize, maxHeight, - onMeasurement, - items, } = this.props; // Spacer top, document-relative: scrolling the page does not change the height. @@ -243,70 +222,26 @@ class FillSpace extends Component { maxHeight ?? Infinity, ); - let contentHeight = 0; - if (this.contentRef) { - contentHeight = this.contentRef.offsetHeight; - } - - // Paginated: hidden probe rendered from `renderItem` itself (no data). - // Legacy: hidden copy. - const paginated = Array.isArray(items) && typeof renderItem === 'function'; let unitHeight = this.state.unitHeight; - if (paginated) { - const probeH = this.probeRef?.offsetHeight; - if (probeH > 0) { - unitHeight = probeH; - } else { - const firstRow = this.contentRef?.querySelector?.('[data-fill-row]') ?? this.contentRef?.firstElementChild; - const h = firstRow?.offsetHeight; - if (h > 0) unitHeight = h; - } - if (!(unitHeight > 0) && measureElement != null && this.unitRef) { - const fallback = this.unitRef.offsetHeight; - if (fallback > 0) unitHeight = fallback; - } - } else if (measureElement != null && this.unitRef) { - const h = this.unitRef.offsetHeight; - if (h > 0) unitHeight = h; - } - const minSize = Math.max(1, Number(minPageSize ?? minCount) || 10); - const effectiveMinCount = paginated ? Math.max(Number(minCount) || 0, minSize) : Number(minCount) || 0; - const minHeightForCount = minHeightFromCount(effectiveMinCount, unitHeight, measureGap); + const probeH = this.probeRef?.offsetHeight; + if (probeH > 0) unitHeight = probeH; + const minSize = Math.max(1, Number(minPageSize) || 10); + const minHeightForCount = minHeightFromCount(minSize, unitHeight); const finalHeight = Math.min( Math.max(clamped, minHeightForCount), maxHeight ?? Infinity, ); - // Fitting unit count (drives page size) and leftover fill count. + // Fitting unit count drives page size. let totalCount = 0; - let fillCount = 0; - if (unitHeight > 0 && (measureElement != null || paginated)) { - totalCount = Math.floor((finalHeight + measureGap) / (unitHeight + measureGap)); - const fillHeight = Math.max(0, finalHeight - contentHeight); - fillCount = Math.floor((fillHeight + measureGap) / (unitHeight + measureGap)); + if (unitHeight > 0) { + totalCount = Math.floor(finalHeight / unitHeight); } - // Report total fitting count once per change (legacy mode + paginated - // debug). Paginated callers use this to display the live measurement - // in a side tile without polling internal state. - if ( - typeof onMeasurement === 'function' && - (measureElement != null || paginated) && - totalCount !== this.lastTotalCount - ) { - this.lastTotalCount = totalCount; - onMeasurement(totalCount, { - height: finalHeight, - unitHeight, - pageSize: paginated && totalCount > 0 ? Math.max(minSize, totalCount) : this.state.pageSize, - measured: paginated ? totalCount > 0 : this.state.measured, - }); - } - - // Paginated: fit count drives page size (min minPageSize). + // Fit count drives page size (min minPageSize). let nextPageSize = this.state.pageSize; let measured = this.state.measured; - if (paginated && totalCount > 0) { + if (totalCount > 0) { nextPageSize = Math.max(minSize, totalCount); measured = true; } @@ -314,18 +249,12 @@ class FillSpace extends Component { if ( Math.abs(finalHeight - this.state.height) > 0.5 || Math.abs(unitHeight - this.state.unitHeight) > 0.5 || - Math.abs(contentHeight - this.state.contentHeight) > 0.5 || - totalCount !== this.state.totalCount || - fillCount !== this.state.fillCount || nextPageSize !== this.state.pageSize || measured !== this.state.measured ) { this.setState({ height: finalHeight, unitHeight, - contentHeight, - totalCount, - fillCount, pageSize: nextPageSize, measured, }); @@ -369,136 +298,21 @@ class FillSpace extends Component { belowRef, containerRef, reserveBottom, - onMeasurement, onWheel, onPageChange, minHeight = 0, - minCount, minPageSize, maxHeight, debug = false, - children, items, renderItem, - measureElement, - measureGap = 0, sx, ...rest } = this.props; - // Paginated mode: FillSpace owns page size (from the rendered row - // height), current page, wheel-pagination and the pagination header. - // Caller feeds `items` plus one row element via `renderItem` (component - // type or callback). - if (Array.isArray(items) && typeof renderItem === 'function') { - return this.renderPaginated({ - minHeight, - maxHeight, - debug, - measureGap, - sx, - rest, - }); - } - - const { height, fillCount } = this.state; - - let fill = null; - if (measureElement != null) { - const fillItems = []; - for (let i = 0; i < fillCount; i++) { - fillItems.push( - - {measureElement} - , - ); - } - fill = ( - - {fillItems} - - ); - } - - return ( - <> - { - this.ref = el; - }} - onWheel={onWheel} - {...rest} - sx={[ - { - position: 'relative', - height: height ?? 'auto', - minHeight, - ...(maxHeight != null ? { maxHeight } : {}), - }, - debug - ? (theme) => ({ - outline: `2px dashed ${theme.palette.primary.main}`, - }) - : undefined, - sx, - ]} - > - {children != null ? ( - { - this.contentRef = el; - }} - sx={{ width: '100%', ...(measureElement == null ? { height: '100%' } : {}) }} - > - {children} - - ) : null} - {fill} - {measureElement != null ? ( - { - this.unitRef = el; - }} - aria-hidden - sx={{ - position: 'absolute', - left: 0, - top: 0, - width: '100%', - visibility: 'hidden', - pointerEvents: 'none', - }} - > - {measureElement} - - ) : null} - - {debug ? ( - ({ - position: 'fixed', - inset: 0, - pointerEvents: 'none', - background: alpha(theme.palette.primary.main, 0.08), - zIndex: 9999, - })} - /> - ) : null} - - ); - } - - /** Pagination header + one page of rows. Wheel over the list flips pages. */ - renderPaginated({ minHeight, maxHeight, debug, measureGap, sx, rest }) { - const { items, renderItem, measureElement, onWheel } = this.props; + // FillSpace owns page size (from the rendered row height), current page, + // wheel-pagination and the pagination header. Caller feeds `items` plus + // one row element via `renderItem` (component type or callback). const { height, pageSize, currentPage, measured } = this.state; const total = items.length; const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); @@ -509,7 +323,7 @@ class FillSpace extends Component { const showPagination = measured && total > pageSize; // Hidden data-free probe: measures `renderItem` itself (no data), so // the first pass already uses the final row height — one pass, no flip. - const probe = renderProbeElement(renderItem) ?? measureElement; + const probe = renderProbeElement(renderItem); return ( <> @@ -599,12 +413,7 @@ class FillSpace extends Component { sx, ]} > - { - this.contentRef = el; - }} - sx={{ width: '100%' }} - > + {pageItems.map((item, i) => (