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


import { useEffect, useRef, useState } from 'react';

import { t } from '@src/translations';

import styles from './styles.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>;
  onOpenSettings: () => void;
  onDelete: () => void;
}

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

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

const INITIAL_POSITION: Position = {
  x: 12,
  y: 12,
};

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

  const [selectedDrawing, setSelectedDrawing] = useState<Drawing | null>(null);
  const [position, setPosition] = useState<Position>(INITIAL_POSITION);

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

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

  const clampPosition = (x: number, y: number): Position => {
    const toolbar = toolbarRef.current;
    const container = toolbar?.parentElement;

    if (!toolbar || !container) {
      return { x, y };
    }

    return {
      x: Math.max(0, Math.min(x, container.clientWidth - toolbar.offsetWidth)),
      y: Math.max(0, Math.min(y, container.clientHeight - toolbar.offsetHeight)),
    };
  };

  const handleDragStart = (
    event: ReactPointerEvent<HTMLButtonElement>,
  ): void => {
    if (event.button !== 0) {
      return;
    }

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

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

  const handleDrag = (
    event: ReactPointerEvent<HTMLButtonElement>,
  ): void => {
    const dragState = dragStateRef.current;

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

    event.preventDefault();

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

  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;
  };

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

  if (!selectedDrawing) {
    return null;
  }

  return (
    <div
      ref={toolbarRef}
      className={styles.toolbar}
      style={{
        transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
      }}
      onClick={stopPropagation}
      onContextMenu={stopPropagation}
      onDoubleClick={stopPropagation}
      onPointerCancel={stopPropagation}
      onPointerDown={stopPropagation}
      onPointerMove={stopPropagation}
      onPointerUp={stopPropagation}
    >
      <button
        className={styles.dragHandle}
        type="button"
        aria-label={t('Move')}
        onPointerCancel={handleDragEnd}
        onPointerDown={handleDragStart}
        onPointerMove={handleDrag}
        onPointerUp={handleDragEnd}
      >
        <span />
        <span />
        <span />
        <span />
        <span />
        <span />
      </button>

      <span className={styles.name}>{selectedDrawing.name}</span>

      {selectedDrawing.hasSettings() && (
        <button
          className={styles.action}
          type="button"
          onClick={onOpenSettings}
        >
          {t('Settings')}
        </button>
      )}

      <button
        className={styles.action}
        type="button"
        onClick={onDelete}
      >
        {t('Delete')}
      </button>
    </div>
  );
}

.toolbar {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  min-height: 32px;
  border: 1px solid var(--neutral-200);
  border-radius: var(--space-0250);
  background: var(--neutral-0);
  box-shadow: 0 2px 8px rgb(0 0 0 / 16%);
  pointer-events: auto;
  user-select: none;
  touch-action: none;
  white-space: nowrap;
}

.dragHandle {
  display: grid;
  grid-template-columns: repeat(2, 3px);
  gap: 2px;
  align-content: center;
  justify-content: center;
  align-self: stretch;
  width: 24px;
  padding: 0;
  border: 0;
  border-right: 1px solid var(--neutral-200);
  background: transparent;
  cursor: grab;
  touch-action: none;

  &:active {
    cursor: grabbing;
  }

  span {
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: var(--neutral-500);
  }
}

.name {
  max-width: 160px;
  padding: 0 var(--space-0500);
  overflow: hidden;
  text-overflow: ellipsis;
}

.action {
  align-self: stretch;
  padding: 0 var(--space-0500);
  border: 0;
  border-left: 1px solid var(--neutral-200);
  background: transparent;
  cursor: pointer;

  &:hover {
    background: var(--neutral-100);
  }
}