Fix the window not to go outside off the screen

This commit is contained in:
duckietm 2025-03-19 11:16:39 +01:00
parent 141d4bc68a
commit a743888fba

View File

@ -9,8 +9,7 @@ const POS_MEMORY: Map<Key, { x: number, y: number }> = new Map();
const BOUNDS_THRESHOLD_TOP: number = 0; const BOUNDS_THRESHOLD_TOP: number = 0;
const BOUNDS_THRESHOLD_LEFT: number = 0; const BOUNDS_THRESHOLD_LEFT: number = 0;
export interface DraggableWindowProps export interface DraggableWindowProps {
{
uniqueKey?: Key; uniqueKey?: Key;
handleSelector?: string; handleSelector?: string;
windowPosition?: string; windowPosition?: string;
@ -21,250 +20,221 @@ export interface DraggableWindowProps
children?: ReactNode; children?: ReactNode;
} }
export const DraggableWindow: FC<DraggableWindowProps> = props => export const DraggableWindow: FC<DraggableWindowProps> = props => {
{
const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = 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 [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
const [ offset, setOffset ] = useState<{ x: number, y: number }>(null); 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 [start, setStart] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
const [ isDragging, setIsDragging ] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [ dragHandler, setDragHandler ] = useState<HTMLElement>(null); const [dragHandler, setDragHandler] = useState<HTMLElement>(null);
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
const bringToTop = useCallback(() => const bringToTop = useCallback(() => {
{
let zIndex = 400; let zIndex = 400;
for (const existingWindow of CURRENT_WINDOWS) {
for(const existingWindow of CURRENT_WINDOWS)
{
zIndex += 1; zIndex += 1;
existingWindow.style.zIndex = zIndex.toString(); existingWindow.style.zIndex = zIndex.toString();
} }
}, []); }, []);
const moveCurrentWindow = useCallback(() => const moveCurrentWindow = useCallback(() => {
{
const index = CURRENT_WINDOWS.indexOf(elementRef.current); const index = CURRENT_WINDOWS.indexOf(elementRef.current);
if (index === -1) {
if(index === -1)
{
CURRENT_WINDOWS.push(elementRef.current); 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.splice(index, 1);
CURRENT_WINDOWS.push(elementRef.current); CURRENT_WINDOWS.push(elementRef.current);
} }
bringToTop(); bringToTop();
}, [ bringToTop ]); }, [bringToTop]);
const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) => const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
{
moveCurrentWindow(); moveCurrentWindow();
}, [ moveCurrentWindow ]); }, [moveCurrentWindow]);
const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) => const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) => {
{
moveCurrentWindow(); moveCurrentWindow();
}, [ moveCurrentWindow ]); }, [moveCurrentWindow]);
const startDragging = useCallback((startX: number, startY: number) => const startDragging = useCallback((startX: number, startY: number) => {
{
setStart({ x: startX, y: startY }); setStart({ x: startX, y: startY });
setIsDragging(true); setIsDragging(true);
}, []); }, []);
const onDragMouseDown = useCallback((event: MouseEvent) => const onDragMouseDown = useCallback((event: MouseEvent) => {
{
startDragging(event.clientX, event.clientY); startDragging(event.clientX, event.clientY);
}, [ startDragging ]); }, [startDragging]);
const onTouchDown = useCallback((event: TouchEvent) => const onTouchDown = useCallback((event: TouchEvent) => {
{
const touch = event.touches[0]; const touch = event.touches[0];
startDragging(touch.clientX, touch.clientY); startDragging(touch.clientX, touch.clientY);
}, [ startDragging ]); }, [startDragging]);
const onDragMouseMove = useCallback((event: MouseEvent) => const clampPosition = useCallback((newX: number, newY: number) => {
{ if (!elementRef.current) return { x: newX, y: newY };
setDelta({ x: (event.clientX - start.x), y: (event.clientY - start.y) });
}, [ start ]); 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 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) }); const clampedPos = clampPosition(newOffsetX, newOffsetY);
}, [ start ]); setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
}, [start, offset, clampPosition, isDragging]);
const completeDrag = useCallback(() => const completeDrag = useCallback(() => {
{ if (!elementRef.current || !dragHandler || !isDragging) return;
if(!elementRef.current || !dragHandler) return;
let offsetX = (offset.x + delta.x); const finalOffsetX = offset.x + delta.x;
let offsetY = (offset.y + delta.y); const finalOffsetY = offset.y + delta.y;
const clampedPos = clampPosition(finalOffsetX, finalOffsetY);
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;
}
setDelta({ x: 0, y: 0 }); setDelta({ x: 0, y: 0 });
setOffset({ x: offsetX, y: offsetY }); setOffset({ x: clampedPos.x, y: clampedPos.y });
setIsDragging(false); setIsDragging(false);
if(uniqueKey !== null) if (uniqueKey !== null) {
{ const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`) } as WindowSaveOptions;
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`) } as WindowSaveOptions; newStorage.offset = { x: clampedPos.x, y: clampedPos.y };
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`, newStorage);
newStorage.offset = { x: offsetX, y: offsetY };
SetLocalStorage<WindowSaveOptions>(`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 ]); }, [completeDrag]);
const onDragTouchUp = useCallback((event: TouchEvent) => const onDragTouchUp = useCallback((event: TouchEvent) => {
{
completeDrag(); completeDrag();
}, [ completeDrag ]); }, [completeDrag]);
useEffect(() => useEffect(() => {
{ const element = elementRef.current as HTMLElement;
const element = (elementRef.current as HTMLElement); if (!element) return;
if(!element) return;
CURRENT_WINDOWS.push(element); CURRENT_WINDOWS.push(element);
bringToTop(); bringToTop();
if(!disableDrag) if (!disableDrag) {
{ const handle = element.querySelector(handleSelector);
const handle = (element.querySelector(handleSelector)); if (handle) setDragHandler(handle as HTMLElement);
if(handle) setDragHandler(handle);
} }
// 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 offsetX = 0;
let offsetY = 0; let offsetY = 0;
switch(windowPosition) switch (windowPosition) {
{
case DraggableWindowPosition.TOP_CENTER: case DraggableWindowPosition.TOP_CENTER:
element.style.top = 50 + offsetTop + 'px'; offsetY = 50 + offsetTop;
element.style.left = `calc(50vw - ${ (element.offsetWidth / 2 + offsetLeft) }px)`; offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
break; break;
case DraggableWindowPosition.CENTER: case DraggableWindowPosition.CENTER:
element.style.top = `calc(50vh - ${ (element.offsetHeight / 2) + offsetTop }px)`; offsetY = (window.innerHeight - windowHeight) / 2 + offsetTop;
element.style.left = `calc(50vw - ${ (element.offsetWidth / 2) + offsetLeft }px)`; offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
break; break;
case DraggableWindowPosition.TOP_LEFT: case DraggableWindowPosition.TOP_LEFT:
element.style.top = 50 + offsetTop + 'px'; offsetY = 50 + offsetTop;
element.style.left = 50 + offsetLeft + 'px'; offsetX = 50 + offsetLeft;
break; 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 }); setDelta({ x: 0, y: 0 });
setOffset({ x: offsetX, y: offsetY });
return () => return () => {
{
const index = CURRENT_WINDOWS.indexOf(element); 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(() => useEffect(() => {
{ if (!offset && !delta) return;
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'; element.style.visibility = 'visible';
}, [ offset, delta ]); }, [offset, delta]);
useEffect(() => useEffect(() => {
{ if (!dragHandler) return;
if(!dragHandler) return;
dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown); dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown);
return () => return () => {
{
dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown); dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown);
}; };
}, [ dragHandler, onDragMouseDown, onTouchDown ]); }, [dragHandler, onDragMouseDown, onTouchDown]);
useEffect(() => useEffect(() => {
{ if (!isDragging) return;
if(!isDragging) return;
document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
document.addEventListener(TouchEventType.TOUCH_END, onDragTouchUp); document.addEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove); document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
return () => return () => {
{
document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp); document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
document.removeEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove); document.removeEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
}; };
}, [ isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove ]); }, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]);
useEffect(() => useEffect(() => {
{ if (!uniqueKey) return;
if(!uniqueKey) return;
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`); const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`);
if (!localStorage || !localStorage.offset) return;
if(!localStorage || !localStorage.offset) return;
const clampedPos = clampPosition(localStorage.offset.x, localStorage.offset.y);
setDelta({ x: 0, y: 0 }); setDelta({ x: 0, y: 0 });
if(localStorage.offset) setOffset(localStorage.offset); setOffset({ x: clampedPos.x, y: clampedPos.y });
}, [ uniqueKey ]); }, [uniqueKey, clampPosition]);
return ( return createPortal(
createPortal( <div ref={elementRef} className="absolute draggable-window" style={dragStyle} onMouseDownCapture={onMouseDown} onTouchStartCapture={onTouchStart}>
<div ref={ elementRef } className="absolute draggable-window" style={ dragStyle } onMouseDownCapture={ onMouseDown } onTouchStartCapture={ onTouchStart }> {children}
{ children } </div>,
</div>, document.getElementById('draggable-windows-container')) document.getElementById('draggable-windows-container')
); );
}; };