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'));
