I'm pretty new to RxJs and haven't read a solution to this. More detailed explanation is in the comments, but basically I want to process a key combination (I think buffer would do this) when a specific key is pressed (like pressing "o" will wait for a short time for other keys to be pressed), but immediately process the key input otherwise (anything other than "o" if "o" hasn't been pressed, or the 'timeout' for "o" has passed).
Observable.fromEvent(document, 'keydown')
// Now I want to only continue processing the event if the user pressed "o99" in series,
// as in pressed "o", followed by "9" and then another "9"
// I think I can do it with buffer
.buffer(() => Observable.timer(1000))
.map((e) => 'option-99')
// However I need to pass the keys through unbuffered if it's anything but an "o" (unless it is following an "o")
// In other words, "o99" is buffered or something, but "9" is not, and processed immediately
.map((e) => e.keyCode)
Thanks
You have a very common case to deal with, which is that of an event whose associated action depends on some control state (i.e. you have a basic state machine). Basically, if you consider those two control states : SIMPLE_KEY
, COMBO
, and those two events : keyup
, and keyupTimer
, then :
SIMPLE_KEY
state
o
, then you switch to COMBO
stateo
, then we remain in same state, and pass the key downstreamCOMBO
state
SIMPLE_KEY
stateSo, to map your event to an action, you need to know the control state you are in. The operator scan
allows you to decide how to process an event depending on an accumulated state.
It could be something like this (https://jsfiddle.net/cbhmxaas/):
function getKeyFromEvent(ev) {return ev.key}
function isKeyPressedIsO(ev) {return getKeyFromEvent(ev) === 'o'}
const KEY = 'key';
const TIMER = 'timer';
const COMBO = 'combo';
const SIMPLE_KEY = 'whatever';
const timeSpanInMs = 1000;
const keyup$ = Rx.Observable.fromEvent(document, 'keyup')
.map(ev => ({event: KEY, data: getKeyFromEvent(ev)}));
const keyupTimer$ = keyup$.flatMapLatest(eventStruct =>
Rx.Observable.of(eventStruct)
// .tap(console.warn.bind(console, 'timer event'))
.delay(timeSpanInMs)
.map(() => ({event : TIMER, data: null}))
);
Rx.Observable.merge(keyup$, keyupTimer$)
// .tap(console.warn.bind(console))
.scan((acc, eventStruct) => {
if (acc.control === SIMPLE_KEY) {
if (eventStruct.event === KEY) {
if (eventStruct.data === `o`) {
return {control: COMBO, keyOrKeys : []}
}
else {
return {control: SIMPLE_KEY, keyOrKeys : eventStruct.data}
}
}
else {
// TIMER event
return {control: SIMPLE_KEY, keyOrKeys : null}
}
}
else {
// we only have two states, so it is COMBO state here
if (eventStruct.event === KEY) {
return {control: COMBO, keyOrKeys : acc.keyOrKeys.concat([eventStruct.data])}
}
else {
// this is a TIMER event, we only have two events
return {control: SIMPLE_KEY, keyOrKeys : acc.keyOrKeys}
}
}
}, {control: SIMPLE_KEY, keyOrKeys : null})
// .tap(console.warn.bind(console))
.filter(state => state.keyOrKeys) // TIMER event in SIMPLE_KEY state
.map (({control, keyOrKeys}) => {
// Here you associate your action
// if keyOrKeys is an array, then you have a combo
// else you have a single key
console.log('key(s) pressed', keyOrKeys)
return keyOrKeys
})
.subscribe (console.log.bind(console))