@syncfusion/react-base
Version:
A common package of core React base, methods and class definitions
925 lines (924 loc) • 38 kB
JavaScript
import { useLayoutEffect } from 'react';
import { extend, isUndefined, isNullOrUndefined, compareElementParent } from './util';
import { closest, setStyleAttribute, createElement, addClass, isVisible, select } 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 Position.
*
* @private
* @param {Partial<IPosition>} props - Initial values for the position properties.
* @returns {IPosition} - The initialized position properties.
*/
export function Position(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: Position({}),
clone: true,
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
};
/* 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(propsRef.handle) && propsRef.handle !== '') {
ele = select(propsRef.handle, element.current);
}
const handler = (propsRef.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);
}, propsRef.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 (propsRef.abort) {
let abortSelectors = propsRef.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 (propsRef.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 (!propsRef.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' && propsRef.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 (propsRef.clone && propsRef.dragTarget) {
const intClosest = closest(evt.target, propsRef.dragTarget);
if (!isNullOrUndefined(intClosest)) {
dragElement = intClosest;
}
}
if (propsRef.isReplaceDragEle) {
dragElement = currentStateCheck(evt.target, dragElement);
}
offset = calculateParentPosition(dragElement);
position = getMousePosition(evt, propsRef.isDragScroll);
const x = initialPosition.x - intCordinate.pageX;
const y = initialPosition.y - intCordinate.pageY;
const distance = Math.sqrt((x * x) + (y * y));
if ((distance >= propsRef.distance || externalInitialize)) {
const ele = getHelperElement(evt);
if (!ele) {
return;
}
if (isChangeTouch) {
evt.preventDefault();
}
const dragTargetElement = helperElement = ele;
parentClientRect = calculateParentPosition(dragTargetElement.offsetParent);
if (propsRef.dragStart) {
const curTarget = getProperTargetElement(evt);
const args = {
event: evt,
element: dragElement,
target: curTarget,
bindEvents: null,
dragElement: dragTargetElement
};
propsRef.dragStart(args);
if (args.cancel) {
return undefined;
}
}
if (propsRef.dragArea) {
setDragArea(propsRef.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 (propsRef.clone && !propsRef.enableTailMode) {
diffX = position.left - offset.left;
diffY = position.top - offset.top;
}
getScrollableValues();
const styles = getComputedStyle(dragElement);
const marginTop = parseFloat(styles.marginTop);
if (propsRef.clone && marginTop !== 0) {
pos.top += marginTop;
}
if (propsRef.enableScrollHandler && !propsRef.clone) {
pos.top -= parentScrollY;
pos.left -= parentScrollX;
}
const posValue = getProcessedPositionValue({
top: `${pos.top - diffY}px`,
left: `${pos.left - diffX}px`
});
if (propsRef.dragArea && typeof propsRef.dragArea !== 'string' && propsRef.dragArea.classList.contains('sf-kanban-content') && propsRef.dragArea.style.position === 'relative') {
pos.top += propsRef.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 (propsRef.clone && evt.changedTouches && Browser.isDevice && Browser.isTouch) {
evt.preventDefault();
}
let left;
let top;
position = getMousePosition(evt, propsRef.isDragScroll);
const docHeight = getDocumentWidthHeight('Height');
if (docHeight < position.top) {
position.top = docHeight;
}
const docWidth = getDocumentWidthHeight('Width');
if (docWidth < position.left) {
position.left = docWidth;
}
if (propsRef.drag) {
const curTarget = getProperTargetElement(evt);
propsRef.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[propsRef.scope] = droppables[propsRef.scope];
eleObj.instance.intOver(evt, eleObj.target);
hoverObject = eleObj;
}
}
else if (hoverObject) {
triggerOutFunction(evt, eleObj);
}
const helperElement = droppables[propsRef.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 (propsRef.dragArea) {
if (propsRef.enableAutoScroll) {
setDragArea(propsRef.dragArea, helperElement, borderWidth, padding, dragLimit);
}
if (pageX !== pagex || propsRef.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 || propsRef.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 (propsRef.dragArea) {
dragLimit.top = propsRef.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 (propsRef.dragArea) {
const isDialogEle = helperElement.classList.contains('sf-dialog');
dragLimit.top = propsRef.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 (propsRef.clone) {
draEleTop += marginTop;
if (dTop < 0) {
if ((marginTop + dTop) >= 0) {
draEleTop = marginTop + dTop;
}
else {
draEleTop -= marginTop;
}
}
if (propsRef.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 (propsRef.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 (propsRef.enableScrollHandler && !propsRef.clone) {
draEleTop -= parentScrollY;
draEleLeft -= parentScrollX;
}
if (propsRef.dragArea && typeof propsRef.dragArea !== 'string' && propsRef.dragArea.classList.contains('sf-kanban-content') && propsRef.dragArea.style.position === 'relative') {
draEleTop += propsRef.dragArea.scrollTop;
}
const dragValue = getProcessedPositionValue({ top: draEleTop + 'px', left: draEleLeft + 'px' });
setStyleAttribute(helperElement, getDragPosition(dragValue));
if (!elementInViewport(helperElement) && propsRef.enableAutoScroll && !helperElement.classList.contains('sf-treeview')) {
helperElement.scrollIntoView();
}
let elements = document.querySelectorAll(':hover');
if (propsRef.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 (propsRef.dragStop) {
const curTarget = getProperTargetElement(evt);
propsRef.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[propsRef.scope] = droppables[propsRef.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[propsRef.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[propsRef.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 = propsRef.clone ? intCoord.pageX
: (intCoord.pageX + (isOffsetParent ? 0 : dragEle.offsetParent.scrollLeft)) - relativeXPosition;
pageY = propsRef.clone ? intCoord.pageY
: (intCoord.pageY + (isOffsetParent ? 0 : dragEle.offsetParent.scrollTop)) - relativeYPosition;
if (!propsRef.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 = propsRef.clone ? intCoord.pageX : (intCoord.pageX + window.pageXOffset) - relativeXPosition;
pageY = propsRef.clone ? intCoord.pageY : (intCoord.pageY + window.pageYOffset) - relativeYPosition;
if (document.scrollingElement && (!propsRef.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 + propsRef.cursorAt.left),
top: pageY - (margin.top + propsRef.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 (propsRef.clone) {
if (propsRef.helper) {
element = propsRef.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[propsRef.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 (propsRef.queryPositionInfo) {
return propsRef.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 (propsRef.axis) {
if (propsRef.axis === 'x') {
delete temp.top;
}
else if (propsRef.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;
}
/**
* Retrieves the underlying HTML element from a possibly forwarded ref or custom element.
*
* @param {RefObject<HTMLElement>} elementRef - The ref object containing the element.
* @returns {HTMLElement} The actual HTML element
*/
function getActualElement(elementRef) {
if (elementRef.current) {
if (!(elementRef.current instanceof HTMLElement) &&
elementRef.current.element &&
elementRef.current.element instanceof HTMLElement) {
return elementRef.current.element;
}
}
return elementRef.current;
}
useLayoutEffect(() => {
if (!propsRef.element.current) {
return undefined;
}
propsRef.element.current = getActualElement(propsRef.element);
addClass([propsRef.element.current], ['sf-lib', 'sf-draggable']);
bind();
return () => {
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);
};