11

I'm trying to get my controller to watch for a combination of keys. For argument's sake, let's say: up up down down left right left right b a. How can I get angular to look out for these regardless of where in the page the user currently is?

Korra
  • 489
  • 3
  • 8
  • 20
  • Keep in mind that you [could](http://stackoverflow.com/a/4954431/1927876) do `if (e.ctrlKey && e.which === number)`. – Adam Zerner Jul 19 '15 at 20:20
  • If you want to trigger code with a combination of keys on keyPress (eg `Ctrl + a`), have a look at this (using AngularJS factory): https://jsfiddle.net/firehist/nzUBg/ – Raghu Kumara Chari Jul 10 '14 at 13:18

7 Answers7

12

Looks like you can use the ng-keydown to do this.

Here is a working plunker.

For this sample, I just bound ng-keydown to <body>. Works pretty well to catch all the keyboard events globally.

As @charlietfl points out, ng-keydown registers a lot of keyboard events so to make this usable would be a lot of work. For example, if you were trying to listen for a combination (like ctrl + r), then the ctrl key will register many times.

JS:

var myApp = angular.module('myApp', []);

myApp.controller('Ctrl', function($scope) {


    $scope.keyBuffer = [];

    function arrays_equal(a,b) { return !(a<b || b<a); }

    $scope.down = function(e) {

      $scope.keyBuffer.push(e.keyCode);

      var upUp = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
      if (arrays_equal(upUp, $scope.keyBuffer)) {

        alert('thats it!');
      }
    };

  });

HTML:

<body ng-controller="Ctrl" ng-keydown="down($event)">
Davin Tryon
  • 66,517
  • 15
  • 143
  • 132
  • 1
    was playing with this but have to check for break in squence by user, empty array if it breaks, index existing array before push new key...starts getting time consuming making it bulletproof – charlietfl Nov 11 '13 at 17:27
  • @charlietfl yes, exactly. Because `ng-keydown` repeats if you hold down the key. I hadn't checked this out until now. maybe `ng-keyup` would work better? – Davin Tryon Nov 11 '13 at 17:28
  • 1
    lots more to it once you dig in.. – charlietfl Nov 11 '13 at 17:29
  • This works, I've also added buffer functionality so keyBuffer is never longer than upUp. As Kos Prov mentioned in a different answer on this thread, it does have the disadvantage that digest is called on every single keypress. – Korra Nov 11 '13 at 19:15
4

I'm using a different way to do it.

$scope.keyboard = {
  buffer: [],
  detectCombination: function() {
    var codes = {};

    this.buffer.forEach(function(code) {
      codes['key_' + code] = 1;
    });

    if ((codes.key_91 || codes.key_93) && codes.key_8) {
      // I'm looking for 'command + delete'
    }
  },
  keydown: function($event) {
    this.buffer.push($event.keyCode);
    this.detectCombination();
  },
  keyup: function($event, week) {
    this.buffer = [];
  }
};
borisdiakur
  • 10,387
  • 7
  • 68
  • 100
Chris Fan
  • 126
  • 4
3

Detecting Backspace-Key (Mac) and Del-Key (PC):

<body ng-controller="Ctrl" ng-keydown="keyDown($event)">..<body>

$scope.keyDown = function(value){
    if(value.keyCode == 46 || value.keyCode == 8) {
        //alert('Delete Key Pressed');
    }
};
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
2

This is all untested, but you could use ng-keypress

<body ng-keypress="logKeys($rootScope,$event)">...</body>

To call a function something like:

appCtrl.$scope.logKeys = function($rootScope,$event){
    $rootScope.keyLog.shift(); // Remove First Item of Array
    $rootScope.keyLog.push($event.keyCode); // Adds new key press to end of Array
    if($scope.$rootScope.keyLog[0] !== 38) { return false; } // 38 == up key
    if($scope.$rootScope.keyLog[1] !== 38) { return false; }
    if($scope.$rootScope.keyLog[2] !== 40) { return false; } // 40 = down key
    if($scope.$rootScope.keyLog[3] !== 40) { return false; }
    if($scope.$rootScope.keyLog[4] !== 27) { return false; } // 37 = left key
    if($scope.$rootScope.keyLog[5] !== 39) { return false; } // 39 = right key
    if($scope.$rootScope.keyLog[6] !== 37) { return false; }
    if($scope.$rootScope.keyLog[7] !== 39) { return false; }
    if($scope.$rootScope.keyLog[8] !== 65) { return false; } // 65 = a
    if($scope.$rootScope.keyLog[9] !== 66) { return false; } // 66 = b

    $rootScope.doThisWhenAllKeysPressed(); // Got this far, must all match!
    return true;
}

Outside an input field, I don't think ng-keypress works, but the keypress from angular-ui might.

I'm sure there should be an array diff kinda function too, but the specific call evades me right now.

OddEssay
  • 1,334
  • 11
  • 19
0

Check out this plunker. I've implemented a simple '2 UP keystrokes in a row' scenario.

You can do it in plain jQuery and communicate the event with a $rootScope.$broadcast.

Register the jQuery code in and Angular run callback (guarantees that angular has already bootstraped):

app.run(function($rootScope) {
  var upHitOnce = false;
  $(document).keyup(function(event) {
    if (event.which == 38) {
      if (upHitOnce) {
        $rootScope.$broadcast('DoubleUpFired');
        $rootScope.$apply();
        upHitOnce = false;
      } else {
        upHitOnce = true;
      }
    } else {
      upHitOnce = false;
    }
  });
});

and then any controller can listen to this event like:

$scope.$on('DoubleUpFired', function() {
    $scope.fired = true;
});

Binding an ng-keydown action callback to body is ok, but has a small disadvantage. It fires a $digest on every keystroke. What you really want is a $digest only when the sequence has been entered when you somehow need to update the UI.

EDIT

See comments on how to remove actual jQuery dependency.

Kos Prov
  • 4,207
  • 1
  • 18
  • 14
  • I like the fact that it only calls digest when the sequence is entered, but is there any way to do this without explicitly using jQuery? – Korra Nov 11 '13 at 19:27
  • 1
    jQuery is optional. Angular has a jqLite implementation built-in. Just replace `$(document).keyup(function(event) { ... }` with `angular.element(document).bind('keyup', function(event) { ... }` – Kos Prov Nov 11 '13 at 20:07
  • 1
    To be honest, even `$rootScope.$apply()` is a waste. It initiates a `$digest` for the whole scope hierarcy. You could fire the event, and every scope that listens to it can explicitly call `$digest()` on itself. That way, only the interested parts of the page will be re-rendered. Check it out [here](http://plnkr.co/edit/sw4YnOxCEsLFeVq3khfk?p=preview). Of course, no human being can persive a difference between the two ways and a developer could easily forget it. – Kos Prov Nov 11 '13 at 20:10
0

Here's my take on it:

var app = angular.module('contra', []);
app.directive('code', function () {
    function codeState() {
        this.currentState = 0;
        this.keys = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
        this.keyPressed = function (key) {
            if (this.keys[this.currentState] == key) {
                this.currentState++;
                if (this.currentState == this.keys.length) {
                    this.currentState = 0;
                    return true;
                }
            } else {
                this.currentState = 0;
            }
            return false;
        };
    };
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            var cs = new codeState();
            scope.isValid = "NO";
            element.bind("keydown", function (event) {
                scope.$apply(function () {
                    if (cs.keyPressed(event.which)) {
                        scope.isValid = "YES";
                        console.log("CODE ENTERED");
                    } else {
                        scope.isValid = "NO";
                    }
                });
            });
        }
    }
});

What's different about this is it's a directive so if you attach this on the body, it'll apply to the whole page. This also allows for entering the code multiple times.

Plunkr:

http://plnkr.co/edit/tISvsjYKYDrSvA8pu2St

Manny D
  • 20,310
  • 2
  • 29
  • 31
  • Does the plnkr work? I added this to my app directive, it binds to it, but I don't see how to fire it. When I try the plnkr, it just reports NO to the example. Am I doing something wrong? – ScottFoster1000 Oct 28 '17 at 06:51
0

If you are trying 'ctrl+s' or 'commond+s' ( change the commondKey ) to do save, maybe can use like it :

directive :

(function () {

  'use strict';
  var lastKey = 0;
  //var commondKey = 17;
  var commondKey = 91;
  angular
    .module('xinshu')
    .directive('saveEnter', function () {
      return function (scope, element, attrs) {
        element.bind("keydown", function (event) {
          if (event.which != commondKey && event.which != 83) {
            lastKey = 0;
          }
          if (lastKey == commondKey && event.which == 83) {
            scope.$apply(function () {
              scope.$eval(attrs.saveEnter);
            });
            event.preventDefault();
          }
          lastKey = event.which;
        });
      };
    });
})();

element :

<input id="title" save-enter="vm.saveTitle()"/>

You can rename the saveEnter in directive, with change the save-enter in html.

The 'vm.saveTitle()' is the fuc your want to do.

isdot
  • 51
  • 1
  • 4