From 043d21c43a61eed4e0900577599defb4ae6b480c Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 21 Mar 2020 22:32:58 +0100 Subject: [PATCH] column scroll --- packages/web/src/TabContent.js | 5 +- packages/web/src/datagrid/DataGridCore.js | 96 ++++- .../{SeriesSizes.js => SeriesSizes-old.js} | 0 packages/web/src/datagrid/SeriesSizes.ts | 331 ++++++++++++++++++ packages/web/src/datagrid/types.ts | 2 +- packages/web/src/tabs/TableDataTab.js | 4 +- 6 files changed, 419 insertions(+), 19 deletions(-) rename packages/web/src/datagrid/{SeriesSizes.js => SeriesSizes-old.js} (100%) create mode 100644 packages/web/src/datagrid/SeriesSizes.ts diff --git a/packages/web/src/TabContent.js b/packages/web/src/TabContent.js index b9be4ece2..9ea38f9ed 100644 --- a/packages/web/src/TabContent.js +++ b/packages/web/src/TabContent.js @@ -48,10 +48,11 @@ export default function TabContent() { return _.keys(mountedTabs).map(tabid => { const { TabComponent, props } = mountedTabs[tabid]; + const tabVisible = tabid == (selectedTab && selectedTab.tabid); return ( // @ts-ignore - - + + ); }); diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index f1eb0714f..a1eaa1e26 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -110,7 +110,7 @@ function CellFormattedValue({ value }) { /** @param props {import('./types').DataGridProps} */ export default function DataGridCore(props) { - const { conid, database, display, isMainGrid } = props; + const { conid, database, display, tabVisible } = props; const columns = display.getGridColumns(); // console.log(`GRID, conid=${conid}, database=${database}, sql=${sql}`); @@ -124,6 +124,12 @@ export default function DataGridCore(props) { const loadedTimeRef = React.useRef(0); + const [vScrollValueToSet, setvScrollValueToSet] = React.useState(); + const [vScrollValueToSetDate, setvScrollValueToSetDate] = React.useState(new Date()); + + const [hScrollValueToSet, sethScrollValueToSet] = React.useState(); + const [hScrollValueToSetDate, sethScrollValueToSetDate] = React.useState(new Date()); + const [currentCell, setCurrentCell] = React.useState(topLeftCell); const [selectedCells, setSelectedCells] = React.useState(emptyCellArray); const [dragStartCell, setDragStartCell] = React.useState(nullCell); @@ -186,19 +192,23 @@ export default function DataGridCore(props) { const [headerRowRef, { height: rowHeight }] = useDimensions(); const [tableBodyRef] = useDimensions(); const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions(); - const tableRef = React.useRef(); + const [tableRef, { height: tableHeight, width: tableWidth }, tableElement] = useDimensions(); const columnSizes = React.useMemo(() => countColumnSizes(), [loadedRows, containerWidth, display]); + const headerColWidth = 40; // console.log('containerWidth', containerWidth); const gridScrollAreaHeight = containerHeight - 2 * rowHeight; - const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize; + const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize - headerColWidth; const visibleRowCountUpperBound = Math.ceil(gridScrollAreaHeight / Math.floor(rowHeight)); const visibleRowCountLowerBound = Math.floor(gridScrollAreaHeight / Math.ceil(rowHeight)); // const visibleRowCountUpperBound = 20; // const visibleRowCountLowerBound = 20; + // console.log('containerHeight', containerHeight); + // console.log('visibleRowCountUpperBound', visibleRowCountUpperBound); + // console.log('rowHeight', rowHeight); const reload = () => { setLoadProps({ @@ -221,11 +231,11 @@ export default function DataGridCore(props) { }); React.useEffect(() => { - if (isMainGrid) { + if (tabVisible) { // @ts-ignore - if (tableRef.current) tableRef.current.focus(); + if (tableElement) tableElement.focus(); } - }, []); + }, [tabVisible, tableElement]); if (!loadedRows || !columns) return null; const rowCountNewIncluded = loadedRows.length; @@ -280,7 +290,7 @@ export default function DataGridCore(props) { // if (headerWidth > this.rowHeaderWidth) this.rowHeaderWidth = headerWidth; context.font = '14px Helvetica'; - for (let row of loadedRows) { + for (let row of loadedRows.slice(0, 20)) { for (let colIndex = 0; colIndex < columns.length; colIndex++) { let uqName = columns[colIndex].uniqueName; let text = row[uqName]; @@ -397,9 +407,8 @@ export default function DataGridCore(props) { if (col < 0) col = 0; if (col >= columnSizes.realCount) col = columnSizes.realCount - 1; setCurrentCell([row, col]); - if (!event.shiftKey) { - setSelectedCells([]); - } + setSelectedCells([...(event.ctrlKey ? selectedCells : []), [row, col]]); + scrollIntoView([row, col]); // this.selectedCells.push(this.currentCell); // this.scrollIntoView(this.currentCell); @@ -407,6 +416,50 @@ export default function DataGridCore(props) { return true; } + function scrollIntoView(cell) { + const [row, col] = cell; + + if (row != null) { + let newRow = null; + const rowCount = rowCountNewIncluded; + + if (row < firstVisibleRowScrollIndex) newRow = row; + else if (row + 1 >= firstVisibleRowScrollIndex + visibleRowCountLowerBound) + newRow = row - visibleRowCountLowerBound + 2; + + if (newRow < 0) newRow = 0; + if (newRow >= rowCount) newRow = rowCount - 1; + + if (newRow != null) { + setFirstVisibleRowScrollIndex(newRow); + // firstVisibleRowScrollIndex = newRow; + setvScrollValueToSet(newRow); + setvScrollValueToSetDate(new Date()); + // vscroll.value = newRow; + } + //int newRow = _rowSizes.ScrollInView(FirstVisibleRowScrollIndex, cell.Row.Value - _rowSizes.FrozenCount, GridScrollAreaHeight); + //ScrollContent(newRow, FirstVisibleColumnScrollIndex); + } + + if (col != null) { + if (col >= columnSizes.frozenCount) { + let newColumn = columnSizes.scrollInView( + firstVisibleColumnScrollIndex, + col - columnSizes.frozenCount, + gridScrollAreaWidth + ); + setFirstVisibleColumnScrollIndex(newColumn); + + // @ts-ignore + sethScrollValueToSet(newColumn); + sethScrollValueToSetDate(new Date()); + + // firstVisibleColumnScrollIndex = newColumn; + // hscroll.value = newColumn; + } + } + } + function cellIsSelected(row, col) { const [currentRow, currentCol] = currentCell; if (row == currentRow && col == currentCol) return true; @@ -424,7 +477,8 @@ export default function DataGridCore(props) { // console.log('containerHeight', containerHeight); const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth); - // console.log('visibleColumnCount', visibleColumnCount); + console.log('visibleColumnCount', visibleColumnCount); + console.log('gridScrollAreaWidth', gridScrollAreaWidth); const visibleRealColumnIndexes = []; const modelIndexes = {}; @@ -459,7 +513,7 @@ export default function DataGridCore(props) { }); } - const hederColwidthPx = '40px'; + const hederColwidthPx = `${headerColWidth}px`; const filterCount = display.filterCount; const handleClearFilters = () => { @@ -467,6 +521,12 @@ export default function DataGridCore(props) { }; // console.log('visibleRealColumnIndexes', visibleRealColumnIndexes); + console.log( + 'gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()', + gridScrollAreaWidth, + columnSizes.getVisibleScrollSizeSum() + ); + return ( ( - + {firstVisibleRowScrollIndex + index + 1} {realColumns.map(col => (
x.size); + } + public get realCount(): number { + return this.frozenCount + this.scrollCount; + } + + // public clear(): void { + // this.scrollItems = []; + // this.sizeOverridesByModelIndex = {}; + // this.positions = []; + // this.scrollIndexes = []; + // this.frozenItems = []; + // this.hiddenAndFrozenModelIndexes = null; + // this.frozenModelIndexes = null; + // } + public putSizeOverride(modelIndex: number, size: number, sizeByUser = false): void { + if (this.maxSize && size > this.maxSize && !sizeByUser) { + size = this.maxSize; + } + + let currentSize = this.sizeOverridesByModelIndex[modelIndex]; + if (sizeByUser || !currentSize || size > currentSize) { + this.sizeOverridesByModelIndex[modelIndex] = size; + } + // if (!_.has(this.sizeOverridesByModelIndex, modelIndex)) + // this.sizeOverridesByModelIndex[modelIndex] = size; + // if (size > this.sizeOverridesByModelIndex[modelIndex]) + // this.sizeOverridesByModelIndex[modelIndex] = size; + } + public buildIndex(): void { + this.scrollItems = []; + this.scrollIndexes = _.filter( + _.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount), + x => x >= 0 + ); + this.scrollIndexes.sort(); + let lastScrollIndex: number = -1; + let lastEndPosition: number = 0; + this.scrollIndexes.forEach(scrollIndex => { + let modelIndex: number = this.realToModel(scrollIndex + this.frozenCount); + let size: number = this.sizeOverridesByModelIndex[modelIndex]; + let item = new SeriesSizeItem(); + item.scrollIndex = scrollIndex; + item.modelIndex = modelIndex; + item.size = size; + item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize; + this.scrollItems.push(item); + lastScrollIndex = scrollIndex; + lastEndPosition = item.endPosition; + }); + this.positions = _.map(this.scrollItems, x => x.position); + this.frozenItems = []; + let lastpos: number = 0; + for (let i: number = 0; i < this.frozenCount; i++) { + let modelIndex: number = this.frozenModelIndexes[i]; + let size: number = this.getSizeByModelIndex(modelIndex); + let item = new SeriesSizeItem(); + item.frozenIndex = i; + item.modelIndex = modelIndex; + item.size = size; + item.position = lastpos; + this.frozenItems.push(item); + lastpos += size; + } + } + + public getScrollIndexOnPosition(position: number): number { + let itemOrder: number = _.sortedIndex(this.positions, position); + if (this.positions[itemOrder] == position) return itemOrder; + if (itemOrder == 0) return Math.floor(position / this.defaultSize); + if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex; + return ( + Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) + + this.scrollItems[itemOrder - 1].scrollIndex + ); + } + public getFrozenIndexOnPosition(position: number): number { + this.frozenItems.forEach(function(item) { + if (position >= item.position && position <= item.endPosition) return item.frozenIndex; + }); + return -1; + } + // public getSizeSum(startScrollIndex: number, endScrollIndex: number): number { + // let order1: number = _.sortedIndexOf(this.scrollIndexes, startScrollIndex); + // let order2: number = _.sortedIndexOf(this.scrollIndexes, endScrollIndex); + // let count: number = endScrollIndex - startScrollIndex; + // if (order1 < 0) + // order1 = ~order1; + // if (order2 < 0) + // order2 = ~order2; + // let result: number = 0; + // for (let i: number = order1; i <= order2; i++) { + // if (i < 0) + // continue; + // if (i >= this.scrollItems.length) + // continue; + // let item = this.scrollItems[i]; + // if (item.scrollIndex < startScrollIndex) + // continue; + // if (item.scrollIndex >= endScrollIndex) + // continue; + // result += item.size; + // count--; + // } + // result += count * this.defaultSize; + // return result; + // } + public getSizeByModelIndex(modelIndex: number): number { + if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex]; + return this.defaultSize; + } + public getSizeByScrollIndex(scrollIndex: number): number { + return this.getSizeByRealIndex(scrollIndex + this.frozenCount); + } + public getSizeByRealIndex(realIndex: number): number { + let modelIndex: number = this.realToModel(realIndex); + return this.getSizeByModelIndex(modelIndex); + } + public removeSizeOverride(realIndex: number): void { + let modelIndex: number = this.realToModel(realIndex); + delete this.sizeOverridesByModelIndex[modelIndex]; + } + public getScroll(sourceScrollIndex: number, targetScrollIndex: number): number { + if (sourceScrollIndex < targetScrollIndex) { + return -_.sum( + _.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x)) + ); + } else { + return _.sum( + _.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x)) + ); + } + } + public modelIndexIsInScrollArea(modelIndex: number): boolean { + let realIndex = this.modelToReal(modelIndex); + return realIndex >= this.frozenCount; + } + public getTotalScrollSizeSum(): number { + let scrollSizeOverrides = _.map( + _.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)), + x => this.sizeOverridesByModelIndex[x] + ); + return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize; + } + public getVisibleScrollSizeSum(): number { + let scrollSizeOverrides = _.map( + _.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)), + x => this.sizeOverridesByModelIndex[x] + ); + return ( + _.sum(scrollSizeOverrides) + + (this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize + ); + } + private intKeys(value): number[] { + return _.keys(value).map(x => _.parseInt(x)); + } + public getPositionByRealIndex(realIndex: number): number { + if (realIndex < 0) return 0; + if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position; + return this.getPositionByScrollIndex(realIndex - this.frozenCount); + } + public getPositionByScrollIndex(scrollIndex: number): number { + let order: number = _.sortedIndex(this.scrollIndexes, scrollIndex); + if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position; + order--; + if (order < 0) return scrollIndex * this.defaultSize; + return ( + this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize + ); + } + public getVisibleScrollCount(firstVisibleIndex: number, viewportSize: number): number { + let res: number = 0; + let index: number = firstVisibleIndex; + let count: number = 0; + while (res < viewportSize && index <= this.scrollCount) { + res += this.getSizeByScrollIndex(index); + index++; + count++; + } + return count; + } + public getVisibleScrollCountReversed(lastVisibleIndex: number, viewportSize: number): number { + let res: number = 0; + let index: number = lastVisibleIndex; + let count: number = 0; + while (res < viewportSize && index >= 0) { + res += this.getSizeByScrollIndex(index); + index--; + count++; + } + return count; + } + public invalidateAfterScroll( + oldFirstVisible: number, + newFirstVisible: number, + invalidate: (_: number) => void, + viewportSize: number + ): void { + if (newFirstVisible > oldFirstVisible) { + let oldVisibleCount: number = this.getVisibleScrollCount(oldFirstVisible, viewportSize); + let newVisibleCount: number = this.getVisibleScrollCount(newFirstVisible, viewportSize); + for (let i: number = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) { + invalidate(i + this.frozenCount); + } + } else { + for (let i: number = newFirstVisible; i <= oldFirstVisible; i++) { + invalidate(i + this.frozenCount); + } + } + } + public isWholeInView(firstVisibleIndex: number, index: number, viewportSize: number): boolean { + let res: number = 0; + let testedIndex: number = firstVisibleIndex; + while (res < viewportSize && testedIndex < this.count) { + res += this.getSizeByScrollIndex(testedIndex); + if (testedIndex == index) return res <= viewportSize; + testedIndex++; + } + return false; + } + public scrollInView(firstVisibleIndex: number, scrollIndex: number, viewportSize: number): number { + if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) { + return firstVisibleIndex; + } + if (scrollIndex < firstVisibleIndex) { + return scrollIndex; + } + let res: number = 0; + let testedIndex: number = scrollIndex; + while (res < viewportSize && testedIndex >= 0) { + let size: number = this.getSizeByScrollIndex(testedIndex); + if (res + size > viewportSize) return testedIndex + 1; + testedIndex--; + res += size; + } + if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1; + return firstVisibleIndex; + } + public resize(realIndex: number, newSize: number): void { + if (realIndex < 0) return; + let modelIndex: number = this.realToModel(realIndex); + if (modelIndex < 0) return; + this.sizeOverridesByModelIndex[modelIndex] = newSize; + this.buildIndex(); + } + public setExtraordinaryIndexes(hidden: number[], frozen: number[]): void { + //this._hiddenAndFrozenModelIndexes = _.clone(hidden); + hidden = hidden.filter(x => x >= 0); + frozen = frozen.filter(x => x >= 0); + + hidden.sort((a, b) => a - b); + frozen.sort((a, b) => a - b); + this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x)); + this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x)); + this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes); + this.frozenModelIndexes.sort((a, b) => a - b); + if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null; + if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null; + this.buildIndex(); + } + public realToModel(realIndex: number): number { + if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex; + if (realIndex < 0) return -1; + if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex]; + if (this.hiddenAndFrozenModelIndexes == null) return realIndex; + realIndex -= this.frozenCount; + for (let hidItem of this.hiddenAndFrozenModelIndexes) { + if (realIndex < hidItem) return realIndex; + realIndex++; + } + return realIndex; + } + public modelToReal(modelIndex: number): number { + if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex; + if (modelIndex < 0) return -1; + let frozenIndex: number = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1; + if (frozenIndex >= 0) return frozenIndex; + if (this.hiddenAndFrozenModelIndexes == null) return modelIndex; + let hiddenIndex: number = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex); + if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1; + if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount; + return modelIndex; + } + public getFrozenPosition(frozenIndex: number): number { + return this.frozenItems[frozenIndex].position; + } + public hasSizeOverride(modelIndex: number): boolean { + return _.has(this.sizeOverridesByModelIndex, modelIndex); + } + public isVisible(testedRealIndex: number, firstVisibleScrollIndex: number, viewportSize: number): boolean { + if (testedRealIndex < 0) return false; + if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true; + let scrollIndex: number = testedRealIndex - this.frozenCount; + let onPageIndex: number = scrollIndex - firstVisibleScrollIndex; + return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize); + } +} diff --git a/packages/web/src/datagrid/types.ts b/packages/web/src/datagrid/types.ts index beb92d6af..ef43b79b8 100644 --- a/packages/web/src/datagrid/types.ts +++ b/packages/web/src/datagrid/types.ts @@ -4,5 +4,5 @@ export interface DataGridProps { conid: number; database: string; display: GridDisplay; - isMainGrid?: boolean; + tabVisible?: boolean; } diff --git a/packages/web/src/tabs/TableDataTab.js b/packages/web/src/tabs/TableDataTab.js index eafd5a0c6..32eac4f1b 100644 --- a/packages/web/src/tabs/TableDataTab.js +++ b/packages/web/src/tabs/TableDataTab.js @@ -9,7 +9,7 @@ import useConnectionInfo from '../utility/useConnectionInfo'; import engines from '@dbgate/engines'; import getTableInfo from '../utility/getTableInfo'; -export default function TableDataTab({ conid, database, schemaName, pureName }) { +export default function TableDataTab({ conid, database, schemaName, pureName, tabVisible }) { const tableInfo = useTableInfo({ conid, database, schemaName, pureName }); const [config, setConfig] = React.useState(createGridConfig()); const [cache, setCache] = React.useState(createGridCache()); @@ -24,7 +24,7 @@ export default function TableDataTab({ conid, database, schemaName, pureName }) conid={conid} database={database} display={display} - isMainGrid + tabVisible={tabVisible} /> ); }