55

有没有办法在react-web 应用程序中添加长按事件?

我有地址列表。在长按任何地址时,我想触发事件以删除该地址,然后是一个确认框。

4

13 回答 13

99

我创建了一个带有钩子的代码框来处理长按和点击。基本上,在鼠标按下、触摸开始事件时,会使用setTimeout. 当提供的时间过去时,它会触发长按。在鼠标抬起、鼠标离开、触摸结束等时,计时器被清除。

useLongPress.js

import { useCallback, useRef, useState } from "react";

const useLongPress = (
    onLongPress,
    onClick,
    { shouldPreventDefault = true, delay = 300 } = {}
    ) => {
    const [longPressTriggered, setLongPressTriggered] = useState(false);
    const timeout = useRef();
    const target = useRef();

    const start = useCallback(
        event => {
            if (shouldPreventDefault && event.target) {
                    event.target.addEventListener("touchend", preventDefault, {
                    passive: false
                });
                target.current = event.target;
            }
            timeout.current = setTimeout(() => {
                onLongPress(event);
                setLongPressTriggered(true);
            }, delay);
        },
        [onLongPress, delay, shouldPreventDefault]
    );

    const clear = useCallback(
        (event, shouldTriggerClick = true) => {
            timeout.current && clearTimeout(timeout.current);
            shouldTriggerClick && !longPressTriggered && onClick();
            setLongPressTriggered(false);
            if (shouldPreventDefault && target.current) {
                target.current.removeEventListener("touchend", preventDefault);
            }
        },
        [shouldPreventDefault, onClick, longPressTriggered]
    );

    return {
        onMouseDown: e => start(e),
        onTouchStart: e => start(e),
        onMouseUp: e => clear(e),
        onMouseLeave: e => clear(e, false),
        onTouchEnd: e => clear(e)
    };
};

const isTouchEvent = event => {
return "touches" in event;
};

const preventDefault = event => {
if (!isTouchEvent(event)) return;

if (event.touches.length < 2 && event.preventDefault) {
    event.preventDefault();
}
};

export default useLongPress;

要使用钩子, App.js

import useLongPress from "./useLongPress";

export default function App() {

    const onLongPress = () => {
        console.log('longpress is triggered');
    };

    const onClick = () => {
        console.log('click is triggered')
    }

    const defaultOptions = {
        shouldPreventDefault: true,
        delay: 500,
    };
    const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);

    return (
        <div className="App">
            <button {...longPressEvent}>use  Loooong  Press</button>
        </div>
    );
}

类组件的旧答案:

您可以使用 MouseDown、MouseUp、TouchStart、TouchEnd 事件来控制可以充当长按事件的计时器。查看下面的代码

class App extends Component {
  constructor() {
    super()
    this.handleButtonPress = this.handleButtonPress.bind(this)
    this.handleButtonRelease = this.handleButtonRelease.bind(this)
  }
  handleButtonPress () {
    this.buttonPressTimer = setTimeout(() => alert('long press activated'), 1500);
  }
  
  handleButtonRelease () {
    clearTimeout(this.buttonPressTimer);
  }

  render() {
    return (
      <div 
          onTouchStart={this.handleButtonPress} 
          onTouchEnd={this.handleButtonRelease} 
          onMouseDown={this.handleButtonPress} 
          onMouseUp={this.handleButtonRelease} 
          onMouseLeave={this.handleButtonRelease}>
        Button
      </div>
    );
  }
}
于 2018-01-02T07:40:01.133 回答
71

使用 react 16.8 中的钩子,您可以使用函数和钩子重写类。

import { useState, useEffect } from 'react';

export default function useLongPress(callback = () => {}, ms = 300) {
  const [startLongPress, setStartLongPress] = useState(false);

  useEffect(() => {
    let timerId;
    if (startLongPress) {
      timerId = setTimeout(callback, ms);
    } else {
      clearTimeout(timerId);
    }

    return () => {
      clearTimeout(timerId);
    };
  }, [callback, ms, startLongPress]);

  return {
    onMouseDown: () => setStartLongPress(true),
    onMouseUp: () => setStartLongPress(false),
    onMouseLeave: () => setStartLongPress(false),
    onTouchStart: () => setStartLongPress(true),
    onTouchEnd: () => setStartLongPress(false),
  };
}
import useLongPress from './useLongPress';

function MyComponent (props) {
  const backspaceLongPress = useLongPress(props.longPressBackspaceCallback, 500);

  return (
    <Page>
      <Button {...backspaceLongPress}>
        Click me
      </Button>
    </Page>
  );
};

于 2019-02-18T14:52:44.213 回答
23

不错的钩子!但我想做一个小的改进。用于useCallback包装事件处理程序。这确保了这些不会在每次渲染时更改。

import { useState, useEffect, useCallback } from 'react';

export default function useLongPress(callback = () => {}, ms = 300) {
  const [startLongPress, setStartLongPress] = useState(false);

  useEffect(() => {
    let timerId;
    if (startLongPress) {
      timerId = setTimeout(callback, ms);
    } else {
      clearTimeout(timerId);
    }

    return () => {
      clearTimeout(timerId);
    };
  }, [callback, ms, startLongPress]);

  const start = useCallback(() => {
    setStartLongPress(true);
  }, []);
  const stop = useCallback(() => {
    setStartLongPress(false);
  }, []);

  return {
    onMouseDown: start,
    onMouseUp: stop,
    onMouseLeave: stop,
    onTouchStart: start,
    onTouchEnd: stop,
  };
}
于 2019-04-27T13:07:39.203 回答
9

基于@Sublime 我上面关于避免多次重新渲染的评论,我的版本不使用任何触发渲染的东西:

export function useLongPress({
  onClick = () => {},
  onLongPress = () => {},
  ms = 300,
} = {}) {
  const timerRef = useRef(false);
  const eventRef = useRef({});

  const callback = useCallback(() => {
    onLongPress(eventRef.current);
    eventRef.current = {};
    timerRef.current = false;
  }, [onLongPress]);

  const start = useCallback(
    (ev) => {
      ev.persist();
      eventRef.current = ev;
      timerRef.current = setTimeout(callback, ms);
    },
    [callback, ms]
  );

  const stop = useCallback(
    (ev) => {
      ev.persist();
      eventRef.current = ev;
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        onClick(eventRef.current);
        timerRef.current = false;
        eventRef.current = {};
      }
    },
    [onClick]
  );

  return useMemo(
    () => ({
      onMouseDown: start,
      onMouseUp: stop,
      onMouseLeave: stop,
      onTouchStart: start,
      onTouchEnd: stop,
    }),
    [start, stop]
  );
}

它还同时提供onLongPressonClick传递接收到的事件对象。

用法主要如前所述,除了参数现在在对象中传递,所有都是可选的:

  const longPressProps = useLongPress({
    onClick: (ev) => console.log('on click', ev.button, ev.shiftKey),
    onLongPress: (ev) => console.log('on long press', ev.button, ev.shiftKey),
  });

// and later:
  return (<button {...longPressProps}>click me</button>);
于 2020-04-18T10:25:42.837 回答
6

这是最受欢迎的答案的Typescript版本,以防它对任何人有用:

(它还解决了通过使用和克隆事件访问event委托事件中的属性的问题)timeOute.persist()

使用LongPress.ts

import { useCallback, useRef, useState } from "react";
  
function preventDefault(e: Event) {
  if ( !isTouchEvent(e) ) return;
  
  if (e.touches.length < 2 && e.preventDefault) {
    e.preventDefault();
  }
};

export function isTouchEvent(e: Event): e is TouchEvent {
  return e && "touches" in e;
};

interface PressHandlers<T> {
  onLongPress: (e: React.MouseEvent<T> | React.TouchEvent<T>) => void,
  onClick?: (e: React.MouseEvent<T> | React.TouchEvent<T>) => void,
}

interface Options {
  delay?: number,
  shouldPreventDefault?: boolean
}

export default function useLongPress<T>(
  { onLongPress, onClick }: PressHandlers<T>,
  { delay = 300, shouldPreventDefault = true }
  : Options
  = {}
) {
  const [longPressTriggered, setLongPressTriggered] = useState(false);
  const timeout = useRef<NodeJS.Timeout>();
  const target = useRef<EventTarget>();

  const start = useCallback(
    (e: React.MouseEvent<T> | React.TouchEvent<T>) => {
      e.persist();
      const clonedEvent = {...e};
      
      if (shouldPreventDefault && e.target) {
        e.target.addEventListener(
          "touchend",
          preventDefault,
          { passive: false }
        );
        target.current = e.target;
      }

      timeout.current = setTimeout(() => {
        onLongPress(clonedEvent);
        setLongPressTriggered(true);
      }, delay);
    },
    [onLongPress, delay, shouldPreventDefault]
  );

  const clear = useCallback((
      e: React.MouseEvent<T> | React.TouchEvent<T>,
      shouldTriggerClick = true
    ) => {
      timeout.current && clearTimeout(timeout.current);
      shouldTriggerClick && !longPressTriggered && onClick?.(e);

      setLongPressTriggered(false);

      if (shouldPreventDefault && target.current) {
        target.current.removeEventListener("touchend", preventDefault);
      }
    },
    [shouldPreventDefault, onClick, longPressTriggered]
  );

  return {
    onMouseDown: (e: React.MouseEvent<T>) => start(e),
    onTouchStart: (e: React.TouchEvent<T>) => start(e),
    onMouseUp: (e: React.MouseEvent<T>) => clear(e),
    onMouseLeave: (e: React.MouseEvent<T>) => clear(e, false),
    onTouchEnd: (e: React.TouchEvent<T>) => clear(e)
  };
};
于 2021-02-07T14:54:36.557 回答
4

Here's a component that provides onClick and onHold events - adapt as needed...

CodeSandbox: https://codesandbox.io/s/hold-press-event-r8q9w

Usage:

import React from 'react'
import Holdable from './holdable'

function App() {

  function onClick(evt) {
    alert('click ' + evt.currentTarget.id)
  }

  function onHold(evt) {
    alert('hold ' + evt.currentTarget.id)
  }

  const ids = 'Label1,Label2,Label3'.split(',')

  return (
    <div className="App">
      {ids.map(id => (
        <Holdable
          onClick={onClick}
          onHold={onHold}
          id={id}
          key={id}
        >
          {id}
        </Holdable>
      ))}
    </div>
  )
}

holdable.jsx:

import React from 'react'

const holdTime = 500 // ms
const holdDistance = 3**2 // pixels squared

export default function Holdable({id, onClick, onHold, children}) {

  const [timer, setTimer] = React.useState(null)
  const [pos, setPos] = React.useState([0,0])

  function onPointerDown(evt) {
    setPos([evt.clientX, evt.clientY]) // save position for later
    const event = { ...evt } // convert synthetic event to real object
    const timeoutId = window.setTimeout(timesup.bind(null, event), holdTime)
    setTimer(timeoutId)
  }

  function onPointerUp(evt) {
    if (timer) {
      window.clearTimeout(timer)
      setTimer(null)
      onClick(evt)
    }
  }

  function onPointerMove(evt) {
    // cancel hold operation if moved too much
    if (timer) {
      const d = (evt.clientX - pos[0])**2 + (evt.clientY - pos[1])**2
      if (d > holdDistance) {
        setTimer(null)  
        window.clearTimeout(timer)
      }
    }
  }

  function timesup(evt) {
    setTimer(null)
    onHold(evt)
  }

  return (
    <div
      onPointerDown={onPointerDown}
      onPointerUp={onPointerUp}
      onPointerMove={onPointerMove}
      id={id}
    >
      {children}
    </div>
  )
}

Note: this doesn't work with Safari yet - pointer events are coming in v13 though - https://caniuse.com/#feat=pointer

于 2019-09-08T08:42:09.317 回答
2

避免重新渲染的通用钩子

这是我在生产中使用的东西,受原始答案的启发。如果下面有错误,那么我想我在生产中有错误!‍♂️</p>

用法

如果实现需要它,我想保持钩子更简洁并允许可组合性(例如:添加快速输入与慢速输入,而不是单个回调)。

const [onStart, onEnd] = useLongPress(() => alert('Old School Alert'), 1000);

return (
  <button
    type="button"
    onTouchStart={onStart}
    onTouchEnd={onEnd}
  >
    Hold Me (Touch Only)
  </button>
)

执行

这是一个比看起来更简单的实现。只是更多的评论行。

我添加了一堆评论,因此如果您将其复制/粘贴到您的代码库中,您的同事可以在 PR 期间更好地理解它。

import {useCallback, useRef} from 'react';

export default function useLongPress(
  // callback that is invoked at the specified duration or `onEndLongPress`
  callback : () => any,
  // long press duration in milliseconds
  ms = 300
) {
  // used to persist the timer state
  // non zero values means the value has never been fired before
  const timerRef = useRef<number>(0);

  // clear timed callback
  const endTimer = () => {
    clearTimeout(timerRef.current || 0);
    timerRef.current = 0;
  };

  // init timer
  const onStartLongPress = useCallback((e) => {
    // stop any previously set timers
    endTimer();

    // set new timeout
    timerRef.current = window.setTimeout(() => {
      callback();
      endTimer();
    }, ms);
  }, [callback, ms]);

  // determine to end timer early and invoke the callback or do nothing
  const onEndLongPress = useCallback(() => {
    // run the callback fn the timer hasn't gone off yet (non zero)
    if (timerRef.current) {
      endTimer();
      callback();
    }
  }, [callback]);

  return [onStartLongPress, onEndLongPress, endTimer];
}

例子

在示例中使用 500ms 设置。当我按下时,GIF 中的自发圆圈会显示出来。

例子

于 2021-12-09T22:26:27.817 回答
2

这是我自己可以做出的最简单和最好的解决方案。

  • 这样你就不需要传递点击事件
  • 点击事件仍然有效
  • 钩子返回一个函数而不是事件本身,然后您可以在循环中或有条件地使用它,并将不同的回调传递给每个元素。

使用LongPress.js

export default function useLongPress() {
  return function (callback) {
    let timeout;
    let preventClick = false;

    function start() {
      timeout = setTimeout(() => {
        preventClick = true;
        callback();
      }, 300);
    }

    function clear() {
      timeout && clearTimeout(timeout);
      preventClick = false;
    }

    function clickCaptureHandler(e) {
      if (preventClick) {
        e.stopPropagation();
        preventClick = false;
      }
    }

    return {
      onMouseDown: start,
      onTouchStart: start,
      onMouseUp: clear,
      onMouseLeave: clear,
      onTouchMove: clear,
      onTouchEnd: clear,
      onClickCapture: clickCaptureHandler
    };
  }
}

用法:

import useLongPress from './useLongPress';

export default function MyComponent(){
  const onLongPress = useLongPress();
  const buttons = ['button one', 'button two', 'button three'];

  return (
    buttons.map(text => 
      <button
        onClick={() => console.log('click still working')}
        {...onLongPress(() => console.log('long press worked for ' + text))}
      >
      {text}
      </button>
    )
  )
}
于 2021-03-27T07:23:35.107 回答
1

Ionic React LongPress 示例 我将它与 Ionic React 一起使用,效果很好。

import React, {useState}  from 'react';
import { Route, Redirect } from 'react-router';

interface MainTabsProps { }
const MainTabs: React.FC<MainTabsProps> = () => {

// timeout id  
var initial: any;

// setstate
const [start, setStart] = useState(false);

const handleButtonPress = () => {
  initial = setTimeout(() => {
    setStart(true); // start long button          
    console.log('long press button');
    }, 1500);
}

const handleButtonRelease = () => {
  setStart(false); // stop long press   
  clearTimeout(initial); // clear timeout  
  if(start===false) { // is click
    console.log('click button');
  }  
}

  return (
    <IonPage>
      <IonHeader>
        <IonTitle>Ionic React LongPress</IonTitle>
      </IonHeader>    
      <IonContent className="ion-padding">
        <IonButton expand="block"  
          onMouseDown={handleButtonPress} 
          onMouseUp={handleButtonRelease} >LongPress</IonButton>    
      </IonContent>
    </IonPage>
  );
};

export default MainTabs;
于 2020-06-28T16:20:06.253 回答
1

只是想指出钩子在这里不是一个很好的解决方案,因为您不能在回调中使用它们。

例如,如果您想向多个元素添加长按:

items.map(item => <button {...useLongPress(() => handle(item))}>{item}</button>)

得到你:

... React Hooks 必须在 React 函数组件或自定义 React Hook 函数中调用

但是,您可以使用香草 JS:

export default function longPressEvents(callback, ms = 500) {
  let timeout = null

  const start = () => timeout = setTimeout(callback, ms)
  const stop = () => timeout && window.clearTimeout(timeout)

  return callback ? {
    onTouchStart: start,
    onTouchMove: stop,
    onTouchEnd: stop,
  } : {}
}

然后:

items.map(item => <button { ...longPressEvents(() => handle(item)) }>{item}</button>)

演示:https ://codesandbox.io/s/long-press-hook-like-oru24?file=/src/App.js

请注意,longPressEvents它将运行每个渲染。可能没什么大不了的,但要记住一些事情。

于 2020-10-25T03:25:59.950 回答
1

类型脚本示例制作常见的长按事件

import { useCallback, useRef, useState } from "react";

interface Props {
    onLongPress: (e: any) => void;
    onClick: (e: any) => void;
    obj: { shouldPreventDefault: boolean, delay: number }
}

const useLongPress = (props: Props) => {
    const [longPressTriggered, setLongPressTriggered] = useState(false);
    const timeout: any = useRef();
    const target: any = useRef();

    const start = useCallback(
        event => {
            if (props.obj.shouldPreventDefault && event.target) {
                event.target.addEventListener("touchend", preventDefault, {
                    passive: false
                });
                target.current = event.target;
            }
            timeout.current = setTimeout(() => {
                props.onLongPress(event);
                setLongPressTriggered(true);
            }, props.obj.delay);
        },
        [props]
    );

    const clear = useCallback(
        (event, shouldTriggerClick = true) => {
            timeout.current && clearTimeout(timeout.current);
            shouldTriggerClick && !longPressTriggered && props.onClick(event);
            setLongPressTriggered(false);
            if (props.obj.shouldPreventDefault && target.current) {
                target.current.removeEventListener("touchend", preventDefault);
            }
        },
        [longPressTriggered, props]
    );

    return {
        onMouseDown: (e: any) => start(e),
        onTouchStart: (e: any) => start(e),
        onMouseUp: (e: any) => clear(e),
        onMouseLeave: (e: any) => clear(e, false),
        onTouchEnd: (e: any) => clear(e)
    };
};

const isTouchEvent = (event: any) => {
    return "touches" in event;
};

const preventDefault = (event: any) => {
    if (!isTouchEvent(event)) return;

    if (event.touches.length < 2 && event.preventDefault) {
        event.preventDefault();
    }
};

export default useLongPress;

使用该通用功能

import useLongPress from "shared/components/longpress";

    const onLongPress = () => {
        console.log('longpress is triggered');
        // setlongPressCount(longPressCount + 1)
    };

    const onClick = () => {
        console.log('click is triggered')
        // setClickCount(clickCount + 1)
    }

    const defaultOptions = {
        shouldPreventDefault: true,
        delay: 500,
    };


<div {...longPressEvent}></div>

于 2021-04-20T08:25:28.093 回答
1

Brian 的解决方案允许您将参数传递给孩子,我认为这对于 Hook 是不可行的。尽管如此,如果我可以为最常见的情况建议一个更简洁的解决方案,您希望将 onHold 行为添加到单个组件并且您还希望能够更改 onHold 超时。

带有 Chip 组件的 Material-UI 示例:

'use strict';

const {
  Chip
} = MaterialUI

function ChipHoldable({
  onClick = () => {},
  onHold = () => {},
  hold = 500,
  ...props
}) {
  const [timer, setTimer] = React.useState(null);

  function onPointerDown(evt) {
    const event = { ...evt
    }; // convert synthetic event to real object
    const timeoutId = window.setTimeout(timesup.bind(null, event), hold);
    setTimer(timeoutId);
  }

  function onPointerUp(evt) {
    if (timer) {
      window.clearTimeout(timer);
      setTimer(null);
      onClick(evt);
    }
  }

  const onContextMenu = e => e.preventDefault();

  const preventDefault = e => e.preventDefault(); // so that ripple effect would be triggered

  function timesup(evt) {
    setTimer(null);
    onHold(evt);
  }

  return React.createElement(Chip, {
    onPointerUp,
    onPointerDown,
    onContextMenu,
    onClick: preventDefault,
    ...props
  });
}

const App = () =>  <div> {[1,2,3,4].map(i => < ChipHoldable style={{margin:"10px"}}label = {`chip${i}`}
    onClick = {
      () => console.log(`chip ${i} clicked`)
    }
    onHold = {
      () => console.log(`chip ${i} long pressed`)
    }
    />)}
    </div>


ReactDOM.render( <App/>, document.querySelector('#root'));
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
</head>

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
  <script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script>
</body>

</html>

于 2019-12-09T15:28:18.523 回答
1

David 解决方案的改编:当您想要重复触发事件时的 React 钩子。它setInterval改为使用。

export function useHoldPress(callback = () => {}, ms = 300) {
  const [startHoldPress, setStartHoldPress] = useState(false);

  useEffect(() => {
    let timerId;
    if (startHoldPress) {
      timerId = setInterval(callback, ms);
    } else {
      clearTimeout(timerId);
    }

    return () => {
      clearTimeout(timerId);
    };
  }, [startHoldPress]);

  return {
    onMouseDown: () => setStartHoldPress(true),
    onMouseUp: () => setStartHoldPress(false),
    onMouseLeave: () => setStartHoldPress(false),
    onTouchStart: () => setStartHoldPress(true),
    onTouchEnd: () => setStartHoldPress(false)
  };
}
于 2019-12-18T15:12:13.817 回答