I'm trying to get a single Observable that can distinguish between a regular click (0-100ms) and a long press (exactly at 1000ms).
pseudocode
mouseup
between 0 - 100ms -> emit click mouseup
sometime after the long press event Visual representation
time diagram
reproduction
https://codesandbox.io/s/long-press-p4el0?file=/src/index.ts
So far I was able to get close using:
interface UIEventResponse {
type: UIEventType,
event: UIEvent
}
type UIEventType = 'click' | 'longPress'
import { fromEvent, merge, Observable, race, timer } from "rxjs";
import { map, mergeMap, switchMap, take, takeUntil } from "rxjs/operators";
const clickTimeout = 100
const longPressTimeout = 1000
const mouseDown$ = fromEvent<MouseEvent>(window, "mousedown");
const mouseUp$ = fromEvent<MouseEvent>(window, "mouseup");
const click1$ = merge(mouseDown$).pipe(
switchMap((event) => {
return race(
timer(longPressTimeout).pipe(mapTo(true)),
mouseUp$.pipe(mapTo(false))
);
})
);
However, if the user keeps the button pressed until before the longPress event can be emitted, it is still emitting a click event.
So I want to restrict the click event to 0-100ms after the mousedown
. If the user holds for 1 second it should immediately emit a long press. My current code only works for the regular click but the long press afterwards is ignored:
const click2$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
takeUntil(timer(clickTimeout)),
mapTo({
type: "click",
event
})
)
);
})
);
I figure this is because the takeUntil
in the second stream of the race
unsubscribes the race
. How can I prevent the mouseup
event from ignoring the first stream in the race
and thus still have the long press event emitted?
Any help is greatly appreciated.
Not the cleanest solution, but should help you with your problem; you're free to improve it to avoid repetition.
const click2$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
mapTo({
type: "click",
event
}),
timeoutWith(
clickTimeout,
mouseUp$.pipe(
mapTo({
type: "longPress",
event
})
)
)
)
);
})
);
Result
If you click and release in under 100ms, it's a click.
If you click and then release after 100ms, it's a longPress.
If you click and don't release, after 2000ms it's a longPress.
Explanation
The race is still used, but instead of takeUntil(timer(...))
I used timeoutWith
; this allows to set a timeout, and if the due is passed it uses another observable to treat a mouseUp as a long press.
mapTo
used instead of map
to clean things up, but it's not necessary.
NOTE: the first mapTo
in the mouseUp$.pipe
must come before the timeoutWith
, as per my example, otherwise the observable returned would always map to "click".
I am not sure I got the problem right, but maybe the zip
function could be your friend in this case.
Here the code
// first create 2 Observables which emit the mousedown and mouseup respectively
// together with a timestamp representing when the event occured
const mouseDown_1$ = fromEvent<MouseEvent>(window, "mousedown").pipe(
map((event) => {
const ts = Date.now();
return { event, ts };
})
);
const mouseUp_1$ = fromEvent<MouseEvent>(window, "mouseup").pipe(
map((event) => {
const ts = Date.now();
return { event, ts };
})
);
// then use the zip function to build an Observable which emits a tuple when
// both mouseDown_1$ and mouseUp_1$ notify
const click3$ = zip(mouseDown_1$, mouseUp_1$).pipe(
// then calculate the time difference between the timestamps and decide
// whether it was a click or a longPress
map(([down, up]) => {
return up.ts - down.ts < clickTimeout
? { event: down.event, type: "click" }
: { event: down.event, type: "longPress" };
})
);
Thanks to @Giovanni Londero for pointing me in the right direction and helping me find a solution that works for me!
const click$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
mapTo({
type: "click",
event
}),
timeoutWith(clickTimeout, mouseUp$.pipe(mapTo(undefined)))
)
);
}),
filter((val) => !!val)
);
I'm happy to get some recommendations on how to improve this code.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.