4

I want to detect when mousedown is being fired for longer than 500ms, if so - do something. My attempt:

const button = document.querySelector('button')
const stream = Rx.Observable.fromEvent(button, 'mousedown')
const mouseUp$ = Rx.Observable.fromEvent(button, 'mouseup')
stream.delay(500).takeUntil(mouseUp$).subscribe(() => console.log(1))

It works but only the first time it runs. Then, the stream is cancelled due to takeUntil operator. How to make it work everytime?

DEMO

feerlay
  • 2,286
  • 5
  • 23
  • 47
  • 1
    Possible duplicate of [Resubscribe to takeUntil/skipUntil](https://stackoverflow.com/questions/45264019/resubscribe-to-takeuntil-skipuntil) – Simon Groenewolt Jul 22 '18 at 19:12

4 Answers4

7

Start a TimerObservable for 500ms on every mouseDown$ event. If mouseUp$ get's fired within 500ms unsubscribe from TimerObservable.

const button = document.querySelector('button')
const mouseDown$ = Rx.Observable.fromEvent(button, 'mousedown')
const mouseUp$ = Rx.Observable.fromEvent(button, 'mouseup')

const stream$ = mouseDown$.switchMap(() => Rx.Observable.TimerObservable(500).takeUntil(mouseUp$));

stream$.subscribe(() => console.log('Only Fired after 500ms'))

RxJS >= 6.0.0

import { switchMap, takeUntil } from 'rxjs/operators';
import { timer, fromEvent } from 'rxjs';

const button = document.querySelector('button')
const mouseDown$ = fromEvent(button, 'mousedown')
const mouseUp$ = fromEvent(button, 'mouseup')

const stream$ = mouseDown$.pipe(
  switchMap(() => timer(500).pipe(takeUntil(mouseUp$)))
);

stream$.subscribe(() => console.log('Only Fired after 500ms'))
SplitterAlex
  • 2,755
  • 2
  • 20
  • 23
3

Example of directive for mouse hold:

@Directive({ selector: "[appMouseHold]" })
export class MouseHoldDirective implements OnInit, OnDestroy {
  @Input() set appMouseHold(tick: string | number) {
    if (typeof tick === 'string') {
      tick = parseInt(tick, 10);
    }
    
    this.tick = tick || 500;
  }
  private tick: number;
  private readonly _stop = new Subject<void>();
  private readonly _start = new Subject<void>();
  private subscription: Subscription;

  @Output() mousehold = new EventEmitter<number>();
  @Output() mouseholdstart = new EventEmitter<void>();
  @Output() mouseholdend = new EventEmitter<void>();

  ngOnInit() {
    this.subscription = this._start
      .pipe(
        tap(() => this.mouseholdstart.emit()),
        switchMap(() =>
          timer(500, this.tick).pipe(
            takeUntil(this._stop.pipe(tap(() => this.mouseholdend.emit())))
          )
        )
      )
      .subscribe((tick) => {
        this.mousehold.emit(tick);
      });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  @HostListener("mousedown", ["$event"])
  onMouseDown($event) {
    if ($event.button === 0) {
      this._start.next();
    }
  }

  @HostListener("mouseup")
  onMouseUp() {
    this._stop.next();
  }
}

See Stackblitz

For non-angular usage you can simply replace @HostListener handlers with fromEvent() observables

ndraiman
  • 663
  • 1
  • 10
  • 18
  • 1
    great solution! small unexpected stale inc/dec occurs if user removes holded mouse button outside of html button. I have added `@HostListener("mouseout") onMouseOut() { this._stop.next(); }` – Michal.S Jan 29 '21 at 12:20
0

SplitterAlex's answer is good, but takeUntil() completes observable and you can no longer handle events, so my workaround for that was (it does not completes observable)

public touchStartSubject: Subject<any> = new Subject<any>();
public touchStartObservable: Observable<any> = this.touchStartSubject.asObservable();

public touchEndSubject: Subject<any> = new Subject<any>();
public touchEndObservable: Observable<any> = this.touchEndSubject.asObservable();

@HostListener('touchstart', ['$event'])
public touchStart($event: TouchEvent): void {
    this.touchStartSubject.next($event);
}

@HostListener('touchend', ['$event'])
public touchEnd(): void {
    this.touchEndSubject.next(null);
}

this.touchStartObservable
        .pipe(
            mergeMap((res) => race(
                timer(1500).pipe(map(() => res)),
                this.touchEndObservable,
            )),
        )
        .subscribe((res: TouchEvent) => {
             if (!res) return;
             // do stuff
        })

It would be great if someone could improve my answer without if (!res) return; condition e.g. use .error instead of .next for touchEndSubject

0

An example of an Angular directive that also fires the mouse event

import {Directive, ElementRef, Output} from "@angular/core";
import {fromEvent, merge, timer} from "rxjs";
import {filter, skip, switchMap} from "rxjs/operators";

@Directive({
  selector: '[appLongClick]'
})
export class LongClickDirective {

  /**
   * Minimum time between mouse button down to mouse button up
   */
  private readonly DUE_TIME = 500;

  /**
   * Mouse down event (only left button)
   */
  private mousedown = fromEvent(this.el.nativeElement, 'mousedown').pipe(
    filter((ev: MouseEvent) => ev.button === 0)
  );

  /**
   * Click event (mouse left button up)
   */
  private click = fromEvent(this.el.nativeElement, 'click');

  /**
   * After a mouse button down, take the click only if it comes after the due time
   */
  @Output('appLongClick') longClick = this.mousedown.pipe(
    switchMap(() =>
      merge(this.click, timer(this.DUE_TIME)).pipe(
        skip(1),
        filter(this.isPointerEvent),
      ),
    ),
  );

  constructor(private el: ElementRef) {}

  private isPointerEvent(v: unknown): v is PointerEvent {
    return v instanceof PointerEvent;
  }

}

Example of use:

<div (appLongClick)="doSomething($event)"></div>
Moshe Kohavi
  • 387
  • 3
  • 8