UNPKG

@syncfusion/react-base

Version:

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

204 lines (203 loc) 7.34 kB
import { cloneElement, useEffect, useLayoutEffect, useRef } from 'react'; import { Browser } from './browser'; import { addClass, isVisible, matches, removeClass } from './dom'; import { getActualElement, compareElementParent, getUniqueID } from './util'; import { EventHandler } from './event-handler'; import { useDragDropContext } from './dragdrop'; /** * Creates a droppable instance with the specified element and properties. * * @private * @param {RefObject<HTMLElement>} [element] - Reference to the HTML element to make droppable. * @param {IDroppable} [props] - Configuration properties for the droppable instance. * @returns {IDroppable} The configured droppable instance. */ export function useDroppable(element, props) { const droppableId = getUniqueID('sf-droppable'); const droppableContext = useDragDropContext(); const { registerDroppable, unregisterDroppable } = droppableContext || {}; const propsRef = { accept: '', scope: 'default', dragData: {}, drop: null, over: null, out: null, ...props }; const propsStateRef = useRef(propsRef); /** Represents whether the mouse is over the droppable area */ let mouseOverRef = false; /** Indicates if drag stop has been called */ let dragStopCalledRef = true; /** * Method to add drop events. * * @returns {void} */ function addEvent() { EventHandler.add(element.current, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDrop); } /** * Handles interactions when a dragged item is over the droppable area. * * @param {MouseEvent | TouchEvent} event - Mouse or touch event arguments. * @param {Element} [element] - The target element over which the drag is happening. * @returns {void} */ propsRef.intOver = (event, element) => { if (!mouseOverRef) { const drag = propsRef.dragData[propsStateRef.current.scope]; if (propsStateRef.current && propsStateRef.current.over) { propsStateRef.current.over({ event, target: element, dragData: drag }); } mouseOverRef = true; } }; /** * Method for handling interactions when dragged item is out of the droppable area. * * @param {MouseEvent | TouchEvent} event - Mouse or touch event arguments. * @param {Element} [element] - The target element from which the drag is moving out. * @returns {void} */ propsRef.intOut = (event, element) => { if (mouseOverRef) { if (propsStateRef.current && propsStateRef.current.out) { propsStateRef.current.out({ event, target: element }); } mouseOverRef = false; } }; /** * Method to handle drop event. * * @param {MouseEvent | TouchEvent} evt - Mouse or touch event arguments. * @param {HTMLElement} [element] - The target element where the drop is happening. * @returns {void} */ propsRef.intDrop = (evt, element) => { if (!dragStopCalledRef) { return; } else { dragStopCalledRef = false; } let accept = true; const drag = propsRef.dragData[propsStateRef.current.scope]; const isDrag = drag ? (drag.helper && isVisible(drag.helper)) : false; let area; if (isDrag) { area = isDropArea(evt, drag.helper, element); if (propsStateRef.current.accept) { accept = matches(drag.helper, propsStateRef.current.accept); } } if (isDrag && propsStateRef.current && propsStateRef.current.drop && area.canDrop && accept) { propsStateRef.current.drop({ event: evt, target: area.target, droppedElement: drag.helper, dragData: drag }); } mouseOverRef = false; }; /** * Method to check if the drop area is valid. * * @param {MouseEvent | TouchEvent} evt - Mouse or touch event arguments. * @param {HTMLElement} helper - The helper element involved in the drag operation. * @param {HTMLElement} [element] - The element to check for drop validity. * @returns {DropData} - The result indicating if the area is a valid drop target and the target itself. */ function isDropArea(evt, helper, element) { const area = { canDrop: true, target: element || evt.target }; const isTouch = evt.type === 'touchend'; if (isTouch || area.target === helper) { helper.style.display = 'none'; const coord = isTouch ? (evt.changedTouches[0]) : evt; const ele = document.elementFromPoint(coord.clientX, coord.clientY); area.canDrop = false; area.canDrop = compareElementParent(ele, element); if (area.canDrop) { area.target = ele; } helper.style.display = ''; } return area; } /** * Method to clean up and remove event handlers on the component destruction. * * @returns {void} */ function removeEvent() { EventHandler.remove(element.current, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDrop); } useLayoutEffect(() => { element.current = getActualElement(element); if (!element.current) { return undefined; } propsStateRef.current = { ...propsRef, ...props }; addClass([element.current], ['sf-lib', 'sf-droppable']); addEvent(); if (registerDroppable) { registerDroppable(droppableId, { ...propsRef, element: element }); } return () => { if (unregisterDroppable) { unregisterDroppable(droppableId); } if (element.current) { removeClass([element.current], ['sf-lib', 'sf-droppable']); } removeEvent(); }; }, []); return propsRef; } /** * DroppableComponent wraps elements to enable droppable functionality. * It leverages the Droppable hook internally to manage drop behavior. * * @example * ```tsx * import { Droppable } from '@syncfusion/react-base'; * * <Droppable> * <div>Drop here</div> * </Droppable> * ``` * * @param {DroppableProps} props - The props for the DroppableComponent. * @returns {Element} The rendered droppable component. */ export const Droppable = ({ children, className, dropRef, ...restProps }) => { const internalRef = useRef(null); const droppableInstance = useRef(null); useDroppable(internalRef, { ...restProps }); useEffect(() => { if (droppableInstance.current) { Object.assign(droppableInstance.current, { ...restProps }); } }, [restProps]); const combinedRef = (node) => { if (node) { internalRef.current = node; if (dropRef && 'current' in dropRef) { dropRef.current = node; } } }; return cloneElement(children, { ref: combinedRef, className: [ children.props.className, className ].filter(Boolean).join(' ') || undefined }); };