export function BeeTableContextMenuHandler()

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>