有没有办法在react-web 应用程序中添加长按事件?
我有地址列表。在长按任何地址时,我想触发事件以删除该地址,然后是一个确认框。
有没有办法在react-web 应用程序中添加长按事件?
我有地址列表。在长按任何地址时,我想触发事件以删除该地址,然后是一个确认框。
我创建了一个带有钩子的代码框来处理长按和点击。基本上,在鼠标按下、触摸开始事件时,会使用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>
);
}
}
使用 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>
);
};
不错的钩子!但我想做一个小的改进。用于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,
};
}
基于@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]
);
}
它还同时提供onLongPress
和onClick
传递接收到的事件对象。
用法主要如前所述,除了参数现在在对象中传递,所有都是可选的:
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>);
这是最受欢迎的答案的Typescript版本,以防它对任何人有用:
(它还解决了通过使用和克隆事件访问event
委托事件中的属性的问题)timeOut
e.persist()
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)
};
};
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
这是我在生产中使用的东西,受原始答案的启发。如果下面有错误,那么我想我在生产中有错误!♂️</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 中的自发圆圈会显示出来。
这是我自己可以做出的最简单和最好的解决方案。
使用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>
)
)
}
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;
只是想指出钩子在这里不是一个很好的解决方案,因为您不能在回调中使用它们。
例如,如果您想向多个元素添加长按:
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
它将运行每个渲染。可能没什么大不了的,但要记住一些事情。
类型脚本示例制作常见的长按事件
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>
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>
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)
};
}