Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to filter keydowns with rxjs?

I need to get clean keydown/keyup events without repetitions. When you press a key keydown event happens, when you release - keyup. Without messy repeated keydowns.

Here's code:

var keyDowns = rx.Observable.fromEvent(document, 'keydown');
var keyUps = rx.Observable.fromEvent(document, 'keyup'); 
var keyActions = rx.Observable.merge(keyDowns, keyUps);

keyActions.subscribe(function(e) {
    console.log e
});

How to adapt it to do the job?

like image 782
Fake Fish Avatar asked Jul 03 '15 12:07

Fake Fish


1 Answers

Check out Federico's answer below for a much more elegant and idiomatic solution.


You can avoid repeated items from an observable (as you see when a key is held) with distinctUntilChanged which filters out values matching the last emitted value (where matching is determined by equality of emitted values or their values transformed by the keySelector callback argument).

In your case, this could look as simple as something like:

var keyDowns = Rx.Observable.fromEvent(document, 'keydown');
var keyUps = Rx.Observable.fromEvent(document, 'keyup');
var keyActions = Rx.Observable
    .merge(keyDowns, keyUps)
    .distinctUntilChanged(function(e) { return e.type + (e.key || e.which); });

keyActions.subscribe(function(e) {
    console.log(e.type, e.key || e.which, e.keyIdentifier);
});

but this falls short when holding multiple keys. In Chrome OS X, if I press and hold A, S, D, and then release A, S, D I get the following in the console:

keydown 65 U+0041
keydown 83 U+0053
keydown 68 U+0044
keyup 65 U+0041
keydown 68 U+0044
keyup 83 U+0053
keydown 68 U+0044
keyup 68 U+0044

Note the repetition of "keydown 68" (D) when releasing A and S. This is because keydown events only repeat for the most-recently pressed key. Using a similar idea to the one in your comment, we can instead try a custom filter which maintains a list of which keys have been pressed:

var keyDowns = Rx.Observable.fromEvent(document, 'keydown');
var keyUps = Rx.Observable.fromEvent(document, 'keyup');
var keyActions = Rx.Observable
    .merge(keyDowns, keyUps)
    .filter((function() {
        var keysPressed = {};
        return function(e) {
            var k = e.key || e.which;
            if (e.type == 'keyup') {
                delete keysPressed[k];
                return true;
            } else if (e.type == 'keydown') {
                if (keysPressed[k]) {
                    return false;
                } else {
                    keysPressed[k] = true;
                    return true;
                }
            }
        };
    })());

keyActions.subscribe(function(e) {
    console.log(e.type, e.key || e.which, e.keyIdentifier);
});

Pressing and holding A, S, D, and then releasing A, S, D now gives:

keydown 65 U+0041
keydown 83 U+0053
keydown 68 U+0044
keyup 65 U+0041
keyup 83 U+0053
keyup 68 U+0044

which is similar to the approach you mentioned in the comment except that we're filtering the combined stream rather than pre-filtering such that the combined stream looks as we like it.

I'm not experienced enough with rx.js to assert that this is the best way to do this, but it certainly feels more in line with the Rx style. (Any comments appreciated.)

like image 177
icio Avatar answered Oct 12 '22 12:10

icio