Загрузка данных
import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import { useEffect, 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 INITIAL_POSITION: Position = {
x: 12,
y: 12,
};
export function FloatingDrawingToolbar({
selectedDrawing$,
onToggleLock,
onOpenSettings,
onDelete,
}: FloatingDrawingToolbarProps) {
const toolbarRef = useRef<HTMLDivElement | null>(null);
const dragStateRef = useRef<DragState | null>(null);
const [selectedDrawing, setSelectedDrawing] = useState<Drawing | null>(null);
const [isLocked, setIsLocked] = useState(false);
const [position, setPosition] = useState<Position>(INITIAL_POSITION);
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]);
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
type="button"
className={styles.toolbar_handle}
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>
);
}