ONOS-3741: Bind Escape to Cancel and Enter to OK in dialog service.
 - also allow arbitrary keybindings to arbitrary text buttons in dialogs.

Change-Id: I5a01abb13fce41f81e8686866d82d2d08c34a71b
diff --git a/web/gui/src/main/webapp/app/fw/layer/dialog.js b/web/gui/src/main/webapp/app/fw/layer/dialog.js
index f0a6b65..e19280d 100644
--- a/web/gui/src/main/webapp/app/fw/layer/dialog.js
+++ b/web/gui/src/main/webapp/app/fw/layer/dialog.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, $window, fs, ps, bns;
+    var $log, $window, fs, ps, bns, ks;
 
     // configuration
     var defaultSettings = {
@@ -32,11 +32,8 @@
         };
 
     // internal state
-    var pApi, panel, dApi;
-
-    // TODO: ONOS-3741
-    // TODO: ESC key invokes Cancel callback
-    // TODO: Enter invokes OK callback
+    var pApi, panel, dApi,
+        keyBindings = {};
 
     // create the dialog; return its API
     function createDialog(id, opts) {
@@ -99,13 +96,20 @@
         };
     }
 
-    function makeButton(text, callback) {
-        var cb = fs.isF(callback);
+    function makeButton(text, callback, keyName) {
+        var cb = fs.isF(callback),
+            key = fs.isS(keyName);
 
         function invoke() {
             cb && cb();
+            clearBindings();
             panel.hide();
         }
+
+        if (key) {
+            keyBindings[key] = invoke;
+        }
+
         return createDiv('dialog-button')
             .text(text)
             .on('click', invoke);
@@ -125,13 +129,26 @@
         return dApi;
     }
 
-    function addButton(text, cb) {
+    function addButton(text, cb, key) {
         if (pApi) {
-            pApi.appendFooter(makeButton(text, cb));
+            pApi.appendFooter(makeButton(text, cb, key));
         }
         return dApi;
     }
 
+    function addOk(cb) {
+        return addButton('OK', cb, 'enter');
+    }
+
+    function addCancel(cb) {
+        return addButton('Cancel', cb, 'esc');
+    }
+
+    function clearBindings() {
+        keyBindings = {};
+        ks.dialogKeys();
+    }
+
     // opens the dialog (creates if necessary)
     function openDialog(id, opts) {
         $log.debug('Open DIALOG', id, opts);
@@ -145,7 +162,12 @@
         dApi = {
             setTitle: setTitle,
             addContent: addContent,
-            addButton: addButton
+            addButton: addButton,
+            addOk: addOk,
+            addCancel: addCancel,
+            bindKeys: function () {
+                ks.dialogKeys(keyBindings);
+            }
         };
         return dApi;
     }
@@ -154,6 +176,7 @@
     function closeDialog() {
         $log.debug('Close DIALOG');
         if (pApi) {
+            clearBindings();
             panel.hide();
             pApi.destroy();
             pApi = null;
@@ -174,16 +197,18 @@
     angular.module('onosLayer')
     .factory('DialogService',
         ['$log', '$window', 'FnService', 'PanelService', 'ButtonService',
+            'KeyService',
 
         // TODO: for now, $window is not used, but we should provide an option
             // to center the dialog on the window.
 
-        function (_$log_, _$window_, _fs_, _ps_, _bns_) {
+        function (_$log_, _$window_, _fs_, _ps_, _bns_, _ks_) {
             $log = _$log_;
             $window = _$window_;
             fs = _fs_;
             ps = _ps_;
             bns = _bns_;
+            ks = _ks_;
 
             return {
                 openDialog: openDialog,
diff --git a/web/gui/src/main/webapp/app/fw/util/keys.js b/web/gui/src/main/webapp/app/fw/util/keys.js
index 9c60e83..a3c5b78 100644
--- a/web/gui/src/main/webapp/app/fw/util/keys.js
+++ b/web/gui/src/main/webapp/app/fw/util/keys.js
@@ -29,6 +29,7 @@
         keyHandler = {
             globalKeys: {},
             maskedKeys: {},
+            dialogKeys: {},
             viewKeys: {},
             viewFn: null,
             viewGestures: []
@@ -104,6 +105,8 @@
             kh = keyHandler,
             gk = kh.globalKeys[key],
             gcb = fs.isF(gk) || (fs.isA(gk) && fs.isF(gk[0])),
+            dk = kh.dialogKeys[key],
+            dcb = fs.isF(dk),
             vk = kh.viewKeys[key],
             kl = fs.isF(kh.viewKeys._keyListener),
             vcb = fs.isF(vk) || (fs.isA(vk) && fs.isF(vk[0])) || fs.isF(kh.viewFn),
@@ -119,6 +122,12 @@
                 // if the event was 'handled', we are done
                 return;
             }
+            // dialog callback?
+            if (dcb) {
+                dcb(token, key, keyCode, event);
+                // assume dialog handled the event
+                return;
+            }
             // otherwise, let the view callback have a shot
             if (vcb) {
                 vcb(token, key, keyCode, event);
@@ -170,26 +179,46 @@
         return true;
     }
 
-    function setKeyBindings(keyArg) {
-        var viewKeys,
-            masked = [];
+    function filterMaskedKeys(map, caller, remove) {
+        var masked = [],
+            msgs = [];
 
-        if (fs.isF(keyArg)) {
-            // set general key handler callback
-            keyHandler.viewFn = keyArg;
-        } else {
-            // set specific key filter map
-            viewKeys = d3.map(keyArg).keys();
-            viewKeys.forEach(function (key) {
-                if (keyHandler.maskedKeys[key]) {
-                    masked.push('setKeyBindings(): Key "' + key + '" is reserved');
-                }
-            });
-
-            if (masked.length) {
-                $log.warn(masked.join('\n'));
+        d3.map(map).keys().forEach(function (key) {
+            if (keyHandler.maskedKeys[key]) {
+                masked.push(key);
+                msgs.push(caller, ': Key "' + key + '" is reserved');
             }
-            keyHandler.viewKeys = keyArg;
+        });
+
+        if (msgs.length) {
+            $log.warn(msgs.join('\n'));
+        }
+
+        if (remove) {
+            masked.forEach(function (k) {
+                delete map[k];
+            });
+        }
+        return masked;
+    }
+
+    function unexParam(fname, x) {
+        $log.warn(fname, ": unexpected parameter-- ", x);
+    }
+
+    function setKeyBindings(keyArg) {
+        var fname = 'setKeyBindings()',
+            kFunc = fs.isF(keyArg),
+            kMap = fs.isO(keyArg);
+
+        if (kFunc) {
+            // set general key handler callback
+            keyHandler.viewFn = kFunc;
+        } else if (kMap) {
+            filterMaskedKeys(kMap, fname, true);
+            keyHandler.viewKeys = kMap;
+        } else {
+            unexParam(fname, keyArg);
         }
     }
 
@@ -213,6 +242,22 @@
         keyHandler.viewGestures = [];
     }
 
+    function bindDialogKeys(map) {
+        var fname = 'bindDialogKeys()',
+            kMap = fs.isO(map);
+
+        if (kMap) {
+            filterMaskedKeys(map, fname, true);
+            keyHandler.dialogKeys = kMap;
+        } else {
+            unexParam(fname, map);
+        }
+    }
+
+    function unbindDialogKeys() {
+        keyHandler.dialogKeys = {};
+    }
+
     function checkNotGlobal(o) {
         var oops = [];
         if (fs.isO(o)) {
@@ -259,6 +304,13 @@
                     }
                 },
                 unbindKeys: unbindKeys,
+                dialogKeys: function (x) {
+                    if (x === undefined) {
+                        unbindDialogKeys();
+                    } else {
+                        bindDialogKeys(x);
+                    }
+                },
                 addSeq: function (word, data) {
                     fs.addToTrie(seq, word, data);
                 },
diff --git a/web/gui/src/main/webapp/app/view/app/app.js b/web/gui/src/main/webapp/app/view/app/app.js
index 7f620ae..a297bcb 100644
--- a/web/gui/src/main/webapp/app/view/app/app.js
+++ b/web/gui/src/main/webapp/app/view/app/app.js
@@ -308,8 +308,9 @@
             ds.openDialog(dialogId, dialogOpts)
                 .setTitle('Confirm Action')
                 .addContent(createConfirmationText(action, itemId))
-                .addButton('OK', dOk)
-                .addButton('Cancel', dCancel);
+                .addOk(dOk)
+                .addCancel(dCancel)
+                .bindKeys();
         }
 
         $scope.appAction = function (action) {