| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| <link rel="import" href="/base/guid.html"> |
| <link rel="import" href="/ui/base/hot_key.html"> |
| |
| <polymer-element name="tv-ui-b-hotkey-controller"> |
| <script> |
| 'use strict'; |
| Polymer({ |
| created: function() { |
| this.globalMode_ = false; |
| this.slavedToParentController_ = undefined; |
| this.curHost_ = undefined; |
| this.childControllers_ = []; |
| |
| this.bubblingKeyDownHotKeys_ = {}; |
| this.capturingKeyDownHotKeys_ = {}; |
| this.bubblingKeyPressHotKeys_ = {}; |
| this.capturingKeyPressHotKeys_ = {}; |
| |
| this.onBubblingKeyDown_ = this.onKey_.bind(this, false); |
| this.onCapturingKeyDown_ = this.onKey_.bind(this, true); |
| this.onBubblingKeyPress_ = this.onKey_.bind(this, false); |
| this.onCapturingKeyPress_ = this.onKey_.bind(this, true); |
| }, |
| |
| attached: function() { |
| var host = this.findHost_(); |
| if (host.__hotkeyController) |
| throw new Error('Multiple hotkey controllers attached to this host'); |
| |
| host.__hotkeyController = this; |
| this.curHost_ = host; |
| |
| var parentElement; |
| if (host.parentElement) |
| parentElement = host.parentElement; |
| else |
| parentElement = host.parentNode.host; |
| var parentController = tr.b.getHotkeyControllerForElement( |
| parentElement); |
| |
| if (parentController) { |
| this.slavedToParentController_ = parentController; |
| parentController.addChildController_(this); |
| return; |
| } |
| |
| host.addEventListener('keydown', this.onBubblingKeyDown_, false); |
| host.addEventListener('keydown', this.onCapturingKeyDown_, true); |
| host.addEventListener('keypress', this.onBubblingKeyPress_, false); |
| host.addEventListener('keypress', this.onCapturingKeyPress_, true); |
| }, |
| |
| detached: function() { |
| var host = this.curHost_; |
| if (!host) |
| return; |
| |
| delete host.__hotkeyController; |
| this.curHost_ = undefined; |
| |
| if (this.slavedToParentController_) { |
| this.slavedToParentController_.removeChildController_(this); |
| this.slavedToParentController_ = undefined; |
| return; |
| } |
| |
| host.removeEventListener('keydown', this.onBubblingKeyDown_, false); |
| host.removeEventListener('keydown', this.onCapturingKeyDown_, true); |
| host.removeEventListener('keypress', this.onBubblingKeyPress_, false); |
| host.removeEventListener('keypress', this.onCapturingKeyPress_, true); |
| }, |
| |
| addChildController_: function(controller) { |
| var i = this.childControllers_.indexOf(controller); |
| if (i !== -1) |
| throw new Error('Controller already registered'); |
| this.childControllers_.push(controller); |
| }, |
| |
| removeChildController_: function(controller) { |
| var i = this.childControllers_.indexOf(controller); |
| if (i === -1) |
| throw new Error('Controller not registered'); |
| this.childControllers_.splice(i, 1); |
| return controller; |
| }, |
| |
| getKeyMapForEventType_: function(eventType, useCapture) { |
| if (eventType === 'keydown') { |
| if (!useCapture) |
| return this.bubblingKeyDownHotKeys_; |
| else |
| return this.capturingKeyDownHotKeys_; |
| } else if (eventType === 'keypress') { |
| if (!useCapture) |
| return this.bubblingKeyPressHotKeys_; |
| else |
| return this.capturingKeyPressHotKeys_; |
| } else { |
| throw new Error('Unsupported key event'); |
| } |
| }, |
| |
| addHotKey: function(hotKey) { |
| if (!(hotKey instanceof tr.ui.b.HotKey)) |
| throw new Error('hotKey must be a tr.ui.b.HotKey'); |
| |
| var keyMap = this.getKeyMapForEventType_( |
| hotKey.eventType, hotKey.useCapture); |
| |
| for (var i = 0; i < hotKey.keyCodes.length; i++) { |
| var keyCode = hotKey.keyCodes[i]; |
| if (keyMap[keyCode]) |
| throw new Error('Key is already bound for keyCode=' + keyCode); |
| } |
| |
| for (var i = 0; i < hotKey.keyCodes.length; i++) { |
| var keyCode = hotKey.keyCodes[i]; |
| keyMap[keyCode] = hotKey; |
| } |
| return hotKey; |
| }, |
| |
| removeHotKey: function(hotKey) { |
| if (!(hotKey instanceof tr.ui.b.HotKey)) |
| throw new Error('hotKey must be a tr.ui.b.HotKey'); |
| |
| var keyMap = this.getKeyMapForEventType_( |
| hotKey.eventType, hotKey.useCapture); |
| |
| for (var i = 0; i < hotKey.keyCodes.length; i++) { |
| var keyCode = hotKey.keyCodes[i]; |
| if (!keyMap[keyCode]) |
| throw new Error('Key is not bound for keyCode=' + keyCode); |
| keyMap[keyCode] = hotKey; |
| } |
| for (var i = 0; i < hotKey.keyCodes.length; i++) { |
| var keyCode = hotKey.keyCodes[i]; |
| delete keyMap[keyCode]; |
| } |
| return hotKey; |
| }, |
| |
| get globalMode() { |
| return this.globalMode_; |
| }, |
| |
| set globalMode(globalMode) { |
| this.detached(); |
| this.globalMode_ = !!globalMode; |
| this.attached(); |
| }, |
| |
| get topmostConroller_() { |
| if (this.slavedToParentController_) |
| return this.slavedToParentController_.topmostConroller_; |
| return this; |
| }, |
| |
| childRequestsGeneralFocus: function(child) { |
| var topmost = this.topmostConroller_; |
| if (topmost.curHost_) { |
| if (topmost.curHost_.hasAttribute('tabIndex')) { |
| topmost.curHost_.focus(); |
| } else { |
| if (document.activeElement) |
| document.activeElement.blur(); |
| } |
| } else { |
| if (document.activeElement) |
| document.activeElement.blur(); |
| } |
| }, |
| |
| childRequestsBlur: function(child) { |
| child.blur(); |
| |
| var topmost = this.topmostConroller_; |
| if (topmost.curHost_) { |
| topmost.curHost_.focus(); |
| } |
| }, |
| |
| findHost_: function() { |
| if (this.globalMode_) { |
| return document.body; |
| } else { |
| if (this.parentElement) |
| return this.parentElement; |
| |
| var node = this; |
| while (node.parentNode) { |
| node = node.parentNode; |
| } |
| return node.host; |
| } |
| }, |
| |
| appendMatchingHotKeysTo_: function(matchedHotKeys, |
| useCapture, e) { |
| var localKeyMap = this.getKeyMapForEventType_(e.type, useCapture); |
| var localHotKey = localKeyMap[e.keyCode]; |
| if (localHotKey) |
| matchedHotKeys.push(localHotKey); |
| |
| for (var i = 0; i < this.childControllers_.length; i++) { |
| var controller = this.childControllers_[i]; |
| controller.appendMatchingHotKeysTo_(matchedHotKeys, |
| useCapture, e); |
| } |
| }, |
| |
| onKey_: function(useCapture, e) { |
| // Keys dispatched to INPUT elements still bubble, even when they're |
| // handled. So, skip any events that targeted the input element. |
| if (useCapture == false && e.path[0].tagName == 'INPUT') |
| return; |
| |
| var sortedControllers; |
| |
| var matchedHotKeys = []; |
| this.appendMatchingHotKeysTo_(matchedHotKeys, useCapture, e); |
| |
| if (matchedHotKeys.length === 0) |
| return false; |
| |
| if (matchedHotKeys.length > 1) { |
| // TODO(nduca): To do support for coddling hotKeys, we need to |
| // sort the listeners by their capturing/bubbling order and then pick |
| // the one that would topologically win the tie, per DOM dispatch rules. |
| throw new Error('More than one hotKey is currently unsupported'); |
| } |
| |
| |
| var hotKey = matchedHotKeys[0]; |
| |
| var prevented = 0; |
| prevented |= hotKey.call(e); |
| |
| // We want to return false if preventDefaulted, or one of the handlers |
| // return false. But otherwise, we want to return undefiend. |
| return !prevented && e.defaultPrevented; |
| } |
| }); |
| </script> |
| </polymer-element> |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.b', function() { |
| |
| function getHotkeyControllerForElement(refElement) { |
| var curElement = refElement; |
| while (curElement) { |
| if (curElement.tagName === 'tv-ui-b-hotkey-controller') |
| return curElement; |
| |
| if (curElement.__hotkeyController) |
| return curElement.__hotkeyController; |
| |
| if (curElement.parentElement) { |
| curElement = curElement.parentElement; |
| continue; |
| } |
| |
| // Probably inside a shadow |
| curElement = findHost(curElement); |
| } |
| return undefined; |
| } |
| |
| function findHost(initialNode) { |
| var node = initialNode; |
| while (node.parentNode) { |
| node = node.parentNode; |
| } |
| return node.host; |
| } |
| |
| return { |
| getHotkeyControllerForElement: getHotkeyControllerForElement |
| }; |
| }); |
| </script> |