Загрузка данных


import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import {
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from 'react';

import {
  GearIcon,
  LockIcon,
  LockOpenIcon,
  TrashIcon,
} from '@src/components/Icon';

import styles from './index.module.scss';

import type { Drawing } from '@core/Drawings';
import type {
  PointerEvent as ReactPointerEvent,
  SyntheticEvent,
} from 'react';
import type { Observable } from 'rxjs';

interface FloatingDrawingToolbarProps {
  selectedDrawing$: Observable<Drawing | null>;
  onToggleLock: () => void;
  onOpenSettings: () => void;
  onDelete: () => void;
}

interface Position {
  x: number;
  y: number;
}

interface DragState {
  pointerId: number;
  startX: number;
  startY: number;
  initialX: number;
  initialY: number;
}

const TOOLBAR_TOP_OFFSET = 12;

export function FloatingDrawingToolbar({
  selectedDrawing$,
  onToggleLock,
  onOpenSettings,
  onDelete,
}: FloatingDrawingToolbarProps) {
  const toolbarRef = useRef<HTMLDivElement | null>(null);
  const dragStateRef = useRef<DragState | null>(null);
  const positionRef = useRef<Position | null>(null);

  const [selectedDrawing, setSelectedDrawing] =
    useState<Drawing | null>(null);
  const [isLocked, setIsLocked] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState<Position | null>(null);

  useEffect(() => {
    const subscription =
      selectedDrawing$.subscribe(setSelectedDrawing);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing$]);

  useEffect(() => {
    if (!selectedDrawing) {
      setIsLocked(false);

      return;
    }

    const subscription =
      selectedDrawing.subscribeIsLocked(setIsLocked);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing]);

  useLayoutEffect(() => {
    if (!selectedDrawing) {
      return;
    }

    const toolbar = toolbarRef.current;
    const container = toolbar?.parentElement;

    if (!toolbar || !container) {
      return;
    }

    const currentPosition = positionRef.current;

    if (currentPosition) {
      updatePosition(
        clampPosition(
          currentPosition.x,
          currentPosition.y,
          toolbar,
          container,
        ),
      );

      return;
    }

    updatePosition({
      x: Math.round(
        (container.clientWidth - toolbar.offsetWidth) / 2,
      ),
      y: TOOLBAR_TOP_OFFSET,
    });
  }, [selectedDrawing]);

  const handleDragStart = (
    event: ReactPointerEvent<HTMLButtonElement>,
  ): void => {
    const currentPosition = positionRef.current;

    if (event.button !== 0 || !currentPosition) {
      return;
    }

    event.preventDefault();
    event.currentTarget.setPointerCapture(event.pointerId);

    dragStateRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      initialX: currentPosition.x,
      initialY: currentPosition.y,
    };

    setIsDragging(true);
  };

  const handleDrag = (
    event: ReactPointerEvent<HTMLButtonElement>,
  ): void => {
    const dragState = dragStateRef.current;
    const toolbar = toolbarRef.current;
    const container = toolbar?.parentElement;

    if (
      !dragState ||
      dragState.pointerId !== event.pointerId ||
      !toolbar ||
      !container
    ) {
      return;
    }

    event.preventDefault();

    updatePosition(
      clampPosition(
        dragState.initialX +
          event.clientX -
          dragState.startX,
        dragState.initialY +
          event.clientY -
          dragState.startY,
        toolbar,
        container,
      ),
    );
  };

  const handleDragEnd = (
    event: ReactPointerEvent<HTMLButtonElement>,
  ): void => {
    if (dragStateRef.current?.pointerId !== event.pointerId) {
      return;
    }

    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }

    dragStateRef.current = null;
    setIsDragging(false);
  };

  const stopPropagation = (event: SyntheticEvent): void => {
    event.stopPropagation();
  };

  function updatePosition(nextPosition: Position): void {
    positionRef.current = nextPosition;

    setPosition((currentPosition) => {
      if (
        currentPosition?.x === nextPosition.x &&
        currentPosition.y === nextPosition.y
      ) {
        return currentPosition;
      }

      return nextPosition;
    });
  }

  if (!selectedDrawing) {
    return null;
  }

  return (
    <div
      ref={toolbarRef}
      className={styles.toolbar}
      style={{
        visibility: position ? 'visible' : 'hidden',
        transform: `translate3d(
          ${position?.x ?? 0}px,
          ${position?.y ?? 0}px,
          0
        )`,
      }}
      onClick={stopPropagation}
      onContextMenu={stopPropagation}
      onDoubleClick={stopPropagation}
      onPointerCancel={stopPropagation}
      onPointerDown={stopPropagation}
      onPointerMove={stopPropagation}
      onPointerUp={stopPropagation}
    >
      <button
        type="button"
        className={classNames(styles.toolbar_handle, {
          [styles.dragging]: isDragging,
        })}
        onPointerCancel={handleDragEnd}
        onPointerDown={handleDragStart}
        onPointerMove={handleDrag}
        onPointerUp={handleDragEnd}
      >
        <span />
        <span />
        <span />
        <span />
        <span />
        <span />
      </button>

      {selectedDrawing.hasSettings() && (
        <Button
          size="sm"
          className={styles.button}
          onClick={onOpenSettings}
          label={<GearIcon />}
        />
      )}

      <Button
        size="sm"
        className={classNames(styles.button, {
          [styles.pressed]: isLocked,
        })}
        onClick={onToggleLock}
        label={isLocked ? <LockIcon /> : <LockOpenIcon />}
      />

      <Button
        size="sm"
        className={styles.button}
        onClick={onDelete}
        label={<TrashIcon />}
      />
    </div>
  );
}

function clampPosition(
  x: number,
  y: number,
  toolbar: HTMLElement,
  container: HTMLElement,
): Position {
  return {
    x: Math.max(
      0,
      Math.min(
        Math.round(x),
        container.clientWidth - toolbar.offsetWidth,
      ),
    ),
    y: Math.max(
      0,
      Math.min(
        Math.round(y),
        container.clientHeight - toolbar.offsetHeight,
      ),
    ),
  };
}