lucid-ui
Version:
A UI component library from Xandr.
659 lines • 22.9 kB
JavaScript
import _, { omit } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { lucidClassNames } from '../../util/style-helpers';
import { filterTypes, } from '../../util/component-types';
import ArrowIcon from '../Icon/ArrowIcon/ArrowIcon';
import DragCaptureZone from '../DragCaptureZone/DragCaptureZone';
const cx = lucidClassNames.bind('&-Table');
const { any, bool, func, node, number, object, string, oneOf, oneOfType } = PropTypes;
const Thead = (props) => {
const { children, className, ...passThroughs } = props;
return (React.createElement("thead", { ...omit(passThroughs, [
'className',
'children',
'initialState',
'callbackId',
]), className: cx('&-Thead', className) }, renderRowsWithIdentifiedEdges(filterTypes(children, Tr), Th)));
};
Thead.displayName = 'Table.Thead';
Thead.peek = {
description: `
\`Thead\` renders <thead>.
`,
};
Thead.propTypes = {
/**
Appended to the component-specific class names set on the root element.
Value is run through the \`classnames\` library.
*/
className: any,
/**
any valid React children
*/
children: node,
};
const Tbody = (props) => {
const { children, className, ...passThroughs } = props;
return (React.createElement("tbody", { ...omit(passThroughs, [
'className',
'children',
'initialState',
'callbackId',
]), className: cx('&-Tbody', className) }, renderRowsWithIdentifiedEdges(filterTypes(children, Tr), Td)));
};
Tbody.displayName = 'Table.Tbody';
Tbody.peek = {
description: `
\`Tbody\` renders <tbody>.
`,
};
Tbody.propTypes = {
/**
Appended to the component-specific class names set on the root element.
Value is run through the \`classnames\` library.
*/
className: any,
/**
any valid React children
*/
children: node,
};
const Tr = (props) => {
const { className, children, isDisabled, isSelected, isActive, ...passThroughs } = props;
return (React.createElement("tr", { ...omit(passThroughs, [
'children',
'className',
'isDisabled',
'isSelected',
'isActive',
'isActionable',
'initialState',
'callbackId',
]), className: cx('&-Tr', {
'&-is-disabled': isDisabled,
'&-is-selected': isSelected,
'&-is-active': isActive,
}, className) }, children));
};
Tr.defaultProps = {
isDisabled: false,
isSelected: false,
isActive: false,
};
Tr.displayName = 'Table.Tr';
Tr.peek = {
description: `
\`Tr\` renders <tr>.
`,
};
Tr.propTypes = {
/**
any valid React children
*/
children: node,
/**
Appended to the component-specific class names set on the root element.
Value is run through the \`classnames\` library.
*/
className: any,
/**
Applies disabled styles to the row.
*/
isDisabled: bool,
/**
Applies styles to the row for when the row is selected, usually by a
checkbox.
*/
isSelected: bool,
/**
Applies active styles to the row, usually when the row has been clicked.
*/
isActive: bool,
};
export class Th extends React.Component {
constructor() {
super(...arguments);
this.rootRef = React.createRef();
this.state = {
// Represents the actively changing width as the cell is resized.
activeWidth: this.props.width || null,
// Indicates if a `width` prop was explicitly provided.
hasSetWidth: !!this.props.width,
// Indicates whether the cell is currently being resized.
isResizing: false,
// Indicates a mouse drag is in progress
isDragging: false,
// Represents the width when the cell is not actively being resized.
passiveWidth: this.props.width || null,
};
this.getWidth = () => {
const styleWidth = _.get(this.rootRef, 'style.width');
if (_.endsWith(styleWidth, 'px')) {
return parseInt(styleWidth);
}
if (this.rootRef.current) {
return this.rootRef.current.getBoundingClientRect().width;
}
return null;
};
this.handleClickCapture = (event) => {
if (this.state.isDragging) {
event.stopPropagation();
this.setState({
isDragging: false,
});
}
};
this.handleMouseEnter = () => {
this.setState({
isDragging: this.state.isResizing,
});
};
this.handleMouseUp = () => {
this.setState({
isDragging: this.state.isResizing,
});
};
this.handleDragEnded = (coordinates, { event }) => {
this.setState({
isResizing: false,
passiveWidth: this.state.activeWidth,
});
window.document.body.style.cursor = '';
if (this.props.onResize) {
this.props.onResize(this.state.activeWidth, {
event,
props: this.props,
});
}
};
this.handleDragged = (coordinates, { event, props, }) => {
let passiveWidth = this.state.passiveWidth;
const minWidth = this.props.minWidth !== null && _.isString(this.props.minWidth)
? parseInt(this.props.minWidth)
: this.props.minWidth;
if (passiveWidth === null) {
return;
}
else if (_.isString(passiveWidth)) {
passiveWidth = parseInt(passiveWidth);
}
const activeWidth = (minWidth && passiveWidth + coordinates.dX > minWidth) || !minWidth
? passiveWidth + coordinates.dX
: minWidth;
this.setState({ activeWidth });
if (this.props.onResize) {
this.props.onResize(activeWidth, {
event,
props: this.props,
});
}
};
this.handleDragStarted = (coordinates, { event, props, }) => {
const startingWidth = this.getWidth();
this.setState({
activeWidth: startingWidth,
hasSetWidth: true,
isResizing: true,
isDragging: true,
passiveWidth: startingWidth,
});
window.document.body.style.cursor = 'ew-resize';
if (this.props.onResize) {
this.props.onResize(startingWidth, {
event,
props: this.props,
});
}
};
}
UNSAFE_componentWillReceiveProps({ width, }) {
if (!_.isNil(width) && width !== this.props.width) {
this.setState({
hasSetWidth: true,
passiveWidth: width,
});
}
}
render() {
const { children, className, hasBorderRight, hasBorderLeft, isFirstRow, isLastRow, isFirstCol, isFirstSingle, isLastCol, align, isResizable, isSortable, isSorted, sortDirection, style, truncateContent, ...passThroughs } = this.props;
const { activeWidth, hasSetWidth, isResizing, passiveWidth } = this.state;
return (React.createElement("th", { ...omit(passThroughs, [
'className',
'children',
'style',
'align',
'hasBorderRight',
'hasBorderLeft',
'isResizable',
'isSortable',
'isSorted',
'onResize',
'sortDirection',
'width',
'minWidth',
'isFirstRow',
'isLastRow',
'isFirstCol',
'isLastCol',
'isFirstSingle',
'field',
'truncateContent',
'initialState',
'callbackId',
]), className: cx('&-Th', {
'&-is-first-row': isFirstRow,
'&-is-last-row': isLastRow,
'&-is-first-col': isFirstCol,
'&-is-first-single': isFirstSingle,
'&-is-last-col': isLastCol,
'&-align-left': align === 'left',
'&-align-center': align === 'center',
'&-align-right': align === 'right',
'&-is-resizable': isResizable,
'&-is-resizing': isResizing,
'&-is-sortable': isSortable === false ? isSortable : isSorted || isSortable,
'&-is-sorted': isSorted,
'&-has-border-right': hasBorderRight,
'&-has-border-left': hasBorderLeft,
'&-truncate-content': truncateContent,
}, className), ref: this.rootRef, onClickCapture: this.handleClickCapture, onMouseEnter: this.handleMouseEnter, onMouseUp: this.handleMouseUp, style: hasSetWidth
? _.assign({}, style, {
width: isResizing ? activeWidth : passiveWidth,
})
: style },
React.createElement("div", { className: cx('&-Th-inner') },
React.createElement("div", { className: cx('&-Th-inner-content') }, children),
isSorted || isSortable ? (React.createElement("div", { className: cx('&-Th-inner-caret') },
React.createElement(ArrowIcon, { className: cx('&-sort-icon'), direction: sortDirection, size: 10 }))) : null,
isResizable ? (React.createElement(DragCaptureZone, { className: cx('&-Th-inner-resize'), onDrag: this.handleDragged, onDragEnd: this.handleDragEnded, onDragStart: this.handleDragStarted })) : null)));
}
}
Th.displayName = 'Table.Th';
Th.defaultProps = {
align: 'left',
isResizable: false,
isSorted: false,
sortDirection: 'up',
rowSpan: 1,
};
Th.peek = {
description: `
\`Th\` renders <th>.
`,
};
Th.propTypes = {
/**
Aligns the content of a cell. Can be \`left\`, \`center\`, or \`right\`.
*/
align: string,
/**
any valid React children
*/
children: node,
/**
Appended to the component-specific class names set on the root element.
Value is run through the \`classnames\` library.
*/
className: any,
/**
Should be \`true\` to render a right border.
*/
hasBorderRight: bool,
/**
Should be \`true\` to render a left border.
*/
hasBorderLeft: bool,
/**
Styles the cell to indicate it should be resizable and sets up drag-
related events to enable this resizing functionality.
*/
isResizable: bool,
/**
Styles the cell to allow column sorting.
*/
isSortable: bool,
/**
Renders a caret icon to show that the column is sorted.
*/
isSorted: bool,
/**
Called as the user drags the resize handle to resize the column atop
which this table header cell sits.
*/
onResize: func,
/**
Sets the direction of the caret icon when \`isSorted\` is also set.
*/
sortDirection: oneOf(['left', 'up', 'right', 'down', undefined]),
/**
Styles that are passed through to root element.
*/
style: object,
/**
Sets the width of the cell.
*/
width: oneOfType([number, string]),
/**
Sets the min width of the cell.
*/
minWidth: oneOfType([number, string]),
/**
Define the cell as being in the first row.
*/
isFirstRow: bool,
/**
Define the cell as being in the last row.
*/
isLastRow: bool,
/**
Define the cell as being in the first column.
*/
isFirstCol: bool,
/**
Define the cell as being in the last column.
*/
isLastCol: bool,
/**
Define the cell as being the first 1-height cell in the row.
*/
isFirstSingle: bool,
/**
Sets the field value for the cell.
*/
field: string,
/** Truncates `Table.Td` content with ellipses, must be used with `hasFixedHeader` */
/**
Truncates header and adds ellipses.
*/
truncateContent: bool,
};
const Td = (props) => {
const { className, isFirstRow, isLastRow, isFirstCol, isLastCol, isFirstSingle, align, hasBorderRight, hasBorderLeft, truncateContent, ...passThroughs } = props;
return (React.createElement("td", { ...omit(passThroughs, [
'className',
'align',
'hasBorderRight',
'hasBorderLeft',
'isFirstRow',
'isLastRow',
'isFirstCol',
'isLastCol',
'isFirstSingle',
'isEmpty',
'truncateContent',
'initialState',
'callbackId',
'sortDirection',
]), className: cx('&-Td', {
'&-is-first-row': isFirstRow,
'&-is-last-row': isLastRow,
'&-is-first-col': isFirstCol,
'&-is-last-col': isLastCol,
'&-is-first-single': isFirstSingle,
'&-align-left': align === 'left',
'&-align-center': align === 'center',
'&-align-right': align === 'right',
'&-has-border-right': hasBorderRight,
'&-has-border-left': hasBorderLeft,
'&-truncate-content': truncateContent,
}, className) }));
};
Td.displayName = 'Table.Td';
Td.defaultProps = {
align: 'left',
hasBorderRight: false,
hasBorderLeft: false,
rowSpan: 1,
};
Td.peek = {
description: `
\`Td\` renders <td>.
`,
categories: [],
madeFrom: [],
};
Td.propTypes = {
/**
Aligns the content of a cell. Can be \`left\`, \`center\`, or \`right\`.
*/
align: oneOf(['left', 'center', 'right']),
/**
Appended to the component-specific class names set on the root element.
Value is run through the \`classnames\` library.
*/
className: any,
/**
Should be \`true\` to render a right border.
*/
hasBorderRight: bool,
/**
Should be \`true\` to render a left border.
*/
hasBorderLeft: bool,
/**
Define the cell as being in the first row.
*/
isFirstRow: bool,
/**
Define the cell as being in the last row.
*/
isLastRow: bool,
/**
Define the cell as being in the first column.
*/
isFirstCol: bool,
/**
Define the cell as being in the last column.
*/
isLastCol: bool,
/**
Define the cell as being the first 1-height cell in the row.
*/
isFirstSingle: bool,
/**
Indicates if the cell has any data or not.
*/
isEmpty: bool,
/**
Truncates \`Table.Td\` content with ellipses, must be used with \`hasFixedHeader\`
*/
truncateContent: bool,
};
const Table = (props) => {
const { className, hasBorder, density, hasWordWrap, hasLightHeader, hasHover, style, ...passThroughs } = props;
return (React.createElement("table", { ...omit(passThroughs, [
'density',
'hasLightHeader',
'hasBorder',
'hasWordWrap',
'hasHover',
'initialState',
'callbackId',
]), style: style, className: cx('&', {
'&-density-extended': density === 'extended',
'&-density-compressed': density === 'compressed',
'&-has-border': hasBorder,
'&-has-word-wrap': hasWordWrap,
'&-has-light-header': hasLightHeader,
'&-no-hover': !hasHover,
}, className) }));
};
Table.displayName = 'Table';
Table.defaultProps = {
density: 'extended',
hasBorder: false,
hasWordWrap: true,
hasLightHeader: true,
hasHover: true,
};
Table.peek = {
description: `\`Table\` provides the most basic components to create a lucid table. It is recommended to create a wrapper around this component rather than using it directly in an app.`,
categories: ['table'],
madeFrom: ['ArrowIcon', 'DragCaptureZone'],
};
Table.propTypes = {
/**
Styles that are passed through to the root container.
*/
style: object,
/**
Class names that are appended to the defaults.
*/
className: string,
/**
Adjusts the row density of the table to have more or less spacing.
*/
density: oneOf(['compressed', 'extended']),
/**
Allows light header.
*/
hasLightHeader: bool,
/**
Render the table with borders on the outer edge.
*/
hasBorder: bool,
/**
Enables word wrapping in tables cells.
*/
hasWordWrap: bool,
/**
Applies a row hover to rows. Defaults to true.
*/
hasHover: bool,
};
/** ChildComponents */
Table.Thead = Thead;
Table.Th = Th;
Table.Tbody = Tbody;
Table.Tr = Tr;
Table.Td = Td;
function mapToGrid(trList, cellType = Td, mapFn = _.property('element')) {
const cellRowList = _.map(trList, (trElement) => _.map(filterTypes(trElement.props.children, cellType)));
const grid = [];
if (_.isEmpty(cellRowList)) {
return [];
}
// iterate over each row
for (let rowIndex = 0; rowIndex < cellRowList.length; rowIndex++) {
const cellRow = cellRowList[rowIndex];
if (_.isNil(grid[rowIndex])) {
grid[rowIndex] = [];
}
const canonicalRow = rowIndex;
// build out each horizonal duplicates of each cell
for (let cellElementIndex = 0; cellElementIndex < cellRow.length; cellElementIndex++) {
const cellElement = cellRow[cellElementIndex];
let colSpan = 1;
let isCellIncluded = false;
if (_.isNumber(cellElement.props.colSpan)) {
colSpan = cellElement.props.colSpan;
}
const nilCellIndex = _.findIndex(grid[canonicalRow], _.isNil);
const originCol = nilCellIndex !== -1 ? nilCellIndex : grid[canonicalRow].length;
for (let currentColSpan = 0; currentColSpan < colSpan; currentColSpan++) {
grid[canonicalRow][originCol + currentColSpan] = {
element: cellElement,
canonicalPosition: {
row: canonicalRow,
col: originCol,
},
isOriginal: !isCellIncluded,
};
isCellIncluded = true;
}
}
// build out each vertial duplicates of each cell using the new row in the full grid
for (let colIndex = 0; colIndex < grid[canonicalRow].length; colIndex++) {
const gridCell = grid[canonicalRow][colIndex];
if (gridCell.isOriginal) {
const cellElement = _.get(gridCell, 'element');
let rowSpan = 1;
if (_.isNumber(_.get(cellElement, 'props.rowSpan'))) {
rowSpan = _.get(cellElement, 'props.rowSpan');
}
for (let currentRowSpan = 1; currentRowSpan < rowSpan; currentRowSpan++) {
if (_.isNil(grid[canonicalRow + currentRowSpan])) {
grid[canonicalRow + currentRowSpan] = [];
}
grid[canonicalRow + currentRowSpan][colIndex] = _.assign({}, grid[canonicalRow + currentRowSpan - 1][colIndex], { isOriginal: false });
}
}
}
}
// map new values to each cell in the final grid
const finalGrid = [];
for (let rowIndex = 0; rowIndex < grid.length; rowIndex++) {
finalGrid[rowIndex] = [];
for (let colIndex = 0; colIndex < grid[rowIndex].length; colIndex++) {
finalGrid[rowIndex][colIndex] = mapFn(grid[rowIndex][colIndex], { row: rowIndex, col: colIndex }, finalGrid);
}
}
return finalGrid;
}
/**
* renderRowsWithIdentifiedEdges
*
* Returns an equivalent list of Tr's where each cell on the perimeter has props set for: `isFirstRow`, `isLastRow`, `isFirstCol`, `isLastCol`, and `isFirstSingle`
*/
function renderRowsWithIdentifiedEdges(trList, cellType = Td) {
const duplicateReferences = [];
const fullCellGrid = mapToGrid(trList, cellType, ({ element: { props }, isOriginal, canonicalPosition }, currentPos, grid) => {
if (!isOriginal) {
// if cell spans multiple positions
// store current position and return original cell props reference
duplicateReferences.push(currentPos);
return grid[canonicalPosition.row][canonicalPosition.col];
}
return _.assign({}, props); // return a new props object based on old cell
});
if (_.isEmpty(fullCellGrid)) {
return [];
}
const firstRow = _.first(fullCellGrid);
if (_.isUndefined(firstRow)) {
return [];
}
const firstRowIndex = 0;
const lastRowIndex = fullCellGrid.length - 1;
const firstColIndex = 0;
const lastColIndex = firstRow.length - 1;
const firstSingleLookup = {};
// decorate the props of each cell with props that indicate its role in the table
_.forEach(fullCellGrid, (cellList, rowIndex) => _.forEach(cellList, (cellProps, colIndex) => {
if (!_.isNull(cellProps)) {
if (rowIndex === firstRowIndex) {
cellProps.isFirstRow = true;
}
if (rowIndex === lastRowIndex) {
cellProps.isLastRow = true;
}
if (colIndex === firstColIndex) {
cellProps.isFirstCol = true;
}
if (colIndex === lastColIndex) {
cellProps.isLastCol = true;
}
if (!_.has(firstSingleLookup, rowIndex)) {
_.set(firstSingleLookup, rowIndex, false);
}
if (!_.get(firstSingleLookup, rowIndex) &&
_.get(cellProps, 'rowSpan', 1) === 1) {
_.set(firstSingleLookup, rowIndex, true);
cellProps.isFirstSingle = true;
}
}
}));
_.forEach(duplicateReferences, ({ row, col }) => {
fullCellGrid[row][col] = null; // remove duplicate references from grid
});
// render the grid back to elements using the updated cell props
return _.map(trList, (trElement, rowIndex) => (React.createElement(Tr, { ...trElement.props, key: rowIndex }, _.reduce(fullCellGrid[rowIndex], (rowChildren, cellProps, colIndex) => rowChildren.concat(!_.isNull(cellProps)
? [
React.createElement(cellType, _.assign({}, cellProps, { key: colIndex })),
]
: []), []))));
}
export default Table;
//# sourceMappingURL=Table.js.map