UNPKG

@gpa-gemstone/react-table

Version:
511 lines (510 loc) 28.4 kB
"use strict"; 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))); }