diff --git a/examples/src/pages/tests/horizontal-layout/default.page.tsx b/examples/src/pages/tests/horizontal-layout/default.page.tsx index 9bf79d1d..aac58fff 100644 --- a/examples/src/pages/tests/horizontal-layout/default.page.tsx +++ b/examples/src/pages/tests/horizontal-layout/default.page.tsx @@ -13,6 +13,11 @@ const columns: InfiniteTablePropColumns = { field: 'id', type: 'number', /*xdefaultWidth: 80,*/ renderValue: ({ value }) => value - 1, + style: (options) => { + return { + // background : options.rowInfo. + }; + }, }, preferredLanguage: { field: 'preferredLanguage' /*xdefaultWidth: 110 */ }, // age: { field: 'age' /*xdefaultWidth: 70 */ }, diff --git a/examples/src/pages/tests/horizontal-layout/example.page.tsx b/examples/src/pages/tests/horizontal-layout/example.page.tsx new file mode 100644 index 00000000..8928ba1f --- /dev/null +++ b/examples/src/pages/tests/horizontal-layout/example.page.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; + +import { + InfiniteTable, + InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; +import { DataSource } from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + + firstName: string; + lastName: string; + + currency: string; + country: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + age: number; + salary: number; +}; + +const style = () => { + return { + background: `rgb(255 0 0 / 12%)`, + }; +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + style, + }, + canDesign: { + field: 'canDesign', + }, + salary: { + field: 'salary', + type: 'number', + // style, + }, + firstName: { + field: 'firstName', + }, + age: { + field: 'age', + type: 'number', + // style, + }, + + stack: { field: 'stack', renderMenuIcon: false }, + currency: { field: 'currency' }, + country: { field: 'country' }, +}; + +export default () => { + const dataSource = React.useCallback(() => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') + .then((r) => r.json()) + .then((data) => { + return data; + // return new Promise((resolve) => { + // setTimeout(() => { + // resolve(data); + // }, 10); + // }); + }); + }, []); + return ( + <> + + + data={dataSource} + primaryKey="id" + defaultGroupBy={[ + { + field: 'currency', + }, + { + field: 'stack', + }, + ]} + > + + columns={columns} + wrapRowsHorizontally={true} + columnDefaultWidth={120} + domProps={{ + style: { + minHeight: '70vh', + }, + }} + /> + + + + ); +}; diff --git a/examples/src/pages/tests/horizontal-layout/test.page.tsx b/examples/src/pages/tests/horizontal-layout/test.page.tsx index 78d45c00..1c63be12 100644 --- a/examples/src/pages/tests/horizontal-layout/test.page.tsx +++ b/examples/src/pages/tests/horizontal-layout/test.page.tsx @@ -9,26 +9,51 @@ import { useState } from 'react'; type Developer = { id: number; + + firstName: string; + lastName: string; + + currency: string; + country: string; preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + age: number; salary: number; }; + const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', }, - preferredLanguage: { field: 'preferredLanguage' }, - // age: { field: 'age' }, - // salary: { field: 'salary' }, + canDesign: { + field: 'canDesign', + }, + salary: { + field: 'salary', + type: 'number', + }, + firstName: { + field: 'firstName', + }, + age: { + field: 'age', + type: 'number', + }, + + stack: { field: 'stack', renderMenuIcon: false }, + currency: { field: 'currency' }, + country: { field: 'country' }, }; const domProps = { // style: { height: 420 /*30px header, 420 body*/, width: 230 }, - style: { height: '50vh' /*30px header, 420 body*/, width: '30vw' }, + style: { height: '50vh' /*30px header, 420 body*/, width: '80vw' }, }; -const data = Array.from({ length: 10 }, (_, i) => ({ +const data = Array.from({ length: 1000 }, (_, i) => ({ id: i, preferredLanguage: `Lang ${i}`, age: i * 10, diff --git a/source/src/components/HeadlessTable/HorizontalLayoutTableRenderer.tsx b/source/src/components/HeadlessTable/HorizontalLayoutTableRenderer.tsx index b04e9379..c27b2b41 100644 --- a/source/src/components/HeadlessTable/HorizontalLayoutTableRenderer.tsx +++ b/source/src/components/HeadlessTable/HorizontalLayoutTableRenderer.tsx @@ -24,6 +24,37 @@ export class HorizontalLayoutTableRenderer extends ReactHeadlessTableRenderer { }); } + isCellRenderedAndMappedCorrectly(row: number, col: number) { + const rendered = this.mappedCells.isCellRendered(row, col); + + if (!rendered) { + return { + rendered, + mapped: false, + }; + } + + const cellAdditionalInfo = this.mappedCells.getCellAdditionalInfo(row, col); + + if (!cellAdditionalInfo) { + return { + rendered, + mapped: false, + }; + } + + const info = this.getCellRealCoordinates(row, col); + + const mapped = + info.colIndex === cellAdditionalInfo!.renderColIndex && + info.rowIndex === cellAdditionalInfo!.renderRowIndex; + + return { + rendered, + mapped, + }; + } + setTransform = ( element: HTMLElement, rowIndex: number, diff --git a/source/src/components/HeadlessTable/MappedCells.ts b/source/src/components/HeadlessTable/MappedCells.ts index 1b8b6b94..e709426c 100644 --- a/source/src/components/HeadlessTable/MappedCells.ts +++ b/source/src/components/HeadlessTable/MappedCells.ts @@ -14,7 +14,7 @@ import { TableRenderRange } from '../VirtualBrain/MatrixBrain'; * This class has tests - see tests/mapped-cells.spec.ts */ -export class MappedCells extends Logger { +export class MappedCells extends Logger { /** * This is the mapping from element index to cell info. * The index in the array is the element index while the value at the position is an array where @@ -29,14 +29,21 @@ export class MappedCells extends Logger { */ private cellToElementIndex!: DeepMap; + private cellAdditionalInfo!: DeepMap; + /** * Keeps the JSX of rendered elements in memory, so we can possibly reuse it later. */ private renderedElements!: Renderable[]; - constructor() { + private withCellAdditionalInfo: boolean = false; + + constructor(opts?: { withCellAdditionalInfo: boolean }) { super(`MappedCells`); this.init(); + if (opts?.withCellAdditionalInfo) { + this.withCellAdditionalInfo = opts.withCellAdditionalInfo; + } // if (__DEV__) { // (globalThis as any).mappedCells = this; @@ -69,6 +76,7 @@ export class MappedCells extends Logger { init() { this.elementIndexToCell = []; this.cellToElementIndex = new DeepMap(); + this.cellAdditionalInfo = new DeepMap(); this.renderedElements = []; } @@ -79,6 +87,7 @@ export class MappedCells extends Logger { destroy() { this.elementIndexToCell = []; this.cellToElementIndex.clear(); + this.cellAdditionalInfo.clear(); this.renderedElements = []; } @@ -129,6 +138,13 @@ export class MappedCells extends Logger { return this.cellToElementIndex.has([rowIndex, columnIndex]); }; + getCellAdditionalInfo = ( + rowIndex: number, + columnIndex: number, + ): T_ADDITIONAL_CELL_INFO | undefined => { + return this.cellAdditionalInfo.get([rowIndex, columnIndex]); + }; + isElementRendered = (elementIndex: number): boolean => { return !!this.elementIndexToCell[elementIndex]; }; @@ -172,7 +188,8 @@ export class MappedCells extends Logger { rowIndex: number, colIndex: number, elementIndex: number, - renderNode?: Renderable, + renderNode: Renderable | undefined, + cellAdditionalInfo?: T_ADDITIONAL_CELL_INFO, ) => { if (__DEV__) { this.debug( @@ -184,7 +201,11 @@ export class MappedCells extends Logger { const currentCell = this.elementIndexToCell[elementIndex]; if (currentCell) { - this.cellToElementIndex.delete([currentCell[0], currentCell[1]]); + const currentCellKey = [currentCell[0], currentCell[1]]; + this.cellToElementIndex.delete(currentCellKey); + if (this.withCellAdditionalInfo) { + this.cellAdditionalInfo.delete(currentCellKey); + } } if (renderNode) { this.renderedElements[elementIndex] = renderNode; @@ -192,6 +213,9 @@ export class MappedCells extends Logger { this.elementIndexToCell[elementIndex] = [rowIndex, colIndex]; this.cellToElementIndex.set(key, elementIndex); + if (this.withCellAdditionalInfo && cellAdditionalInfo !== undefined) { + this.cellAdditionalInfo.set(key, cellAdditionalInfo); + } }; discardCell = (rowIndex: number, colIndex: number) => { @@ -202,6 +226,9 @@ export class MappedCells extends Logger { this.renderedElements[elementIndex] = null; this.elementIndexToCell[elementIndex] = null; this.cellToElementIndex.delete(key); + if (this.withCellAdditionalInfo) { + this.cellAdditionalInfo.delete(key); + } } }; @@ -213,6 +240,9 @@ export class MappedCells extends Logger { this.renderedElements[elementIndex] = null; this.elementIndexToCell[elementIndex] = null; this.cellToElementIndex.delete(key); + if (this.withCellAdditionalInfo) { + this.cellAdditionalInfo.delete(key); + } return cell; } diff --git a/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx b/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx index 0b2e7b4b..0949e97f 100644 --- a/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx +++ b/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx @@ -91,7 +91,10 @@ export class ReactHeadlessTableRenderer extends Logger { private detailRowDOMRefs: RefCallback[] = []; private detailRowUpdaters: SubscriptionCallback[] = []; - protected mappedCells: MappedCells; + protected mappedCells: MappedCells<{ + renderRowIndex: number; + renderColIndex: number; + }>; private mappedDetailRows: MappedVirtualRows; private items: Renderable[] = []; @@ -199,7 +202,12 @@ export class ReactHeadlessTableRenderer extends Logger { this.brain = brain; this.debugId = debugId; - this.mappedCells = new MappedCells(); + this.mappedCells = new MappedCells<{ + renderRowIndex: number; + renderColIndex: number; + }>({ + withCellAdditionalInfo: brain.isHorizontalLayoutBrain, + }); this.mappedDetailRows = new MappedVirtualRows(); this.renderRange = this.renderRange.bind(this); @@ -573,6 +581,14 @@ export class ReactHeadlessTableRenderer extends Logger { }); }; + isCellRenderedAndMappedCorrectly(row: number, col: number) { + const rendered = this.mappedCells.isCellRendered(row, col); + return { + rendered, + mapped: rendered, + }; + } + renderRange( range: TableRenderRange, @@ -774,7 +790,8 @@ export class ReactHeadlessTableRenderer extends Logger { continue; } visitedCells.set(key, true); - const cellRendered = mappedCells.isCellRendered(row, col); + const { rendered: cellRendered, mapped: cellMappedCorrectly } = + this.isCellRenderedAndMappedCorrectly(row, col); // for cells that belong to the first row of the render range // or to the first column of the render range @@ -806,7 +823,7 @@ export class ReactHeadlessTableRenderer extends Logger { } } - if (cellRendered && !force) { + if (cellRendered && !force && cellMappedCorrectly) { continue; } @@ -857,8 +874,12 @@ export class ReactHeadlessTableRenderer extends Logger { }); extraCells.forEach(([rowIndex, colIndex]) => { - if (mappedCells.isCellRendered(rowIndex, colIndex)) { - if (force) { + const { rendered, mapped } = this.isCellRenderedAndMappedCorrectly( + rowIndex, + colIndex, + ); + if (rendered) { + if (force || !mapped) { const elementIndex = mappedCells.getElementIndexForCell( rowIndex, colIndex, @@ -1300,11 +1321,19 @@ export class ReactHeadlessTableRenderer extends Logger { return; } + const cellAdditionalInfo = this.brain.isHorizontalLayoutBrain + ? { + renderRowIndex, + renderColIndex, + } + : undefined; + this.mappedCells.renderCellAtElement( rowIndex, colIndex, elementIndex, renderedNode, + cellAdditionalInfo, ); itemUpdater(renderedNode); diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts index e5807169..d47a2923 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts @@ -65,7 +65,7 @@ export interface InfiniteTableColumnCellProps groupRenderStrategy: InfiniteTablePropGroupRenderStrategy; getData: () => InfiniteTableRowInfo[]; toggleGroupRow: InfiniteTableToggleGroupRowFn; - rowIndexInPage: number | null; + rowIndexInHorizontalLayoutPage: number | null; pageIndex: number | null; rowIndex: number; rowHeight: number; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx index fba5268f..9c36016e 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx @@ -142,7 +142,7 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { rowStyle, rowClassName, - rowIndexInPage, + rowIndexInHorizontalLayoutPage, getData, cellStyle, @@ -545,8 +545,8 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { ); const odd = - rowIndexInPage != null - ? rowIndexInPage % 2 === 1 + rowIndexInHorizontalLayoutPage != null + ? rowIndexInHorizontalLayoutPage % 2 === 1 : (rowInfo.indexInAll != null ? rowInfo.indexInAll : rowIndex) % 2 === 1; const zebra = showZebraRows ? (odd ? 'odd' : 'even') : false; diff --git a/source/src/components/InfiniteTable/hooks/useCellRendering.tsx b/source/src/components/InfiniteTable/hooks/useCellRendering.tsx index a9137488..01981723 100644 --- a/source/src/components/InfiniteTable/hooks/useCellRendering.tsx +++ b/source/src/components/InfiniteTable/hooks/useCellRendering.tsx @@ -232,7 +232,7 @@ export function useCellRendering( : 'collapsed'; } - const rowIndexInPage = wrapRowsHorizontally + const rowIndexInHorizontalLayoutPage = wrapRowsHorizontally ? brain.getRowIndexInPage(rowIndex) : null; @@ -245,7 +245,7 @@ export function useCellRendering( virtualized: true, showZebraRows, groupRenderStrategy, - rowIndexInPage, + rowIndexInHorizontalLayoutPage, pageIndex, rowIndex, rowInfo, diff --git a/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts b/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts index 7694fe73..166ed550 100644 --- a/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts +++ b/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts @@ -310,6 +310,7 @@ export type InfiniteTableColumnWithRenderDescriptor = RequireAtLeastOne< export type InfiniteTableColumnStylingFnParams = { value: Renderable; column: InfiniteTableComputedColumn; + // rowIndexInHorizontalLayoutPage: null | number; inEdit: boolean; rowHasSelectedCells: boolean; editError: InfiniteTableColumnRenderParamBase['editError']; diff --git a/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts b/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts index 58226f8d..caff8b90 100644 --- a/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts +++ b/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts @@ -1,3 +1,4 @@ +import { raf } from '../../utils/raf'; import { ALL_DIRECTIONS, IBrain, @@ -101,6 +102,12 @@ export class HorizontalLayoutMatrixBrain extends MatrixBrain implements IBrain { constructor(name: string, opts: HorizontalLayoutMatrixBrainOptions) { super(`HorizontalLayout${name ? `:${name}` : ''}`); this.options = opts; + + if (this.options.masterBrain) { + this.options.masterBrain.onTotalPageCountChange(() => { + this.updateRenderCount({ horizontal: true, vertical: true }); + }); + } } getRowIndexInPage(rowIndex: number) { @@ -255,8 +262,18 @@ export class HorizontalLayoutMatrixBrain extends MatrixBrain implements IBrain { } } - const horizontalChange = colsChanged || colWidthChanged || widthChanged; const verticalChange = rowsChanged || rowHeightChanged || heightChanged; + const horizontalChange = + colsChanged || + colWidthChanged || + widthChanged || + /** when something changes vertically, + * it needs to trigger horizontal change as well since + * the number of "virtual" columns needs to be adjusted + * + * THIS IS VERY IMPORTANT TO HAVE HERE + */ + verticalChange; if (horizontalChange || verticalChange) { this.updateRenderCount({ @@ -309,10 +326,17 @@ export class HorizontalLayoutMatrixBrain extends MatrixBrain implements IBrain { // based on the page width, determine the number of rows per page this.rowsPerPage = Math.floor(this.availableHeight / rowHeight); + let shouldNotifyTotalPageCountChange = false; + if (!this.options.masterBrain) { + const prevTotalPageCount = this.totalPageCount; this.totalPageCount = this.rowsPerPage ? Math.ceil(this.initialRows / this.rowsPerPage) : 0; + + if (prevTotalPageCount != this.totalPageCount) { + shouldNotifyTotalPageCountChange = true; + } } this.visiblePageCount = @@ -327,6 +351,49 @@ export class HorizontalLayoutMatrixBrain extends MatrixBrain implements IBrain { this.rows = this.rowsPerPage; super.doUpdateRenderCount(which); + + if (shouldNotifyTotalPageCountChange) { + this.notifyTotalPageCountChange(); + } + } + private onTotalPageCountChangeFns: Set<(totalPageCount: number) => void> = + new Set(); + + private notifyTotalPageCountChange() { + if (this.destroyed) { + return; + } + const fns = this.onTotalPageCountChangeFns; + + fns.forEach((fn) => { + raf(() => { + if (this.destroyed) { + return; + } + // #check-for-presence - see above note + if (fns.has(fn)) { + fn(this.totalPageCount); + } + }); + }); + } + + protected onTotalPageCountChange = (fn: (x: number) => void) => { + this.onTotalPageCountChangeFns.add(fn); + + return () => { + this.onTotalPageCountChangeFns.delete(fn); + }; + }; + + destroy() { + if (this.destroyed) { + return; + } + super.destroy(); + + this.options.masterBrain = undefined; + this.onTotalPageCountChangeFns.clear(); } getVirtualizedContentSizeFor(direction: 'horizontal' | 'vertical') { diff --git a/source/src/components/VirtualBrain/MatrixBrain.ts b/source/src/components/VirtualBrain/MatrixBrain.ts index bbfc5368..da694d9f 100644 --- a/source/src/components/VirtualBrain/MatrixBrain.ts +++ b/source/src/components/VirtualBrain/MatrixBrain.ts @@ -173,7 +173,7 @@ export class MatrixBrain extends Logger implements IBrain { new Set(); private onDestroyFns: VoidFn[] = []; - private destroyed = false; + protected destroyed = false; private onRenderCountChangeFns: Set = new Set(); private onAvailableSizeChangeFns: Set = new Set(); private onScrollStartFns: VoidFunction[] = []; @@ -205,6 +205,7 @@ export class MatrixBrain extends Logger implements IBrain { this.name = name || 'MatrixBrain'; this.update = this.update.bind(this); + this.destroy = this.destroy.bind(this); this.getCellOffset = this.getCellOffset.bind(this); @@ -413,6 +414,7 @@ export class MatrixBrain extends Logger implements IBrain { } this.setRenderCount(count); + return; } this.setRenderCount(this.computeRenderCount(which)); @@ -1649,7 +1651,7 @@ export class MatrixBrain extends Logger implements IBrain { } } - destroy = () => { + destroy() { if (this.destroyed) { return; } @@ -1664,9 +1666,9 @@ export class MatrixBrain extends Logger implements IBrain { this.onScrollFns = []; this.onScrollStartFns = []; this.onScrollStopFns = []; - this.onRenderCountChangeFns = new Set(); - this.onRenderRangeChangeFns = new Set(); - this.onVerticalRenderRangeChangeFns = new Set(); - this.onHorizontalRenderRangeChangeFns = new Set(); - }; + this.onRenderCountChangeFns.clear(); + this.onRenderRangeChangeFns.clear(); + this.onVerticalRenderRangeChangeFns.clear(); + this.onHorizontalRenderRangeChangeFns.clear(); + } } diff --git a/source/src/utils/debugPackage.ts b/source/src/utils/debugPackage.ts index 0001a3eb..3c2cd22c 100644 --- a/source/src/utils/debugPackage.ts +++ b/source/src/utils/debugPackage.ts @@ -3,10 +3,10 @@ import { getGlobal } from './getGlobal'; // colors take from the `debug` package on npm const COLORS = [ - '#0000CC', - '#0000FF', - '#0033CC', - '#0033FF', + // '#0000CC', + // '#0000FF', + // '#0033CC', + // '#0033FF', '#0066CC', '#0066FF', '#0099CC',