diff --git a/components/lib/dialog/Dialog.js b/components/lib/dialog/Dialog.js index 1356dbda48..6b55ba9845 100644 --- a/components/lib/dialog/Dialog.js +++ b/components/lib/dialog/Dialog.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PrimeReact, { localeOption } from '../api/Api'; import { CSSTransition } from '../csstransition/CSSTransition'; -import { useDraggable, useEventListener, useMountEffect, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks'; +import { useEventListener, useMountEffect, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks'; import { Portal } from '../portal/Portal'; import { Ripple } from '../ripple/Ripple'; import { classNames, DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from '../utils/Utils'; @@ -11,13 +11,13 @@ export const Dialog = React.forwardRef((props, ref) => { const [maskVisibleState, setMaskVisibleState] = React.useState(false); const [visibleState, setVisibleState] = React.useState(false); const [maximizedState, setMaximizedState] = React.useState(props.maximized); - const [draggableState, setDraggableState] = React.useState(false); const dialogRef = React.useRef(null); - const headerRef = React.useRef(null); const maskRef = React.useRef(null); const contentRef = React.useRef(null); + const headerRef = React.useRef(null); const footerRef = React.useRef(null); const closeRef = React.useRef(null); + const dragging = React.useRef(false); const resizing = React.useRef(false); const lastPageX = React.useRef(null); const lastPageY = React.useRef(null); @@ -25,10 +25,11 @@ export const Dialog = React.forwardRef((props, ref) => { const attributeSelector = React.useRef(''); const maximized = props.onMaximize ? props.maximized : maximizedState; - const draggable = useDraggable({ targetRef: dialogRef, handleRef: headerRef, onDragStart: props.onDragStart, onDrag: props.onDrag, onDragEnd: props.onDragEnd, enabled: draggableState, keepInViewport: props.keepInViewport }); const [bindDocumentKeyDownListener, unbindDocumentKeyDownListener] = useEventListener({ type: 'keydown', listener: (event) => onKeyDown(event) }); const [bindDocumentResizeListener, unbindDocumentResizeListener] = useEventListener({ type: 'mousemove', target: () => window.document, listener: (event) => onResize(event) }); const [bindDocumentResizeEndListener, unbindDocumentResizEndListener] = useEventListener({ type: 'mouseup', target: () => window.document, listener: (event) => onResizeEnd(event) }); + const [bindDocumentDragListener, unbindDocumentDragListener] = useEventListener({ type: 'mousemove', target: () => window.document, listener: (event) => onDrag(event) }); + const [bindDocumentDragEndListener, unbindDocumentDragEndListener] = useEventListener({ type: 'mouseup', target: () => window.document, listener: (event) => onDragEnd(event) }); const onClose = (event) => { props.onHide(); @@ -108,6 +109,65 @@ export const Dialog = React.forwardRef((props, ref) => { } }; + const onDragStart = (event) => { + if (DomHandler.hasClass(event.target, 'p-dialog-header-icon') || DomHandler.hasClass(event.target.parentElement, 'p-dialog-header-icon')) { + return; + } + + if (props.draggable) { + dragging.current = true; + lastPageX.current = event.pageX; + lastPageY.current = event.pageY; + dialogRef.current.style.margin = '0'; + DomHandler.addClass(document.body, 'p-unselectable-text'); + + props.onDragStart && props.onDragStart(event); + } + }; + + const onDrag = (event) => { + if (dragging.current) { + const width = DomHandler.getOuterWidth(dialogRef.current); + const height = DomHandler.getOuterHeight(dialogRef.current); + const deltaX = event.pageX - lastPageX.current; + const deltaY = event.pageY - lastPageY.current; + const offset = dialogRef.current.getBoundingClientRect(); + const leftPos = offset.left + deltaX; + const topPos = offset.top + deltaY; + const viewport = DomHandler.getViewport(); + + dialogRef.current.style.position = 'fixed'; + + if (props.keepInViewport) { + if (leftPos >= props.minX && leftPos + width < viewport.width) { + lastPageX.current = event.pageX; + dialogRef.current.style.left = leftPos + 'px'; + } + + if (topPos >= props.minY && topPos + height < viewport.height) { + lastPageY.current = event.pageY; + dialogRef.current.style.top = topPos + 'px'; + } + } else { + lastPageX.current = event.pageX; + dialogRef.current.style.left = leftPos + 'px'; + lastPageY.current = event.pageY; + dialogRef.current.style.top = topPos + 'px'; + } + + props.onDrag && props.onDrag(event); + } + }; + + const onDragEnd = (event) => { + if (dragging.current) { + dragging.current = false; + DomHandler.removeClass(document.body, 'p-unselectable-text'); + + props.onDragEnd && props.onDragEnd(event); + } + }; + const onResizeStart = (event) => { if (props.resizable) { resizing.current = true; @@ -191,7 +251,6 @@ export const Dialog = React.forwardRef((props, ref) => { const onEnter = () => { dialogRef.current.setAttribute(attributeSelector.current, ''); - setDraggableState(props.draggable); }; const onEntered = () => { @@ -215,7 +274,7 @@ export const Dialog = React.forwardRef((props, ref) => { }; const onExited = () => { - setDraggableState(false); + dragging.current = false; ZIndexUtils.clear(maskRef.current); setMaskVisibleState(false); disableDocumentSettings(); @@ -246,6 +305,11 @@ export const Dialog = React.forwardRef((props, ref) => { }; const bindGlobalListeners = () => { + if (props.draggable) { + bindDocumentDragListener(); + bindDocumentDragEndListener(); + } + if (props.resizable) { bindDocumentResizeListener(); bindDocumentResizeEndListener(); @@ -258,6 +322,8 @@ export const Dialog = React.forwardRef((props, ref) => { }; const unbindGlobalListeners = () => { + unbindDocumentDragListener(); + unbindDocumentDragEndListener(); unbindDocumentResizeListener(); unbindDocumentResizEndListener(); unbindDocumentKeyDownListener(); @@ -394,7 +460,7 @@ export const Dialog = React.forwardRef((props, ref) => { const headerClassName = classNames('p-dialog-header', props.headerClassName); return ( -
+
{header}
diff --git a/components/lib/hooks/Hooks.js b/components/lib/hooks/Hooks.js index 35e84d63bb..0327f1710e 100644 --- a/components/lib/hooks/Hooks.js +++ b/components/lib/hooks/Hooks.js @@ -1,14 +1,13 @@ -import { useDraggable } from './useDraggable'; -import { useEventListener } from './useEventListener'; -import { useInterval } from './useInterval'; +import { usePrevious } from './usePrevious'; import { useMountEffect } from './useMountEffect'; +import { useUpdateEffect } from './useUpdateEffect'; +import { useUnmountEffect } from './useUnmountEffect'; +import { useEventListener } from './useEventListener'; import { useOverlayListener } from './useOverlayListener'; import { useOverlayScrollListener } from './useOverlayScrollListener'; -import { usePrevious } from './usePrevious'; import { useResizeListener } from './useResizeListener'; -import { useLocalStorage, useSessionStorage, useStorage } from './useStorage'; +import { useInterval } from './useInterval'; +import { useStorage, useLocalStorage, useSessionStorage } from './useStorage'; import { useTimeout } from './useTimeout'; -import { useUnmountEffect } from './useUnmountEffect'; -import { useUpdateEffect } from './useUpdateEffect'; -export { usePrevious, useMountEffect, useUpdateEffect, useUnmountEffect, useEventListener, useOverlayListener, useOverlayScrollListener, useResizeListener, useInterval, useStorage, useLocalStorage, useSessionStorage, useTimeout, useDraggable }; +export { usePrevious, useMountEffect, useUpdateEffect, useUnmountEffect, useEventListener, useOverlayListener, useOverlayScrollListener, useResizeListener, useInterval, useStorage, useLocalStorage, useSessionStorage, useTimeout }; diff --git a/components/lib/hooks/hooks.d.ts b/components/lib/hooks/hooks.d.ts index 2a37b97028..01aa810bf9 100644 --- a/components/lib/hooks/hooks.d.ts +++ b/components/lib/hooks/hooks.d.ts @@ -22,18 +22,6 @@ interface ResizeEventOptions { listener?(event: Event): void; } -interface DraggableOptions { - targetRef: React.Ref; - handleRef: React.Ref; - onDrag?(e: React.DragEvent): void; - onDragEnd?(e: React.DragEvent): void; - onDragStart?(e: React.DragEvent): void; - enabled: true; - keepInViewport: false; - rectLimits?: DOMRect; -} - -export declare function useDraggable(options: DraggableOptions): any; export declare function usePrevious(value: any): any; export declare function useMountEffect(effect: React.EffectCallback): void; export declare function useUpdateEffect(effect: React.EffectCallback, deps?: React.DependencyList): void; diff --git a/components/lib/hooks/useDraggable.js b/components/lib/hooks/useDraggable.js deleted file mode 100644 index 749d672a62..0000000000 --- a/components/lib/hooks/useDraggable.js +++ /dev/null @@ -1,171 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { DomHandler } from '../utils/Utils'; - -/** - * Hook to wrap up draggable logic for dialogs. - * - * @param targetRef the target ref of the draggable - * @param handleRef the handle ref of the draggable - * @param onDragStart callback - * @param onDrag callback - * @param onDragEnd callback - * @param enabled boolean whether this hook is active or not - * @param keepInViewport should the draggable be contained by the viewport - * @param rectLimits a bounding box to limit the draggable to - * @returns { dragging, delta, resetState } - */ -export const useDraggable = ({ targetRef, handleRef, onDragStart, onDragEnd, onDrag, enabled = true, keepInViewport = false, rectLimits }) => { - const [dragging, setDragging] = useState(false); - const [previous, setPrevious] = useState({ x: 0, y: 0 }); - const [delta, setDelta] = useState({ x: 0, y: 0 }); - const initial = useRef({ x: 0, y: 0 }); - const limits = useRef(null); - - /** - * Subscribe to mouse/touch events to start dragging. - */ - useEffect(() => { - const handle = handleRef.current || targetRef.current; - - if (!handle || !enabled) { - return; - } - - handle.addEventListener('mousedown', startDragging); - handle.addEventListener('touchstart', startDragging); - - return () => { - handle.removeEventListener('mousedown', startDragging); - handle.removeEventListener('touchstart', startDragging); - }; - - function startDragging(event) { - setDragging(true); - event.preventDefault(); - targetRef.current.style.willChange = 'transform'; - const source = (event.touches && event.touches[0]) || event; - - initial.current = { x: source.clientX, y: source.clientY }; - - if (keepInViewport || rectLimits) { - const { left, top, width, height } = targetRef.current.getBoundingClientRect(); - const viewport = DomHandler.getViewport(); - - if (keepInViewport) { - limits.current = { - minX: -left + delta.x, - maxX: viewport.width - width - left + delta.x, - minY: -top + delta.y, - maxY: viewport.height - height - top + delta.y - }; - } else { - limits.current = { - minX: rectLimits.left - left + delta.x, - maxX: rectLimits.right - width - left + delta.x, - minY: rectLimits.top - top + delta.y, - maxY: rectLimits.bottom - height - top + delta.y - }; - } - } - - onDragStart && onDragStart(event); - } - }, [targetRef, handleRef, onDragStart, enabled, keepInViewport, delta, rectLimits]); - - /** - * Subscribe to mouse/touch events to drag and stop dragging. - */ - useEffect(() => { - if (dragging) { - document.addEventListener('mousemove', reposition, { passive: true }); - document.addEventListener('touchmove', reposition, { passive: true }); - document.addEventListener('mouseup', stopDragging); - document.addEventListener('touchend', stopDragging); - } else { - document.removeEventListener('mousemove', reposition, { passive: true }); - document.removeEventListener('mouseup', stopDragging); - document.removeEventListener('touchmove', reposition, { passive: true }); - document.removeEventListener('touchend', stopDragging); - } - - return () => { - document.removeEventListener('mousemove', reposition, { passive: true }); - document.removeEventListener('mouseup', stopDragging); - document.removeEventListener('touchmove', reposition, { passive: true }); - document.removeEventListener('touchend', stopDragging); - }; - - function stopDragging(event) { - event.preventDefault(); - targetRef.current.style.willChange = ''; - onDragEnd && onDragEnd(event); - - setDragging(false); - setPrevious(reposition(event)); - } - - function reposition(event) { - const source = (event.changedTouches && event.changedTouches[0]) || (event.touches && event.touches[0]) || event; - const { clientX, clientY } = source; - const x = clientX - initial.current.x + previous.x; - const y = clientY - initial.current.y + previous.y; - - const newDelta = calculateDelta({ x, y, limits: limits.current }); - - setDelta(newDelta); - onDrag && onDrag(event); - - return newDelta; - } - }, [targetRef, onDrag, onDragEnd, handleRef, dragging, previous, keepInViewport, rectLimits]); - - /** - * Listen to delta drag changes and set the target position. - */ - useEffect(() => { - if (targetRef.current) { - targetRef.current.style.transform = `translate(${delta.x}px, ${delta.y}px)`; - } - }, [targetRef, delta]); - - /** - * Listen to drag start/stop and update DOM values. - */ - useEffect(() => { - const handle = handleRef.current || targetRef.current; - - if (handle) { - handle.style.cursor = dragging ? 'grabbing' : 'move'; - } - - if (targetRef.current) { - targetRef.current.setAttribute('aria-grabbed', dragging); - } - - if (dragging) { - DomHandler.addClass(document.body, 'p-unselectable-text'); - } else { - DomHandler.removeClass(document.body, 'p-unselectable-text'); - } - }, [targetRef, handleRef, dragging]); - - const calculateDelta = ({ x, y, limits }) => { - if (!limits) { - return { x, y }; - } - - const { minX, maxX, minY, maxY } = limits; - - return { - x: Math.min(Math.max(x, minX), maxX), - y: Math.min(Math.max(y, minY), maxY) - }; - }; - - const resetState = useCallback(() => { - setDelta({ x: 0, y: 0 }); - setPrevious({ x: 0, y: 0 }); - }, [setDelta, setPrevious]); - - return { dragging, delta, resetState }; -}; diff --git a/components/lib/utils/DomHandler.js b/components/lib/utils/DomHandler.js index 10073a1c7e..5c27fdba91 100644 --- a/components/lib/utils/DomHandler.js +++ b/components/lib/utils/DomHandler.js @@ -115,10 +115,14 @@ export default class DomHandler { } static getViewport() { - const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); - const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); - - return { width: vw, height: vh }; + let win = window, + d = document, + e = d.documentElement, + g = d.getElementsByTagName('body')[0], + w = win.innerWidth || e.clientWidth || g.clientWidth, + h = win.innerHeight || e.clientHeight || g.clientHeight; + + return { width: w, height: h }; } static getOffset(el) {