Загрузка данных
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);
}
}