@gpa-gemstone/react-table
Version:
Table for GPA web applications
511 lines (510 loc) • 28.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Table = Table;
// ******************************************************************************************************
// Table.tsx - Gbtc
//
// Copyright © 2023, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 11/18/2023 - C. Lackner
// Generated original version of source code.
// 05/31/2024 - C. Lackner
// Refactored to fix sizing issues.
// 12/04/2024 - G. Santos
// Refactored to fix performance issues.
//
// ******************************************************************************************************
const React = require("react");
const _ = require("lodash");
const Column_1 = require("./Column");
const helper_functions_1 = require("@gpa-gemstone/helper-functions");
const FilterableColumn_1 = require("./FilterableColumn");
const defaultTableStyle = {
padding: 0,
flex: 1,
tableLayout: 'fixed',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
marginBottom: 0,
width: '100%'
};
const defaultHeadStyle = {
fontSize: 'auto',
tableLayout: 'fixed',
display: 'table',
width: '100%'
};
const defaultBodyStyle = {
flex: 1,
display: 'block',
overflow: 'auto'
};
const defaultRowStyle = {
display: 'table',
tableLayout: 'fixed',
width: '100%'
};
const defaultDataHeadStyle = {
display: 'inline-block',
position: 'relative',
borderTop: 'none',
width: 'auto'
};
const defaultDataCellStyle = {
overflowX: 'hidden',
display: 'inline-block',
width: 'auto'
};
const IsColumnProps = (props) => ((props === null || props === void 0 ? void 0 : props['Key']) != null);
const IsColumnAdjustable = (props) => {
const propValue = props === null || props === void 0 ? void 0 : props['Adjustable'];
if (propValue === false || propValue === true)
return propValue;
return false;
};
const lastColumnWidth = 17;
function Table(props) {
const bodyRef = React.useRef(null);
const colWidthsRef = React.useRef(new Map());
const oldWidthRef = React.useRef(0);
const [currentTableWidth, setCurrentTableWidth] = React.useState(0);
const [scrolled, setScrolled] = React.useState(false);
const [trigger, setTrigger] = React.useState(0);
// Style consts
const tableStyle = React.useMemo(() => (Object.assign(Object.assign({}, defaultTableStyle), props.TableStyle)), [props.TableStyle]);
const headStyle = React.useMemo(() => (Object.assign(Object.assign({}, defaultHeadStyle), props.TheadStyle)), [props.TheadStyle]);
const bodyStyle = React.useMemo(() => (Object.assign(Object.assign({}, defaultBodyStyle), props.TbodyStyle)), [props.TbodyStyle]);
const rowStyle = React.useMemo(() => (Object.assign(Object.assign({}, defaultRowStyle), props.RowStyle)), [props.RowStyle]);
// Send warning if styles are overridden
React.useEffect(() => {
if (props.TableStyle !== undefined)
console.warn('TableStyle properties may be overridden if needed. consider using the defaults');
if (props.TheadStyle !== undefined)
console.warn('TheadStyle properties may be overridden if needed. consider using the defaults');
if (props.TbodyStyle !== undefined)
console.warn('TBodyStyle properties may be overridden if needed. consider using the defaults');
if (props.RowStyle !== undefined)
console.warn('RowStyle properties may be overridden if needed. consider using the defaults');
}, []);
// Measure widths and hide columns
React.useLayoutEffect(() => {
if (currentTableWidth <= 0)
return;
// Helper functions for the calculations
const getWidthfromProps = (p, type) => {
var _a, _b;
// This priotizes rowstyling for width over header, since it was decided that they need to be the same
if (((_a = p === null || p === void 0 ? void 0 : p.RowStyle) === null || _a === void 0 ? void 0 : _a[type]) !== undefined)
return p.RowStyle[type];
if (((_b = p === null || p === void 0 ? void 0 : p.HeaderStyle) === null || _b === void 0 ? void 0 : _b[type]) !== undefined)
return p.HeaderStyle[type];
return undefined;
};
// Construct base map
const newMap = new Map();
React.Children.forEach(props.children, (element) => {
if (React.isValidElement(element) && IsColumnProps(element.props)) {
if (newMap.get(element.props.Key) != null)
console.error("Multiple of the same key detected in table, this will cause issues.");
newMap.set(element.props.Key, { minWidth: 10, maxWidth: undefined, width: 100 });
}
});
// If width is the same and keys are identical, we can skip the operation
if (currentTableWidth === oldWidthRef.current &&
(newMap.size === colWidthsRef.current.size &&
![...newMap.keys()].some(key => !colWidthsRef.current.has(key))))
return;
// Find and set widths for map
['minWidth', 'width', 'maxWidth'].forEach(type => {
const widthsContainer = document.createElement("div");
widthsContainer.style.height = '0px';
widthsContainer.style.width = `${currentTableWidth}px`;
// Append columns as divs for measurement
const autoKeys = [];
const measureKeys = [];
React.Children.forEach(props.children, (element) => {
if (React.isValidElement(element) && IsColumnProps(element.props)) {
let widthValue = getWidthfromProps(element.props, type);
if (type === 'width' && widthValue == null)
widthValue = 'auto';
if (widthValue != null) {
if (widthValue === 'auto')
autoKeys.push(element.props.Key);
else {
const widthElement = document.createElement("div");
widthElement.id = element.props.Key + "_measurement";
widthElement.style.height = '0px';
if ((widthValue === null || widthValue === void 0 ? void 0 : widthValue.length) != null)
widthElement.style.width = widthValue;
else
widthElement.style.width = `${widthValue}px`;
widthsContainer.appendChild(widthElement);
measureKeys.push(element.props.Key);
}
}
}
});
document.body.appendChild(widthsContainer);
// Handle Measurements
let autoSpace = currentTableWidth;
measureKeys.forEach(key => {
const element = document.getElementById(key + "_measurement");
const elementWidth = element === null || element === void 0 ? void 0 : element.getBoundingClientRect().width;
if (elementWidth != null) {
const widthObj = newMap.get(key);
if (widthObj != null) {
autoSpace -= elementWidth;
widthObj[type] = elementWidth;
}
else
console.error("Could not find width object for Key: " + key);
}
else
console.error("Could not find measurement div with Key: " + key);
});
document.body.removeChild(widthsContainer);
// Handle Autos (width type only)
if (type === 'width' && autoKeys.length > 0) {
const spacePerElement = autoSpace / autoKeys.length;
autoKeys.forEach(key => {
const widthObj = newMap.get(key);
if (widthObj != null)
widthObj[type] = spacePerElement;
else
console.error("Could not find width object for Key: " + key);
});
}
});
let remainingSpace = currentTableWidth;
[...newMap.keys()].forEach(key => {
const widthObj = newMap.get(key);
if (widthObj != null) {
if (widthObj.minWidth <= remainingSpace) {
// This follows behavior consistent with MDN documentation on how these width types should behave
if (widthObj.minWidth > widthObj.width)
widthObj.width = widthObj.minWidth;
if (widthObj.maxWidth != null && widthObj.minWidth > widthObj.maxWidth)
widthObj.maxWidth = widthObj.minWidth;
if (widthObj.maxWidth != null && widthObj.width > widthObj.maxWidth)
widthObj.width = widthObj.maxWidth;
// Constrain Width to remainingSpace
if (widthObj.width > remainingSpace)
widthObj.width = remainingSpace;
remainingSpace -= widthObj.width;
}
else {
widthObj.minWidth = 0;
widthObj.width = 0;
widthObj.maxWidth = 0;
}
}
else
console.error("Could not find width object for Key: " + key);
});
colWidthsRef.current = newMap;
oldWidthRef.current = currentTableWidth;
setTrigger(c => c + 1);
}, [props.children, currentTableWidth]);
const setTableWidth = React.useCallback(_.debounce(() => {
var _a, _b, _c, _d;
if (bodyRef.current == null)
return;
// Note: certain body classes may break this check if they set overflow to scroll
let newScroll = false;
const dims = (_a = bodyRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); //use getBoundClientRect() to get un rounded values
if (((_b = props.TbodyStyle) === null || _b === void 0 ? void 0 : _b.overflowY) === 'scroll' || ((_c = props.TbodyStyle) === null || _c === void 0 ? void 0 : _c.overflow) === 'scroll')
newScroll = true;
else
newScroll = (dims === null || dims === void 0 ? void 0 : dims.height) < bodyRef.current.scrollHeight;
setScrolled(newScroll);
// Pick whichever is larger so we dont double subtract
const scrollbar = newScroll ? (0, helper_functions_1.GetScrollbarWidth)() : 0;
const last = props.LastColumn !== undefined ? lastColumnWidth : 0;
const spacer = Math.max(scrollbar, last);
setCurrentTableWidth(((_d = dims.width) !== null && _d !== void 0 ? _d : 17) - spacer);
}, 100), []);
React.useEffect(() => {
let resizeObserver;
const intervalHandle = setInterval(() => {
if ((bodyRef === null || bodyRef === void 0 ? void 0 : bodyRef.current) == null)
return;
resizeObserver = new ResizeObserver(() => {
setTableWidth();
});
resizeObserver.observe(bodyRef.current);
clearInterval(intervalHandle);
}, 10);
return () => {
clearInterval(intervalHandle);
if (resizeObserver != null && resizeObserver.disconnect != null)
resizeObserver.disconnect();
};
}, []);
const handleSort = React.useCallback((data, event) => {
if (data.colKey !== null)
props.OnSort(data, event);
}, [props.OnSort]);
return (React.createElement("table", { className: props.TableClass !== undefined ? props.TableClass : 'table table-hover', style: tableStyle },
React.createElement(Header, { Class: props.TheadClass, Style: headStyle, SortKey: props.SortKey, Ascending: props.Ascending, LastColumn: props.LastColumn, OnSort: handleSort, ColWidths: colWidthsRef, SetFilters: props.SetFilters, Filters: props.Filters, Trigger: trigger, TriggerRerender: () => setTrigger(c => c + 1) }, props.children),
React.createElement(Rows, { DragStart: props.OnDragStart, Data: props.Data, RowStyle: rowStyle, BodyStyle: bodyStyle, BodyClass: props.TbodyClass, OnClick: props.OnClick, Selected: props.Selected, KeySelector: props.KeySelector, BodyRef: bodyRef, BodyScrolled: scrolled, ColWidths: colWidthsRef, Trigger: trigger }, props.children),
props.LastRow !== undefined ? (React.createElement("tfoot", { style: props.TfootStyle, className: props.TfootClass },
React.createElement("tr", { style: props.RowStyle !== undefined ? Object.assign({}, props.RowStyle) : {} }, props.LastRow))) : null));
}
function Rows(props) {
const bodyStyle = React.useMemo(() => (Object.assign(Object.assign({}, props.BodyStyle), { display: "block" })), [props.BodyStyle]);
const onClick = React.useCallback((e, item, index) => {
if (props.OnClick !== undefined)
props.OnClick({
colKey: undefined,
colField: undefined,
row: item,
data: null,
index: index,
}, e);
}, [props.OnClick]);
return (React.createElement("tbody", { style: bodyStyle, className: props.BodyClass, ref: props.BodyRef }, props.Data.map((d, i) => {
const style = props.RowStyle !== undefined ? Object.assign({}, props.RowStyle) : {};
if (style.cursor === undefined && (props.OnClick !== undefined || props.DragStart !== undefined))
style.cursor = 'pointer';
if (props.Selected !== undefined && props.Selected(d, i))
style.backgroundColor = 'var(--warning)';
const key = props.KeySelector(d, i);
return (React.createElement("tr", { key: key, style: style, onClick: (e) => onClick(e, d, i) }, React.Children.map(props.children, (element) => {
var _a, _b, _c, _d, _e, _f;
if (!React.isValidElement(element))
return null;
if (!IsColumnProps(element.props))
return null;
const colWidth = props.ColWidths.current.get(element.props.Key);
if (colWidth == null || colWidth.width === 0)
return null;
let cursor = undefined;
if (((_b = (_a = element.props) === null || _a === void 0 ? void 0 : _a.RowStyle) === null || _b === void 0 ? void 0 : _b.cursor) != null)
cursor = element.props.RowStyle.cursor;
else if ((props === null || props === void 0 ? void 0 : props.OnClick) != null) {
cursor = 'pointer';
}
else if ((props === null || props === void 0 ? void 0 : props.DragStart) != null)
cursor = 'grab';
const style = Object.assign(Object.assign(Object.assign({}, defaultDataCellStyle), ((_c = element.props) === null || _c === void 0 ? void 0 : _c.RowStyle)), { width: colWidth.width, cursor: cursor });
return (React.createElement(Column_1.ColumnDataWrapper, { key: element.key, onClick: (e) => {
var _a, _b;
if (props.OnClick == null)
return;
return props.OnClick({
colKey: element.props.Key,
colField: (_a = element.props) === null || _a === void 0 ? void 0 : _a.Field,
row: d,
data: d[(_b = element.props) === null || _b === void 0 ? void 0 : _b.Field],
index: i,
}, e);
}, dragStart: props.DragStart == null ? undefined :
(e) => {
var _a, _b;
if (props.DragStart == null)
return;
return props.DragStart({
colKey: element.props.Key,
colField: (_a = element.props) === null || _a === void 0 ? void 0 : _a.Field,
row: d,
data: d[(_b = element.props) === null || _b === void 0 ? void 0 : _b.Field],
index: i,
}, e);
}, style: style }, ((_d = element.props) === null || _d === void 0 ? void 0 : _d.Content) != null
? element.props.Content({
item: d,
key: element.props.Key,
field: (_e = element.props) === null || _e === void 0 ? void 0 : _e.Field,
style: style,
index: i,
})
: ((_f = element.props) === null || _f === void 0 ? void 0 : _f.Field) != null
? d[element.props.Field]
: null));
})));
})));
}
function Header(props) {
var _a;
const headStyle = React.useMemo(() => (Object.assign(Object.assign({}, defaultHeadStyle), props.Style)), [props.Style]);
// Consts for adjustable columns
const [mouseDown, setMouseDown] = React.useState(0);
const [currentKeys, setCurrentKeys] = React.useState(undefined);
const [deltaW, setDeltaW] = React.useState(0);
const [tentativeLimits, setTentativeLimits] = React.useState({ min: -Infinity, max: Infinity });
// Consts for filterable columns
const [filters, setFilters] = React.useState(((_a = props.Filters) !== null && _a !== void 0 ? _a : []));
const [guid] = React.useState((0, helper_functions_1.CreateGuid)());
const getLeftKey = React.useCallback((key, colWidthsRef) => {
// Filtering down to shown adjustables only
var _a, _b;
const mapCallback = (element) => {
var _a;
if (!React.isValidElement(element))
return null;
const keyWidth = (_a = colWidthsRef.current.get(key)) === null || _a === void 0 ? void 0 : _a.width;
if (keyWidth == null || keyWidth <= 0)
return null;
if (IsColumnProps(element.props) && IsColumnAdjustable(element.props))
return element.props.Key;
return null;
};
const children = (_a = props.children) !== null && _a !== void 0 ? _a : [];
const keys = ((_b = React.Children.map(children, mapCallback)) !== null && _b !== void 0 ? _b : []).filter((item) => item !== null);
const index = keys.indexOf(key);
if (index <= 0)
return undefined;
return keys[index - 1];
}, [props.children]);
const calculateDeltaLimits = React.useCallback((mapKeys, colWidthsRef) => {
if (mapKeys === undefined)
return ({ min: -Infinity, max: Infinity });
const widthObjLeft = colWidthsRef.current.get(mapKeys[0]);
const widthObjRight = colWidthsRef.current.get(mapKeys[1]);
if (widthObjLeft == null || widthObjRight == null)
return ({ min: -Infinity, max: Infinity });
const limitByShrinkLeft = widthObjLeft.width - widthObjLeft.minWidth;
const limitByGrowthLeft = widthObjLeft.maxWidth == null ? Number.MAX_SAFE_INTEGER : (widthObjLeft.maxWidth - widthObjLeft.width);
const limitByShrinkRight = widthObjRight.width - widthObjRight.minWidth;
const limitByGrowthRight = widthObjRight.maxWidth == null ? Number.MAX_SAFE_INTEGER : (widthObjRight.maxWidth - widthObjRight.width);
// Recall that a left movement is a negative deltaW
const minDeltaW = -(limitByShrinkLeft < limitByGrowthRight ? limitByShrinkLeft : limitByGrowthRight);
const maxDeltaW = limitByShrinkRight < limitByGrowthLeft ? limitByShrinkRight : limitByGrowthLeft;
return ({ min: minDeltaW, max: maxDeltaW });
}, []);
const getDeltaSign = React.useCallback((index) => {
// Recall that a left movement is a negative deltaW
if (index === 0)
return 1;
else if (index === 1)
return -1;
else
return 0;
}, []);
const finishAdjustment = React.useCallback((adjustment, adjustKeys, colWidthsRef) => {
const deltaLimits = calculateDeltaLimits(adjustKeys, colWidthsRef);
let delta;
if (adjustment > deltaLimits.max)
delta = deltaLimits.max;
else if (adjustment < deltaLimits.min)
delta = deltaLimits.min;
else
delta = adjustment;
if (Math.abs(delta) > 5) {
const leftWidthObj = colWidthsRef.current.get(adjustKeys[0]);
const rightWidthObj = colWidthsRef.current.get(adjustKeys[1]);
if (leftWidthObj == null || rightWidthObj == null) {
console.error(`Unable to finalize adjustment on keys ${adjustKeys[0]}, ${adjustKeys[1]}`);
}
else {
leftWidthObj.width += (getDeltaSign(0) * delta);
rightWidthObj.width += (getDeltaSign(1) * delta);
}
}
setMouseDown(0);
setTentativeLimits({ min: -Infinity, max: Infinity });
setCurrentKeys(undefined);
setDeltaW(0);
}, [calculateDeltaLimits, getDeltaSign]);
const onMove = React.useCallback((e) => {
if (currentKeys === undefined)
return;
const w = e.screenX - mouseDown;
setDeltaW(w);
}, [mouseDown, currentKeys]);
const updateFilters = React.useCallback((flts, fld) => {
setFilters((fls) => {
const newFilters = fls.filter(item => item.FieldName !== fld).concat(flts);
if (props.SetFilters == null)
console.error("Attempted to set filters from column when no set filter arguement to table was provided. Data has not been filtered!");
else
props.SetFilters(newFilters);
return newFilters;
});
}, [props.SetFilters]);
React.useEffect(() => { var _a; return setFilters((_a = props.Filters) !== null && _a !== void 0 ? _a : []); }, [props.Filters]);
return (React.createElement("thead", { className: props.Class, style: headStyle, onMouseMove: (e) => {
onMove(e.nativeEvent);
e.stopPropagation();
}, onMouseUp: (e) => {
e.stopPropagation();
if (currentKeys == null)
return;
finishAdjustment(deltaW, currentKeys, props.ColWidths);
props.TriggerRerender();
}, onMouseLeave: (e) => {
e.stopPropagation();
if (currentKeys == null)
return;
finishAdjustment(deltaW, currentKeys, props.ColWidths);
props.TriggerRerender();
} },
React.createElement("tr", { style: { width: '100%', display: 'table' } },
React.Children.map(props.children, (element) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
if (!React.isValidElement(element))
return null;
if (!IsColumnProps(element.props))
return null;
const colWidth = props.ColWidths.current.get(element.props.Key);
if (colWidth == null || colWidth.width === 0)
return null;
// Handling temporary width changes due to being in mid-adjustments
let currentWidth = colWidth.width;
const keyIndex = (_a = currentKeys === null || currentKeys === void 0 ? void 0 : currentKeys.indexOf(element.props.Key)) !== null && _a !== void 0 ? _a : -1;
if (keyIndex > -1) {
let delta;
if (deltaW > tentativeLimits.max)
delta = tentativeLimits.max;
else if (deltaW < tentativeLimits.min)
delta = tentativeLimits.min;
else
delta = deltaW;
currentWidth += (getDeltaSign(keyIndex) * delta);
}
let cursor = undefined;
if (((_c = (_b = element.props) === null || _b === void 0 ? void 0 : _b.HeaderStyle) === null || _c === void 0 ? void 0 : _c.cursor) != null)
cursor = element.props.HeaderStyle.cursor;
else if (((_e = (_d = element.props) === null || _d === void 0 ? void 0 : _d.AllowSort) !== null && _e !== void 0 ? _e : true))
cursor = 'pointer';
const style = Object.assign(Object.assign(Object.assign({}, defaultDataHeadStyle), (_f = element.props) === null || _f === void 0 ? void 0 : _f.HeaderStyle), { width: currentWidth, cursor: cursor });
let startAdjustment;
if (IsColumnAdjustable(element.props))
startAdjustment = (e) => {
const leftKey = getLeftKey(element.props.Key, props.ColWidths);
if (leftKey != null) {
const newCurrentKeys = [leftKey, element.props.Key];
setCurrentKeys(newCurrentKeys);
setMouseDown(e.screenX);
setTentativeLimits(calculateDeltaLimits(newCurrentKeys, props.ColWidths));
setDeltaW(0);
}
};
return (React.createElement(Column_1.ColumnHeaderWrapper, { onSort: (e) => {
var _a;
return props.OnSort({ colKey: element.props.Key, colField: (_a = element.props) === null || _a === void 0 ? void 0 : _a.Field, ascending: props.Ascending }, e);
}, sorted: props.SortKey === element.props.Key && ((_h = (_g = element.props) === null || _g === void 0 ? void 0 : _g.AllowSort) !== null && _h !== void 0 ? _h : true), asc: props.Ascending, colKey: element.props.Key, key: element.props.Key, allowSort: (_j = element.props) === null || _j === void 0 ? void 0 : _j.AllowSort, startAdjustment: startAdjustment, style: style },
' ',
(element.type === FilterableColumn_1.default) ?
React.createElement(FilterableColumn_1.FilterableColumnHeader, { Label: (_k = element.props) === null || _k === void 0 ? void 0 : _k.children, Filter: filters.filter(f => { var _a, _b; return f.FieldName === ((_b = (_a = element.props) === null || _a === void 0 ? void 0 : _a.Field) === null || _b === void 0 ? void 0 : _b.toString()); }), SetFilter: (f) => { var _a; return updateFilters(f, (_a = element.props) === null || _a === void 0 ? void 0 : _a.Field); }, Field: (_l = element.props) === null || _l === void 0 ? void 0 : _l.Field, Type: (_m = element.props) === null || _m === void 0 ? void 0 : _m.Type, Options: (_o = element.props) === null || _o === void 0 ? void 0 : _o.Enum, ExpandedLabel: (_p = element.props) === null || _p === void 0 ? void 0 : _p.ExpandedLabel, Guid: guid, Unit: (_q = element.props) === null || _q === void 0 ? void 0 : _q.Unit }) :
((_r = element.props.children) !== null && _r !== void 0 ? _r : element.props.Key),
' '));
}),
props.LastColumn !== undefined ?
React.createElement("th", { style: { width: lastColumnWidth, padding: 0, maxWidth: lastColumnWidth } }, props.LastColumn)
: null)));
}