mirror of https://github.com/mkrhere/pw2
				
				
			
				 3 changed files with 232 additions and 6 deletions
			
			
		@ -0,0 +1,185 @@ | 
				
			|||
import { css, cx } from "@emotion/css"; | 
				
			|||
import React, { useEffect, useRef, useState } from "react"; | 
				
			|||
 | 
				
			|||
export interface DraggableButtonProps | 
				
			|||
	extends React.ButtonHTMLAttributes<HTMLButtonElement> {} | 
				
			|||
 | 
				
			|||
interface Pos { | 
				
			|||
	x: number; | 
				
			|||
	y: number; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
const relativePos = (pos: Pos, container: DOMRect) => { | 
				
			|||
	return { | 
				
			|||
		x: pos.x - container.left, | 
				
			|||
		y: pos.y - container.top, | 
				
			|||
	}; | 
				
			|||
}; | 
				
			|||
 | 
				
			|||
export const DraggableButton = React.forwardRef< | 
				
			|||
	HTMLButtonElement, | 
				
			|||
	DraggableButtonProps | 
				
			|||
>(({ children, ...props }, ref) => { | 
				
			|||
	const [position, setPosition] = useState({ x: 0, y: 0 }); | 
				
			|||
	const [isDragging, setIsDragging] = useState(false); | 
				
			|||
	const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); | 
				
			|||
	const myRef = useRef<HTMLButtonElement | null>(null); | 
				
			|||
	const containerRef = useRef<DOMRect | null>(null); | 
				
			|||
	const [isInitialized, setIsInitialized] = useState(false); | 
				
			|||
 | 
				
			|||
	// Convert initial position and set up element on mount
 | 
				
			|||
	useEffect(() => { | 
				
			|||
		const el = myRef.current; | 
				
			|||
		if (!el || isInitialized) return; | 
				
			|||
 | 
				
			|||
		const rect = el.getBoundingClientRect(); | 
				
			|||
		const parentRect = el.parentElement?.getBoundingClientRect() ?? rect; | 
				
			|||
 | 
				
			|||
		// Convert current position to top/left
 | 
				
			|||
		const top = rect.top - parentRect.top; | 
				
			|||
		const left = rect.left - parentRect.left; | 
				
			|||
 | 
				
			|||
		// Set position and clear any bottom/right values
 | 
				
			|||
		el.style.position = "absolute"; | 
				
			|||
		el.style.top = `${top}px`; | 
				
			|||
		el.style.left = `${left}px`; | 
				
			|||
		el.style.bottom = "unset"; | 
				
			|||
		el.style.right = "unset"; | 
				
			|||
 | 
				
			|||
		setPosition({ x: left, y: top }); | 
				
			|||
		setIsInitialized(true); | 
				
			|||
	}, []); | 
				
			|||
 | 
				
			|||
	useEffect(() => { | 
				
			|||
		const el = myRef.current; | 
				
			|||
		if (!el || !isInitialized) return; | 
				
			|||
 | 
				
			|||
		containerRef.current = el.parentElement?.getBoundingClientRect() ?? null; | 
				
			|||
 | 
				
			|||
		el.style.transition = "none"; | 
				
			|||
		el.style.left = `${position.x}px`; | 
				
			|||
		el.style.top = `${position.y}px`; | 
				
			|||
 | 
				
			|||
		const handleKeyDown = (e: KeyboardEvent) => { | 
				
			|||
			if (e.key === "Escape") setIsDragging(false); | 
				
			|||
		}; | 
				
			|||
 | 
				
			|||
		window.addEventListener("keydown", handleKeyDown); | 
				
			|||
		return () => window.removeEventListener("keydown", handleKeyDown); | 
				
			|||
	}, [position, isInitialized]); | 
				
			|||
 | 
				
			|||
	const getEventPos = ( | 
				
			|||
		e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, | 
				
			|||
	): Pos => { | 
				
			|||
		if ("touches" in e) { | 
				
			|||
			return { | 
				
			|||
				x: e.touches[0].clientX, | 
				
			|||
				y: e.touches[0].clientY, | 
				
			|||
			}; | 
				
			|||
		} | 
				
			|||
		return { | 
				
			|||
			x: e.clientX, | 
				
			|||
			y: e.clientY, | 
				
			|||
		}; | 
				
			|||
	}; | 
				
			|||
 | 
				
			|||
	const handleStart = (e: React.MouseEvent | React.TouchEvent) => { | 
				
			|||
		if (!myRef.current || !containerRef.current) return; | 
				
			|||
 | 
				
			|||
		// Prevent scrolling on touch devices
 | 
				
			|||
		if ("touches" in e) { | 
				
			|||
			e.preventDefault(); | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		setIsDragging(true); | 
				
			|||
 | 
				
			|||
		const eventPos = getEventPos(e); | 
				
			|||
		const relative = relativePos(eventPos, containerRef.current); | 
				
			|||
 | 
				
			|||
		setDragOffset({ | 
				
			|||
			x: relative.x - position.x, | 
				
			|||
			y: relative.y - position.y, | 
				
			|||
		}); | 
				
			|||
	}; | 
				
			|||
 | 
				
			|||
	const handleMove = (e: MouseEvent | TouchEvent) => { | 
				
			|||
		if (!isDragging || !containerRef.current) return; | 
				
			|||
 | 
				
			|||
		// Prevent scrolling on touch devices
 | 
				
			|||
		if ("touches" in e) { | 
				
			|||
			e.preventDefault(); | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		const eventPos = getEventPos(e); | 
				
			|||
		const relative = relativePos(eventPos, containerRef.current); | 
				
			|||
 | 
				
			|||
		setPosition({ | 
				
			|||
			x: relative.x - dragOffset.x, | 
				
			|||
			y: relative.y - dragOffset.y, | 
				
			|||
		}); | 
				
			|||
	}; | 
				
			|||
 | 
				
			|||
	const handleEnd = () => { | 
				
			|||
		setIsDragging(false); | 
				
			|||
	}; | 
				
			|||
 | 
				
			|||
	useEffect(() => { | 
				
			|||
		if (isDragging) { | 
				
			|||
			// Mouse events
 | 
				
			|||
			window.addEventListener("mousemove", handleMove); | 
				
			|||
			window.addEventListener("mouseup", handleEnd); | 
				
			|||
 | 
				
			|||
			// Touch events
 | 
				
			|||
			window.addEventListener("touchmove", handleMove, { passive: false }); | 
				
			|||
			window.addEventListener("touchend", handleEnd); | 
				
			|||
			window.addEventListener("touchcancel", handleEnd); | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		return () => { | 
				
			|||
			// Mouse events
 | 
				
			|||
			window.removeEventListener("mousemove", handleMove); | 
				
			|||
			window.removeEventListener("mouseup", handleEnd); | 
				
			|||
 | 
				
			|||
			// Touch events
 | 
				
			|||
			window.removeEventListener("touchmove", handleMove); | 
				
			|||
			window.removeEventListener("touchend", handleEnd); | 
				
			|||
			window.removeEventListener("touchcancel", handleEnd); | 
				
			|||
		}; | 
				
			|||
	}, [isDragging, dragOffset]); | 
				
			|||
 | 
				
			|||
	return ( | 
				
			|||
		<button | 
				
			|||
			ref={el => { | 
				
			|||
				if (!el) return; | 
				
			|||
 | 
				
			|||
				// Start with absolute positioning
 | 
				
			|||
				el.style.position = "absolute"; | 
				
			|||
				myRef.current = el; | 
				
			|||
 | 
				
			|||
				// Store initial container bounds
 | 
				
			|||
				const parent = el.parentElement; | 
				
			|||
				if (parent) { | 
				
			|||
					containerRef.current = parent.getBoundingClientRect(); | 
				
			|||
				} | 
				
			|||
 | 
				
			|||
				if (ref) | 
				
			|||
					if (typeof ref === "function") ref(el); | 
				
			|||
					else ref.current = el; | 
				
			|||
			}} | 
				
			|||
			onMouseDown={handleStart} | 
				
			|||
			onTouchStart={handleStart} | 
				
			|||
			{...props} | 
				
			|||
			className={cx( | 
				
			|||
				props.className, | 
				
			|||
				css` | 
				
			|||
					cursor: ${isDragging ? "grabbing" : "grab"}; | 
				
			|||
					touch-action: none; /* Prevent scrolling while dragging on touch devices */ | 
				
			|||
					.dynamic-gradient { | 
				
			|||
						cursor: inherit; | 
				
			|||
					} | 
				
			|||
				`,
 | 
				
			|||
			)}> | 
				
			|||
			{children} | 
				
			|||
		</button> | 
				
			|||
	); | 
				
			|||
}); | 
				
			|||
					Loading…
					
					
				
		Reference in new issue