197 lines
5.9 KiB
JavaScript
197 lines
5.9 KiB
JavaScript
import { ref, watchEffect } from "vue";
|
|
import { createEventHook, refAutoReset } from "@vueuse/shared";
|
|
|
|
//#region src/shared/useGraceArea.ts
|
|
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);
|
|
}
|
|
|
|
//#endregion
|
|
export { useGraceArea };
|
|
//# sourceMappingURL=useGraceArea.js.map
|