import { useEventListener } from "@vueuse/core"; import { isClient, resolveUnref, toRefs } from "@vueuse/shared"; import { computed, ref } from "vue-demi"; /** * Make elements draggable. * * @see https://vueuse.org/useDraggable * @param target * @param options */ export function useDraggable(target, options = {}) { const draggingElement = options.draggingElement ?? window; const draggingHandle = options.handle ?? target; const position = ref(resolveUnref(options.initialValue) ?? { x: 0, y: 0 }); const pressedDelta = ref(); const cleanUpPointerMove = ref(); const cleanUpDragMove = ref(); const filterEvent = (e) => { if (options.pointerTypes) { return options.pointerTypes.includes(e.pointerType); } return true; }; const handleEvent = (e) => { if (resolveUnref(options.preventDefault)) { e.preventDefault(); } if (resolveUnref(options.stopPropagation)) { e.stopPropagation(); } }; const start = (e) => { if (!filterEvent(e)) { return; } if (resolveUnref(options.exact) && e.target !== resolveUnref(target)) { return; } const rect = resolveUnref(target)?.getBoundingClientRect(); const pos = { x: e.clientX - rect.left, y: e.clientY - rect.top, }; if (options.onStart?.(pos, e) === false) { return; } pressedDelta.value = pos; // define move inline because the event lister scope never receives updates to the initial pressedDelta value // (interestingly not a problem if using move directly) const move = (e) => { if (!filterEvent(e)) { return; } if (!pressedDelta.value) { return; } position.value = { x: e.clientX - pressedDelta.value.x, y: e.clientY - pressedDelta.value.y, }; options.onMove?.(position.value, e); handleEvent(e); }; function throttleMove(e) { return throttledWrite(() => move(e)); } cleanUpPointerMove.value && cleanUpPointerMove.value(); cleanUpDragMove.value && cleanUpDragMove.value(); cleanUpDragMove.value = useEventListener(window, "dragover", throttleMove, options.useCapture ?? true); cleanUpPointerMove.value = useEventListener( draggingElement, "pointermove", throttleMove, options.useCapture ?? true, ); handleEvent(e); }; const end = (e) => { if (!filterEvent(e)) { return; } if (!pressedDelta.value) { return; } pressedDelta.value = undefined; options.onEnd?.(position.value, e); handleEvent(e); cleanUpPointerMove.value && cleanUpPointerMove.value(); cleanUpDragMove.value && cleanUpDragMove.value(); }; if (isClient) { const useCapture = options.useCapture ?? true; useEventListener(draggingHandle, "dragstart", start, useCapture); useEventListener(draggingHandle, "pointerdown", start, useCapture); useEventListener(draggingElement, "dragend", end, useCapture); useEventListener(draggingElement, "pointerup", end, useCapture); } return { ...toRefs(position), position, isDragging: computed(() => !!pressedDelta.value), style: computed(() => `left:${position.value.x}px;top:${position.value.y}px;`), }; } function throttle(timer) { let queuedCallback; return (callback) => { if (!queuedCallback) { timer(() => { const cb = queuedCallback; queuedCallback = null; cb(); }); } queuedCallback = callback; }; } const throttledWrite = throttle(requestAnimationFrame);