import { refAutoReset, createEventHook } from '@vueuse/shared'; import { ref, watchEffect } from 'vue'; function useGraceArea(triggerElement, containerElement) { const isPointerInTransit = refAutoReset(false, 300); const pointerGraceArea = ref(null); const pointerExit = createEventHook(); function handleRemoveGraceArea() { pointerGraceArea.value = null; isPointerInTransit.value = false; } function handleCreateGraceArea(event, hoverTarget) { const currentTarget = event.currentTarget; const exitPoint = { x: event.clientX, y: event.clientY }; const exitSide = getExitSideFromRect(exitPoint, currentTarget.getBoundingClientRect()); const paddedExitPoints = getPaddedExitPoints(exitPoint, exitSide); const hoverTargetPoints = getPointsFromRect(hoverTarget.getBoundingClientRect()); const graceArea = getHull([...paddedExitPoints, ...hoverTargetPoints]); pointerGraceArea.value = graceArea; isPointerInTransit.value = true; } watchEffect((cleanupFn) => { if (triggerElement.value && containerElement.value) { const handleTriggerLeave = (event) => handleCreateGraceArea(event, containerElement.value); const handleContentLeave = (event) => handleCreateGraceArea(event, triggerElement.value); triggerElement.value.addEventListener("pointerleave", handleTriggerLeave); containerElement.value.addEventListener("pointerleave", handleContentLeave); cleanupFn(() => { triggerElement.value?.removeEventListener("pointerleave", handleTriggerLeave); containerElement.value?.removeEventListener("pointerleave", handleContentLeave); }); } }); watchEffect((cleanupFn) => { if (pointerGraceArea.value) { const handleTrackPointerGrace = (event) => { if (!pointerGraceArea.value || !(event.target instanceof HTMLElement)) return; const target = event.target; const pointerPosition = { x: event.clientX, y: event.clientY }; const hasEnteredTarget = triggerElement.value?.contains(target) || containerElement.value?.contains(target); const isPointerOutsideGraceArea = !isPointInPolygon(pointerPosition, pointerGraceArea.value); const isAnotherGraceAreaTrigger = !!target.closest("[data-grace-area-trigger]"); if (hasEnteredTarget) { handleRemoveGraceArea(); } else if (isPointerOutsideGraceArea || isAnotherGraceAreaTrigger) { handleRemoveGraceArea(); pointerExit.trigger(); } }; triggerElement.value?.ownerDocument.addEventListener("pointermove", handleTrackPointerGrace); cleanupFn(() => triggerElement.value?.ownerDocument.removeEventListener("pointermove", handleTrackPointerGrace)); } }); return { isPointerInTransit, onPointerExit: pointerExit.on }; } function getExitSideFromRect(point, rect) { const top = Math.abs(rect.top - point.y); const bottom = Math.abs(rect.bottom - point.y); const right = Math.abs(rect.right - point.x); const left = Math.abs(rect.left - point.x); switch (Math.min(top, bottom, right, left)) { case left: return "left"; case right: return "right"; case top: return "top"; case bottom: return "bottom"; default: throw new Error("unreachable"); } } function getPaddedExitPoints(exitPoint, exitSide, padding = 5) { const paddedExitPoints = []; switch (exitSide) { case "top": paddedExitPoints.push( { x: exitPoint.x - padding, y: exitPoint.y + padding }, { x: exitPoint.x + padding, y: exitPoint.y + padding } ); break; case "bottom": paddedExitPoints.push( { x: exitPoint.x - padding, y: exitPoint.y - padding }, { x: exitPoint.x + padding, y: exitPoint.y - padding } ); break; case "left": paddedExitPoints.push( { x: exitPoint.x + padding, y: exitPoint.y - padding }, { x: exitPoint.x + padding, y: exitPoint.y + padding } ); break; case "right": paddedExitPoints.push( { x: exitPoint.x - padding, y: exitPoint.y - padding }, { x: exitPoint.x - padding, y: exitPoint.y + padding } ); break; } return paddedExitPoints; } function getPointsFromRect(rect) { const { top, right, bottom, left } = rect; return [ { x: left, y: top }, { x: right, y: top }, { x: right, y: bottom }, { x: left, y: bottom } ]; } function isPointInPolygon(point, polygon) { const { x, y } = point; let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x; const yi = polygon[i].y; const xj = polygon[j].x; const yj = polygon[j].y; const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; } function getHull(points) { const newPoints = points.slice(); newPoints.sort((a, b) => { if (a.x < b.x) return -1; else if (a.x > b.x) return 1; else if (a.y < b.y) return -1; else if (a.y > b.y) return 1; else return 0; }); return getHullPresorted(newPoints); } function getHullPresorted(points) { if (points.length <= 1) return points.slice(); const upperHull = []; for (let i = 0; i < points.length; i++) { const p = points[i]; while (upperHull.length >= 2) { const q = upperHull[upperHull.length - 1]; const r = upperHull[upperHull.length - 2]; if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop(); else break; } upperHull.push(p); } upperHull.pop(); const lowerHull = []; for (let i = points.length - 1; i >= 0; i--) { const p = points[i]; while (lowerHull.length >= 2) { const q = lowerHull[lowerHull.length - 1]; const r = lowerHull[lowerHull.length - 2]; if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop(); else break; } lowerHull.push(p); } lowerHull.pop(); if (upperHull.length === 1 && lowerHull.length === 1 && upperHull[0].x === lowerHull[0].x && upperHull[0].y === lowerHull[0].y) { return upperHull; } else { return upperHull.concat(lowerHull); } } export { useGraceArea as u }; //# sourceMappingURL=useGraceArea.js.map