Skip to content

Commit

Permalink
Sheet table has good horizontal and vertical scrolling.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikh2000 authored and erikh2000 committed Dec 29, 2024
1 parent 3844250 commit a98dbe7
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 61 deletions.
15 changes: 14 additions & 1 deletion src/components/sheetTable/DOMTextMeasurer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ class DOMTextMeasurer {
private _className:string;
private _measureElement:HTMLElement|null;
private _isInitialized:boolean;
private _wordWidths:Record<string,number> = {};
private _wordWidths:Record<string,number>;
private _lineHeight:number;

// Measurement will be based on styles inherited from both parentElement and className.
constructor(parentElement:HTMLElement, className:string) {
this._parentElement = parentElement;
this._className = className;
this._measureElement = null;
this._isInitialized = false;
this._wordWidths = {};
this._lineHeight = 0;
}

private _initializeAsNeeded() {
Expand All @@ -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;
}

Expand All @@ -64,6 +72,11 @@ class DOMTextMeasurer {
this._measureElement.textContent = '';
return totalWidth;
}

public getLineHeight():number {
this._initializeAsNeeded();
return this._lineHeight;
}
}

export default DOMTextMeasurer;
11 changes: 6 additions & 5 deletions src/components/sheetTable/SheetFooter.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 1 addition & 5 deletions src/components/sheetTable/SheetFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ type Props = {
}

function SheetFooter({text}:Props) {
return (
<div className={styles.footer}>
{text}
</div>
);
return (<div className={styles.footer}>{text}</div>);
}

export default SheetFooter;
15 changes: 5 additions & 10 deletions src/components/sheetTable/SheetHeader.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 8 additions & 5 deletions src/components/sheetTable/SheetHeader.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>) {
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 (
<span key={columnI} style={style}>
{column.name}
Expand All @@ -18,10 +21,10 @@ function SheetHeader({columns, columnWidths}:Props) {

const className = styles.sheetHeader;
return (
<div className={className}>
<div className={className} style={{width:"200vw"}} ref={ref}>
{cells}
</div>
);
}

export default SheetHeader;
export default forwardRef(SheetHeader);
44 changes: 33 additions & 11 deletions src/components/sheetTable/SheetRow.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
27 changes: 19 additions & 8 deletions src/components/sheetTable/SheetRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<span key={cellI} style={style}>
<span key={cellI} className={className} style={style}>
{cell}
</span>
);
});

const isEven = rowNo % 2 === 0;
const className = isEven ? styles.sheetRowEven : styles.sheetRowOdd;
return (
<div className={className}>
{cells}
</div>
<div className={styles.sheetRow}>{cells}</div>
);
}

Expand Down
26 changes: 24 additions & 2 deletions src/components/sheetTable/SheetTable.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
58 changes: 48 additions & 10 deletions src/components/sheetTable/SheetTable.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLDivElement>;

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];
Expand All @@ -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<HTMLDivElement>(null);
const headerInnerElement = useRef<HTMLDivElement>(null);
const rowsScrollElement = useRef<HTMLDivElement>(null);
const [columnWidths, setColumnWidths] = useState<number[]>([]);

useEffect(() => {
Expand All @@ -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) =>
<SheetRow key={rowI} row={row} rowNo={rowI+1} columnWidths={columnWidths} />
<SheetRow key={rowI} row={row} rowNo={rowI+1} rowCount={rowCount} columnWidths={columnWidths} />
);

const rowScrollContainerStyle = useMemo(() => _getRowScrollContainerStyle(displayRowCount, rowsScrollElement.current), [displayRowCount]);
const displayFooterText = _getFooterText(footerText, sheet);
return (
<div className={styles.scrollContainer}>
<div className={styles.sheetTable} ref={sheetTableElement}>
<SheetHeader columns={sheet.columns} columnWidths={columnWidths} />
{rowsContent}
<SheetFooter text={footerText ?? ''}/>
<div className={styles.sheetTable} ref={sheetTableElement}>
<div className={styles.headerScrollContainer}>
<SheetHeader columns={sheet.columns} columnWidths={columnWidths} ref={headerInnerElement}/>
</div>
<div className={styles.rowsScrollContainer} style={rowScrollContainerStyle} ref={rowsScrollElement} onScroll={() => _syncScrollableElements(headerInnerElement, rowsScrollElement)}>
<div className={styles.rowsInnerContainer}>
{rowsContent}
</div>
</div><SheetFooter text={displayFooterText}/>
</div>
);
}
Expand Down
6 changes: 2 additions & 4 deletions src/homeScreen/SheetPane.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -19,8 +18,7 @@ function _noSheetLoadedContent() {

function _sheetContent(sheet:HoneSheet|null, _selectedRowNo:number, _onRowSelect:(rowNo:number)=>void) {
if (!sheet) return _noSheetLoadedContent();
// return <SheetView sheet={sheet} selectedRowNo={selectedRowNo} onRowSelect={onRowSelect}/>;
return <SheetTable sheet={sheet} />;
return <SheetTable sheet={sheet} footerText={GeneratedFooterText.ROW_COUNT} displayRowCount={10} />;
}

function SheetPane({sheet, className, onImportSheet, selectedRowNo, onRowSelect, onExportSheet}:Props) {
Expand Down

0 comments on commit a98dbe7

Please sign in to comment.