in packages/boxed-expression-component/src/table/BeeTable/BeeTableContextMenuHandler.tsx [88:590]
export function BeeTableContextMenuHandler({
tableRef,
operationConfig,
allowedOperations,
reactTableInstance,
onRowAdded,
onRowDuplicated,
onRowDeleted,
onRowReset,
onColumnAdded,
onColumnDeleted,
isReadOnly,
}: BeeTableContextMenuHandlerProps) {
const { i18n } = useBoxedExpressionEditorI18n();
const { setCurrentlyOpenContextMenu } = useBoxedExpressionEditor();
const [menuDrilledIn, setMenuDrilledIn] = useState<string[]>([]);
const [drillDownPath, setDrillDownPath] = useState<string[]>([]);
const [menuHeights, setMenuHeights] = useState<{ [key: string]: number }>({});
const [direction, setDirection] = useState(InsertRowColumnsDirection.AboveOrRight);
const [insertMultipleRowColumnsValue, setInsertMultipleRowColumnsValue] = React.useState<number>(
DEFAULT_MULTIPLE_ROWS_COLUMNS_INSERTION
);
const { activeCell, selectionStart, selectionEnd } = useBeeTableSelection();
const menuId = "menu-" + activeCell?.columnIndex + "-" + activeCell?.rowIndex;
const [lastActiveMenu, setLastActiveMenu] = useState("");
const [lastRootMenuId, setLastRootMenuId] = useState(menuId);
const activeMenuId = useMemo(() => {
if (menuId !== lastRootMenuId) {
return menuId;
} else {
return lastActiveMenu;
}
}, [lastActiveMenu, lastRootMenuId, menuId]);
const rootMenuId = useMemo(() => {
if (menuId !== lastRootMenuId) {
return menuId;
}
return lastRootMenuId;
}, [lastRootMenuId, menuId]);
useEffect(() => {
setLastRootMenuId(menuId);
}, [menuId]);
useEffect(() => {
// If menuId changes it means that user clicked in another cell, so we have to close the currently open
// context menu in order to force it to be reopened with the correct options for the new cell
setCurrentlyOpenContextMenu(undefined);
}, [menuId, setCurrentlyOpenContextMenu]);
const drillIn = useCallback((_event, fromMenuId, toMenuId, pathId) => {
setMenuDrilledIn((prev) => [...prev, fromMenuId]);
setDrillDownPath((prev) => [...prev, pathId]);
setLastActiveMenu(toMenuId);
}, []);
const drillOut = useCallback((_event, toMenuId) => {
setMenuDrilledIn((prev) => prev.slice(0, prev.length - 1));
setDrillDownPath((prev) => prev.slice(0, prev.length - 1));
setLastActiveMenu(toMenuId);
}, []);
const setMenuHeight = useCallback(
(menuId: string, height: number) => {
setMenuHeights((prev) => {
if (prev[menuId] === undefined || (menuId !== rootMenuId && prev[menuId] !== height)) {
return { ...prev, [menuId]: height };
}
return prev;
});
},
[rootMenuId]
);
const selection: BeeTableSelection = useMemo(() => {
return {
active: activeCell,
selectionStart: selectionStart,
selectionEnd: selectionEnd,
};
}, [activeCell, selectionStart, selectionEnd]);
const { copy, cut, paste, erase } = useBeeTableSelectionDispatch();
const columns = useMemo(() => {
if (!activeCell) {
return undefined;
}
const rowIndex = activeCell.rowIndex;
return rowIndex < 0 // Header cells to be read from headerGroups
? _.nth(reactTableInstance.headerGroups, rowIndex)?.headers
: reactTableInstance.allColumns;
}, [activeCell, reactTableInstance.allColumns, reactTableInstance.headerGroups]);
const column = useMemo(() => {
if (!activeCell) {
return undefined;
}
const columnIndex = activeCell.columnIndex;
const rowIndex = activeCell.rowIndex;
if (rowIndex < 0) {
// column index for rows with index < -1 is equal to count of cells on given row
// so for the example below, 'output' column index is 1
// +-----+--------+--------+------------------------+
// | | | | output | <- rowIndex: -2
// | # | in-1 | in-2 +----------+-------------+
// | | | | out-1 | out-2 | <- rowIndex: -1
// +-----+--------+--------+----------+-------------+
//
// See the same principle in: src/table/BeeTable/BeeTable.tsx#getColumnCount
const nonPlaceholderColumns = columns?.filter((col) => !col?.placeholderOf);
if (nonPlaceholderColumns) {
return nonPlaceholderColumns[columnIndex];
} else {
console.error(`No column found at [${rowIndex}, ${columnIndex}]`);
}
} else {
return columns?.[columnIndex];
}
}, [activeCell, columns]);
const operationLabel = useCallback(
(operation: BeeTableOperation) => {
switch (operation) {
case BeeTableOperation.ColumnInsertLeft:
return i18n.columnOperations.insertLeft;
case BeeTableOperation.ColumnInsertRight:
return i18n.columnOperations.insertRight;
case BeeTableOperation.ColumnInsertN:
return i18n.insert;
case BeeTableOperation.ColumnDelete:
return i18n.columnOperations.delete;
case BeeTableOperation.RowInsertAbove:
return i18n.rowOperations.insertAbove;
case BeeTableOperation.RowInsertBelow:
return i18n.rowOperations.insertBelow;
case BeeTableOperation.RowInsertN:
return i18n.insert;
case BeeTableOperation.RowDelete:
return i18n.rowOperations.delete;
case BeeTableOperation.RowReset:
return i18n.rowOperations.reset;
case BeeTableOperation.RowDuplicate:
return i18n.rowOperations.duplicate;
case BeeTableOperation.SelectionCopy:
return i18n.terms.copy;
case BeeTableOperation.SelectionCut:
return i18n.terms.cut;
case BeeTableOperation.SelectionPaste:
return i18n.terms.paste;
case BeeTableOperation.SelectionReset:
return i18n.terms.reset;
default:
assertUnreachable(operation);
}
},
[i18n]
);
const operationGroups = useMemo(() => {
if (!activeCell) {
return [];
}
if (isReadOnly) {
const operationGroup: BeeTableOperationGroup = {
group: "",
items: [
{
name: operationLabel(BeeTableOperation.SelectionCopy),
type: BeeTableOperation.SelectionCopy,
},
],
};
return [operationGroup];
}
if (_.isArray(operationConfig)) {
return operationConfig;
}
return (operationConfig ?? {})[column?.groupType || ""];
}, [activeCell, column?.groupType, isReadOnly, operationConfig, operationLabel]);
const allOperations = useMemo(() => {
return operationGroups.flatMap(({ items }) => items);
}, [operationGroups]);
const operationIcon = useCallback((operation: BeeTableOperation) => {
switch (operation) {
case BeeTableOperation.ColumnInsertLeft:
return <PlusIcon />;
case BeeTableOperation.ColumnInsertRight:
return <PlusIcon />;
case BeeTableOperation.ColumnInsertN:
return <PlusIcon />;
case BeeTableOperation.ColumnDelete:
return <TrashIcon />;
case BeeTableOperation.RowInsertAbove:
return <PlusIcon />;
case BeeTableOperation.RowInsertBelow:
return <PlusIcon />;
case BeeTableOperation.RowInsertN:
return <PlusIcon />;
case BeeTableOperation.RowDelete:
return <TrashIcon />;
case BeeTableOperation.RowReset:
return <CompressIcon />;
case BeeTableOperation.RowDuplicate:
return <BlueprintIcon />;
case BeeTableOperation.SelectionCopy:
return <CopyIcon />;
case BeeTableOperation.SelectionCut:
return <CutIcon />;
case BeeTableOperation.SelectionPaste:
return <PasteIcon />;
case BeeTableOperation.SelectionReset:
return <CompressIcon />;
default:
assertUnreachable(operation);
}
}, []);
const handleOperation = useCallback(
(operation: BeeTableOperation | undefined | null) => {
if (operation === undefined || operation === null) {
return;
}
if (!activeCell) {
return [];
}
const rowIndex = activeCell.rowIndex;
const columnIndex = activeCell.columnIndex;
switch (operation) {
case BeeTableOperation.ColumnInsertLeft:
onColumnAdded?.({
beforeIndex: columnIndex - 1,
currentIndex: columnIndex,
groupType: column?.groupType,
columnsCount: 1,
insertDirection: InsertRowColumnsDirection.BelowOrLeft,
});
console.debug(`Insert column left to ${columnIndex}`);
break;
case BeeTableOperation.ColumnInsertRight:
onColumnAdded?.({
beforeIndex: columnIndex,
currentIndex: columnIndex,
groupType: column?.groupType,
columnsCount: 1,
insertDirection: InsertRowColumnsDirection.AboveOrRight,
});
console.debug(`Insert column right to ${columnIndex}`);
break;
case BeeTableOperation.ColumnInsertN:
if (direction === InsertRowColumnsDirection.AboveOrRight) {
onColumnAdded?.({
beforeIndex: columnIndex,
currentIndex: columnIndex,
groupType: column?.groupType,
columnsCount: insertMultipleRowColumnsValue,
insertDirection: InsertRowColumnsDirection.AboveOrRight,
});
} else {
onColumnAdded?.({
beforeIndex: columnIndex - 1,
currentIndex: columnIndex,
groupType: column?.groupType,
columnsCount: insertMultipleRowColumnsValue,
insertDirection: InsertRowColumnsDirection.BelowOrLeft,
});
}
console.debug(`Insert n columns to ${columnIndex}`);
break;
case BeeTableOperation.ColumnDelete:
onColumnDeleted?.({
columnIndex: columnIndex - 1,
groupType: column?.groupType,
});
console.debug(`Delete column ${columnIndex}`);
break;
case BeeTableOperation.RowInsertAbove:
onRowAdded?.({
beforeIndex: rowIndex,
rowsCount: 1,
insertDirection: InsertRowColumnsDirection.AboveOrRight,
});
console.debug(`Insert row above to ${rowIndex}`);
break;
case BeeTableOperation.RowInsertBelow:
onRowAdded?.({
beforeIndex: rowIndex + 1,
rowsCount: 1,
insertDirection: InsertRowColumnsDirection.BelowOrLeft,
});
console.debug(`Insert row below to ${rowIndex}`);
break;
case BeeTableOperation.RowInsertN:
if (direction === InsertRowColumnsDirection.AboveOrRight) {
onRowAdded?.({
beforeIndex: rowIndex,
rowsCount: insertMultipleRowColumnsValue,
insertDirection: InsertRowColumnsDirection.AboveOrRight,
});
} else {
onRowAdded?.({
beforeIndex: rowIndex + 1,
rowsCount: insertMultipleRowColumnsValue,
insertDirection: InsertRowColumnsDirection.BelowOrLeft,
});
}
console.debug(`Insert n rows to ${columnIndex}`);
break;
case BeeTableOperation.RowDelete:
onRowDeleted?.({ rowIndex: rowIndex });
console.debug(`Delete row ${rowIndex}`);
break;
case BeeTableOperation.RowReset:
onRowReset?.({ rowIndex: rowIndex });
console.debug(`Reset row ${rowIndex}`);
break;
case BeeTableOperation.RowDuplicate:
onRowDuplicated?.({ rowIndex: rowIndex });
console.debug(`Duplicate row ${rowIndex}`);
break;
case BeeTableOperation.SelectionCopy:
copy();
console.debug(
`Copying area from: [${selectionStart?.rowIndex}, ${selectionStart?.columnIndex}] to [${selectionEnd?.rowIndex}, ${selectionEnd?.columnIndex}]`
);
break;
case BeeTableOperation.SelectionCut:
cut();
console.debug(
`Cuting area from: [${selectionStart?.rowIndex}, ${selectionStart?.columnIndex}] to [${selectionEnd?.rowIndex}, ${selectionEnd?.columnIndex}]`
);
break;
case BeeTableOperation.SelectionPaste:
paste();
console.debug(`Pasting into: [${selectionStart?.rowIndex}, ${selectionStart?.columnIndex}]`);
break;
case BeeTableOperation.SelectionReset:
erase();
console.debug(
`Reseting area from: [${selectionStart?.rowIndex}, ${selectionStart?.columnIndex}] to [${selectionEnd?.rowIndex}, ${selectionEnd?.columnIndex}]`
);
break;
default:
assertUnreachable(operation);
}
setCurrentlyOpenContextMenu(undefined);
},
[
activeCell,
selectionStart,
selectionEnd,
setCurrentlyOpenContextMenu,
onColumnAdded,
column?.groupType,
onColumnDeleted,
onRowAdded,
onRowDeleted,
onRowReset,
onRowDuplicated,
copy,
cut,
paste,
erase,
direction,
insertMultipleRowColumnsValue,
]
);
const onMinus = useCallback(() => {
const newValue = (insertMultipleRowColumnsValue || 0) - 1;
setInsertMultipleRowColumnsValue(newValue);
}, [insertMultipleRowColumnsValue]);
const onChange = useCallback((event: React.FormEvent<HTMLInputElement>) => {
const value = (event.target as HTMLInputElement).value;
const intValue = Math.abs(parseInt(value));
setInsertMultipleRowColumnsValue(intValue === 0 ? 1 : Math.min(intValue, MAXIMUM_ROWS_COLUMNS_PER_INSERTION));
}, []);
const onPlus = useCallback(() => {
const newValue = (insertMultipleRowColumnsValue || 0) + 1;
setInsertMultipleRowColumnsValue(newValue);
}, [insertMultipleRowColumnsValue]);
const insertRowColumnsNumberInput = useMemo(() => {
return (
<NumberInput
value={insertMultipleRowColumnsValue}
onMinus={onMinus}
onChange={onChange}
onPlus={onPlus}
inputName="input"
inputAriaLabel="number input"
minusBtnAriaLabel="minus"
plusBtnAriaLabel="plus"
// allowEmptyInput={false}
min={1}
max={MAXIMUM_ROWS_COLUMNS_PER_INSERTION}
style={{ textAlign: "center" }}
onDoubleClick={(e) => e.stopPropagation()}
/>
);
}, [insertMultipleRowColumnsValue, onMinus, onChange, onPlus]);
const contextMenuContainer = React.createRef<HTMLDivElement>();
const { xPos, yPos, isOpen } = useCustomContextMenuHandler(tableRef);
const resetDrillDownMenu = useCallback(() => {
setMenuDrilledIn([]);
setDrillDownPath([]);
setMenuHeights({});
setLastActiveMenu(rootMenuId);
setInsertMultipleRowColumnsValue(DEFAULT_MULTIPLE_ROWS_COLUMNS_INSERTION);
}, [rootMenuId]);
useEffect(() => {
if (!isOpen) {
resetDrillDownMenu();
}
}, [isOpen, resetDrillDownMenu]);
const style = useMemo(() => {
return {
top: yPos + "px",
left: xPos + "px",
};
}, [xPos, yPos]);
useLayoutEffect(() => {
if (contextMenuContainer.current) {
const bounds = contextMenuContainer.current.getBoundingClientRect();
let contextMenuHeight = menuHeights[activeMenuId];
const availableHeight = document.documentElement.clientHeight;
if (contextMenuHeight + yPos >= availableHeight) {
const offset = contextMenuHeight + yPos - availableHeight;
contextMenuHeight = contextMenuHeight - offset;
contextMenuContainer.current.style.height = contextMenuHeight + "px";
contextMenuContainer.current.style.overflowY = "scroll";
} else {
contextMenuContainer.current.style.overflowY = "visible";
}
if (contextMenuHeight <= availableHeight && contextMenuHeight + yPos > availableHeight) {
const offset = contextMenuHeight + yPos - availableHeight;
contextMenuContainer.current.style.top = yPos - offset + "px";
contextMenuContainer.current.style.left = xPos + 2 + "px";
}
const contextMenuWidth = bounds.width;
const availableWidth = document.documentElement.clientWidth;
if (contextMenuWidth <= availableWidth && contextMenuWidth + xPos > availableWidth) {
const offset = contextMenuWidth + xPos - availableWidth;
contextMenuContainer.current.style.left = xPos - offset - 2 + "px";
}
}
});
const allowedOperationsForSelection = useMemo(() => {
return allowedOperations({
selection,
column,
columns,
});
}, [allowedOperations, selection, column, columns]);
const hasAllowedOperations = useMemo(() => {
return allOperations.some((operation) => allowedOperationsForSelection.includes(operation.type));
}, [allOperations, allowedOperationsForSelection]);
let countGroupsWithAllowedOperations = 0;
function toggleInsertDirection() {
setDirection(
direction === InsertRowColumnsDirection.AboveOrRight
? InsertRowColumnsDirection.BelowOrLeft
: InsertRowColumnsDirection.AboveOrRight
);
}
function createDrillDownMenu(group: string, operation: BeeTableOperation) {
return (
<DrilldownMenu id={"insertNColumnsMenu" + operation.toString()}>
<MenuItem direction="up" onClick={(e) => e.stopPropagation()}>
Back
</MenuItem>
<Divider />
<MenuGroup label={group}>
<Flex direction={{ default: "column" }} style={{ padding: "16px" }}>
<Flex direction={{ default: "column" }} width={"300px"}>
<FlexItem onClick={(event) => event.stopPropagation()}>{insertRowColumnsNumberInput}</FlexItem>