Skip to content

Commit

Permalink
Merge pull request #28 from concord-consortium/186024366-flex-fixed-h…
Browse files Browse the repository at this point in the history
…eader

186024366 flex fixed header
  • Loading branch information
eireland authored Oct 24, 2023
2 parents 20b3a5b + ceb99f8 commit 8e80b74
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 98 deletions.
2 changes: 0 additions & 2 deletions src/components/app.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
.selectView {
padding: 10px;
height: 100vh;

.buttons {
display: flex;
gap: 5px;
Expand Down
63 changes: 58 additions & 5 deletions src/components/draggable-table-tags.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import React, { useMemo, useRef } from "react";
import { useDraggableTableContext, Side } from "../hooks/useDraggableTable";
import { useTableTopScrollTopContext } from "../hooks/useTableScrollTop";

import AddIcon from "../assets/plus-level-1.svg";

Expand All @@ -10,6 +11,8 @@ const highlightColor = "#FBF719";
const border = `5px solid ${highlightColor}`;
const borderLeft = border;
const borderRight = border;
const kCellHeight = 16;
const kMinNumHeaders = 3;

const getStyle = (id: string, dragOverId?: string, dragSide?: Side) => {
return id === dragOverId ? (dragSide === "left" ? {borderLeft} : {borderRight}) : {};
Expand Down Expand Up @@ -81,16 +84,66 @@ interface DraggagleTableDataProps {
attrTitle: string;
style?: React.CSSProperties;
isParent?: boolean;
resizeCounter?: number;
parentLevel?: number;
}

export const DraggagleTableData: React.FC<DraggagleTableDataProps>
= ({collectionId, attrTitle, children, isParent}) => {
= ({collectionId, attrTitle, children, isParent, resizeCounter, parentLevel=0}) => {
const {dragOverId, dragSide} = useDraggableTableContext();
const {style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide);

const {tableScrollTop, scrollY} = useTableTopScrollTopContext();

const cellRef = useRef<HTMLTableCellElement | null>(null);

const cellTextTop = useMemo (() =>{
if (!cellRef.current || !isParent) {
return 0;
} else {
const {top, bottom, height} = cellRef.current.getBoundingClientRect();
const stickyHeaders = tableScrollTop === 0;
const stickyHeaderHeight = (kMinNumHeaders + parentLevel) * kCellHeight;
const visibleTop = stickyHeaders ? Math.max(top, stickyHeaderHeight) : top;
const visibleBottom = Math.min(window.innerHeight, bottom);
const availableHeight = Math.abs(visibleBottom - visibleTop);

let newTop;

if (top >= visibleTop && bottom <= visibleBottom) { // the whole cell is visible
return 0;
} else if (top < visibleTop && bottom < window.innerHeight) {
// we can see the bottom border of the cell but not the top
const hiddenHeightOfCell = height - availableHeight;
newTop = Math.max(0, (hiddenHeightOfCell - kCellHeight + (availableHeight / 2)));
} else if (top >= visibleTop && bottom > visibleBottom) {
// we can see the top border of the cell but not the bottom
newTop = Math.max(0, ((availableHeight) / 2));
} else {
// we are in the middle of a cell - we can see neither the top nor the bottom border
const hiddenTopPartOfCell = Math.max(0, visibleTop - top);
newTop = Math.max(0, (hiddenTopPartOfCell - kCellHeight + (availableHeight) / 2));
}
return newTop;
}
// resizeCounter is a hack to force rerender of text positioning when window is resized
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableScrollTop, isParent, scrollY, parentLevel, resizeCounter]);


const textStyle: React.CSSProperties = {top: cellTextTop};
if (cellTextTop === 0) {
textStyle.alignContent = "center";
textStyle.bottom = 0;
}
return (
<td style={style} className={`draggable-table-data ${isParent ? "parent-data" : ""}`}>
{children}
<td style={style} className={`draggable-table-data ${isParent ? css.parentData : ""}`} ref={cellRef}>
{isParent
? <>
<span style={{opacity: 0}}>{children}</span>
<div style={textStyle} className={css.cellTextValue}>{children}</div>
</>
: children
}
</td>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
select, input {
margin-left: 5px;
}
}
}
1 change: 0 additions & 1 deletion src/components/nested-table.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.nestedTableWrapper {
height: 100vh;
width: 95vw;
margin: 0 auto;
}
7 changes: 5 additions & 2 deletions src/components/nested-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const NestedTable = (props: IProps) => {
}, [interactiveState.dataSetName, updateInteractiveState]);

useEffect(() => {
const style = interactiveState.padding ? {padding: "7px"} : {padding: "0px"};
const style = interactiveState.padding ? {padding: "3px"} : {padding: "0px"};
setPaddingStyle(style);
}, [interactiveState.padding]);

Expand Down Expand Up @@ -108,7 +108,8 @@ export const NestedTable = (props: IProps) => {
);
};

const mapCellsFromValues = (collectionId: number, rowKey: string, values: IValues, isParent?: boolean) => {
const mapCellsFromValues = (collectionId: number, rowKey: string, values: IValues, isParent?: boolean,
resizeCounter?: number, parentLevel?: number) => {
return Object.keys(values).map((key, index) => {
const val = values[key];
if (typeof val === "string" || typeof val === "number") {
Expand All @@ -118,6 +119,8 @@ export const NestedTable = (props: IProps) => {
attrTitle={key}
key={`${rowKey}-${val}-${index}}`}
isParent={isParent}
resizeCounter={resizeCounter}
parentLevel={parentLevel}
>
{val}
</DraggagleTableData>
Expand Down
103 changes: 38 additions & 65 deletions src/components/portrait-view.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { ICollection, IProcessedCaseObj, ITableProps } from "../types";
import { DraggableTableContainer, DroppableTableData, DroppableTableHeader } from "./draggable-table-tags";
import { TableScrollTopContext, useTableScrollTop } from "../hooks/useTableScrollTop";

import css from "./tables.scss";

export type PortraitViewRowProps =
{collectionId: number, caseObj: IProcessedCaseObj, index?: null|number, isParent: boolean} & ITableProps;
export type PortraitViewRowProps = {collectionId: number, caseObj: IProcessedCaseObj, index?: null|number,
isParent: boolean, resizeCounter: number, parentLevel?: number}
& ITableProps;

export const PortraitViewRow = (props: PortraitViewRowProps) => {
const {paddingStyle, mapCellsFromValues, mapHeadersFromValues, showHeaders,
getClassName, collectionId, caseObj, index, isParent} = props;
getClassName, collectionId, caseObj, index, isParent, resizeCounter, parentLevel} = props;

const {children, values} = caseObj;

Expand All @@ -23,24 +25,25 @@ export const PortraitViewRow = (props: PortraitViewRowProps) => {
{index === 0 &&
<tr className={`${css[getClassName(caseObj)]}`}>
{mapHeadersFromValues(collectionId, `first-row-${index}`, values)}
{showHeaders && (
<DroppableTableHeader collectionId={collectionId}>{children[0].collection.name}</DroppableTableHeader>
)}
{showHeaders ? (
<DroppableTableHeader collectionId={collectionId}>{children[0].collection.name}</DroppableTableHeader>
) : <th />}
</tr>
}
<tr className={`${css[getClassName(caseObj)]} parent-row`}>
{mapCellsFromValues(collectionId, `parent-row-${index}`, values, isParent)}
{mapCellsFromValues(collectionId, `parent-row-${index}`, values, isParent, resizeCounter, parentLevel)}
<DroppableTableData collectionId={collectionId} style={paddingStyle}>
<DraggableTableContainer collectionId={collectionId}>
<table style={paddingStyle} className={`${css.subTable} ${css[getClassName(children[0])]}`}>
<tbody>
<tbody className={`table-body ${css[getClassName(children[0])]}`}>
{caseObj.children.map((child, i) => {
const nextProps: PortraitViewRowProps = {
...props,
collectionId: child.collection.id,
caseObj: child,
index: i,
isParent
isParent,
parentLevel: parentLevel !== undefined && parentLevel !== null ? parentLevel + 1 : undefined,
};
if (i === 0 && !child.children.length) {
return (
Expand All @@ -67,68 +70,34 @@ export const PortraitViewRow = (props: PortraitViewRowProps) => {

export const PortraitView = (props: ITableProps) => {
const {collectionClasses, selectedDataSet, collections, getValueLength} = props;
const tableRef = useRef<HTMLTableElement | null>(null);
const tableScrollTop = useTableScrollTop(tableRef);
const [resizeCounter, setResizeCounter] = useState(0);

const thresh = useMemo(() => {
const t: number[] = [];
for (let i = 0; i <= 100; i++) {
t.push(i / 100);
t.push(i/100);
}
return t;
},[]);
}, []);

const [scrolling, setScrolling] = useState(false);
const [scrollTop, setScrollTop] = useState(0);

useEffect(() => {
const onScroll = (e: any) => {
setScrollTop(e.target.documentElement.scrollTop);
setScrolling(e.target.documentElement.scrollTop > scrollTop);
const handleIntersection = (entries: IntersectionObserverEntry[], o: any) => {
setResizeCounter((prevState) => prevState + 1);
};
window.addEventListener("scroll", onScroll);

return () => window.removeEventListener("scroll", onScroll);
}, [scrollTop]);

useEffect(() => {
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
const target = entry.target;
const entryRect = target.getBoundingClientRect();
const entryHeight = entryRect.height;
const intersectionRect = entry.intersectionRect;
const visibleHeight = intersectionRect.height;
const intersectionHeightRatio = visibleHeight/entryHeight;
const cells = Array.from(target.querySelectorAll<HTMLElement>(".parent-data"));

if (cells) {
cells.forEach(cell => {
cell.style.position = "relative";
if (entry.isIntersecting && intersectionHeightRatio < 0.85) {
if (intersectionRect.top === 0) { //we're in the bottom part of the visible rect
cell.style.verticalAlign = "top";
cell.style.top = `${(visibleHeight/2) - entryRect.top - 16}px`;
} else { //we're in the top part of the visible rect
cell.style.verticalAlign = "top";
cell.style.top = `${visibleHeight/2}px`;
}
} else {
cell.style.top = "0";
cell.style.verticalAlign = "middle";
}
});
}
});
};
const observer = new IntersectionObserver(handleIntersection, { threshold: thresh });
document.querySelectorAll(".parent-row").forEach((cell) => {
observer.observe(cell);
const observer = new IntersectionObserver(handleIntersection, {threshold: thresh});
document.querySelectorAll(`.parent-row`).forEach((row) => {
observer.observe(row);
});
return () => {
// Clean up the observer when the component unmounts
document.querySelectorAll(".parent-row").forEach((cell) => {
observer.unobserve(cell);
document.querySelectorAll(`.parent-row`).forEach((row) => {
observer.unobserve(row);
});
};
}, [scrollTop, scrolling, thresh]);

}, [thresh]);

const renderTable = () => {
const parentColl = collections.filter((coll: ICollection) => !coll.parent)[0];
Expand All @@ -138,10 +107,10 @@ export const PortraitView = (props: ITableProps) => {

return (
<DraggableTableContainer>
<table className={`${css.mainTable} ${css.portraitTable} ${css[className]}`}>
<tbody>
<table className={`${css.mainTable} ${css.portraitTable} ${css[className]}`} ref={tableRef}>
<tbody className={`table-body ${css[className]}`}>
<tr className={css.mainHeader}>
<th colSpan={valueCount}>{selectedDataSet.name}</th>
<th className={css.datasetNameHeader} colSpan={valueCount}>{selectedDataSet.name}</th>
</tr>
<tr className={css[className]}>
<th colSpan={valueCount}>{parentColl.name}</th>
Expand All @@ -154,6 +123,8 @@ export const PortraitView = (props: ITableProps) => {
caseObj={caseObj}
index={index}
isParent={true}
resizeCounter={resizeCounter}
parentLevel={0}
/>
))}
</tbody>
Expand All @@ -163,8 +134,10 @@ export const PortraitView = (props: ITableProps) => {
};

return (
<div className={css.portaitTableContainer}>
{collections.length && collectionClasses.length && renderTable()}
</div>
<TableScrollTopContext.Provider value={tableScrollTop}>
<div className={css.portraitTableContainer}>
{collections.length && collectionClasses.length && renderTable()}
</div>
</TableScrollTopContext.Provider>
);
};
Loading

0 comments on commit 8e80b74

Please sign in to comment.