UNPKG

@carbon/ibm-products

Version:
546 lines (544 loc) 18.4 kB
/** * Copyright IBM Corp. 2020, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ import { __toESM } from "../../_virtual/_rolldown/runtime.js"; import { require_classnames } from "../../node_modules/classnames/index.js"; import { pkg } from "../../settings.js"; import { usePreviousValue } from "../../global/js/hooks/usePreviousValue.js"; import { prepareProps } from "../../global/js/utils/props-helper.js"; import { deepCloneObject } from "../../global/js/utils/deepCloneObject.js"; import { removeCellSelections } from "./utils/removeCellSelections.js"; import { useSpreadsheetMouseUp } from "./hooks/useSpreadsheetMouseUp.js"; import { createCellSelectionArea } from "./utils/createCellSelectionArea.js"; import { checkActiveHeaderCell } from "./utils/checkActiveHeaderCell.js"; import { checkSelectedHeaderCell } from "./utils/checkSelectedHeaderCell.js"; import { getSpreadsheetWidth } from "./utils/getSpreadsheetWidth.js"; import { handleBodyCellClick, handleBodyCellHover, handleRowHeaderClick } from "./utils/commonEventHandlers.js"; import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { px } from "@carbon/layout"; import { FixedSizeList } from "react-window"; //#region src/components/DataSpreadsheet/DataSpreadsheetBody.tsx /** * Copyright IBM Corp. 2022, 2022 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ var import_classnames = /* @__PURE__ */ __toESM(require_classnames()); const blockClass = `${pkg.prefix}--data-spreadsheet`; const DataSpreadsheetBody = forwardRef(({ 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] = useState(false); const contentScrollRef = useRef(void 0); const previousState = usePreviousValue({ selectionAreaData, clickAndHoldActive, rowHeight: defaultColumn?.rowHeight }) || {}; useEffect(() => { ref?.current.style.setProperty(`--${blockClass}--total-width`, px((totalColumnsWidth || 0) + (scrollBarSize || 0))); }, [ ref, scrollBarSize, totalColumnsWidth ]); 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 ]); useEffect(() => { if (selectionAreas && selectionAreas.length) selectionAreas.map((area) => { 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) => { return [...deepCloneObject(prev).filter((item) => item.selectionId !== area.matcher), selectionData]; }); } if (!area.areaCreated && area.point1 && area.point2 && area.matcher) createCellSelectionArea({ ref, area, blockClass, defaultColumn, selectionAreas, setSelectionAreas, setActiveCellInsideSelectionArea, visibleColumns }); }); }, [ selectionAreas, setSelectionAreas, defaultColumn, onSelectionAreaChange, setSelectionAreaData, ref, activeCellCoordinates, setActiveCellInsideSelectionArea, visibleColumns, hasCustomRowHeader ]); const populateSelectionAreaCellData = ({ rowStart, rowEnd, columnStart, columnEnd }) => { 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({ currentMatcher, setClickAndHoldActive, setSelectionAreas, setValidStartingPoint, selectedHeaderReorderActive, setSelectedHeaderReorderActive, validStartingPoint, ref, setHeaderCellHoldActive, setColumnOrder, visibleColumns, setActiveCellCoordinates, rows, activeCellCoordinates, defaultColumn, selectionAreas }); 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 = px(newActiveCellTopPosition); removeCellSelections({ matcher: void 0, spreadsheetRef: ref }); selectionAreas?.map((area) => { if (!area.areaCreated && area.point1 && area.point2 && area.matcher) return createCellSelectionArea({ ref, area, blockClass, defaultColumn, selectionAreas, setSelectionAreas, setActiveCellInsideSelectionArea, visibleColumns }); }); } } }, [ defaultColumn, ref, activeCellCoordinates, previousState?.rowHeight, selectionAreas, setActiveCellInsideSelectionArea, setSelectionAreas, visibleColumns, hasCustomRowHeader ]); const removeDuplicateSelections = 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}`; if (uniqueAttrArray.indexOf(uniqueAttrStr) == -1) uniqueAttrArray.push(uniqueAttrStr); else { selectorEl.remove(); removedSelectionAreaMatcherArr.push(selectorEl.getAttribute("data-matcher-id")); } }); if (removedSelectionAreaMatcherArr.length) { setSelectionAreas?.((prev) => { return deepCloneObject(prev).filter((item) => !removedSelectionAreaMatcherArr.includes(item.matcher)); }); setSelectionAreaData?.((prev) => { return deepCloneObject(prev).filter((item) => !removedSelectionAreaMatcherArr.includes(item.selectionId)); }); } }, [ ref, setSelectionAreas, setSelectionAreaData ]); useEffect(() => { removeDuplicateSelections(); }, [selectionAreas, removeDuplicateSelections]); const handleBodyCellClickEvent = useCallback((cell, columnIndex) => { return (event) => { 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; }; useEffect(() => { contentScrollRef.current.addEventListener("scroll", () => handleBodyScroll()); const contentScrollElementRef = contentScrollRef.current || new HTMLElement(); return () => { contentScrollElementRef.removeEventListener("scroll", handleBodyScroll); }; }); const handleBodyCellHoverEvent = useCallback((cell, columnIndex) => { return () => { handleBodyCellHover(cell, columnIndex, clickAndHoldActive, currentMatcher, setSelectionAreas); }; }, [ clickAndHoldActive, currentMatcher, setSelectionAreas ]); const handleRowHeaderClickEvent = useCallback((index) => { return (event) => { handleRowHeaderClick(index, event, columns, ref, setSelectionAreas, setCurrentMatcher, setActiveCellCoordinates, activeCellCoordinates, rows, setSelectionAreaData); }; }, [ columns, ref, setSelectionAreas, setCurrentMatcher, setActiveCellCoordinates, activeCellCoordinates, rows, setSelectionAreaData ]); 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 /* @__PURE__ */ React.createElement("div", null); }; const RenderRow = useCallback(({ index, style }) => { const row = rows?.[index]; if (rows && rows.length) { prepareRow?.(row); const rowProps = prepareProps(row.getRowProps({ style }), "key"); return /* @__PURE__ */ React.createElement("div", { key: { ...row.getRowProps().key }, ...rowProps, className: (0, import_classnames.default)(`${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: (0, import_classnames.default)(`${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(index, selectionAreas, "row")), [`${blockClass}__td-th--selected-header`]: !hasCustomRowHeader && checkSelectedHeaderCell(index, selectionAreas, "row", columns) }), style: { width: defaultColumn?.rowHeaderWidth, flexDirection: hasCustomRowHeader ? renderRowHeaderDirection === "Left" ? "row-reverse" : row : void 0 } }, index + 1, hasCustomRowHeader && typeof renderRowHeader === "function" && renderRowHeader(index))), row.cells.map((cell, index) => { const cellProps = prepareProps(cell.getCellProps(), "key"); return /* @__PURE__ */ React.createElement("div", { 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: (0, import_classnames.default)(`${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 = useRef(void 0); return /* @__PURE__ */ React.createElement("div", { ref: spreadsheetBodyRef, className: (0, import_classnames.default)(`${blockClass}__body--container`), ...getTableBodyProps?.() }, /* @__PURE__ */ React.createElement(FixedSizeList, { className: (0, import_classnames.default)(`${blockClass}__list--container`, `${blockClass}__list--container--${id}`), height: 400, itemCount: rows?.length || defaultEmptyRowCount, itemSize: defaultColumn?.rowHeight, width: getSpreadsheetWidth({ headerGroup: void 0, type: void 0, scrollBarSizeValue: scrollBarSize, totalVisibleColumns, defaultColumn, totalColumnsWidth, visibleColumns }), outerRef: contentScrollRef }, rows?.length ? RenderRow : RenderEmptyRows)); }); DataSpreadsheetBody.propTypes = { /** * Object containing the active cell coordinates */ /**@ts-ignore */ activeCellCoordinates: PropTypes.shape({ row: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), column: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }), /** *This is the ref of the button input, which is the active cell element */ /**@ts-ignore */ activeCellRef: PropTypes.object, /** * Is the user clicking and holding in the data spreadsheet body */ clickAndHoldActive: PropTypes.bool, /** * All of the spreadsheet columns */ columns: PropTypes.array, /** * This represents the id of the current cell selection area */ currentMatcher: PropTypes.string, /** * Default spreadsheet sizing values */ /**@ts-ignore */ defaultColumn: PropTypes.shape({ rowHeight: PropTypes.number, rowHeaderWidth: PropTypes.number, width: PropTypes.number }), /** * Sets the number of empty rows to be created when there is no data provided */ defaultEmptyRowCount: PropTypes.number, /** * Function to set table body prop values */ /**@ts-ignore */ getTableBodyProps: PropTypes.func, /** * Check if spreadsheet is using custom row header component attached */ hasCustomRowHeader: PropTypes.bool, /** * Headers provided from useTable hook */ headerGroups: PropTypes.arrayOf(PropTypes.object), /** * The spreadsheet id */ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** * The event handler that is called when the active cell changes */ onActiveCellChange: PropTypes.func, /** * The event handler that is called to set the rows for the empty spreadsheet */ onDataUpdate: PropTypes.func, /** * The event handler that is called when the selection areas change */ onSelectionAreaChange: PropTypes.func, /** * Prepare row function from react-table */ prepareRow: PropTypes.func, /** * Component next to numbering rows */ renderRowHeader: PropTypes.func, /** * Component next to numbering rows */ renderRowHeaderDirection: PropTypes.string, /** * All of the spreadsheet row data */ rows: PropTypes.arrayOf(PropTypes.object), /** * The scrollbar width */ scrollBarSize: PropTypes.number, /** * Header reordering is active */ selectedHeaderReorderActive: PropTypes.bool, /** * Array of selection area data */ selectionAreaData: PropTypes.array, /** * Array of selection areas */ selectionAreas: PropTypes.array, /** * Setter fn for activeCellCoordinates state value */ setActiveCellCoordinates: PropTypes.func, /** * Setter fn for active cell inside of selection area */ setActiveCellInsideSelectionArea: PropTypes.func, /** * Setter fn for clickAndHold state value */ setClickAndHoldActive: PropTypes.func, /** * Setter fn for column ordering, provided from react-table */ setColumnOrder: PropTypes.func, /** * Setter fn for containerHasFocus state value */ setContainerHasFocus: PropTypes.func, /** * Set current columns after drag drop */ setCurrentColumns: PropTypes.func, /** * Setter fn for currentMatcher state value */ setCurrentMatcher: PropTypes.func, /** * Setter fn for header cell hold active value */ setHeaderCellHoldActive: PropTypes.func, /** * Set header reordering active or not */ setSelectedHeaderReorderActive: PropTypes.func, /** * Setter fn for selectionAreaData state value */ setSelectionAreaData: PropTypes.func, /** * Setter fn for selectionAreas state value */ setSelectionAreas: PropTypes.func, /** * The total columns width */ totalColumnsWidth: PropTypes.number, /** * The total number of columns to be initially visible, additional columns will be rendered and * visible via horizontal scrollbar */ totalVisibleColumns: PropTypes.number, /** * Prop from react-table used to reorder columns */ /**@ts-ignore */ visibleColumns: PropTypes.array }; //#endregion export { DataSpreadsheetBody };