From 6f12b4fd14e5237db394117715697306d260b3a5 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 1 Feb 2020 18:00:44 +0100 Subject: [PATCH] virtual datagrid --- README.md | 2 +- web/package.json | 1 + web/src/datagrid/DataGrid.js | 158 ++++++++++++-- web/src/datagrid/ScrollBars.js | 10 +- web/src/datagrid/SeriesSizes.js | 340 +++++++++++++++++++++++++++++++ web/src/utility/useDimensions.js | 133 ++++++++---- web/yarn.lock | 5 + 7 files changed, 590 insertions(+), 59 deletions(-) create mode 100644 web/src/datagrid/SeriesSizes.js diff --git a/README.md b/README.md index 94ac1fb2..53cb77bf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) -[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz) +[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur) # DbGate - database administration tool diff --git a/web/package.json b/web/package.json index f72fdbe8..3ecb1790 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "react-dom": "^16.12.0", "react-modal": "^3.11.1", "react-scripts": "3.3.0", + "resize-observer-polyfill": "^1.5.1", "socket.io-client": "^2.3.0", "styled-components": "^4.4.1", "uuid": "^3.4.0" diff --git a/web/src/datagrid/DataGrid.js b/web/src/datagrid/DataGrid.js index 694eb7ca..e38fae70 100644 --- a/web/src/datagrid/DataGrid.js +++ b/web/src/datagrid/DataGrid.js @@ -1,11 +1,18 @@ -import React, { useState } from 'react'; +import React from 'react'; import useFetch from '../utility/useFetch'; import styled from 'styled-components'; import theme from '../theme'; import { HorizontalScrollBar, VerticalScrollBar } from './ScrollBars'; import useDimensions from '../utility/useDimensions'; +import { SeriesSizes } from './SeriesSizes'; -const GridContainer = styled.div``; +const GridContainer = styled.div` + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +`; const Table = styled.table` position: absolute; @@ -62,15 +69,24 @@ export default function DataGrid({ params }) { params, }); const { rows, columns } = data || {}; - const [firstVisibleRowScrollIndex, setFirstVisibleRowScrollIndex] = useState(0); + const [firstVisibleRowScrollIndex, setFirstVisibleRowScrollIndex] = React.useState(0); + const [firstVisibleColumnScrollIndex, setFirstVisibleColumnScrollIndex] = React.useState(0); const [headerRowRef, { height: rowHeight }] = useDimensions(); - const [tableBodyRef, { height: gridScrollAreaHeight }] = useDimensions(); + const [tableBodyRef] = useDimensions(); + const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions(); - // const visibleRowCountUpperBound = Math.ceil(gridScrollAreaHeight / Math.floor(rowHeight)); - // const visibleRowCountLowerBound = Math.floor(gridScrollAreaHeight / Math.ceil(rowHeight)); - const visibleRowCountUpperBound = 20; - const visibleRowCountLowerBound = 20; + const columnSizes = React.useMemo(() => countColumnSizes(), [data, containerWidth]); + + console.log('containerWidth', containerWidth); + + const gridScrollAreaHeight = containerHeight - 2 * rowHeight; + const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize; + + const visibleRowCountUpperBound = Math.ceil(gridScrollAreaHeight / Math.floor(rowHeight)); + const visibleRowCountLowerBound = Math.floor(gridScrollAreaHeight / Math.ceil(rowHeight)); + // const visibleRowCountUpperBound = 20; + // const visibleRowCountLowerBound = 20; if (!columns || !rows) return null; const rowCountNewIncluded = rows.length; @@ -79,16 +95,119 @@ export default function DataGrid({ params }) { setFirstVisibleRowScrollIndex(value); }; - console.log('visibleRowCountUpperBound', visibleRowCountUpperBound); - console.log('gridScrollAreaHeight', gridScrollAreaHeight); + function countColumnSizes() { + let canvas = document.createElement('canvas'); + let context = canvas.getContext('2d'); + + //return this.context.measureText(txt).width; + const columnSizes = new SeriesSizes(); + if (!rows || !columns) return columnSizes; + + console.log('countColumnSizes', rows.length, containerWidth); + + columnSizes.maxSize = (containerWidth * 2) / 3; + columnSizes.count = columns.length; + + // columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes()); + columnSizes.setExtraordinaryIndexes([], []); + + // for (let colIndex = 0; colIndex < columns.length; colIndex++) { + // //this.columnSizes.PutSizeOverride(col, this.columns[col].Name.length * 8); + // let column = columns[colIndex]; + + // if (column.columnClientObject != null && column.columnClientObject.notNull) context.font = "bold 14px Helvetica"; + // else context.font = "14px Helvetica"; + + // let text = column.headerText; + // let headerWidth = context.measureText(text).width + 32; + + // if (column.columnClientObject != null && column.columnClientObject.icon != null) headerWidth += 16; + // if (this.getFilterOnColumn(column.uniquePath)) headerWidth += 16; + // if (this.getSortOrder(column.uniquePath)) headerWidth += 16; + + // this.columnSizes.putSizeOverride(colIndex, headerWidth); + // } + + // let headerWidth = this.rowHeaderWidthDefault; + // if (this.rowCount) headerWidth = context.measureText(this.rowCount.toString()).width + 8; + // this.rowHeaderWidth = this.rowHeaderWidthDefault; + // if (headerWidth > this.rowHeaderWidth) this.rowHeaderWidth = headerWidth; + + context.font = '14px Helvetica'; + for (let row of data.rows) { + for (let colIndex = 0; colIndex < columns.length; colIndex++) { + let colName = columns[colIndex].name; + let text = row[colName]; + let width = context.measureText(text).width + 8; + // console.log('colName', colName, text, width); + columnSizes.putSizeOverride(colIndex, width); + // let colName = this.columns[colIndex].uniquePath; + // let text: string = row[colName].gridText; + // let width = context.measureText(text).width + 8; + // if (row[colName].dataPrefix) width += context.measureText(row[colName].dataPrefix).width + 3; + // this.columnSizes.putSizeOverride(colIndex, width); + } + } + + // for (let modelIndex = 0; modelIndex < this.columns.length; modelIndex++) { + // let width = getHashValue(this.widthHashPrefix + this.columns[modelIndex].uniquePath); + // if (width) this.columnSizes.putSizeOverride(modelIndex, _.toNumber(width), true); + // } + + columnSizes.buildIndex(); + return columnSizes; + } + + // console.log('visibleRowCountUpperBound', visibleRowCountUpperBound); + // console.log('gridScrollAreaHeight', gridScrollAreaHeight); + // console.log('containerHeight', containerHeight); + + const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth); + console.log('visibleColumnCount', visibleColumnCount); + + const visibleRealColumnIndexes = []; + const modelIndexes = {}; + const realColumns = []; + + // frozen columns + for (let colIndex = 0; colIndex < columnSizes.frozenCount; colIndex++) { + visibleRealColumnIndexes.push(colIndex); + } + // scroll columns + for ( + let colIndex = firstVisibleColumnScrollIndex; + colIndex < firstVisibleColumnScrollIndex + visibleColumnCount; + colIndex++ + ) { + visibleRealColumnIndexes.push(colIndex + columnSizes.frozenCount); + } + + // real columns + for (let colIndex of visibleRealColumnIndexes) { + let modelColumnIndex = columnSizes.realToModel(colIndex); + modelIndexes[colIndex] = modelColumnIndex; + + let col = columns[modelColumnIndex]; + if (!col) continue; + const widthNumber = columnSizes.getSizeByRealIndex(colIndex); + realColumns.push({ + ...col, + widthPx: `${widthNumber}px`, + }); + } + + console.log('visibleRealColumnIndexes', visibleRealColumnIndexes); return ( - + - {columns.map(col => ( - + {realColumns.map(col => ( + {col.name} ))} @@ -99,8 +218,11 @@ export default function DataGrid({ params }) { .slice(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound) .map((row, index) => ( - {columns.map(col => ( - + {realColumns.map(col => ( + {row[col.name]} ))} @@ -108,7 +230,11 @@ export default function DataGrid({ params }) { ))}
- + { const position01 = (valueToSet - minimum) / (maximum - minimum + 1); const position = position01 * (contentSize - width); - if (ref.current) ref.current.scrollLeft = Math.floor(position); + if (node) node.scrollLeft = Math.floor(position); }, [valueToSetDate]); return ( @@ -61,17 +61,17 @@ export function VerticalScrollBar({ maximum, viewportRatio = 0.5, }) { - const [ref, { height }] = useDimensions(); + const [ref, { height }, node] = useDimensions(); const contentSize = Math.round(height / viewportRatio); React.useEffect(() => { const position01 = (valueToSet - minimum) / (maximum - minimum + 1); const position = position01 * (contentSize - height); - if (ref.current) ref.current.scrollTop = Math.floor(position); + if (node) node.scrollTop = Math.floor(position); }, [valueToSetDate]); const handleScroll = () => { - const position = ref.current.scrollTop; + const position = node.scrollTop; const ratio = position / (contentSize - height); if (ratio < 0) return 0; let res = ratio * (maximum - minimum + 1) + minimum; diff --git a/web/src/datagrid/SeriesSizes.js b/web/src/datagrid/SeriesSizes.js new file mode 100644 index 00000000..df96d5e3 --- /dev/null +++ b/web/src/datagrid/SeriesSizes.js @@ -0,0 +1,340 @@ +import _ from 'lodash'; + +export class SeriesSizeItem { + constructor() { + this.scrollIndex = -1; + this.frozenIndex = -1; + this.modelIndex = 0; + this.size = 0; + this.position = 0; + } + + // modelIndex; + // size; + // position; + + get endPosition() { + return this.position + this.size; + } +} + +export class SeriesSizes { + constructor() { + this.scrollItems = []; + this.sizeOverridesByModelIndex = {}; + this.positions = []; + this.scrollIndexes = []; + this.frozenItems = []; + this.hiddenAndFrozenModelIndexes = null; + this.frozenModelIndexes = null; + + this.count = 0; + this.maxSize = 1000; + this.defaultSize = 50; + } + + // private sizeOverridesByModelIndex: { [id] } = {}; + // count; + // defaultSize; + // maxSize; + // private hiddenAndFrozenModelIndexes[] = []; + // private frozenModelIndexes[] = []; + // private hiddenModelIndexes[] = []; + // private scrollItems: SeriesSizeItem[] = []; + // private positions[] = []; + // private scrollIndexes[] = []; + // private frozenItems: SeriesSizeItem[] = []; + + get scrollCount() { + return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0); + } + get frozenCount() { + return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0; + } + get frozenSize() { + return _.sumBy(this.frozenItems, x => x.size); + } + get realCount() { + return this.frozenCount + this.scrollCount; + } + + putSizeOverride(modelIndex, size, sizeByUser = false) { + 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; + } + buildIndex() { + this.scrollItems = []; + this.scrollIndexes = _.filter( + _.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount), + x => x >= 0 + ); + this.scrollIndexes.sort(); + let lastScrollIndex = -1; + let lastEndPosition = 0; + this.scrollIndexes.forEach(scrollIndex => { + let modelIndex = this.realToModel(scrollIndex + this.frozenCount); + let size = 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 = 0; + for (let i = 0; i < this.frozenCount; i++) { + let modelIndex = this.frozenModelIndexes[i]; + let size = 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; + } + } + + getScrollIndexOnPosition(position) { + let itemOrder = _.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 + ); + } + getFrozenIndexOnPosition(position) { + this.frozenItems.forEach(function(item) { + if (position >= item.position && position <= item.endPosition) return item.frozenIndex; + }); + return -1; + } + // getSizeSum(startScrollIndex, endScrollIndex) { + // let order1 = _.sortedIndexOf(this.scrollIndexes, startScrollIndex); + // let order2 = _.sortedIndexOf(this.scrollIndexes, endScrollIndex); + // let count = endScrollIndex - startScrollIndex; + // if (order1 < 0) + // order1 = ~order1; + // if (order2 < 0) + // order2 = ~order2; + // let result = 0; + // for (let i = 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; + // } + getSizeByModelIndex(modelIndex) { + if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex]; + return this.defaultSize; + } + getSizeByScrollIndex(scrollIndex) { + return this.getSizeByRealIndex(scrollIndex + this.frozenCount); + } + getSizeByRealIndex(realIndex) { + let modelIndex = this.realToModel(realIndex); + return this.getSizeByModelIndex(modelIndex); + } + removeSizeOverride(realIndex) { + let modelIndex = this.realToModel(realIndex); + delete this.sizeOverridesByModelIndex[modelIndex]; + } + getScroll(sourceScrollIndex, targetScrollIndex) { + 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)) + ); + } + } + modelIndexIsInScrollArea(modelIndex) { + let realIndex = this.modelToReal(modelIndex); + return realIndex >= this.frozenCount; + } + getTotalScrollSizeSum() { + 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; + } + getVisibleScrollSizeSum() { + 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 + ); + } + intKeys(value) { + return _.keys(value).map(x => _.parseInt(x)); + } + getPositionByRealIndex(realIndex) { + if (realIndex < 0) return 0; + if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position; + return this.getPositionByScrollIndex(realIndex - this.frozenCount); + } + getPositionByScrollIndex(scrollIndex) { + let order = _.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 + ); + } + getVisibleScrollCount(firstVisibleIndex, viewportSize) { + let res = 0; + let index = firstVisibleIndex; + let count = 0; + while (res < viewportSize && index <= this.scrollCount) { + console.log('this.getSizeByScrollIndex(index)', this.getSizeByScrollIndex(index)); + res += this.getSizeByScrollIndex(index); + index++; + count++; + } + console.log('getVisibleScrollCount', firstVisibleIndex, viewportSize, count); + return count; + } + getVisibleScrollCountReversed(lastVisibleIndex, viewportSize) { + let res = 0; + let index = lastVisibleIndex; + let count = 0; + while (res < viewportSize && index >= 0) { + res += this.getSizeByScrollIndex(index); + index--; + count++; + } + return count; + } + invalidateAfterScroll(oldFirstVisible, newFirstVisible, invalidate, viewportSize) { + if (newFirstVisible > oldFirstVisible) { + let oldVisibleCount = this.getVisibleScrollCount(oldFirstVisible, viewportSize); + let newVisibleCount = this.getVisibleScrollCount(newFirstVisible, viewportSize); + for (let i = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) { + invalidate(i + this.frozenCount); + } + } else { + for (let i = newFirstVisible; i <= oldFirstVisible; i++) { + invalidate(i + this.frozenCount); + } + } + } + isWholeInView(firstVisibleIndex, index, viewportSize) { + let res = 0; + let testedIndex = firstVisibleIndex; + while (res < viewportSize && testedIndex < this.count) { + res += this.getSizeByScrollIndex(testedIndex); + if (testedIndex == index) return res <= viewportSize; + testedIndex++; + } + return false; + } + scrollInView(firstVisibleIndex, scrollIndex, viewportSize) { + if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) { + return firstVisibleIndex; + } + if (scrollIndex < firstVisibleIndex) { + return scrollIndex; + } + let res = 0; + let testedIndex = scrollIndex; + while (res < viewportSize && testedIndex >= 0) { + let size = this.getSizeByScrollIndex(testedIndex); + if (res + size > viewportSize) return testedIndex + 1; + testedIndex--; + res += size; + } + if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1; + return firstVisibleIndex; + } + resize(realIndex, newSize) { + if (realIndex < 0) return; + let modelIndex = this.realToModel(realIndex); + if (modelIndex < 0) return; + this.sizeOverridesByModelIndex[modelIndex] = newSize; + this.buildIndex(); + } + setExtraordinaryIndexes(hidden, frozen) { + //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(); + } + realToModel(realIndex) { + 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; + } + modelToReal(modelIndex) { + if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex; + if (modelIndex < 0) return -1; + let frozenIndex = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1; + if (frozenIndex >= 0) return frozenIndex; + if (this.hiddenAndFrozenModelIndexes == null) return modelIndex; + let hiddenIndex = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex); + if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1; + if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount; + return modelIndex; + } + getFrozenPosition(frozenIndex) { + return this.frozenItems[frozenIndex].position; + } + hasSizeOverride(modelIndex) { + return _.has(this.sizeOverridesByModelIndex, modelIndex); + } + isVisible(testedRealIndex, firstVisibleScrollIndex, viewportSize) { + if (testedRealIndex < 0) return false; + if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true; + let scrollIndex = testedRealIndex - this.frozenCount; + let onPageIndex = scrollIndex - firstVisibleScrollIndex; + return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize); + } +} diff --git a/web/src/utility/useDimensions.js b/web/src/utility/useDimensions.js index faa22a80..62f83e2e 100644 --- a/web/src/utility/useDimensions.js +++ b/web/src/utility/useDimensions.js @@ -1,48 +1,107 @@ -import { useState, useCallback, useLayoutEffect } from 'react'; +// import { useState, useCallback, useLayoutEffect } from 'react'; -function getDimensionObject(node) { - const rect = node.getBoundingClientRect(); +// function getDimensionObject(node) { +// const rect = node.getBoundingClientRect(); - return { - width: rect.width, - height: rect.height, - top: 'x' in rect ? rect.x : rect.top, - left: 'y' in rect ? rect.y : rect.left, - x: 'x' in rect ? rect.x : rect.left, - y: 'y' in rect ? rect.y : rect.top, - right: rect.right, - bottom: rect.bottom, - }; -} +// return { +// width: rect.width, +// height: rect.height, +// top: 'x' in rect ? rect.x : rect.top, +// left: 'y' in rect ? rect.y : rect.left, +// x: 'x' in rect ? rect.x : rect.left, +// y: 'y' in rect ? rect.y : rect.top, +// right: rect.right, +// bottom: rect.bottom, +// }; +// } -function useDimensions({ liveMeasure = true } = {}) { - const [dimensions, setDimensions] = useState({}); +// function useDimensions({ liveMeasure = true } = {}) { +// const [dimensions, setDimensions] = useState({}); +// const [node, setNode] = useState(null); + +// const ref = useCallback(node => { +// setNode(node); +// }, []); + +// useLayoutEffect(() => { +// if (node) { +// const measure = () => window.requestAnimationFrame(() => setDimensions(getDimensionObject(node))); +// measure(); + +// if (liveMeasure) { +// window.addEventListener('resize', measure); +// window.addEventListener('scroll', measure); + +// return () => { +// window.removeEventListener('resize', measure); +// window.removeEventListener('scroll', measure); +// }; +// } +// } +// }, [node]); + +// return [ref, dimensions, node]; +// } + +// export default useDimensions; + +import { useLayoutEffect, useState, useCallback } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +// Export hook +export default function useDimensions(dependencies = []) { const [node, setNode] = useState(null); - - const ref = useCallback(node => { - setNode(node); + const ref = useCallback(newNode => { + setNode(newNode); + }, []); + + // Keep track of measurements + const [dimensions, setDimensions] = useState({ + x: 0, + y: 0, + left: 0, + top: 0, + right: 0, + bottom: 0, + width: 0, + height: 0, + }); + + // Define measure function + const measure = useCallback(innerNode => { + const rect = innerNode.getBoundingClientRect(); + setDimensions({ + x: rect.left, + y: rect.top, + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + width: rect.width, + height: rect.height, + }); }, []); - // @ts-ignore - ref.current = node; useLayoutEffect(() => { - if (node) { - const measure = () => window.requestAnimationFrame(() => setDimensions(getDimensionObject(node))); - measure(); - - if (liveMeasure) { - window.addEventListener('resize', measure); - window.addEventListener('scroll', measure); - - return () => { - window.removeEventListener('resize', measure); - window.removeEventListener('scroll', measure); - }; - } + if (!node) { + return; } - }, [node]); + + // Set initial measurements + measure(node); + + // Observe resizing of element + const resizeObserver = new ResizeObserver(() => { + measure(node); + }); + + resizeObserver.observe(node); + + // Cleanup + return () => { + resizeObserver.disconnect(); + }; + }, [node, measure, ...dependencies]); return [ref, dimensions, node]; } - -export default useDimensions; diff --git a/web/yarn.lock b/web/yarn.lock index 92a16f56..e5e95db2 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -8981,6 +8981,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"