blob: 0a7c5ff6f93a825ee58cbe37a87f1e196bf5b103 [file] [log] [blame]
<!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>