UNPKG

@aibsweb/faceted-search

Version:
591 lines (514 loc) 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _react = _interopRequireDefault(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _AutoSizer = require("react-virtualized/dist/es/AutoSizer"); var _CellMeasurer = require("react-virtualized/dist/es/CellMeasurer"); var _Table = require("react-virtualized/dist/es/Table"); var _WindowScroller = require("react-virtualized/dist/es/WindowScroller"); var _InfiniteLoader = require("react-virtualized/dist/es/InfiniteLoader"); var _lodash = require("lodash"); var _reactCustomScrollbars = require("react-custom-scrollbars"); var _dataHandlers = _interopRequireDefault(require("./utils/data-handlers")); var _enums = require("../../enums"); var _stringHelper = require("../../helpers/string-helper"); require("../../../scss/data-table.scss"); var _loadIndicator = _interopRequireDefault(require("../status/load-indicator")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 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 _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } var DataTable = /*#__PURE__*/ function (_React$Component) { _inherits(DataTable, _React$Component); /** * A component used to display, search, and filter data provided by a graphQL endpoint. * @constructor * @instance * @param {Object} props These are the properties for this component. * @param {Array} props.data The data to display in the table * @param {Object} props.store Reference to state store * @param {Boolean} props.isLoading Apollo Query component is loading * @param {String} props.componentDefinition name of the component definition * @param {String} props.renderer name of the renderer called -> 'data-table' * @param {String} props.className class name * @param {Number} props.position used for class styling * @param {String} props.columnFont font information * @param {Number} props.columnMarginHorizontal column margin * @param {Number} props.offsetIncrement value to increment paging offset for data * @param {Object[]} props.schema information for each column and data accessors * @param {Function} props.getCustomCellContentByType for custom cell types to render * @param {Number} props.minTableWidth minimum width of table resizing */ function DataTable(props) { var _this; _classCallCheck(this, DataTable); _this = _possibleConstructorReturn(this, _getPrototypeOf(DataTable).call(this, props)); _this.state = { selectedSortColumn: (0, _lodash.get)(props, 'store.sort[0].category', ''), sortDirectionAscending: (0, _lodash.get)(props, 'store.sort[0].ascending', true), scrollToIndex: -1, // the value -1 provides an unspecified index nextPageOffset: props.offsetIncrement, cachedData: props.data }; // create a map from the schema column key to the column name if ((0, _lodash.isArray)(props.schema)) { _this.keyToColumnMap = props.schema.reduce(function (acc, category) { acc[category.key] = category.column; return acc; }, {}); } else { _this.keyToColumnMap = {}; } // The CellMeasurerCache stores CellMeasurer measurements and shares them with a parent Grid. // https://github.com/bvaughn/react-virtualized/blob/master/docs/CellMeasurer.md#cellmeasurercache _this.cellMeasurerCache = new _CellMeasurer.CellMeasurerCache({ minHeight: 60, fixedWidth: true }); // used with WindowScroller React Virtualized component _this.dataTableWrapperRef = _react["default"].createRef(); // used to synchronize scrolling positions with table header and grid _this.dataTableHeaderWrapperRef = _react["default"].createRef(); // bindings _this.columnCellRenderer = _this.columnCellRenderer.bind(_assertThisInitialized(_this)); _this.tableRowGetter = _this.tableRowGetter.bind(_assertThisInitialized(_this)); _this.onSort = _this.onSort.bind(_assertThisInitialized(_this)); // infiniteScrollRequestResolve holds a reference to the resolve function of the loadMore promise. // This property is also used as a flag for when infinite scroll is requesting data. // See: https://github.com/bvaughn/react-virtualized/blob/master/docs/InfiniteLoader.md // under 'loadMoreRows' in the docs. _this.infiniteScrollRequestResolve = null; return _this; } _createClass(DataTable, [{ key: "dataIsNew", value: function dataIsNew() { var cachedDataIsEmpty = this.state.cachedData.length === 0; var thereIsDataFromProps = this.props.data.length > 0; var isNotLoading = !this.props.isLoading; return cachedDataIsEmpty && thereIsDataFromProps && isNotLoading; } }, { key: "filtersChanged", value: function filtersChanged(prevProps) { return this.props.store.filter.length !== prevProps.store.filter.length; } }, { key: "sortChanged", value: function sortChanged() { var selectedSortColumnChanged = this.state.selectedSortColumn !== (0, _lodash.get)(this.props.store.sort, ['0', 'category'], ''); var sortDirectionAscendingChanged = this.state.sortDirectionAscending !== (0, _lodash.get)(this.props.store.sort, ['0', 'ascending'], true); return selectedSortColumnChanged || sortDirectionAscendingChanged; } }, { key: "scrollDataIsLoaded", value: function scrollDataIsLoaded(prevProps) { // infiniteScrollRequestResolve: used as a flag to notify that more data is requested. var infiniteScrollRequestResolve = !!this.infiniteScrollRequestResolve; // didLoad: Apollo Query was loading in the previous cycle AND is NOT loading this cycle. var didLoad = prevProps.isLoading && !this.props.isLoading; // cachedDataLengthEqualsOffset: if the nextPageOffset is not equal to the current cache length, then there is no more data to be requested. var cachedDataLengthEqualsOffset = this.state.nextPageOffset === this.state.cachedData.length; // usingApolloCacheData: if data is pulled from the Apollo cache, then the loading state will always be false. However, the previous offset will not equal the current offset. var usingApolloCacheData = prevProps.store.offset !== this.props.store.offset && !this.props.isLoading && !prevProps.isLoading; return infiniteScrollRequestResolve && cachedDataLengthEqualsOffset && (didLoad || usingApolloCacheData); } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps) { var _this2 = this; /** * cases to check: * 1. new data is passed in through props, * then set cached data with new data. * 2. filters changed, * then clear cached data * and update stored filter length * and reset the next page offset * and clear cell measurer cache * and go to the top of the data table. * 3. sort changed * then clear cached data * and update stored sort data * and reset the next page offset * and clear cell measurer cache * and go to the top of the data table. * 4. scroll fetched more data * then update next page offset * and append new data to cached data. */ // 1. if (this.dataIsNew()) { this.setState({ cachedData: this.props.data }); } // 2. else if (this.filtersChanged(prevProps)) { this.setState({ cachedData: [], nextPageOffset: this.props.offsetIncrement, scrollToIndex: 0, scrollTop: 0 }, function () { _this2.cellMeasurerCache.clearAll(); }); } // 3. else if (this.sortChanged()) { this.setState({ selectedSortColumn: (0, _lodash.get)(this.props.store.sort, ['0', 'category'], ''), sortDirectionAscending: (0, _lodash.get)(this.props.store.sort, ['0', 'ascending'], true), cachedData: [], nextPageOffset: this.props.offsetIncrement, scrollToIndex: 0, scrollTop: 0 }, function () { _this2.cellMeasurerCache.clearAll(); }); } // 4. else if (this.scrollDataIsLoaded(prevProps)) { this.infiniteScrollRequestResolve(); this.infiniteScrollRequestResolve = null; if (this.props.data.length) { this.setState({ nextPageOffset: this.state.nextPageOffset + this.props.offsetIncrement, cachedData: [].concat(_toConsumableArray(this.state.cachedData), _toConsumableArray(this.props.data)) }); } } } }, { key: "componentWillUnmount", value: function componentWillUnmount() { // on unmount, reset the page offset to start from the begining var event = new CustomEvent(_enums.EVENT_KEYS.DT_PAGING, { detail: { offset: 0 } }); document.dispatchEvent(event); } /** * Used to create a table cell matching the desired height/width * more detail on the parameters: https://github.com/bvaughn/react-virtualized/blob/master/docs/Column.md#cellrenderer * * @param {string} dataKey Indicates which data handler to use when rendering cell content * @param {Class} parent This components parent element * @param {Number} rowIndex The index of the current row * @param {Number} columnIndex The index of the current column * @param {Object} columnData The schema for this cell * @param {Object} rowData The data for this row to be rendered as described in the columnData */ }, { key: "columnCellRenderer", value: function columnCellRenderer(_ref) { var dataKey = _ref.dataKey, parent = _ref.parent, rowIndex = _ref.rowIndex, columnIndex = _ref.columnIndex, columnData = _ref.columnData, rowData = _ref.rowData; var dataTableCellNumberStyle = ''; var content; // Determine content from column type. switch (columnData.type) { case 'string': content = _dataHandlers["default"].handleString(columnData, rowData); if (/^[0-9]*$/.test(rowData[dataKey])) { dataTableCellNumberStyle = 'data-table-cell-number'; } break; case 'multi-string': content = _dataHandlers["default"].handleMultiString(columnData, rowData); break; case 'link': content = _dataHandlers["default"].handleLink(columnData, rowData); break; case 'multi-link': content = _dataHandlers["default"].handleMultiLink(columnData, rowData); break; case 'relative-link': content = _dataHandlers["default"].handleRelativeLink(columnData, rowData); break; case 'icon-link': content = _dataHandlers["default"].handleIconLink(columnData, rowData); break; case 'markdown': content = _react["default"].createElement("div", { dangerouslySetInnerHTML: { __html: _dataHandlers["default"].handleMarkdown(columnData, rowData) } }); break; case 'number': content = _dataHandlers["default"].handleNumber(columnData, rowData); break; default: content = this.props.getCustomCellContentByType(columnData, rowData); break; } // The CellMeasurer is a High-order component that automatically measures a cell's contents by temporarily rendering it in a way that is not visible to the user. // In this case we use it to measure the cell height. return _react["default"].createElement(_CellMeasurer.CellMeasurer, { cache: this.cellMeasurerCache, columnIndex: columnIndex, key: dataKey, parent: parent, rowIndex: rowIndex }, _react["default"].createElement("div", { className: "data-table-cell ".concat(dataTableCellNumberStyle) }, content)); } /** * Use the schema stored in state to create create the components for each column in this row * * @param {Number} width The expected width of this column. */ }, { key: "computeColumns", value: function computeColumns(width) { var _this3 = this; var schema = this.props.schema; return schema.map(function (col) { var disableSort = col['disable-sort'] || false; var headerClassName = disableSort ? 'data-table-header sort-disabled' : 'data-table-header sort-enabled'; var dataKey = col.type === 'link' ? 'text-key' : 'key'; var columnProps = { cellRenderer: _this3.columnCellRenderer, columnData: col, dataKey: col['sort-property'] || dataKey, disableSort: disableSort, flexGrow: 1, flexShrink: 1, key: col[dataKey], headerClassName: headerClassName, label: col.column, width: width / schema.length }; return _react["default"].createElement(_Table.Column, columnProps); }); } /** * Returns the data for the row at index * * @param {Number} index The index of the row that this should return * @returns {Object} The data for the row at index */ }, { key: "tableRowGetter", value: function tableRowGetter(_ref2) { var index = _ref2.index; return this.state.cachedData[index]; } /** * Computes the width based on widest column name. * @returns {Number} */ }, { key: "getWidthByMaxColumn", value: function getWidthByMaxColumn() { var _this$props = this.props, columnFont = _this$props.columnFont, columnMarginHorizontal = _this$props.columnMarginHorizontal; var columnNameWidths = this.props.schema.map(function (_ref3) { var column = _ref3.column; return (0, _stringHelper.getRenderedStringWidth)(column, columnFont); }); var maxColumnWidth = Math.max.apply(Math, _toConsumableArray(columnNameWidths)) + columnMarginHorizontal + 1; return maxColumnWidth * this.props.schema.length; } /** * This ensures that generated widths are not smaller than the minimum width for the table * * @param {Number} autoSizerWidth The width that autosizer suggests * @returns {Number} The computed width of this element. */ }, { key: "computeTableWidth", value: function computeTableWidth(autoSizerWidth) { return Math.max(autoSizerWidth, this.props.minTableWidth, this.getWidthByMaxColumn()); } }, { key: "onSort", value: function onSort(_ref4) { var sortBy = _ref4.sortBy, sortDirection = _ref4.sortDirection; var ascending = sortDirection === 'ASC'; var sort = { category: sortBy, ascending: ascending }; var event = new CustomEvent(_enums.EVENT_KEYS.DT_SORT, { detail: sort }); document.dispatchEvent(event); } }, { key: "loadMore", value: function loadMore() { var _this4 = this; return new Promise(function (resolve) { var event = new CustomEvent(_enums.EVENT_KEYS.DT_PAGING, { detail: { offset: _this4.state.nextPageOffset } }); document.dispatchEvent(event); _this4.infiniteScrollRequestResolve = resolve; // set scroll index to -1 to prevent automatically scrolling to somewhere else. _this4.setState({ scrollToIndex: -1 }); }); } }, { key: "handleScroll", value: function handleScroll(args) { // Sync horizontal scrolling this.dataTableHeaderWrapperRef.current.scrollLeft = args.target.scrollLeft; // Use state to sync vertical scroll this.setState({ scrollTop: args.target.scrollTop }); } }, { key: "getRowClass", value: function getRowClass(_ref5) { var index = _ref5.index; var rowClass = 'data-table-row'; var oddClass = 'odd'; if (index < 0 || index % 2 !== 0) { return "".concat(rowClass, " ").concat(oddClass); } return rowClass; } /** * React Lifecycle Event * @ignore */ }, { key: "render", value: function render() { var _this5 = this; var cachedData = this.state.cachedData; var headerHeight = 50; // This value is tied to the css class .data-table-header-wrapper // render loading indicator if cached data is cleared and data is currently being fetched if (cachedData.length === 0 && this.props.isLoading) { return _loadIndicator["default"]; } return _react["default"].createElement("div", { className: 'data-table-container' }, _react["default"].createElement("div", { className: 'data-table-header-wrapper', ref: this.dataTableHeaderWrapperRef }, _react["default"].createElement(_AutoSizer.AutoSizer, null, function (_ref6) { var width = _ref6.width, height = _ref6.height; var tableProps = { className: 'data-table', headerHeight: headerHeight, headerStyle: { paddingRight: '0px', flexGrow: 1 }, // Prevents horizontal scrolling when all columns fit on screen. height: height || 0, // Keeps the data table at the same height as the filter-box. overscanRowCount: 0, rowClassName: _this5.getRowClass, rowCount: 0, rowGetter: function rowGetter() {}, rowHeight: 0, sort: _this5.onSort, sortBy: _this5.state.selectedSortColumn, sortDirection: _this5.state.sortDirectionAscending ? 'ASC' : 'DESC', width: _this5.computeTableWidth(width) }; return _react["default"].createElement(_Table.Table, tableProps, _this5.computeColumns(tableProps.width)); })), _react["default"].createElement("div", { className: 'data-table-wrapper', ref: this.dataTableWrapperRef }, _react["default"].createElement(_reactCustomScrollbars.Scrollbars, { className: 'data-table__scrollbars', onScroll: this.handleScroll.bind(this) }, _react["default"].createElement(_InfiniteLoader.InfiniteLoader, { isRowLoaded: function isRowLoaded(_ref7) { var index = _ref7.index; return !!cachedData[index]; }, loadMoreRows: this.loadMore.bind(this), rowCount: 1000000, threshold: this.props.offsetIncrement / 2 }, function (_ref8) { var onRowsRendered = _ref8.onRowsRendered, registerChild = _ref8.registerChild; return (// add key to prevent crashing, see -> https://github.com/bvaughn/react-virtualized/issues/1256 _react["default"].createElement(_WindowScroller.WindowScroller, { scrollElement: _this5.dataTableWrapperRef.current, key: _this5.dataTableWrapperRef.current }, function (_ref9) { var height = _ref9.height, scrollTop = _ref9.scrollTop; return _react["default"].createElement(_AutoSizer.AutoSizer, { disableHeight: true }, function (_ref10) { var width = _ref10.width; var tableProps = { autoHeight: true, className: 'data-table', deferredMeasurementCache: _this5.cellMeasurerCache, disableHeader: true, estimatedRowSize: 90, gridClassName: 'data-table-grid', height: height || 0, // Keeps the data table at the same height as the filter-box. onRowsRendered: onRowsRendered, overscanRowCount: 2, ref: registerChild, rowClassName: _this5.getRowClass, rowCount: cachedData.length, rowGetter: _this5.tableRowGetter, rowHeight: _this5.cellMeasurerCache.rowHeight, scrollToIndex: _this5.state.scrollToIndex, scrollTop: scrollTop, width: _this5.computeTableWidth(width) }; return _react["default"].createElement(_Table.Table, _extends({}, tableProps, { scrollTop: _this5.state.scrollTop }), _this5.computeColumns(tableProps.width)); }); }) ); })))); } }]); return DataTable; }(_react["default"].Component); exports["default"] = DataTable; DataTable.propTypes = { data: _propTypes["default"].arrayOf(_propTypes["default"].object).isRequired, store: _propTypes["default"].object.isRequired, isLoading: _propTypes["default"].bool.isRequired, componentDefinition: _propTypes["default"].string, renderer: _propTypes["default"].string, className: _propTypes["default"].string, position: _propTypes["default"].number, columnFont: _propTypes["default"].string, columnMarginHorizontal: _propTypes["default"].number, offsetIncrement: _propTypes["default"].number, schema: _propTypes["default"].arrayOf(_propTypes["default"].object).isRequired, getCustomCellContentByType: _propTypes["default"].func, minTableWidth: _propTypes["default"].number }; DataTable.defaultProps = { getCustomCellContentByType: function getCustomCellContentByType(columnData, rowData) { return rowData[columnData.key]; }, minTableWidth: 930, columnFont: '700 normal 0.875rem roboto, sans-serif', columnMarginHorizontal: 20, offsetIncrement: 1000 };