UNPKG

@syncfusion/react-base

Version:

A common package of core React base, methods and class definitions

962 lines (961 loc) 40.3 kB
import { cloneElement, useEffect, useLayoutEffect, useRef } from 'react'; import { extend, isUndefined, isNullOrUndefined, compareElementParent, getActualElement } from './util'; import { closest, setStyleAttribute, createElement, addClass, isVisible, select, removeClass } from './dom'; import { Browser } from './browser'; import { EventHandler } from './event-handler'; import { setDragArea, elementInViewport, getDocumentWidthHeight, getCoordinates, calculateParentPosition, getPathElements, getScrollParent } from './drag-util'; import { useDragDropContext } from './dragdrop'; /** * The default position coordinates used for initializing or resetting positions. */ const defaultPosition = { left: 0, top: 0, bottom: 0, right: 0 }; /** * Drag state object to check if dragging has started. */ const isDraggedObject = { isDragged: false }; /** * Hook to manage draggable Position. * * @private * @param {Partial<IPosition>} props - Initial values for the position properties. * @returns {IPosition} - The initialized draggable position properties. */ export function DraggablePosition(props) { const propsRef = { left: 0, top: 0, ...props }; return propsRef; } /** * Draggable function provides support to enable draggable functionality in Dom Elements. * * @param {RefObject<HTMLElement>} element - The reference to the HTML element to be made draggable * @param {IDraggable} [props] - Optional properties to configure the draggable behavior * @returns {IDraggable} A Draggable object with draggable functionality */ export function useDraggable(element, props) { const droppableContext = useDragDropContext(); const propsRef = { cursorAt: DraggablePosition({}), clone: false, dragArea: null, isDragScroll: false, isReplaceDragEle: false, isPreventSelect: true, distance: 1, handle: '', abort: '', helper: null, scope: 'default', dragTarget: '', axis: null, queryPositionInfo: null, enableTailMode: false, skipDistanceCheck: false, preventDefault: true, enableAutoScroll: false, enableTapHold: false, tapHoldThreshold: 750, enableScrollHandler: false, element: element, ...props }; const propsStateRef = useRef(propsRef); /* Global Variables */ let target; let initialPosition; let relativeXPosition; let relativeYPosition; let margin; let offset; let position; let dragLimit = useDraggable.getDefaultPosition(); let borderWidth = useDraggable.getDefaultPosition(); const padding = useDraggable.getDefaultPosition(); let pageX; let diffX = 0; let prevLeft = 0; let prevTop = 0; let dragProcessStarted = false; let tapHoldTimer = null; let dragElePosition; let currentStateTarget; let externalInitialize = false; let diffY = 0; let pageY; let helperElement; let hoverObject; let parentClientRect; let parentScrollX = 0; let parentScrollY = 0; let initialScrollX = 0; let initialScrollY = 0; const droppables = {}; /** * Toggles event listeners for the draggable element. * * @param {boolean} [isUnWire] - Flag to determine if events should be removed. * @returns {void} */ function toggleEvents(isUnWire) { let ele; if (!isNullOrUndefined(propsStateRef.current.handle) && propsStateRef.current.handle !== '') { ele = select(propsStateRef.current.handle, element.current); } const handler = (propsStateRef.current.enableTapHold && Browser.isDevice && Browser.isTouch) ? mobileInitialize : initialize; if (isUnWire) { EventHandler.remove(ele || element.current, Browser.isSafari() ? 'touchstart' : Browser.touchStartEvent, handler); } else { EventHandler.add(ele || element.current, Browser.isSafari() ? 'touchstart' : Browser.touchStartEvent, handler); } } /** * Initializes drag events for mobile devices with tap hold support. * * @param {MouseEvent | TouchEvent} evt - The initial event that triggered the drag. * @returns {void} */ function mobileInitialize(evt) { const target = evt.currentTarget; tapHoldTimer = setTimeout(() => { externalInitialize = true; removeTapholdTimer(); initialize(evt, target); }, propsStateRef.current.tapHoldThreshold); EventHandler.add(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, removeTapholdTimer, this); EventHandler.add(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, removeTapholdTimer, this); } /** * Binds drag-related events to the drag target element. * * @param {HTMLElement} dragTargetElement - The element that will act as the drag target. * @returns {void} */ function bindDragEvents(dragTargetElement) { if (isVisible(dragTargetElement)) { EventHandler.add(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDrag, this); EventHandler.add(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, intDragStop, this); setGlobalDroppables(false, element.current, dragTargetElement); } else { toggleEvents(); document.body.classList.remove('sf-prevent-select'); } } /** * Removes the tap hold timer and detaches related event listeners. * * @returns {void} */ function removeTapholdTimer() { clearTimeout(tapHoldTimer); EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, removeTapholdTimer); EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, removeTapholdTimer); } /** * Retrieves the scrollable parent of a given element along a specified axis. * * @param {HTMLElement} element - The element whose scrollable parent is to be found. * @param {string} axis - The axis ('vertical' or 'horizontal') to check for scrollability. * @returns {HTMLElement | null} - The scrollable parent element, or null if none found. */ // eslint-disable-next-line const getScrollableParent = (element, axis) => { const scroll = { 'vertical': 'scrollHeight', 'horizontal': 'scrollWidth' }; const client = { 'vertical': 'clientHeight', 'horizontal': 'clientWidth' }; if (isNullOrUndefined(element)) { return null; } if (element[scroll[`${axis}`]] > (element[client[`${axis}`]])) { if (axis === 'vertical' ? element.scrollTop > 0 : element.scrollLeft > 0) { if (axis === 'vertical') { parentScrollY += (parentScrollY === 0 ? element.scrollTop : element.scrollTop - parentScrollY); } else { parentScrollX += (parentScrollX === 0 ? element.scrollLeft : element.scrollLeft - parentScrollX); } if (!isNullOrUndefined(element)) { return getScrollableParent(element.parentNode, axis); } else { return element; } } else { return getScrollableParent(element.parentNode, axis); } } else { return getScrollableParent(element.parentNode, axis); } }; /** * Calculates and stores scrollable values for the draggable element. * * @returns {void} */ function getScrollableValues() { parentScrollX = 0; parentScrollY = 0; } /** * Initializes the drag operation. * * @param {MouseEvent | TouchEvent} evt - The event that initiated the drag action. * @param {EventTarget} [curTarget] - The current target element of the event. * @returns {void} */ function initialize(evt, curTarget) { element.current = getActualElement(element); currentStateTarget = evt.target; if (isDragStarted()) { return; } else { isDragStarted(true); externalInitialize = false; } target = evt.currentTarget || curTarget; dragProcessStarted = false; if (propsStateRef.current.abort) { let abortSelectors = propsStateRef.current.abort; if (typeof abortSelectors === 'string') { abortSelectors = [abortSelectors]; } for (let i = 0; i < abortSelectors.length; i++) { if (!isNullOrUndefined(closest(evt.target, abortSelectors[`${i}`]))) { if (isDragStarted()) { isDragStarted(true); } return; } } } if (propsStateRef.current.preventDefault && !isUndefined(evt.changedTouches) && evt.type !== 'touchstart') { evt.preventDefault(); } element.current.setAttribute('aria-grabbed', 'true'); const intCoord = getCoordinates(evt); initialPosition = { x: intCoord.pageX, y: intCoord.pageY }; if (!propsStateRef.current.clone) { const pos = element.current.getBoundingClientRect(); getScrollableValues(); relativeXPosition = intCoord.pageX - (pos.left + parentScrollX); relativeYPosition = intCoord.pageY - (pos.top + parentScrollY); } if (externalInitialize) { intDragStart(evt); } else { EventHandler.add(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDragStart, this); EventHandler.add(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDestroy, this); } toggleEvents(true); if (evt.type !== 'touchstart' && propsStateRef.current.isPreventSelect) { document.body.classList.add('sf-prevent-select'); } externalInitialize = false; EventHandler.trigger(document.documentElement, Browser.isSafari() ? 'touchstart' : Browser.touchStartEvent, evt); } /** * Initiates the drag start operation. * * @param {MouseEvent | TouchEvent} evt - The event that initiates the drag start. * @returns {void} */ function intDragStart(evt) { removeTapholdTimer(); if (document.scrollingElement) { initialScrollX = document.scrollingElement.scrollLeft; initialScrollY = document.scrollingElement.scrollTop; } const isChangeTouch = !isUndefined(evt.changedTouches); if (isChangeTouch && (evt.changedTouches.length !== 1)) { return; } const intCordinate = getCoordinates(evt); let pos; const styleProp = getComputedStyle(element.current); margin = { left: parseInt(styleProp.marginLeft, 10), top: parseInt(styleProp.marginTop, 10), right: parseInt(styleProp.marginRight, 10), bottom: parseInt(styleProp.marginBottom, 10) }; let dragElement = element.current; if (propsStateRef.current.clone && propsStateRef.current.dragTarget) { const intClosest = closest(evt.target, propsStateRef.current.dragTarget); if (!isNullOrUndefined(intClosest)) { dragElement = intClosest; } } if (propsStateRef.current.isReplaceDragEle) { dragElement = currentStateCheck(evt.target, dragElement); } offset = calculateParentPosition(dragElement); position = getMousePosition(evt, propsStateRef.current.isDragScroll); const x = initialPosition.x - intCordinate.pageX; const y = initialPosition.y - intCordinate.pageY; const distance = Math.sqrt((x * x) + (y * y)); if ((distance >= propsStateRef.current.distance || externalInitialize)) { const ele = getHelperElement(evt); if (!ele) { return; } if (isChangeTouch) { evt.preventDefault(); } const dragTargetElement = helperElement = ele; parentClientRect = calculateParentPosition(dragTargetElement.offsetParent); if (propsStateRef.current && propsStateRef.current.dragStart) { const curTarget = getProperTargetElement(evt); const args = { event: evt, element: dragElement, target: curTarget, bindEvents: null, dragElement: dragTargetElement }; propsStateRef.current.dragStart(args); if (args.cancel) { propsRef.intDestroy(); return undefined; } } if (propsStateRef.current.dragArea) { setDragArea(propsStateRef.current.dragArea, helperElement, borderWidth, padding, dragLimit); } else { dragLimit = { left: 0, right: 0, bottom: 0, top: 0 }; borderWidth = { top: 0, left: 0 }; } pos = { left: position.left - parentClientRect.left, top: position.top - parentClientRect.top }; if (propsStateRef.current.clone && !propsStateRef.current.enableTailMode) { diffX = position.left - offset.left; diffY = position.top - offset.top; } getScrollableValues(); const styles = getComputedStyle(dragElement); const marginTop = parseFloat(styles.marginTop); if (propsStateRef.current.clone && marginTop !== 0) { pos.top += marginTop; } if (propsStateRef.current.enableScrollHandler && !propsStateRef.current.clone) { pos.top -= parentScrollY; pos.left -= parentScrollX; } const posValue = getProcessedPositionValue({ top: `${pos.top - diffY}px`, left: `${pos.left - diffX}px` }); if (propsStateRef.current.dragArea && typeof propsStateRef.current.dragArea !== 'string' && propsStateRef.current.dragArea.classList.contains('sf-kanban-content') && propsStateRef.current.dragArea.style.position === 'relative') { pos.top += propsStateRef.current.dragArea.scrollTop; } dragElePosition = { top: pos.top, left: pos.left }; setStyleAttribute(dragTargetElement, getDragPosition({ position: 'absolute', left: posValue.left, top: posValue.top })); EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDragStart); EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDestroy); bindDragEvents(dragTargetElement); } } /** * Initializes the variables and manages the drag operation progress. * * @param {MouseEvent |TouchEvent} evt - The event that triggers the drag. * @returns {void} */ function intDrag(evt) { if (!isUndefined(evt.changedTouches) && (evt.changedTouches.length !== 1)) { return; } if (propsStateRef.current.clone && evt.changedTouches && Browser.isDevice && Browser.isTouch) { evt.preventDefault(); } let left; let top; position = getMousePosition(evt, propsStateRef.current.isDragScroll); const docHeight = getDocumentWidthHeight('Height'); if (docHeight < position.top) { position.top = docHeight; } const docWidth = getDocumentWidthHeight('Width'); if (docWidth < position.left) { position.left = docWidth; } if (propsStateRef.current && propsStateRef.current.drag) { const curTarget = getProperTargetElement(evt); propsStateRef.current.drag({ event: evt, element: element.current, target: curTarget }); } const eleObj = checkTargetElement(evt); if (eleObj.target && eleObj.instance) { let flag = true; if (hoverObject) { if (hoverObject.instance !== eleObj.instance) { triggerOutFunction(evt, eleObj); } else { flag = false; } } if (flag) { eleObj.instance.dragData[propsStateRef.current.scope] = droppables[propsStateRef.current.scope]; eleObj.instance.intOver(evt, eleObj.target); hoverObject = eleObj; } } else if (hoverObject) { triggerOutFunction(evt, eleObj); } const helperElement = droppables[propsStateRef.current.scope].helper; parentClientRect = calculateParentPosition(helperElement.offsetParent); const tLeft = parentClientRect.left; const tTop = parentClientRect.top; const intCoord = getCoordinates(evt); const pagex = intCoord.pageX; const pagey = intCoord.pageY; const dLeft = position.left - diffX; const dTop = position.top - diffY; const styles = getComputedStyle(helperElement); if (propsStateRef.current.dragArea) { if (propsStateRef.current.enableAutoScroll) { setDragArea(propsStateRef.current.dragArea, helperElement, borderWidth, padding, dragLimit); } if (pageX !== pagex || propsStateRef.current.skipDistanceCheck) { const helperWidth = helperElement.offsetWidth + (parseFloat(styles.marginLeft) + parseFloat(styles.marginRight)); if (dragLimit.left > dLeft && dLeft > 0) { left = dragLimit.left; } else if (dragLimit.right + window.pageXOffset < dLeft + helperWidth && dLeft > 0) { left = dLeft - (dLeft - dragLimit.right) + window.pageXOffset - helperWidth; } else { left = dLeft < 0 ? dragLimit.left : dLeft; } } if (pageY !== pagey || propsStateRef.current.skipDistanceCheck) { const helperHeight = helperElement.offsetHeight + (parseFloat(styles.marginTop) + parseFloat(styles.marginBottom)); if (dragLimit.top > dTop && dTop > 0) { top = dragLimit.top; } else if (dragLimit.bottom + window.pageYOffset < dTop + helperHeight && dTop > 0) { top = dTop - (dTop - dragLimit.bottom) + window.pageYOffset - helperHeight; } else { top = dTop < 0 ? dragLimit.top : dTop; } } } else { left = dLeft; top = dTop; } const iTop = tTop + borderWidth.top; const iLeft = tLeft + borderWidth.left; if (dragProcessStarted) { if (isNullOrUndefined(top)) { top = prevTop; } if (isNullOrUndefined(left)) { left = prevLeft; } } let draEleTop; let draEleLeft; if (helperElement.classList.contains('sf-treeview')) { if (propsStateRef.current.dragArea) { dragLimit.top = propsStateRef.current.clone ? dragLimit.top : 0; draEleTop = (top - iTop) < 0 ? dragLimit.top : (top - borderWidth.top); draEleLeft = (left - iLeft) < 0 ? dragLimit.left : (left - borderWidth.left); } else { draEleTop = top - borderWidth.top; draEleLeft = left - borderWidth.left; } } else { if (propsStateRef.current.dragArea) { const isDialogEle = helperElement.classList.contains('sf-dialog'); dragLimit.top = propsStateRef.current.clone ? dragLimit.top : 0; draEleTop = (top - iTop) < 0 ? dragLimit.top : (top - iTop); draEleLeft = (left - iLeft) < 0 ? isDialogEle ? (left - (iLeft - borderWidth.left)) : dragElePosition.left : (left - iLeft); } else { draEleTop = top - iTop; draEleLeft = left - iLeft; } } const marginTop = parseFloat(getComputedStyle(element.current).marginTop); if (marginTop > 0) { if (propsStateRef.current.clone) { draEleTop += marginTop; if (dTop < 0) { if ((marginTop + dTop) >= 0) { draEleTop = marginTop + dTop; } else { draEleTop -= marginTop; } } if (propsStateRef.current.dragArea) { draEleTop = (dragLimit.bottom < draEleTop) ? dragLimit.bottom : draEleTop; } } if ((top - iTop) < 0) { if (dTop + marginTop + (helperElement.offsetHeight - iTop) >= 0) { const tempDraEleTop = dragLimit.top + dTop - iTop; if ((tempDraEleTop + marginTop + iTop) < 0) { draEleTop -= marginTop + iTop; } else { draEleTop = tempDraEleTop; } } else { draEleTop -= marginTop + iTop; } } } if (propsStateRef.current.dragArea && helperElement.classList.contains('sf-treeview')) { const helperHeight = helperElement.offsetHeight + (parseFloat(styles.marginTop) + parseFloat(styles.marginBottom)); draEleTop = (draEleTop + helperHeight) > dragLimit.bottom ? (dragLimit.bottom - helperHeight) : draEleTop; } if (propsStateRef.current.enableScrollHandler && !propsStateRef.current.clone) { draEleTop -= parentScrollY; draEleLeft -= parentScrollX; } if (propsStateRef.current.dragArea && typeof propsStateRef.current.dragArea !== 'string' && propsStateRef.current.dragArea.classList.contains('sf-kanban-content') && propsStateRef.current.dragArea.style.position === 'relative') { draEleTop += propsStateRef.current.dragArea.scrollTop; } const dragValue = getProcessedPositionValue({ top: draEleTop + 'px', left: draEleLeft + 'px' }); setStyleAttribute(helperElement, getDragPosition(dragValue)); if (!elementInViewport(helperElement) && propsStateRef.current.enableAutoScroll && !helperElement.classList.contains('sf-treeview')) { helperElement.scrollIntoView(); } let elements = document.querySelectorAll(':hover'); if (propsStateRef.current.enableAutoScroll && helperElement.classList.contains('sf-treeview')) { if (elements.length === 0) { elements = getPathElements(evt); } let scrollParent = getScrollParent(elements, false); if (elementInViewport(helperElement)) { getScrollPosition(scrollParent, draEleTop); } else if (!elementInViewport(helperElement)) { elements = [].slice.call(document.querySelectorAll(':hover')); if (elements.length === 0) { elements = getPathElements(evt); } scrollParent = getScrollParent(elements, true); getScrollPosition(scrollParent, draEleTop); } } dragProcessStarted = true; prevLeft = left; prevTop = top; position.left = left; position.top = top; pageX = pagex; pageY = pagey; } /** * Stops the drag operation and performs cleanup. * * @param {MouseEvent | TouchEvent} evt - The event that initiated the drag stop. * @returns {void} */ function intDragStop(evt) { dragProcessStarted = false; initialScrollX = 0; initialScrollY = 0; if (!isUndefined(evt.changedTouches) && (evt.changedTouches.length !== 1)) { return; } const type = ['touchend', 'pointerup', 'mouseup']; if (type.indexOf(evt.type) !== -1) { if (propsStateRef.current && propsStateRef.current.dragStop) { const curTarget = getProperTargetElement(evt); propsStateRef.current.dragStop({ event: evt, element: element.current, target: curTarget, helper: helperElement }); } propsRef.intDestroy(); } else { element.current.setAttribute('aria-grabbed', 'false'); } const eleObj = checkTargetElement(evt); if (eleObj.target && eleObj.instance) { eleObj.instance.dragStopCalled = true; eleObj.instance.dragData[propsStateRef.current.scope] = droppables[propsStateRef.current.scope]; eleObj.instance.intDrop(evt, eleObj.target); } setGlobalDroppables(true); document.body.classList.remove('sf-prevent-select'); } /** * Method to bind events. * * @returns {void} */ function bind() { toggleEvents(); if (Browser.isIE) { addClass([propsRef.element.current], 'sf-block-touch'); } droppables[propsStateRef.current.scope] = {}; } /** * Destroys the draggable instance by removing event listeners and cleaning up resources. * * @returns {void} */ propsRef.intDestroy = () => { dragProcessStarted = false; toggleEvents(); document.body.classList.remove('sf-prevent-select'); element.current.setAttribute('aria-grabbed', 'false'); EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDragStart); EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, intDragStop); EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDestroy); EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDrag); if (isDragStarted()) { isDragStarted(true); } }; /** * Method to clean up and remove event handlers on the component destruction. * * @returns {void} */ propsRef.destroy = () => { toggleEvents(true); }; /** * Triggers the out function for the previous hover target when a new draggable * target is detected or when the pointer is out of the current drop zone. * * @param {MouseEvent | TouchEvent} evt - The event object. * @param {DropObject} eleObj - The drop object containing target and instance. * @returns {void} */ function triggerOutFunction(evt, eleObj) { hoverObject.instance.intOut(evt, eleObj.target); hoverObject.instance.dragData[propsStateRef.current.scope] = null; hoverObject = null; } /** * Checks and retrieves the correct target element under the pointer during a drag operation. * * @param {MouseEvent | TouchEvent} evt - The event object. * @returns {HTMLElement} - The correct target element. */ function getProperTargetElement(evt) { const intCoord = getCoordinates(evt); let ele; const prevStyle = helperElement.style.pointerEvents || ''; const isPointer = evt.type.indexOf('pointer') !== -1 && Browser.info.name === 'safari' && parseInt(Browser.info.version, 10) > 12; if (compareElementParent(evt.target, helperElement) || evt.type.indexOf('touch') !== -1 || isPointer) { helperElement.style.pointerEvents = 'none'; ele = document.elementFromPoint(intCoord.clientX, intCoord.clientY); helperElement.style.pointerEvents = prevStyle; } else { ele = evt.target; } return ele; } /** * Retrieves the position of the mouse or touch event relative to the document or parent element. * * @param {MouseEvent | TouchEvent} evt - The drag event. * @param {boolean} [isdragscroll] - Indicates if the dragging is performed with scrolling. * @returns {IPosition} - The left and top coordinates of the drag event. */ function getMousePosition(evt, isdragscroll) { const dragEle = evt.srcElement !== undefined ? evt.srcElement : evt.target; const intCoord = getCoordinates(evt); let pageX; let pageY; const isOffsetParent = isNullOrUndefined(dragEle.offsetParent); if (isdragscroll) { pageX = propsStateRef.current.clone ? intCoord.pageX : (intCoord.pageX + (isOffsetParent ? 0 : dragEle.offsetParent.scrollLeft)) - relativeXPosition; pageY = propsStateRef.current.clone ? intCoord.pageY : (intCoord.pageY + (isOffsetParent ? 0 : dragEle.offsetParent.scrollTop)) - relativeYPosition; if (!propsStateRef.current.clone) { const offsetParent = dragEle.offsetParent; if (!isOffsetParent && offsetParent) { const currentScrollLeft = offsetParent.scrollLeft; const currentScrollTop = offsetParent.scrollTop; const scrollDeltaX = currentScrollLeft - initialScrollX; const scrollDeltaY = currentScrollTop - initialScrollY; pageX = pageX - scrollDeltaX; pageY = pageY - scrollDeltaY; } } } else { pageX = propsStateRef.current.clone ? intCoord.pageX : (intCoord.pageX + window.pageXOffset) - relativeXPosition; pageY = propsStateRef.current.clone ? intCoord.pageY : (intCoord.pageY + window.pageYOffset) - relativeYPosition; if (document.scrollingElement && (!propsStateRef.current.clone)) { const ele = document.scrollingElement; const currentScrollX = ele.scrollLeft; const currentScrollY = ele.scrollTop; const scrollDeltaX = currentScrollX - initialScrollX; const scrollDeltaY = currentScrollY - initialScrollY; pageX = pageX - scrollDeltaX; pageY = pageY - scrollDeltaY; } } return { left: pageX - (margin.left + propsStateRef.current.cursorAt.left), top: pageY - (margin.top + propsStateRef.current.cursorAt.top) }; } /** * Retrieves or creates the helper element for the drag operation. * * @param {MouseEvent | TouchEvent} evt - The event triggering the drag. * @returns {HTMLElement} - The helper element used during dragging. */ function getHelperElement(evt) { let element; if (propsStateRef.current.clone) { if (propsStateRef.current && propsStateRef.current.helper) { element = propsStateRef.current.helper({ sender: evt, element: target }); } else { element = createElement('div', { className: 'sf-drag-helper sf-block-touch', innerHTML: 'Draggable' }); document.body.appendChild(element); } } else { element = propsRef.element.current; } return element; } /** * Sets the global drop object for the current scope, managing the relationship * between draggable and droppable elements. * * @param {boolean} reset - Whether to reset or set the droppable object. * @param {HTMLElement} [drag] - The current draggable element. * @param {HTMLElement} [helper] - The helper element used during dragging. * @returns {void} */ function setGlobalDroppables(reset, drag, helper) { droppables[propsStateRef.current.scope] = reset ? null : { draggable: drag, helper: helper, draggedElement: propsRef.element.current }; } /** * Checks and retrieves the drop target and its associated droppable instance. * * @param {MouseEvent | TouchEvent} evt - The event object. * @returns {DropObject} - Contains the drop target and the droppable instance. */ function checkTargetElement(evt) { const dropTarget = getProperTargetElement(evt); let dropInstance = getDropInstance(dropTarget); if (!dropInstance && dropTarget && !isNullOrUndefined(dropTarget.parentNode)) { const parent = closest(dropTarget.parentNode, '.sf-droppable') || dropTarget.parentElement; if (parent) { dropInstance = getDropInstance(parent); } } return { target: dropTarget, instance: dropInstance }; } /** * Retrieves the drop instance associated with a DOM element. * * @param {Element} ele - The DOM element to find the drop instance for * @returns {DropOption} The drop instance if found, otherwise undefined */ function getDropInstance(ele) { let droppables; let dropInstance; if (droppableContext) { const { getAllDroppables } = droppableContext; droppables = getAllDroppables(); for (const id in droppables) { if (Object.prototype.hasOwnProperty.call(droppables, id)) { const instance = droppables[`${id}`]; if (instance.element && instance.element.current === ele) { dropInstance = instance; break; } } } } else { return undefined; } return dropInstance; } /** * Checks if the dragging has started and toggles the isDragged state. * * @param {boolean} [change] - Optional flag to change the drag state. * @returns {boolean} - The current drag state. */ function isDragStarted(change) { if (change) { isDraggedObject.isDragged = !isDraggedObject.isDragged; } return isDraggedObject.isDragged; } /** * Processes the position values of a draggable element. If a custom * queryPositionInfo function is provided, it will use that to process * the position. Otherwise, it returns the original value. * * @param {DragPosition} value - The position values (left and top) to be processed. * @returns {DragPosition} - The processed or original position values. */ function getProcessedPositionValue(value) { if (propsStateRef.current && propsStateRef.current.queryPositionInfo) { return propsStateRef.current.queryPositionInfo(value); } return value; } /** * Computes the drag position of an element based on specified constraints or axis limitations. * * @param {DragPosition | { position: string }} dragValue - The raw drag position values. * @returns {Record<string, string | number>} - Adjusted drag position values with applied constraints. */ function getDragPosition(dragValue) { const temp = { ...dragValue }; if (propsStateRef.current.axis) { if (propsStateRef.current.axis === 'x') { delete temp.top; } else if (propsStateRef.current.axis === 'y') { delete temp.left; } } return temp; } /** * Adjusts the scroll position of a parent element to ensure the draggable element * remains visible during scrolling. * * @param {Element} nodeEle - The element intended to be scrolled. * @param {number} draEleTop - The top position of the draggable element. * @returns {void} */ function getScrollPosition(nodeEle, draEleTop) { if (nodeEle === document.scrollingElement) { if ((nodeEle.clientHeight + nodeEle.scrollTop - helperElement.clientHeight) < draEleTop && nodeEle.getBoundingClientRect().height + parentClientRect.top > draEleTop) { nodeEle.scrollTop += helperElement.clientHeight; } else if (nodeEle.scrollTop > draEleTop - helperElement.clientHeight) { nodeEle.scrollTop -= helperElement.clientHeight; } } else if (nodeEle) { const docScrollTop = document.scrollingElement.scrollTop; const helperClientHeight = helperElement.clientHeight; if ((nodeEle.clientHeight + nodeEle.getBoundingClientRect().top - helperClientHeight + docScrollTop) < draEleTop) { nodeEle.scrollTop += helperElement.clientHeight; } else if (nodeEle.getBoundingClientRect().top > (draEleTop - helperClientHeight - docScrollTop)) { nodeEle.scrollTop -= helperElement.clientHeight; } } } /** * Checks and returns the appropriate current state element. * Determines whether to use the current state's target or revert to a specified element. * * @param {HTMLElement} ele - The current element. * @param {HTMLElement} [oldEle] - The previous element, if any. * @returns {HTMLElement} - The element considered to be in the current state. */ function currentStateCheck(ele, oldEle) { let elem; if (!isNullOrUndefined(currentStateTarget) && currentStateTarget !== ele) { elem = currentStateTarget; } else { elem = !isNullOrUndefined(oldEle) ? oldEle : ele; } return elem; } useLayoutEffect(() => { propsRef.element.current = getActualElement(propsRef.element); if (!propsRef.element.current) { return undefined; } propsStateRef.current = { ...propsRef, ...props }; addClass([propsRef.element.current], ['sf-lib', 'sf-draggable']); bind(); return () => { if (propsRef.element.current) { removeClass([propsRef.element.current], ['sf-lib', 'sf-draggable']); } propsRef.destroy(); }; }); return propsRef; } /** * Retrieves the default position coordinates. * * @returns {PositionCoordinates} - The default position coordinates with left, top, bottom, and right set to 0. */ useDraggable.getDefaultPosition = () => { return extend({}, defaultPosition); }; /** * DraggableComponent wraps elements to enable draggable functionality. * It leverages the Draggable hook internally to manage drag behavior. * * @example * ```tsx * import { Draggable } from '@syncfusion/react-base'; * * <Draggable> * <div>Drag me</div> * </Draggable> * ``` * * @param {IDraggableProps} props - The props for the DraggableComponent. * @returns {Element} The rendered draggable component. */ export const Draggable = ({ children, className, dragRef, ...restProps }) => { const internalRef = useRef(null); const draggableInstance = useRef(null); useDraggable(internalRef, { clone: false, ...restProps }); useEffect(() => { if (draggableInstance.current) { Object.assign(draggableInstance.current, { ...restProps }); } }, [restProps]); const combinedRef = (node) => { if (node) { internalRef.current = node; if (dragRef && 'current' in dragRef) { dragRef.current = node; } } }; return cloneElement(children, { ref: combinedRef, className: [ children.props.className, className ].filter(Boolean).join(' ') || undefined }); };