UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

401 lines (368 loc) 13.4 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) 2023 Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) 2023 Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 4.2.0 */ import React, { Component } from "react"; import PropTypes from "prop-types"; import { ROW_SETTING_HEIGHT_COMPACT } from "../../../models/entity/rowsSetting/rowsSettingEntity"; const PADDING_SIZE = 8; const PADDING_FIRST_COLUMN_ADDED = 20; export const TableContext = React.createContext({ columns: [], // The columns to display tableWidth: null, // The table width tableviewWidth: null, // The tableview width isDraggingColumn: false, // Is the column dragging isSelectAllChecked: () => {}, // Is the select all checked isRowSelected: () => {}, // Is the row selected isSortedColumn: () => {}, // Is the column sorted isSortedAsc: () => {}, // Is the column sorted ascending canMoveColumn: () => {}, // Can move the column onSortChange: () => {}, // Whenever the sort change onResizeColumn: () => {}, // Whenever the size of a column change onReorderColumns: () => {}, // Whenever the column is reordered onStartDraggingColumn: () => {}, // Whenever a column start to be dragged onEndDraggingColumn: () => {}, // Whenever a column end to be dragged onChangeColumns: () => {}, // Whenever columns change }); /** * This component represents a table */ export default class TableContextProvider extends Component { /** * Default constructor * @param props Component props */ constructor(props) { super(props); this.state = this.defaultState; this.createRefs(); } /** * Returns the component default state */ get defaultState() { return { columns: this.sanitizeColumnsFromProps, // The columns to display tableWidth: null, // The table width tableviewWidth: null, // The tableview width isDraggingColumn: false, // Is the column dragging isSelectAllChecked: this.isSelectAllChecked.bind(this), // Is the select all checked isRowSelected: this.isRowSelected.bind(this), // Is the row selected isSortedColumn: this.isSortedColumn.bind(this), // Is the column sorted isSortedAsc: this.isSortedAsc.bind(this), // Is the column sorted ascending canMoveColumn: this.canMoveColumn.bind(this), // Can move the column onSortChange: this.handleSortByColumnClick.bind(this), // Whenever the sort change onResizeColumn: this.handleResizeColumn.bind(this), // Whenever the size of a column change onReorderColumns: this.handleReorderColumns.bind(this), // Whenever the column is reordered onStartDraggingColumn: this.handleStartDraggingColumns.bind(this), // Whenever a column start to be dragged onEndDraggingColumn: this.handleEndDraggingColumns.bind(this), // Whenever a column end to be dragged onChangeColumns: this.handleChangeColumns.bind(this), // Whenever columns change }; } /** * Create DOM nodes or React elements references in order to be able to access them programmatically. */ createRefs() { this.tableviewRef = React.createRef(); } /** * ComponentDidMount * Invoked immediately after component is inserted into the tree * @return {void} */ componentDidMount() { this.prepareTableColumns(this.state.columns); } /** * Returns true if there is at least 1 column width that is not set with the default width. * @param {array} columns * @returns {boolean} */ isNotDefaultColumn(columns) { const isNotDefaultWidth = (column) => column.width !== column.defaultWidth; return columns.some(isNotDefaultWidth); } /** * Sanitize columns from props to prevent negative width due to wrong calculation * If one column width is negative, fallback to default width for all columns * @returns {*[]} */ get sanitizeColumnsFromProps() { const columns = [...this.props.columns]; const isNegativeWidth = (column) => column.width < 0; if (columns.some(isNegativeWidth)) { // Set default widths for all columns columns.forEach((column) => (column.width = column.defaultWidth)); } return columns; } /** * Prepare table columns */ prepareTableColumns() { const tableWidth = this.getTableWidth(this.state.columns); if (this.isNotDefaultColumn(this.state.columns)) { const tableviewWidth = this.tableviewRef.current.clientWidth; this.setState({ tableWidth, tableviewWidth }); } else { // Set the column this.setColumnsWidthFromActualWidth(this.state.columns); } } /** * Whenever the component has updated in terms of props */ componentDidUpdate(prevProps) { if (prevProps.columns.length > this.props.columns.length) { this.removeColumn(); } else if (prevProps.columns.length < this.props.columns.length) { this.addColumns(); } else if (prevProps.columns !== this.props.columns && !this.isNotDefaultColumn(this.state.columns)) { this.setColumnsWidthFromActualWidth(this.state.columns); } else if (prevProps.columns !== this.props.columns) { const columns = this.props.columns; this.setState({ columns }, () => this.recomputeTableWidth(columns)); } } /** * Remove a column */ removeColumn() { const filterByIdPresent = (column) => this.props.columns.some((defaultColumn) => defaultColumn.id === column.id); const columns = this.state.columns.filter(filterByIdPresent); this.setState({ columns }, () => this.recomputeTableWidth(columns)); } /** * Recompute the table width and set the state with the computed values. * @param {*} columns */ recomputeTableWidth(columns) { this.setState({ tableviewWidth: this.tableviewRef.current.clientWidth, tableWidth: this.getTableWidth(columns), }); } /** * Add columns */ addColumns() { const columns = [...this.state.columns]; const columnIndexToAdd = []; this.props.columns.forEach((column, index) => { const isColumnNonExisting = this.state.columns.every((item) => column.id !== item.id); if (isColumnNonExisting) { columnIndexToAdd.push(index); } }); columnIndexToAdd.forEach((index) => { columns.splice(index, 0, this.props.columns[index]); }); // Add the column to its default position and width this.setState({ columns }, () => this.recomputeTableWidth(columns)); } /** * Handle the change of sorter ( on property or direction ) * @param propertyName The name of the property to sort on */ async handleSortByColumnClick(propertyName) { this.props.onSortChange?.(propertyName); } /** * Handle reorder columns * @param fromIndex * @param toIndex */ handleReorderColumns(fromIndex, toIndex) { const columns = [...this.state.columns]; const column = columns.splice(fromIndex, 1)[0]; columns.splice(toIndex, 0, column); this.setState({ columns }); } /** * Handle resize column * @param {number} index The index of the column to resize * @param {number} width The width to apply * @return {Promise<void>} */ handleResizeColumn(index, width) { const columns = [...this.state.columns]; // Update the width columns[index].width = width; // Get table width const tableWidth = this.getTableWidth(columns); this.setState({ columns, tableWidth }); } /** * Handle change columns */ handleChangeColumns() { // Get the columns settings properties const columns = this.state.columns.map((column, index) => ({ id: column.id, label: column.label, width: column.width, position: index, })); this.props.onChange(columns); } /** * Get the total width for the table in order to have only one column resizing * @param {array} columns * @return {number} */ getTableWidth(columns) { // Starting from 20 to have checkbox column width and add padding for each column displayed return ( columns.filter((c) => !c.excludeFromWidthComputation).reduce((sum, col) => sum + col.width, 0) + this.getColumnsPaddingWidth(columns) ); } /** * Get the sum of columns widths no resizable from default * @param {array} columns * @return {number} */ getColumnWidthNoResizable(columns) { return columns .filter((c) => !c.excludeFromWidthComputation && !c.resizable) .reduce((sum, col) => sum + parseFloat(col.width), 0); } /** * Get the columns padding widths * @param {array} columns * @return {number} */ getColumnsPaddingWidth(columns) { // Get the columns padding widths from the displayed columns return PADDING_SIZE * columns.filter((c) => !c.excludeFromWidthComputation).length + PADDING_FIRST_COLUMN_ADDED; } /** * Set the columns width based on actual width of the tableview width to maintain the same proportionality * @param columns */ setColumnsWidthFromActualWidth(columns) { const actualWidth = this.getTableWidth(columns); const tableviewWidth = this.tableviewRef.current.clientWidth; // Fixed width not to be taken into account const fixedWidth = this.getColumnWidthNoResizable(columns) + this.getColumnsPaddingWidth(columns); // Prevent wrong calculation if actual width or tableview width are null or negative if (actualWidth > fixedWidth && tableviewWidth > fixedWidth) { // Subtract all constant widths that do not change with screen width const columnsResizableWidth = actualWidth - fixedWidth; // Calculate the ratio between two widths const ratio = (tableviewWidth - fixedWidth) / columnsResizableWidth; // Scale the widths with the ratio columns.forEach((column) => { if (column.resizable) { // rounding down avoids a slight shift on the left of the grid that happens sometimes and a scrolling column.width = Math.floor(column.width * ratio); } }); // Get the table width from all columns const tableWidth = this.getTableWidth(columns); this.setState({ columns, tableWidth, tableviewWidth }); } } /** * Returns true if the given item is selected * @param rowId A row id */ isRowSelected(rowId) { return this.props.selectedRowsIds?.some((selectedRow) => rowId === selectedRow); } /** * Is select all checked */ isSelectAllChecked() { return this.props.rows.length === this.props.selectedRowsIds?.length; } /** * Check if the grid is sorted for a given column * @param columnName The column name */ isSortedColumn(columnName) { return this.props.sorter.propertyName === columnName; } /** * Check if the sort is ascendant. * @returns {boolean} */ isSortedAsc() { return this.props.sorter.asc; } /** * If column can be moved * @param columnIndex * @param moveX * @return {boolean} */ canMoveColumn(columnIndex, moveX) { const columnMovable = (column) => column.draggable; const findLastIndex = (lastColumnIndex, column, index) => (columnMovable(column) ? index : lastColumnIndex); const canBeMovedBefore = columnIndex > this.state.columns.findIndex(columnMovable) && moveX < 0; const canBeMovedAfter = columnIndex < this.state.columns.reduce(findLastIndex, -1) && moveX > 0; return canBeMovedBefore || canBeMovedAfter; } /** * On start dragging column */ handleStartDraggingColumns() { this.setState({ isDraggingColumn: true }); } /** * On end dragging column */ handleEndDraggingColumns() { this.setState({ isDraggingColumn: false }); } /** * Render the component * @return {JSX} */ render() { return ( <TableContext.Provider value={this.state}> <div ref={this.tableviewRef} className={`tableview ${this.props.rowsSetting?.height ? this.props.rowsSetting.height : ROW_SETTING_HEIGHT_COMPACT}`} > {this.props.children} </div> </TableContext.Provider> ); } } TableContextProvider.propTypes = { columns: PropTypes.array.isRequired, // The columns to display rows: PropTypes.array.isRequired, // The rows to display rowsSetting: PropTypes.object, // The rows setting to adapt display sorter: PropTypes.object, // The sorter object containing the property name and the direction selectedRowsIds: PropTypes.array, // The selected row ids onSortChange: PropTypes.func, // The on sort property onChange: PropTypes.func, // The on change property children: PropTypes.any, // The children }; /** * Resource Workspace Context Consumer HOC * @param WrappedComponent */ export function withTable(WrappedComponent) { return class withTable extends React.Component { render() { return ( <TableContext.Consumer> {(tableContext) => <WrappedComponent tableContext={tableContext} {...this.props} />} </TableContext.Consumer> ); } }; }