diff --git a/src/layout/card/NitroCardView.tsx b/src/layout/card/NitroCardView.tsx index a00913cd..aaf4178a 100644 --- a/src/layout/card/NitroCardView.tsx +++ b/src/layout/card/NitroCardView.tsx @@ -5,12 +5,12 @@ import { NitroCardViewProps } from './NitroCardView.types'; export const NitroCardView: FC = props => { - const { className = '', disableDrag = false, simple = false, theme = 'primary', children = null } = props; + const { className = '', simple = false, theme = 'primary', children = null, ...rest } = props; return (
- +
{ children }
diff --git a/src/layout/card/NitroCardView.types.ts b/src/layout/card/NitroCardView.types.ts index fcdd2f9a..0e41ed2f 100644 --- a/src/layout/card/NitroCardView.types.ts +++ b/src/layout/card/NitroCardView.types.ts @@ -1,7 +1,8 @@ -export interface NitroCardViewProps +import { DraggableWindowProps } from '../draggable-window'; + +export interface NitroCardViewProps extends DraggableWindowProps { className?: string; - disableDrag?: boolean; simple?: boolean; theme?: string; } diff --git a/src/layout/draggable-window/DraggableWindow.tsx b/src/layout/draggable-window/DraggableWindow.tsx index e9fc0537..0a299d42 100644 --- a/src/layout/draggable-window/DraggableWindow.tsx +++ b/src/layout/draggable-window/DraggableWindow.tsx @@ -1,19 +1,27 @@ -import { FC, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react'; -import Draggable from 'react-draggable'; -import { DraggableWindowProps } from './DraggableWindow.types'; +import { MouseEventType } from '@nitrots/nitro-renderer'; +import { FC, Key, MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { DraggableWindowPosition, DraggableWindowProps } from './DraggableWindow.types'; -const currentWindows: HTMLDivElement[] = []; +const CURRENT_WINDOWS: HTMLElement[] = []; +const POS_MEMORY: Map = new Map(); +const BOUNDS_THRESHOLD_TOP: number = 0; +const BOUNDS_THRESHOLD_LEFT: number = 0; export const DraggableWindow: FC = props => { - const { disableDrag = false, noCenter = false, handle = '.drag-handler', draggableOptions = {}, children = null } = props; + const { uniqueKey = null, handleSelector = '.drag-handler', position = DraggableWindowPosition.CENTER, disableDrag = false, children = null } = 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 elementRef = useRef(); const bringToTop = useCallback(() => { let zIndex = 400; - for(const existingWindow of currentWindows) + for(const existingWindow of CURRENT_WINDOWS) { zIndex += 1; @@ -21,69 +29,170 @@ export const DraggableWindow: FC = props => } }, []); - const onMouseDown = useCallback((event: MouseEvent) => + const onMouseDown = useCallback((event: ReactMouseEvent) => { - const index = currentWindows.indexOf(elementRef.current); + const index = CURRENT_WINDOWS.indexOf(elementRef.current); if(index === -1) { - currentWindows.push(elementRef.current); + CURRENT_WINDOWS.push(elementRef.current); } - else if(index === (currentWindows.length - 1)) return; + else if(index === (CURRENT_WINDOWS.length - 1)) return; else if(index >= 0) { - currentWindows.splice(index, 1); + CURRENT_WINDOWS.splice(index, 1); - currentWindows.push(elementRef.current); + CURRENT_WINDOWS.push(elementRef.current); } bringToTop(); }, [ bringToTop ]); + const onDragMouseDown = useCallback((event: MouseEvent) => + { + setStart({ x: event.clientX, y: event.clientY }); + setIsDragging(true); + }, []); + + const onDragMouseMove = useCallback((event: MouseEvent) => + { + setDelta({ x: (event.clientX - start.x), y: (event.clientY - start.y) }); + }, [ start ]); + + const onDragMouseUp = useCallback((event: MouseEvent) => + { + if(!elementRef.current || !dragHandler) 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; + } + + setDelta({ x: 0, y: 0 }); + setOffset({ x: offsetX, y: offsetY }); + setIsDragging(false); + + if(uniqueKey !== null) POS_MEMORY.set(uniqueKey, { x: offsetX, y: offsetY }); + }, [ dragHandler, delta, offset, uniqueKey ]); + useEffect(() => { - if(!elementRef) return; - - const element = elementRef.current; + const element = (elementRef.current as HTMLElement); - currentWindows.push(element); + if(!element) return; + + CURRENT_WINDOWS.push(element); bringToTop(); - if(!noCenter) + if(!disableDrag) { - const left = ((document.body.clientWidth / 2) - (element.clientWidth / 2)); - const top = ((document.body.clientHeight / 2) - (element.clientHeight / 2)); + const handle = (element.querySelector(handleSelector) as HTMLElement); - element.style.left = `${ left }px`; - element.style.top = `${ top }px`; - } - else - { - element.style.left = `0px`; - element.style.top = `0px`; + if(handle) setDragHandler(handle); } - element.style.visibility = 'visible'; + let offsetX = 0; + let offsetY = 0; + + switch(position) + { + case DraggableWindowPosition.TOP_CENTER: + element.style.top = '50px'; + element.style.left = `calc(50vw - ${ (element.offsetWidth / 2) }px)`; + break; + case DraggableWindowPosition.CENTER: + element.style.top = `calc(50vh - ${ (element.offsetHeight / 2) }px)`; + element.style.left = `calc(50vw - ${ (element.offsetWidth / 2) }px)`; + break; + } + + if(uniqueKey !== null) + { + const memory = POS_MEMORY.get(uniqueKey); + + if(memory) + { + offsetX = memory.x; + offsetY = memory.y; + } + } + + setDelta({ x: 0, y: 0}); + setOffset({ x: offsetX, y: offsetY }); return () => { - const index = currentWindows.indexOf(element); + const index = CURRENT_WINDOWS.indexOf(element); - if(index >= 0) currentWindows.splice(index, 1); + if(index >= 0) CURRENT_WINDOWS.splice(index, 1); } - }, [ elementRef, noCenter, bringToTop ]); + }, [ handleSelector, position, uniqueKey, disableDrag, bringToTop ]); - const getWindowContent = useMemo(() => + useEffect(() => { - return ( -
- { children } -
- ); - }, [ children, onMouseDown ]); + if(!offset && !delta) return; + + const element = (elementRef.current as HTMLElement); - return disableDrag ? getWindowContent : { getWindowContent }; + if(!element) return; + + element.style.transform = `translate(${ offset.x + delta.x }px, ${ offset.y + delta.y }px)`; + element.style.visibility = 'visible'; + }, [ offset, delta ]); + + useEffect(() => + { + if(!dragHandler) return; + + dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); + + return () => + { + dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); + } + }, [ dragHandler, onDragMouseDown ]); + + useEffect(() => + { + if(!isDragging) return; + + document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); + document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); + + return () => + { + document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); + document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); + } + }, [ isDragging, onDragMouseUp, onDragMouseMove ]); + + return ( +
+ { children } +
+ ); } diff --git a/src/layout/draggable-window/DraggableWindow.types.tsx b/src/layout/draggable-window/DraggableWindow.types.tsx index 9bba06fa..278e866a 100644 --- a/src/layout/draggable-window/DraggableWindow.types.tsx +++ b/src/layout/draggable-window/DraggableWindow.types.tsx @@ -1,11 +1,16 @@ -import { ReactNode } from 'react'; -import { DraggableProps } from 'react-draggable'; +import { Key } from 'react'; export interface DraggableWindowProps { - handle?: string; - draggableOptions?: Partial; + uniqueKey?: Key; + handleSelector?: string; + position?: string; disableDrag?: boolean; - noCenter?: boolean; - children?: ReactNode; +} + +export class DraggableWindowPosition +{ + public static CENTER: string = 'DWP_CENTER'; + public static TOP_CENTER: string = 'DWP_TOP_CENTER'; + public static NOTHING: string = 'DWP_NOTHING'; }