@carbon/ibm-products
Version:
Carbon for IBM Products
548 lines (546 loc) • 19.7 kB
JavaScript
/**
* 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.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_index = require("../../node_modules/classnames/index.js");
const require_settings = require("../../settings.js");
const require_usePreviousValue = require("../../global/js/hooks/usePreviousValue.js");
const require_props_helper = require("../../global/js/utils/props-helper.js");
const require_deepCloneObject = require("../../global/js/utils/deepCloneObject.js");
const require_removeCellSelections = require("./utils/removeCellSelections.js");
const require_useSpreadsheetMouseUp = require("./hooks/useSpreadsheetMouseUp.js");
const require_createCellSelectionArea = require("./utils/createCellSelectionArea.js");
const require_checkActiveHeaderCell = require("./utils/checkActiveHeaderCell.js");
const require_checkSelectedHeaderCell = require("./utils/checkSelectedHeaderCell.js");
const require_getSpreadsheetWidth = require("./utils/getSpreadsheetWidth.js");
const require_commonEventHandlers = require("./utils/commonEventHandlers.js");
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let _carbon_layout = require("@carbon/layout");
let react_window = require("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__ */ require_runtime.__toESM(require_index.default);
const blockClass = `${require_settings.pkg.prefix}--data-spreadsheet`;
const DataSpreadsheetBody = (0, react.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] = (0, react.useState)(false);
const contentScrollRef = (0, react.useRef)(void 0);
const previousState = require_usePreviousValue.usePreviousValue({
selectionAreaData,
clickAndHoldActive,
rowHeight: defaultColumn?.rowHeight
}) || {};
(0, react.useEffect)(() => {
ref?.current.style.setProperty(`--${blockClass}--total-width`, (0, _carbon_layout.px)((totalColumnsWidth || 0) + (scrollBarSize || 0)));
}, [
ref,
scrollBarSize,
totalColumnsWidth
]);
(0, 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
]);
(0, react.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 [...require_deepCloneObject.deepCloneObject(prev).filter((item) => item.selectionId !== area.matcher), selectionData];
});
}
if (!area.areaCreated && area.point1 && area.point2 && area.matcher) require_createCellSelectionArea.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;
};
require_useSpreadsheetMouseUp.useSpreadsheetMouseUp({
currentMatcher,
setClickAndHoldActive,
setSelectionAreas,
setValidStartingPoint,
selectedHeaderReorderActive,
setSelectedHeaderReorderActive,
validStartingPoint,
ref,
setHeaderCellHoldActive,
setColumnOrder,
visibleColumns,
setActiveCellCoordinates,
rows,
activeCellCoordinates,
defaultColumn,
selectionAreas
});
(0, 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 = (0, _carbon_layout.px)(newActiveCellTopPosition);
require_removeCellSelections.removeCellSelections({
matcher: void 0,
spreadsheetRef: ref
});
selectionAreas?.map((area) => {
if (!area.areaCreated && area.point1 && area.point2 && area.matcher) return require_createCellSelectionArea.createCellSelectionArea({
ref,
area,
blockClass,
defaultColumn,
selectionAreas,
setSelectionAreas,
setActiveCellInsideSelectionArea,
visibleColumns
});
});
}
}
}, [
defaultColumn,
ref,
activeCellCoordinates,
previousState?.rowHeight,
selectionAreas,
setActiveCellInsideSelectionArea,
setSelectionAreas,
visibleColumns,
hasCustomRowHeader
]);
const removeDuplicateSelections = (0, 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}`;
if (uniqueAttrArray.indexOf(uniqueAttrStr) == -1) uniqueAttrArray.push(uniqueAttrStr);
else {
selectorEl.remove();
removedSelectionAreaMatcherArr.push(selectorEl.getAttribute("data-matcher-id"));
}
});
if (removedSelectionAreaMatcherArr.length) {
setSelectionAreas?.((prev) => {
return require_deepCloneObject.deepCloneObject(prev).filter((item) => !removedSelectionAreaMatcherArr.includes(item.matcher));
});
setSelectionAreaData?.((prev) => {
return require_deepCloneObject.deepCloneObject(prev).filter((item) => !removedSelectionAreaMatcherArr.includes(item.selectionId));
});
}
}, [
ref,
setSelectionAreas,
setSelectionAreaData
]);
(0, react.useEffect)(() => {
removeDuplicateSelections();
}, [selectionAreas, removeDuplicateSelections]);
const handleBodyCellClickEvent = (0, react.useCallback)((cell, columnIndex) => {
return (event) => {
require_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;
};
(0, react.useEffect)(() => {
contentScrollRef.current.addEventListener("scroll", () => handleBodyScroll());
const contentScrollElementRef = contentScrollRef.current || new HTMLElement();
return () => {
contentScrollElementRef.removeEventListener("scroll", handleBodyScroll);
};
});
const handleBodyCellHoverEvent = (0, react.useCallback)((cell, columnIndex) => {
return () => {
require_commonEventHandlers.handleBodyCellHover(cell, columnIndex, clickAndHoldActive, currentMatcher, setSelectionAreas);
};
}, [
clickAndHoldActive,
currentMatcher,
setSelectionAreas
]);
const handleRowHeaderClickEvent = (0, react.useCallback)((index) => {
return (event) => {
require_commonEventHandlers.handleRowHeaderClick(index, event, columns, ref, setSelectionAreas, setCurrentMatcher, setActiveCellCoordinates, activeCellCoordinates, rows, setSelectionAreaData);
};
}, [
columns,
ref,
setSelectionAreas,
setCurrentMatcher,
setActiveCellCoordinates,
activeCellCoordinates,
rows,
setSelectionAreaData
]);
(0, 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 /* @__PURE__ */ react.default.createElement("div", null);
};
const RenderRow = (0, react.useCallback)(({ index, style }) => {
const row = rows?.[index];
if (rows && rows.length) {
prepareRow?.(row);
const rowProps = require_props_helper.prepareProps(row.getRowProps({ style }), "key");
return /* @__PURE__ */ react.default.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.default.createElement("div", {
role: "rowheader",
className: `${blockClass}__td-th--cell-container`
}, /* @__PURE__ */ react.default.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 || require_checkActiveHeaderCell.checkActiveHeaderCell(index, selectionAreas, "row")),
[`${blockClass}__td-th--selected-header`]: !hasCustomRowHeader && require_checkSelectedHeaderCell.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 = require_props_helper.prepareProps(cell.getCellProps(), "key");
return /* @__PURE__ */ react.default.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.default.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 = (0, react.useRef)(void 0);
return /* @__PURE__ */ react.default.createElement("div", {
ref: spreadsheetBodyRef,
className: (0, import_classnames.default)(`${blockClass}__body--container`),
...getTableBodyProps?.()
}, /* @__PURE__ */ react.default.createElement(react_window.FixedSizeList, {
className: (0, import_classnames.default)(`${blockClass}__list--container`, `${blockClass}__list--container--${id}`),
height: 400,
itemCount: rows?.length || defaultEmptyRowCount,
itemSize: defaultColumn?.rowHeight,
width: require_getSpreadsheetWidth.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: prop_types.default.shape({
row: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number]),
column: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number])
}),
/**
*This is the ref of the button input, which is the active cell element
*/
/**@ts-ignore */
activeCellRef: prop_types.default.object,
/**
* Is the user clicking and holding in the data spreadsheet body
*/
clickAndHoldActive: prop_types.default.bool,
/**
* All of the spreadsheet columns
*/
columns: prop_types.default.array,
/**
* This represents the id of the current cell selection area
*/
currentMatcher: prop_types.default.string,
/**
* Default spreadsheet sizing values
*/
/**@ts-ignore */
defaultColumn: prop_types.default.shape({
rowHeight: prop_types.default.number,
rowHeaderWidth: prop_types.default.number,
width: prop_types.default.number
}),
/**
* Sets the number of empty rows to be created when there is no data provided
*/
defaultEmptyRowCount: prop_types.default.number,
/**
* Function to set table body prop values
*/
/**@ts-ignore */
getTableBodyProps: prop_types.default.func,
/**
* Check if spreadsheet is using custom row header component attached
*/
hasCustomRowHeader: prop_types.default.bool,
/**
* Headers provided from useTable hook
*/
headerGroups: prop_types.default.arrayOf(prop_types.default.object),
/**
* The spreadsheet id
*/
id: prop_types.default.oneOfType([prop_types.default.number, prop_types.default.string]),
/**
* The event handler that is called when the active cell changes
*/
onActiveCellChange: prop_types.default.func,
/**
* The event handler that is called to set the rows for the empty spreadsheet
*/
onDataUpdate: prop_types.default.func,
/**
* The event handler that is called when the selection areas change
*/
onSelectionAreaChange: prop_types.default.func,
/**
* Prepare row function from react-table
*/
prepareRow: prop_types.default.func,
/**
* Component next to numbering rows
*/
renderRowHeader: prop_types.default.func,
/**
* Component next to numbering rows
*/
renderRowHeaderDirection: prop_types.default.string,
/**
* All of the spreadsheet row data
*/
rows: prop_types.default.arrayOf(prop_types.default.object),
/**
* The scrollbar width
*/
scrollBarSize: prop_types.default.number,
/**
* Header reordering is active
*/
selectedHeaderReorderActive: prop_types.default.bool,
/**
* Array of selection area data
*/
selectionAreaData: prop_types.default.array,
/**
* Array of selection areas
*/
selectionAreas: prop_types.default.array,
/**
* Setter fn for activeCellCoordinates state value
*/
setActiveCellCoordinates: prop_types.default.func,
/**
* Setter fn for active cell inside of selection area
*/
setActiveCellInsideSelectionArea: prop_types.default.func,
/**
* Setter fn for clickAndHold state value
*/
setClickAndHoldActive: prop_types.default.func,
/**
* Setter fn for column ordering, provided from react-table
*/
setColumnOrder: prop_types.default.func,
/**
* Setter fn for containerHasFocus state value
*/
setContainerHasFocus: prop_types.default.func,
/**
* Set current columns after drag drop
*/
setCurrentColumns: prop_types.default.func,
/**
* Setter fn for currentMatcher state value
*/
setCurrentMatcher: prop_types.default.func,
/**
* Setter fn for header cell hold active value
*/
setHeaderCellHoldActive: prop_types.default.func,
/**
* Set header reordering active or not
*/
setSelectedHeaderReorderActive: prop_types.default.func,
/**
* Setter fn for selectionAreaData state value
*/
setSelectionAreaData: prop_types.default.func,
/**
* Setter fn for selectionAreas state value
*/
setSelectionAreas: prop_types.default.func,
/**
* The total columns width
*/
totalColumnsWidth: prop_types.default.number,
/**
* The total number of columns to be initially visible, additional columns will be rendered and
* visible via horizontal scrollbar
*/
totalVisibleColumns: prop_types.default.number,
/**
* Prop from react-table used to reorder columns
*/
/**@ts-ignore */
visibleColumns: prop_types.default.array
};
//#endregion
exports.DataSpreadsheetBody = DataSpreadsheetBody;