teableio/teable

View on GitHub
packages/sdk/src/components/grid/hooks/useDrag.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { useRef, useState } from 'react';
import { DEFAULT_DRAG_STATE } from '../configs';
import type { IDragState, IMouseState, IRange, IScrollState } from '../interface';
import { DragRegionType, RegionType, DraggableType } from '../interface';
import type { CoordinateManager, CombinedSelection } from '../managers';
import { inRange } from '../utils';

export const getDropTargetIndex = (
  coordInstance: CoordinateManager,
  mouseState: IMouseState,
  scrollState: IScrollState,
  dragType: DragRegionType
) => {
  const { x, y } = mouseState;
  const { scrollLeft, scrollTop } = scrollState;
  const { freezeRegionWidth, rowInitSize } = coordInstance;

  if (dragType === DragRegionType.Columns) {
    const offsetX = x <= freezeRegionWidth ? x : scrollLeft + x;
    const hoverColumnIndex = coordInstance.getColumnStartIndex(offsetX);
    const hoverColumnOffsetX = coordInstance.getColumnOffset(hoverColumnIndex);
    const hoverColumnWidth = coordInstance.getColumnWidth(hoverColumnIndex);
    return inRange(offsetX, hoverColumnOffsetX, hoverColumnOffsetX + hoverColumnWidth / 2)
      ? hoverColumnIndex
      : hoverColumnIndex + 1;
  }
  if (dragType === DragRegionType.Rows) {
    const offsetY = y <= rowInitSize ? y : scrollTop + y;
    const hoverRowIndex = coordInstance.getRowStartIndex(offsetY);
    const hoverRowOffsetY = coordInstance.getRowOffset(hoverRowIndex);
    const hoverRowHeight = coordInstance.getRowHeight(hoverRowIndex);
    return inRange(offsetY, hoverRowOffsetY, hoverRowOffsetY + hoverRowHeight / 2)
      ? hoverRowIndex
      : hoverRowIndex + 1;
  }
  return -Infinity;
};

export const useDrag = (
  coordInstance: CoordinateManager,
  scrollState: IScrollState,
  selection: CombinedSelection,
  draggable?: DraggableType
) => {
  // Prevents Drag and Drop from Being Too Reactive
  const startPosition = useRef(0);
  const [dragState, setDragState] = useState<IDragState>(DEFAULT_DRAG_STATE);
  const { scrollTop, scrollLeft } = scrollState;

  const onDragStart = (
    mouseState: IMouseState,
    onEnd: (type: DragRegionType, dragIndexs: IRange[]) => void
  ) => {
    if (draggable === DraggableType.None) return;

    const { type, rowIndex: hoverRowIndex, columnIndex: hoverColumnIndex, x, y } = mouseState;
    const { isRowSelection, isColumnSelection, ranges: selectionRanges } = selection;

    if (type === RegionType.RowHeaderDragHandler && draggable !== DraggableType.Column) {
      startPosition.current = y;
      const ranges =
        isRowSelection && selection.includes([hoverRowIndex, hoverRowIndex])
          ? selectionRanges
          : ([[hoverRowIndex, hoverRowIndex]] as IRange[]);
      onEnd(DragRegionType.Rows, ranges);
      setDragState({
        type: DragRegionType.Rows,
        ranges,
        delta: y + scrollTop - coordInstance.getRowOffset(hoverRowIndex),
        isDragging: false,
      });
    }

    if (type === RegionType.ColumnHeader && draggable !== DraggableType.Row) {
      startPosition.current = x;
      const ranges =
        isColumnSelection && selection.includes([hoverColumnIndex, hoverColumnIndex])
          ? selectionRanges
          : ([[hoverColumnIndex, hoverColumnIndex]] as IRange[]);
      onEnd(DragRegionType.Columns, ranges);
      setDragState({
        type: DragRegionType.Columns,
        ranges,
        delta: x - coordInstance.getColumnRelativeOffset(hoverColumnIndex, scrollLeft),
        isDragging: false,
      });
    }
  };

  const onDragChange = (mouseState: IMouseState) => {
    const { type, isDragging } = dragState;
    if (isDragging) return;
    if (![DragRegionType.Rows, DragRegionType.Columns].includes(type)) return;

    const { x, y } = mouseState;
    const prevPosition = type === DragRegionType.Rows ? y : x;
    const moveDistance = Math.abs(prevPosition - startPosition.current);
    if (moveDistance < 5) return;

    setDragState((prev) => ({ ...prev, isDragging: true }));
  };

  const onDragEnd = (
    mouseState: IMouseState,
    onEnd: (dragIndexs: IRange[], dropIndex: number) => void
  ) => {
    const { type, isDragging } = dragState;

    if (!isDragging || !onEnd) return setDragState(DEFAULT_DRAG_STATE);

    if ([DragRegionType.Columns, DragRegionType.Rows].includes(type)) {
      const { ranges } = dragState;
      const targetIndex = getDropTargetIndex(coordInstance, mouseState, scrollState, type);
      if (!inRange(targetIndex, ranges[0][0], ranges[ranges.length - 1][1])) {
        onEnd(ranges, targetIndex);
      }
    }
    setDragState(DEFAULT_DRAG_STATE);
  };

  return {
    dragState,
    setDragState,
    onDragStart,
    onDragChange,
    onDragEnd,
  };
};