1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 tvcm.require('tvcm.guid');
9 tvcm.exportTo('tvcm', function() {
11 * KeyEventManager avoids leaks when listening for keys.
13 * A common but leaky pattern is:
14 * document.addEventListener('key*', function().bind(this))
18 * KeyEventManager.instance.addListener('keyDown', func, this);
20 * This will not leak. BUT, note, if "this" is not attached to the document,
21 * it will NOT receive input events.
23 * Conceptually, KeyEventManager works by making the this refrence "weak",
24 * which is actually accomplished by putting a guid on the thisArg. When keys
25 * are received, we look for elements with that guid and dispatch the keys to
28 function KeyEventManager(opt_document) {
29 this.document_ = opt_document || document;
30 if (KeyEventManager.instance)
31 throw new Error('KeyEventManager is a singleton.');
32 this.onEvent_ = this.onEvent_.bind(this);
33 this.document_.addEventListener('keydown', this.onEvent_);
34 this.document_.addEventListener('keypress', this.onEvent_);
35 this.document_.addEventListener('keyup', this.onEvent_);
38 KeyEventManager.instance = undefined;
40 KeyEventManager.resetInstanceForUnitTesting = function() {
41 if (KeyEventManager.instance) {
42 KeyEventManager.instance.destroy();
43 KeyEventManager.instance = undefined;
45 KeyEventManager.instance = new KeyEventManager();
48 KeyEventManager.prototype = {
49 addListener: function(type, handler, thisArg) {
50 if (!thisArg.keyEventManagerGuid_) {
51 thisArg.keyEventManagerGuid_ = tvcm.GUID.allocate();
52 thisArg.keyEventManagerRefCount_ = 0;
54 thisArg.classList.add('key-event-manager-target');
55 thisArg.keyEventManagerRefCount_++;
57 var guid = thisArg.keyEventManagerGuid_;
58 this.listeners_.push({
65 onEvent_: function(event) {
66 // This does standard DOM event propagation of the given event, but using
67 // guids to locate the thisArg for each listener. See event_target.js for
68 // notes on how this works.
69 var preventDefaultState = undefined;
70 var stopPropagationCalled = false;
72 var oldPreventDefault = event.preventDefault;
73 event.preventDefault = function() {
74 preventDefaultState = false;
75 oldPreventDefault.call(this);
78 var oldStopPropagation = event.stopPropagation;
79 event.stopPropagation = function() {
80 stopPropagationCalled = true;
81 oldStopPropagation.call(this);
84 event.stopImmediatePropagation = function() {
85 throw new Error('Not implemented');
88 var possibleThisArgs = this.document_.querySelectorAll(
89 '.key-event-manager-target');
90 var possibleThisArgsByGUID = {};
91 for (var i = 0; i < possibleThisArgs.length; i++) {
92 possibleThisArgsByGUID[possibleThisArgs[i].keyEventManagerGuid_] =
96 // We need to copy listeners_ and verify the thisArgs exists on each loop
97 // iteration because the event callbacks can change the DOM and listener
99 var listeners = this.listeners_.concat();
100 var type = event.type;
102 for (var i = 0; i < listeners.length; i++) {
103 var listener = listeners[i];
104 if (listener.type !== type)
106 // thisArg went away.
107 var thisArg = possibleThisArgsByGUID[listener.guid];
111 var handler = listener.handler;
112 if (handler.handleEvent)
113 prevented |= handler.handleEvent.call(handler, event) === false;
115 prevented |= handler.call(thisArg, event) === false;
116 if (stopPropagationCalled)
120 // We want to return false if preventDefaulted, or one of the handlers
121 // return false. But otherwise, we want to return undefiend.
122 return !prevented && preventDefaultState;
125 removeListener: function(type, handler, thisArg) {
126 if (thisArg.keyEventManagerGuid_ === undefined)
127 throw new Error('Was not registered with KeyEventManager');
128 if (thisArg.keyEventManagerRefCount_ === 0)
129 throw new Error('No events were registered on the provided thisArg');
130 for (var i = 0; i < this.listeners_.length; i++) {
131 var listener = this.listeners_[i];
132 if (listener.type == type &&
133 listener.handler == handler &&
134 listener.guid == thisArg.keyEventManagerGuid_) {
135 thisArg.keyEventManagerRefCount_--;
136 if (thisArg.keyEventManagerRefCount_ === 0)
137 thisArg.classList.remove('key-event-manager-target');
138 this.listeners_.splice(i, 1);
142 throw new Error('Listener not found');
145 destroy: function() {
146 this.listeners_.splice(0);
147 this.document_.removeEventListener('keydown', this.onEvent_);
148 this.document_.removeEventListener('keypress', this.onEvent_);
149 this.document_.removeEventListener('keyup', this.onEvent_);
152 dispatchFakeEvent: function(type, args) {
153 var e = new KeyboardEvent(type, args);
154 return KeyEventManager.instance.onEvent_.call(undefined, e);
158 KeyEventManager.instance = new KeyEventManager();
161 KeyEventManager: KeyEventManager