網站搜尋

angular - RXJS 對鍵盤/滑鼠點擊組合作出反應


我對 RXJS 還算新手。我想要達到的效果是當使用者按下空白鍵(或任何其他鍵)並用滑鼠拖曳元素時,能夠拖曳一個 HtmlElement。

因此,拖曳的開始是由按下 SPACE 鍵後左鍵點擊(SPACE-down_LeftClick)或先左鍵點擊再按下 SPACE 鍵(LeftClick_SPACE-down)觸發的。兩者都被接受以方便使用。拖曳的結束是由釋放 SPACE 鍵(keyup 事件)或滑鼠點擊(mouseup 事件)觸發的,以先發生者為準。

實作的想法是有一個 'dragstart' 可觀察對象和一個 'dragend' 可觀察對象,我可以訂閱它們。我有 4 個對應事件的可觀察對象,開始時會根據特定的鍵(空格鍵)和滑鼠按鈕(左鍵)進行過濾:

spaceDown$ = fromEvent<KeyboardEvent>(document, "keydown").pipe( filter(key => key.code === 'Space'))
spaceUp$ = fromEvent<KeyboardEvent>(document, "keyup").pipe( filter( key => key.code === 'Space'))
mouseDown$ = fromEvent<MouseEvent>(document, "mousedown").pipe( filter( mouse => mouse.button == 0))
mouseUp$ = fromEvent<MouseEvent>(document, "mouseup").pipe( filter( mouse => mouse.button == 0))

我正在努力的是如何在 RXJS 中實現所需的行為。我嘗試過使用 'combineLatest'、'withLatestFrom'、'merge' 不同的流,但沒有一個像我希望的那樣運作。

有什麼方法可以解決這個問題嗎?

答案#1

我會將來源的 observables 映射到一個布林值,其中 true 代表按下。然後為鍵盤和滑鼠創建合併的 observables。接著再次將這兩個流合併。如果那裡的兩個值都為 true,則表示拖曳開始事件,否則就是拖曳結束。

const spaceDown$ = fromEvent<KeyboardEvent>(document, 'keydown').pipe(
  filter((key) => key.code === 'Space'),
  map(() => true)
);

const spaceUp$ = fromEvent<KeyboardEvent>(document, 'keyup').pipe(
  filter((key) => key.code === 'Space'),
  map(() => false)
);

const mouseDown$ = fromEvent<MouseEvent>(document, 'mousedown').pipe(
  filter((mouse) => mouse.button == 0),
  map(() => true)
);

const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup').pipe(
  filter((mouse) => mouse.button == 0),
  map(() => false)
);

const key$ = merge(spaceDown$, spaceUp$).pipe(startWith(false));
const mouse$ = merge(mouseDown$, mouseUp$).pipe(startWith(false));

const combined$ = combineLatest([
  key$,
  mouse$,
]).pipe(
  map(([key, mouse]) => key && mouse),
  distinctUntilChanged(),
);

combined$.subscribe((res) => console.log('combined$', res));

// If different streams are required for dragstart and dragend:

const [dragStart$, dragEnd$] = partition(combined$, (val) => val);

dragStart$.subscribe(() => console.log('dragStart$'));
dragEnd$.subscribe(() => console.log('dragEnd$'));

答案#2

所以,對此你有多種方法,這裡是我的兩種看法:

第一個:所謂的「假」版本

在此解決方案中,你使用行為主題來控制動作,並且只是篩選何時進行監聽。

如果我對運算子不太了解,我會使用這種方法:它容易理解,而且效果相當不錯。

import {
  BehaviorSubject,
  combineLatest,
  filter,
  fromEvent,
  map,
  Observable,
  switchMap,
  throttleTime,
} from 'rxjs';

const filterSpace = (source: Observable<KeyboardEvent>) =>
  source.pipe(filter((v) => v.code === 'Space'));

const filterLeft = (source: Observable<MouseEvent>) =>
  source.pipe(filter((v) => v.button === 0));

const mousedown$ = fromEvent(document, 'mousedown').pipe(filterLeft);
const mouseup$ = fromEvent(document, 'mouseup').pipe(filterLeft);

const spacedown$ = fromEvent(document, 'keydown').pipe(filterSpace);
const spaceup$ = fromEvent(document, 'keyup').pipe(filterSpace);

const movement$ = fromEvent(document, 'mousemove');

const clicking$ = new BehaviorSubject(false);
const spacing$ = new BehaviorSubject(false);

mousedown$.subscribe(() => clicking$.next(true));
mouseup$.subscribe(() => clicking$.next(false));

spacedown$.subscribe(() => spacing$.next(true));
spaceup$.subscribe(() => spacing$.next(false));

const moving$ = movement$.pipe(
  switchMap((event) =>
    combineLatest([clicking$, spacing$]).pipe(
      filter((conds) => conds.every((v) => !!v)),
      map(() => event)
    )
  )
);

moving$.pipe(throttleTime(100)).subscribe(() => console.log('dragging'));

第二個:所謂的「進階」版本

這個有點難理解,但我相信這就是你所要求的。

我們首先創建「starting」監聽器,它是兩個條件(點擊 + 空格)的 combineLatest。我們取第一次出現以便在事件發生時立即關閉它,並重複它以便我們可以……好吧,重複這個行為。

注意:你也可以改用過濾器運算子,就像先前使用 .every 的解法,但嘿,那也算是一種炫耀,不是嗎?

接下來,我們設定我們的「停止」條件:如果任一條件被滿足,這個可觀測對象就會發出。

與 combineLatest 不同,我們不需要 first 和 repeat,因為 race 不像 combineLatest 那樣持有狀態:它只是當其中一個成員發出時才發出。

最後,我們將行為切換到我們想要監聽的實際可觀察對象(滑鼠移動),我們持續監聽直到達到停止條件,當完成後,我們再次重複,以便行為再次重複。

import {
  combineLatest,
  filter,
  first,
  fromEvent,
  Observable,
  race,
  repeat,
  switchMap,
  takeUntil,
  throttleTime,
} from 'rxjs';

const filterSpace = (source: Observable<KeyboardEvent>) =>
  source.pipe(filter((v) => v.code === 'Space'));

const filterLeft = (source: Observable<MouseEvent>) =>
  source.pipe(filter((v) => v.button === 0));

const mousedown$ = fromEvent(document, 'mousedown').pipe(filterLeft);
const mouseup$ = fromEvent(document, 'mouseup').pipe(filterLeft);

const spacedown$ = fromEvent(document, 'keydown').pipe(filterSpace);
const spaceup$ = fromEvent(document, 'keyup').pipe(filterSpace);

const movement$ = fromEvent(document, 'mousemove');

const starting$ = combineLatest([mousedown$, spacedown$]).pipe(
  first(),
  repeat()
);
const stopping$ = race(mouseup$, spaceup$);

const moving$ = starting$.pipe(
  switchMap(() => movement$),
  takeUntil(stopping$),
  repeat()
);

moving$.pipe(throttleTime(100)).subscribe(() => console.log('dragging'));