diff --git a/src/components/sheetTable/DOMTextMeasurer.ts b/src/components/sheetTable/DOMTextMeasurer.ts index 19d6251..d88ef0d 100644 --- a/src/components/sheetTable/DOMTextMeasurer.ts +++ b/src/components/sheetTable/DOMTextMeasurer.ts @@ -18,7 +18,8 @@ class DOMTextMeasurer { private _className:string; private _measureElement:HTMLElement|null; private _isInitialized:boolean; - private _wordWidths:Record = {}; + private _wordWidths:Record; + private _lineHeight:number; // Measurement will be based on styles inherited from both parentElement and className. constructor(parentElement:HTMLElement, className:string) { @@ -26,6 +27,8 @@ class DOMTextMeasurer { this._className = className; this._measureElement = null; this._isInitialized = false; + this._wordWidths = {}; + this._lineHeight = 0; } private _initializeAsNeeded() { @@ -39,6 +42,11 @@ class DOMTextMeasurer { this._parentElement.appendChild(this._measureElement); this._wordWidths = _getOrCreateWordWidths(this._className); + this._measureElement.textContent = 'M'; + this._lineHeight = this._measureElement.offsetHeight; + console.log('DOMTextMeasurer lineHeight', this._lineHeight); + this._measureElement.textContent = ''; + this._isInitialized = true; } @@ -64,6 +72,11 @@ class DOMTextMeasurer { this._measureElement.textContent = ''; return totalWidth; } + + public getLineHeight():number { + this._initializeAsNeeded(); + return this._lineHeight; + } } export default DOMTextMeasurer; \ No newline at end of file diff --git a/src/components/sheetTable/SheetFooter.module.css b/src/components/sheetTable/SheetFooter.module.css index 2100dad..f8b4f4a 100644 --- a/src/components/sheetTable/SheetFooter.module.css +++ b/src/components/sheetTable/SheetFooter.module.css @@ -1,9 +1,10 @@ .footer { width:auto; background-color: lightgrey; - border-style: solid; - border-width: 0 1vh 1vh 1vh; - border-color: lightgrey; - border-bottom-left-radius: 1vh; - border-bottom-right-radius: 1vh; + margin-top: -.4vh; + font-size: 1.2vh; + color: darkgrey; + margin-top: .3vh; + margin-right: .3vh; + text-align: right; } \ No newline at end of file diff --git a/src/components/sheetTable/SheetFooter.tsx b/src/components/sheetTable/SheetFooter.tsx index 4d90267..c169f84 100644 --- a/src/components/sheetTable/SheetFooter.tsx +++ b/src/components/sheetTable/SheetFooter.tsx @@ -5,11 +5,7 @@ type Props = { } function SheetFooter({text}:Props) { - return ( -
- {text} -
- ); + return (
{text}
); } export default SheetFooter; \ No newline at end of file diff --git a/src/components/sheetTable/SheetHeader.module.css b/src/components/sheetTable/SheetHeader.module.css index 055ad82..e38e9b8 100644 --- a/src/components/sheetTable/SheetHeader.module.css +++ b/src/components/sheetTable/SheetHeader.module.css @@ -1,18 +1,13 @@ .sheetHeader { - overflow-y: hidden; - overflow-x: hidden; - display: flex; + display: flex; background-color: lightgrey; - border-style: solid; - border-width: 1vh 1vh 0 1vh; - border-color: lightgrey; - border-top-left-radius: 1vh; - border-top-right-radius: 1vh; } .sheetHeader span { display: inline-block; white-space: nowrap; - margin-left: .5rem; - margin-right: .5rem; + margin-top: -.3vh; + margin-bottom: .2vh; + padding-left: .5rem; + padding-right: .5rem; } \ No newline at end of file diff --git a/src/components/sheetTable/SheetHeader.tsx b/src/components/sheetTable/SheetHeader.tsx index f1a0cd2..a5c67f9 100644 --- a/src/components/sheetTable/SheetHeader.tsx +++ b/src/components/sheetTable/SheetHeader.tsx @@ -1,14 +1,17 @@ +import { forwardRef } from 'react'; + import HoneColumn from '@/sheets/types/HoneColumn'; import styles from './SheetHeader.module.css'; type Props = { columns:HoneColumn[], - columnWidths:number[], + columnWidths:number[] } -function SheetHeader({columns, columnWidths}:Props) { +function SheetHeader(props:Props, ref:React.Ref) { + const {columns, columnWidths} = props; const cells = columns.map((column, columnI) => { - const style = columnWidths[columnI] ? {width:columnWidths[columnI]} : {}; + const style = columnWidths[columnI] ? {minWidth:columnWidths[columnI]} : {}; return ( {column.name} @@ -18,10 +21,10 @@ function SheetHeader({columns, columnWidths}:Props) { const className = styles.sheetHeader; return ( -
+
{cells}
); } -export default SheetHeader; \ No newline at end of file +export default forwardRef(SheetHeader); \ No newline at end of file diff --git a/src/components/sheetTable/SheetRow.module.css b/src/components/sheetTable/SheetRow.module.css index 25c755c..52a2cfc 100644 --- a/src/components/sheetTable/SheetRow.module.css +++ b/src/components/sheetTable/SheetRow.module.css @@ -1,25 +1,47 @@ .sheetRow { border-bottom: 1px solid #e0e0e0; - overflow: hidden; display: flex; - border-style: solid; - border-width: 0vh 1vh 0 1vh; - border-color: lightgrey; } -.sheetCell, .sheetRow span { +.measureCellText { display: inline-block; white-space: nowrap; - margin-left: .5rem; - margin-right: .5rem; + padding-left: .5rem; + padding-right: .5rem; } -.sheetRowEven { - composes: sheetRow; +.sheetCell { + display: inline-block; + white-space: nowrap; + padding-left: .5rem; + padding-right: .5rem; background-color: #f2f2f2; } -.sheetRowOdd { - composes: sheetRow; +.oddRow { background-color: #ffffff; +} + +.sheetCellTopLeft { + composes: sheetCell; + border-style: hidden; + border-top-left-radius: 1vh; +} + +.sheetCellTopRight { + composes: sheetCell; + border-style: hidden; + border-top-right-radius: 1vh; +} + +.sheetCellBottomLeft { + composes: sheetCell; + border-style: hidden; + border-bottom-left-radius: 1vh; +} + +.sheetCellBottomRight { + composes: sheetCell; + border-style: hidden; + border-bottom-right-radius: 1vh; } \ No newline at end of file diff --git a/src/components/sheetTable/SheetRow.tsx b/src/components/sheetTable/SheetRow.tsx index 253246c..3e64a80 100644 --- a/src/components/sheetTable/SheetRow.tsx +++ b/src/components/sheetTable/SheetRow.tsx @@ -4,25 +4,36 @@ import styles from './SheetRow.module.css'; type Props = { row:Row, rowNo:number, + rowCount:number, columnWidths:number[] } -function SheetRow({row, rowNo, columnWidths}:Props) { +function _classNameForCell(colNo:number, rowNo:number, colCount:number, rowCount:number):string { + const colorStyle = (rowNo % 2 === 0) ? '' : ` ${styles.oddRow}`; + if (rowNo === 1) { + if (colNo === 1) return `${styles.sheetCellTopLeft}${colorStyle}`; + if (colNo === colCount) return `${styles.sheetCellTopRight}${colorStyle}`; + } else if (rowNo === rowCount) { + if (colNo === 1) return `${styles.sheetCellBottomLeft}${colorStyle}`; + if (colNo === colCount) return `${styles.sheetCellBottomRight}${colorStyle}`; + } + return `${styles.sheetCell}${colorStyle}`; +} + +function SheetRow({row, rowNo, rowCount, columnWidths}:Props) { + const colCount = row.length; const cells = row.map((cell, cellI) => { - const style = columnWidths[cellI] ? {width:columnWidths[cellI]} : {}; + const style = columnWidths[cellI] ? {minWidth:columnWidths[cellI]} : {}; + const className = _classNameForCell(cellI+1, rowNo, colCount, rowCount); return ( - + {cell} ); }); - const isEven = rowNo % 2 === 0; - const className = isEven ? styles.sheetRowEven : styles.sheetRowOdd; return ( -
- {cells} -
+
{cells}
); } diff --git a/src/components/sheetTable/SheetTable.module.css b/src/components/sheetTable/SheetTable.module.css index 15fb511..337acdd 100644 --- a/src/components/sheetTable/SheetTable.module.css +++ b/src/components/sheetTable/SheetTable.module.css @@ -1,10 +1,32 @@ -.scrollContainer { +@value colors: "@/components/commonPalette.module.css"; +@value button from colors; + +.headerScrollContainer { width: 90vw; /* I honestly don't understand why 90vw works and 100vw doesn't. Seems fragile. */ - overflow-x: auto; + overflow-x: clip; +} + +.rowsScrollContainer { + width: 90vw; overflow-y: auto; + overflow-x: auto; + display: inline-block; +} + +.rowsScrollContainer:hover { + scrollbar-color: button white; +} + +.rowsInnerContainer { + display: inline-block; } .sheetTable { width: max-content; display: inline-block; + background-color: lightgrey; + border-color: lightgrey; + border-style: solid; + border-width: .5vh .5vh 1vh .5vh; + border-radius: 1vh; } \ No newline at end of file diff --git a/src/components/sheetTable/SheetTable.tsx b/src/components/sheetTable/SheetTable.tsx index 2f55298..cc6eea2 100644 --- a/src/components/sheetTable/SheetTable.tsx +++ b/src/components/sheetTable/SheetTable.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useMemo, RefObject, CSSProperties } from 'react'; import styles from './SheetTable.module.css'; import rowStyles from './SheetRow.module.css'; @@ -7,14 +7,22 @@ import SheetHeader from './SheetHeader'; import HoneSheet from '@/sheets/types/HoneSheet'; import SheetFooter from './SheetFooter'; import DOMTextMeasurer from './DOMTextMeasurer'; +import { plural } from '@/common/englishGrammarUtil'; + +export enum GeneratedFooterText { + ROW_COUNT = 0, +} type Props = { sheet:HoneSheet, - footerText?:string + displayRowCount?:number, + footerText?:string|GeneratedFooterText } +type DivRef = RefObject; + function _measureColumnWidths(sheetTableElement:HTMLDivElement, sheet:HoneSheet):number[] { - const measurer = new DOMTextMeasurer(sheetTableElement, rowStyles.sheetRowCell); + const measurer = new DOMTextMeasurer(sheetTableElement, rowStyles.measureCellText); const widths = sheet.columns.map(column => measurer.measureTextWidth(column.name)); for(let rowI = 0; rowI < sheet.rows.length; rowI++) { const row = sheet.rows[rowI]; @@ -26,8 +34,32 @@ function _measureColumnWidths(sheetTableElement:HTMLDivElement, sheet:HoneSheet) return widths; } -function SheetTable({sheet, footerText}:Props) { +function _getFooterText(footerText:string|GeneratedFooterText|undefined, sheet:HoneSheet):string { + if (footerText === undefined) return ''; + if (footerText === GeneratedFooterText.ROW_COUNT) return `${sheet.rows.length} ${plural('row', sheet.rows.length)}`; + return footerText; +} + +function _syncScrollableElements(headerInnerElement:DivRef, rowsScrollElement:DivRef) { + console.log('syncing scrollable elements'); + if (!headerInnerElement.current || !rowsScrollElement.current) return; + console.log('scrolling'); + const scrollLeft = rowsScrollElement.current.scrollLeft; + headerInnerElement.current.style.transform = `translateX(-${scrollLeft}px)`; +} + +function _getRowScrollContainerStyle(displayRowCount:number|undefined, parentElement:HTMLDivElement|null):CSSProperties { + if (!displayRowCount || !parentElement) return {}; + const measurer = new DOMTextMeasurer(parentElement, rowStyles.measureCellText); + const lineHeight = measurer.getLineHeight(); + console.log('lineHeight', lineHeight); + return {maxHeight:displayRowCount * lineHeight + 'px'}; +} + +function SheetTable({sheet, footerText, displayRowCount}:Props) { const sheetTableElement = useRef(null); + const headerInnerElement = useRef(null); + const rowsScrollElement = useRef(null); const [columnWidths, setColumnWidths] = useState([]); useEffect(() => { @@ -36,17 +68,23 @@ function SheetTable({sheet, footerText}:Props) { setColumnWidths(nextColumnWidths); }, [sheet, sheet.rows]); + const rowCount = sheet.rows.length; const rowsContent = columnWidths.length === 0 ? null : sheet.rows.map((row, rowI) => - + ); + const rowScrollContainerStyle = useMemo(() => _getRowScrollContainerStyle(displayRowCount, rowsScrollElement.current), [displayRowCount]); + const displayFooterText = _getFooterText(footerText, sheet); return ( -
-
- - {rowsContent} - +
+
+
+
_syncScrollableElements(headerInnerElement, rowsScrollElement)}> +
+ {rowsContent} +
+
); } diff --git a/src/homeScreen/SheetPane.tsx b/src/homeScreen/SheetPane.tsx index 0af577f..e36c2c9 100644 --- a/src/homeScreen/SheetPane.tsx +++ b/src/homeScreen/SheetPane.tsx @@ -1,8 +1,7 @@ -// import SheetView from "./SheetView"; import Pane, { ButtonDefinition } from "@/components/pane/Pane"; import { getComment } from "./interactions/comment"; import HoneSheet from "@/sheets/types/HoneSheet"; -import SheetTable from "@/components/sheetTable/SheetTable"; +import SheetTable, { GeneratedFooterText } from "@/components/sheetTable/SheetTable"; type Props = { sheet: HoneSheet|null, @@ -19,8 +18,7 @@ function _noSheetLoadedContent() { function _sheetContent(sheet:HoneSheet|null, _selectedRowNo:number, _onRowSelect:(rowNo:number)=>void) { if (!sheet) return _noSheetLoadedContent(); - // return ; - return ; + return ; } function SheetPane({sheet, className, onImportSheet, selectedRowNo, onRowSelect, onExportSheet}:Props) {