export function BeeTableSelectionContextProvider()

in packages/boxed-expression-component/src/selection/BeeTableSelectionContext.tsx [226:909]


export function BeeTableSelectionContextProvider({ children }: React.PropsWithChildren<{}>) {
  const refs = React.useRef<Map<number, Map<number, Set<BeeTableCellRef>>>>(new Map());

  const [_selection, _setSelection] = useState<BeeTableSelection>(NEUTRAL_SELECTION);
  const [_currentDepth, _setCurrentDepth] = useState<{ active: number | undefined; max: number }>(
    INITIAL_CURRENT_DEPTH
  );

  const {
    isSelectionHere: isParentSelectionThere,
    activeCellForNestedTables: parentActiveCell,
    currentDepth: parentCurrentDepth,
    depth: parentDepth,
  } = useBeeTableSelection();

  const { setCurrentDepth: setParentCurrentDepth, resetSelectionAt: resetParentSelectionAt } =
    useBeeTableSelectionDispatch();
  const { containerCellCoordinates } = useBeeTableCoordinates();

  //

  const depth = parentDepth + 1;
  const activeDepth = parentCurrentDepth.active ?? _currentDepth.active;
  const activeMaxDepth = Math.max(parentCurrentDepth.max, _currentDepth.max);
  const setCurrentDepth = setParentCurrentDepth ?? _setCurrentDepth;

  const isSelectionHere = useMemo(() => {
    return coincides(parentActiveCell, containerCellCoordinates) && isParentSelectionThere;
  }, [containerCellCoordinates, isParentSelectionThere, parentActiveCell]);

  const selection = useMemo(() => {
    if (depth === activeDepth && isSelectionHere) {
      return _selection;
    }

    return NEUTRAL_SELECTION;
  }, [_selection, activeDepth, depth, isSelectionHere]);

  const selectionRef = React.useRef<BeeTableSelection>(selection);
  useEffect(() => {
    selectionRef.current = selection;
  }, [selection]);

  //
  // paste batching strategy (begin)
  //
  // This is a hack to make React batch the multiple state updates we're doing here with the calls to `setValue`.
  // Every call to `setValue` mutates the expression, so batching is essential for performance reasons.
  // This effect runs once when pasteData is truthy. Then, after running, it sets pasteData to a falsy value, which short-circuits it.
  //
  // This can be refactored to be simpler when upgrading to React 18, as batching is automatic, even outside event handlers and hooks.
  const [pasteData, setPasteData] = useState("");
  useEffect(() => {
    if (!pasteData) {
      return;
    }

    const clipboardValue = pasteData;

    if (!selectionRef.current?.selectionStart || !selectionRef.current?.selectionEnd) {
      return;
    }

    const clipboardMatrix = clipboardValue
      .split(CLIPBOARD_TO_TEXT_ROW_SEPARATOR)
      .map((r) => r.split(CLIPBOARD_COLUMN_SEPARATOR));

    const { startRow, endRow, startColumn, endColumn } = getSelectionIterationBoundaries(selectionRef.current);

    const pasteEndRow = Math.max(endRow, startRow + clipboardMatrix.length - 1);
    const pasteEndColumn = Math.max(endColumn, startColumn + clipboardMatrix[0].length - 1);

    for (let r = startRow; r <= pasteEndRow; r++) {
      for (let c = startColumn; c <= pasteEndColumn; c++) {
        refs.current
          ?.get(r)
          ?.get(c)
          ?.forEach((e) => e.setValue?.(clipboardMatrix[r - startRow]?.[c - startColumn]));
      }
    }

    _setSelection({
      active: {
        rowIndex: startRow,
        columnIndex: startColumn,
        isEditing: false,
      },
      selectionStart: {
        rowIndex: startRow,
        columnIndex: startColumn,
        isEditing: false,
      },
      selectionEnd: {
        rowIndex: pasteEndRow,
        columnIndex: pasteEndColumn,
        isEditing: false,
      },
    });

    setPasteData("");
  }, [pasteData]);

  // paste batching strategy (end)

  const value = useMemo(() => {
    return {
      activeCell: selection.active,
      selectionStart: selection.selectionStart,
      selectionEnd: selection.selectionEnd,
      activeCellForNestedTables: _selection.active,
      currentDepth: {
        active: activeDepth,
        max: activeMaxDepth,
      },
      depth,
      isSelectionHere,
    };
  }, [
    _selection.active,
    activeDepth,
    activeMaxDepth,
    depth,
    isSelectionHere,
    selection.active,
    selection.selectionEnd,
    selection.selectionStart,
  ]);

  const dispatch = useMemo<BeeTableSelectionDispatchContextType>(() => {
    return {
      setCurrentDepth: (newCurrentDepthAction) => {
        setCurrentDepth((prev) => {
          const newCurrentDepth =
            typeof newCurrentDepthAction === "function"
              ? newCurrentDepthAction(prev ?? SELECTION_MIN_ACTIVE_DEPTH)
              : newCurrentDepthAction;

          return {
            max: Math.max(SELECTION_MIN_MAX_DEPTH, newCurrentDepth.max),
            active: newCurrentDepth.active ?? SELECTION_MIN_ACTIVE_DEPTH,
          };
        });
      },
      mutateSelection: ({
        part,
        columnCount,
        rowCount,
        deltaColumns,
        deltaRows,
        isEditingActiveCell,
        keepInsideSelection,
      }) => {
        _setSelection((prev) => {
          if (!prev.active) {
            return prev;
          }

          const isExpanded = isSelectionExpanded(prev);
          const { startRow, startColumn, endRow, endColumn } = getSelectionIterationBoundaries(prev);

          const boundaries =
            isExpanded && keepInsideSelection
              ? {
                  rows: { min: startRow, max: endRow },
                  columns: { min: startColumn, max: endColumn },
                }
              : {
                  rows: { min: 0, max: rowCount - 1 },
                  columns: { min: 1, max: columnCount(prev.active.rowIndex) - 1 },
                };

          const prevCoords =
            part === SelectionPart.ActiveCell
              ? {
                  rowIndex: prev.active.rowIndex,
                  columnIndex: prev.active.columnIndex,
                }
              : part === SelectionPart.SelectionEnd
                ? {
                    rowIndex: prev.selectionEnd?.rowIndex,
                    columnIndex: prev.selectionEnd?.columnIndex,
                  }
                : part === SelectionPart.SelectionStart
                  ? {
                      rowIndex: prev.selectionStart?.rowIndex,
                      columnIndex: prev.selectionStart?.columnIndex,
                    }
                  : (() => {
                      throw new Error("Impossible case for SelectionPart");
                    })();

          const newRowIndex =
            (prevCoords.rowIndex ?? 0) < 0
              ? prevCoords.rowIndex ?? 0 // Don't move away from header cells
              : Math.min(boundaries.rows.max, Math.max(boundaries.rows.min, (prevCoords.rowIndex ?? 0) + deltaRows));

          const newColumnIndex =
            prevCoords.columnIndex === 0
              ? prevCoords.columnIndex // Don't move away from rowIndex cells
              : Math.min(
                  boundaries.columns.max,
                  Math.max(boundaries.columns.min, (prevCoords.columnIndex ?? 0) + deltaColumns)
                );

          switch (part) {
            case SelectionPart.SelectionEnd:
              return {
                ...prev,
                selectionEnd: {
                  rowIndex: newRowIndex,
                  columnIndex: newColumnIndex,
                  isEditing: prev.selectionEnd?.isEditing ?? false,
                },
              };
            case SelectionPart.SelectionStart:
              return {
                ...prev,
                selectionStart: {
                  rowIndex: newRowIndex,
                  columnIndex: newColumnIndex,
                  isEditing: prev.selectionStart?.isEditing ?? false,
                },
              };
            case SelectionPart.ActiveCell:
              if (!isExpanded || !keepInsideSelection) {
                return {
                  active: {
                    rowIndex: newRowIndex,
                    columnIndex: newColumnIndex,
                    isEditing: isEditingActiveCell,
                  },
                  selectionEnd: {
                    rowIndex: newRowIndex,
                    columnIndex: newColumnIndex,
                    isEditing: false,
                  },
                  selectionStart: {
                    rowIndex: newRowIndex,
                    columnIndex: newColumnIndex,
                    isEditing: false,
                  },
                };
              }

              // Wrap-around inside selection
              //
              // Direction: left-to-right, top-to-bottom
              //
              // ===============================================
              // Enter         --> Top-Down, LTR
              // Shift + Enter --> Bottom-Up, RTL
              // Tab           --> LTR, Top-Down
              // Shift + Tab   --> RTL, Bottom-Up
              // ===============================================

              const targetRow = prev.active.rowIndex + deltaRows;
              const targetColumn = prev.active.columnIndex + deltaColumns;

              if (targetRow > boundaries.rows.max) {
                const nextColumn = prev.active.columnIndex + 1;
                return {
                  ...prev,
                  active: {
                    rowIndex: boundaries.rows.min,
                    columnIndex: nextColumn > boundaries.columns.max ? boundaries.columns.min : nextColumn,
                    isEditing: isEditingActiveCell,
                  },
                };
              } else if (targetColumn < boundaries.columns.min) {
                const previousRow = prev.active.rowIndex - 1;
                return {
                  ...prev,
                  active: {
                    rowIndex: previousRow < boundaries.rows.min ? boundaries.rows.max : previousRow,
                    columnIndex: boundaries.columns.max,
                    isEditing: isEditingActiveCell,
                  },
                };
              } else if (targetColumn > boundaries.columns.max) {
                const nextRow = prev.active.rowIndex + 1;
                return {
                  ...prev,
                  active: {
                    rowIndex: nextRow > boundaries.rows.max ? boundaries.rows.min : nextRow,
                    columnIndex: boundaries.columns.min,
                    isEditing: isEditingActiveCell,
                  },
                };
              } else if (targetRow < boundaries.rows.min) {
                const previousColumn = prev.active.columnIndex - 1;
                return {
                  ...prev,
                  active: {
                    rowIndex: boundaries.rows.max,
                    columnIndex: previousColumn < boundaries.columns.min ? boundaries.columns.max : previousColumn,
                    isEditing: isEditingActiveCell,
                  },
                };
              } else {
                return {
                  ...prev,
                  active: {
                    rowIndex: newRowIndex,
                    columnIndex: newColumnIndex,
                    isEditing: isEditingActiveCell,
                  },
                };
              }
            default:
              assertUnreachable(part);
          }
        });
      },
      adaptSelection: ({
        atRowIndex,
        rowCountDelta,
        atColumnIndex,
        columnCountDelta,
      }: {
        atRowIndex: number;
        rowCountDelta: number;
        atColumnIndex: number;
        columnCountDelta: number;
      }) => {
        _setSelection((prev) => {
          if (!prev || !prev.active || !prev.selectionStart || !prev.selectionEnd) {
            return prev;
          }

          let moveRows = 0;
          let growRows = 0;
          let activeMoveRows = 0;

          if (atRowIndex >= 0) {
            if (atRowIndex <= prev.selectionStart.rowIndex) {
              moveRows = rowCountDelta;
            } else if (atRowIndex <= prev.selectionEnd.rowIndex) {
              growRows = rowCountDelta;
            }

            if (atRowIndex <= prev.active.rowIndex) {
              activeMoveRows = rowCountDelta;
            }
          }

          let moveColumns = 0;
          let growColumns = 0;
          let activeMoveColumns = 0;

          if (atColumnIndex >= 0) {
            if (atColumnIndex <= prev.selectionStart.columnIndex) {
              moveColumns = columnCountDelta;
            } else if (atColumnIndex <= prev.selectionEnd.columnIndex) {
              growColumns = columnCountDelta;
            }

            if (atColumnIndex <= prev.active.columnIndex) {
              activeMoveColumns = columnCountDelta;
            }
          }

          return {
            active: {
              rowIndex: prev.active.rowIndex + activeMoveRows,
              columnIndex: prev.active.columnIndex + activeMoveColumns,
              isEditing: prev.active.isEditing,
            },
            selectionStart: {
              rowIndex: prev.selectionStart.rowIndex + moveRows,
              columnIndex: prev.selectionStart.columnIndex + moveColumns,
              isEditing: prev.selectionStart.isEditing,
            },
            selectionEnd: {
              rowIndex: prev.selectionEnd.rowIndex + moveRows + growRows,
              columnIndex: prev.selectionEnd.columnIndex + moveColumns + growColumns,
              isEditing: prev.selectionEnd.isEditing,
            },
          };
        });
      },
      copy: () => {
        if (!selectionRef.current?.selectionStart || !selectionRef.current?.selectionEnd) {
          return;
        }

        const clipboardMatrix: string[][] = [];

        const { startRow, endRow, startColumn, endColumn } = getSelectionIterationBoundaries(selectionRef.current);
        for (let r = startRow; r <= endRow; r++) {
          clipboardMatrix[r - startRow] ??= [];
          for (let c = startColumn; c <= endColumn; c++) {
            clipboardMatrix[r - startRow][c - startColumn] = [...(refs.current?.get(r)?.get(c) ?? [])]
              ?.flatMap((ref) => (ref.getValue ? [ref.getValue()] : []))
              .join(""); // FIXME: What to do? Only one ref should be yielding the content. See https://github.com/apache/incubator-kie-issues/issues/170
          }
        }

        const clipboardValue = clipboardMatrix
          .map((r) => r.join(CLIPBOARD_COLUMN_SEPARATOR))
          .join(TEXT_TO_CLIPBOARD_ROW_SEPARATOR);
        navigator.clipboard.writeText(clipboardValue);
      },
      cut: () => {
        if (!selectionRef.current?.selectionStart || !selectionRef.current?.selectionEnd) {
          return;
        }

        const clipboardMatrix: string[][] = [];

        const { startRow, endRow, startColumn, endColumn } = getSelectionIterationBoundaries(selectionRef.current);
        for (let r = startRow; r <= endRow; r++) {
          clipboardMatrix[r - startRow] ??= [];
          for (let c = startColumn; c <= endColumn; c++) {
            clipboardMatrix[r - startRow][c - startColumn] = [...(refs.current?.get(r)?.get(c) ?? [])]
              ?.flatMap((ref) => {
                const cellValue = ref.getValue ? [ref.getValue()] : [];
                ref.setValue?.(CELL_EMPTY_VALUE);
                return cellValue;
              })
              .join(""); // What to do? Only one ref should be yielding the content. See https://github.com/apache/incubator-kie-issues/issues/170
          }
        }

        const clipboardValue = clipboardMatrix
          .map((row) => row.join(CLIPBOARD_COLUMN_SEPARATOR))
          .join(TEXT_TO_CLIPBOARD_ROW_SEPARATOR);

        navigator.clipboard.writeText(clipboardValue);
      },
      paste: () => {
        navigator.clipboard.readText().then((clipboardValue) => {
          setPasteData(clipboardValue);
        });
      },
      erase: () => {
        if (!selectionRef.current?.selectionStart || !selectionRef.current?.selectionEnd) {
          return;
        }

        const { startRow, endRow, startColumn, endColumn } = getSelectionIterationBoundaries(selectionRef.current);
        for (let r = startRow; r <= endRow; r++) {
          for (let c = startColumn; c <= endColumn; c++) {
            refs.current
              ?.get(r)
              ?.get(c)
              ?.forEach((ref) => {
                ref.setValue?.(CELL_EMPTY_VALUE);
              });
          }
        }
      },
      resetSelectionAt: (newSelectionAction) => {
        resetParentSelectionAt?.({
          columnIndex: containerCellCoordinates?.columnIndex ?? 1,
          rowIndex: containerCellCoordinates?.rowIndex ?? 0,
          isEditing: false,
        });

        if (!newSelectionAction) {
          setCurrentDepth((prev) => ({
            max: prev.max,
            active: Math.max(SELECTION_MIN_ACTIVE_DEPTH, depth - 1),
          }));
          return;
        }

        setCurrentDepth((prev) => ({
          max: prev.max,
          active: depth,
        }));

        _setSelection((prev) => {
          const newActiveCell =
            typeof newSelectionAction === "function" //
              ? newSelectionAction(prev.active)
              : newSelectionAction;

          return {
            active: newActiveCell,
            selectionStart: newActiveCell?.keepSelection ? prev.selectionStart : newActiveCell,
            selectionEnd: newActiveCell?.keepSelection ? prev.selectionEnd : newActiveCell,
          };
        });
      },
      setSelectionEnd: (newSelectionEndAction) => {
        _setSelection((prev) => {
          const newSelectionEnd =
            typeof newSelectionEndAction === "function"
              ? newSelectionEndAction(prev.selectionEnd)
              : newSelectionEndAction;

          // do not change selection if currently a cell is being edited
          if (prev.active?.isEditing) {
            return prev;
          }
          // Selecting a header cell from another header cell
          // Do not allow selecting multi-line header cells
          else if (
            (prev.selectionEnd?.rowIndex ?? 0) < 0 &&
            (newSelectionEnd?.rowIndex ?? 0) < 0 &&
            prev.selectionEnd?.rowIndex !== newSelectionEnd?.rowIndex
          ) {
            return prev;
          }
          // Selecting a rowIndex cell from a header cell.
          // Do not allow selecting rowIndex cells from header cells
          else if ((prev.selectionEnd?.rowIndex ?? 0) < 0 && newSelectionEnd?.columnIndex === 0) {
            return prev;
          }
          // Selecting a normal cell from a rowIndex cell
          // Do not allow leaving the rowIndex cells
          else if (prev.selectionEnd?.columnIndex === 0) {
            return {
              ...prev,
              selectionEnd: {
                columnIndex: 0,
                rowIndex: newSelectionEnd?.rowIndex ?? prev.selectionEnd.rowIndex,
                isEditing: false,
              },
            };
          }
          // Selecting a normal cell from a header cell
          // Do not allow selecting header and normal cells simultaneously
          else if ((prev.selectionEnd?.rowIndex ?? 0) < 0) {
            return {
              ...prev,
              selectionEnd: {
                columnIndex: newSelectionEnd?.columnIndex ?? 0,
                rowIndex: prev.selectionEnd?.rowIndex ?? 0,
                isEditing: false,
              },
            };
          }
          // Selecting a rowIndex cell from a normal cell
          // Do not allow selecting rowIndex and normal cells simultaneously
          else if (newSelectionEnd?.columnIndex === 0) {
            return {
              ...prev,
              selectionEnd: {
                columnIndex: 1,
                rowIndex: Math.max(0, newSelectionEnd?.rowIndex ?? 0),
                isEditing: false,
              },
            };
          }
          // Selecting a header cell from a normal cell
          // Do not allow selecting rowIndex and normal cells simultaneously
          else if ((newSelectionEnd?.rowIndex ?? 0) < 0) {
            return {
              ...prev,
              selectionEnd: {
                columnIndex: newSelectionEnd?.columnIndex ?? 0,
                rowIndex: 0,
                isEditing: false,
              },
            };
          }
          // Selecting a normal cell from another normal cell
          else {
            return { ...prev, selectionEnd: newSelectionEnd };
          }
        });
      },
      registerSelectableCellRef: (rowIndex, columnIndex, ref) => {
        refs.current?.set(rowIndex, refs.current?.get(rowIndex) ?? new Map());
        const prev = refs.current?.get(rowIndex)?.get(columnIndex) ?? new Set();
        refs.current?.get(rowIndex)?.set(columnIndex, new Set([...prev, ref]));
        const isActive = coincides(selectionRef.current?.active, { rowIndex, columnIndex });
        ref.setStatus?.({
          isActive,
          isEditing: isActive && (selectionRef.current?.active?.isEditing ?? false),
          isSelected: !isActive && isCellSelected(rowIndex, columnIndex, selectionRef.current),
        });
        return ref;
      },
      deregisterSelectableCellRef: (rowIndex, columnIndex, ref) => {
        ref.setStatus?.(NEUTRAL_CELL_STATUS);
        refs.current?.get(rowIndex)?.get(columnIndex)?.delete(ref);
      },
    };
  }, [
    containerCellCoordinates?.columnIndex,
    containerCellCoordinates?.rowIndex,
    depth,
    resetParentSelectionAt,
    setCurrentDepth,
  ]);

  // If there's no selection on the table that is coming into focus, we focus at the top-left cell.
  useEffect(() => {
    if (!selection.active && depth === activeDepth && isSelectionHere) {
      dispatch.resetSelectionAt({
        rowIndex: 0,
        columnIndex: 1,
        isEditing: false,
      });
    }
  }, [activeDepth, containerCellCoordinates, depth, dispatch, isSelectionHere, parentActiveCell, selection]);

  // Paint the selection
  useEffect(() => {
    if (!selection.active || !selection.selectionStart || !selection.selectionEnd) {
      return;
    }

    const currentRefs = refs.current;
    const active = selection.active;

    const { startRow, endRow, startColumn, endColumn } = getSelectionIterationBoundaries(selection);
    for (let r = startRow; r <= endRow; r++) {
      // Select rowIndex cells
      currentRefs
        .get(r)
        ?.get(0)
        ?.forEach((e) => e.setStatus?.({ isActive: false, isEditing: false, isSelected: true }));

      for (let c = startColumn; c <= endColumn; c++) {
        // Select header cells
        if (startRow >= 0) {
          currentRefs
            .get(-1)
            ?.get(c)
            ?.forEach((e) => e.setStatus?.({ isActive: false, isEditing: false, isSelected: true }));
        }

        // Select normal cells
        const refs = currentRefs.get(r)?.get(c);
        refs?.forEach((ref) =>
          ref.setStatus?.({
            isActive: false,
            isEditing: false,
            isSelected: true,
            selectedPositions: getSelectedPositions(selection, { rowIndex: r, columnIndex: c }),
          })
        );
      }
    }

    // Active cell. Sometimes it is not inside the selection.
    currentRefs
      .get(active.rowIndex)
      ?.get(active.columnIndex)
      ?.forEach((r) =>
        r.setStatus?.({
          isActive: true,
          isEditing: active?.isEditing ?? false,
          isSelected: !coincides(selectionRef.current?.selectionStart, selectionRef.current?.selectionEnd),
        })
      );

    // Cleanup
    return () => {
      for (let r = startRow; r <= endRow; r++) {
        currentRefs
          .get(r)
          ?.get(0)
          ?.forEach((e) => e.setStatus?.(NEUTRAL_CELL_STATUS));

        for (let c = startColumn; c <= endColumn; c++) {
          currentRefs
            .get(-1)
            ?.get(c)
            ?.forEach((e) => e.setStatus?.(NEUTRAL_CELL_STATUS));

          const refs = currentRefs.get(r)?.get(c);
          refs?.forEach((ref) => ref.setStatus?.(NEUTRAL_CELL_STATUS));
        }
      }

      currentRefs
        .get(active.rowIndex)
        ?.get(active.columnIndex)
        ?.forEach((r) => r.setStatus?.(NEUTRAL_CELL_STATUS));
    };
  }, [selection, selectionRef.current?.selectionStart, selectionRef.current?.selectionEnd]);

  return (
    <BeeTableSelectionContext.Provider value={value}>
      <BeeTableSelectionDispatchContext.Provider value={dispatch}>
        <>{children}</>
      </BeeTableSelectionDispatchContext.Provider>
    </BeeTableSelectionContext.Provider>
  );
}