|
@@ -1,522 +0,0 @@
|
|
|
-import React from "react";
|
|
|
-import platform from "platform";
|
|
|
-
|
|
|
-export const isMacOS = () => {
|
|
|
- return platform.os.family === "OS X";
|
|
|
-};
|
|
|
-
|
|
|
-// From https://stackoverflow.com/questions/20110224/what-is-the-height-of-a-line-in-a-wheel-event-deltamode-dom-delta-line
|
|
|
-const getScrollLineHeight = () => {
|
|
|
- const iframe = document.createElement("iframe");
|
|
|
- iframe.src = "#";
|
|
|
- document.body.appendChild(iframe);
|
|
|
-
|
|
|
- // Write content in Iframe
|
|
|
- const idoc = iframe.contentWindow.document;
|
|
|
- idoc.open();
|
|
|
- idoc.write(
|
|
|
- "<!DOCTYPE html><html><head></head><body><span>a</span></body></html>"
|
|
|
- );
|
|
|
- idoc.close();
|
|
|
-
|
|
|
- const scrollLineHeight = idoc.body.firstElementChild.offsetHeight;
|
|
|
- document.body.removeChild(iframe);
|
|
|
-
|
|
|
- return scrollLineHeight;
|
|
|
-};
|
|
|
-
|
|
|
-const LINE_HEIGHT = getScrollLineHeight();
|
|
|
-// Reasonable default from https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
|
|
|
-const PAGE_HEIGHT = 800;
|
|
|
-
|
|
|
-const otherPointer = (pointers, currentPointer) => {
|
|
|
- const p2 = Object.keys(pointers)
|
|
|
- .map((p) => Number(p))
|
|
|
- .find((pointer) => pointer !== currentPointer);
|
|
|
- return pointers[p2];
|
|
|
-};
|
|
|
-
|
|
|
-const computeDistance = ([x1, y1], [x2, y2]) => {
|
|
|
- const distanceX = Math.abs(x1 - x2);
|
|
|
- const distanceY = Math.abs(y1 - y2);
|
|
|
-
|
|
|
- return Math.hypot(distanceX, distanceY);
|
|
|
-};
|
|
|
-
|
|
|
-const empty = () => {};
|
|
|
-
|
|
|
-const Gesture = ({
|
|
|
- children,
|
|
|
- onDrag = empty,
|
|
|
- onDragStart = empty,
|
|
|
- onDragEnd = empty,
|
|
|
- onPan = empty,
|
|
|
- onTap = empty,
|
|
|
- onLongTap = empty,
|
|
|
- onDoubleTap = empty,
|
|
|
- onZoom,
|
|
|
-}) => {
|
|
|
- const wrapperRef = React.useRef(null);
|
|
|
- const stateRef = React.useRef({
|
|
|
- moving: false,
|
|
|
- pointers: {},
|
|
|
- mainPointer: undefined,
|
|
|
- });
|
|
|
- const queueRef = React.useRef([]);
|
|
|
-
|
|
|
- // Queue event to avoid async mess
|
|
|
- const queue = React.useCallback((callback) => {
|
|
|
- queueRef.current.push(async () => {
|
|
|
- await callback();
|
|
|
- queueRef.current.shift();
|
|
|
- if (queueRef.current.length !== 0) {
|
|
|
- await queueRef.current[0]();
|
|
|
- }
|
|
|
- });
|
|
|
- if (queueRef.current.length === 1) {
|
|
|
- queueRef.current[0]();
|
|
|
- }
|
|
|
- }, []);
|
|
|
-
|
|
|
- const onWheel = React.useCallback(
|
|
|
- (e) => {
|
|
|
- const {
|
|
|
- deltaX,
|
|
|
- deltaY,
|
|
|
- clientX,
|
|
|
- clientY,
|
|
|
- deltaMode,
|
|
|
- ctrlKey,
|
|
|
- altKey,
|
|
|
- metaKey,
|
|
|
- target,
|
|
|
- } = e;
|
|
|
-
|
|
|
- // On a MacOs trackpad, the pinch gesture sets the ctrlKey to true.
|
|
|
- // In that situation, we want to use the custom scaling, not the browser default zoom.
|
|
|
- // Hence in this situation we avoid to return immediately.
|
|
|
- if (altKey || (ctrlKey && !isMacOS())) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // On a trackpad, the pinch and pan events are differentiated by the crtlKey value.
|
|
|
- // On a pinch gesture, the ctrlKey is set to true, so we want to have a scaling effect.
|
|
|
- // If we are only moving the fingers in the same direction, a pan is needed.
|
|
|
- // Ref: https://medium.com/@auchenberg/detecting-multi-touch-trackpad-gestures-in-javascript-a2505babb10e
|
|
|
- if (isMacOS() && !ctrlKey) {
|
|
|
- queue(() =>
|
|
|
- onPan({
|
|
|
- deltaX: -2 * deltaX,
|
|
|
- deltaY: -2 * deltaY,
|
|
|
- button: 1,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target,
|
|
|
- event: e,
|
|
|
- })
|
|
|
- );
|
|
|
- } else {
|
|
|
- // Quit if onZoom is not set
|
|
|
- if (onZoom === undefined || !deltaY) return;
|
|
|
-
|
|
|
- let scale = deltaY;
|
|
|
-
|
|
|
- switch (deltaMode) {
|
|
|
- case 1: // Pixel
|
|
|
- scale *= LINE_HEIGHT;
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- scale *= PAGE_HEIGHT;
|
|
|
- break;
|
|
|
- default:
|
|
|
- }
|
|
|
-
|
|
|
- if (isMacOS()) {
|
|
|
- scale *= 2;
|
|
|
- }
|
|
|
-
|
|
|
- queue(() => onZoom({ scale, clientX, clientY, event: e }));
|
|
|
- }
|
|
|
- },
|
|
|
- [onPan, onZoom, queue]
|
|
|
- );
|
|
|
-
|
|
|
- const onPointerDown = React.useCallback(
|
|
|
- ({
|
|
|
- target,
|
|
|
- button,
|
|
|
- clientX,
|
|
|
- clientY,
|
|
|
- pointerId,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- isPrimary,
|
|
|
- }) => {
|
|
|
- // Add pointer to map
|
|
|
- stateRef.current.pointers[pointerId] = { clientX, clientY };
|
|
|
-
|
|
|
- if (isPrimary) {
|
|
|
- // Clean mainPoint on primary pointer
|
|
|
- stateRef.current.mainPointer = undefined;
|
|
|
- }
|
|
|
-
|
|
|
- if (stateRef.current.mainPointer !== undefined) {
|
|
|
- if (stateRef.current.mainPointer !== pointerId) {
|
|
|
- // This is not the main pointer
|
|
|
- try {
|
|
|
- const { clientX: clientX2, clientY: clientY2 } = otherPointer(
|
|
|
- stateRef.current.pointers,
|
|
|
- pointerId
|
|
|
- );
|
|
|
- const newClientX = (clientX2 + clientX) / 2;
|
|
|
- const newClientY = (clientY2 + clientY) / 2;
|
|
|
-
|
|
|
- const distance = computeDistance(
|
|
|
- [clientX2, clientY2],
|
|
|
- [clientX, clientY]
|
|
|
- );
|
|
|
-
|
|
|
- // We update previous position as the new position is the center beetween both finger
|
|
|
- Object.assign(stateRef.current, {
|
|
|
- pressed: true,
|
|
|
- moving: false,
|
|
|
- gestureStart: false,
|
|
|
- startX: clientX,
|
|
|
- startY: clientY,
|
|
|
- prevX: newClientX,
|
|
|
- prevY: newClientY,
|
|
|
- startDistance: distance,
|
|
|
- prevDistance: distance,
|
|
|
- });
|
|
|
- } catch (e) {
|
|
|
- console.log("Error while getting other pointer. Ignoring", e);
|
|
|
- stateRef.current.mainPointer === undefined;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // We set the mainpointer
|
|
|
- stateRef.current.mainPointer = pointerId;
|
|
|
-
|
|
|
- // And prepare move
|
|
|
- Object.assign(stateRef.current, {
|
|
|
- pressed: true,
|
|
|
- moving: false,
|
|
|
- gestureStart: false,
|
|
|
- startX: clientX,
|
|
|
- startY: clientY,
|
|
|
- prevX: clientX,
|
|
|
- prevY: clientY,
|
|
|
- currentButton: button,
|
|
|
- target,
|
|
|
- timeStart: Date.now(),
|
|
|
- longTapTimeout: setTimeout(async () => {
|
|
|
- stateRef.current.noTap = true;
|
|
|
- queue(() =>
|
|
|
- onLongTap({
|
|
|
- clientX,
|
|
|
- clientY,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target,
|
|
|
- })
|
|
|
- );
|
|
|
- }, 750),
|
|
|
- });
|
|
|
-
|
|
|
- try {
|
|
|
- target.setPointerCapture(pointerId);
|
|
|
- } catch (e) {
|
|
|
- console.log("Fail to capture pointer", e);
|
|
|
- }
|
|
|
- },
|
|
|
- [onLongTap, queue]
|
|
|
- );
|
|
|
-
|
|
|
- const onPointerMove = React.useCallback(
|
|
|
- (e) => {
|
|
|
- if (stateRef.current.pressed) {
|
|
|
- const {
|
|
|
- pointerId,
|
|
|
- clientX: eventClientX,
|
|
|
- clientY: eventClientY,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- pointerType,
|
|
|
- } = e;
|
|
|
-
|
|
|
- if (stateRef.current.mainPointer !== pointerId) {
|
|
|
- // Event from other pointer
|
|
|
- stateRef.current.pointers[pointerId] = {
|
|
|
- clientX: eventClientX,
|
|
|
- clientY: eventClientY,
|
|
|
- };
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- stateRef.current.moving = true;
|
|
|
-
|
|
|
- // Do we have two fingers ?
|
|
|
- const twoFingers = Object.keys(stateRef.current.pointers).length === 2;
|
|
|
-
|
|
|
- let clientX, clientY, distance;
|
|
|
-
|
|
|
- if (twoFingers) {
|
|
|
- // Find other pointerId
|
|
|
- const { clientX: clientX2, clientY: clientY2 } = otherPointer(
|
|
|
- stateRef.current.pointers,
|
|
|
- pointerId
|
|
|
- );
|
|
|
-
|
|
|
- // Update client X with the center of each touch
|
|
|
- clientX = (clientX2 + eventClientX) / 2;
|
|
|
- clientY = (clientY2 + eventClientY) / 2;
|
|
|
- distance = computeDistance(
|
|
|
- [clientX2, clientY2],
|
|
|
- [eventClientX, eventClientY]
|
|
|
- );
|
|
|
- } else {
|
|
|
- clientX = eventClientX;
|
|
|
- clientY = eventClientY;
|
|
|
- }
|
|
|
-
|
|
|
- // We drag if
|
|
|
- // On non touch device
|
|
|
- // - Button is 0
|
|
|
- // - Alt key is no pressed
|
|
|
- // or on touch devices
|
|
|
- // - We use only one finger
|
|
|
- const shouldDrag =
|
|
|
- pointerType !== "touch"
|
|
|
- ? stateRef.current.currentButton === 0 && !altKey
|
|
|
- : !twoFingers;
|
|
|
-
|
|
|
- if (shouldDrag) {
|
|
|
- // Send drag start on first move
|
|
|
- if (!stateRef.current.gestureStart) {
|
|
|
- wrapperRef.current.style.cursor = "move";
|
|
|
- stateRef.current.gestureStart = true;
|
|
|
- // Clear tap timeout
|
|
|
- clearTimeout(stateRef.current.longTapTimeout);
|
|
|
-
|
|
|
- queue(() =>
|
|
|
- onDragStart({
|
|
|
- deltaX: 0,
|
|
|
- deltaY: 0,
|
|
|
- startX: stateRef.current.startX,
|
|
|
- startY: stateRef.current.startY,
|
|
|
- distanceX: 0,
|
|
|
- distanceY: 0,
|
|
|
- button: stateRef.current.currentButton,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target: stateRef.current.target,
|
|
|
- event: e,
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
- // Create closure
|
|
|
- const deltaX = clientX - stateRef.current.prevX;
|
|
|
- const deltaY = clientY - stateRef.current.prevY;
|
|
|
- const distanceX = clientX - stateRef.current.startX;
|
|
|
- const distanceY = clientY - stateRef.current.startY;
|
|
|
-
|
|
|
- // Drag event
|
|
|
- queue(() =>
|
|
|
- onDrag({
|
|
|
- deltaX,
|
|
|
- deltaY,
|
|
|
- startX: stateRef.current.startX,
|
|
|
- startY: stateRef.current.startY,
|
|
|
- distanceX,
|
|
|
- distanceY,
|
|
|
- button: stateRef.current.currentButton,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target: stateRef.current.target,
|
|
|
- event: e,
|
|
|
- })
|
|
|
- );
|
|
|
- } else {
|
|
|
- if (!stateRef.current.gestureStart) {
|
|
|
- wrapperRef.current.style.cursor = "move";
|
|
|
- stateRef.current.gestureStart = true;
|
|
|
- // Clear tap timeout on first move
|
|
|
- clearTimeout(stateRef.current.longTapTimeout);
|
|
|
- }
|
|
|
-
|
|
|
- // Create closure
|
|
|
- const deltaX = clientX - stateRef.current.prevX;
|
|
|
- const deltaY = clientY - stateRef.current.prevY;
|
|
|
- const target = stateRef.current.target;
|
|
|
-
|
|
|
- // Pan event
|
|
|
- queue(() =>
|
|
|
- onPan({
|
|
|
- deltaX,
|
|
|
- deltaY,
|
|
|
- button: stateRef.current.currentButton,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target,
|
|
|
- event: e,
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- if (
|
|
|
- twoFingers &&
|
|
|
- distance !== stateRef.current.prevDistance &&
|
|
|
- onZoom
|
|
|
- ) {
|
|
|
- const scale = stateRef.current.prevDistance - distance;
|
|
|
-
|
|
|
- if (Math.abs(scale) > 0) {
|
|
|
- queue(() =>
|
|
|
- onZoom({
|
|
|
- scale,
|
|
|
- clientX,
|
|
|
- clientY,
|
|
|
- event: e,
|
|
|
- })
|
|
|
- );
|
|
|
- stateRef.current.prevDistance = distance;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- stateRef.current.prevX = clientX;
|
|
|
- stateRef.current.prevY = clientY;
|
|
|
- }
|
|
|
- },
|
|
|
- [onDrag, onDragStart, onPan, onZoom, queue]
|
|
|
- );
|
|
|
-
|
|
|
- const onPointerUp = React.useCallback(
|
|
|
- (e) => {
|
|
|
- const {
|
|
|
- clientX,
|
|
|
- clientY,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target,
|
|
|
- pointerId,
|
|
|
- } = e;
|
|
|
-
|
|
|
- if (!stateRef.current.pointers[pointerId]) {
|
|
|
- // Pointer already gone previously with another event
|
|
|
- // ignoring it
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Remove pointer from map
|
|
|
- delete stateRef.current.pointers[pointerId];
|
|
|
-
|
|
|
- if (stateRef.current.mainPointer !== pointerId) {
|
|
|
- // If this is not the main pointer we quit here
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- while (Object.keys(stateRef.current.pointers).length > 0) {
|
|
|
- // If was main pointer but we have another one, this one become main
|
|
|
- stateRef.current.mainPointer = Number(
|
|
|
- Object.keys(stateRef.current.pointers)[0]
|
|
|
- );
|
|
|
- try {
|
|
|
- stateRef.current.target.setPointerCapture(
|
|
|
- stateRef.current.mainPointer
|
|
|
- );
|
|
|
- return;
|
|
|
- } catch (e) {
|
|
|
- console.log("Fails to set pointer capture", e);
|
|
|
- stateRef.current.mainPointer = undefined;
|
|
|
- delete stateRef.current.pointers[
|
|
|
- Object.keys(stateRef.current.pointers)[0]
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- stateRef.current.mainPointer = undefined;
|
|
|
- stateRef.current.pressed = false;
|
|
|
-
|
|
|
- // Clear longTap
|
|
|
- clearTimeout(stateRef.current.longTapTimeout);
|
|
|
-
|
|
|
- if (stateRef.current.moving) {
|
|
|
- // If we were moving, send drag end event
|
|
|
- stateRef.current.moving = false;
|
|
|
- queue(() =>
|
|
|
- onDragEnd({
|
|
|
- deltaX: clientX - stateRef.current.prevX,
|
|
|
- deltaY: clientY - stateRef.current.prevY,
|
|
|
- startX: stateRef.current.startX,
|
|
|
- startY: stateRef.current.startY,
|
|
|
- distanceX: clientX - stateRef.current.startX,
|
|
|
- distanceY: clientY - stateRef.current.startY,
|
|
|
- button: stateRef.current.currentButton,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- event: e,
|
|
|
- })
|
|
|
- );
|
|
|
- wrapperRef.current.style.cursor = "auto";
|
|
|
- } else {
|
|
|
- const now = Date.now();
|
|
|
-
|
|
|
- if (stateRef.current.noTap) {
|
|
|
- stateRef.current.noTap = false;
|
|
|
- } else {
|
|
|
- // Send tap event only if time less than 300ms
|
|
|
- if (stateRef.current.timeStart - now < 300) {
|
|
|
- queue(() =>
|
|
|
- onTap({
|
|
|
- clientX,
|
|
|
- clientY,
|
|
|
- altKey,
|
|
|
- ctrlKey,
|
|
|
- metaKey,
|
|
|
- target,
|
|
|
- })
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- [onDragEnd, onTap, queue]
|
|
|
- );
|
|
|
-
|
|
|
- const onDoubleTapHandler = React.useCallback(
|
|
|
- (event) => {
|
|
|
- onDoubleTap(event);
|
|
|
- },
|
|
|
- [onDoubleTap]
|
|
|
- );
|
|
|
-
|
|
|
- return (
|
|
|
- <div
|
|
|
- onWheel={onWheel}
|
|
|
- onPointerDown={onPointerDown}
|
|
|
- onPointerMove={onPointerMove}
|
|
|
- onPointerUp={onPointerUp}
|
|
|
- onPointerOut={onPointerUp}
|
|
|
- onPointerLeave={onPointerUp}
|
|
|
- onPointerCancel={onPointerUp}
|
|
|
- onDoubleClick={onDoubleTapHandler}
|
|
|
- style={{ touchAction: "none" }}
|
|
|
- ref={wrapperRef}
|
|
|
- >
|
|
|
- {children}
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|
|
|
-
|
|
|
-export default Gesture;
|