UNPKG

window-table

Version:

Windowing Table for React based on React Window

774 lines (667 loc) 25.8 kB
import { createElement, PureComponent, useReducer as useReducer$1, useMemo as useMemo$2, memo, forwardRef, useRef, useState as useState$1, useEffect, createContext, useContext, useCallback, Fragment } from 'react'; import { VariableSizeList, areEqual } from 'react-window'; import debounce from 'lodash.debounce'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /** * Detect Element Resize. * https://github.com/sdecima/javascript-detect-element-resize * Sebastian Decima * * Forked from version 0.5.3; includes the following modifications: * 1) Guard against unsafe 'window' and 'document' references (to support SSR). * 2) Defer initialization code via a top-level function wrapper (to support SSR). * 3) Avoid unnecessary reflows by not measuring size for scroll events bubbling from children. * 4) Add nonce for style element. **/ function createDetectElementResize(nonce) { // Check `document` and `window` in case of server-side rendering var _window = typeof window !== 'undefined' ? window : global; var requestFrame = function requestFrame(fn) { return (_window.requestAnimationFrame || function (fn) { return _window.setTimeout(fn, 20); })(fn); }; var cancelFrame = function cancelFrame(id) { return (_window.cancelAnimationFrame || _window.clearTimeout)(id); }; var resetTriggers = function resetTriggers(element) { var triggers = element.__resizeTriggers__, expand = triggers.firstElementChild, contract = triggers.lastElementChild, expandChild = expand.firstElementChild; contract.scrollLeft = contract.scrollWidth; contract.scrollTop = contract.scrollHeight; expandChild.style.width = expand.offsetWidth + 1 + "px"; expandChild.style.height = expand.offsetHeight + 1 + "px"; expand.scrollLeft = expand.scrollWidth; expand.scrollTop = expand.scrollHeight; }; var checkTriggers = function checkTriggers(element) { return element.offsetWidth !== element.__resizeLast__.width || element.offsetHeight !== element.__resizeLast__.height; }; var scrollListener = function scrollListener(e) { var _this = this; // Don't measure (which forces) reflow for scrolls that happen inside of children! if (e.target.className.indexOf('contract-trigger') < 0 && e.target.className.indexOf('expand-trigger') < 0) { return; } resetTriggers(this); if (this.__resizeRAF__) { cancelFrame(this.__resizeRAF__); } this.__resizeRAF__ = requestFrame(function () { if (checkTriggers(_this)) { _this.__resizeLast__.width = _this.offsetWidth; _this.__resizeLast__.height = _this.offsetHeight; _this.__resizeListeners__.forEach(function (fn) { fn.call(_this, e); }); } }); }; var createStyles = function createStyles(doc) { if (!doc.getElementById('detectElementResize')) { var css = "\n .resize-triggers {\n visibility: hidden;\n }\n .resize-triggers, .resize-triggers > div,\n .contract-trigger:before {\n content: \" \";\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n overflow:\n hidden; z-index: -1;\n }\n .resize-triggers > div {\n background: #eee;\n overflow: auto;\n }\n .contract-trigger:before {\n width: 200%;\n height: 200%;\n }\n ", head = doc.head || doc.getElementsByTagName('head')[0], style = doc.createElement('style'); style.id = 'detectElementResize'; style.type = 'text/css'; if (nonce != null) { style.setAttribute('nonce', nonce); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(doc.createTextNode(css)); } head.appendChild(style); } }; var addResizeListener = function addResizeListener(element, fn) { if (!element.__resizeTriggers__) { var doc = element.ownerDocument; var elementStyle = _window.getComputedStyle(element); if (elementStyle && elementStyle.position === 'static') { element.style.position = 'relative'; } createStyles(doc); element.__resizeLast__ = {}; element.__resizeListeners__ = []; (element.__resizeTriggers__ = doc.createElement('div')).className = 'resize-triggers'; element.__resizeTriggers__.innerHTML = '<div class="expand-trigger"><div></div></div>' + '<div class="contract-trigger"></div>'; element.appendChild(element.__resizeTriggers__); resetTriggers(element); element.addEventListener('scroll', scrollListener, true); } element.__resizeListeners__.push(fn); }; var removeResizeListener = function removeResizeListener(element, fn) { element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); if (!element.__resizeListeners__.length) { element.removeEventListener('scroll', scrollListener, true); if (element.__resizeTriggers__.__animationListener__) { element.__resizeTriggers__.__animationListener__ = null; } try { element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__); } catch (e) {// Preact compat; see developit/preact-compat/issues/228 } } }; return { addResizeListener: addResizeListener, removeResizeListener: removeResizeListener }; } var AutoSizer = /*#__PURE__*/function (_React$PureComponent) { _inheritsLoose(AutoSizer, _React$PureComponent); function AutoSizer() { var _this; _this = _React$PureComponent.apply(this, arguments) || this; _this.state = { height: 0, width: 0 }; _this._onResize = function () { var onResize = _this.props.onResize; if (_this._parentNode) { // Guard against AutoSizer component being removed from the DOM immediately after being added. // This can result in invalid style values which can result in NaN values if we don't handle them. // See issue #150 for more context. var height = _this._parentNode.offsetHeight || 0; var width = _this._parentNode.offsetWidth || 0; var style = window.getComputedStyle(_this._parentNode) || {}; var paddingLeft = parseInt(style.paddingLeft, 10) || 0; var paddingRight = parseInt(style.paddingRight, 10) || 0; var paddingTop = parseInt(style.paddingTop, 10) || 0; var paddingBottom = parseInt(style.paddingBottom, 10) || 0; var newHeight = height - paddingTop - paddingBottom; var newWidth = width - paddingLeft - paddingRight; if (_this.state.height !== newHeight || _this.state.width !== newWidth) { _this.setState({ height: height - paddingTop - paddingBottom, width: width - paddingLeft - paddingRight }); onResize({ height: height, width: width }); } } }; _this._setRef = function (autoSizer) { _this._autoSizer = autoSizer; }; return _this; } var _proto = AutoSizer.prototype; _proto.componentDidMount = function componentDidMount() { if (this._autoSizer && this._autoSizer.parentNode && this._autoSizer.parentNode.ownerDocument && this._autoSizer.parentNode.ownerDocument.defaultView && this._autoSizer.parentNode instanceof this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement) { // Delay access of parentNode until mount. // This handles edge-cases where the component has already been unmounted before its ref has been set, // As well as libraries like react-lite which have a slightly different lifecycle. this._parentNode = this._autoSizer.parentNode; // Defer requiring resize handler in order to support server-side rendering. // See issue #41 this._detectElementResize = createDetectElementResize(''); this._detectElementResize.addResizeListener(this._parentNode, this._onResize); this._onResize(); } }; _proto.componentWillUnmount = function componentWillUnmount() { if (this._detectElementResize && this._parentNode) { this._detectElementResize.removeResizeListener(this._parentNode, this._onResize); } }; _proto.render = function render() { // Outer div should not force width/height since that may prevent containers from shrinking. // Inner component should overflow and use calculated width/height. // See issue #68 for more information. var outerStyle = { overflow: 'visible' }; outerStyle.height = 0; outerStyle.width = 0; var Component = this.props.innerElementType || 'div'; return createElement(Component, { ref: this._setRef, style: outerStyle }); }; return AutoSizer; }(PureComponent); AutoSizer.defaultProps = { onResize: function onResize() {} }; var useMemo = useMemo$2, useReducer = useReducer$1; // Define the initial state of dimensions // Also to be used as a state which will not trigger a re-render on changes // So that we can change state from the useReducer, only when required var cache = { header: [0, 0], row: [0, 0], table: [0, 0] }; var reducer = function reducer(state, _ref) { var entity = _ref.entity, dimensions = _ref.dimensions; if (entity) { var _extends2; // Keep updates in cache cache = _extends({}, cache, (_extends2 = {}, _extends2[entity] = dimensions, _extends2)); if (JSON.stringify(state[entity]) !== JSON.stringify(cache[entity])) { return cache; } } return state; }; var Measurer = function Measurer(_ref2) { var measure = _ref2.measure, entity = _ref2.entity, debounceWait = _ref2.debounceWait, _ref2$innerElementTyp = _ref2.innerElementType, innerElementType = _ref2$innerElementTyp === void 0 ? 'div' : _ref2$innerElementTyp; var debounced = useMemo(function () { return debounce(measure, debounceWait, { leading: true }); }, [measure, debounceWait]); var dispatch = debounceWait > 0 ? debounced : measure; return createElement(AutoSizer, { innerElementType: innerElementType, onResize: function onResize(_ref3) { var height = _ref3.height, width = _ref3.width; dispatch({ dimensions: [height, width], entity: entity }); } }); }; var useTableMeasurer = function useTableMeasurer() { return useReducer(reducer, cache); }; var objectProps = ['style', 'sampleRow']; var otherProps = ['columns', 'data', 'rowHeight', 'height', 'width', 'className', 'rowClassName', 'classNamePrefix']; function areTablePropsEqual(prev, next) { var areObjectPropsEqual = objectProps.every(function (propName) { return JSON.stringify(prev[propName]) === JSON.stringify(next[propName]); }); if (!areObjectPropsEqual) { return false; } return otherProps.every(function (propName) { return prev[propName] === next[propName]; }); } var TableContext = /*#__PURE__*/createContext({ columns: [], data: [], Cell: 'div', Row: 'div', Table: 'div', Body: 'div', classNamePrefix: '', tableClassName: '', rowClassName: '', rowWidthOffset: 0, setSize: function setSize() { return {}; }, variableSizeRows: false }); var RowCells = function RowCells(_ref) { var columns = _ref.columns, classNamePrefix = _ref.classNamePrefix, datum = _ref.datum, Cell = _ref.Cell, _ref$index = _ref.index, index = _ref$index === void 0 ? 0 : _ref$index, setSize = _ref.setSize; return createElement(Fragment, null, columns.map(function (column, i) { var key = column.key, width = column.width, _column$Component = column.Component, Component = _column$Component === void 0 ? 'div' : _column$Component; // Using i as the key, because it doesn't matter much, // as we are only looping through columns in one row only var extraProps = typeof Component === 'string' ? {} : { row: datum, column: column, index: index, setSize: setSize || function () {} }; return createElement(Cell, { key: i, style: { width: width + "px", flexGrow: width, display: 'inline-block', boxSizing: 'border-box' }, className: classNamePrefix + "table-cell", row: datum, column: column, index: index }, createElement(Component, Object.assign({}, extraProps), datum[key])); })); }; var RowRenderer = function RowRenderer(_ref2) { var index = _ref2.index, style = _ref2.style; var _useContext = useContext(TableContext), columns = _useContext.columns, data = _useContext.data, Cell = _useContext.Cell, classNamePrefix = _useContext.classNamePrefix, Row = _useContext.Row, rowClassName = _useContext.rowClassName, setSize = _useContext.setSize, variableSizeRows = _useContext.variableSizeRows; var rowClassNameStr = useMemo$2(function () { return typeof rowClassName === 'function' ? rowClassName(index) : rowClassName; }, [index, rowClassName]); var setSizeRef = useRef(setSize); var setSizeCb = useCallback(function () { if (!variableSizeRows && index !== 0) { return; } var el = document.querySelector("#window-table-row-ref-" + index); if (!el) { return; } if (el.scrollHeight > el.getBoundingClientRect().height) { setSizeRef.current(index, (el == null ? void 0 : el.scrollHeight) + 1 || 0); } }, [index, variableSizeRows]); useEffect(function () { setTimeout(function () { setSizeCb(); }, 0); }, [index, style.height, setSizeCb]); return createElement(Row, { id: "window-table-row-ref-" + index, style: _extends({}, style, { display: 'flex', overflow: 'auto' }), className: "" + classNamePrefix + rowClassNameStr + " " + classNamePrefix + rowClassNameStr + "-" + index, index: index, row: data[index] }, createElement(RowCells, { datum: data[index], Cell: Cell, classNamePrefix: classNamePrefix, columns: columns, index: index, setSize: setSizeCb })); }; var MemoRowRenderer = /*#__PURE__*/memo(RowRenderer, areEqual); var HeaderRowRenderer = function HeaderRowRenderer(_ref3) { var Header = _ref3.Header, HeaderRow = _ref3.HeaderRow, DefaultHeaderCell = _ref3.HeaderCell, children = _ref3.children; var _useContext2 = useContext(TableContext), columns = _useContext2.columns, classNamePrefix = _useContext2.classNamePrefix, rowWidthOffset = _useContext2.rowWidthOffset; var rowRef = useRef(null); var color = rowRef && rowRef.current && rowRef.current.firstChild && getComputedStyle(rowRef.current.firstChild).backgroundColor; return createElement(Header, { id: "window-table-header-ref", className: classNamePrefix + "table-header", style: { backgroundColor: color } }, createElement(HeaderRow, { style: { display: 'flex', width: "calc(100% - " + rowWidthOffset + "px)" }, className: classNamePrefix + "table-header-row", ref: rowRef }, children, columns.map(function (column) { var key = column.key, width = column.width, title = column.title, _column$HeaderCell = column.HeaderCell, HeaderCell = _column$HeaderCell === void 0 ? DefaultHeaderCell : _column$HeaderCell; return createElement(HeaderCell, { key: "header" + key, style: { width: width + "px", display: 'inline-block', flexGrow: width }, className: classNamePrefix + "table-header-cell", column: column }, title); }))); }; var TableBodyRenderer = function TableBodyRenderer(_ref4) { var children = _ref4.children, props = _objectWithoutPropertiesLoose(_ref4, ["children"]); var _useContext3 = useContext(TableContext), Table = _useContext3.Table, classNamePrefix = _useContext3.classNamePrefix, tableClassName = _useContext3.tableClassName, Body = _useContext3.Body; return createElement(Table, Object.assign({}, props, { className: tableClassName }), createElement(Body, { className: classNamePrefix + "table-body" }, children)); }; var WindowTable = /*#__PURE__*/forwardRef(function (_ref5, ref) { var columns = _ref5.columns, data = _ref5.data, _ref5$rowHeight = _ref5.rowHeight, rowHeight = _ref5$rowHeight === void 0 ? 40 : _ref5$rowHeight, height = _ref5.height, width = _ref5.width, _ref5$overscanCount = _ref5.overscanCount, overscanCount = _ref5$overscanCount === void 0 ? 1 : _ref5$overscanCount, _ref5$disableHeader = _ref5.disableHeader, disableHeader = _ref5$disableHeader === void 0 ? false : _ref5$disableHeader, _ref5$style = _ref5.style, style = _ref5$style === void 0 ? {} : _ref5$style, _ref5$Cell = _ref5.Cell, Cell = _ref5$Cell === void 0 ? 'div' : _ref5$Cell, _ref5$HeaderCell = _ref5.HeaderCell, HeaderCell = _ref5$HeaderCell === void 0 ? 'div' : _ref5$HeaderCell, _ref5$Table = _ref5.Table, Table = _ref5$Table === void 0 ? 'div' : _ref5$Table, _ref5$Header = _ref5.Header, Header = _ref5$Header === void 0 ? 'div' : _ref5$Header, _ref5$HeaderRow = _ref5.HeaderRow, HeaderRow = _ref5$HeaderRow === void 0 ? 'div' : _ref5$HeaderRow, _ref5$Row = _ref5.Row, Row = _ref5$Row === void 0 ? 'div' : _ref5$Row, _ref5$Body = _ref5.Body, Body = _ref5$Body === void 0 ? 'div' : _ref5$Body, _ref5$className = _ref5.className, className = _ref5$className === void 0 ? '' : _ref5$className, _ref5$rowClassName = _ref5.rowClassName, rowClassName = _ref5$rowClassName === void 0 ? 'table-row' : _ref5$rowClassName, _ref5$classNamePrefix = _ref5.classNamePrefix, classNamePrefix = _ref5$classNamePrefix === void 0 ? '' : _ref5$classNamePrefix, _ref5$debounceWait = _ref5.debounceWait, debounceWait = _ref5$debounceWait === void 0 ? 0 : _ref5$debounceWait, _ref5$variableSizeRow = _ref5.variableSizeRows, variableSizeRows = _ref5$variableSizeRow === void 0 ? false : _ref5$variableSizeRow, tableOuterRef = _ref5.tableOuterRef, tableOuterElementType = _ref5.tableOuterElementType, rest = _objectWithoutPropertiesLoose(_ref5, ["columns", "data", "rowHeight", "height", "width", "overscanCount", "disableHeader", "style", "Cell", "HeaderCell", "Table", "Header", "HeaderRow", "Row", "Body", "className", "rowClassName", "classNamePrefix", "debounceWait", "headerCellInnerElementType", "tableCellInnerElementType", "variableSizeRows", "tableOuterRef", "tableOuterElementType"]); var localRef = useRef(); ref = ref || localRef; var _useState = useState$1(0), headerHeight = _useState[0], setHeaderHeight = _useState[1]; var measurerRowRef = useRef(null); var tableClassName = classNamePrefix + "table " + className; var columnWidthsSum = columns.reduce(function (sum, _ref6) { var width = _ref6.width; return sum + width; }, 0); var _useTableMeasurer = useTableMeasurer(), dimensions = _useTableMeasurer[0], measure = _useTableMeasurer[1]; var _dimensions$table = dimensions.table, tableHeight = _dimensions$table[0], tableWidth = _dimensions$table[1]; var bodyHeight = (height || tableHeight) - headerHeight; var effectiveWidth = width || Math.max(columnWidthsSum, tableWidth); var rowWidth = measurerRowRef.current && measurerRowRef.current.clientWidth || tableWidth; var rowWidthOffset = tableWidth - rowWidth; var _useState2 = useState$1({}), sizeMap = _useState2[0], setSizeMap = _useState2[1]; var getItemSize = function getItemSize(index) { if (!variableSizeRows) { return sizeMap[0] || rowHeight; } return sizeMap[index] || rowHeight; }; var tblCtx = { columns: columns, data: data, Cell: Cell, Row: Row, Table: Table, Body: Body, classNamePrefix: classNamePrefix, tableClassName: tableClassName, rowClassName: rowClassName, rowWidthOffset: rowWidthOffset, variableSizeRows: variableSizeRows, setSize: function setSize(index, height) { var _ref7, _ref7$current; if (!height) { return; } // console.log(sizeMap, index, height); setSizeMap(function (map) { var _extends2; return _extends({}, map, (_extends2 = {}, _extends2[index] = Math.max(height, map[index] || 0), _extends2)); }); (_ref7 = ref) == null ? void 0 : (_ref7$current = _ref7.current) == null ? void 0 : _ref7$current.resetAfterIndex == null ? void 0 : _ref7$current.resetAfterIndex(index); } }; var first = sizeMap[0]; useEffect(function () { var _document$querySelect; setHeaderHeight(((_document$querySelect = document.querySelector('#window-table-header-ref')) == null ? void 0 : _document$querySelect.scrollHeight) || 0); }, [first]); return createElement("div", Object.assign({ style: _extends({ height: height ? height + "px" : '100%', width: width ? width + "px" : '100%', overflow: 'auto', maxHeight: '100vh', minHeight: '200px' }, style) }, rest), createElement(TableContext.Provider, { value: tblCtx }, createElement("div", null, !disableHeader && tableWidth > 0 && createElement(Table, { style: { width: effectiveWidth + "px", marginBottom: 0 }, className: tableClassName }, createElement(HeaderRowRenderer, { Header: Header, HeaderRow: HeaderRow, HeaderCell: HeaderCell })), !!data.length && createElement(VariableSizeList, { ref: ref, height: bodyHeight, itemCount: data.length, itemSize: getItemSize, width: effectiveWidth, innerElementType: TableBodyRenderer, overscanCount: overscanCount, outerRef: tableOuterRef, outerElementType: tableOuterElementType }, MemoRowRenderer))), (!height || !width) && /*Measure table dimensions only if explicit height or width are not supplied*/ createElement(Measurer, { measure: measure, entity: "table", debounceWait: debounceWait })); }); var WindowTable$1 = /*#__PURE__*/memo(WindowTable, areTablePropsEqual); var getTHead = function getTHead(headerClassName) { if (headerClassName === void 0) { headerClassName = ''; } var THead = function THead(props) { return createElement("thead", Object.assign({}, props, { className: headerClassName + " " + props.className })); }; return THead; }; function Html5Table(_ref) { var headerClassName = _ref.headerClassName, props = _objectWithoutPropertiesLoose(_ref, ["headerClassName"]); return createElement(WindowTable$1, Object.assign({ Cell: "td", HeaderCell: "th", Header: getTHead(headerClassName), HeaderRow: "tr", Row: "tr", Body: "tbody", Table: "table", headerCellInnerElementType: "th", tableCellInnerElementType: "td" }, props)); } var useState = useState$1, useMemo$1 = useMemo$2; /** * A hook giving a combination of immediate and debounced state * @param initialState * @param wait */ function useDebouncedState(initialState, wait) { if (wait === void 0) { wait = 100; } var _useState = useState(initialState), immediateState = _useState[0], setImmediateState = _useState[1]; var _useState2 = useState(initialState), debouncedState = _useState2[0], setState = _useState2[1]; var setDebouncedState = useMemo$1(function () { return debounce(setState, wait); }, [wait]); var setCombinedState = function setCombinedState(state) { setDebouncedState.cancel(); setImmediateState(state); setDebouncedState(state); }; return [immediateState, debouncedState, setCombinedState]; } /** * A hook for fast data filtering * @param filterFn * @param data * @param filterText */ var useFilter = function useFilter(filterFn, data, filterText) { return useMemo$1(function () { if (!filterText.length) { return data; } return filterFn(data, filterText); }, [data, filterFn, filterText]); }; /** * A simple utility for creating functions for trivial data filtering * @param fields */ function createFilter(fields) { return function (originalData, filterText) { return originalData.filter(function (data) { return fields.some(function (field) { var fieldData = data[field] ? String(data[field]) : ''; return !!(filterText && fieldData.toLowerCase().includes(filterText.trim().toLowerCase())); }); }); }; } export default WindowTable$1; export { Html5Table, WindowTable$1 as WindowTable, createFilter, useDebouncedState, useFilter }; //# sourceMappingURL=window-table.esm.js.map