UNPKG

@carbon/ibm-products

Version:

Carbon for IBM Products

564 lines (548 loc) 21.5 kB
/** * Copyright IBM Corp. 2020, 2025 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var index = require('../../_virtual/index.js'); var reactWindow = require('react-window'); var cx = require('classnames'); var layout = require('@carbon/layout'); var settings = require('../../settings.js'); var deepCloneObject = require('../../global/js/utils/deepCloneObject.js'); var removeCellSelections = require('./utils/removeCellSelections.js'); var createCellSelectionArea = require('./utils/createCellSelectionArea.js'); var checkActiveHeaderCell = require('./utils/checkActiveHeaderCell.js'); var checkSelectedHeaderCell = require('./utils/checkSelectedHeaderCell.js'); var getSpreadsheetWidth = require('./utils/getSpreadsheetWidth.js'); var commonEventHandlers = require('./utils/commonEventHandlers.js'); var propsHelper = require('../../global/js/utils/props-helper.js'); var usePreviousValue = require('../../global/js/hooks/usePreviousValue.js'); var useSpreadsheetMouseUp = require('./hooks/useSpreadsheetMouseUp.js'); var _div; const blockClass = `${settings.pkg.prefix}--data-spreadsheet`; const DataSpreadsheetBody = /*#__PURE__*/React.forwardRef((_ref, ref) => { let { activeCellRef, columns, activeCellCoordinates, defaultColumn, defaultEmptyRowCount, getTableBodyProps, headerGroups, setCurrentColumns, id, onDataUpdate, renderRowHeader, renderRowHeaderDirection, hasCustomRowHeader, prepareRow, rows, selectionAreaData, setSelectionAreaData, setActiveCellCoordinates, selectedHeaderReorderActive, setSelectedHeaderReorderActive, selectionAreas, setContainerHasFocus, setSelectionAreas, scrollBarSize, totalColumnsWidth, clickAndHoldActive, setClickAndHoldActive, currentMatcher, setCurrentMatcher, onSelectionAreaChange, setActiveCellInsideSelectionArea, totalVisibleColumns, setHeaderCellHoldActive, setColumnOrder, visibleColumns } = _ref; const [validStartingPoint, setValidStartingPoint] = React.useState(false); const contentScrollRef = React.useRef(undefined); const previousState = usePreviousValue.usePreviousValue({ selectionAreaData, clickAndHoldActive, rowHeight: defaultColumn?.rowHeight }) || {}; // Set custom css property containing the spreadsheet total width React.useEffect(() => { ref?.current.style.setProperty(`--${blockClass}--total-width`, layout.px((totalColumnsWidth || 0) + (scrollBarSize || 0))); }, [ref, scrollBarSize, totalColumnsWidth]); // Call the `onSelectionAreaChange` handler to send selection area data // back to the consumer React.useEffect(() => { if (selectionAreaData?.length) { let selectionChanged = false; if (previousState?.selectionAreaData?.length !== selectionAreaData?.length || selectionAreaData?.[0]?.cells.length !== previousState?.selectionAreaData?.[0]?.cells.length) { selectionChanged = true; } if (!clickAndHoldActive && previousState?.clickAndHoldActive || selectionChanged) { onSelectionAreaChange?.(selectionAreaData); } } }, [previousState?.selectionAreaData, selectionAreaData, onSelectionAreaChange, clickAndHoldActive, previousState?.clickAndHoldActive]); // Create cell selection areas based on selectionAreas array React.useEffect(() => { if (selectionAreas && selectionAreas.length) { selectionAreas.map(area => { // Setup selection area data that will be sent back to consumer via onSelectionAreaChange prop if (area.areaCreated) { const rowStart = Math.min(area.point1.row, area.point2.row); const rowEnd = Math.max(area.point1.row, area.point2.row); const columnStart = Math.min(area.point1.column, area.point2.column); const columnEnd = Math.max(area.point1.column, area.point2.column); const selectionData = { rows: { start: rowStart, end: rowEnd }, columns: { start: columnStart, end: columnEnd }, cells: populateSelectionAreaCellData({ rowStart, rowEnd, columnStart, columnEnd }), selectionId: area.matcher }; setSelectionAreaData?.(prev => { const prevValues = deepCloneObject.deepCloneObject(prev); const newAreaData = prevValues.filter(item => item.selectionId !== area.matcher); return [...newAreaData, selectionData]; }); } if (!area.areaCreated && area.point1 && area.point2 && area.matcher) { createCellSelectionArea.createCellSelectionArea({ ref, area, blockClass, defaultColumn, selectionAreas, setSelectionAreas, setActiveCellInsideSelectionArea, visibleColumns }); } return; }); } }, [selectionAreas, setSelectionAreas, defaultColumn, onSelectionAreaChange, setSelectionAreaData, ref, activeCellCoordinates, setActiveCellInsideSelectionArea, visibleColumns, hasCustomRowHeader]); const populateSelectionAreaCellData = _ref2 => { let { rowStart, rowEnd, columnStart, columnEnd } = _ref2; const cellContainer = []; for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) { for (let columnIndex = columnStart; columnIndex <= columnEnd; columnIndex++) { cellContainer.push([rowIndex, columnIndex, `${blockClass}__cell--${rowIndex}--${columnIndex}`]); } } return cellContainer; }; useSpreadsheetMouseUp.useSpreadsheetMouseUp({ currentMatcher, setClickAndHoldActive, setSelectionAreas, setValidStartingPoint, selectedHeaderReorderActive, setSelectedHeaderReorderActive, validStartingPoint, ref, setHeaderCellHoldActive, setColumnOrder, visibleColumns, setActiveCellCoordinates, rows, activeCellCoordinates, defaultColumn, selectionAreas }); // Make sure that if the cellSize prop changes, the active // cell also gets updated with the new size and new top placement // value. All of the cell selections will be updated as well React.useEffect(() => { let listContainer; let activeCellButton; if (spreadsheetBodyRef?.current) { listContainer = spreadsheetBodyRef?.current; activeCellButton = listContainer?.querySelector(`.${blockClass}__active-cell--highlight`); } if (activeCellButton && defaultColumn?.rowHeight !== previousState.rowHeight) { activeCellButton.style.height = `${defaultColumn?.rowHeight}px`; if (activeCellCoordinates) { const activeTargetElement = ref?.current.querySelector(`[data-row-index="${activeCellCoordinates.row}"][data-column-index="${activeCellCoordinates.column}"]`); const listContainer = ref?.current.querySelector(`.${blockClass}__list--container`); let newActiveCellTopPosition; if (activeTargetElement && listContainer) { newActiveCellTopPosition = activeTargetElement?.getBoundingClientRect().top - listContainer.getBoundingClientRect().top; } activeCellButton.style.top = layout.px(newActiveCellTopPosition); removeCellSelections.removeCellSelections({ matcher: undefined, spreadsheetRef: ref }); selectionAreas?.map(area => { if (!area.areaCreated && area.point1 && area.point2 && area.matcher) { return createCellSelectionArea.createCellSelectionArea({ ref, area, blockClass, defaultColumn, selectionAreas, setSelectionAreas, setActiveCellInsideSelectionArea, visibleColumns }); } }); } } }, [defaultColumn, ref, activeCellCoordinates, previousState?.rowHeight, selectionAreas, setActiveCellInsideSelectionArea, setSelectionAreas, visibleColumns, hasCustomRowHeader]); //this method will check for any duplicate selection area and remove. //same selections are those have the same height, width, top, left styles. These inline styles are being set in createCellSelection util. const removeDuplicateSelections = React.useCallback(() => { const uniqueAttrArray = [], removedSelectionAreaMatcherArr = []; ref?.current.querySelectorAll(`.${blockClass}__selection-area--element`).forEach(selectorEl => { const { top, left, height, width } = selectorEl.style; const uniqueAttrStr = `${top}${left}${height}${width}`; // eg: 20px30px70px90px if (uniqueAttrArray.indexOf(uniqueAttrStr) == -1) { uniqueAttrArray.push(uniqueAttrStr); } else { selectorEl.remove(); // this is identified as duplicate selection and hence removing. removedSelectionAreaMatcherArr.push(selectorEl.getAttribute('data-matcher-id')); } }); //clean the duplicate selectionAreaData and selectionArea if (removedSelectionAreaMatcherArr.length) { setSelectionAreas?.(prev => { const prevValues = deepCloneObject.deepCloneObject(prev); return prevValues.filter(item => !removedSelectionAreaMatcherArr.includes(item.matcher)); }); setSelectionAreaData?.(prev => { const prevValues = deepCloneObject.deepCloneObject(prev); return prevValues.filter(item => !removedSelectionAreaMatcherArr.includes(item.selectionId)); }); } }, [ref, setSelectionAreas, setSelectionAreaData]); //selectionAreas will be set when ever a selection area is made. React.useEffect(() => { removeDuplicateSelections(); }, [selectionAreas, removeDuplicateSelections]); // onClick fn for each cell in the data spreadsheet body, // adds the active cell highlight const handleBodyCellClickEvent = React.useCallback((cell, columnIndex) => { return event => { commonEventHandlers.handleBodyCellClick(cell, columnIndex, event, currentMatcher, activeCellCoordinates, selectionAreas, setActiveCellCoordinates, setSelectionAreas, setContainerHasFocus, setClickAndHoldActive, setCurrentMatcher, ref, setSelectionAreaData, setActiveCellInsideSelectionArea, activeCellRef, setValidStartingPoint); }; }, [currentMatcher, activeCellCoordinates, selectionAreas, setActiveCellCoordinates, setSelectionAreas, setContainerHasFocus, setClickAndHoldActive, setCurrentMatcher, ref, setSelectionAreaData, setActiveCellInsideSelectionArea, activeCellRef]); const handleBodyScroll = () => { const headerRowElement = ref.current.querySelector(` .${blockClass}__header--container .${blockClass}__tr`) || new HTMLDivElement(); headerRowElement.scrollLeft = contentScrollRef?.current.scrollLeft; }; React.useEffect(() => { contentScrollRef.current.addEventListener('scroll', () => handleBodyScroll()); const contentScrollElementRef = contentScrollRef.current || new HTMLElement(); return () => { contentScrollElementRef.removeEventListener('scroll', handleBodyScroll); }; }); const handleBodyCellHoverEvent = React.useCallback((cell, columnIndex) => { return () => { commonEventHandlers.handleBodyCellHover(cell, columnIndex, clickAndHoldActive, currentMatcher, setSelectionAreas); }; }, [clickAndHoldActive, currentMatcher, setSelectionAreas]); const handleRowHeaderClickEvent = React.useCallback(index => { return event => { commonEventHandlers.handleRowHeaderClick(index, event, columns, ref, setSelectionAreas, setCurrentMatcher, setActiveCellCoordinates, activeCellCoordinates, rows, setSelectionAreaData); }; }, [columns, ref, setSelectionAreas, setCurrentMatcher, setActiveCellCoordinates, activeCellCoordinates, rows, setSelectionAreaData]); // Builds the empty rows and calls `onDataUpdate` to set the new empty rows // using defaultEmptyRowCount to determine how many empty rows are created. React.useEffect(() => { if (!rows?.length) { const buildEmptyRows = () => { const emptyRowData = []; [...Array(defaultEmptyRowCount)].map(() => { const emptyCell = {}; headerGroups?.[0]?.headers.map(header => { emptyCell[header.id] = null; }); emptyRowData.push(emptyCell); }); onDataUpdate?.(emptyRowData); }; buildEmptyRows(); } if (headerGroups?.[0] && typeof setCurrentColumns === 'function') { const headers = headerGroups[0].headers; setCurrentColumns(headers); } }, [rows, headerGroups, defaultEmptyRowCount, onDataUpdate, setCurrentColumns]); const RenderEmptyRows = () => { return _div || (_div = /*#__PURE__*/React.createElement("div", null)); }; // Renders each row/cell in the spreadsheet body const RenderRow = React.useCallback(_ref3 => { let { index, style } = _ref3; const row = rows?.[index]; if (rows && rows.length) { prepareRow?.(row); const rowProps = propsHelper.prepareProps(row.getRowProps({ style }), 'key'); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ key: { ...row.getRowProps().key } }, rowProps, { className: cx(`${blockClass}__tr`), "data-row-index": index, "aria-rowindex": index + 1, "aria-owns": `${blockClass}__cell-editor-text-area` }), /*#__PURE__*/React.createElement("div", { role: "rowheader", className: `${blockClass}__td-th--cell-container` }, /*#__PURE__*/React.createElement("button", { id: `${blockClass}__cell--${index}--header`, tabIndex: -1, "data-row-index": index, "data-column-index": "header", type: "button", onClick: handleRowHeaderClickEvent(index), className: cx(`${blockClass}__td`, `${blockClass}__td-th`, `${blockClass}--interactive-cell-element`, { [`${blockClass}__td_custom`]: hasCustomRowHeader ? true : false, [`${blockClass}__td-th--active-header`]: !hasCustomRowHeader && (activeCellCoordinates?.row === index || checkActiveHeaderCell.checkActiveHeaderCell(index, selectionAreas, 'row')), [`${blockClass}__td-th--selected-header`]: !hasCustomRowHeader && checkSelectedHeaderCell.checkSelectedHeaderCell(index, selectionAreas, 'row', columns) }), style: { width: defaultColumn?.rowHeaderWidth, flexDirection: hasCustomRowHeader ? renderRowHeaderDirection === 'Left' ? 'row-reverse' : row : undefined } }, index + 1, hasCustomRowHeader && typeof renderRowHeader === 'function' && renderRowHeader(index))), row.cells.map((cell, index) => { const cellProps = propsHelper.prepareProps(cell.getCellProps(), 'key'); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ key: `cell_${index}`, "aria-colindex": index + 1 }, cellProps, { role: "gridcell", style: { ...cell.getCellProps().style, display: 'grid', minWidth: cell?.column?.width || defaultColumn?.width } }), /*#__PURE__*/React.createElement("button", { id: `${blockClass}__cell--${cell.row.index}--${index}`, tabIndex: -1, "data-row-index": cell.row.index, "data-column-index": index, className: cx(`${blockClass}__td`, `${blockClass}__body--td`, `${blockClass}--interactive-cell-element`), onMouseDown: handleBodyCellClickEvent(cell, index), onMouseOver: handleBodyCellHoverEvent(cell, index), onFocus: () => {}, type: "button" }, cell.render('Cell'))); })); } }, [prepareRow, renderRowHeader, rows, hasCustomRowHeader, activeCellCoordinates?.row, selectionAreas, handleRowHeaderClickEvent, handleBodyCellClickEvent, handleBodyCellHoverEvent, columns, defaultColumn, renderRowHeaderDirection]); const spreadsheetBodyRef = React.useRef(undefined); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ ref: spreadsheetBodyRef, className: cx(`${blockClass}__body--container`) }, getTableBodyProps?.()), /*#__PURE__*/React.createElement(reactWindow.FixedSizeList, { className: cx(`${blockClass}__list--container`, `${blockClass}__list--container--${id}`), height: 400, itemCount: rows?.length || defaultEmptyRowCount, itemSize: defaultColumn?.rowHeight, width: getSpreadsheetWidth.getSpreadsheetWidth({ headerGroup: undefined, type: undefined, scrollBarSizeValue: scrollBarSize, totalVisibleColumns, defaultColumn, totalColumnsWidth, visibleColumns }), outerRef: contentScrollRef }, rows?.length ? RenderRow : RenderEmptyRows)); }); DataSpreadsheetBody.propTypes = { /** * Object containing the active cell coordinates */ /**@ts-ignore */ activeCellCoordinates: index.default.shape({ row: index.default.oneOfType([index.default.string, index.default.number]), column: index.default.oneOfType([index.default.string, index.default.number]) }), /** *This is the ref of the button input, which is the active cell element */ /**@ts-ignore */ activeCellRef: index.default.object, /** * Is the user clicking and holding in the data spreadsheet body */ clickAndHoldActive: index.default.bool, /** * All of the spreadsheet columns */ columns: index.default.array, /** * This represents the id of the current cell selection area */ currentMatcher: index.default.string, /** * Default spreadsheet sizing values */ /**@ts-ignore */ defaultColumn: index.default.shape({ rowHeight: index.default.number, rowHeaderWidth: index.default.number, width: index.default.number }), /** * Sets the number of empty rows to be created when there is no data provided */ defaultEmptyRowCount: index.default.number, /** * Function to set table body prop values */ /**@ts-ignore */ getTableBodyProps: index.default.func, /** * Check if spreadsheet is using custom row header component attached */ hasCustomRowHeader: index.default.bool, /** * Headers provided from useTable hook */ headerGroups: index.default.arrayOf(index.default.object), /** * The spreadsheet id */ id: index.default.oneOfType([index.default.number, index.default.string]), /** * The event handler that is called when the active cell changes */ onActiveCellChange: index.default.func, /** * The event handler that is called to set the rows for the empty spreadsheet */ onDataUpdate: index.default.func, /** * The event handler that is called when the selection areas change */ onSelectionAreaChange: index.default.func, /** * Prepare row function from react-table */ prepareRow: index.default.func, /** * Component next to numbering rows */ renderRowHeader: index.default.func, /** * Component next to numbering rows */ renderRowHeaderDirection: index.default.string, /** * All of the spreadsheet row data */ rows: index.default.arrayOf(index.default.object), /** * The scrollbar width */ scrollBarSize: index.default.number, /** * Header reordering is active */ selectedHeaderReorderActive: index.default.bool, /** * Array of selection area data */ selectionAreaData: index.default.array, /** * Array of selection areas */ selectionAreas: index.default.array, /** * Setter fn for activeCellCoordinates state value */ setActiveCellCoordinates: index.default.func, /** * Setter fn for active cell inside of selection area */ setActiveCellInsideSelectionArea: index.default.func, /** * Setter fn for clickAndHold state value */ setClickAndHoldActive: index.default.func, /** * Setter fn for column ordering, provided from react-table */ setColumnOrder: index.default.func, /** * Setter fn for containerHasFocus state value */ setContainerHasFocus: index.default.func, /** * Set current columns after drag drop */ setCurrentColumns: index.default.func, /** * Setter fn for currentMatcher state value */ setCurrentMatcher: index.default.func, /** * Setter fn for header cell hold active value */ setHeaderCellHoldActive: index.default.func, /** * Set header reordering active or not */ setSelectedHeaderReorderActive: index.default.func, /** * Setter fn for selectionAreaData state value */ setSelectionAreaData: index.default.func, /** * Setter fn for selectionAreas state value */ setSelectionAreas: index.default.func, /** * The total columns width */ totalColumnsWidth: index.default.number, /** * The total number of columns to be initially visible, additional columns will be rendered and * visible via horizontal scrollbar */ totalVisibleColumns: index.default.number, /** * Prop from react-table used to reorder columns */ /**@ts-ignore */ visibleColumns: index.default.array }; exports.DataSpreadsheetBody = DataSpreadsheetBody;