diff --git a/components/lib/dialog/Dialog.js b/components/lib/dialog/Dialog.js
index 6b55ba9845..1356dbda48 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 { useEventListener, useMountEffect, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks';
+import { useDraggable, 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,11 +25,10 @@ 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();
@@ -109,65 +108,6 @@ 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;
@@ -251,6 +191,7 @@ export const Dialog = React.forwardRef((props, ref) => {
const onEnter = () => {
dialogRef.current.setAttribute(attributeSelector.current, '');
+ setDraggableState(props.draggable);
};
const onEntered = () => {
@@ -274,7 +215,7 @@ export const Dialog = React.forwardRef((props, ref) => {
};
const onExited = () => {
- dragging.current = false;
+ setDraggableState(false);
ZIndexUtils.clear(maskRef.current);
setMaskVisibleState(false);
disableDocumentSettings();
@@ -305,11 +246,6 @@ export const Dialog = React.forwardRef((props, ref) => {
};
const bindGlobalListeners = () => {
- if (props.draggable) {
- bindDocumentDragListener();
- bindDocumentDragEndListener();
- }
-
if (props.resizable) {
bindDocumentResizeListener();
bindDocumentResizeEndListener();
@@ -322,8 +258,6 @@ export const Dialog = React.forwardRef((props, ref) => {
};
const unbindGlobalListeners = () => {
- unbindDocumentDragListener();
- unbindDocumentDragEndListener();
unbindDocumentResizeListener();
unbindDocumentResizEndListener();
unbindDocumentKeyDownListener();
@@ -460,7 +394,7 @@ export const Dialog = React.forwardRef((props, ref) => {
const headerClassName = classNames('p-dialog-header', props.headerClassName);
return (
-
+
diff --git a/components/lib/hooks/Hooks.js b/components/lib/hooks/Hooks.js
index 0327f1710e..35e84d63bb 100644
--- a/components/lib/hooks/Hooks.js
+++ b/components/lib/hooks/Hooks.js
@@ -1,13 +1,14 @@
-import { usePrevious } from './usePrevious';
-import { useMountEffect } from './useMountEffect';
-import { useUpdateEffect } from './useUpdateEffect';
-import { useUnmountEffect } from './useUnmountEffect';
+import { useDraggable } from './useDraggable';
import { useEventListener } from './useEventListener';
+import { useInterval } from './useInterval';
+import { useMountEffect } from './useMountEffect';
import { useOverlayListener } from './useOverlayListener';
import { useOverlayScrollListener } from './useOverlayScrollListener';
+import { usePrevious } from './usePrevious';
import { useResizeListener } from './useResizeListener';
-import { useInterval } from './useInterval';
-import { useStorage, useLocalStorage, useSessionStorage } from './useStorage';
+import { useLocalStorage, useSessionStorage, useStorage } 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 };
+export { usePrevious, useMountEffect, useUpdateEffect, useUnmountEffect, useEventListener, useOverlayListener, useOverlayScrollListener, useResizeListener, useInterval, useStorage, useLocalStorage, useSessionStorage, useTimeout, useDraggable };
diff --git a/components/lib/hooks/hooks.d.ts b/components/lib/hooks/hooks.d.ts
index 01aa810bf9..2a37b97028 100644
--- a/components/lib/hooks/hooks.d.ts
+++ b/components/lib/hooks/hooks.d.ts
@@ -22,6 +22,18 @@ 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
new file mode 100644
index 0000000000..749d672a62
--- /dev/null
+++ b/components/lib/hooks/useDraggable.js
@@ -0,0 +1,171 @@
+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 5c27fdba91..10073a1c7e 100644
--- a/components/lib/utils/DomHandler.js
+++ b/components/lib/utils/DomHandler.js
@@ -115,14 +115,10 @@ export default class DomHandler {
}
static getViewport() {
- 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 };
+ 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 };
}
static getOffset(el) {