From 624b4deb2fe3b9053d37e0b0a35159f9a84ec08a Mon Sep 17 00:00:00 2001 From: radubrehar Date: Fri, 4 Oct 2024 23:55:11 +0300 Subject: [PATCH] implement column reorder in horizontal layout --- .../tests/horizontal-layout/test.page.tsx | 5 +- .../HorizontalLayoutTableRenderer.tsx | 13 ++- .../ReactHeadlessTableRenderer.tsx | 23 +++- .../InfiniteTable/InfiniteCls.css.ts | 4 + .../InfiniteTableHeaderCell.tsx | 14 ++- .../ResizeHandle/columnResizer.ts | 31 ++++-- .../InfiniteTableHeader/header.css.ts | 9 ++ .../InfiniteTableColumnCell.tsx | 9 +- .../InfiniteTable/components/cell.css.ts | 8 ++ .../hooks/reorderColumnsOnDrag.ts | 101 ++++++++++------- .../InfiniteTable/hooks/useCellClassName.ts | 2 + .../hooks/useColumnPointerEvents.ts | 68 +++++++++--- .../InfiniteTable/hooks/useDOMProps.ts | 32 +++--- .../InfiniteTable/state/getInitialState.ts | 1 + .../InfiniteTable/types/InfiniteTableState.ts | 1 + .../InfiniteTable/utils/CSSCalc.jestspec.ts | 91 +++++++++++++++ .../components/InfiniteTable/utils/CSSCalc.ts | 104 ++++++++++++++++++ .../InfiniteTable/utils/infiniteDOMUtils.ts | 86 +++++++++++++-- .../InfiniteTable/vars-default-light.css.ts | 2 + .../src/components/InfiniteTable/vars.css.ts | 2 + .../HorizontalLayoutMatrixBrain.ts | 4 + .../components/VirtualBrain/MatrixBrain.ts | 49 ++++++--- 22 files changed, 545 insertions(+), 114 deletions(-) create mode 100644 source/src/components/InfiniteTable/utils/CSSCalc.jestspec.ts create mode 100644 source/src/components/InfiniteTable/utils/CSSCalc.ts diff --git a/examples/src/pages/tests/horizontal-layout/test.page.tsx b/examples/src/pages/tests/horizontal-layout/test.page.tsx index 6653039a..f27da088 100644 --- a/examples/src/pages/tests/horizontal-layout/test.page.tsx +++ b/examples/src/pages/tests/horizontal-layout/test.page.tsx @@ -27,13 +27,16 @@ const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', + defaultWidth: 100, }, canDesign: { field: 'canDesign', + defaultWidth: 200, }, salary: { field: 'salary', type: 'number', + defaultWidth: 300, }, // firstName: { // field: 'firstName', @@ -53,7 +56,7 @@ const domProps = { style: { height: '50vh' /*30px header, 420 body*/, width: '80vw' }, }; -const data = Array.from({ length: 50 }, (_, i) => ({ +const data = Array.from({ length: 100 }, (_, 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 b90b0f9b..4ecf5170 100644 --- a/source/src/components/HeadlessTable/HorizontalLayoutTableRenderer.tsx +++ b/source/src/components/HeadlessTable/HorizontalLayoutTableRenderer.tsx @@ -1,7 +1,7 @@ +import { InternalVarUtils } from '../InfiniteTable/utils/infiniteDOMUtils'; import { ThemeVars } from '../InfiniteTable/vars.css'; import { HorizontalLayoutMatrixBrain } from '../VirtualBrain/HorizontalLayoutMatrixBrain'; import { - columnOffsetAtIndex, columnOffsetAtIndexWhileReordering, currentTransformY, ReactHeadlessTableRenderer, @@ -81,7 +81,9 @@ export class HorizontalLayoutTableRenderer extends ReactHeadlessTableRenderer { const pageWidth = ThemeVars.runtime.totalVisibleColumnsWidthVar; const pageOffset = pageIndex ? `calc(${pageWidth} * ${pageIndex})` : '0px'; - const columnOffsetX = `${columnOffsetAtIndex}-${horizontalLayoutCoords.colIndex}`; + const columnOffsetX = InternalVarUtils.columnOffsets.get( + horizontalLayoutCoords.colIndex, + ); const columnOffsetXWhileReordering = `${columnOffsetAtIndexWhileReordering}-${horizontalLayoutCoords.colIndex}`; const currentTransformYValue = `${y}px`; @@ -93,11 +95,12 @@ export class HorizontalLayoutTableRenderer extends ReactHeadlessTableRenderer { element.style.setProperty(currentTransformY, currentTransformYValue); } - const xOffset = `calc(var(${columnOffsetX}) + ${pageOffset})`; - const transformX = `var(${columnOffsetXWhileReordering}, ${xOffset})`; + const xOffset = `calc(var(${columnOffsetXWhileReordering}, ${columnOffsetX}) + ${pageOffset})`; + // const transformX = `var(${columnOffsetXWhileReordering}, ${xOffset})`; + // const transformXIncludeReordering = `calc(var(${columnOffsetXWhileReordering}, ${xOffset}) + var(${pageOffset}))`; const transformY = `var(${currentTransformY})`; - const transformValue = `translate3d(${transformX}, ${transformY}, 0px)`; + const transformValue = `translate3d(${xOffset}, ${transformY}, 0px)`; //@ts-ignore if (element.__transformValue !== transformValue) { diff --git a/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx b/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx index 3d776d46..fcf8cec6 100644 --- a/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx +++ b/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx @@ -9,6 +9,7 @@ import { InternalVars } from '../InfiniteTable/internalVars.css'; import { ScrollAdjustPosition } from '../InfiniteTable/types/InfiniteTableProps'; import { getParentInfiniteNode, + InternalVarUtils, setInfiniteScrollPosition, } from '../InfiniteTable/utils/infiniteDOMUtils'; import { AvoidReactDiff } from '../RawList/AvoidReactDiff'; @@ -154,7 +155,7 @@ export class ReactHeadlessTableRenderer extends Logger { }: { x: number; y: number; scrollLeft?: boolean; scrollTop?: boolean }, zIndex: number | 'auto' | undefined | null, ) => { - const columnOffsetX = `${columnOffsetAtIndex}-${colIndex}`; + const columnOffsetX = InternalVarUtils.columnOffsets.get(colIndex); const columnOffsetXWhileReordering = `${columnOffsetAtIndexWhileReordering}-${colIndex}`; // const columnZIndex = `${columnZIndexAtIndex}-${colIndex}`; @@ -186,7 +187,7 @@ export class ReactHeadlessTableRenderer extends Logger { element.style.setProperty(currentTransformY, currentTransformYValue); } - const transformValue = `translate3d(var(${columnOffsetXWhileReordering}, var(${columnOffsetX})), var(${currentTransformY}), 0)`; + const transformValue = `translate3d(var(${columnOffsetXWhileReordering}, ${columnOffsetX}), var(${currentTransformY}), 0)`; // this does not change, but we need for initial setup //@ts-ignore @@ -718,6 +719,24 @@ export class ReactHeadlessTableRenderer extends Logger { const fixedRanges = this.getFixedRanges(range); const ranges = [range, ...fixedRanges]; + const alwaysRenderedColumns = this.brain.getAlwaysRenderedColumns(); + + if (alwaysRenderedColumns.length > 0) { + const startCol = range.start[1]; + const endCol = range.end[1]; + + alwaysRenderedColumns.forEach((colIndex) => { + const colInRange = startCol <= colIndex && colIndex < endCol; + + if (!colInRange) { + ranges.push({ + start: [range.start[0], colIndex], + end: [range.end[0], colIndex + 1], + }); + } + }); + } + const extraCellsMap = new Map(); const extraCells = ranges.map(this.getExtraSpanCellsForRange).flat(); /** diff --git a/source/src/components/InfiniteTable/InfiniteCls.css.ts b/source/src/components/InfiniteTable/InfiniteCls.css.ts index 2dd8d8f3..030ca3a2 100644 --- a/source/src/components/InfiniteTable/InfiniteCls.css.ts +++ b/source/src/components/InfiniteTable/InfiniteCls.css.ts @@ -54,6 +54,10 @@ const activeCellBorderVarsForUnfocusedState = { }; export const InfiniteClsRecipe = recipe({ variants: { + horizontalLayout: { + true: {}, + false: {}, + }, focused: { true: {}, false: {}, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx index 5f2c88e0..563b5493 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx @@ -153,6 +153,7 @@ export function InfiniteTableHeaderCell( columnHeaderHeight, columnReorderDragColumnId, columnMenuVisibleForColumnId, + columnReorderInPageIndex, }, getDataSourceMasterContext, } = useInfiniteTable(); @@ -167,8 +168,13 @@ export function InfiniteTableHeaderCell( const { allRowsSelected, someRowsSelected, selectionMode } = useDataSourceState(); + const horizontalLayoutPageIndex = props.horizontalLayoutPageIndex; const dragging = columnReorderDragColumnId === column.id; + const insideDisabledDraggingPage = + columnReorderInPageIndex != null + ? horizontalLayoutPageIndex !== columnReorderInPageIndex + : false; const { onResize, height, width, headerOptions, columnsMap } = props; const sortInfo = column.computedSortInfo; @@ -261,7 +267,7 @@ export function InfiniteTableHeaderCell( const menuIcon = ; const initialRenderParam: InfiniteTableColumnHeaderParam = { - horizontalLayoutPageIndex: props.horizontalLayoutPageIndex, + horizontalLayoutPageIndex, dragging, domRef: ref, insideColumnMenu: false, @@ -493,6 +499,7 @@ export function InfiniteTableHeaderCell( const { onPointerDown, proxyPosition } = useColumnPointerEvents({ columnId: column.id, domRef, + horizontalLayoutPageIndex, }); let draggingProxy = null; @@ -563,7 +570,7 @@ export function InfiniteTableHeaderCell( column.components?.FilterOperatorSwitch; const resizeHandle = useColumnResizeHandle(column, { - horizontalLayoutPageIndex: props.horizontalLayoutPageIndex, + horizontalLayoutPageIndex, }); const zIndex = `var(${columnZIndexAtIndex}-${column.computedVisibleIndex})`; @@ -614,6 +621,7 @@ export function InfiniteTableHeaderCell( HeaderCellRecipe, { dragging, + insideDisabledDraggingPage, align, verticalAlign, rowSelected: false, @@ -636,7 +644,7 @@ export function InfiniteTableHeaderCell( {showColumnFilters ? ( column.computedFilterable ? ( ( @@ -24,7 +23,7 @@ export function getColumnResizer( return { resize(diff: number) { // set the new width for the current column - setInfiniteColumnWidth( + InternalVarUtils.columnWidths.set( colIndex, column.computedWidth + diff, domRef.current, @@ -38,7 +37,7 @@ export function getColumnResizer( if (!nextColumn) { return; } - setInfiniteColumnWidth( + InternalVarUtils.columnWidths.set( nextColIndex, nextColumn.computedWidth - diff, domRef.current, @@ -47,7 +46,7 @@ export function getColumnResizer( if (nextColumn.computedPinned === 'start') { addToInfiniteColumnOffset(nextColumn, diff, domRef.current); } else { - setInfiniteColumnOffset( + InternalVarUtils.columnOffsets.set( nextColIndex, nextColumn.computedOffset + diff, domRef.current, @@ -79,7 +78,11 @@ export function getColumnResizer( if (col.computedPinned === 'end') { continue; } - setInfiniteColumnOffset(i, col.computedOffset + diff, domRef.current); + InternalVarUtils.columnOffsets.set( + i, + col.computedOffset + diff, + domRef.current, + ); } return; } @@ -90,7 +93,11 @@ export function getColumnResizer( if (col.computedPinned) { continue; } - setInfiniteColumnOffset(i, col.computedOffset + diff, domRef.current); + InternalVarUtils.columnOffsets.set( + i, + col.computedOffset + diff, + domRef.current, + ); } }, }; @@ -137,14 +144,14 @@ export function getColumnGroupResizer( const column = columns[colIndex]; const newWidth = column.computedWidth + diff; - setInfiniteColumnWidth(colIndex, newWidth, node); + InternalVarUtils.columnWidths.set(colIndex, newWidth, node); if (column.computedPinned === 'end') { return; } if (offset) { - setInfiniteColumnOffset(colIndex, offset, node); + InternalVarUtils.columnOffsets.set(colIndex, offset, node); } offset += newWidth; }); @@ -159,7 +166,11 @@ export function getColumnGroupResizer( if (col.computedPinned === 'end') { continue; } - setInfiniteColumnOffset(i, col.computedOffset + diffSum, node); + InternalVarUtils.columnOffsets.set( + i, + col.computedOffset + diffSum, + node, + ); } }, }; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts b/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts index 3700620d..97430d93 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts @@ -212,6 +212,15 @@ export const HeaderCellRecipe = recipe({ true: {}, false: {}, }, + insideDisabledDraggingPage: { + true: { + opacity: + ThemeVars.components.Cell + .horizontalLayoutColumnReorderDisabledPageOpacity, + }, + + false: {}, + }, first: { true: ColumnCellVariantsObject.first, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx index 5fefa96d..f6949da3 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx @@ -198,7 +198,8 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { componentActions: dataSourceActions, } = useDataSourceContextValue(); - const { activeRowIndex, keyboardNavigation } = getState(); + const { activeRowIndex, keyboardNavigation, columnReorderInPageIndex } = + getState(); const rowActive = rowIndex === activeRowIndex && keyboardNavigation === 'row'; const renderingContext: InfiniteTableColumnRenderingContext = { @@ -617,6 +618,11 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { } : null; + const insideDisabledDraggingPage = + columnReorderInPageIndex != null + ? horizontalLayoutPageIndex !== columnReorderInPageIndex + : false; + const afterChildren = editor; const theChildren = renderChildren(); const cellProps: InfiniteTableCellProps & @@ -645,6 +651,7 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { ColumnCellRecipe, { dragging: false, + insideDisabledDraggingPage, zebra, align, verticalAlign, diff --git a/source/src/components/InfiniteTable/components/cell.css.ts b/source/src/components/InfiniteTable/components/cell.css.ts index 85e9c944..e63a8021 100644 --- a/source/src/components/InfiniteTable/components/cell.css.ts +++ b/source/src/components/InfiniteTable/components/cell.css.ts @@ -249,6 +249,14 @@ export const ColumnCellRecipe = recipe({ ], variants: { dragging: { false: {}, true: {} }, + insideDisabledDraggingPage: { + true: { + opacity: + ThemeVars.components.Cell + .horizontalLayoutColumnReorderDisabledPageOpacity, + }, + false: {}, + }, cellSelected: { false: {}, true: {} }, align: { start: {}, diff --git a/source/src/components/InfiniteTable/hooks/reorderColumnsOnDrag.ts b/source/src/components/InfiniteTable/hooks/reorderColumnsOnDrag.ts index 1ab8a543..c3528d8e 100644 --- a/source/src/components/InfiniteTable/hooks/reorderColumnsOnDrag.ts +++ b/source/src/components/InfiniteTable/hooks/reorderColumnsOnDrag.ts @@ -11,8 +11,10 @@ import { InfiniteTablePropColumnPinning, InfiniteTableProps, } from '../types/InfiniteTableProps'; +import { CSSCalc } from '../utils/CSSCalc'; import { clearInfiniteColumnReorderDuration, + InternalVarUtils, restoreInfiniteColumnReorderDuration, setInfiniteColumnOffsetWhileReordering, setInfiniteVarOnRoot, @@ -33,8 +35,8 @@ type MousePosition = { type ColumnBreakpoint = { columnId: string; - index: number; - breakpoint: number; + columnIndex: number; + offsetX: number; }; type GetBreakPointsOptions = { @@ -52,6 +54,8 @@ export type ReorderParams = { computedPinnedStartColumns: InfiniteTableComputedColumn[]; computedPinnedEndColumns: InfiniteTableComputedColumn[]; computedUnpinnedColumns: InfiniteTableComputedColumn[]; + horizontalLayoutPageIndex: number | null; + pageWidth: number | null; draggableColumnsRestrictTo: NonUndefined< InfiniteTableProps['draggableColumnsRestrictTo'] @@ -76,8 +80,6 @@ export type ReorderParams = { api: InfiniteTableApi; setProxyPosition: (position: TopLeftOrNull) => void; - columnOffsetAtIndexCSSVar: string; - columnWidthAtIndexCSSVar: string; }; const PROXY_OFFSET = 10; @@ -87,8 +89,6 @@ const pinnedStartWidthCSSVar = stripVar(InternalVars.pinnedStartWidth); export function reorderColumnsOnDrag(params: ReorderParams) { const { - columnOffsetAtIndexCSSVar, - columnWidthAtIndexCSSVar, computedVisibleColumns, computedVisibleColumnsMap, computedPinnedEndColumns, @@ -101,6 +101,8 @@ export function reorderColumnsOnDrag(params: ReorderParams) { tableRect, setProxyPosition, api: imperativeApi, + horizontalLayoutPageIndex, + pageWidth, } = params; // returns the total width of the pinned start columns @@ -128,20 +130,25 @@ export function reorderColumnsOnDrag(params: ReorderParams) { } function isColumnVisible(b: ColumnBreakpoint) { - if (b.index === dragColumnIndex) { + if (b.columnIndex === dragColumnIndex) { return true; } - const col = computedVisibleColumns[b.index]; + const col = computedVisibleColumns[b.columnIndex]; if (col.computedPinned) { return true; } const scrollLeft = brain.getScrollPosition().scrollLeft; + const colOffset = + col.computedOffset + + (horizontalLayoutPageIndex != null && pageWidth != null + ? pageWidth * horizontalLayoutPageIndex + : 0); return ( - col.computedOffset >= scrollLeft + getCurrentPinnedWidth('start') && - col.computedOffset + col.computedWidth < + colOffset >= scrollLeft + getCurrentPinnedWidth('start') && + colOffset + col.computedWidth < scrollLeft + tableRect.width - getCurrentPinnedWidth('end') ); } @@ -155,12 +162,17 @@ export function reorderColumnsOnDrag(params: ReorderParams) { let breakpoints: ColumnBreakpoint[] = []; columns.forEach((c) => { - const b = { + if (c.computedVisibleIndex === dragColumnIndex) { + // the current column is the drag column, + // and we don't need a breakpoint for it + // the breakpoints of all the other columns will be enough + return; + } + + const b: ColumnBreakpoint = { columnId: c.id, - index: c.computedVisibleIndex, - breakpoint: - // tableRect.left + - c.computedOffset + Math.round(c.computedWidth / 2), + columnIndex: c.computedVisibleIndex, + offsetX: c.computedOffset + Math.round(c.computedWidth / 2), }; if (isColumnVisible(b)) { @@ -344,7 +356,7 @@ export function reorderColumnsOnDrag(params: ReorderParams) { } function updateDropIndex(newDropIndex: number) { - if (newDropIndex === currentDropIndex) { + if (newDropIndex == null || newDropIndex === currentDropIndex) { return; } @@ -409,27 +421,34 @@ export function reorderColumnsOnDrag(params: ReorderParams) { newPosition = isDragColumn ? // this is the column we're dragging to the left // so we place it exactly at the offset of the currentDropIndex - `var(${columnOffsetAtIndexCSSVar}-${currentDropIndex})` + InternalVarUtils.columnOffsets.get(currentDropIndex) : isOutsideCol ? // this col should have the initial pos, so we're good - `var(${columnOffsetAtIndexCSSVar}-${colIndex})` + InternalVarUtils.columnOffsets.get(colIndex) : // place it at the default offset + the width of the drag column // as these are columns situated between the dragindex and the dropindex - `calc( var(${columnOffsetAtIndexCSSVar}-${colIndex}) + var(${columnWidthAtIndexCSSVar}-${dragColumnIndex}) )`; + CSSCalc.add( + InternalVarUtils.columnOffsets.get(colIndex), + InternalVarUtils.columnWidths.get(dragColumnIndex), + ).done(); // newColumnIndexes.push(colCurrentIndex); } else { newPosition = isOutsideCol ? // this is the default value for a column - `var(${columnOffsetAtIndexCSSVar}-${colIndex})` + InternalVarUtils.columnOffsets.get(colIndex) : isDragColumn ? // this is the column we're dragging to the right - `calc( var(${columnOffsetAtIndexCSSVar}-${ - currentDropIndex - 1 - }) + var(${columnWidthAtIndexCSSVar}-${ - currentDropIndex - 1 - }) - var(${columnWidthAtIndexCSSVar}-${dragColumnIndex}) )` - : `calc( var(${columnOffsetAtIndexCSSVar}-${colIndex}) - var(${columnWidthAtIndexCSSVar}-${dragColumnIndex}) )`; + CSSCalc.add( + InternalVarUtils.columnOffsets.get(currentDropIndex - 1), + InternalVarUtils.columnWidths.get(currentDropIndex - 1), + ) + .minus(InternalVarUtils.columnWidths.get(dragColumnIndex)) + .done() + : CSSCalc.minus( + InternalVarUtils.columnOffsets.get(colIndex), + InternalVarUtils.columnWidths.get(dragColumnIndex), + ).done(); } setInfiniteColumnOffsetWhileReordering( @@ -477,29 +496,35 @@ export function reorderColumnsOnDrag(params: ReorderParams) { ? dragColumn.computedOffset + diffX : dragColumn.computedOffset + dragColumn.computedWidth + diffX; - let idx = binarySearch<{ breakpoint: number }, number>( + let idx = binarySearch<{ offsetX: number }, number>( breakpoints, currentPos, - ({ breakpoint }, value) => { - return breakpoint < value ? -1 : breakpoint > value ? 1 : 0; + ({ offsetX }, value) => { + return offsetX < value ? -1 : offsetX > value ? 1 : 0; }, ); if (idx < 0) { idx = ~idx; } - idx = breakpoints[idx] - ? breakpoints[idx].index - : breakpoints[breakpoints.length - 1].index + 1; - - if (idx != null && idx !== currentDropIndex) { - if (dir === 1 && currentDropIndex === dragColumnIndex + 1) { - idx = dragColumnIndex; + if (!breakpoints.length) { + if (computedVisibleColumns.length > 1) { + console.error('No breakpoints available, something is wrong!'); } - - updateDropIndex(idx); + idx = 0; + } else { + idx = breakpoints[idx] + ? breakpoints[idx].columnIndex + : breakpoints[breakpoints.length - 1].columnIndex + 1; + } + // the drag column has 2 valid positionswhen it doesn't change the order - either before itself or after itself + // which is basically the same position, but let's normalise to only have one of them + if (idx === dragColumnIndex + 1) { + idx = dragColumnIndex; } + updateDropIndex(idx); + const columnPinning: InfiniteTablePropColumnPinning = {}; // will need to adjust pinning end to take into account the count of pinned end cols after the drag diff --git a/source/src/components/InfiniteTable/hooks/useCellClassName.ts b/source/src/components/InfiniteTable/hooks/useCellClassName.ts index 4ba41460..0750ac70 100644 --- a/source/src/components/InfiniteTable/hooks/useCellClassName.ts +++ b/source/src/components/InfiniteTable/hooks/useCellClassName.ts @@ -11,6 +11,7 @@ export function useCellClassName( baseClasses: string[], variants: (x: ColumnCellVariantsType | HeaderCellVariantsType) => string, extraFlags: { + insideDisabledDraggingPage: boolean; groupRow: boolean; firstRow: boolean; firstRowInHorizontalLayoutPage: boolean; @@ -39,6 +40,7 @@ export function useCellClassName( rowSelected: extraFlags.rowSelected ?? 'null', rowActive: extraFlags.rowActive, dragging: extraFlags.dragging, + insideDisabledDraggingPage: extraFlags.insideDisabledDraggingPage, firstRow: extraFlags.firstRow ?? false, firstRowInHorizontalLayoutPage: extraFlags.firstRowInHorizontalLayoutPage ?? false, diff --git a/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts b/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts index 27672ffa..698ec7b4 100644 --- a/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts +++ b/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts @@ -21,9 +21,8 @@ import { import { shallowEqualObjects } from '../../../utils/shallowEqualObjects'; import { InfiniteTablePropColumnPinning } from '../types'; import adjustColumnOrderForAllColumns from './adjustColumnOrderForAllColumns'; +import { HorizontalLayoutMatrixBrain } from '../../VirtualBrain/HorizontalLayoutMatrixBrain'; -const columnOffsetAtIndex = stripVar(InternalVars.columnOffsetAtIndex); -const columnWidthAtIndex = stripVar(InternalVars.columnWidthAtIndex); const baseZIndexForCells = stripVar(InternalVars.baseZIndexForCells); type TopLeft = { @@ -52,9 +51,11 @@ const equalPinning = ( export const useColumnPointerEvents = ({ columnId, domRef, + horizontalLayoutPageIndex, }: { columnId: string; domRef: React.MutableRefObject; + horizontalLayoutPageIndex: number | null; }) => { const [proxyPosition, setProxyPosition] = useState(null); @@ -115,6 +116,15 @@ export const useColumnPointerEvents = ({ const initialAvailableSize = brain.getAvailableSize(); const dragColumnIndex = dragColumn.computedVisibleIndex; + const dragColumnIndexInHorizontalLayout = brain.getVirtualColIndex( + dragColumn.computedVisibleIndex, + { + pageIndex: horizontalLayoutPageIndex || 0, + }, + ); + const pageWidth = brain.isHorizontalLayoutBrain + ? (brain as HorizontalLayoutMatrixBrain).getPageWidth() + : null; let initialCursor: React.CSSProperties['cursor'] = target.style.cursor ?? 'auto'; @@ -123,9 +133,9 @@ export const useColumnPointerEvents = ({ const dragger = reorderColumnsOnDrag({ brain, + horizontalLayoutPageIndex, + pageWidth, draggableColumnsRestrictTo, - columnOffsetAtIndexCSSVar: columnOffsetAtIndex, - columnWidthAtIndexCSSVar: columnWidthAtIndex, computedPinnedEndColumns, computedPinnedStartColumns, computedUnpinnedColumns, @@ -145,6 +155,9 @@ export const useColumnPointerEvents = ({ let reorderDragResult: ReorderDragResult | null = null; + let discardAlwaysRenderedColumn: VoidFunction | undefined; + let restoreRenderRange: VoidFunction | undefined; + function persistColumnOrder(reorderDragResult: ReorderDragResult) { const { columnPinning, columnOrder } = reorderDragResult; @@ -186,17 +199,41 @@ export const useColumnPointerEvents = ({ // by increasing the render count // we could have a method that says: keep this column rendered (the current column) // even if the scrolling changes horizontally - brain.setRenderCount({ - horizontal: computedVisibleColumns.length, - vertical: undefined, - }); - headerBrain.setRenderCount({ - horizontal: computedVisibleColumns.length, - vertical: undefined, - }); + + const discardForBody = brain.keepColumnRendered( + dragColumnIndexInHorizontalLayout, + ); + const discardForHeader = headerBrain.keepColumnRendered( + dragColumnIndexInHorizontalLayout, + ); + + if (brain.isHorizontalLayoutBrain) { + const extendBy = true; + const restoreBodyRange = brain.extendRenderRange({ + left: extendBy, + right: extendBy, + }); + const restoreHeaderRange = headerBrain.extendRenderRange({ + left: extendBy, + right: extendBy, + }); + restoreRenderRange = () => { + restoreBodyRange(); + restoreHeaderRange(); + }; + } + + discardAlwaysRenderedColumn = () => { + discardForBody(); + discardForHeader(); + }; actions.columnReorderDragColumnId = dragColumn.id; + if (horizontalLayoutPageIndex != null) { + actions.columnReorderInPageIndex = horizontalLayoutPageIndex; + } + setInfiniteColumnZIndex( dragColumnIndex, `calc( var(${baseZIndexForCells}) + 10000 )`, @@ -220,6 +257,8 @@ export const useColumnPointerEvents = ({ brain.update({ ...initialAvailableSize, }); + discardAlwaysRenderedColumn?.(); + restoreRenderRange?.(); computedVisibleColumns.forEach((col) => { clearInfiniteColumnReorderDuration( @@ -267,6 +306,9 @@ export const useColumnPointerEvents = ({ } actions.columnReorderDragColumnId = false; + if (horizontalLayoutPageIndex != null) { + actions.columnReorderInPageIndex = null; + } }; target.addEventListener('pointermove', onPointerMove); @@ -274,7 +316,7 @@ export const useColumnPointerEvents = ({ target.setPointerCapture(e.pointerId); }, - [columnId], + [columnId, horizontalLayoutPageIndex], ); return { diff --git a/source/src/components/InfiniteTable/hooks/useDOMProps.ts b/source/src/components/InfiniteTable/hooks/useDOMProps.ts index 8fdcf481..e4ecb754 100644 --- a/source/src/components/InfiniteTable/hooks/useDOMProps.ts +++ b/source/src/components/InfiniteTable/hooks/useDOMProps.ts @@ -11,10 +11,7 @@ import { } from '../InfiniteCls.css'; import { InternalVars } from '../internalVars.css'; import { InfiniteTableComputedColumn } from '../types'; -import { - getCSSVarNameForColOffset, - getCSSVarNameForColWidth, -} from '../utils/infiniteDOMUtils'; +import { InternalVarUtils } from '../utils/infiniteDOMUtils'; import { rafFn } from '../utils/rafFn'; import { ThemeVars } from '../vars.css'; import { useInfiniteTable } from './useInfiniteTable'; @@ -51,7 +48,6 @@ const scrollbarWidthHorizontal = stripVar( const scrollbarWidthVertical = stripVar(InternalVars.scrollbarWidthVertical); const columnWidthAtIndex = stripVar(InternalVars.columnWidthAtIndex); -const columnOffsetAtIndex = stripVar(InternalVars.columnOffsetAtIndex); const columnZIndexAtIndex = stripVar(InternalVars.columnZIndexAtIndex); const scrollLeftCSSVar = stripVar(InternalVars.scrollLeft); // const columnReorderEffectDurationAtIndex = stripVar( @@ -120,6 +116,7 @@ export function useDOMProps( onSelfBlur, bodySize, activeCellIndex, + wrapRowsHorizontally, } = state; const { computedPinnedStartColumnsWidth, @@ -137,10 +134,11 @@ export function useDOMProps( const index = col.computedVisibleIndex; //@ts-ignore - vars[`${columnWidthAtIndex}-${index}`] = col.computedWidth + 'px'; + vars[InternalVarUtils.columnWidths.varName.get(index)] = + col.computedWidth + 'px'; //@ts-ignore - vars[`${columnOffsetAtIndex}-${index}`] = + vars[InternalVarUtils.columnOffsets.varName.get(index)] = col.computedPinned === 'start' ? `calc( ${col.computedOffset}px + var(${scrollLeftCSSVar}) )` : col.computedPinned === 'end' @@ -148,8 +146,8 @@ export function useDOMProps( `calc( var(${pinnedEndOffsetCSSVar}) ${ prevPinnedEndCols.length ? '+' : '' } ${prevPinnedEndCols - .map( - (c) => `var(${columnWidthAtIndex}-${c.computedVisibleIndex})`, + .map((c) => + InternalVarUtils.columnWidths.get(c.computedVisibleIndex), ) .join(' + ')} + var(${scrollLeftCSSVar}) )` : `${col.computedOffset}px`; @@ -208,12 +206,12 @@ export function useDOMProps( if (activeCellIndex != null) { //@ts-ignore - cssVars[activeCellColWidth] = `var(${getCSSVarNameForColWidth( + cssVars[activeCellColWidth] = InternalVarUtils.columnWidths.get( activeCellIndex[1], - )})`; - const defaultActiveCellColOffset = `var(${getCSSVarNameForColOffset( + ); + const defaultActiveCellColOffset = InternalVarUtils.columnOffsets.get( activeCellIndex[1], - )})`; + ); if (state.brain.isHorizontalLayoutBrain) { const pageIndex = state.brain.getPageIndexForRow(activeCellIndex[0]); @@ -242,7 +240,7 @@ export function useDOMProps( cssVars[pinnedStartWidthCSSVar] = `calc( ` + computedPinnedStartColumns - .map((c) => `var(${getCSSVarNameForColWidth(c.computedVisibleIndex)})`) + .map((c) => InternalVarUtils.columnWidths.get(c.computedVisibleIndex)) .join(' + ') + ')'; @@ -250,7 +248,7 @@ export function useDOMProps( cssVars[pinnedEndWidthCSSVar] = `calc( ` + computedPinnedEndColumns - .map((c) => `var(${getCSSVarNameForColWidth(c.computedVisibleIndex)})`) + .map((c) => InternalVarUtils.columnWidths.get(c.computedVisibleIndex)) .join(' + ') + ')'; //@ts-ignore @@ -336,6 +334,9 @@ export function useDOMProps( InfiniteTableClassName, InfiniteCls, + wrapRowsHorizontally + ? `${InfiniteTableClassName}--horizontal-layout` + : null, domProps?.className, computedPinnedStartColumnsWidth ? `${InfiniteTableClassName}--has-pinned-start ${InfiniteClsHasPinnedStart}` @@ -358,6 +359,7 @@ export function useDOMProps( : null, InfiniteClsRecipe({ + horizontalLayout: !!wrapRowsHorizontally, hasPinnedStart: !!computedPinnedStartColumnsWidth, hasPinnedEnd: !!computedPinnedEndColumnsWidth, // hasPinnedStartOverflow: !!computed.computedPinnedStartOverflow, diff --git a/source/src/components/InfiniteTable/state/getInitialState.ts b/source/src/components/InfiniteTable/state/getInitialState.ts index 0bf1407f..45854c0d 100644 --- a/source/src/components/InfiniteTable/state/getInitialState.ts +++ b/source/src/components/InfiniteTable/state/getInitialState.ts @@ -189,6 +189,7 @@ export function initSetupState({ scrollLeft: 0, }, columnReorderDragColumnId: false, + columnReorderInPageIndex: null, ready: false, focused: false, focusedWithin: false, diff --git a/source/src/components/InfiniteTable/types/InfiniteTableState.ts b/source/src/components/InfiniteTable/types/InfiniteTableState.ts index a78aed6b..10d0df0a 100644 --- a/source/src/components/InfiniteTable/types/InfiniteTableState.ts +++ b/source/src/components/InfiniteTable/types/InfiniteTableState.ts @@ -113,6 +113,7 @@ export interface InfiniteTableSetupState { focused: boolean; ready: boolean; columnReorderDragColumnId: false | string; + columnReorderInPageIndex: number | null; columnVisibilityForGrouping: Record; focusedWithin: boolean; scrollPosition: ScrollPosition; diff --git a/source/src/components/InfiniteTable/utils/CSSCalc.jestspec.ts b/source/src/components/InfiniteTable/utils/CSSCalc.jestspec.ts new file mode 100644 index 00000000..22852bc6 --- /dev/null +++ b/source/src/components/InfiniteTable/utils/CSSCalc.jestspec.ts @@ -0,0 +1,91 @@ +import { CSSCalc as calc } from './CSSCalc'; + +it('should work for +', () => { + expect(calc.add(2, 3).done()).toEqual('calc(2 + 3)'); + expect(calc.add(calc.subtract(5, 3), 2).done()).toEqual('calc(5 - 3 + 2)'); + expect(calc.add(calc.subtract(calc.add(2, 3), 5)).done()).toEqual( + 'calc(2 + 3 - 5)', + ); + expect(calc.add(calc.subtract(calc.add(2, 3), 5), 12).done()).toEqual( + 'calc(2 + 3 - 5 + 12)', + ); +}); + +it('should work for real-case usage', () => { + expect(calc.add('a', 'b').subtract('x').add('y').done()).toEqual( + 'calc(a + b - x + y)', + ); +}); + +it('should work for + with nested calcs', () => { + expect(calc.add(1, calc.multiply(2, 3).done()).done()).toEqual( + 'calc(1 + calc(2 * 3))', + ); +}); + +it('should work for + with chaining', () => { + expect(calc.add(2, 3).subtract(1).done()).toEqual('calc(2 + 3 - 1)'); +}); + +it('should work for *', () => { + expect(calc.multiply(2, 3).done()).toEqual('calc(2 * 3)'); + expect(calc.multiply(calc.subtract(5, 3), 2).done()).toEqual( + 'calc((5 - 3) * 2)', + ); + expect(calc.multiply(calc.subtract(calc.multiply(2, 3), 5)).done()).toEqual( + 'calc(2 * 3 - 5)', + ); + expect( + calc.multiply(calc.subtract(calc.multiply(2, 3), 5), 12).done(), + ).toEqual('calc((2 * 3 - 5) * 12)'); +}); + +it('should work for * with nested calcs', () => { + expect(calc.multiply(1, calc.multiply(2, 3).done()).done()).toEqual( + 'calc(1 * calc(2 * 3))', + ); +}); + +it('should work for * with chaining', () => { + expect(calc.multiply(2, 3).subtract(1).done()).toEqual('calc(2 * 3 - 1)'); +}); + +it('should work for /', () => { + expect(calc.divide(2, 3).done()).toEqual('calc(2 / 3)'); + expect(calc.divide(calc.subtract(5, 3), 2).done()).toEqual( + 'calc((5 - 3) / 2)', + ); + expect(calc.subtract(calc.divide(2, 3), 5).done()).toEqual('calc(2 / 3 - 5)'); + expect(calc.divide(calc.subtract(calc.divide(2, 3), 5), 12).done()).toEqual( + 'calc((2 / 3 - 5) / 12)', + ); +}); + +it('should work for / with nested calcs', () => { + expect(calc.divide(1, calc.divide(2, 3).done()).done()).toEqual( + 'calc(1 / calc(2 / 3))', + ); +}); + +it('should work for / with chaining', () => { + expect(calc.divide(2, 3).subtract(1).done()).toEqual('calc(2 / 3 - 1)'); +}); + +it('should work for complex case', () => { + expect(calc.multiply(2, calc.add(3, 4).done()).done()).toEqual( + 'calc(2 * calc(3 + 4))', + ); + expect( + calc + .multiply( + 2, + calc.add( + 3, + calc.subtract(9, calc.multiply(calc.minus(8, 7), calc.add('b', 1))), + ), + 5, + calc.divide(6, calc.minus(7).done()), + ) + .done(), + ).toEqual('calc(2 * (3 + 9 - (8 - 7) * (b + 1)) * 5 * (6 / calc(7)))'); +}); diff --git a/source/src/components/InfiniteTable/utils/CSSCalc.ts b/source/src/components/InfiniteTable/utils/CSSCalc.ts new file mode 100644 index 00000000..f7218463 --- /dev/null +++ b/source/src/components/InfiniteTable/utils/CSSCalc.ts @@ -0,0 +1,104 @@ +type CSSContinuationType = { + __operation: CSSCalcOperation | null; + __get: (parent?: CSSContinuationType) => string; + done: () => string; +} & CSSCalcMethods; + +type CSSCalcOperation = '*' | '/' | '+' | '-'; +type CSSOperand = string | number | CSSContinuationType; + +type CSSCalcMethods = { + add: (...args: CSSOperand[]) => CSSContinuationType; + subtract: (...args: CSSOperand[]) => CSSContinuationType; + minus: (...args: CSSOperand[]) => CSSContinuationType; + multiply: (...args: CSSOperand[]) => CSSContinuationType; + divide: (...args: CSSOperand[]) => CSSContinuationType; +}; + +function mapToContinuation(args: CSSOperand[]): CSSContinuationType[] { + return args.map((arg) => + arg instanceof CssContinuation ? arg : new CssContinuation(null, [arg]), + ); +} + +export class CssContinuation implements CSSContinuationType { + __operands: CSSOperand[] = []; + __operation: CSSCalcOperation | null = null; + + constructor(operation: CSSCalcOperation | null, operands: CSSOperand[] = []) { + this.__operands = operands; + this.__operation = operation; + } + + add(...args: CSSOperand[]): CSSContinuationType { + return new CssContinuation('+', [this, ...args]); + } + subtract(...args: CSSOperand[]): CSSContinuationType { + return new CssContinuation('-', [this, ...args]); + } + minus(...args: CSSOperand[]): CSSContinuationType { + return this.subtract(...args); + } + multiply(...args: CSSOperand[]): CSSContinuationType { + return new CssContinuation('*', [this, ...args]); + } + divide(...args: CSSOperand[]): CSSContinuationType { + return new CssContinuation('/', [this, ...args]); + } + __get(parent?: CSSContinuationType): string { + const { __operation, __operands } = this; + + const result = __operands + .map((operand) => operandToString(operand, this)) + .join(` ${__operation} `); + + const skipParens = + __operands.length <= 1 + ? true + : parent?.__operation === '+' || parent?.__operation === '-'; + + return skipParens ? result : `(${result})`; + } + done(): string { + const str = this.__get(); + + const hasParens = str.startsWith('(') && str.endsWith(')'); + const value = hasParens ? str : `(${str})`; + + return `calc${value}`; + } +} + +function operandToString( + operand: CSSOperand, + parent?: CSSContinuationType, +): string { + if (typeof operand === 'string' || typeof operand === 'number') { + return `${operand}`; + } + if (!parent) { + return operand.done(); + } + + return operand.__get(parent); +} + +const add = (...args: CSSOperand[]) => + new CssContinuation('+', mapToContinuation(args)); + +const subtract = (...args: CSSOperand[]) => + new CssContinuation('-', mapToContinuation(args)); + +const multiply = (...args: CSSOperand[]) => + new CssContinuation('*', mapToContinuation(args)); + +const divide = (...args: CSSOperand[]) => + new CssContinuation('/', mapToContinuation(args)); + +export const CSSCalc = { + add, + subtract, + minus: subtract, + multiply, + divide, +}; diff --git a/source/src/components/InfiniteTable/utils/infiniteDOMUtils.ts b/source/src/components/InfiniteTable/utils/infiniteDOMUtils.ts index b2d94d53..2846d0f5 100644 --- a/source/src/components/InfiniteTable/utils/infiniteDOMUtils.ts +++ b/source/src/components/InfiniteTable/utils/infiniteDOMUtils.ts @@ -42,9 +42,8 @@ export function setInfiniteVarOnNode( node: HTMLElement | null, ) { if (node) { - const prop = InternalVars[varName as keyof typeof InternalVars] - ? stripVar(InternalVars[varName as keyof typeof InternalVars]) - : varName; + const name = varName as keyof typeof InternalVars; + const prop = InternalVars[name] ? stripVar(InternalVars[name]) : varName; node.style.setProperty(prop, `${varValue}`); } @@ -67,12 +66,13 @@ export function setInfiniteVarsOnNode( ) { if (node) { for (var varName in vars) { - node.style.setProperty( - InternalVars[varName as keyof typeof InternalVars] - ? stripVar(InternalVars[varName as keyof typeof InternalVars]) - : varName, - `${vars[varName as keyof typeof InternalVars]}`, - ); + const propName = InternalVars[varName as keyof typeof InternalVars] + ? stripVar(InternalVars[varName as keyof typeof InternalVars]) + : varName; + + const propValue = `${vars[varName as keyof typeof InternalVars]}`; + + node.style.setProperty(propName, propValue); } } } @@ -169,12 +169,74 @@ export function restoreInfiniteColumnReorderDuration( ); } +type InternalVarUtilsType = { + columnWidths: { + get: (index: number) => string; + getVarName: (index: number) => string; + varName: { + get: (index: number) => string; + }; + set: ( + index: number, + value: string | number, + node: HTMLElement | null, + ) => void; + }; + + columnOffsets: { + get: (index: number) => string; + getVarName: (index: number) => string; + varName: { + get: (index: number) => string; + }; + set: ( + index: number, + value: string | number, + node: HTMLElement | null, + ) => void; + }; +}; + +export const InternalVarUtils: InternalVarUtilsType = { + columnWidths: { + get(index: number) { + return `var(${this.getVarName(index)})`; + }, + getVarName(index: number) { + return this.varName.get(index); + }, + varName: { + get(index: number) { + return `${columnWidthAtIndex}-${index}`; + }, + }, + set(index: number, value: string | number, node: HTMLElement | null) { + value = typeof value === 'number' ? value + 'px' : value; + setInfiniteVarOnRoot(this.varName.get(index), value, node); + }, + }, + columnOffsets: { + get(index: number) { + return `var(${this.getVarName(index)})`; + }, + getVarName(index: number) { + return this.varName.get(index); + }, + varName: { + get(index: number) { + return `${columnOffsetAtIndex}-${index}`; + }, + }, + set(index: number, value: string | number, node: HTMLElement | null) { + value = typeof value === 'number' ? value + 'px' : value; + setInfiniteVarOnRoot(this.varName.get(index), value, node); + }, + }, +}; + export function getCSSVarNameForColWidth(colIndex: number) { return `${columnWidthAtIndex}-${colIndex}`; } -export function getCSSVarNameForColOffset(colIndex: number) { - return `${columnOffsetAtIndex}-${colIndex}`; -} export function setInfiniteScrollPosition( scrollPosition: ScrollPosition, diff --git a/source/src/components/InfiniteTable/vars-default-light.css.ts b/source/src/components/InfiniteTable/vars-default-light.css.ts index a8f0be3a..7043ba07 100644 --- a/source/src/components/InfiniteTable/vars-default-light.css.ts +++ b/source/src/components/InfiniteTable/vars-default-light.css.ts @@ -66,6 +66,8 @@ const CellVars = { [ThemeVars.components.Cell.color]: 'currentColor', [ThemeVars.components.Cell.borderWidth]: '1px', [ThemeVars.components.Cell.flashingOverlayZIndex]: -1, + [ThemeVars.components.Cell + .horizontalLayoutColumnReorderDisabledPageOpacity]: 0.3, [ThemeVars.components.Cell.flashingBackground]: ThemeVars.color.accent, [ThemeVars.components.Cell.flashingUpBackground]: ThemeVars.color.success, [ThemeVars.components.Cell.flashingDownBackground]: ThemeVars.color.error, diff --git a/source/src/components/InfiniteTable/vars.css.ts b/source/src/components/InfiniteTable/vars.css.ts index 800bb378..be6354e4 100644 --- a/source/src/components/InfiniteTable/vars.css.ts +++ b/source/src/components/InfiniteTable/vars.css.ts @@ -172,6 +172,8 @@ export const ThemeVars = createGlobalThemeContract( borderRadius: 'cell-border-radius', reorderEffectDuration: 'column-reorder-effect-duration', pinnedBorder: 'pinned-cell-border', + horizontalLayoutColumnReorderDisabledPageOpacity: + 'horizontal-layout-column-reorder-disabled-page-opacity', /** * Text color inside rows. Defaults to `currentColor` diff --git a/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts b/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts index 9275a5ec..ceee8fa1 100644 --- a/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts +++ b/source/src/components/VirtualBrain/HorizontalLayoutMatrixBrain.ts @@ -309,6 +309,10 @@ export class HorizontalLayoutMatrixBrain extends MatrixBrain implements IBrain { this._totalPageCount = value; } + getPageWidth() { + return this.pageWidth; + } + doUpdateRenderCount(which: WhichDirection = ALL_DIRECTIONS) { const rowHeight = this.getInitialRowHeight(); diff --git a/source/src/components/VirtualBrain/MatrixBrain.ts b/source/src/components/VirtualBrain/MatrixBrain.ts index 3f7c1a1e..380f786b 100644 --- a/source/src/components/VirtualBrain/MatrixBrain.ts +++ b/source/src/components/VirtualBrain/MatrixBrain.ts @@ -125,6 +125,8 @@ export class MatrixBrain extends Logger implements IBrain { protected cols: MatrixBrainOptions['cols'] = 0; protected rows: MatrixBrainOptions['rows'] = 0; + protected alwaysRenderedColumns: Set = new Set(); + /** * This is only here for easier accessing in the renderer, when the horizontal layout is enabled. * In this way, the API is the same for both brains, so we don't have to cast the brain type in the renderer. @@ -432,10 +434,13 @@ export class MatrixBrain extends Logger implements IBrain { const currentRenderCount = this.horizontalRenderCount; const restore = () => { - this.setRenderCount({ - horizontal: currentRenderCount, - vertical: undefined, - }); + this.setRenderCount( + { + horizontal: currentRenderCount, + vertical: undefined, + }, + true, + ); }; const { start, end } = this.getRenderRange(); @@ -948,13 +953,16 @@ export class MatrixBrain extends Logger implements IBrain { return result; }; - setRenderCount = ({ - horizontal, - vertical, - }: { - horizontal: number | undefined; - vertical: number | undefined; - }) => { + setRenderCount = ( + { + horizontal, + vertical, + }: { + horizontal: number | undefined; + vertical: number | undefined; + }, + force?: boolean, + ) => { if (horizontal === undefined) { horizontal = this.horizontalRenderCount; } @@ -964,7 +972,7 @@ export class MatrixBrain extends Logger implements IBrain { const horizontalSame = horizontal === this.horizontalRenderCount; const verticalSame = vertical === this.verticalRenderCount; - if (horizontalSame && verticalSame) { + if (horizontalSame && verticalSame && !force) { return; } @@ -972,8 +980,8 @@ export class MatrixBrain extends Logger implements IBrain { this.verticalRenderCount = vertical; this.updateRenderRange({ - horizontal: !horizontalSame, - vertical: !verticalSame, + horizontal: force ? true : !horizontalSame, + vertical: force ? true : !verticalSame, }); this.notifyRenderCountChange(); @@ -1581,6 +1589,18 @@ export class MatrixBrain extends Logger implements IBrain { return result; } + keepColumnRendered = (colIndex: number) => { + this.alwaysRenderedColumns.add(colIndex); + + return () => { + this.alwaysRenderedColumns.delete(colIndex); + }; + }; + + getAlwaysRenderedColumns = () => { + return Array.from(this.alwaysRenderedColumns); + }; + setRenderRange = ({ horizontal, vertical, @@ -1739,6 +1759,7 @@ export class MatrixBrain extends Logger implements IBrain { this.rowspanParent.clear(); this.colspanParent.clear(); + this.alwaysRenderedColumns.clear(); this.destroyed = true; this.onDestroyFns = []; this.onScrollFns = [];