3 Copyright (c) 2014 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
7 <link rel="import" href="/tvcm/guid.html">
11 tvcm.exportTo('tvcm', function() {
14 * KeyEventManager avoids leaks when listening for keys.
16 * A common but leaky pattern is:
17 * document.addEventListener('key*', function().bind(this))
21 * KeyEventManager.instance.addListener('keyDown', func, this);
23 * This will not leak. BUT, note, if "this" is not attached to the document,
24 * it will NOT receive input events.
26 * Conceptually, KeyEventManager works by making the this refrence "weak",
27 * which is actually accomplished by putting a guid on the thisArg. When keys
28 * are received, we look for elements with that guid and dispatch the keys to
31 function KeyEventManager(opt_document) {
32 this.document_ = opt_document || document;
33 if (KeyEventManager.instance)
34 throw new Error('KeyEventManager is a singleton.');
35 this.onEvent_ = this.onEvent_.bind(this);
36 this.document_.addEventListener('keydown', this.onEvent_);
37 this.document_.addEventListener('keypress', this.onEvent_);
38 this.document_.addEventListener('keyup', this.onEvent_);
41 KeyEventManager.instance = undefined;
43 document.head.addEventListener('tvcm-unittest-will-run', function() {
44 if (KeyEventManager.instance) {
45 KeyEventManager.instance.destroy();
46 KeyEventManager.instance = undefined;
48 KeyEventManager.instance = new KeyEventManager();
51 KeyEventManager.prototype = {
52 addListener: function(type, handler, thisArg) {
53 if (!thisArg.keyEventManagerGuid_) {
54 thisArg.keyEventManagerGuid_ = tvcm.GUID.allocate();
55 thisArg.keyEventManagerRefCount_ = 0;
57 thisArg.classList.add('key-event-manager-target');
58 thisArg.keyEventManagerRefCount_++;
60 var guid = thisArg.keyEventManagerGuid_;
61 this.listeners_.push({
68 onEvent_: function(event) {
69 // This does standard DOM event propagation of the given event, but using
70 // guids to locate the thisArg for each listener. See event_target.js for
71 // notes on how this works.
72 var preventDefaultState = undefined;
73 var stopPropagationCalled = false;
75 var oldPreventDefault = event.preventDefault;
76 event.preventDefault = function() {
77 preventDefaultState = false;
78 oldPreventDefault.call(this);
81 var oldStopPropagation = event.stopPropagation;
82 event.stopPropagation = function() {
83 stopPropagationCalled = true;
84 oldStopPropagation.call(this);
87 event.stopImmediatePropagation = function() {
88 throw new Error('Not implemented');
91 var possibleThisArgs = this.document_.querySelectorAll(
92 '.key-event-manager-target');
93 var possibleThisArgsByGUID = {};
94 for (var i = 0; i < possibleThisArgs.length; i++) {
95 possibleThisArgsByGUID[possibleThisArgs[i].keyEventManagerGuid_] =
99 // We need to copy listeners_ and verify the thisArgs exists on each loop
100 // iteration because the event callbacks can change the DOM and listener
102 var listeners = this.listeners_.concat();
103 var type = event.type;
105 for (var i = 0; i < listeners.length; i++) {
106 var listener = listeners[i];
107 if (listener.type !== type)
109 // thisArg went away.
110 var thisArg = possibleThisArgsByGUID[listener.guid];
114 var handler = listener.handler;
115 if (handler.handleEvent)
116 prevented |= handler.handleEvent.call(handler, event) === false;
118 prevented |= handler.call(thisArg, event) === false;
119 if (stopPropagationCalled)
123 // We want to return false if preventDefaulted, or one of the handlers
124 // return false. But otherwise, we want to return undefiend.
125 return !prevented && preventDefaultState;
128 removeListener: function(type, handler, thisArg) {
129 if (thisArg.keyEventManagerGuid_ === undefined)
130 throw new Error('Was not registered with KeyEventManager');
131 if (thisArg.keyEventManagerRefCount_ === 0)
132 throw new Error('No events were registered on the provided thisArg');
133 for (var i = 0; i < this.listeners_.length; i++) {
134 var listener = this.listeners_[i];
135 if (listener.type == type &&
136 listener.handler == handler &&
137 listener.guid == thisArg.keyEventManagerGuid_) {
138 thisArg.keyEventManagerRefCount_--;
139 if (thisArg.keyEventManagerRefCount_ === 0)
140 thisArg.classList.remove('key-event-manager-target');
141 this.listeners_.splice(i, 1);
145 throw new Error('Listener not found');
148 destroy: function() {
149 this.listeners_.splice(0);
150 this.document_.removeEventListener('keydown', this.onEvent_);
151 this.document_.removeEventListener('keypress', this.onEvent_);
152 this.document_.removeEventListener('keyup', this.onEvent_);
155 dispatchFakeEvent: function(type, args) {
156 var e = new KeyboardEvent(type, args);
157 return KeyEventManager.instance.onEvent_.call(undefined, e);
161 KeyEventManager.instance = new KeyEventManager();
164 KeyEventManager: KeyEventManager