import React, { createContext, ReactNode, useRef, useContext, useEffect, RefObject, useState } from 'react' import {styled} from '@karma.run/react' export interface Point { readonly x: number readonly y: number } export const DraggableContext = createContext | null>(null) const DraggableContainerWrapper = styled('div', () => ({ _className: process.env.NODE_ENV !== 'production' ? 'DraggableContainer' : undefined, position: 'relative', overflow: 'hidden', width: '100%', height: '100%', userSelect: 'none' })) export interface DraggableContainerProps { readonly children?: ReactNode } export function DraggableContainer({children}: DraggableContainerProps) { const ref = useRef(null) return ( {children} ) } export interface DraggableProps { readonly point: Point readonly children?: ReactNode readonly disabled?: boolean onChange?(point: Point): void } const DraggableWrapper = styled('div', () => ({ _className: process.env.NODE_ENV !== 'production' ? 'Draggable' : undefined, cursor: 'move', position: 'absolute', transform: 'translate(-50%, -50%)', transformOrigin: 'center', userSelect: 'none' })) export function Draggable({children, point, disabled, onChange}: DraggableProps) { const [layouted, setLayouted] = useState(false) const draggableRef = useRef(null) const containerRef = useContext(DraggableContext) if (!containerRef) throw new Error('Draggable cannot be used without a DraggableContainer.') useEffect(() => { let containerWidth = containerRef.current!.clientWidth let containerHeight = containerRef.current!.clientHeight let realPoint: Point = {x: containerWidth * point.x, y: containerHeight * point.y} let lastMousePosition: Point | null = null setTransform(realPoint) setLayouted(true) function clampPoint(point: Point) { return { x: Math.max(0, Math.min(containerWidth, point.x)), y: Math.max(0, Math.min(containerHeight, point.y)) } } function setTransform(point: Point) { const clampedPoint = clampPoint(point) draggableRef.current!.style.transform = `translate(-50%, -50%) translate(${clampedPoint.x}px, ${clampedPoint.y}px)` } function handleMouseMove(e: MouseEvent) { if (lastMousePosition) { const deltaPoint: Point = { x: lastMousePosition.x - e.pageX, y: lastMousePosition.y - e.pageY } realPoint = { x: realPoint.x - deltaPoint.x, y: realPoint.y - deltaPoint.y } setTransform(realPoint) lastMousePosition = {x: e.pageX, y: e.pageY} } } function handleMouseDown(e: MouseEvent) { lastMousePosition = {x: e.pageX, y: e.pageY} } function handleMouseUp() { const clampedPoint = clampPoint(realPoint) lastMousePosition = null if (onChange) { onChange({x: clampedPoint.x / containerWidth, y: clampedPoint.y / containerHeight}) } } function handleResize() { containerWidth = containerRef!.current!.clientWidth containerHeight = containerRef!.current!.clientHeight realPoint = {x: containerWidth * point.x, y: containerHeight * point.y} setTransform(realPoint) } // TODO: Consider using ResizeObserver window.addEventListener('resize', handleResize) if (!disabled) { window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) draggableRef.current!.addEventListener('mousedown', handleMouseDown) } return () => { window.removeEventListener('resize', handleResize) if (!disabled) { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) draggableRef.current!.removeEventListener('mousedown', handleMouseDown) } } }, [containerRef, draggableRef, point, disabled]) return ( {children} ) }