diff --git a/src/components/sheetTable/DOMTextMeasurer.ts b/src/components/sheetTable/DOMTextMeasurer.ts index 64f3a03..19d6251 100644 --- a/src/components/sheetTable/DOMTextMeasurer.ts +++ b/src/components/sheetTable/DOMTextMeasurer.ts @@ -1,9 +1,26 @@ +const MEASURABLE_SPACE = ' \u200B'; // Adding Unicode zero-width character to end will cause the browser to measure the preceding space. + +type WordWidths = Record; +type WordWidthsCache = Record; + +// Shared per-layout word widths across all instances of DOMTextMeasurer. +const theWordWidthsCache:WordWidthsCache = {}; + +// Use classname as key to cache word widths specific to that layout. +// Still possible to have collisions if two different layouts use the same classname. If you have that problem, you can create a separate className for each layout. +function _getOrCreateWordWidths(className:string):WordWidths { + if (theWordWidthsCache[className]) return theWordWidthsCache[className]; + return theWordWidthsCache[className] = {}; +} + class DOMTextMeasurer { private _parentElement:HTMLElement; private _className:string; private _measureElement:HTMLElement|null; private _isInitialized:boolean; + private _wordWidths:Record = {}; + // Measurement will be based on styles inherited from both parentElement and className. constructor(parentElement:HTMLElement, className:string) { this._parentElement = parentElement; this._className = className; @@ -20,6 +37,7 @@ class DOMTextMeasurer { this._measureElement.style.visibility = 'hidden'; this._measureElement.style.pointerEvents = 'none'; this._parentElement.appendChild(this._measureElement); + this._wordWidths = _getOrCreateWordWidths(this._className); this._isInitialized = true; } @@ -27,10 +45,24 @@ class DOMTextMeasurer { public measureTextWidth(text:string):number { this._initializeAsNeeded(); if (!this._measureElement) throw 'Unexpected'; - this._measureElement.textContent = text; - const width = this._measureElement.offsetWidth; - this._measureElement.textContent = ''; // Save memory. - return width; + + let totalWidth = 0; + const words = text.split(' '); + for(let i = 0; i < words.length; i++) { + const word = words[i] + (i < words.length - 1 ? MEASURABLE_SPACE : ''); + if (this._wordWidths[word]) { + totalWidth += this._wordWidths[word]; + continue; + } + + this._measureElement.textContent = word; + const width = this._measureElement.offsetWidth; + this._wordWidths[word] = width; + totalWidth += width; + } + + this._measureElement.textContent = ''; + return totalWidth; } } diff --git a/src/components/sheetTable/SheetHeader.tsx b/src/components/sheetTable/SheetHeader.tsx index 5849561..f1a0cd2 100644 --- a/src/components/sheetTable/SheetHeader.tsx +++ b/src/components/sheetTable/SheetHeader.tsx @@ -1,26 +1,16 @@ import HoneColumn from '@/sheets/types/HoneColumn'; import styles from './SheetHeader.module.css'; -import { useRef, useEffect } from "react"; type Props = { columns:HoneColumn[], columnWidths:number[], - onMeasureColumns:(columnWidths:number[])=>void } -function SheetHeader({columns, columnWidths, onMeasureColumns}:Props) { - const spansRef = useRef<(HTMLSpanElement|null)[]>([]); - - useEffect(() => { - if (columnWidths.length === columns.length) return; // No new columns to measure. - const measuredWidths = spansRef.current.map(span => span ? span.offsetWidth : 0); - onMeasureColumns(measuredWidths); - }, [columns, columnWidths, onMeasureColumns]); - +function SheetHeader({columns, columnWidths}:Props) { const cells = columns.map((column, columnI) => { const style = columnWidths[columnI] ? {width:columnWidths[columnI]} : {}; return ( - spansRef.current[columnI] = el} style={style}> + {column.name} ); diff --git a/src/components/sheetTable/SheetRow.module.css b/src/components/sheetTable/SheetRow.module.css index d02cf72..25c755c 100644 --- a/src/components/sheetTable/SheetRow.module.css +++ b/src/components/sheetTable/SheetRow.module.css @@ -7,7 +7,7 @@ border-color: lightgrey; } -.sheetRow span { +.sheetCell, .sheetRow span { display: inline-block; white-space: nowrap; margin-left: .5rem; diff --git a/src/components/sheetTable/SheetRow.tsx b/src/components/sheetTable/SheetRow.tsx index 1a4ae33..253246c 100644 --- a/src/components/sheetTable/SheetRow.tsx +++ b/src/components/sheetTable/SheetRow.tsx @@ -1,28 +1,17 @@ import Row from "@/sheets/types/Row" import styles from './SheetRow.module.css'; -import { useRef, useEffect } from "react"; type Props = { row:Row, rowNo:number, - columnWidths:number[], - onMeasureColumns:(columnWidths:number[])=>void + columnWidths:number[] } -function SheetRow({row, rowNo, columnWidths, onMeasureColumns}:Props) { - const spansRef = useRef<(HTMLSpanElement|null)[]>([]); - - useEffect(() => { - if (columnWidths.length === row.length) return; // No new columns to measure. - console.log('Measuring columns...'); - const measuredWidths = spansRef.current.map(span => span ? span.offsetWidth : 0); - onMeasureColumns(measuredWidths); - }, [row, columnWidths, onMeasureColumns]); - +function SheetRow({row, rowNo, columnWidths}:Props) { const cells = row.map((cell, cellI) => { const style = columnWidths[cellI] ? {width:columnWidths[cellI]} : {}; return ( - spansRef.current[cellI] = el} style={style}> + {cell} ); diff --git a/src/components/sheetTable/SheetTable.tsx b/src/components/sheetTable/SheetTable.tsx index 6d309ec..2f55298 100644 --- a/src/components/sheetTable/SheetTable.tsx +++ b/src/components/sheetTable/SheetTable.tsx @@ -1,80 +1,49 @@ -import { useState, useRef, useEffect, MutableRefObject, useLayoutEffect } from 'react'; +import { useState, useRef, useEffect } from 'react'; import styles from './SheetTable.module.css'; +import rowStyles from './SheetRow.module.css'; import SheetRow from "./SheetRow"; import SheetHeader from './SheetHeader'; import HoneSheet from '@/sheets/types/HoneSheet'; import SheetFooter from './SheetFooter'; +import DOMTextMeasurer from './DOMTextMeasurer'; type Props = { sheet:HoneSheet, footerText?:string } -type MeasurementState = { - isMeasuring:boolean, - widths:number[], - totalMeasureCount:number, - measuredRowNos:Set -} - -function _duplicateMeasurementState(from:MeasurementState):MeasurementState { - return { - isMeasuring:from.isMeasuring, - widths:[...from.widths], - totalMeasureCount:from.totalMeasureCount, - measuredRowNos:new Set(from.measuredRowNos) - }; -} - -function _updateColumnWidths(rowNo:number, widths:number[], measurementStateRef:MutableRefObject, setColumnWidths:Function) { - if (measurementStateRef.current.measuredRowNos.has(rowNo) || !measurementStateRef.current.isMeasuring) return; - const nextState = _duplicateMeasurementState(measurementStateRef.current); - nextState.measuredRowNos.add(rowNo); - for(let i=0; i measurer.measureTextWidth(column.name)); + for(let rowI = 0; rowI < sheet.rows.length; rowI++) { + const row = sheet.rows[rowI]; + for(let cellI = 0; cellI < row.length; cellI++) { + const cell = '' + row[cellI]; + widths[cellI] = Math.max(widths[cellI], measurer.measureTextWidth(cell)); } } - if (nextState.totalMeasureCount - nextState.measuredRowNos.size === 0) { - console.log('Finished measuring columns. Widths=', nextState.widths); - nextState.isMeasuring = false; - setColumnWidths(nextState.widths); - } - measurementStateRef.current = nextState; -} - -function _createEmptyMeasurementState(rowCount:number):MeasurementState { - return { - isMeasuring:true, - widths:[], - totalMeasureCount:rowCount+1, //+1 for header. - measuredRowNos:new Set() - }; + return widths; } function SheetTable({sheet, footerText}:Props) { - const measurementStateRef = useRef(_createEmptyMeasurementState(sheet.rows.length)); + const sheetTableElement = useRef(null); const [columnWidths, setColumnWidths] = useState([]); useEffect(() => { - if (measurementStateRef.current.isMeasuring) return; - console.log('Measuring columns...'); - measurementStateRef.current = _createEmptyMeasurementState(sheet.rows.length); - setColumnWidths([]); + if (!sheetTableElement.current) return; + const nextColumnWidths:number[] = _measureColumnWidths(sheetTableElement.current, sheet); + setColumnWidths(nextColumnWidths); }, [sheet, sheet.rows]); - console.log('SheetTable rendering with columnWidths=', columnWidths); - const rowsContent = sheet.rows.map((row, rowI) => - _updateColumnWidths(rowI+1, measuredWidths, measurementStateRef, setColumnWidths)}/> + const rowsContent = columnWidths.length === 0 ? null : sheet.rows.map((row, rowI) => + ); return (
-
- _updateColumnWidths(0, measuredWidths, measurementStateRef, setColumnWidths)}/> +
+ {rowsContent}