window-table
Version:
Windowing Table for React based on React Window
774 lines (667 loc) • 25.8 kB
JavaScript
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