UNPKG

lucid-ui

Version:

A UI component library from Xandr.

659 lines 22.9 kB
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