diff --git a/dev/examples/mouse-events.tsx b/dev/examples/mouse-events.tsx new file mode 100644 index 0000000000..0934d42291 --- /dev/null +++ b/dev/examples/mouse-events.tsx @@ -0,0 +1,17 @@ +import * as React from "react" +import { useRef } from "react" +import { usePanGesture } from "@framer" +import { Box } from "../styled" +export const App = () => { + const ref = useRef(null) + const [point, setPoint] = React.useState({ x: 0, y: 0 }) + usePanGesture( + { + onPan: ({ devicePoint }) => { + setPoint(devicePoint) + }, + }, + ref + ) + return +} diff --git a/src/events/event-info.ts b/src/events/event-info.ts new file mode 100644 index 0000000000..c0c75441de --- /dev/null +++ b/src/events/event-info.ts @@ -0,0 +1,59 @@ +import { EventInfo, Point, EventHandler } from "./types" + +interface EventLike { + pageX: number + pageY: number + target: EventTarget | null +} + +const pointForTarget = (event: EventLike, target: HTMLElement | null): Point => { + if (!target) { + return { x: event.pageX, y: event.pageY } + } + // Safari + if (window.webkitConvertPointFromPageToNode) { + let webkitPoint = new WebKitPoint(event.pageX, event.pageY) + webkitPoint = window.webkitConvertPointFromPageToNode(target, webkitPoint) + return { x: webkitPoint.x, y: webkitPoint.y } + } + // All other browsers + // TODO: This does not work with rotate yet + const rect = target.getBoundingClientRect() + let scaleX = 1 + if (target.style.width && target.style.width !== "") { + scaleX = parseFloat(target.style.width) / rect.width + } + let scaleY = 1 + if (target.style.height && target.style.height !== "") { + scaleY = parseFloat(target.style.height) / rect.height + } + const scale = { + x: scaleX, + y: scaleY, + } + const point = { + x: scale.x * (event.pageX - rect.left - target.clientLeft + target.scrollLeft), + y: scale.y * (event.pageY - rect.top - target.clientTop + target.scrollTop), + } + return point +} + +const extractEventInfo = (event: EventLike): EventInfo => { + const target = event.target instanceof HTMLElement ? event.target : null + const point = pointForTarget(event, target) + const devicePoint = pointForTarget(event, document.body) + return { point, devicePoint } +} + +export const wrapHandler = (handler?: EventHandler): EventListener | undefined => { + if (!handler) { + return undefined + } + const listener: EventListener = (event: any, info?: EventInfo) => { + if (!info) { + info = extractEventInfo(event) + } + handler(event, info) + } + return listener +} diff --git a/src/events/index.ts b/src/events/index.ts new file mode 100644 index 0000000000..3300b5d594 --- /dev/null +++ b/src/events/index.ts @@ -0,0 +1,2 @@ +export { useMouseEvents, useTouchEvents, usePointerEvents } from "./use-pointer-events" +export { Point, EventInfo } from "./types" diff --git a/src/events/types.ts b/src/events/types.ts new file mode 100644 index 0000000000..6555e893d8 --- /dev/null +++ b/src/events/types.ts @@ -0,0 +1,11 @@ +export interface Point { + x: number + y: number +} + +export interface EventInfo { + point: Point + devicePoint: Point +} + +export type EventHandler = (event: Event, info: EventInfo) => void diff --git a/src/events/use-event.ts b/src/events/use-event.ts new file mode 100644 index 0000000000..455c82b54f --- /dev/null +++ b/src/events/use-event.ts @@ -0,0 +1,39 @@ +import { RefObject, useEffect } from "react" + +export const eventListener = ( + target: EventTarget, + name: string, + handler: EventListenerOrEventListenerObject, + options?: AddEventListenerOptions +): [() => void, () => void] => { + const startListening = () => { + target.addEventListener(name, handler, options) + } + const stopListening = () => { + target.removeEventListener(name, handler, options) + } + return [startListening, stopListening] +} + +export const useEvent = ( + type: string, + ref: RefObject | EventTarget, + handler?: EventListener, + options?: AddEventListenerOptions +): [() => void, () => void] | undefined => { + if (!handler) { + return + } + if (ref instanceof EventTarget) { + return eventListener(ref, type, handler, options) + } + useEffect(() => { + if (!handler || !ref.current) { + return + } + const [start, stop] = eventListener(ref.current, type, handler, options) + start() + return stop + }) + return +} diff --git a/src/events/use-pointer-events.ts b/src/events/use-pointer-events.ts new file mode 100644 index 0000000000..e7fe30c5ca --- /dev/null +++ b/src/events/use-pointer-events.ts @@ -0,0 +1,86 @@ +import { RefObject } from "react" +import { useEvent } from "./use-event" +import { wrapHandler } from "./event-info" +import { EventHandler } from "./types" + +const mergeUseEventResults = ( + ...values: ([() => void, () => void] | undefined)[] +): [() => void, () => void] | undefined => { + if (values.every(v => v === undefined)) { + return + } + const start = () => { + for (const value of values) { + if (value) { + value[0]() + } + } + } + const stop = () => { + for (const value of values) { + if (value) { + value[1]() + } + } + } + return [start, stop] +} + +export const useMouseEvents = ( + { onMouseDown, onMouseMove, onMouseUp }: { [key: string]: EventHandler }, + ref: RefObject | EventTarget, + options?: AddEventListenerOptions +): [() => void, () => void] | undefined => { + const down = useEvent("mousedown", ref, wrapHandler(onMouseDown), options) + const move = useEvent("mousemove", ref, wrapHandler(onMouseMove), options) + const up = useEvent("mouseup", ref, wrapHandler(onMouseUp), options) + return mergeUseEventResults(down, move, up) +} + +export const useTouchEvents = ( + { onTouchStart, onTouchMove, onTouchEnd }: { [key: string]: EventHandler }, + ref: RefObject | EventTarget, + options?: AddEventListenerOptions +) => { + const down = useEvent("touchstart", ref, wrapHandler(onTouchStart), options) + const move = useEvent("touchmove", ref, wrapHandler(onTouchMove), options) + const up = useEvent("touchend", ref, wrapHandler(onTouchEnd), options) + return mergeUseEventResults(down, move, up) +} + +export const useNativePointerEvents = ( + { onPointerDown, onPointerMove, onPointerUp }: { [key: string]: EventHandler }, + ref: RefObject | EventTarget, + options?: AddEventListenerOptions +) => { + const down = useEvent("pointerdown", ref, wrapHandler(onPointerDown), options) + const move = useEvent("pointermove", ref, wrapHandler(onPointerMove), options) + const up = useEvent("pointerup", ref, wrapHandler(onPointerUp), options) + return mergeUseEventResults(down, move, up) +} + +const supportsPointerEvents = + window.onpointerdown === null && window.onpointermove === null && window.onpointerup === null +const supportsTouchEvents = window.ontouchstart === null && window.ontouchmove === null && window.ontouchend === null +const supportsMouseEvents = window.onmousedown === null && window.onmousemove === null && window.onmouseup === null + +export const usePointerEvents = ( + { onPointerDown, onPointerMove, onPointerUp }: { [key: string]: EventHandler }, + ref: RefObject | EventTarget, + options?: AddEventListenerOptions +) => { + let mouseEvents = {}, + touchEvents = {}, + pointerEvents = {} + if (supportsPointerEvents) { + pointerEvents = { onPointerDown, onPointerMove, onPointerUp } + } else if (supportsTouchEvents) { + touchEvents = { onTouchStart: onPointerDown, onTouchMove: onPointerMove, onTouchEnd: onPointerUp } + } else if (supportsMouseEvents) { + mouseEvents = { onMouseDown: onPointerDown, onMouseMove: onPointerMove, onMouseUp: onPointerUp } + } + const pointer = useNativePointerEvents(pointerEvents, ref, options) + const touch = useTouchEvents(touchEvents, ref, options) + const mouse = useMouseEvents(mouseEvents, ref, options) + return mergeUseEventResults(pointer, touch, mouse) || [] +} diff --git a/src/gestures/index.ts b/src/gestures/index.ts new file mode 100644 index 0000000000..2d26e224fc --- /dev/null +++ b/src/gestures/index.ts @@ -0,0 +1 @@ +export { usePanGesture } from "./use-pan-gesture" diff --git a/src/gestures/use-pan-gesture.ts b/src/gestures/use-pan-gesture.ts new file mode 100644 index 0000000000..335a85bf07 --- /dev/null +++ b/src/gestures/use-pan-gesture.ts @@ -0,0 +1,88 @@ +import { RefObject, useMemo } from "react" +import { EventInfo, usePointerEvents, Point } from "../events" + +export const usePanGesture = ( + { + onPan, + onPanStart, + onPanEnd, + }: { [key: string]: (info: { point: Point; devicePoint: Point; delta: Point }, event: Event) => void }, + ref: RefObject +) => { + let session: null | any = null + const onPointerMove = useMemo( + () => { + return (event: Event, { point, devicePoint }: EventInfo) => { + if (!session) { + // tslint:disable-next-line:no-console + console.error("Pointer move without started session") + return + } + + const delta = { + x: devicePoint.x - session.lastDevicePoint.x, + y: devicePoint.y - session.lastDevicePoint.y, + } + if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) { + if (session.startEvent) { + if (onPan) { + onPan({ point, devicePoint, delta }, event) + } + } else { + if (onPanStart) { + onPanStart({ point, devicePoint, delta }, event) + } + session.startEvent = event + } + } + session.lastDevicePoint = devicePoint + } + }, + [onPan, onPanStart] + ) + const onPointerUp = useMemo( + () => { + return (event: Event, { point, devicePoint }: EventInfo) => { + if (!session) { + // tslint:disable-next-line:no-console + console.error("Pointer end without started session") + return + } + const delta = { + x: devicePoint.x - session.lastDevicePoint.x, + y: devicePoint.y - session.lastDevicePoint.y, + } + stopPointerMove() + stopPointerUp() + if (onPanEnd) { + onPanEnd({ point, devicePoint, delta }, event) + } + session = null + } + }, + [onPanEnd, onPointerMove] + ) + + const [startPointerUp, stopPointerUp] = usePointerEvents({ onPointerUp }, window) + const [startPointerMove, stopPointerMove] = usePointerEvents({ onPointerMove }, window, { capture: true }) + const onPointerDown = useMemo( + () => { + return (event: Event, { devicePoint }: EventInfo) => { + session = { + target: event.target, + lastDevicePoint: devicePoint, + } + startPointerMove() + startPointerUp() + } + }, + [onPointerUp, onPointerMove] + ) + + usePointerEvents({ onPointerDown }, ref) + + // TODO + const handlers = {} + + return handlers +} diff --git a/src/index.ts b/src/index.ts index e8e8e43bfc..62c5a01b7e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,6 @@ import { useTransform } from "./hooks/use-transform" import { usePose } from "./hooks/use-pose" export { motion, useMotionValue, useTransform, usePose } + +export { useMouseEvents, useTouchEvents, usePointerEvents } from "./events" +export { usePanGesture } from "./gestures"