From a743888fba6c65cc46431595df185b0d228f3e12 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 19 Mar 2025 11:16:39 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=93=20Fix=20the=20window=20not=20to=20go?= =?UTF-8?q?=20outside=20off=20the=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../draggable-window/DraggableWindow.tsx | 280 ++++++++---------- 1 file changed, 125 insertions(+), 155 deletions(-) diff --git a/src/common/draggable-window/DraggableWindow.tsx b/src/common/draggable-window/DraggableWindow.tsx index 37f1e2c..770cc51 100644 --- a/src/common/draggable-window/DraggableWindow.tsx +++ b/src/common/draggable-window/DraggableWindow.tsx @@ -5,12 +5,11 @@ import { GetLocalStorage, SetLocalStorage, WindowSaveOptions } from '../../api'; import { DraggableWindowPosition } from './DraggableWindowPosition'; const CURRENT_WINDOWS: HTMLElement[] = []; -const POS_MEMORY: Map = new Map(); +const POS_MEMORY: Map = new Map(); const BOUNDS_THRESHOLD_TOP: number = 0; const BOUNDS_THRESHOLD_LEFT: number = 0; -export interface DraggableWindowProps -{ +export interface DraggableWindowProps { uniqueKey?: Key; handleSelector?: string; windowPosition?: string; @@ -21,250 +20,221 @@ export interface DraggableWindowProps children?: ReactNode; } -export const DraggableWindow: FC = props => -{ +export const DraggableWindow: FC = props => { const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = props; - const [ delta, setDelta ] = useState<{ x: number, y: number }>(null); - const [ offset, setOffset ] = useState<{ x: number, y: number }>(null); - const [ start, setStart ] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); - const [ isDragging, setIsDragging ] = useState(false); - const [ dragHandler, setDragHandler ] = useState(null); + const [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); + const [offset, setOffset] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); + const [start, setStart] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [dragHandler, setDragHandler] = useState(null); const elementRef = useRef(); - const bringToTop = useCallback(() => - { + const bringToTop = useCallback(() => { let zIndex = 400; - - for(const existingWindow of CURRENT_WINDOWS) - { + for (const existingWindow of CURRENT_WINDOWS) { zIndex += 1; - existingWindow.style.zIndex = zIndex.toString(); } }, []); - const moveCurrentWindow = useCallback(() => - { + const moveCurrentWindow = useCallback(() => { const index = CURRENT_WINDOWS.indexOf(elementRef.current); - - if(index === -1) - { + if (index === -1) { CURRENT_WINDOWS.push(elementRef.current); - } - - else if(index === (CURRENT_WINDOWS.length - 1)) return; - - else if(index >= 0) - { + } else if (index === (CURRENT_WINDOWS.length - 1)) return; + else if (index >= 0) { CURRENT_WINDOWS.splice(index, 1); - CURRENT_WINDOWS.push(elementRef.current); } - bringToTop(); - }, [ bringToTop ]); + }, [bringToTop]); - const onMouseDown = useCallback((event: ReactMouseEvent) => - { + const onMouseDown = useCallback((event: ReactMouseEvent) => { moveCurrentWindow(); - }, [ moveCurrentWindow ]); + }, [moveCurrentWindow]); - const onTouchStart = useCallback((event: ReactTouchEvent) => - { + const onTouchStart = useCallback((event: ReactTouchEvent) => { moveCurrentWindow(); - }, [ moveCurrentWindow ]); + }, [moveCurrentWindow]); - const startDragging = useCallback((startX: number, startY: number) => - { + const startDragging = useCallback((startX: number, startY: number) => { setStart({ x: startX, y: startY }); setIsDragging(true); }, []); - const onDragMouseDown = useCallback((event: MouseEvent) => - { + const onDragMouseDown = useCallback((event: MouseEvent) => { startDragging(event.clientX, event.clientY); - }, [ startDragging ]); + }, [startDragging]); - const onTouchDown = useCallback((event: TouchEvent) => - { + const onTouchDown = useCallback((event: TouchEvent) => { const touch = event.touches[0]; - startDragging(touch.clientX, touch.clientY); - }, [ startDragging ]); + }, [startDragging]); - const onDragMouseMove = useCallback((event: MouseEvent) => - { - setDelta({ x: (event.clientX - start.x), y: (event.clientY - start.y) }); - }, [ start ]); + const clampPosition = useCallback((newX: number, newY: number) => { + if (!elementRef.current) return { x: newX, y: newY }; + + const windowWidth = elementRef.current.offsetWidth; + const windowHeight = elementRef.current.offsetHeight; + const viewportWidth = window.innerWidth; // Use window.innerWidth for viewport + const viewportHeight = window.innerHeight; // Use window.innerHeight for viewport + + // Clamp X and Y to keep the window fully within the viewport + const clampedX = Math.max(BOUNDS_THRESHOLD_LEFT, Math.min(newX, viewportWidth - windowWidth)); + const clampedY = Math.max(BOUNDS_THRESHOLD_TOP, Math.min(newY, viewportHeight - windowHeight)); + + return { x: clampedX, y: clampedY }; + }, []); + + const onDragMouseMove = useCallback((event: MouseEvent) => { + if (!elementRef.current || !isDragging) return; + + const newDeltaX = event.clientX - start.x; + const newDeltaY = event.clientY - start.y; + const newOffsetX = offset.x + newDeltaX; + const newOffsetY = offset.y + newDeltaY; + + const clampedPos = clampPosition(newOffsetX, newOffsetY); + setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y }); + }, [start, offset, clampPosition, isDragging]); + + const onDragTouchMove = useCallback((event: TouchEvent) => { + if (!elementRef.current || !isDragging) return; - const onDragTouchMove = useCallback((event: TouchEvent) => - { const touch = event.touches[0]; + const newDeltaX = touch.clientX - start.x; + const newDeltaY = touch.clientY - start.y; + const newOffsetX = offset.x + newDeltaX; + const newOffsetY = offset.y + newDeltaY; - setDelta({ x: (touch.clientX - start.x), y: (touch.clientY - start.y) }); - }, [ start ]); + const clampedPos = clampPosition(newOffsetX, newOffsetY); + setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y }); + }, [start, offset, clampPosition, isDragging]); - const completeDrag = useCallback(() => - { - if(!elementRef.current || !dragHandler) return; + const completeDrag = useCallback(() => { + if (!elementRef.current || !dragHandler || !isDragging) return; - let offsetX = (offset.x + delta.x); - let offsetY = (offset.y + delta.y); - - const left = elementRef.current.offsetLeft + offsetX; - const top = elementRef.current.offsetTop + offsetY; - - if(top < BOUNDS_THRESHOLD_TOP) - { - offsetY = -elementRef.current.offsetTop; - } - - else if((top + dragHandler.offsetHeight) >= (document.body.offsetHeight - BOUNDS_THRESHOLD_TOP)) - { - offsetY = (document.body.offsetHeight - elementRef.current.offsetHeight) - elementRef.current.offsetTop; - } - - if((left + elementRef.current.offsetWidth) < BOUNDS_THRESHOLD_LEFT) - { - offsetX = -elementRef.current.offsetLeft; - } - - else if(left >= (document.body.offsetWidth - BOUNDS_THRESHOLD_LEFT)) - { - offsetX = (document.body.offsetWidth - elementRef.current.offsetWidth) - elementRef.current.offsetLeft; - } + const finalOffsetX = offset.x + delta.x; + const finalOffsetY = offset.y + delta.y; + const clampedPos = clampPosition(finalOffsetX, finalOffsetY); setDelta({ x: 0, y: 0 }); - setOffset({ x: offsetX, y: offsetY }); + setOffset({ x: clampedPos.x, y: clampedPos.y }); setIsDragging(false); - if(uniqueKey !== null) - { - const newStorage = { ...GetLocalStorage(`nitro.windows.${ uniqueKey }`) } as WindowSaveOptions; - - newStorage.offset = { x: offsetX, y: offsetY }; - - SetLocalStorage(`nitro.windows.${ uniqueKey }`, newStorage); + if (uniqueKey !== null) { + const newStorage = { ...GetLocalStorage(`nitro.windows.${uniqueKey}`) } as WindowSaveOptions; + newStorage.offset = { x: clampedPos.x, y: clampedPos.y }; + SetLocalStorage(`nitro.windows.${uniqueKey}`, newStorage); } - }, [ dragHandler, delta, offset, uniqueKey ]); + }, [dragHandler, delta, offset, uniqueKey, clampPosition, isDragging]); - const onDragMouseUp = useCallback((event: MouseEvent) => - { + const onDragMouseUp = useCallback((event: MouseEvent) => { completeDrag(); - }, [ completeDrag ]); + }, [completeDrag]); - const onDragTouchUp = useCallback((event: TouchEvent) => - { + const onDragTouchUp = useCallback((event: TouchEvent) => { completeDrag(); - }, [ completeDrag ]); + }, [completeDrag]); - useEffect(() => - { - const element = (elementRef.current as HTMLElement); - - if(!element) return; + useEffect(() => { + const element = elementRef.current as HTMLElement; + if (!element) return; CURRENT_WINDOWS.push(element); - bringToTop(); - if(!disableDrag) - { - const handle = (element.querySelector(handleSelector)); - - if(handle) setDragHandler(handle); + if (!disableDrag) { + const handle = element.querySelector(handleSelector); + if (handle) setDragHandler(handle as HTMLElement); } + // Initial positioning + const windowWidth = element.offsetWidth || 340; // Fallback width if not yet rendered + const windowHeight = element.offsetHeight || 462; // Fallback height if not yet rendered let offsetX = 0; let offsetY = 0; - switch(windowPosition) - { + switch (windowPosition) { case DraggableWindowPosition.TOP_CENTER: - element.style.top = 50 + offsetTop + 'px'; - element.style.left = `calc(50vw - ${ (element.offsetWidth / 2 + offsetLeft) }px)`; + offsetY = 50 + offsetTop; + offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft; break; case DraggableWindowPosition.CENTER: - element.style.top = `calc(50vh - ${ (element.offsetHeight / 2) + offsetTop }px)`; - element.style.left = `calc(50vw - ${ (element.offsetWidth / 2) + offsetLeft }px)`; + offsetY = (window.innerHeight - windowHeight) / 2 + offsetTop; + offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft; break; case DraggableWindowPosition.TOP_LEFT: - element.style.top = 50 + offsetTop + 'px'; - element.style.left = 50 + offsetLeft + 'px'; + offsetY = 50 + offsetTop; + offsetX = 50 + offsetLeft; break; } + const clampedPos = clampPosition(offsetX, offsetY); + element.style.left = '0px'; // Reset base position + element.style.top = '0px'; // Reset base position + setOffset({ x: clampedPos.x, y: clampedPos.y }); setDelta({ x: 0, y: 0 }); - setOffset({ x: offsetX, y: offsetY }); - return () => - { + return () => { const index = CURRENT_WINDOWS.indexOf(element); - - if(index >= 0) CURRENT_WINDOWS.splice(index, 1); + if (index >= 0) CURRENT_WINDOWS.splice(index, 1); }; - }, [ handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop ]); + }, [handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop]); - useEffect(() => - { - if(!offset && !delta) return; + useEffect(() => { + if (!offset && !delta) return; - const element = (elementRef.current as HTMLElement); + const element = elementRef.current as HTMLElement; + if (!element) return; - if(!element) return; - - element.style.transform = `translate(${ offset.x + delta.x }px, ${ offset.y + delta.y }px)`; + element.style.transform = `translate(${offset.x + delta.x}px, ${offset.y + delta.y}px)`; element.style.visibility = 'visible'; - }, [ offset, delta ]); + }, [offset, delta]); - useEffect(() => - { - if(!dragHandler) return; + useEffect(() => { + if (!dragHandler) return; dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown); - return () => - { + return () => { dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown); }; - }, [ dragHandler, onDragMouseDown, onTouchDown ]); + }, [dragHandler, onDragMouseDown, onTouchDown]); - useEffect(() => - { - if(!isDragging) return; + useEffect(() => { + if (!isDragging) return; document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); document.addEventListener(TouchEventType.TOUCH_END, onDragTouchUp); document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove); - return () => - { + return () => { document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp); document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); document.removeEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove); }; - }, [ isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove ]); + }, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]); - useEffect(() => - { - if(!uniqueKey) return; + useEffect(() => { + if (!uniqueKey) return; - const localStorage = GetLocalStorage(`nitro.windows.${ uniqueKey }`); - - if(!localStorage || !localStorage.offset) return; + const localStorage = GetLocalStorage(`nitro.windows.${uniqueKey}`); + if (!localStorage || !localStorage.offset) return; + const clampedPos = clampPosition(localStorage.offset.x, localStorage.offset.y); setDelta({ x: 0, y: 0 }); - if(localStorage.offset) setOffset(localStorage.offset); - }, [ uniqueKey ]); + setOffset({ x: clampedPos.x, y: clampedPos.y }); + }, [uniqueKey, clampPosition]); - return ( - createPortal( -
- { children } -
, document.getElementById('draggable-windows-container')) + return createPortal( +
+ {children} +
, + document.getElementById('draggable-windows-container') ); -}; +}; \ No newline at end of file