• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<!--
3Copyright (c) 2014 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7<link rel="import" href="/tracing/base/guid.html">
8<link rel="import" href="/tracing/ui/base/hot_key.html">
9
10<polymer-element name="tv-ui-b-hotkey-controller">
11  <script>
12  'use strict';
13  Polymer({
14    created: function() {
15      this.isAttached_ = false;
16      this.globalMode_ = false;
17      this.slavedToParentController_ = undefined;
18      this.curHost_ = undefined;
19      this.childControllers_ = [];
20
21      this.bubblingKeyDownHotKeys_ = {};
22      this.capturingKeyDownHotKeys_ = {};
23      this.bubblingKeyPressHotKeys_ = {};
24      this.capturingKeyPressHotKeys_ = {};
25
26      this.onBubblingKeyDown_ = this.onKey_.bind(this, false);
27      this.onCapturingKeyDown_ = this.onKey_.bind(this, true);
28      this.onBubblingKeyPress_ = this.onKey_.bind(this, false);
29      this.onCapturingKeyPress_ = this.onKey_.bind(this, true);
30    },
31
32    attached: function() {
33      this.isAttached_ = true;
34
35      var host = this.findHost_();
36      if (host.__hotkeyController)
37        throw new Error('Multiple hotkey controllers attached to this host');
38
39      host.__hotkeyController = this;
40      this.curHost_ = host;
41
42      var parentElement;
43      if (host.parentElement)
44        parentElement = host.parentElement;
45      else
46        parentElement = host.parentNode.host;
47      var parentController = tr.b.getHotkeyControllerForElement(
48          parentElement);
49
50      if (parentController) {
51        this.slavedToParentController_ = parentController;
52        parentController.addChildController_(this);
53        return;
54      }
55
56      host.addEventListener('keydown', this.onBubblingKeyDown_, false);
57      host.addEventListener('keydown', this.onCapturingKeyDown_, true);
58      host.addEventListener('keypress', this.onBubblingKeyPress_, false);
59      host.addEventListener('keypress', this.onCapturingKeyPress_, true);
60    },
61
62    detached: function() {
63      this.isAttached_ = false;
64
65      var host = this.curHost_;
66      if (!host)
67        return;
68
69      delete host.__hotkeyController;
70      this.curHost_ = undefined;
71
72      if (this.slavedToParentController_) {
73        this.slavedToParentController_.removeChildController_(this);
74        this.slavedToParentController_ = undefined;
75        return;
76      }
77
78      host.removeEventListener('keydown', this.onBubblingKeyDown_, false);
79      host.removeEventListener('keydown', this.onCapturingKeyDown_, true);
80      host.removeEventListener('keypress', this.onBubblingKeyPress_, false);
81      host.removeEventListener('keypress', this.onCapturingKeyPress_, true);
82    },
83
84    addChildController_: function(controller) {
85      var i = this.childControllers_.indexOf(controller);
86      if (i !== -1)
87        throw new Error('Controller already registered');
88      this.childControllers_.push(controller);
89    },
90
91    removeChildController_: function(controller) {
92      var i = this.childControllers_.indexOf(controller);
93      if (i === -1)
94        throw new Error('Controller not registered');
95      this.childControllers_.splice(i, 1);
96      return controller;
97    },
98
99    getKeyMapForEventType_: function(eventType, useCapture) {
100      if (eventType === 'keydown') {
101        if (!useCapture)
102          return this.bubblingKeyDownHotKeys_;
103        else
104          return this.capturingKeyDownHotKeys_;
105      } else if (eventType === 'keypress') {
106        if (!useCapture)
107          return this.bubblingKeyPressHotKeys_;
108        else
109          return this.capturingKeyPressHotKeys_;
110      } else {
111        throw new Error('Unsupported key event');
112      }
113    },
114
115    addHotKey: function(hotKey) {
116      if (!(hotKey instanceof tr.ui.b.HotKey))
117        throw new Error('hotKey must be a tr.ui.b.HotKey');
118
119      var keyMap = this.getKeyMapForEventType_(
120          hotKey.eventType, hotKey.useCapture);
121
122      for (var i = 0; i < hotKey.keyCodes.length; i++) {
123        var keyCode = hotKey.keyCodes[i];
124        if (keyMap[keyCode])
125          throw new Error('Key is already bound for keyCode=' + keyCode);
126      }
127
128      for (var i = 0; i < hotKey.keyCodes.length; i++) {
129        var keyCode = hotKey.keyCodes[i];
130        keyMap[keyCode] = hotKey;
131      }
132      return hotKey;
133    },
134
135    removeHotKey: function(hotKey) {
136      if (!(hotKey instanceof tr.ui.b.HotKey))
137        throw new Error('hotKey must be a tr.ui.b.HotKey');
138
139      var keyMap = this.getKeyMapForEventType_(
140          hotKey.eventType, hotKey.useCapture);
141
142      for (var i = 0; i < hotKey.keyCodes.length; i++) {
143        var keyCode = hotKey.keyCodes[i];
144        if (!keyMap[keyCode])
145          throw new Error('Key is not bound for keyCode=' + keyCode);
146        keyMap[keyCode] = hotKey;
147      }
148      for (var i = 0; i < hotKey.keyCodes.length; i++) {
149        var keyCode = hotKey.keyCodes[i];
150        delete keyMap[keyCode];
151      }
152      return hotKey;
153    },
154
155    get globalMode() {
156      return this.globalMode_;
157    },
158
159    set globalMode(globalMode) {
160      var wasAttached = this.isAttached_;
161      if (wasAttached)
162        this.detached();
163      this.globalMode_ = !!globalMode;
164      if (wasAttached)
165        this.attached();
166    },
167
168    get topmostConroller_() {
169      if (this.slavedToParentController_)
170        return this.slavedToParentController_.topmostConroller_;
171      return this;
172    },
173
174    childRequestsGeneralFocus: function(child) {
175      var topmost = this.topmostConroller_;
176      if (topmost.curHost_) {
177        if (topmost.curHost_.hasAttribute('tabIndex')) {
178          topmost.curHost_.focus();
179        } else {
180          if (document.activeElement)
181            document.activeElement.blur();
182        }
183      } else {
184        if (document.activeElement)
185          document.activeElement.blur();
186      }
187    },
188
189    childRequestsBlur: function(child) {
190      child.blur();
191
192      var topmost = this.topmostConroller_;
193      if (topmost.curHost_) {
194        topmost.curHost_.focus();
195      }
196    },
197
198    findHost_: function() {
199      if (this.globalMode_) {
200        return document.body;
201      } else {
202        if (this.parentElement)
203          return this.parentElement;
204
205        var node = this;
206        while (node.parentNode) {
207          node = node.parentNode;
208        }
209        return node.host;
210      }
211    },
212
213    appendMatchingHotKeysTo_: function(matchedHotKeys,
214                                        useCapture, e) {
215      var localKeyMap = this.getKeyMapForEventType_(e.type, useCapture);
216      var localHotKey = localKeyMap[e.keyCode];
217      if (localHotKey)
218        matchedHotKeys.push(localHotKey);
219
220      for (var i = 0; i < this.childControllers_.length; i++) {
221        var controller = this.childControllers_[i];
222        controller.appendMatchingHotKeysTo_(matchedHotKeys,
223                                             useCapture, e);
224      }
225    },
226
227    onKey_: function(useCapture, e) {
228      // Keys dispatched to INPUT elements still bubble, even when they're
229      // handled. So, skip any events that targeted the input element.
230      if (useCapture == false && e.path[0].tagName == 'INPUT')
231        return;
232
233      var sortedControllers;
234
235      var matchedHotKeys = [];
236      this.appendMatchingHotKeysTo_(matchedHotKeys, useCapture, e);
237
238      if (matchedHotKeys.length === 0)
239        return false;
240
241      if (matchedHotKeys.length > 1) {
242        // TODO(nduca): To do support for coddling hotKeys, we need to
243        // sort the listeners by their capturing/bubbling order and then pick
244        // the one that would topologically win the tie, per DOM dispatch rules.
245        throw new Error('More than one hotKey is currently unsupported');
246      }
247
248
249      var hotKey = matchedHotKeys[0];
250
251      var prevented = 0;
252      prevented |= hotKey.call(e);
253
254      // We want to return false if preventDefaulted, or one of the handlers
255      // return false. But otherwise, we want to return undefiend.
256      return !prevented && e.defaultPrevented;
257    }
258  });
259  </script>
260</polymer-element>
261<script>
262'use strict';
263
264tr.exportTo('tr.b', function() {
265
266  function getHotkeyControllerForElement(refElement) {
267    var curElement = refElement;
268    while (curElement) {
269      if (curElement.tagName === 'tv-ui-b-hotkey-controller')
270        return curElement;
271
272      if (curElement.__hotkeyController)
273        return curElement.__hotkeyController;
274
275      if (curElement.parentElement) {
276        curElement = curElement.parentElement;
277        continue;
278      }
279
280      // Probably inside a shadow
281      curElement = findHost(curElement);
282    }
283    return undefined;
284  }
285
286  function findHost(initialNode) {
287    var node = initialNode;
288    while (node.parentNode) {
289      node = node.parentNode;
290    }
291    return node.host;
292  }
293
294  return {
295    getHotkeyControllerForElement: getHotkeyControllerForElement
296  };
297});
298</script>
299