@aibsweb/faceted-search
Version:
A generalized faceted search application.
591 lines (514 loc) • 24.7 kB
JavaScript
"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
};