Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
@@ -0,0 +1,151 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Action event wrapper implementation.
* @author eae@google.com (Emil A Eklund)
*/
goog.provide('goog.events.actionEventWrapper');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.dom');
goog.require('goog.events');
/** @suppress {extraRequire} */
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.events.EventWrapper');
goog.require('goog.events.KeyCodes');
goog.require('goog.userAgent');
/**
* Event wrapper for action handling. Fires when an element is activated either
* by clicking it or by focusing it and pressing Enter.
*
* @constructor
* @implements {goog.events.EventWrapper}
* @private
*/
goog.events.ActionEventWrapper_ = function() {
};
/**
* Singleton instance of ActionEventWrapper_.
* @type {goog.events.ActionEventWrapper_}
*/
goog.events.actionEventWrapper = new goog.events.ActionEventWrapper_();
/**
* Event types used by the wrapper.
*
* @type {Array<goog.events.EventType>}
* @private
*/
goog.events.ActionEventWrapper_.EVENT_TYPES_ = [
goog.events.EventType.CLICK,
goog.userAgent.GECKO ?
goog.events.EventType.KEYPRESS : goog.events.EventType.KEYDOWN,
goog.events.EventType.KEYUP
];
/**
* Adds an event listener using the wrapper on a DOM Node or an object that has
* implemented {@link goog.events.EventTarget}. A listener can only be added
* once to an object.
*
* @param {goog.events.ListenableType} target The target to listen to events on.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
* method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {Object=} opt_scope Element in whose scope to call the listener.
* @param {goog.events.EventHandler=} opt_eventHandler Event handler to add
* listener to.
* @override
*/
goog.events.ActionEventWrapper_.prototype.listen = function(target, listener,
opt_capt, opt_scope, opt_eventHandler) {
var callback = function(e) {
var listenerFn = goog.events.wrapListener(listener);
var role = goog.dom.isElement(e.target) ?
goog.a11y.aria.getRole(/** @type {!Element} */ (e.target)) : null;
if (e.type == goog.events.EventType.CLICK && e.isMouseActionButton()) {
listenerFn.call(opt_scope, e);
} else if ((e.keyCode == goog.events.KeyCodes.ENTER ||
e.keyCode == goog.events.KeyCodes.MAC_ENTER) &&
e.type != goog.events.EventType.KEYUP) {
// convert keydown to keypress for backward compatibility.
e.type = goog.events.EventType.KEYPRESS;
listenerFn.call(opt_scope, e);
} else if (e.keyCode == goog.events.KeyCodes.SPACE &&
e.type == goog.events.EventType.KEYUP &&
(role == goog.a11y.aria.Role.BUTTON ||
role == goog.a11y.aria.Role.TAB)) {
listenerFn.call(opt_scope, e);
e.preventDefault();
}
};
callback.listener_ = listener;
callback.scope_ = opt_scope;
if (opt_eventHandler) {
opt_eventHandler.listen(target,
goog.events.ActionEventWrapper_.EVENT_TYPES_,
callback, opt_capt);
} else {
goog.events.listen(target,
goog.events.ActionEventWrapper_.EVENT_TYPES_,
callback, opt_capt);
}
};
/**
* Removes an event listener added using goog.events.EventWrapper.listen.
*
* @param {goog.events.ListenableType} target The node to remove listener from.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
* method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {Object=} opt_scope Element in whose scope to call the listener.
* @param {goog.events.EventHandler=} opt_eventHandler Event handler to remove
* listener from.
* @override
*/
goog.events.ActionEventWrapper_.prototype.unlisten = function(target, listener,
opt_capt, opt_scope, opt_eventHandler) {
for (var type, j = 0; type = goog.events.ActionEventWrapper_.EVENT_TYPES_[j];
j++) {
var listeners = goog.events.getListeners(target, type, !!opt_capt);
for (var obj, i = 0; obj = listeners[i]; i++) {
if (obj.listener.listener_ == listener &&
obj.listener.scope_ == opt_scope) {
if (opt_eventHandler) {
opt_eventHandler.unlisten(target, type, obj.listener, opt_capt,
opt_scope);
} else {
goog.events.unlisten(target, type, obj.listener, opt_capt, opt_scope);
}
break;
}
}
}
};
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.actionEventWrapper
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.actionEventWrapperTest');
</script>
</head>
<body>
<div id="a" tabindex="0">
</div>
</body>
</html>
@@ -0,0 +1,270 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.actionEventWrapperTest');
goog.setTestOnly('goog.events.actionEventWrapperTest');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.actionEventWrapper');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
var a, eh, events;
function setUpPage() {
a = document.getElementById('a');
}
function setUp() {
events = [];
eh = new goog.events.EventHandler();
assertEquals('No listeners registered yet', 0,
goog.events.getListeners(a).length);
}
function tearDown() {
eh.dispose();
}
var Foo = function() {};
Foo.prototype.test = function(e) {
events.push(e);
};
function assertListenersExist(el, listenerCount, capt) {
var EVENT_TYPES = goog.events.ActionEventWrapper_.EVENT_TYPES_;
for (var i = 0; i < EVENT_TYPES.length; ++i) {
assertEquals(listenerCount, goog.events.getListeners(
el, EVENT_TYPES[i], capt).length);
}
}
function testAddActionListener() {
var listener = function(e) { events.push(e);};
goog.events.listenWithWrapper(a, goog.events.actionEventWrapper, listener);
assertListenersExist(a, 1, false);
goog.testing.events.fireClickSequence(a);
assertEquals('1 event should have been dispatched', 1, events.length);
assertEquals('Should be an click event', 'click', events[0].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('2 events should have been dispatched', 2, events.length);
assertEquals('Should be a keypress event', 'keypress', events[1].type);
goog.a11y.aria.setRole(
/** @type {!Element} */ (a), goog.a11y.aria.Role.BUTTON);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('3 events should have been dispatched', 3, events.length);
assertEquals('Should be a keyup event', 'keyup', events[2].type);
assertTrue('Should be default prevented.', events[2].defaultPrevented);
goog.a11y.aria.removeRole(/** @type {!Element} */ (a));
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('3 events should have been dispatched', 3, events.length);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ESC);
assertEquals('3 events should have been dispatched', 3, events.length);
goog.a11y.aria.setRole(
/** @type {!Element} */ (a), goog.a11y.aria.Role.TAB);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('4 events should have been dispatched', 4, events.length);
assertEquals('Should be a keyup event', 'keyup', events[2].type);
assertTrue('Should be default prevented.', events[2].defaultPrevented);
goog.a11y.aria.removeRole(/** @type {!Element} */ (a));
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('4 events should have been dispatched', 4, events.length);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ESC);
assertEquals('4 events should have been dispatched', 4, events.length);
goog.events.unlistenWithWrapper(a, goog.events.actionEventWrapper,
listener);
assertListenersExist(a, 0, false);
}
function testAddActionListenerForHandleEvent() {
var listener = {
handleEvent: function(e) { events.push(e); }
};
goog.events.listenWithWrapper(a, goog.events.actionEventWrapper, listener);
assertListenersExist(a, 1, false);
goog.testing.events.fireClickSequence(a);
assertEquals('1 event should have been dispatched', 1, events.length);
assertEquals('Should be an click event', 'click', events[0].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('2 events should have been dispatched', 2, events.length);
assertEquals('Should be a keypress event', 'keypress', events[1].type);
goog.a11y.aria.setRole(
/** @type {!Element} */ (a), goog.a11y.aria.Role.BUTTON);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('3 events should have been dispatched', 3, events.length);
assertEquals('Should be a keyup event', 'keyup', events[2].type);
assertTrue('Should be default prevented.', events[2].defaultPrevented);
goog.a11y.aria.removeRole(/** @type {!Element} */ (a));
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('3 events should have been dispatched', 3, events.length);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ESC);
assertEquals('3 events should have been dispatched', 3, events.length);
goog.events.unlistenWithWrapper(a, goog.events.actionEventWrapper,
listener);
assertListenersExist(a, 0, false);
}
function testAddActionListenerInCaptPhase() {
var count = 0;
var captListener = function(e) {
events.push(e);
assertEquals(0, count);
count++;
};
var bubbleListener = function(e) {
events.push(e);
assertEquals(1, count);
count = 0;
};
goog.events.listenWithWrapper(a, goog.events.actionEventWrapper,
captListener, true);
goog.events.listenWithWrapper(a, goog.events.actionEventWrapper,
bubbleListener);
assertListenersExist(a, 1, false);
assertListenersExist(a, 1, true);
goog.testing.events.fireClickSequence(a);
assertEquals('2 event should have been dispatched', 2, events.length);
assertEquals('Should be an click event', 'click', events[0].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('4 events should have been dispatched', 4, events.length);
assertEquals('Should be a keypress event', 'keypress', events[2].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('4 events should have been dispatched', 4, events.length);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ESC);
assertEquals('4 events should have been dispatched', 4, events.length);
goog.events.unlistenWithWrapper(a, goog.events.actionEventWrapper,
captListener, true);
goog.events.unlistenWithWrapper(a, goog.events.actionEventWrapper,
bubbleListener);
assertListenersExist(a, 0, false);
assertListenersExist(a, 0, true);
}
function testRemoveActionListener() {
var listener1 = function(e) { events.push(e); };
var listener2 = function(e) { events.push({type: 'err'}); };
goog.events.listenWithWrapper(a, goog.events.actionEventWrapper, listener1);
assertListenersExist(a, 1, false);
goog.events.listenWithWrapper(a, goog.events.actionEventWrapper, listener2);
assertListenersExist(a, 2, false);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('2 events should have been dispatched', 2, events.length);
assertEquals('Should be a keypress event', 'keypress', events[0].type);
assertEquals('Should be an err event', 'err', events[1].type);
goog.events.unlistenWithWrapper(a, goog.events.actionEventWrapper,
listener2);
assertListenersExist(a, 1, false);
events = [];
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('1 event should have been dispatched', 1, events.length);
assertEquals('Should be a keypress event', 'keypress', events[0].type);
goog.events.unlistenWithWrapper(a, goog.events.actionEventWrapper,
listener1);
assertListenersExist(a, 0, false);
}
function testEventHandlerActionListener() {
var listener = function(e) { events.push(e); };
eh.listenWithWrapper(a, goog.events.actionEventWrapper, listener);
assertListenersExist(a, 1, false);
goog.testing.events.fireClickSequence(a);
assertEquals('1 event should have been dispatched', 1, events.length);
assertEquals('Should be an click event', 'click', events[0].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('2 events should have been dispatched', 2, events.length);
assertEquals('Should be a keypress event', 'keypress', events[1].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('2 events should have been dispatched', 2, events.length);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ESC);
assertEquals('2 events should have been dispatched', 2, events.length);
eh.unlistenWithWrapper(a, goog.events.actionEventWrapper,
listener);
assertListenersExist(a, 0, false);
}
function testEventHandlerActionListenerWithScope() {
var foo = new Foo();
var eh2 = new goog.events.EventHandler(foo);
eh2.listenWithWrapper(a, goog.events.actionEventWrapper, foo.test);
assertListenersExist(a, 1, false);
goog.testing.events.fireClickSequence(a);
assertEquals('1 event should have been dispatched', 1, events.length);
assertEquals('Should be an click event', 'click', events[0].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ENTER);
assertEquals('2 events should have been dispatched', 2, events.length);
assertEquals('Should be a keypress event', 'keypress', events[1].type);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.SPACE);
assertEquals('2 events should have been dispatched', 2, events.length);
goog.testing.events.fireKeySequence(a, goog.events.KeyCodes.ESC);
assertEquals('2 events should have been dispatched', 2, events.length);
eh2.dispose();
assertListenersExist(a, 0, false);
delete foo;
}
@@ -0,0 +1,184 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This file contains a class to provide a unified mechanism for
* CLICK and enter KEYDOWN events. This provides better accessibility by
* providing the given functionality to a keyboard user which is otherwise
* would be available only via a mouse click.
*
* If there is an existing CLICK listener or planning to be added as below -
*
* <code>this.eventHandler_.listen(el, CLICK, this.onClick_);<code>
*
* it can be replaced with an ACTION listener as follows:
*
* <code>this.eventHandler_.listen(
* new goog.events.ActionHandler(el),
* ACTION,
* this.onAction_);<code>
*
*/
goog.provide('goog.events.ActionEvent');
goog.provide('goog.events.ActionHandler');
goog.provide('goog.events.ActionHandler.EventType');
goog.provide('goog.events.BeforeActionEvent');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.userAgent');
/**
* A wrapper around an element that you want to listen to ACTION events on.
* @param {Element|Document} element The element or document to listen on.
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.events.ActionHandler = function(element) {
goog.events.EventTarget.call(this);
/**
* This is the element that we will listen to events on.
* @type {Element|Document}
* @private
*/
this.element_ = element;
goog.events.listen(element, goog.events.ActionHandler.KEY_EVENT_TYPE_,
this.handleKeyDown_, false, this);
goog.events.listen(element, goog.events.EventType.CLICK,
this.handleClick_, false, this);
};
goog.inherits(goog.events.ActionHandler, goog.events.EventTarget);
/**
* Enum type for the events fired by the action handler
* @enum {string}
*/
goog.events.ActionHandler.EventType = {
ACTION: 'action',
BEFOREACTION: 'beforeaction'
};
/**
* Key event type to listen for.
* @type {string}
* @private
*/
goog.events.ActionHandler.KEY_EVENT_TYPE_ = goog.userAgent.GECKO ?
goog.events.EventType.KEYPRESS :
goog.events.EventType.KEYDOWN;
/**
* Handles key press events.
* @param {!goog.events.BrowserEvent} e The key press event.
* @private
*/
goog.events.ActionHandler.prototype.handleKeyDown_ = function(e) {
if (e.keyCode == goog.events.KeyCodes.ENTER ||
goog.userAgent.WEBKIT && e.keyCode == goog.events.KeyCodes.MAC_ENTER) {
this.dispatchEvents_(e);
}
};
/**
* Handles mouse events.
* @param {!goog.events.BrowserEvent} e The click event.
* @private
*/
goog.events.ActionHandler.prototype.handleClick_ = function(e) {
this.dispatchEvents_(e);
};
/**
* Dispatches BeforeAction and Action events to the element
* @param {!goog.events.BrowserEvent} e The event causing dispatches.
* @private
*/
goog.events.ActionHandler.prototype.dispatchEvents_ = function(e) {
var beforeActionEvent = new goog.events.BeforeActionEvent(e);
// Allow application specific logic here before the ACTION event.
// For example, Gmail uses this event to restore keyboard focus
if (!this.dispatchEvent(beforeActionEvent)) {
// If the listener swallowed the BEFOREACTION event, don't dispatch the
// ACTION event.
return;
}
// Wrap up original event and send it off
var actionEvent = new goog.events.ActionEvent(e);
try {
this.dispatchEvent(actionEvent);
} finally {
// Stop propagating the event
e.stopPropagation();
}
};
/** @override */
goog.events.ActionHandler.prototype.disposeInternal = function() {
goog.events.ActionHandler.superClass_.disposeInternal.call(this);
goog.events.unlisten(this.element_, goog.events.ActionHandler.KEY_EVENT_TYPE_,
this.handleKeyDown_, false, this);
goog.events.unlisten(this.element_, goog.events.EventType.CLICK,
this.handleClick_, false, this);
delete this.element_;
};
/**
* This class is used for the goog.events.ActionHandler.EventType.ACTION event.
* @param {!goog.events.BrowserEvent} browserEvent Browser event object.
* @constructor
* @extends {goog.events.BrowserEvent}
* @final
*/
goog.events.ActionEvent = function(browserEvent) {
goog.events.BrowserEvent.call(this, browserEvent.getBrowserEvent());
this.type = goog.events.ActionHandler.EventType.ACTION;
};
goog.inherits(goog.events.ActionEvent, goog.events.BrowserEvent);
/**
* This class is used for the goog.events.ActionHandler.EventType.BEFOREACTION
* event. BEFOREACTION gives a chance to the application so the keyboard focus
* can be restored back, if required.
* @param {!goog.events.BrowserEvent} browserEvent Browser event object.
* @constructor
* @extends {goog.events.BrowserEvent}
* @final
*/
goog.events.BeforeActionEvent = function(browserEvent) {
goog.events.BrowserEvent.call(this, browserEvent.getBrowserEvent());
this.type = goog.events.ActionHandler.EventType.BEFOREACTION;
};
goog.inherits(goog.events.BeforeActionEvent, goog.events.BrowserEvent);
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.ActionHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.ActionHandlerTest');
</script>
</head>
<body>
<div id="actionDiv">
action
</div>
</body>
</html>
@@ -0,0 +1,80 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.ActionHandlerTest');
goog.setTestOnly('goog.events.ActionHandlerTest');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.ActionHandler');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
var actionHandler;
function setUp() {
actionHandler = new goog.events.ActionHandler(
goog.dom.getElement('actionDiv'));
}
function tearDown() {
actionHandler.dispose();
}
// Tests to see that both the BEFOREACTION and ACTION events are fired
function testActionHandlerWithBeforeActionHandler() {
var actionEventFired = false;
var beforeActionFired = false;
goog.events.listen(actionHandler,
goog.events.ActionHandler.EventType.ACTION,
function(e) {
actionEventFired = true;
});
goog.events.listen(actionHandler,
goog.events.ActionHandler.EventType.BEFOREACTION,
function(e) {
beforeActionFired = true;
});
goog.testing.events.fireClickSequence(goog.dom.getElement('actionDiv'));
assertTrue('BEFOREACTION event was not fired', beforeActionFired);
assertTrue('ACTION event was not fired', actionEventFired);
}
// Tests to see that the ACTION event is fired, even if there is no
// BEFOREACTION handler.
function testActionHandlerWithoutBeforeActionHandler() {
var actionEventFired = false;
goog.events.listen(actionHandler,
goog.events.ActionHandler.EventType.ACTION,
function(e) {actionEventFired = true;});
goog.testing.events.fireClickSequence(goog.dom.getElement('actionDiv'));
assertTrue('ACTION event was not fired', actionEventFired);
}
// If the BEFOREACTION listener swallows the event, it should cancel the
// ACTION event.
function testBeforeActionCancel() {
var actionEventFired = false;
var beforeActionFired = false;
goog.events.listen(actionHandler,
goog.events.ActionHandler.EventType.ACTION,
function(e) {actionEvent = e;});
goog.events.listen(actionHandler,
goog.events.ActionHandler.EventType.BEFOREACTION,
function(e) {
beforeActionFired = true;
e.preventDefault();
});
goog.testing.events.fireClickSequence(goog.dom.getElement('actionDiv'));
assertTrue(beforeActionFired);
assertFalse(actionEventFired);
}
@@ -0,0 +1,446 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview A patched, standardized event object for browser events.
*
* <pre>
* The patched event object contains the following members:
* - type {string} Event type, e.g. 'click'
* - target {Object} The element that actually triggered the event
* - currentTarget {Object} The element the listener is attached to
* - relatedTarget {Object} For mouseover and mouseout, the previous object
* - offsetX {number} X-coordinate relative to target
* - offsetY {number} Y-coordinate relative to target
* - clientX {number} X-coordinate relative to viewport
* - clientY {number} Y-coordinate relative to viewport
* - screenX {number} X-coordinate relative to the edge of the screen
* - screenY {number} Y-coordinate relative to the edge of the screen
* - button {number} Mouse button. Use isButton() to test.
* - keyCode {number} Key-code
* - ctrlKey {boolean} Was ctrl key depressed
* - altKey {boolean} Was alt key depressed
* - shiftKey {boolean} Was shift key depressed
* - metaKey {boolean} Was meta key depressed
* - defaultPrevented {boolean} Whether the default action has been prevented
* - state {Object} History state object
*
* NOTE: The keyCode member contains the raw browser keyCode. For normalized
* key and character code use {@link goog.events.KeyHandler}.
* </pre>
*
* @author arv@google.com (Erik Arvidsson)
*/
goog.provide('goog.events.BrowserEvent');
goog.provide('goog.events.BrowserEvent.MouseButton');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.reflect');
goog.require('goog.userAgent');
/**
* Accepts a browser event object and creates a patched, cross browser event
* object.
* The content of this object will not be initialized if no event object is
* provided. If this is the case, init() needs to be invoked separately.
* @param {Event=} opt_e Browser event object.
* @param {EventTarget=} opt_currentTarget Current target for event.
* @constructor
* @extends {goog.events.Event}
*/
goog.events.BrowserEvent = function(opt_e, opt_currentTarget) {
goog.events.BrowserEvent.base(this, 'constructor', opt_e ? opt_e.type : '');
/**
* Target that fired the event.
* @override
* @type {Node}
*/
this.target = null;
/**
* Node that had the listener attached.
* @override
* @type {Node|undefined}
*/
this.currentTarget = null;
/**
* For mouseover and mouseout events, the related object for the event.
* @type {Node}
*/
this.relatedTarget = null;
/**
* X-coordinate relative to target.
* @type {number}
*/
this.offsetX = 0;
/**
* Y-coordinate relative to target.
* @type {number}
*/
this.offsetY = 0;
/**
* X-coordinate relative to the window.
* @type {number}
*/
this.clientX = 0;
/**
* Y-coordinate relative to the window.
* @type {number}
*/
this.clientY = 0;
/**
* X-coordinate relative to the monitor.
* @type {number}
*/
this.screenX = 0;
/**
* Y-coordinate relative to the monitor.
* @type {number}
*/
this.screenY = 0;
/**
* Which mouse button was pressed.
* @type {number}
*/
this.button = 0;
/**
* Keycode of key press.
* @type {number}
*/
this.keyCode = 0;
/**
* Keycode of key press.
* @type {number}
*/
this.charCode = 0;
/**
* Whether control was pressed at time of event.
* @type {boolean}
*/
this.ctrlKey = false;
/**
* Whether alt was pressed at time of event.
* @type {boolean}
*/
this.altKey = false;
/**
* Whether shift was pressed at time of event.
* @type {boolean}
*/
this.shiftKey = false;
/**
* Whether the meta key was pressed at time of event.
* @type {boolean}
*/
this.metaKey = false;
/**
* History state object, only set for PopState events where it's a copy of the
* state object provided to pushState or replaceState.
* @type {Object}
*/
this.state = null;
/**
* Whether the default platform modifier key was pressed at time of event.
* (This is control for all platforms except Mac, where it's Meta.)
* @type {boolean}
*/
this.platformModifierKey = false;
/**
* The browser event object.
* @private {Event}
*/
this.event_ = null;
if (opt_e) {
this.init(opt_e, opt_currentTarget);
}
};
goog.inherits(goog.events.BrowserEvent, goog.events.Event);
/**
* Normalized button constants for the mouse.
* @enum {number}
*/
goog.events.BrowserEvent.MouseButton = {
LEFT: 0,
MIDDLE: 1,
RIGHT: 2
};
/**
* Static data for mapping mouse buttons.
* @type {!Array<number>}
*/
goog.events.BrowserEvent.IEButtonMap = [
1, // LEFT
4, // MIDDLE
2 // RIGHT
];
/**
* Accepts a browser event object and creates a patched, cross browser event
* object.
* @param {Event} e Browser event object.
* @param {EventTarget=} opt_currentTarget Current target for event.
*/
goog.events.BrowserEvent.prototype.init = function(e, opt_currentTarget) {
this.event_ = e;
var type = this.type = e.type;
// TODO(nicksantos): Change this.target to type EventTarget.
this.target = /** @type {Node} */ (e.target) || e.srcElement;
// TODO(nicksantos): Change this.currentTarget to type EventTarget.
this.currentTarget = /** @type {Node} */ (opt_currentTarget);
var relatedTarget = /** @type {Node} */ (e.relatedTarget);
if (relatedTarget) {
// There's a bug in FireFox where sometimes, relatedTarget will be a
// chrome element, and accessing any property of it will get a permission
// denied exception. See:
// https://bugzilla.mozilla.org/show_bug.cgi?id=497780
if (goog.userAgent.GECKO) {
if (!goog.reflect.canAccessProperty(relatedTarget, 'nodeName')) {
relatedTarget = null;
}
}
// TODO(arv): Use goog.events.EventType when it has been refactored into its
// own file.
} else if (type == goog.events.EventType.MOUSEOVER) {
relatedTarget = e.fromElement;
} else if (type == goog.events.EventType.MOUSEOUT) {
relatedTarget = e.toElement;
}
this.relatedTarget = relatedTarget;
// Lazily calculate the offsetX/Y properties because they trigger style
// recalc and layout. The original event's getters are themselves lazy.
if (Object.defineProperties) {
Object.defineProperties(this,
/** @lends {goog.events.BrowserEvent.prototype} */ ({
offsetX: {
configurable: true,
enumerable: true,
get: this.getOffsetX_,
set: this.setOffsetX_
},
offsetY: {
configurable: true,
enumerable: true,
get: this.getOffsetY_,
set: this.setOffsetY_
}
}));
} else {
this.offsetX = this.getOffsetX_();
this.offsetY = this.getOffsetY_();
}
this.clientX = e.clientX !== undefined ? e.clientX : e.pageX;
this.clientY = e.clientY !== undefined ? e.clientY : e.pageY;
this.screenX = e.screenX || 0;
this.screenY = e.screenY || 0;
this.button = e.button;
this.keyCode = e.keyCode || 0;
this.charCode = e.charCode || (type == 'keypress' ? e.keyCode : 0);
this.ctrlKey = e.ctrlKey;
this.altKey = e.altKey;
this.shiftKey = e.shiftKey;
this.metaKey = e.metaKey;
this.platformModifierKey = goog.userAgent.MAC ? e.metaKey : e.ctrlKey;
this.state = e.state;
if (e.defaultPrevented) {
this.preventDefault();
}
};
/**
* Tests to see which button was pressed during the event. This is really only
* useful in IE and Gecko browsers. And in IE, it's only useful for
* mousedown/mouseup events, because click only fires for the left mouse button.
*
* Safari 2 only reports the left button being clicked, and uses the value '1'
* instead of 0. Opera only reports a mousedown event for the middle button, and
* no mouse events for the right button. Opera has default behavior for left and
* middle click that can only be overridden via a configuration setting.
*
* There's a nice table of this mess at http://www.unixpapa.com/js/mouse.html.
*
* @param {goog.events.BrowserEvent.MouseButton} button The button
* to test for.
* @return {boolean} True if button was pressed.
*/
goog.events.BrowserEvent.prototype.isButton = function(button) {
if (!goog.events.BrowserFeature.HAS_W3C_BUTTON) {
if (this.type == 'click') {
return button == goog.events.BrowserEvent.MouseButton.LEFT;
} else {
return !!(this.event_.button &
goog.events.BrowserEvent.IEButtonMap[button]);
}
} else {
return this.event_.button == button;
}
};
/**
* Whether this has an "action"-producing mouse button.
*
* By definition, this includes left-click on windows/linux, and left-click
* without the ctrl key on Macs.
*
* @return {boolean} The result.
*/
goog.events.BrowserEvent.prototype.isMouseActionButton = function() {
// Webkit does not ctrl+click to be a right-click, so we
// normalize it to behave like Gecko and Opera.
return this.isButton(goog.events.BrowserEvent.MouseButton.LEFT) &&
!(goog.userAgent.WEBKIT && goog.userAgent.MAC && this.ctrlKey);
};
/**
* @override
*/
goog.events.BrowserEvent.prototype.stopPropagation = function() {
goog.events.BrowserEvent.superClass_.stopPropagation.call(this);
if (this.event_.stopPropagation) {
this.event_.stopPropagation();
} else {
this.event_.cancelBubble = true;
}
};
/**
* @override
*/
goog.events.BrowserEvent.prototype.preventDefault = function() {
goog.events.BrowserEvent.superClass_.preventDefault.call(this);
var be = this.event_;
if (!be.preventDefault) {
be.returnValue = false;
if (goog.events.BrowserFeature.SET_KEY_CODE_TO_PREVENT_DEFAULT) {
/** @preserveTry */
try {
// Most keys can be prevented using returnValue. Some special keys
// require setting the keyCode to -1 as well:
//
// In IE7:
// F3, F5, F10, F11, Ctrl+P, Crtl+O, Ctrl+F (these are taken from IE6)
//
// In IE8:
// Ctrl+P, Crtl+O, Ctrl+F (F1-F12 cannot be stopped through the event)
//
// We therefore do this for all function keys as well as when Ctrl key
// is pressed.
var VK_F1 = 112;
var VK_F12 = 123;
if (be.ctrlKey || be.keyCode >= VK_F1 && be.keyCode <= VK_F12) {
be.keyCode = -1;
}
} catch (ex) {
// IE throws an 'access denied' exception when trying to change
// keyCode in some situations (e.g. srcElement is input[type=file],
// or srcElement is an anchor tag rewritten by parent's innerHTML).
// Do nothing in this case.
}
}
} else {
be.preventDefault();
}
};
/**
* @return {Event} The underlying browser event object.
*/
goog.events.BrowserEvent.prototype.getBrowserEvent = function() {
return this.event_;
};
/** @private @return {number} */
goog.events.BrowserEvent.prototype.getOffsetX_ = function() {
// Webkit emits a lame warning whenever layerX/layerY is accessed.
// http://code.google.com/p/chromium/issues/detail?id=101733
return (goog.userAgent.WEBKIT || this.event_.offsetX !== undefined) ?
this.event_.offsetX : this.event_.layerX;
};
/** @private @param {number} offset */
goog.events.BrowserEvent.prototype.setOffsetX_ = function(offset) {
Object.defineProperties(this,
/** @lends {goog.events.BrowserEvent.prototype} */ ({
offsetX: {
writable: true,
enumerable: true,
configurable: true,
value: offset
}
}));
};
/** @private @return {number} */
goog.events.BrowserEvent.prototype.getOffsetY_ = function() {
return (goog.userAgent.WEBKIT || this.event_.offsetY !== undefined) ?
this.event_.offsetY : this.event_.layerY;
};
/** @private @param {number} offset */
goog.events.BrowserEvent.prototype.setOffsetY_ = function(offset) {
Object.defineProperties(this,
/** @lends {goog.events.BrowserEvent.prototype} */ ({
offsetY: {
writable: true,
enumerable: true,
configurable: true,
value: offset
}
}));
};
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<!--
Author: nicksantos@google.com (Nick Santos)
-->
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
JsUnit tests for goog.events.BrowserEvent
</title>
<script type="text/javascript" src="../base.js">
</script>
<script type="text/javascript">
goog.require('goog.events.BrowserEventTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,165 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.BrowserEventTest');
goog.setTestOnly('goog.events.BrowserEventTest');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.BrowserFeature');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var stubs = new goog.testing.PropertyReplacer();
var Button = goog.events.BrowserEvent.MouseButton;
function setUp() {
stubs.reset();
}
/**
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=497780
*/
function testInvalidNodeBug() {
if (!goog.userAgent.GECKO) return;
var event = {};
event.relatedTarget = {};
event.relatedTarget.__defineGetter__(
'nodeName',
function() {
throw Error('https://bugzilla.mozilla.org/show_bug.cgi?id=497780');
});
assertThrows(function() { return event.relatedTarget.nodeName; });
var bEvent = new goog.events.BrowserEvent(event);
assertEquals(event, bEvent.event_);
assertNull(bEvent.relatedTarget);
}
function testPreventDefault() {
var event = {};
event.defaultPrevented = false;
var bEvent = new goog.events.BrowserEvent(event);
assertFalse(bEvent.defaultPrevented);
bEvent.preventDefault();
assertTrue(bEvent.defaultPrevented);
}
function testDefaultPrevented() {
var event = {};
event.defaultPrevented = true;
var bEvent = new goog.events.BrowserEvent(event);
assertTrue(bEvent.defaultPrevented);
}
function testIsButtonIe() {
stubs.set(goog.events.BrowserFeature, 'HAS_W3C_BUTTON', false);
assertIsButton(
createBrowserEvent('mousedown', 1),
Button.LEFT,
true);
assertIsButton(
createBrowserEvent('click', 0),
Button.LEFT,
true);
assertIsButton(
createBrowserEvent('mousedown', 2),
Button.RIGHT,
false);
assertIsButton(
createBrowserEvent('mousedown', 4),
Button.MIDDLE,
false);
}
function testIsButtonWebkitMac() {
stubs.set(goog.events.BrowserFeature, 'HAS_W3C_BUTTON', true);
stubs.set(goog.userAgent, 'WEBKIT', true);
stubs.set(goog.userAgent, 'MAC', true);
assertIsButton(
createBrowserEvent('mousedown', 0),
Button.LEFT,
true);
assertIsButton(
createBrowserEvent('mousedown', 0, true),
Button.LEFT,
false);
assertIsButton(
createBrowserEvent('mousedown', 2),
Button.RIGHT,
false);
assertIsButton(
createBrowserEvent('mousedown', 2, true),
Button.RIGHT,
false);
assertIsButton(
createBrowserEvent('mousedown', 1),
Button.MIDDLE,
false);
assertIsButton(
createBrowserEvent('mousedown', 1, true),
Button.MIDDLE,
false);
}
function testIsButtonGecko() {
stubs.set(goog.events.BrowserFeature, 'HAS_W3C_BUTTON', true);
stubs.set(goog.userAgent, 'GECKO', true);
stubs.set(goog.userAgent, 'MAC', true);
assertIsButton(
createBrowserEvent('mousedown', 0),
Button.LEFT,
true);
assertIsButton(
createBrowserEvent('mousedown', 2, true),
Button.RIGHT,
false);
}
function testOffsets() {
var e = createBrowserEvent('mousedown', 0);
assertEquals(10, e.offsetX);
assertEquals(10, e.offsetY);
e.offsetX = 20;
e.offsetY = 20;
assertEquals(20, e.offsetX);
assertEquals(20, e.offsetY);
e.offsetX = 30;
e.offsetY = 30;
assertEquals(30, e.offsetX);
assertEquals(30, e.offsetY);
}
function createBrowserEvent(type, button, opt_ctrlKey) {
return new goog.events.BrowserEvent({
type: type,
button: button,
ctrlKey: !!opt_ctrlKey,
offsetX: 10,
offsetY: 10
});
}
function assertIsButton(event, button, isActionButton) {
for (var key in Button) {
assertEquals(
'Testing isButton(' + key + ') against ' +
button + ' and type ' + event.type,
Button[key] == button, event.isButton(Button[key]));
}
assertEquals(isActionButton, event.isMouseActionButton());
}
@@ -0,0 +1,85 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Browser capability checks for the events package.
*
*/
goog.provide('goog.events.BrowserFeature');
goog.require('goog.userAgent');
/**
* Enum of browser capabilities.
* @enum {boolean}
*/
goog.events.BrowserFeature = {
/**
* Whether the button attribute of the event is W3C compliant. False in
* Internet Explorer prior to version 9; document-version dependent.
*/
HAS_W3C_BUTTON: !goog.userAgent.IE ||
goog.userAgent.isDocumentModeOrHigher(9),
/**
* Whether the browser supports full W3C event model.
*/
HAS_W3C_EVENT_SUPPORT: !goog.userAgent.IE ||
goog.userAgent.isDocumentModeOrHigher(9),
/**
* To prevent default in IE7-8 for certain keydown events we need set the
* keyCode to -1.
*/
SET_KEY_CODE_TO_PREVENT_DEFAULT: goog.userAgent.IE &&
!goog.userAgent.isVersionOrHigher('9'),
/**
* Whether the {@code navigator.onLine} property is supported.
*/
HAS_NAVIGATOR_ONLINE_PROPERTY: !goog.userAgent.WEBKIT ||
goog.userAgent.isVersionOrHigher('528'),
/**
* Whether HTML5 network online/offline events are supported.
*/
HAS_HTML5_NETWORK_EVENT_SUPPORT:
goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9b') ||
goog.userAgent.IE && goog.userAgent.isVersionOrHigher('8') ||
goog.userAgent.OPERA && goog.userAgent.isVersionOrHigher('9.5') ||
goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('528'),
/**
* Whether HTML5 network events fire on document.body, or otherwise the
* window.
*/
HTML5_NETWORK_EVENTS_FIRE_ON_BODY:
goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('8') ||
goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9'),
/**
* Whether touch is enabled in the browser.
*/
TOUCH_ENABLED:
('ontouchstart' in goog.global ||
!!(goog.global['document'] &&
document.documentElement &&
'ontouchstart' in document.documentElement) ||
// IE10 uses non-standard touch events, so it has a different check.
!!(goog.global['navigator'] &&
goog.global['navigator']['msMaxTouchPoints']))
};
@@ -0,0 +1,143 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview A base class for event objects.
*
*/
goog.provide('goog.events.Event');
goog.provide('goog.events.EventLike');
/**
* goog.events.Event no longer depends on goog.Disposable. Keep requiring
* goog.Disposable here to not break projects which assume this dependency.
* @suppress {extraRequire}
*/
goog.require('goog.Disposable');
goog.require('goog.events.EventId');
/**
* A typedef for event like objects that are dispatchable via the
* goog.events.dispatchEvent function. strings are treated as the type for a
* goog.events.Event. Objects are treated as an extension of a new
* goog.events.Event with the type property of the object being used as the type
* of the Event.
* @typedef {string|Object|goog.events.Event|goog.events.EventId}
*/
goog.events.EventLike;
/**
* A base class for event objects, so that they can support preventDefault and
* stopPropagation.
*
* @param {string|!goog.events.EventId} type Event Type.
* @param {Object=} opt_target Reference to the object that is the target of
* this event. It has to implement the {@code EventTarget} interface
* declared at {@link http://developer.mozilla.org/en/DOM/EventTarget}.
* @constructor
*/
goog.events.Event = function(type, opt_target) {
/**
* Event type.
* @type {string}
*/
this.type = type instanceof goog.events.EventId ? String(type) : type;
/**
* TODO(tbreisacher): The type should probably be
* EventTarget|goog.events.EventTarget.
*
* Target of the event.
* @type {Object|undefined}
*/
this.target = opt_target;
/**
* Object that had the listener attached.
* @type {Object|undefined}
*/
this.currentTarget = this.target;
/**
* Whether to cancel the event in internal capture/bubble processing for IE.
* @type {boolean}
* @public
* @suppress {underscore|visibility} Technically public, but referencing this
* outside this package is strongly discouraged.
*/
this.propagationStopped_ = false;
/**
* Whether the default action has been prevented.
* This is a property to match the W3C specification at
* {@link http://www.w3.org/TR/DOM-Level-3-Events/
* #events-event-type-defaultPrevented}.
* Must be treated as read-only outside the class.
* @type {boolean}
*/
this.defaultPrevented = false;
/**
* Return value for in internal capture/bubble processing for IE.
* @type {boolean}
* @public
* @suppress {underscore|visibility} Technically public, but referencing this
* outside this package is strongly discouraged.
*/
this.returnValue_ = true;
};
/**
* Stops event propagation.
*/
goog.events.Event.prototype.stopPropagation = function() {
this.propagationStopped_ = true;
};
/**
* Prevents the default action, for example a link redirecting to a url.
*/
goog.events.Event.prototype.preventDefault = function() {
this.defaultPrevented = true;
this.returnValue_ = false;
};
/**
* Stops the propagation of the event. It is equivalent to
* {@code e.stopPropagation()}, but can be used as the callback argument of
* {@link goog.events.listen} without declaring another function.
* @param {!goog.events.Event} e An event.
*/
goog.events.Event.stopPropagation = function(e) {
e.stopPropagation();
};
/**
* Prevents the default action. It is equivalent to
* {@code e.preventDefault()}, but can be used as the callback argument of
* {@link goog.events.listen} without declaring another function.
* @param {!goog.events.Event} e An event.
*/
goog.events.Event.preventDefault = function(e) {
e.preventDefault();
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!-- Author: attila@google.com (Attila Bodis) -->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.Event
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.EventTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,68 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.EventTest');
goog.setTestOnly('goog.events.EventTest');
goog.require('goog.events.Event');
goog.require('goog.events.EventId');
goog.require('goog.events.EventTarget');
goog.require('goog.testing.jsunit');
var e, target;
function setUp() {
target = new goog.events.EventTarget();
e = new goog.events.Event('eventType', target);
}
function tearDown() {
target.dispose();
}
function testConstructor() {
assertNotNull('Event must not be null', e);
assertEquals('Event type must be as expected', 'eventType', e.type);
assertEquals('Event target must be as expected', target, e.target);
assertEquals('Current target must be as expected', target,
e.currentTarget);
}
function testStopPropagation() {
// This test breaks encapsulation because there is no public getter for
// propagationStopped_.
assertFalse('Propagation must not have been stopped',
e.propagationStopped_);
e.stopPropagation();
assertTrue('Propagation must have been stopped', e.propagationStopped_);
}
function testPreventDefault() {
// This test breaks encapsulation because there is no public getter for
// returnValue_.
assertTrue('Return value must be true', e.returnValue_);
e.preventDefault();
assertFalse('Return value must be false', e.returnValue_);
}
function testDefaultPrevented() {
assertFalse('Default action must not be prevented', e.defaultPrevented);
e.preventDefault();
assertTrue('Default action must be prevented', e.defaultPrevented);
}
function testEventId() {
e = new goog.events.Event(new goog.events.EventId('eventType'));
assertEquals('Event type must be as expected', 'eventType', e.type);
}
@@ -0,0 +1,459 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Class to create objects which want to handle multiple events
* and have their listeners easily cleaned up via a dispose method.
*
* Example:
* <pre>
* function Something() {
* Something.base(this);
*
* ... set up object ...
*
* // Add event listeners
* this.listen(this.starEl, goog.events.EventType.CLICK, this.handleStar);
* this.listen(this.headerEl, goog.events.EventType.CLICK, this.expand);
* this.listen(this.collapseEl, goog.events.EventType.CLICK, this.collapse);
* this.listen(this.infoEl, goog.events.EventType.MOUSEOVER, this.showHover);
* this.listen(this.infoEl, goog.events.EventType.MOUSEOUT, this.hideHover);
* }
* goog.inherits(Something, goog.events.EventHandler);
*
* Something.prototype.disposeInternal = function() {
* Something.base(this, 'disposeInternal');
* goog.dom.removeNode(this.container);
* };
*
*
* // Then elsewhere:
*
* var activeSomething = null;
* function openSomething() {
* activeSomething = new Something();
* }
*
* function closeSomething() {
* if (activeSomething) {
* activeSomething.dispose(); // Remove event listeners
* activeSomething = null;
* }
* }
* </pre>
*
*/
goog.provide('goog.events.EventHandler');
goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.object');
goog.forwardDeclare('goog.events.EventWrapper');
/**
* Super class for objects that want to easily manage a number of event
* listeners. It allows a short cut to listen and also provides a quick way
* to remove all events listeners belonging to this object.
* @param {SCOPE=} opt_scope Object in whose scope to call the listeners.
* @constructor
* @extends {goog.Disposable}
* @template SCOPE
*/
goog.events.EventHandler = function(opt_scope) {
goog.Disposable.call(this);
// TODO(mknichel): Rename this to this.scope_ and fix the classes in google3
// that access this private variable. :(
this.handler_ = opt_scope;
/**
* Keys for events that are being listened to.
* @type {!Object<!goog.events.Key>}
* @private
*/
this.keys_ = {};
};
goog.inherits(goog.events.EventHandler, goog.Disposable);
/**
* Utility array used to unify the cases of listening for an array of types
* and listening for a single event, without using recursion or allocating
* an array each time.
* @type {!Array<string>}
* @const
* @private
*/
goog.events.EventHandler.typeArray_ = [];
/**
* Listen to an event on a Listenable. If the function is omitted then the
* EventHandler's handleEvent method will be used.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type to listen for or array of event types.
* @param {function(this:SCOPE, EVENTOBJ):?|{handleEvent:function(?):?}|null=}
* opt_fn Optional callback function to be used as the listener or an object
* with handleEvent function.
* @param {boolean=} opt_capture Optional whether to use capture phase.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template EVENTOBJ
*/
goog.events.EventHandler.prototype.listen = function(
src, type, opt_fn, opt_capture) {
return this.listen_(src, type, opt_fn, opt_capture);
};
/**
* Listen to an event on a Listenable. If the function is omitted then the
* EventHandler's handleEvent method will be used.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type to listen for or array of event types.
* @param {function(this:T, EVENTOBJ):?|{handleEvent:function(this:T, ?):?}|
* null|undefined} fn Optional callback function to be used as the
* listener or an object with handleEvent function.
* @param {boolean|undefined} capture Optional whether to use capture phase.
* @param {T} scope Object in whose scope to call the listener.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template T,EVENTOBJ
*/
goog.events.EventHandler.prototype.listenWithScope = function(
src, type, fn, capture, scope) {
// TODO(mknichel): Deprecate this function.
return this.listen_(src, type, fn, capture, scope);
};
/**
* Listen to an event on a Listenable. If the function is omitted then the
* EventHandler's handleEvent method will be used.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type to listen for or array of event types.
* @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
* Optional callback function to be used as the listener or an object with
* handleEvent function.
* @param {boolean=} opt_capture Optional whether to use capture phase.
* @param {Object=} opt_scope Object in whose scope to call the listener.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template EVENTOBJ
* @private
*/
goog.events.EventHandler.prototype.listen_ = function(src, type, opt_fn,
opt_capture,
opt_scope) {
if (!goog.isArray(type)) {
if (type) {
goog.events.EventHandler.typeArray_[0] = type.toString();
}
type = goog.events.EventHandler.typeArray_;
}
for (var i = 0; i < type.length; i++) {
var listenerObj = goog.events.listen(
src, type[i], opt_fn || this.handleEvent,
opt_capture || false,
opt_scope || this.handler_ || this);
if (!listenerObj) {
// When goog.events.listen run on OFF_AND_FAIL or OFF_AND_SILENT
// (goog.events.CaptureSimulationMode) in IE8-, it will return null
// value.
return this;
}
var key = listenerObj.key;
this.keys_[key] = listenerObj;
}
return this;
};
/**
* Listen to an event on a Listenable. If the function is omitted, then the
* EventHandler's handleEvent method will be used. After the event has fired the
* event listener is removed from the target. If an array of event types is
* provided, each event type will be listened to once.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type to listen for or array of event types.
* @param {function(this:SCOPE, EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
* Optional callback function to be used as the listener or an object with
* handleEvent function.
* @param {boolean=} opt_capture Optional whether to use capture phase.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template EVENTOBJ
*/
goog.events.EventHandler.prototype.listenOnce = function(
src, type, opt_fn, opt_capture) {
return this.listenOnce_(src, type, opt_fn, opt_capture);
};
/**
* Listen to an event on a Listenable. If the function is omitted, then the
* EventHandler's handleEvent method will be used. After the event has fired the
* event listener is removed from the target. If an array of event types is
* provided, each event type will be listened to once.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type to listen for or array of event types.
* @param {function(this:T, EVENTOBJ):?|{handleEvent:function(this:T, ?):?}|
* null|undefined} fn Optional callback function to be used as the
* listener or an object with handleEvent function.
* @param {boolean|undefined} capture Optional whether to use capture phase.
* @param {T} scope Object in whose scope to call the listener.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template T,EVENTOBJ
*/
goog.events.EventHandler.prototype.listenOnceWithScope = function(
src, type, fn, capture, scope) {
// TODO(mknichel): Deprecate this function.
return this.listenOnce_(src, type, fn, capture, scope);
};
/**
* Listen to an event on a Listenable. If the function is omitted, then the
* EventHandler's handleEvent method will be used. After the event has fired
* the event listener is removed from the target. If an array of event types is
* provided, each event type will be listened to once.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type to listen for or array of event types.
* @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
* Optional callback function to be used as the listener or an object with
* handleEvent function.
* @param {boolean=} opt_capture Optional whether to use capture phase.
* @param {Object=} opt_scope Object in whose scope to call the listener.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template EVENTOBJ
* @private
*/
goog.events.EventHandler.prototype.listenOnce_ = function(
src, type, opt_fn, opt_capture, opt_scope) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
this.listenOnce_(src, type[i], opt_fn, opt_capture, opt_scope);
}
} else {
var listenerObj = goog.events.listenOnce(
src, type, opt_fn || this.handleEvent, opt_capture,
opt_scope || this.handler_ || this);
if (!listenerObj) {
// When goog.events.listen run on OFF_AND_FAIL or OFF_AND_SILENT
// (goog.events.CaptureSimulationMode) in IE8-, it will return null
// value.
return this;
}
var key = listenerObj.key;
this.keys_[key] = listenerObj;
}
return this;
};
/**
* Adds an event listener with a specific event wrapper on a DOM Node or an
* object that has implemented {@link goog.events.EventTarget}. A listener can
* only be added once to an object.
*
* @param {EventTarget|goog.events.EventTarget} src The node to listen to
* events on.
* @param {goog.events.EventWrapper} wrapper Event wrapper to use.
* @param {function(this:SCOPE, ?):?|{handleEvent:function(?):?}|null} listener
* Callback method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
*/
goog.events.EventHandler.prototype.listenWithWrapper = function(
src, wrapper, listener, opt_capt) {
// TODO(mknichel): Remove the opt_scope from this function and then
// templatize it.
return this.listenWithWrapper_(src, wrapper, listener, opt_capt);
};
/**
* Adds an event listener with a specific event wrapper on a DOM Node or an
* object that has implemented {@link goog.events.EventTarget}. A listener can
* only be added once to an object.
*
* @param {EventTarget|goog.events.EventTarget} src The node to listen to
* events on.
* @param {goog.events.EventWrapper} wrapper Event wrapper to use.
* @param {function(this:T, ?):?|{handleEvent:function(this:T, ?):?}|null}
* listener Optional callback function to be used as the
* listener or an object with handleEvent function.
* @param {boolean|undefined} capture Optional whether to use capture phase.
* @param {T} scope Object in whose scope to call the listener.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @template T
*/
goog.events.EventHandler.prototype.listenWithWrapperAndScope = function(
src, wrapper, listener, capture, scope) {
// TODO(mknichel): Deprecate this function.
return this.listenWithWrapper_(src, wrapper, listener, capture, scope);
};
/**
* Adds an event listener with a specific event wrapper on a DOM Node or an
* object that has implemented {@link goog.events.EventTarget}. A listener can
* only be added once to an object.
*
* @param {EventTarget|goog.events.EventTarget} src The node to listen to
* events on.
* @param {goog.events.EventWrapper} wrapper Event wrapper to use.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
* method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {Object=} opt_scope Element in whose scope to call the listener.
* @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
* chaining of calls.
* @private
*/
goog.events.EventHandler.prototype.listenWithWrapper_ = function(
src, wrapper, listener, opt_capt, opt_scope) {
wrapper.listen(src, listener, opt_capt, opt_scope || this.handler_ || this,
this);
return this;
};
/**
* @return {number} Number of listeners registered by this handler.
*/
goog.events.EventHandler.prototype.getListenerCount = function() {
var count = 0;
for (var key in this.keys_) {
if (Object.prototype.hasOwnProperty.call(this.keys_, key)) {
count++;
}
}
return count;
};
/**
* Unlistens on an event.
* @param {goog.events.ListenableType} src Event source.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type or array of event types to unlisten to.
* @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
* Optional callback function to be used as the listener or an object with
* handleEvent function.
* @param {boolean=} opt_capture Optional whether to use capture phase.
* @param {Object=} opt_scope Object in whose scope to call the listener.
* @return {!goog.events.EventHandler} This object, allowing for chaining of
* calls.
* @template EVENTOBJ
*/
goog.events.EventHandler.prototype.unlisten = function(src, type, opt_fn,
opt_capture,
opt_scope) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
this.unlisten(src, type[i], opt_fn, opt_capture, opt_scope);
}
} else {
var listener = goog.events.getListener(src, type,
opt_fn || this.handleEvent,
opt_capture, opt_scope || this.handler_ || this);
if (listener) {
goog.events.unlistenByKey(listener);
delete this.keys_[listener.key];
}
}
return this;
};
/**
* Removes an event listener which was added with listenWithWrapper().
*
* @param {EventTarget|goog.events.EventTarget} src The target to stop
* listening to events on.
* @param {goog.events.EventWrapper} wrapper Event wrapper to use.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener The
* listener function to remove.
* @param {boolean=} opt_capt In DOM-compliant browsers, this determines
* whether the listener is fired during the capture or bubble phase of the
* event.
* @param {Object=} opt_scope Element in whose scope to call the listener.
* @return {!goog.events.EventHandler} This object, allowing for chaining of
* calls.
*/
goog.events.EventHandler.prototype.unlistenWithWrapper = function(src, wrapper,
listener, opt_capt, opt_scope) {
wrapper.unlisten(src, listener, opt_capt,
opt_scope || this.handler_ || this, this);
return this;
};
/**
* Unlistens to all events.
*/
goog.events.EventHandler.prototype.removeAll = function() {
goog.object.forEach(this.keys_, goog.events.unlistenByKey);
this.keys_ = {};
};
/**
* Disposes of this EventHandler and removes all listeners that it registered.
* @override
* @protected
*/
goog.events.EventHandler.prototype.disposeInternal = function() {
goog.events.EventHandler.superClass_.disposeInternal.call(this);
this.removeAll();
};
/**
* Default event handler
* @param {goog.events.Event} e Event object.
*/
goog.events.EventHandler.prototype.handleEvent = function(e) {
throw Error('EventHandler.handleEvent not implemented');
};
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.EventHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.EventHandlerTest');
</script>
</head>
<body>
<div id="a">
</div>
<div id="b">
</div>
<div id="c">
</div>
<div id="d">
</div>
</body>
</html>
@@ -0,0 +1,247 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.EventHandlerTest');
goog.setTestOnly('goog.events.EventHandlerTest');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var a, b, c, d, eh;
function setUpPage() {
a = document.getElementById('a');
b = document.getElementById('b');
c = document.getElementById('c');
d = document.getElementById('d');
}
function tearDown() {
goog.dispose(eh);
}
function testEventHandlerClearsListeners() {
function tmp() {}
goog.events.listen(a, 'click', tmp);
assertEquals(1, goog.events.getListeners(a, 'click', false).length);
eh = new goog.events.EventHandler();
eh.listen(a, 'click');
eh.listen(a, 'keypress');
eh.listen(b, 'mouseover');
eh.listen(c, 'mousedown');
eh.listen(d, 'click');
eh.listen(d, 'mousedown');
assertEquals(2, goog.events.getListeners(a, 'click', false).length);
assertEquals(1, goog.events.getListeners(a, 'keypress', false).length);
assertEquals(1, goog.events.getListeners(b, 'mouseover', false).length);
assertEquals(1, goog.events.getListeners(c, 'mousedown', false).length);
assertEquals(1, goog.events.getListeners(d, 'click', false).length);
assertEquals(1, goog.events.getListeners(d, 'mousedown', false).length);
eh.unlisten(d, 'mousedown');
assertEquals(2, goog.events.getListeners(a, 'click', false).length);
assertEquals(1, goog.events.getListeners(a, 'keypress', false).length);
assertEquals(1, goog.events.getListeners(b, 'mouseover', false).length);
assertEquals(1, goog.events.getListeners(c, 'mousedown', false).length);
assertEquals(1, goog.events.getListeners(d, 'click', false).length);
assertEquals(0, goog.events.getListeners(d, 'mousedown', false).length);
eh.dispose();
assertEquals(1, goog.events.getListeners(a, 'click', false).length);
assertEquals(0, goog.events.getListeners(a, 'keypress', false).length);
assertEquals(0, goog.events.getListeners(b, 'mouseover', false).length);
assertEquals(0, goog.events.getListeners(c, 'mousedown', false).length);
assertEquals(0, goog.events.getListeners(d, 'click', false).length);
assertEquals(0, goog.events.getListeners(d, 'mousedown', false).length);
goog.events.unlisten(a, 'click', tmp);
assertEquals(0, goog.events.getListeners(a, 'click', false).length);
}
function testListenArray() {
eh = new goog.events.EventHandler();
eh.listen(a, ['click', 'mousedown', 'mouseup']);
assertEquals(1, goog.events.getListeners(a, 'click', false).length);
assertEquals(1, goog.events.getListeners(a, 'mousedown', false).length);
assertEquals(1, goog.events.getListeners(a, 'mouseup', false).length);
eh.unlisten(a, ['click', 'mousedown', 'mouseup']);
assertEquals(0, goog.events.getListeners(a, 'click', false).length);
assertEquals(0, goog.events.getListeners(a, 'mousedown', false).length);
assertEquals(0, goog.events.getListeners(a, 'mouseup', false).length);
eh.listen(a, ['click', 'mousedown', 'mouseup']);
assertEquals(1, goog.events.getListeners(a, 'click', false).length);
assertEquals(1, goog.events.getListeners(a, 'mousedown', false).length);
assertEquals(1, goog.events.getListeners(a, 'mouseup', false).length);
eh.removeAll();
assertEquals(0, goog.events.getListeners(a, 'click', false).length);
assertEquals(0, goog.events.getListeners(a, 'mousedown', false).length);
assertEquals(0, goog.events.getListeners(a, 'mouseup', false).length);
}
function testListenOnceRemovesListenerWhenFired() {
var target = new goog.events.EventTarget();
eh = new goog.events.EventHandler();
var handler = goog.testing.recordFunction();
eh.listenOnce(target, 'click', handler);
target.dispatchEvent('click');
assertEquals('One event should have been dispatched',
1, handler.getCallCount());
target.dispatchEvent('click');
assertEquals('No event should have been dispatched',
1, handler.getCallCount());
}
function testListenOnceListenerIsCleanedUp() {
var target = new goog.events.EventTarget();
eh = new goog.events.EventHandler();
var handler = goog.testing.recordFunction();
eh.listenOnce(target, 'click', handler);
eh.removeAll();
target.dispatchEvent('click');
assertEquals(0, handler.getCallCount());
}
function testClearListenersWithListenOnceListenerRemoved() {
var target = new goog.events.EventTarget();
eh = new goog.events.EventHandler();
var handler = goog.testing.recordFunction();
eh.listenOnce(target, 'click', handler);
assertNotNull(goog.events.getListener(target, 'click', handler, false, eh));
target.dispatchEvent('click');
assertEquals('One event should have been dispatched',
1, handler.getCallCount());
assertNull(goog.events.getListener(target, 'click', handler, false, eh));
eh.removeAll();
target.dispatchEvent('click');
assertEquals('No event should have been dispatched',
1, handler.getCallCount());
}
function testListenOnceArray() {
var target = new goog.events.EventTarget();
eh = new goog.events.EventHandler();
var handler = goog.testing.recordFunction();
eh.listenOnce(target, ['click', 'mousedown', 'mouseup'], handler);
target.dispatchEvent('click');
assertEquals('1 event should have been dispatched',
1, handler.getCallCount());
assertEquals('Should be a click event',
'click', handler.getLastCall().getArgument(0).type);
target.dispatchEvent('click');
assertEquals('No event should be dispatched',
1, handler.getCallCount());
target.dispatchEvent('mouseup');
assertEquals('1 event should have been dispatched',
2, handler.getCallCount());
assertEquals('Should be a mouseup event',
'mouseup', handler.getLastCall().getArgument(0).type);
target.dispatchEvent('mouseup');
assertEquals('No event should be dispatched',
2, handler.getCallCount());
target.dispatchEvent('mousedown');
assertEquals('1 event should have been dispatched',
3, handler.getCallCount());
assertEquals('Should be a mousedown event',
'mousedown', handler.getLastCall().getArgument(0).type);
target.dispatchEvent('mousedown');
assertEquals('No event should be dispatched',
3, handler.getCallCount());
}
function testListenUnlistenWithObjectHandler() {
var target = new goog.events.EventTarget();
eh = new goog.events.EventHandler();
var handlerObj = {
handleEvent: goog.testing.recordFunction()
};
eh.listen(target, 'click', handlerObj);
target.dispatchEvent('click');
assertEquals('One event should have been dispatched',
1, handlerObj.handleEvent.getCallCount());
target.dispatchEvent('click');
assertEquals('One event should have been dispatched',
2, handlerObj.handleEvent.getCallCount());
eh.unlisten(target, 'click', handlerObj);
target.dispatchEvent('click');
assertEquals('No event should have been dispatched',
2, handlerObj.handleEvent.getCallCount());
}
function testListenOnceWithObjectHandler() {
var target = new goog.events.EventTarget();
eh = new goog.events.EventHandler();
var handlerObj = {
handleEvent: goog.testing.recordFunction()
};
eh.listenOnce(target, 'click', handlerObj);
target.dispatchEvent('click');
assertEquals('One event should have been dispatched',
1, handlerObj.handleEvent.getCallCount());
target.dispatchEvent('click');
assertEquals('No event should have been dispatched',
1, handlerObj.handleEvent.getCallCount());
}
function testGetListenerCount() {
eh = new goog.events.EventHandler();
assertEquals('0 listeners registered initially', 0, eh.getListenerCount());
var target = new goog.events.EventTarget();
eh.listen(target, 'click', goog.nullFunction, false);
eh.listen(target, 'click', goog.nullFunction, true);
assertEquals('2 listeners registered', 2, eh.getListenerCount());
eh.unlisten(target, 'click', goog.nullFunction, true);
assertEquals('1 listener removed, 1 left', 1, eh.getListenerCount());
eh.removeAll();
assertEquals('all listeners removed', 0, eh.getListenerCount());
}
@@ -0,0 +1,47 @@
// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.EventId');
/**
* A templated class that is used when registering for events. Typical usage:
* <code>
* /** @type {goog.events.EventId<MyEventObj>}
* var myEventId = new goog.events.EventId(
* goog.events.getUniqueId(('someEvent'));
*
* // No need to cast or declare here since the compiler knows the correct
* // type of 'evt' (MyEventObj).
* something.listen(myEventId, function(evt) {});
* </code>
*
* @param {string} eventId
* @template T
* @constructor
* @struct
* @final
*/
goog.events.EventId = function(eventId) {
/** @const */ this.id = eventId;
};
/**
* @override
*/
goog.events.EventId.prototype.toString = function() {
return this.id;
};
@@ -0,0 +1,983 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview An event manager for both native browser event
* targets and custom JavaScript event targets
* ({@code goog.events.Listenable}). This provides an abstraction
* over browsers' event systems.
*
* It also provides a simulation of W3C event model's capture phase in
* Internet Explorer (IE 8 and below). Caveat: the simulation does not
* interact well with listeners registered directly on the elements
* (bypassing goog.events) or even with listeners registered via
* goog.events in a separate JS binary. In these cases, we provide
* no ordering guarantees.
*
* The listeners will receive a "patched" event object. Such event object
* contains normalized values for certain event properties that differs in
* different browsers.
*
* Example usage:
* <pre>
* goog.events.listen(myNode, 'click', function(e) { alert('woo') });
* goog.events.listen(myNode, 'mouseover', mouseHandler, true);
* goog.events.unlisten(myNode, 'mouseover', mouseHandler, true);
* goog.events.removeAll(myNode);
* </pre>
*
* in IE and event object patching]
* @author arv@google.com (Erik Arvidsson)
*
* @see ../demos/events.html
* @see ../demos/event-propagation.html
* @see ../demos/stopevent.html
*/
// IMPLEMENTATION NOTES:
// goog.events stores an auxiliary data structure on each EventTarget
// source being listened on. This allows us to take advantage of GC,
// having the data structure GC'd when the EventTarget is GC'd. This
// GC behavior is equivalent to using W3C DOM Events directly.
goog.provide('goog.events');
goog.provide('goog.events.CaptureSimulationMode');
goog.provide('goog.events.Key');
goog.provide('goog.events.ListenableType');
goog.require('goog.asserts');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.Listenable');
goog.require('goog.events.ListenerMap');
goog.forwardDeclare('goog.debug.ErrorHandler');
goog.forwardDeclare('goog.events.EventWrapper');
/**
* @typedef {number|goog.events.ListenableKey}
*/
goog.events.Key;
/**
* @typedef {EventTarget|goog.events.Listenable}
*/
goog.events.ListenableType;
/**
* Property name on a native event target for the listener map
* associated with the event target.
* @private @const {string}
*/
goog.events.LISTENER_MAP_PROP_ = 'closure_lm_' + ((Math.random() * 1e6) | 0);
/**
* String used to prepend to IE event types.
* @const
* @private
*/
goog.events.onString_ = 'on';
/**
* Map of computed "on<eventname>" strings for IE event types. Caching
* this removes an extra object allocation in goog.events.listen which
* improves IE6 performance.
* @const
* @dict
* @private
*/
goog.events.onStringMap_ = {};
/**
* @enum {number} Different capture simulation mode for IE8-.
*/
goog.events.CaptureSimulationMode = {
/**
* Does not perform capture simulation. Will asserts in IE8- when you
* add capture listeners.
*/
OFF_AND_FAIL: 0,
/**
* Does not perform capture simulation, silently ignore capture
* listeners.
*/
OFF_AND_SILENT: 1,
/**
* Performs capture simulation.
*/
ON: 2
};
/**
* @define {number} The capture simulation mode for IE8-. By default,
* this is ON.
*/
goog.define('goog.events.CAPTURE_SIMULATION_MODE', 2);
/**
* Estimated count of total native listeners.
* @private {number}
*/
goog.events.listenerCountEstimate_ = 0;
/**
* Adds an event listener for a specific event on a native event
* target (such as a DOM element) or an object that has implemented
* {@link goog.events.Listenable}. A listener can only be added once
* to an object and if it is added again the key for the listener is
* returned. Note that if the existing listener is a one-off listener
* (registered via listenOnce), it will no longer be a one-off
* listener after a call to listen().
*
* @param {EventTarget|goog.events.Listenable} src The node to listen
* to events on.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type or array of event types.
* @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
* listener Callback method, or an object with a handleEvent function.
* WARNING: passing an Object is now softly deprecated.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {T=} opt_handler Element in whose scope to call the listener.
* @return {goog.events.Key} Unique key for the listener.
* @template T,EVENTOBJ
*/
goog.events.listen = function(src, type, listener, opt_capt, opt_handler) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
goog.events.listen(src, type[i], listener, opt_capt, opt_handler);
}
return null;
}
listener = goog.events.wrapListener(listener);
if (goog.events.Listenable.isImplementedBy(src)) {
return src.listen(
/** @type {string|!goog.events.EventId} */ (type),
listener, opt_capt, opt_handler);
} else {
return goog.events.listen_(
/** @type {!EventTarget} */ (src),
/** @type {string|!goog.events.EventId} */ (type),
listener, /* callOnce */ false, opt_capt, opt_handler);
}
};
/**
* Adds an event listener for a specific event on a native event
* target. A listener can only be added once to an object and if it
* is added again the key for the listener is returned.
*
* Note that a one-off listener will not change an existing listener,
* if any. On the other hand a normal listener will change existing
* one-off listener to become a normal listener.
*
* @param {EventTarget} src The node to listen to events on.
* @param {string|!goog.events.EventId} type Event type.
* @param {!Function} listener Callback function.
* @param {boolean} callOnce Whether the listener is a one-off
* listener or otherwise.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {Object=} opt_handler Element in whose scope to call the listener.
* @return {goog.events.ListenableKey} Unique key for the listener.
* @private
*/
goog.events.listen_ = function(
src, type, listener, callOnce, opt_capt, opt_handler) {
if (!type) {
throw Error('Invalid event type');
}
var capture = !!opt_capt;
if (capture && !goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
if (goog.events.CAPTURE_SIMULATION_MODE ==
goog.events.CaptureSimulationMode.OFF_AND_FAIL) {
goog.asserts.fail('Can not register capture listener in IE8-.');
return null;
} else if (goog.events.CAPTURE_SIMULATION_MODE ==
goog.events.CaptureSimulationMode.OFF_AND_SILENT) {
return null;
}
}
var listenerMap = goog.events.getListenerMap_(src);
if (!listenerMap) {
src[goog.events.LISTENER_MAP_PROP_] = listenerMap =
new goog.events.ListenerMap(src);
}
var listenerObj = listenerMap.add(
type, listener, callOnce, opt_capt, opt_handler);
// If the listenerObj already has a proxy, it has been set up
// previously. We simply return.
if (listenerObj.proxy) {
return listenerObj;
}
var proxy = goog.events.getProxy();
listenerObj.proxy = proxy;
proxy.src = src;
proxy.listener = listenerObj;
// Attach the proxy through the browser's API
if (src.addEventListener) {
src.addEventListener(type.toString(), proxy, capture);
} else {
// The else above used to be else if (src.attachEvent) and then there was
// another else statement that threw an exception warning the developer
// they made a mistake. This resulted in an extra object allocation in IE6
// due to a wrapper object that had to be implemented around the element
// and so was removed.
src.attachEvent(goog.events.getOnString_(type.toString()), proxy);
}
goog.events.listenerCountEstimate_++;
return listenerObj;
};
/**
* Helper function for returning a proxy function.
* @return {!Function} A new or reused function object.
*/
goog.events.getProxy = function() {
var proxyCallbackFunction = goog.events.handleBrowserEvent_;
// Use a local var f to prevent one allocation.
var f = goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT ?
function(eventObject) {
return proxyCallbackFunction.call(f.src, f.listener, eventObject);
} :
function(eventObject) {
var v = proxyCallbackFunction.call(f.src, f.listener, eventObject);
// NOTE(chrishenry): In IE, we hack in a capture phase. However, if
// there is inline event handler which tries to prevent default (for
// example <a href="..." onclick="return false">...</a>) in a
// descendant element, the prevent default will be overridden
// by this listener if this listener were to return true. Hence, we
// return undefined.
if (!v) return v;
};
return f;
};
/**
* Adds an event listener for a specific event on a native event
* target (such as a DOM element) or an object that has implemented
* {@link goog.events.Listenable}. After the event has fired the event
* listener is removed from the target.
*
* If an existing listener already exists, listenOnce will do
* nothing. In particular, if the listener was previously registered
* via listen(), listenOnce() will not turn the listener into a
* one-off listener. Similarly, if there is already an existing
* one-off listener, listenOnce does not modify the listeners (it is
* still a once listener).
*
* @param {EventTarget|goog.events.Listenable} src The node to listen
* to events on.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type or array of event types.
* @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
* listener Callback method.
* @param {boolean=} opt_capt Fire in capture phase?.
* @param {T=} opt_handler Element in whose scope to call the listener.
* @return {goog.events.Key} Unique key for the listener.
* @template T,EVENTOBJ
*/
goog.events.listenOnce = function(src, type, listener, opt_capt, opt_handler) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
goog.events.listenOnce(src, type[i], listener, opt_capt, opt_handler);
}
return null;
}
listener = goog.events.wrapListener(listener);
if (goog.events.Listenable.isImplementedBy(src)) {
return src.listenOnce(
/** @type {string|!goog.events.EventId} */ (type),
listener, opt_capt, opt_handler);
} else {
return goog.events.listen_(
/** @type {!EventTarget} */ (src),
/** @type {string|!goog.events.EventId} */ (type),
listener, /* callOnce */ true, opt_capt, opt_handler);
}
};
/**
* Adds an event listener with a specific event wrapper on a DOM Node or an
* object that has implemented {@link goog.events.Listenable}. A listener can
* only be added once to an object.
*
* @param {EventTarget|goog.events.Listenable} src The target to
* listen to events on.
* @param {goog.events.EventWrapper} wrapper Event wrapper to use.
* @param {function(this:T, ?):?|{handleEvent:function(?):?}|null} listener
* Callback method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {T=} opt_handler Element in whose scope to call the listener.
* @template T
*/
goog.events.listenWithWrapper = function(src, wrapper, listener, opt_capt,
opt_handler) {
wrapper.listen(src, listener, opt_capt, opt_handler);
};
/**
* Removes an event listener which was added with listen().
*
* @param {EventTarget|goog.events.Listenable} src The target to stop
* listening to events on.
* @param {string|Array<string>|
* !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
* type Event type or array of event types to unlisten to.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener The
* listener function to remove.
* @param {boolean=} opt_capt In DOM-compliant browsers, this determines
* whether the listener is fired during the capture or bubble phase of the
* event.
* @param {Object=} opt_handler Element in whose scope to call the listener.
* @return {?boolean} indicating whether the listener was there to remove.
* @template EVENTOBJ
*/
goog.events.unlisten = function(src, type, listener, opt_capt, opt_handler) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
goog.events.unlisten(src, type[i], listener, opt_capt, opt_handler);
}
return null;
}
listener = goog.events.wrapListener(listener);
if (goog.events.Listenable.isImplementedBy(src)) {
return src.unlisten(
/** @type {string|!goog.events.EventId} */ (type),
listener, opt_capt, opt_handler);
}
if (!src) {
// TODO(chrishenry): We should tighten the API to only accept
// non-null objects, or add an assertion here.
return false;
}
var capture = !!opt_capt;
var listenerMap = goog.events.getListenerMap_(
/** @type {!EventTarget} */ (src));
if (listenerMap) {
var listenerObj = listenerMap.getListener(
/** @type {string|!goog.events.EventId} */ (type),
listener, capture, opt_handler);
if (listenerObj) {
return goog.events.unlistenByKey(listenerObj);
}
}
return false;
};
/**
* Removes an event listener which was added with listen() by the key
* returned by listen().
*
* @param {goog.events.Key} key The key returned by listen() for this
* event listener.
* @return {boolean} indicating whether the listener was there to remove.
*/
goog.events.unlistenByKey = function(key) {
// TODO(chrishenry): Remove this check when tests that rely on this
// are fixed.
if (goog.isNumber(key)) {
return false;
}
var listener = /** @type {goog.events.ListenableKey} */ (key);
if (!listener || listener.removed) {
return false;
}
var src = listener.src;
if (goog.events.Listenable.isImplementedBy(src)) {
return src.unlistenByKey(listener);
}
var type = listener.type;
var proxy = listener.proxy;
if (src.removeEventListener) {
src.removeEventListener(type, proxy, listener.capture);
} else if (src.detachEvent) {
src.detachEvent(goog.events.getOnString_(type), proxy);
}
goog.events.listenerCountEstimate_--;
var listenerMap = goog.events.getListenerMap_(
/** @type {!EventTarget} */ (src));
// TODO(chrishenry): Try to remove this conditional and execute the
// first branch always. This should be safe.
if (listenerMap) {
listenerMap.removeByKey(listener);
if (listenerMap.getTypeCount() == 0) {
// Null the src, just because this is simple to do (and useful
// for IE <= 7).
listenerMap.src = null;
// We don't use delete here because IE does not allow delete
// on a window object.
src[goog.events.LISTENER_MAP_PROP_] = null;
}
} else {
listener.markAsRemoved();
}
return true;
};
/**
* Removes an event listener which was added with listenWithWrapper().
*
* @param {EventTarget|goog.events.Listenable} src The target to stop
* listening to events on.
* @param {goog.events.EventWrapper} wrapper Event wrapper to use.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener The
* listener function to remove.
* @param {boolean=} opt_capt In DOM-compliant browsers, this determines
* whether the listener is fired during the capture or bubble phase of the
* event.
* @param {Object=} opt_handler Element in whose scope to call the listener.
*/
goog.events.unlistenWithWrapper = function(src, wrapper, listener, opt_capt,
opt_handler) {
wrapper.unlisten(src, listener, opt_capt, opt_handler);
};
/**
* Removes all listeners from an object. You can also optionally
* remove listeners of a particular type.
*
* @param {Object|undefined} obj Object to remove listeners from. Must be an
* EventTarget or a goog.events.Listenable.
* @param {string|!goog.events.EventId=} opt_type Type of event to remove.
* Default is all types.
* @return {number} Number of listeners removed.
*/
goog.events.removeAll = function(obj, opt_type) {
// TODO(chrishenry): Change the type of obj to
// (!EventTarget|!goog.events.Listenable).
if (!obj) {
return 0;
}
if (goog.events.Listenable.isImplementedBy(obj)) {
return obj.removeAllListeners(opt_type);
}
var listenerMap = goog.events.getListenerMap_(
/** @type {!EventTarget} */ (obj));
if (!listenerMap) {
return 0;
}
var count = 0;
var typeStr = opt_type && opt_type.toString();
for (var type in listenerMap.listeners) {
if (!typeStr || type == typeStr) {
// Clone so that we don't need to worry about unlistenByKey
// changing the content of the ListenerMap.
var listeners = listenerMap.listeners[type].concat();
for (var i = 0; i < listeners.length; ++i) {
if (goog.events.unlistenByKey(listeners[i])) {
++count;
}
}
}
}
return count;
};
/**
* Gets the listeners for a given object, type and capture phase.
*
* @param {Object} obj Object to get listeners for.
* @param {string|!goog.events.EventId} type Event type.
* @param {boolean} capture Capture phase?.
* @return {Array<goog.events.Listener>} Array of listener objects.
*/
goog.events.getListeners = function(obj, type, capture) {
if (goog.events.Listenable.isImplementedBy(obj)) {
return obj.getListeners(type, capture);
} else {
if (!obj) {
// TODO(chrishenry): We should tighten the API to accept
// !EventTarget|goog.events.Listenable, and add an assertion here.
return [];
}
var listenerMap = goog.events.getListenerMap_(
/** @type {!EventTarget} */ (obj));
return listenerMap ? listenerMap.getListeners(type, capture) : [];
}
};
/**
* Gets the goog.events.Listener for the event or null if no such listener is
* in use.
*
* @param {EventTarget|goog.events.Listenable} src The target from
* which to get listeners.
* @param {?string|!goog.events.EventId<EVENTOBJ>} type The type of the event.
* @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null} listener The
* listener function to get.
* @param {boolean=} opt_capt In DOM-compliant browsers, this determines
* whether the listener is fired during the
* capture or bubble phase of the event.
* @param {Object=} opt_handler Element in whose scope to call the listener.
* @return {goog.events.ListenableKey} the found listener or null if not found.
* @template EVENTOBJ
*/
goog.events.getListener = function(src, type, listener, opt_capt, opt_handler) {
// TODO(chrishenry): Change type from ?string to string, or add assertion.
type = /** @type {string} */ (type);
listener = goog.events.wrapListener(listener);
var capture = !!opt_capt;
if (goog.events.Listenable.isImplementedBy(src)) {
return src.getListener(type, listener, capture, opt_handler);
}
if (!src) {
// TODO(chrishenry): We should tighten the API to only accept
// non-null objects, or add an assertion here.
return null;
}
var listenerMap = goog.events.getListenerMap_(
/** @type {!EventTarget} */ (src));
if (listenerMap) {
return listenerMap.getListener(type, listener, capture, opt_handler);
}
return null;
};
/**
* Returns whether an event target has any active listeners matching the
* specified signature. If either the type or capture parameters are
* unspecified, the function will match on the remaining criteria.
*
* @param {EventTarget|goog.events.Listenable} obj Target to get
* listeners for.
* @param {string|!goog.events.EventId=} opt_type Event type.
* @param {boolean=} opt_capture Whether to check for capture or bubble-phase
* listeners.
* @return {boolean} Whether an event target has one or more listeners matching
* the requested type and/or capture phase.
*/
goog.events.hasListener = function(obj, opt_type, opt_capture) {
if (goog.events.Listenable.isImplementedBy(obj)) {
return obj.hasListener(opt_type, opt_capture);
}
var listenerMap = goog.events.getListenerMap_(
/** @type {!EventTarget} */ (obj));
return !!listenerMap && listenerMap.hasListener(opt_type, opt_capture);
};
/**
* Provides a nice string showing the normalized event objects public members
* @param {Object} e Event Object.
* @return {string} String of the public members of the normalized event object.
*/
goog.events.expose = function(e) {
var str = [];
for (var key in e) {
if (e[key] && e[key].id) {
str.push(key + ' = ' + e[key] + ' (' + e[key].id + ')');
} else {
str.push(key + ' = ' + e[key]);
}
}
return str.join('\n');
};
/**
* Returns a string with on prepended to the specified type. This is used for IE
* which expects "on" to be prepended. This function caches the string in order
* to avoid extra allocations in steady state.
* @param {string} type Event type.
* @return {string} The type string with 'on' prepended.
* @private
*/
goog.events.getOnString_ = function(type) {
if (type in goog.events.onStringMap_) {
return goog.events.onStringMap_[type];
}
return goog.events.onStringMap_[type] = goog.events.onString_ + type;
};
/**
* Fires an object's listeners of a particular type and phase
*
* @param {Object} obj Object whose listeners to call.
* @param {string|!goog.events.EventId} type Event type.
* @param {boolean} capture Which event phase.
* @param {Object} eventObject Event object to be passed to listener.
* @return {boolean} True if all listeners returned true else false.
*/
goog.events.fireListeners = function(obj, type, capture, eventObject) {
if (goog.events.Listenable.isImplementedBy(obj)) {
return obj.fireListeners(type, capture, eventObject);
}
return goog.events.fireListeners_(obj, type, capture, eventObject);
};
/**
* Fires an object's listeners of a particular type and phase.
* @param {Object} obj Object whose listeners to call.
* @param {string|!goog.events.EventId} type Event type.
* @param {boolean} capture Which event phase.
* @param {Object} eventObject Event object to be passed to listener.
* @return {boolean} True if all listeners returned true else false.
* @private
*/
goog.events.fireListeners_ = function(obj, type, capture, eventObject) {
/** @type {boolean} */
var retval = true;
var listenerMap = goog.events.getListenerMap_(
/** @type {EventTarget} */ (obj));
if (listenerMap) {
// TODO(chrishenry): Original code avoids array creation when there
// is no listener, so we do the same. If this optimization turns
// out to be not required, we can replace this with
// listenerMap.getListeners(type, capture) instead, which is simpler.
var listenerArray = listenerMap.listeners[type.toString()];
if (listenerArray) {
listenerArray = listenerArray.concat();
for (var i = 0; i < listenerArray.length; i++) {
var listener = listenerArray[i];
// We might not have a listener if the listener was removed.
if (listener && listener.capture == capture && !listener.removed) {
var result = goog.events.fireListener(listener, eventObject);
retval = retval && (result !== false);
}
}
}
}
return retval;
};
/**
* Fires a listener with a set of arguments
*
* @param {goog.events.Listener} listener The listener object to call.
* @param {Object} eventObject The event object to pass to the listener.
* @return {boolean} Result of listener.
*/
goog.events.fireListener = function(listener, eventObject) {
var listenerFn = listener.listener;
var listenerHandler = listener.handler || listener.src;
if (listener.callOnce) {
goog.events.unlistenByKey(listener);
}
return listenerFn.call(listenerHandler, eventObject);
};
/**
* Gets the total number of listeners currently in the system.
* @return {number} Number of listeners.
* @deprecated This returns estimated count, now that Closure no longer
* stores a central listener registry. We still return an estimation
* to keep existing listener-related tests passing. In the near future,
* this function will be removed.
*/
goog.events.getTotalListenerCount = function() {
return goog.events.listenerCountEstimate_;
};
/**
* Dispatches an event (or event like object) and calls all listeners
* listening for events of this type. The type of the event is decided by the
* type property on the event object.
*
* If any of the listeners returns false OR calls preventDefault then this
* function will return false. If one of the capture listeners calls
* stopPropagation, then the bubble listeners won't fire.
*
* @param {goog.events.Listenable} src The event target.
* @param {goog.events.EventLike} e Event object.
* @return {boolean} If anyone called preventDefault on the event object (or
* if any of the handlers returns false) this will also return false.
* If there are no handlers, or if all handlers return true, this returns
* true.
*/
goog.events.dispatchEvent = function(src, e) {
goog.asserts.assert(
goog.events.Listenable.isImplementedBy(src),
'Can not use goog.events.dispatchEvent with ' +
'non-goog.events.Listenable instance.');
return src.dispatchEvent(e);
};
/**
* Installs exception protection for the browser event entry point using the
* given error handler.
*
* @param {goog.debug.ErrorHandler} errorHandler Error handler with which to
* protect the entry point.
*/
goog.events.protectBrowserEventEntryPoint = function(errorHandler) {
goog.events.handleBrowserEvent_ = errorHandler.protectEntryPoint(
goog.events.handleBrowserEvent_);
};
/**
* Handles an event and dispatches it to the correct listeners. This
* function is a proxy for the real listener the user specified.
*
* @param {goog.events.Listener} listener The listener object.
* @param {Event=} opt_evt Optional event object that gets passed in via the
* native event handlers.
* @return {boolean} Result of the event handler.
* @this {EventTarget} The object or Element that fired the event.
* @private
*/
goog.events.handleBrowserEvent_ = function(listener, opt_evt) {
if (listener.removed) {
return true;
}
// Synthesize event propagation if the browser does not support W3C
// event model.
if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
var ieEvent = opt_evt ||
/** @type {Event} */ (goog.getObjectByName('window.event'));
var evt = new goog.events.BrowserEvent(ieEvent, this);
/** @type {boolean} */
var retval = true;
if (goog.events.CAPTURE_SIMULATION_MODE ==
goog.events.CaptureSimulationMode.ON) {
// If we have not marked this event yet, we should perform capture
// simulation.
if (!goog.events.isMarkedIeEvent_(ieEvent)) {
goog.events.markIeEvent_(ieEvent);
var ancestors = [];
for (var parent = evt.currentTarget; parent;
parent = parent.parentNode) {
ancestors.push(parent);
}
// Fire capture listeners.
var type = listener.type;
for (var i = ancestors.length - 1; !evt.propagationStopped_ && i >= 0;
i--) {
evt.currentTarget = ancestors[i];
var result = goog.events.fireListeners_(ancestors[i], type, true, evt);
retval = retval && result;
}
// Fire bubble listeners.
//
// We can technically rely on IE to perform bubble event
// propagation. However, it turns out that IE fires events in
// opposite order of attachEvent registration, which broke
// some code and tests that rely on the order. (While W3C DOM
// Level 2 Events TR leaves the event ordering unspecified,
// modern browsers and W3C DOM Level 3 Events Working Draft
// actually specify the order as the registration order.)
for (var i = 0; !evt.propagationStopped_ && i < ancestors.length; i++) {
evt.currentTarget = ancestors[i];
var result = goog.events.fireListeners_(ancestors[i], type, false, evt);
retval = retval && result;
}
}
} else {
retval = goog.events.fireListener(listener, evt);
}
return retval;
}
// Otherwise, simply fire the listener.
return goog.events.fireListener(
listener, new goog.events.BrowserEvent(opt_evt, this));
};
/**
* This is used to mark the IE event object so we do not do the Closure pass
* twice for a bubbling event.
* @param {Event} e The IE browser event.
* @private
*/
goog.events.markIeEvent_ = function(e) {
// Only the keyCode and the returnValue can be changed. We use keyCode for
// non keyboard events.
// event.returnValue is a bit more tricky. It is undefined by default. A
// boolean false prevents the default action. In a window.onbeforeunload and
// the returnValue is non undefined it will be alerted. However, we will only
// modify the returnValue for keyboard events. We can get a problem if non
// closure events sets the keyCode or the returnValue
var useReturnValue = false;
if (e.keyCode == 0) {
// We cannot change the keyCode in case that srcElement is input[type=file].
// We could test that that is the case but that would allocate 3 objects.
// If we use try/catch we will only allocate extra objects in the case of a
// failure.
/** @preserveTry */
try {
e.keyCode = -1;
return;
} catch (ex) {
useReturnValue = true;
}
}
if (useReturnValue ||
/** @type {boolean|undefined} */ (e.returnValue) == undefined) {
e.returnValue = true;
}
};
/**
* This is used to check if an IE event has already been handled by the Closure
* system so we do not do the Closure pass twice for a bubbling event.
* @param {Event} e The IE browser event.
* @return {boolean} True if the event object has been marked.
* @private
*/
goog.events.isMarkedIeEvent_ = function(e) {
return e.keyCode < 0 || e.returnValue != undefined;
};
/**
* Counter to create unique event ids.
* @private {number}
*/
goog.events.uniqueIdCounter_ = 0;
/**
* Creates a unique event id.
*
* @param {string} identifier The identifier.
* @return {string} A unique identifier.
* @idGenerator
*/
goog.events.getUniqueId = function(identifier) {
return identifier + '_' + goog.events.uniqueIdCounter_++;
};
/**
* @param {EventTarget} src The source object.
* @return {goog.events.ListenerMap} A listener map for the given
* source object, or null if none exists.
* @private
*/
goog.events.getListenerMap_ = function(src) {
var listenerMap = src[goog.events.LISTENER_MAP_PROP_];
// IE serializes the property as well (e.g. when serializing outer
// HTML). So we must check that the value is of the correct type.
return listenerMap instanceof goog.events.ListenerMap ? listenerMap : null;
};
/**
* Expando property for listener function wrapper for Object with
* handleEvent.
* @private @const {string}
*/
goog.events.LISTENER_WRAPPER_PROP_ = '__closure_events_fn_' +
((Math.random() * 1e9) >>> 0);
/**
* @param {Object|Function} listener The listener function or an
* object that contains handleEvent method.
* @return {!Function} Either the original function or a function that
* calls obj.handleEvent. If the same listener is passed to this
* function more than once, the same function is guaranteed to be
* returned.
*/
goog.events.wrapListener = function(listener) {
goog.asserts.assert(listener, 'Listener can not be null.');
if (goog.isFunction(listener)) {
return listener;
}
goog.asserts.assert(
listener.handleEvent, 'An object listener must have handleEvent method.');
if (!listener[goog.events.LISTENER_WRAPPER_PROP_]) {
listener[goog.events.LISTENER_WRAPPER_PROP_] =
function(e) { return listener.handleEvent(e); };
}
return listener[goog.events.LISTENER_WRAPPER_PROP_];
};
// Register the browser event handler as an entry point, so that
// it can be monitored for exception handling, etc.
goog.debug.entryPointRegistry.register(
/**
* @param {function(!Function): !Function} transformer The transforming
* function.
*/
function(transformer) {
goog.events.handleBrowserEvent_ = transformer(
goog.events.handleBrowserEvent_);
});
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.eventsTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,745 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.eventsTest');
goog.setTestOnly('goog.eventsTest');
goog.require('goog.asserts.AssertionError');
goog.require('goog.debug.EntryPointMonitor');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.CaptureSimulationMode');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.Listener');
goog.require('goog.functions');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var originalHandleBrowserEvent = goog.events.handleBrowserEvent_;
var propertyReplacer;
var et1, et2, et3;
function setUp() {
et1 = new goog.events.EventTarget();
et2 = new goog.events.EventTarget();
et3 = new goog.events.EventTarget();
propertyReplacer = new goog.testing.PropertyReplacer();
}
function tearDown() {
goog.events.CAPTURE_SIMULATION_MODE =
goog.events.CaptureSimulationMode.ON;
goog.events.handleBrowserEvent_ = originalHandleBrowserEvent;
goog.disposeAll(et1, et2, et3);
goog.events.removeAll(document.body);
propertyReplacer.reset();
}
function testProtectBrowserEventEntryPoint() {
var errorHandlerFn = goog.testing.recordFunction();
var errorHandler = new goog.debug.ErrorHandler(errorHandlerFn);
goog.events.protectBrowserEventEntryPoint(errorHandler);
var browserEventHandler =
goog.testing.recordFunction(goog.events.handleBrowserEvent_);
goog.events.handleBrowserEvent_ = function() {
try {
browserEventHandler.apply(this, arguments);
} catch (e) {
// Ignored.
}
};
var err = Error('test');
var body = document.body;
goog.events.listen(body, goog.events.EventType.CLICK, function() {
throw err;
});
dispatchClick(body);
assertEquals('Error handler callback should be called.',
1, errorHandlerFn.getCallCount());
assertEquals(err, errorHandlerFn.getLastCall().getArgument(0));
assertEquals(1, browserEventHandler.getCallCount());
var err2 = browserEventHandler.getLastCall().getError();
assertNotNull(err2);
assertTrue(
err2 instanceof goog.debug.ErrorHandler.ProtectedFunctionError);
}
function testSelfRemove() {
var callback = function() {
// This listener removes itself during event dispatching, so it
// is marked as 'removed' but not actually removed until after event
// dispatching ends.
goog.events.removeAll(et1, 'click');
// Test that goog.events.getListener ignores events marked as 'removed'.
assertNull(goog.events.getListener(et1, 'click', callback));
};
var key = goog.events.listen(et1, 'click', callback);
goog.events.dispatchEvent(et1, 'click');
}
function testHasListener() {
var div = document.createElement('div');
assertFalse(goog.events.hasListener(div));
var key = goog.events.listen(div, 'click', function() {});
assertTrue(goog.events.hasListener(div));
assertTrue(goog.events.hasListener(div, 'click'));
assertTrue(goog.events.hasListener(div, 'click', false));
assertTrue(goog.events.hasListener(div, undefined, false));
assertFalse(goog.events.hasListener(div, 'click', true));
assertFalse(goog.events.hasListener(div, undefined, true));
assertFalse(goog.events.hasListener(div, 'mouseup'));
// Test that hasListener returns false when all listeners are removed.
goog.events.unlistenByKey(key);
assertFalse(goog.events.hasListener(div));
}
function testHasListenerWithEventTarget() {
assertFalse(goog.events.hasListener(et1));
function callback() {};
goog.events.listen(et1, 'test', callback, true);
assertTrue(goog.events.hasListener(et1));
assertTrue(goog.events.hasListener(et1, 'test'));
assertTrue(goog.events.hasListener(et1, 'test', true));
assertTrue(goog.events.hasListener(et1, undefined, true));
assertFalse(goog.events.hasListener(et1, 'click'));
assertFalse(goog.events.hasListener(et1, 'test', false));
goog.events.unlisten(et1, 'test', callback, true);
assertFalse(goog.events.hasListener(et1));
}
function testHasListenerWithMultipleTargets() {
function callback() {};
goog.events.listen(et1, 'test1', callback, true);
goog.events.listen(et2, 'test2', callback, true);
assertTrue(goog.events.hasListener(et1));
assertTrue(goog.events.hasListener(et2));
assertTrue(goog.events.hasListener(et1, 'test1'));
assertTrue(goog.events.hasListener(et2, 'test2'));
assertFalse(goog.events.hasListener(et1, 'et2'));
assertFalse(goog.events.hasListener(et2, 'et1'));
goog.events.removeAll(et1);
goog.events.removeAll(et2);
}
function testBubbleSingle() {
et1.setParentEventTarget(et2);
et2.setParentEventTarget(et3);
var count = 0;
function callback() {
count++;
}
goog.events.listen(et3, 'test', callback, false);
et1.dispatchEvent('test');
assertEquals(1, count);
goog.events.removeAll(et1);
goog.events.removeAll(et2);
goog.events.removeAll(et3);
}
function testCaptureSingle() {
et1.setParentEventTarget(et2);
et2.setParentEventTarget(et3);
var count = 0;
function callback() {
count++;
}
goog.events.listen(et3, 'test', callback, true);
et1.dispatchEvent('test');
assertEquals(1, count);
goog.events.removeAll(et1);
goog.events.removeAll(et2);
goog.events.removeAll(et3);
}
function testCaptureAndBubble() {
et1.setParentEventTarget(et2);
et2.setParentEventTarget(et3);
var count = 0;
function callbackCapture1() {
count++;
assertEquals(3, count);
}
function callbackBubble1() {
count++;
assertEquals(4, count);
}
function callbackCapture2() {
count++;
assertEquals(2, count);
}
function callbackBubble2() {
count++;
assertEquals(5, count);
}
function callbackCapture3() {
count++;
assertEquals(1, count);
}
function callbackBubble3() {
count++;
assertEquals(6, count);
}
goog.events.listen(et1, 'test', callbackCapture1, true);
goog.events.listen(et1, 'test', callbackBubble1, false);
goog.events.listen(et2, 'test', callbackCapture2, true);
goog.events.listen(et2, 'test', callbackBubble2, false);
goog.events.listen(et3, 'test', callbackCapture3, true);
goog.events.listen(et3, 'test', callbackBubble3, false);
et1.dispatchEvent('test');
assertEquals(6, count);
goog.events.removeAll(et1);
goog.events.removeAll(et2);
goog.events.removeAll(et3);
}
function testCapturingRemovesBubblingListener() {
var bubbleCount = 0;
function callbackBubble() {
bubbleCount++;
}
var captureCount = 0;
function callbackCapture() {
captureCount++;
goog.events.removeAll(et1);
}
goog.events.listen(et1, 'test', callbackCapture, true);
goog.events.listen(et1, 'test', callbackBubble, false);
et1.dispatchEvent('test');
assertEquals(1, captureCount);
assertEquals(0, bubbleCount);
}
function dispatchClick(target) {
if (target.click) {
target.click();
} else {
var e = document.createEvent('MouseEvents');
e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false,
false, false, false, 0, null);
target.dispatchEvent(e);
}
}
function testHandleBrowserEventBubblingListener() {
var count = 0;
var body = document.body;
goog.events.listen(body, 'click', function() {
count++;
});
dispatchClick(body);
assertEquals(1, count);
}
function testHandleBrowserEventCapturingListener() {
var count = 0;
var body = document.body;
goog.events.listen(body, 'click', function() {
count++;
}, true);
dispatchClick(body);
assertEquals(1, count);
}
function testHandleBrowserEventCapturingAndBubblingListener() {
var count = 1;
var body = document.body;
goog.events.listen(body, 'click', function() {
count += 3;
}, true);
goog.events.listen(body, 'click', function() {
count *= 5;
}, false);
dispatchClick(body);
assertEquals(20, count);
}
function testHandleBrowserEventCapturingRemovesBubblingListener() {
var body = document.body;
var bubbleCount = 0;
function callbackBubble() {
bubbleCount++;
}
var captureCount = 0;
function callbackCapture() {
captureCount++;
goog.events.removeAll(body);
}
goog.events.listen(body, 'click', callbackCapture, true);
goog.events.listen(body, 'click', callbackBubble, false);
dispatchClick(body);
assertEquals(1, captureCount);
assertEquals(0, bubbleCount);
}
function testHandleEventPropagationOnParentElement() {
var count = 1;
goog.events.listen(document.documentElement, 'click', function() {
count += 3;
}, true);
goog.events.listen(document.documentElement, 'click', function() {
count *= 5;
}, false);
dispatchClick(document.body);
assertEquals(20, count);
}
function testEntryPointRegistry() {
var monitor = new goog.debug.EntryPointMonitor();
var replacement = function() {};
monitor.wrap = goog.testing.recordFunction(
goog.functions.constant(replacement));
goog.debug.entryPointRegistry.monitorAll(monitor);
assertTrue(monitor.wrap.getCallCount() >= 1);
assertEquals(replacement, goog.events.handleBrowserEvent_);
}
// Fixes bug http://b/6434926
function testListenOnceHandlerDispatchCausingInfiniteLoop() {
var handleFoo = goog.testing.recordFunction(function() {
et1.dispatchEvent('foo');
});
goog.events.listenOnce(et1, 'foo', handleFoo);
et1.dispatchEvent('foo');
assertEquals('Handler should be called only once.',
1, handleFoo.getCallCount());
}
function testCreationStack() {
if (!new Error().stack)
return;
propertyReplacer.replace(goog.events.Listener, 'ENABLE_MONITORING', true);
var div = document.createElement('div');
var key = goog.events.listen(
div, goog.events.EventType.CLICK, goog.nullFunction);
var listenerStack = key.creationStack;
// Check that the name of this test function occurs in the stack trace.
assertContains('testCreationStack', listenerStack);
goog.events.unlistenByKey(key);
}
function testListenOnceAfterListenDoesNotChangeExistingListener() {
var listener = goog.testing.recordFunction();
goog.events.listen(document.body, 'click', listener);
goog.events.listenOnce(document.body, 'click', listener);
dispatchClick(document.body);
dispatchClick(document.body);
dispatchClick(document.body);
assertEquals(3, listener.getCallCount());
}
function testListenOnceAfterListenOnceDoesNotChangeExistingListener() {
var listener = goog.testing.recordFunction();
goog.events.listenOnce(document.body, 'click', listener);
goog.events.listenOnce(document.body, 'click', listener);
dispatchClick(document.body);
dispatchClick(document.body);
dispatchClick(document.body);
assertEquals(1, listener.getCallCount());
}
function testListenAfterListenOnceRemoveOnceness() {
var listener = goog.testing.recordFunction();
goog.events.listenOnce(document.body, 'click', listener);
goog.events.listen(document.body, 'click', listener);
dispatchClick(document.body);
dispatchClick(document.body);
dispatchClick(document.body);
assertEquals(3, listener.getCallCount());
}
function testUnlistenAfterListenOnce() {
var listener = goog.testing.recordFunction();
goog.events.listenOnce(document.body, 'click', listener);
goog.events.unlisten(document.body, 'click', listener);
dispatchClick(document.body);
goog.events.listenOnce(document.body, 'click', listener);
goog.events.listen(document.body, 'click', listener);
goog.events.unlisten(document.body, 'click', listener);
dispatchClick(document.body);
goog.events.listen(document.body, 'click', listener);
goog.events.listenOnce(document.body, 'click', listener);
goog.events.unlisten(document.body, 'click', listener);
dispatchClick(document.body);
goog.events.listenOnce(document.body, 'click', listener);
goog.events.listenOnce(document.body, 'click', listener);
goog.events.unlisten(document.body, 'click', listener);
dispatchClick(document.body);
assertEquals(0, listener.getCallCount());
}
function testEventBubblingWithReentrantDispatch_bubbling() {
runEventPropogationWithReentrantDispatch(false);
}
function testEventBubblingWithReentrantDispatch_capture() {
runEventPropogationWithReentrantDispatch(true);
}
function runEventPropogationWithReentrantDispatch(useCapture) {
var eventType = 'test-event-type';
var child = et1;
var parent = et2;
child.setParentEventTarget(parent);
var firstTarget = useCapture ? parent : child;
var secondTarget = useCapture ? child : parent;
var firstListener = function(evt) {
if (evt.isFirstEvent) {
// Fires another event of the same type the first time it is invoked.
child.dispatchEvent(new goog.events.Event(eventType));
}
};
goog.events.listen(firstTarget, eventType, firstListener, useCapture);
var secondListener = goog.testing.recordFunction();
goog.events.listen(secondTarget, eventType, secondListener, useCapture);
// Fire the first event.
var firstEvent = new goog.events.Event(eventType);
firstEvent.isFirstEvent = true;
child.dispatchEvent(firstEvent);
assertEquals(2, secondListener.getCallCount());
}
function testEventPropogationWhenListenerRemoved_bubbling() {
runEventPropogationWhenListenerRemoved(false);
}
function testEventPropogationWhenListenerRemoved_capture() {
runEventPropogationWhenListenerRemoved(true);
}
function runEventPropogationWhenListenerRemoved(useCapture) {
var eventType = 'test-event-type';
var child = et1;
var parent = et2;
child.setParentEventTarget(parent);
var firstTarget = useCapture ? parent : child;
var secondTarget = useCapture ? child : parent;
var firstListener = goog.testing.recordFunction();
var secondListener = goog.testing.recordFunction();
goog.events.listenOnce(firstTarget, eventType, firstListener, useCapture);
goog.events.listen(secondTarget, eventType, secondListener, useCapture);
child.dispatchEvent(new goog.events.Event(eventType));
assertEquals(1, secondListener.getCallCount());
}
function testEventPropogationWhenListenerAdded_bubbling() {
runEventPropogationWhenListenerAdded(false);
}
function testEventPropogationWhenListenerAdded_capture() {
runEventPropogationWhenListenerAdded(true);
}
function runEventPropogationWhenListenerAdded(useCapture) {
var eventType = 'test-event-type';
var child = et1;
var parent = et2;
child.setParentEventTarget(parent);
var firstTarget = useCapture ? parent : child;
var secondTarget = useCapture ? child : parent;
var firstListener = function() {
goog.events.listen(secondTarget, eventType, secondListener, useCapture);
};
var secondListener = goog.testing.recordFunction();
goog.events.listen(firstTarget, eventType, firstListener, useCapture);
child.dispatchEvent(new goog.events.Event(eventType));
assertEquals(1, secondListener.getCallCount());
}
function testEventPropogationWhenListenerAddedAndRemoved_bubbling() {
runEventPropogationWhenListenerAddedAndRemoved(false);
}
function testEventPropogationWhenListenerAddedAndRemoved_capture() {
runEventPropogationWhenListenerAddedAndRemoved(true);
}
function runEventPropogationWhenListenerAddedAndRemoved(useCapture) {
var eventType = 'test-event-type';
var child = et1;
var parent = et2;
child.setParentEventTarget(parent);
var firstTarget = useCapture ? parent : child;
var secondTarget = useCapture ? child : parent;
var firstListener = function() {
goog.events.listen(secondTarget, eventType, secondListener, useCapture);
};
var secondListener = goog.testing.recordFunction();
goog.events.listenOnce(firstTarget, eventType, firstListener, useCapture);
child.dispatchEvent(new goog.events.Event(eventType));
assertEquals(1, secondListener.getCallCount());
}
function testAssertWhenUsedWithUninitializedCustomEventTarget() {
var SubClass = function() { /* does not call superclass ctor */ };
goog.inherits(SubClass, goog.events.EventTarget);
var instance = new SubClass();
var e;
e = assertThrows(function() {
goog.events.listen(instance, 'test1', function() {});
});
assertTrue(e instanceof goog.asserts.AssertionError);
e = assertThrows(function() {
goog.events.dispatchEvent(instance, 'test1');
});
assertTrue(e instanceof goog.asserts.AssertionError);
e = assertThrows(function() {
instance.dispatchEvent('test1');
});
assertTrue(e instanceof goog.asserts.AssertionError);
}
function testAssertWhenDispatchEventIsUsedWithNonCustomEventTarget() {
var obj = {};
e = assertThrows(function() {
goog.events.dispatchEvent(obj, 'test1');
});
assertTrue(e instanceof goog.asserts.AssertionError);
}
function testPropagationStoppedDuringCapture() {
var captureHandler = goog.testing.recordFunction(function(e) {
e.stopPropagation();
});
var bubbleHandler = goog.testing.recordFunction();
var body = document.body;
var div = document.createElement('div');
body.appendChild(div);
try {
goog.events.listen(body, 'click', captureHandler, true);
goog.events.listen(div, 'click', bubbleHandler, false);
goog.events.listen(body, 'click', bubbleHandler, false);
dispatchClick(div);
assertEquals(1, captureHandler.getCallCount());
assertEquals(0, bubbleHandler.getCallCount());
goog.events.unlisten(body, 'click', captureHandler, true);
dispatchClick(div);
assertEquals(2, bubbleHandler.getCallCount());
} finally {
goog.dom.removeNode(div);
goog.events.removeAll(body);
goog.events.removeAll(div);
}
}
function testPropagationStoppedDuringBubble() {
var captureHandler = goog.testing.recordFunction();
var bubbleHandler1 = goog.testing.recordFunction(function(e) {
e.stopPropagation();
});
var bubbleHandler2 = goog.testing.recordFunction();
var body = document.body;
var div = document.createElement('div');
body.appendChild(div);
try {
goog.events.listen(body, 'click', captureHandler, true);
goog.events.listen(div, 'click', bubbleHandler1, false);
goog.events.listen(body, 'click', bubbleHandler2, false);
dispatchClick(div);
assertEquals(1, captureHandler.getCallCount());
assertEquals(1, bubbleHandler1.getCallCount());
assertEquals(0, bubbleHandler2.getCallCount());
} finally {
goog.dom.removeNode(div);
goog.events.removeAll(body);
goog.events.removeAll(div);
}
}
function testAddingCaptureListenerDuringBubbleShouldNotFireTheListener() {
var body = document.body;
var div = document.createElement('div');
body.appendChild(div);
var captureHandler1 = goog.testing.recordFunction();
var captureHandler2 = goog.testing.recordFunction();
var bubbleHandler = goog.testing.recordFunction(function(e) {
goog.events.listen(body, 'click', captureHandler1, true);
goog.events.listen(div, 'click', captureHandler2, true);
});
try {
goog.events.listen(div, 'click', bubbleHandler, false);
dispatchClick(div);
// These verify that the capture handlers registered in the bubble
// handler is not invoked in the same event propagation phase.
assertEquals(0, captureHandler1.getCallCount());
assertEquals(0, captureHandler2.getCallCount());
assertEquals(1, bubbleHandler.getCallCount());
} finally {
goog.dom.removeNode(div);
goog.events.removeAll(body);
goog.events.removeAll(div);
}
}
function testRemovingCaptureListenerDuringBubbleWouldNotFireListenerTwice() {
var body = document.body;
var div = document.createElement('div');
body.appendChild(div);
var captureHandler = goog.testing.recordFunction();
var bubbleHandler1 = goog.testing.recordFunction(function(e) {
goog.events.unlisten(body, 'click', captureHandler, true);
});
var bubbleHandler2 = goog.testing.recordFunction();
try {
goog.events.listen(body, 'click', captureHandler, true);
goog.events.listen(div, 'click', bubbleHandler1, false);
goog.events.listen(body, 'click', bubbleHandler2, false);
dispatchClick(div);
assertEquals(1, captureHandler.getCallCount());
// Verify that neither of these handlers are called more than once.
assertEquals(1, bubbleHandler1.getCallCount());
assertEquals(1, bubbleHandler2.getCallCount());
} finally {
goog.dom.removeNode(div);
goog.events.removeAll(body);
goog.events.removeAll(div);
}
}
function testCaptureSimulationModeOffAndFail() {
goog.events.CAPTURE_SIMULATION_MODE =
goog.events.CaptureSimulationMode.OFF_AND_FAIL;
var captureHandler = goog.testing.recordFunction();
if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
var err = assertThrows(function() {
goog.events.listen(document.body, 'click', captureHandler, true);
});
assertTrue(err instanceof goog.asserts.AssertionError);
// Sanity tests.
dispatchClick(document.body);
assertEquals(0, captureHandler.getCallCount());
} else {
goog.events.listen(document.body, 'click', captureHandler, true);
dispatchClick(document.body);
assertEquals(1, captureHandler.getCallCount());
}
}
function testCaptureSimulationModeOffAndSilent() {
goog.events.CAPTURE_SIMULATION_MODE =
goog.events.CaptureSimulationMode.OFF_AND_SILENT;
var captureHandler = goog.testing.recordFunction();
goog.events.listen(document.body, 'click', captureHandler, true);
if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
dispatchClick(document.body);
assertEquals(0, captureHandler.getCallCount());
} else {
dispatchClick(document.body);
assertEquals(1, captureHandler.getCallCount());
}
}
@@ -0,0 +1,394 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview A disposable implementation of a custom
* listenable/event target. See also: documentation for
* {@code goog.events.Listenable}.
*
* @author arv@google.com (Erik Arvidsson) [Original implementation]
* @see ../demos/eventtarget.html
* @see goog.events.Listenable
*/
goog.provide('goog.events.EventTarget');
goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.Listenable');
goog.require('goog.events.ListenerMap');
goog.require('goog.object');
/**
* An implementation of {@code goog.events.Listenable} with full W3C
* EventTarget-like support (capture/bubble mechanism, stopping event
* propagation, preventing default actions).
*
* You may subclass this class to turn your class into a Listenable.
*
* Unless propagation is stopped, an event dispatched by an
* EventTarget will bubble to the parent returned by
* {@code getParentEventTarget}. To set the parent, call
* {@code setParentEventTarget}. Subclasses that don't support
* changing the parent can override the setter to throw an error.
*
* Example usage:
* <pre>
* var source = new goog.events.EventTarget();
* function handleEvent(e) {
* alert('Type: ' + e.type + '; Target: ' + e.target);
* }
* source.listen('foo', handleEvent);
* // Or: goog.events.listen(source, 'foo', handleEvent);
* ...
* source.dispatchEvent('foo'); // will call handleEvent
* ...
* source.unlisten('foo', handleEvent);
* // Or: goog.events.unlisten(source, 'foo', handleEvent);
* </pre>
*
* @constructor
* @extends {goog.Disposable}
* @implements {goog.events.Listenable}
*/
goog.events.EventTarget = function() {
goog.Disposable.call(this);
/**
* Maps of event type to an array of listeners.
* @private {!goog.events.ListenerMap}
*/
this.eventTargetListeners_ = new goog.events.ListenerMap(this);
/**
* The object to use for event.target. Useful when mixing in an
* EventTarget to another object.
* @private {!Object}
*/
this.actualEventTarget_ = this;
/**
* Parent event target, used during event bubbling.
*
* TODO(chrishenry): Change this to goog.events.Listenable. This
* currently breaks people who expect getParentEventTarget to return
* goog.events.EventTarget.
*
* @private {goog.events.EventTarget}
*/
this.parentEventTarget_ = null;
};
goog.inherits(goog.events.EventTarget, goog.Disposable);
goog.events.Listenable.addImplementation(goog.events.EventTarget);
/**
* An artificial cap on the number of ancestors you can have. This is mainly
* for loop detection.
* @const {number}
* @private
*/
goog.events.EventTarget.MAX_ANCESTORS_ = 1000;
/**
* Returns the parent of this event target to use for bubbling.
*
* @return {goog.events.EventTarget} The parent EventTarget or null if
* there is no parent.
* @override
*/
goog.events.EventTarget.prototype.getParentEventTarget = function() {
return this.parentEventTarget_;
};
/**
* Sets the parent of this event target to use for capture/bubble
* mechanism.
* @param {goog.events.EventTarget} parent Parent listenable (null if none).
*/
goog.events.EventTarget.prototype.setParentEventTarget = function(parent) {
this.parentEventTarget_ = parent;
};
/**
* Adds an event listener to the event target. The same handler can only be
* added once per the type. Even if you add the same handler multiple times
* using the same type then it will only be called once when the event is
* dispatched.
*
* @param {string} type The type of the event to listen for.
* @param {function(?):?|{handleEvent:function(?):?}|null} handler The function
* to handle the event. The handler can also be an object that implements
* the handleEvent method which takes the event object as argument.
* @param {boolean=} opt_capture In DOM-compliant browsers, this determines
* whether the listener is fired during the capture or bubble phase
* of the event.
* @param {Object=} opt_handlerScope Object in whose scope to call
* the listener.
* @deprecated Use {@code #listen} instead, when possible. Otherwise, use
* {@code goog.events.listen} if you are passing Object
* (instead of Function) as handler.
*/
goog.events.EventTarget.prototype.addEventListener = function(
type, handler, opt_capture, opt_handlerScope) {
goog.events.listen(this, type, handler, opt_capture, opt_handlerScope);
};
/**
* Removes an event listener from the event target. The handler must be the
* same object as the one added. If the handler has not been added then
* nothing is done.
*
* @param {string} type The type of the event to listen for.
* @param {function(?):?|{handleEvent:function(?):?}|null} handler The function
* to handle the event. The handler can also be an object that implements
* the handleEvent method which takes the event object as argument.
* @param {boolean=} opt_capture In DOM-compliant browsers, this determines
* whether the listener is fired during the capture or bubble phase
* of the event.
* @param {Object=} opt_handlerScope Object in whose scope to call
* the listener.
* @deprecated Use {@code #unlisten} instead, when possible. Otherwise, use
* {@code goog.events.unlisten} if you are passing Object
* (instead of Function) as handler.
*/
goog.events.EventTarget.prototype.removeEventListener = function(
type, handler, opt_capture, opt_handlerScope) {
goog.events.unlisten(this, type, handler, opt_capture, opt_handlerScope);
};
/** @override */
goog.events.EventTarget.prototype.dispatchEvent = function(e) {
this.assertInitialized_();
var ancestorsTree, ancestor = this.getParentEventTarget();
if (ancestor) {
ancestorsTree = [];
var ancestorCount = 1;
for (; ancestor; ancestor = ancestor.getParentEventTarget()) {
ancestorsTree.push(ancestor);
goog.asserts.assert(
(++ancestorCount < goog.events.EventTarget.MAX_ANCESTORS_),
'infinite loop');
}
}
return goog.events.EventTarget.dispatchEventInternal_(
this.actualEventTarget_, e, ancestorsTree);
};
/**
* Removes listeners from this object. Classes that extend EventTarget may
* need to override this method in order to remove references to DOM Elements
* and additional listeners.
* @override
*/
goog.events.EventTarget.prototype.disposeInternal = function() {
goog.events.EventTarget.superClass_.disposeInternal.call(this);
this.removeAllListeners();
this.parentEventTarget_ = null;
};
/** @override */
goog.events.EventTarget.prototype.listen = function(
type, listener, opt_useCapture, opt_listenerScope) {
this.assertInitialized_();
return this.eventTargetListeners_.add(
String(type), listener, false /* callOnce */, opt_useCapture,
opt_listenerScope);
};
/** @override */
goog.events.EventTarget.prototype.listenOnce = function(
type, listener, opt_useCapture, opt_listenerScope) {
return this.eventTargetListeners_.add(
String(type), listener, true /* callOnce */, opt_useCapture,
opt_listenerScope);
};
/** @override */
goog.events.EventTarget.prototype.unlisten = function(
type, listener, opt_useCapture, opt_listenerScope) {
return this.eventTargetListeners_.remove(
String(type), listener, opt_useCapture, opt_listenerScope);
};
/** @override */
goog.events.EventTarget.prototype.unlistenByKey = function(key) {
return this.eventTargetListeners_.removeByKey(key);
};
/** @override */
goog.events.EventTarget.prototype.removeAllListeners = function(opt_type) {
// TODO(chrishenry): Previously, removeAllListeners can be called on
// uninitialized EventTarget, so we preserve that behavior. We
// should remove this when usages that rely on that fact are purged.
if (!this.eventTargetListeners_) {
return 0;
}
return this.eventTargetListeners_.removeAll(opt_type);
};
/** @override */
goog.events.EventTarget.prototype.fireListeners = function(
type, capture, eventObject) {
// TODO(chrishenry): Original code avoids array creation when there
// is no listener, so we do the same. If this optimization turns
// out to be not required, we can replace this with
// getListeners(type, capture) instead, which is simpler.
var listenerArray = this.eventTargetListeners_.listeners[String(type)];
if (!listenerArray) {
return true;
}
listenerArray = listenerArray.concat();
var rv = true;
for (var i = 0; i < listenerArray.length; ++i) {
var listener = listenerArray[i];
// We might not have a listener if the listener was removed.
if (listener && !listener.removed && listener.capture == capture) {
var listenerFn = listener.listener;
var listenerHandler = listener.handler || listener.src;
if (listener.callOnce) {
this.unlistenByKey(listener);
}
rv = listenerFn.call(listenerHandler, eventObject) !== false && rv;
}
}
return rv && eventObject.returnValue_ != false;
};
/** @override */
goog.events.EventTarget.prototype.getListeners = function(type, capture) {
return this.eventTargetListeners_.getListeners(String(type), capture);
};
/** @override */
goog.events.EventTarget.prototype.getListener = function(
type, listener, capture, opt_listenerScope) {
return this.eventTargetListeners_.getListener(
String(type), listener, capture, opt_listenerScope);
};
/** @override */
goog.events.EventTarget.prototype.hasListener = function(
opt_type, opt_capture) {
var id = goog.isDef(opt_type) ? String(opt_type) : undefined;
return this.eventTargetListeners_.hasListener(id, opt_capture);
};
/**
* Sets the target to be used for {@code event.target} when firing
* event. Mainly used for testing. For example, see
* {@code goog.testing.events.mixinListenable}.
* @param {!Object} target The target.
*/
goog.events.EventTarget.prototype.setTargetForTesting = function(target) {
this.actualEventTarget_ = target;
};
/**
* Asserts that the event target instance is initialized properly.
* @private
*/
goog.events.EventTarget.prototype.assertInitialized_ = function() {
goog.asserts.assert(
this.eventTargetListeners_,
'Event target is not initialized. Did you call the superclass ' +
'(goog.events.EventTarget) constructor?');
};
/**
* Dispatches the given event on the ancestorsTree.
*
* @param {!Object} target The target to dispatch on.
* @param {goog.events.Event|Object|string} e The event object.
* @param {Array<goog.events.Listenable>=} opt_ancestorsTree The ancestors
* tree of the target, in reverse order from the closest ancestor
* to the root event target. May be null if the target has no ancestor.
* @return {boolean} If anyone called preventDefault on the event object (or
* if any of the listeners returns false) this will also return false.
* @private
*/
goog.events.EventTarget.dispatchEventInternal_ = function(
target, e, opt_ancestorsTree) {
var type = e.type || /** @type {string} */ (e);
// If accepting a string or object, create a custom event object so that
// preventDefault and stopPropagation work with the event.
if (goog.isString(e)) {
e = new goog.events.Event(e, target);
} else if (!(e instanceof goog.events.Event)) {
var oldEvent = e;
e = new goog.events.Event(type, target);
goog.object.extend(e, oldEvent);
} else {
e.target = e.target || target;
}
var rv = true, currentTarget;
// Executes all capture listeners on the ancestors, if any.
if (opt_ancestorsTree) {
for (var i = opt_ancestorsTree.length - 1; !e.propagationStopped_ && i >= 0;
i--) {
currentTarget = e.currentTarget = opt_ancestorsTree[i];
rv = currentTarget.fireListeners(type, true, e) && rv;
}
}
// Executes capture and bubble listeners on the target.
if (!e.propagationStopped_) {
currentTarget = e.currentTarget = target;
rv = currentTarget.fireListeners(type, true, e) && rv;
if (!e.propagationStopped_) {
rv = currentTarget.fireListeners(type, false, e) && rv;
}
}
// Executes all bubble listeners on the ancestors, if any.
if (opt_ancestorsTree) {
for (i = 0; !e.propagationStopped_ && i < opt_ancestorsTree.length; i++) {
currentTarget = e.currentTarget = opt_ancestorsTree[i];
rv = currentTarget.fireListeners(type, false, e) && rv;
}
}
return rv;
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.EventTarget
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.EventTargetTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,72 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.EventTargetTest');
goog.setTestOnly('goog.events.EventTargetTest');
goog.require('goog.events.EventTarget');
goog.require('goog.events.Listenable');
goog.require('goog.events.eventTargetTester');
goog.require('goog.events.eventTargetTester.KeyType');
goog.require('goog.events.eventTargetTester.UnlistenReturnType');
goog.require('goog.testing.jsunit');
function setUp() {
var newListenableFn = function() {
return new goog.events.EventTarget();
};
var listenFn = function(src, type, listener, opt_capt, opt_handler) {
return src.listen(type, listener, opt_capt, opt_handler);
};
var unlistenFn = function(src, type, listener, opt_capt, opt_handler) {
return src.unlisten(type, listener, opt_capt, opt_handler);
};
var unlistenByKeyFn = function(src, key) {
return src.unlistenByKey(key);
};
var listenOnceFn = function(src, type, listener, opt_capt, opt_handler) {
return src.listenOnce(type, listener, opt_capt, opt_handler);
};
var dispatchEventFn = function(src, e) {
return src.dispatchEvent(e);
};
var removeAllFn = function(src, opt_type, opt_capture) {
return src.removeAllListeners(opt_type, opt_capture);
};
var getListenersFn = function(src, type, capture) {
return src.getListeners(type, capture);
};
var getListenerFn = function(src, type, listener, capture, opt_handler) {
return src.getListener(type, listener, capture, opt_handler);
};
var hasListenerFn = function(src, opt_type, opt_capture) {
return src.hasListener(opt_type, opt_capture);
};
goog.events.eventTargetTester.setUp(
newListenableFn, listenFn, unlistenFn, unlistenByKeyFn,
listenOnceFn, dispatchEventFn,
removeAllFn, getListenersFn, getListenerFn, hasListenerFn,
goog.events.eventTargetTester.KeyType.NUMBER,
goog.events.eventTargetTester.UnlistenReturnType.BOOLEAN, false);
}
function tearDown() {
goog.events.eventTargetTester.tearDown();
}
function testRuntimeTypeIsCorrect() {
var target = new goog.events.EventTarget();
assertTrue(goog.events.Listenable.isImplementedBy(target));
}
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.EventTarget
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.EventTargetGoogEventsTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,77 @@
// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.EventTargetGoogEventsTest');
goog.setTestOnly('goog.events.EventTargetGoogEventsTest');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.eventTargetTester');
goog.require('goog.events.eventTargetTester.KeyType');
goog.require('goog.events.eventTargetTester.UnlistenReturnType');
goog.require('goog.testing');
goog.require('goog.testing.jsunit');
function setUp() {
var newListenableFn = function() {
return new goog.events.EventTarget();
};
var unlistenByKeyFn = function(src, key) {
return goog.events.unlistenByKey(key);
};
goog.events.eventTargetTester.setUp(
newListenableFn, goog.events.listen, goog.events.unlisten,
unlistenByKeyFn, goog.events.listenOnce, goog.events.dispatchEvent,
goog.events.removeAll, goog.events.getListeners,
goog.events.getListener, goog.events.hasListener,
goog.events.eventTargetTester.KeyType.NUMBER,
goog.events.eventTargetTester.UnlistenReturnType.BOOLEAN, true);
}
function tearDown() {
goog.events.eventTargetTester.tearDown();
}
function testUnlistenProperCleanup() {
goog.events.listen(eventTargets[0], EventType.A, listeners[0]);
goog.events.unlisten(eventTargets[0], EventType.A, listeners[0]);
goog.events.listen(eventTargets[0], EventType.A, listeners[0]);
eventTargets[0].unlisten(EventType.A, listeners[0]);
}
function testUnlistenByKeyProperCleanup() {
var keyNum = goog.events.listen(eventTargets[0], EventType.A, listeners[0]);
goog.events.unlistenByKey(keyNum);
}
function testListenOnceProperCleanup() {
goog.events.listenOnce(eventTargets[0], EventType.A, listeners[0]);
eventTargets[0].dispatchEvent(EventType.A);
}
function testListenWithObject() {
var obj = {};
obj.handleEvent = goog.testing.recordFunction();
goog.events.listen(eventTargets[0], EventType.A, obj);
eventTargets[0].dispatchEvent(EventType.A);
assertEquals(1, obj.handleEvent.getCallCount());
}
function testListenWithObjectHandleEventReturningFalse() {
var obj = {};
obj.handleEvent = function() { return false; };
goog.events.listen(eventTargets[0], EventType.A, obj);
assertFalse(eventTargets[0].dispatchEvent(EventType.A));
}
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.EventTarget
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.EventTargetW3CTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,49 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.EventTargetW3CTest');
goog.setTestOnly('goog.events.EventTargetW3CTest');
goog.require('goog.events.EventTarget');
goog.require('goog.events.eventTargetTester');
goog.require('goog.events.eventTargetTester.KeyType');
goog.require('goog.events.eventTargetTester.UnlistenReturnType');
goog.require('goog.testing.jsunit');
function setUp() {
var newListenableFn = function() {
return new goog.events.EventTarget();
};
var listenFn = function(src, type, listener, opt_capt, opt_handler) {
src.addEventListener(type, listener, opt_capt, opt_handler);
};
var unlistenFn = function(src, type, listener, opt_capt, opt_handler) {
src.removeEventListener(type, listener, opt_capt, opt_handler);
};
var dispatchEventFn = function(src, e) {
return src.dispatchEvent(e);
};
goog.events.eventTargetTester.setUp(
newListenableFn, listenFn, unlistenFn, null /* unlistenByKeyFn */,
null /* listenOnceFn */, dispatchEventFn, null /* removeAllFn */,
null /* getListenersFn */, null /* getListenerFn */,
null /* hasListenerFn */,
goog.events.eventTargetTester.KeyType.UNDEFINED,
goog.events.eventTargetTester.UnlistenReturnType.UNDEFINED, true);
}
function tearDown() {
goog.events.eventTargetTester.tearDown();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,232 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Event Types.
*
* @author arv@google.com (Erik Arvidsson)
*/
goog.provide('goog.events.EventType');
goog.require('goog.userAgent');
/**
* Returns a prefixed event name for the current browser.
* @param {string} eventName The name of the event.
* @return {string} The prefixed event name.
* @suppress {missingRequire|missingProvide}
* @private
*/
goog.events.getVendorPrefixedName_ = function(eventName) {
return goog.userAgent.WEBKIT ? 'webkit' + eventName :
(goog.userAgent.OPERA ? 'o' + eventName.toLowerCase() :
eventName.toLowerCase());
};
/**
* Constants for event names.
* @enum {string}
*/
goog.events.EventType = {
// Mouse events
CLICK: 'click',
RIGHTCLICK: 'rightclick',
DBLCLICK: 'dblclick',
MOUSEDOWN: 'mousedown',
MOUSEUP: 'mouseup',
MOUSEOVER: 'mouseover',
MOUSEOUT: 'mouseout',
MOUSEMOVE: 'mousemove',
MOUSEENTER: 'mouseenter',
MOUSELEAVE: 'mouseleave',
// Select start is non-standard.
// See http://msdn.microsoft.com/en-us/library/ie/ms536969(v=vs.85).aspx.
SELECTSTART: 'selectstart', // IE, Safari, Chrome
// Wheel events
// http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
WHEEL: 'wheel',
// Key events
KEYPRESS: 'keypress',
KEYDOWN: 'keydown',
KEYUP: 'keyup',
// Focus
BLUR: 'blur',
FOCUS: 'focus',
DEACTIVATE: 'deactivate', // IE only
// NOTE: The following two events are not stable in cross-browser usage.
// WebKit and Opera implement DOMFocusIn/Out.
// IE implements focusin/out.
// Gecko implements neither see bug at
// https://bugzilla.mozilla.org/show_bug.cgi?id=396927.
// The DOM Events Level 3 Draft deprecates DOMFocusIn in favor of focusin:
// http://dev.w3.org/2006/webapi/DOM-Level-3-Events/html/DOM3-Events.html
// You can use FOCUS in Capture phase until implementations converge.
FOCUSIN: goog.userAgent.IE ? 'focusin' : 'DOMFocusIn',
FOCUSOUT: goog.userAgent.IE ? 'focusout' : 'DOMFocusOut',
// Forms
CHANGE: 'change',
SELECT: 'select',
SUBMIT: 'submit',
INPUT: 'input',
PROPERTYCHANGE: 'propertychange', // IE only
// Drag and drop
DRAGSTART: 'dragstart',
DRAG: 'drag',
DRAGENTER: 'dragenter',
DRAGOVER: 'dragover',
DRAGLEAVE: 'dragleave',
DROP: 'drop',
DRAGEND: 'dragend',
// Touch events
// Note that other touch events exist, but we should follow the W3C list here.
// http://www.w3.org/TR/touch-events/#list-of-touchevent-types
TOUCHSTART: 'touchstart',
TOUCHMOVE: 'touchmove',
TOUCHEND: 'touchend',
TOUCHCANCEL: 'touchcancel',
// Misc
BEFOREUNLOAD: 'beforeunload',
CONSOLEMESSAGE: 'consolemessage',
CONTEXTMENU: 'contextmenu',
DOMCONTENTLOADED: 'DOMContentLoaded',
ERROR: 'error',
HELP: 'help',
LOAD: 'load',
LOSECAPTURE: 'losecapture',
ORIENTATIONCHANGE: 'orientationchange',
READYSTATECHANGE: 'readystatechange',
RESIZE: 'resize',
SCROLL: 'scroll',
UNLOAD: 'unload',
// HTML 5 History events
// See http://www.w3.org/TR/html5/history.html#event-definitions
HASHCHANGE: 'hashchange',
PAGEHIDE: 'pagehide',
PAGESHOW: 'pageshow',
POPSTATE: 'popstate',
// Copy and Paste
// Support is limited. Make sure it works on your favorite browser
// before using.
// http://www.quirksmode.org/dom/events/cutcopypaste.html
COPY: 'copy',
PASTE: 'paste',
CUT: 'cut',
BEFORECOPY: 'beforecopy',
BEFORECUT: 'beforecut',
BEFOREPASTE: 'beforepaste',
// HTML5 online/offline events.
// http://www.w3.org/TR/offline-webapps/#related
ONLINE: 'online',
OFFLINE: 'offline',
// HTML 5 worker events
MESSAGE: 'message',
CONNECT: 'connect',
// CSS animation events.
/** @suppress {missingRequire} */
ANIMATIONSTART: goog.events.getVendorPrefixedName_('AnimationStart'),
/** @suppress {missingRequire} */
ANIMATIONEND: goog.events.getVendorPrefixedName_('AnimationEnd'),
/** @suppress {missingRequire} */
ANIMATIONITERATION: goog.events.getVendorPrefixedName_('AnimationIteration'),
// CSS transition events. Based on the browser support described at:
// https://developer.mozilla.org/en/css/css_transitions#Browser_compatibility
/** @suppress {missingRequire} */
TRANSITIONEND: goog.events.getVendorPrefixedName_('TransitionEnd'),
// W3C Pointer Events
// http://www.w3.org/TR/pointerevents/
POINTERDOWN: 'pointerdown',
POINTERUP: 'pointerup',
POINTERCANCEL: 'pointercancel',
POINTERMOVE: 'pointermove',
POINTEROVER: 'pointerover',
POINTEROUT: 'pointerout',
POINTERENTER: 'pointerenter',
POINTERLEAVE: 'pointerleave',
GOTPOINTERCAPTURE: 'gotpointercapture',
LOSTPOINTERCAPTURE: 'lostpointercapture',
// IE specific events.
// See http://msdn.microsoft.com/en-us/library/ie/hh772103(v=vs.85).aspx
// Note: these events will be supplanted in IE11.
MSGESTURECHANGE: 'MSGestureChange',
MSGESTUREEND: 'MSGestureEnd',
MSGESTUREHOLD: 'MSGestureHold',
MSGESTURESTART: 'MSGestureStart',
MSGESTURETAP: 'MSGestureTap',
MSGOTPOINTERCAPTURE: 'MSGotPointerCapture',
MSINERTIASTART: 'MSInertiaStart',
MSLOSTPOINTERCAPTURE: 'MSLostPointerCapture',
MSPOINTERCANCEL: 'MSPointerCancel',
MSPOINTERDOWN: 'MSPointerDown',
MSPOINTERENTER: 'MSPointerEnter',
MSPOINTERHOVER: 'MSPointerHover',
MSPOINTERLEAVE: 'MSPointerLeave',
MSPOINTERMOVE: 'MSPointerMove',
MSPOINTEROUT: 'MSPointerOut',
MSPOINTEROVER: 'MSPointerOver',
MSPOINTERUP: 'MSPointerUp',
// Native IMEs/input tools events.
TEXT: 'text',
TEXTINPUT: 'textInput',
COMPOSITIONSTART: 'compositionstart',
COMPOSITIONUPDATE: 'compositionupdate',
COMPOSITIONEND: 'compositionend',
// Webview tag events
// See http://developer.chrome.com/dev/apps/webview_tag.html
EXIT: 'exit',
LOADABORT: 'loadabort',
LOADCOMMIT: 'loadcommit',
LOADREDIRECT: 'loadredirect',
LOADSTART: 'loadstart',
LOADSTOP: 'loadstop',
RESPONSIVE: 'responsive',
SIZECHANGED: 'sizechanged',
UNRESPONSIVE: 'unresponsive',
// HTML5 Page Visibility API. See details at
// {@code goog.labs.dom.PageVisibilityMonitor}.
VISIBILITYCHANGE: 'visibilitychange',
// LocalStorage event.
STORAGE: 'storage',
// DOM Level 2 mutation events (deprecated).
DOMSUBTREEMODIFIED: 'DOMSubtreeModified',
DOMNODEINSERTED: 'DOMNodeInserted',
DOMNODEREMOVED: 'DOMNodeRemoved',
DOMNODEREMOVEDFROMDOCUMENT: 'DOMNodeRemovedFromDocument',
DOMNODEINSERTEDINTODOCUMENT: 'DOMNodeInsertedIntoDocument',
DOMATTRMODIFIED: 'DOMAttrModified',
DOMCHARACTERDATAMODIFIED: 'DOMCharacterDataModified'
};
@@ -0,0 +1,66 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Definition of the goog.events.EventWrapper interface.
*
* @author eae@google.com (Emil A Eklund)
*/
goog.provide('goog.events.EventWrapper');
/**
* Interface for event wrappers.
* @interface
*/
goog.events.EventWrapper = function() {
};
/**
* Adds an event listener using the wrapper on a DOM Node or an object that has
* implemented {@link goog.events.EventTarget}. A listener can only be added
* once to an object.
*
* @param {goog.events.ListenableType} src The node to listen to events on.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
* method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {Object=} opt_scope Element in whose scope to call the listener.
* @param {goog.events.EventHandler=} opt_eventHandler Event handler to add
* listener to.
*/
goog.events.EventWrapper.prototype.listen = function(src, listener, opt_capt,
opt_scope, opt_eventHandler) {
};
/**
* Removes an event listener added using goog.events.EventWrapper.listen.
*
* @param {goog.events.ListenableType} src The node to remove listener from.
* @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
* method, or an object with a handleEvent function.
* @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
* false).
* @param {Object=} opt_scope Element in whose scope to call the listener.
* @param {goog.events.EventHandler=} opt_eventHandler Event handler to remove
* listener from.
*/
goog.events.EventWrapper.prototype.unlisten = function(src, listener, opt_capt,
opt_scope, opt_eventHandler) {
};
@@ -0,0 +1,222 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides a files drag and drop event detector. It works on
* HTML5 browsers.
*
* @see ../demos/filedrophandler.html
*/
goog.provide('goog.events.FileDropHandler');
goog.provide('goog.events.FileDropHandler.EventType');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.log');
goog.require('goog.log.Level');
/**
* A files drag and drop event detector. Gets an {@code element} as parameter
* and fires {@code goog.events.FileDropHandler.EventType.DROP} event when files
* are dropped in the {@code element}.
*
* @param {Element|Document} element The element or document to listen on.
* @param {boolean=} opt_preventDropOutside Whether to prevent a drop on the
* area outside the {@code element}. Default false.
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.events.FileDropHandler = function(element, opt_preventDropOutside) {
goog.events.EventTarget.call(this);
/**
* Handler for drag/drop events.
* @type {!goog.events.EventHandler<!goog.events.FileDropHandler>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
var doc = element;
if (opt_preventDropOutside) {
doc = goog.dom.getOwnerDocument(element);
}
// Add dragenter listener to the owner document of the element.
this.eventHandler_.listen(doc,
goog.events.EventType.DRAGENTER,
this.onDocDragEnter_);
// Add dragover listener to the owner document of the element only if the
// document is not the element itself.
if (doc != element) {
this.eventHandler_.listen(doc,
goog.events.EventType.DRAGOVER,
this.onDocDragOver_);
}
// Add dragover and drop listeners to the element.
this.eventHandler_.listen(element,
goog.events.EventType.DRAGOVER,
this.onElemDragOver_);
this.eventHandler_.listen(element,
goog.events.EventType.DROP,
this.onElemDrop_);
};
goog.inherits(goog.events.FileDropHandler, goog.events.EventTarget);
/**
* Whether the drag event contains files. It is initialized only in the
* dragenter event. It is used in all the drag events to prevent default actions
* only if the drag contains files. Preventing default actions is necessary to
* go from dragenter to dragover and from dragover to drop. However we do not
* always want to prevent default actions, e.g. when the user drags text or
* links on a text area we should not prevent the browser default action that
* inserts the text in the text area. It is also necessary to stop propagation
* when handling drag events on the element to prevent them from propagating
* to the document.
* @private
* @type {boolean}
*/
goog.events.FileDropHandler.prototype.dndContainsFiles_ = false;
/**
* A logger, used to help us debug the algorithm.
* @type {goog.log.Logger}
* @private
*/
goog.events.FileDropHandler.prototype.logger_ =
goog.log.getLogger('goog.events.FileDropHandler');
/**
* The types of events fired by this class.
* @enum {string}
*/
goog.events.FileDropHandler.EventType = {
DROP: goog.events.EventType.DROP
};
/** @override */
goog.events.FileDropHandler.prototype.disposeInternal = function() {
goog.events.FileDropHandler.superClass_.disposeInternal.call(this);
this.eventHandler_.dispose();
};
/**
* Dispatches the DROP event.
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
goog.events.FileDropHandler.prototype.dispatch_ = function(e) {
goog.log.fine(this.logger_, 'Firing DROP event...');
var event = new goog.events.BrowserEvent(e.getBrowserEvent());
event.type = goog.events.FileDropHandler.EventType.DROP;
this.dispatchEvent(event);
};
/**
* Handles dragenter on the document.
* @param {goog.events.BrowserEvent} e The dragenter event.
* @private
*/
goog.events.FileDropHandler.prototype.onDocDragEnter_ = function(e) {
goog.log.log(this.logger_, goog.log.Level.FINER,
'"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
var dt = e.getBrowserEvent().dataTransfer;
// Check whether the drag event contains files.
this.dndContainsFiles_ = !!(dt &&
((dt.types &&
(goog.array.contains(dt.types, 'Files') ||
goog.array.contains(dt.types, 'public.file-url'))) ||
(dt.files && dt.files.length > 0)));
// If it does
if (this.dndContainsFiles_) {
// Prevent default actions.
e.preventDefault();
}
goog.log.log(this.logger_, goog.log.Level.FINER,
'dndContainsFiles_: ' + this.dndContainsFiles_);
};
/**
* Handles dragging something over the document.
* @param {goog.events.BrowserEvent} e The dragover event.
* @private
*/
goog.events.FileDropHandler.prototype.onDocDragOver_ = function(e) {
goog.log.log(this.logger_, goog.log.Level.FINEST,
'"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
if (this.dndContainsFiles_) {
// Prevent default actions.
e.preventDefault();
// Disable the drop on the document outside the drop zone.
var dt = e.getBrowserEvent().dataTransfer;
dt.dropEffect = 'none';
}
};
/**
* Handles dragging something over the element (drop zone).
* @param {goog.events.BrowserEvent} e The dragover event.
* @private
*/
goog.events.FileDropHandler.prototype.onElemDragOver_ = function(e) {
goog.log.log(this.logger_, goog.log.Level.FINEST,
'"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
if (this.dndContainsFiles_) {
// Prevent default actions and stop the event from propagating further to
// the document. Both lines are needed! (See comment above).
e.preventDefault();
e.stopPropagation();
// Allow the drop on the drop zone.
var dt = e.getBrowserEvent().dataTransfer;
dt.effectAllowed = 'all';
dt.dropEffect = 'copy';
}
};
/**
* Handles dropping something onto the element (drop zone).
* @param {goog.events.BrowserEvent} e The drop event.
* @private
*/
goog.events.FileDropHandler.prototype.onElemDrop_ = function(e) {
goog.log.log(this.logger_, goog.log.Level.FINER,
'"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
// If the drag and drop event contains files.
if (this.dndContainsFiles_) {
// Prevent default actions and stop the event from propagating further to
// the document. Both lines are needed! (See comment above).
e.preventDefault();
e.stopPropagation();
// Dispatch DROP event.
this.dispatch_(e);
}
};
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.FileDropHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.FileDropHandlerTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,250 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.FileDropHandlerTest');
goog.setTestOnly('goog.events.FileDropHandlerTest');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.FileDropHandler');
goog.require('goog.testing.jsunit');
var textarea;
var doc;
var handler;
var dnd;
var files;
function setUp() {
textarea = new goog.events.EventTarget();
doc = new goog.events.EventTarget();
textarea.ownerDocument = doc;
handler = new goog.events.FileDropHandler(textarea);
dnd = false;
files = null;
goog.events.listen(handler, goog.events.FileDropHandler.EventType.DROP,
function(e) {
dnd = true;
files =
e.getBrowserEvent().dataTransfer.files;
});
}
function tearDown() {
textarea.dispose();
doc.dispose();
handler.dispose();
}
function testOneFile() {
var preventDefault = false;
var expectedfiles = [{ fileName: 'file1.jpg' }];
var dt = { types: ['Files'], files: expectedfiles };
// Assert that default actions are prevented on dragenter.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: dt
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that default actions are prevented on dragover.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGOVER,
dataTransfer: dt
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that the drop effect is set to 'copy'.
assertEquals('copy', dt.dropEffect);
// Assert that default actions are prevented on drop.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DROP,
dataTransfer: dt
}));
assertTrue(preventDefault);
// Assert that DROP has been fired.
assertTrue(dnd);
assertEquals(1, files.length);
assertEquals(expectedfiles[0].fileName, files[0].fileName);
}
function testMultipleFiles() {
var preventDefault = false;
var expectedfiles = [{ fileName: 'file1.jpg' }, { fileName: 'file2.jpg' }];
var dt = { types: ['Files', 'text'], files: expectedfiles };
// Assert that default actions are prevented on dragenter.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: dt
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that default actions are prevented on dragover.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGOVER,
dataTransfer: dt
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that the drop effect is set to 'copy'.
assertEquals('copy', dt.dropEffect);
// Assert that default actions are prevented on drop.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DROP,
dataTransfer: dt
}));
assertTrue(preventDefault);
// Assert that DROP has been fired.
assertTrue(dnd);
assertEquals(2, files.length);
assertEquals(expectedfiles[0].fileName, files[0].fileName);
assertEquals(expectedfiles[1].fileName, files[1].fileName);
}
function testNoFiles() {
var preventDefault = false;
var dt = { types: ['text'] };
// Assert that default actions are not prevented on dragenter.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: dt
}));
assertFalse(preventDefault);
preventDefault = false;
// Assert that default actions are not prevented on dragover.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGOVER,
dataTransfer: dt
}));
assertFalse(preventDefault);
preventDefault = false;
// Assert that default actions are not prevented on drop.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DROP,
dataTransfer: dt
}));
assertFalse(preventDefault);
// Assert that DROP has not been fired.
assertFalse(dnd);
assertNull(files);
}
function testDragEnter() {
var preventDefault = false;
// Assert that default actions are prevented on dragenter.
// In Chrome the dragenter event has an empty file list and the types is
// set to 'Files'.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: { types: ['Files'], files: [] }
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that default actions are prevented on dragenter.
// In Safari 4 the dragenter event has an empty file list and the types is
// set to 'public.file-url'.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: { types: ['public.file-url'], files: [] }
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that default actions are not prevented on dragenter
// when the drag contains no files.
textarea.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: { types: ['text'], files: [] }
}));
assertFalse(preventDefault);
}
function testPreventDropOutside() {
var preventDefault = false;
var dt = { types: ['Files'], files: [{ fileName: 'file1.jpg' }] };
// Assert that default actions are not prevented on dragenter on the
// document outside the text area.
doc.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: dt
}));
assertFalse(preventDefault);
preventDefault = false;
// Assert that default actions are not prevented on dragover on the
// document outside the text area.
doc.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGOVER,
dataTransfer: dt
}));
assertFalse(preventDefault);
preventDefault = false;
handler.dispose();
// Create a new FileDropHandler that prevents drops outside the text area.
handler = new goog.events.FileDropHandler(textarea, true);
// Assert that default actions are now prevented on dragenter on the
// document outside the text area.
doc.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGENTER,
dataTransfer: dt
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert that default actions are now prevented on dragover on the
// document outside the text area.
doc.dispatchEvent(new goog.events.BrowserEvent({
preventDefault: function() { preventDefault = true; },
type: goog.events.EventType.DRAGOVER,
dataTransfer: dt
}));
assertTrue(preventDefault);
preventDefault = false;
// Assert also that the drop effect is set to 'none'.
assertEquals('none', dt.dropEffect);
}
@@ -0,0 +1,107 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This event handler allows you to catch focusin and focusout
* events on descendants. Unlike the "focus" and "blur" events which do not
* propagate consistently, and therefore must be added to the element that is
* focused, this allows you to attach one listener to an ancester and you will
* be notified when the focus state changes of ony of its descendants.
* @author arv@google.com (Erik Arvidsson)
* @see ../demos/focushandler.html
*/
goog.provide('goog.events.FocusHandler');
goog.provide('goog.events.FocusHandler.EventType');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.userAgent');
/**
* This event handler allows you to catch focus events when descendants gain or
* loses focus.
* @param {Element|Document} element The node to listen on.
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.events.FocusHandler = function(element) {
goog.events.EventTarget.call(this);
/**
* This is the element that we will listen to the real focus events on.
* @type {Element|Document}
* @private
*/
this.element_ = element;
// In IE we use focusin/focusout and in other browsers we use a capturing
// listner for focus/blur
var typeIn = goog.userAgent.IE ? 'focusin' : 'focus';
var typeOut = goog.userAgent.IE ? 'focusout' : 'blur';
/**
* Store the listen key so it easier to unlisten in dispose.
* @private
* @type {goog.events.Key}
*/
this.listenKeyIn_ =
goog.events.listen(this.element_, typeIn, this, !goog.userAgent.IE);
/**
* Store the listen key so it easier to unlisten in dispose.
* @private
* @type {goog.events.Key}
*/
this.listenKeyOut_ =
goog.events.listen(this.element_, typeOut, this, !goog.userAgent.IE);
};
goog.inherits(goog.events.FocusHandler, goog.events.EventTarget);
/**
* Enum type for the events fired by the focus handler
* @enum {string}
*/
goog.events.FocusHandler.EventType = {
FOCUSIN: 'focusin',
FOCUSOUT: 'focusout'
};
/**
* This handles the underlying events and dispatches a new event.
* @param {goog.events.BrowserEvent} e The underlying browser event.
*/
goog.events.FocusHandler.prototype.handleEvent = function(e) {
var be = e.getBrowserEvent();
var event = new goog.events.BrowserEvent(be);
event.type = e.type == 'focusin' || e.type == 'focus' ?
goog.events.FocusHandler.EventType.FOCUSIN :
goog.events.FocusHandler.EventType.FOCUSOUT;
this.dispatchEvent(event);
};
/** @override */
goog.events.FocusHandler.prototype.disposeInternal = function() {
goog.events.FocusHandler.superClass_.disposeInternal.call(this);
goog.events.unlistenByKey(this.listenKeyIn_);
goog.events.unlistenByKey(this.listenKeyOut_);
delete this.element_;
};
@@ -0,0 +1,369 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Input Method Editors (IMEs) are OS-level widgets that make
* it easier to type non-ascii characters on ascii keyboards (in particular,
* characters that require more than one keystroke).
*
* When the user wants to type such a character, a modal menu pops up and
* suggests possible "next" characters in the IME character sequence. After
* typing N characters, the user hits "enter" to commit the IME to the field.
* N differs from language to language.
*
* This class offers high-level events for how the user is interacting with the
* IME in editable regions.
*
* Known Issues:
*
* Firefox always fires an extra pair of compositionstart/compositionend events.
* We do not normalize for this.
*
* Opera does not fire any IME events.
*
* Spurious UPDATE events are common on all browsers.
*
* We currently do a bad job detecting when the IME closes on IE, and
* make a "best effort" guess on when we know it's closed.
*
* @author nicksantos@google.com (Nick Santos) (Ported to Closure)
*/
goog.provide('goog.events.ImeHandler');
goog.provide('goog.events.ImeHandler.Event');
goog.provide('goog.events.ImeHandler.EventType');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.userAgent');
/**
* Dispatches high-level events for IMEs.
* @param {Element} el The element to listen on.
* @extends {goog.events.EventTarget}
* @constructor
* @final
*/
goog.events.ImeHandler = function(el) {
goog.events.ImeHandler.base(this, 'constructor');
/**
* The element to listen on.
* @type {Element}
* @private
*/
this.el_ = el;
/**
* Tracks the keyup event only, because it has a different life-cycle from
* other events.
* @type {goog.events.EventHandler<!goog.events.ImeHandler>}
* @private
*/
this.keyUpHandler_ = new goog.events.EventHandler(this);
/**
* Tracks all the browser events.
* @type {goog.events.EventHandler<!goog.events.ImeHandler>}
* @private
*/
this.handler_ = new goog.events.EventHandler(this);
if (goog.events.ImeHandler.USES_COMPOSITION_EVENTS) {
this.handler_.
listen(el, goog.events.EventType.COMPOSITIONSTART,
this.handleCompositionStart_).
listen(el, goog.events.EventType.COMPOSITIONEND,
this.handleCompositionEnd_).
listen(el, goog.events.EventType.COMPOSITIONUPDATE,
this.handleTextModifyingInput_);
}
this.handler_.
listen(el, goog.events.EventType.TEXTINPUT, this.handleTextInput_).
listen(el, goog.events.EventType.TEXT, this.handleTextModifyingInput_).
listen(el, goog.events.EventType.KEYDOWN, this.handleKeyDown_);
};
goog.inherits(goog.events.ImeHandler, goog.events.EventTarget);
/**
* Event types fired by ImeHandler. These events do not make any guarantees
* about whether they were fired before or after the event in question.
* @enum {string}
*/
goog.events.ImeHandler.EventType = {
// After the IME opens.
START: 'startIme',
// An update to the state of the IME. An 'update' does not necessarily mean
// that the text contents of the field were modified in any way.
UPDATE: 'updateIme',
// After the IME closes.
END: 'endIme'
};
/**
* An event fired by ImeHandler.
* @param {goog.events.ImeHandler.EventType} type The type.
* @param {goog.events.BrowserEvent} reason The trigger for this event.
* @constructor
* @extends {goog.events.Event}
* @final
*/
goog.events.ImeHandler.Event = function(type, reason) {
goog.events.ImeHandler.Event.base(this, 'constructor', type);
/**
* The event that triggered this.
* @type {goog.events.BrowserEvent}
*/
this.reason = reason;
};
goog.inherits(goog.events.ImeHandler.Event, goog.events.Event);
/**
* Whether to use the composition events.
* @type {boolean}
*/
goog.events.ImeHandler.USES_COMPOSITION_EVENTS =
goog.userAgent.GECKO ||
(goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher(532));
/**
* Stores whether IME mode is active.
* @type {boolean}
* @private
*/
goog.events.ImeHandler.prototype.imeMode_ = false;
/**
* The keyCode value of the last keyDown event. This value is used for
* identiying whether or not a textInput event is sent by an IME.
* @type {number}
* @private
*/
goog.events.ImeHandler.prototype.lastKeyCode_ = 0;
/**
* @return {boolean} Whether an IME is active.
*/
goog.events.ImeHandler.prototype.isImeMode = function() {
return this.imeMode_;
};
/**
* Handles the compositionstart event.
* @param {goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleCompositionStart_ =
function(e) {
this.handleImeActivate_(e);
};
/**
* Handles the compositionend event.
* @param {goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleCompositionEnd_ = function(e) {
this.handleImeDeactivate_(e);
};
/**
* Handles the compositionupdate and text events.
* @param {goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleTextModifyingInput_ =
function(e) {
if (this.isImeMode()) {
this.processImeComposition_(e);
}
};
/**
* Handles IME activation.
* @param {goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleImeActivate_ = function(e) {
if (this.imeMode_) {
return;
}
// Listens for keyup events to handle unexpected IME keydown events on older
// versions of webkit.
//
// In those versions, we currently use textInput events deactivate IME
// (see handleTextInput_() for the reason). However,
// Safari fires a keydown event (as a result of pressing keys to commit IME
// text) with keyCode == WIN_IME after textInput event. This activates IME
// mode again unnecessarily. To prevent this problem, listens keyup events
// which can use to determine whether IME text has been committed.
if (goog.userAgent.WEBKIT &&
!goog.events.ImeHandler.USES_COMPOSITION_EVENTS) {
this.keyUpHandler_.listen(this.el_,
goog.events.EventType.KEYUP, this.handleKeyUpSafari4_);
}
this.imeMode_ = true;
this.dispatchEvent(
new goog.events.ImeHandler.Event(
goog.events.ImeHandler.EventType.START, e));
};
/**
* Handles the IME compose changes.
* @param {goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.processImeComposition_ = function(e) {
this.dispatchEvent(
new goog.events.ImeHandler.Event(
goog.events.ImeHandler.EventType.UPDATE, e));
};
/**
* Handles IME deactivation.
* @param {goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleImeDeactivate_ = function(e) {
this.imeMode_ = false;
this.keyUpHandler_.removeAll();
this.dispatchEvent(
new goog.events.ImeHandler.Event(
goog.events.ImeHandler.EventType.END, e));
};
/**
* Handles a key down event.
* @param {!goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleKeyDown_ = function(e) {
// Firefox and Chrome have a separate event for IME composition ('text'
// and 'compositionupdate', respectively), other browsers do not.
if (!goog.events.ImeHandler.USES_COMPOSITION_EVENTS) {
var imeMode = this.isImeMode();
// If we're in IE and we detect an IME input on keyDown then activate
// the IME, otherwise if the imeMode was previously active, deactivate.
if (!imeMode && e.keyCode == goog.events.KeyCodes.WIN_IME) {
this.handleImeActivate_(e);
} else if (imeMode && e.keyCode != goog.events.KeyCodes.WIN_IME) {
if (goog.events.ImeHandler.isImeDeactivateKeyEvent_(e)) {
this.handleImeDeactivate_(e);
}
} else if (imeMode) {
this.processImeComposition_(e);
}
}
// Safari on Mac doesn't send IME events in the right order so that we must
// ignore some modifier key events to insert IME text correctly.
if (goog.events.ImeHandler.isImeDeactivateKeyEvent_(e)) {
this.lastKeyCode_ = e.keyCode;
}
};
/**
* Handles a textInput event.
* @param {!goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleTextInput_ = function(e) {
// Some WebKit-based browsers including Safari 4 don't send composition
// events. So, we turn down IME mode when it's still there.
if (!goog.events.ImeHandler.USES_COMPOSITION_EVENTS &&
goog.userAgent.WEBKIT &&
this.lastKeyCode_ == goog.events.KeyCodes.WIN_IME &&
this.isImeMode()) {
this.handleImeDeactivate_(e);
}
};
/**
* Handles the key up event for any IME activity. This handler is just used to
* prevent activating IME unnecessary in Safari at this time.
* @param {!goog.events.BrowserEvent} e The event.
* @private
*/
goog.events.ImeHandler.prototype.handleKeyUpSafari4_ = function(e) {
if (this.isImeMode()) {
switch (e.keyCode) {
// These keyup events indicates that IME text has been committed or
// cancelled. We should turn off IME mode when these keyup events
// received.
case goog.events.KeyCodes.ENTER:
case goog.events.KeyCodes.TAB:
case goog.events.KeyCodes.ESC:
this.handleImeDeactivate_(e);
break;
}
}
};
/**
* Returns whether the given event should be treated as an IME
* deactivation trigger.
* @param {!goog.events.Event} e The event.
* @return {boolean} Whether the given event is an IME deactivate trigger.
* @private
*/
goog.events.ImeHandler.isImeDeactivateKeyEvent_ = function(e) {
// Which key events involve IME deactivation depends on the user's
// environment (i.e. browsers, platforms, and IMEs). Usually Shift key
// and Ctrl key does not involve IME deactivation, so we currently assume
// that these keys are not IME deactivation trigger.
switch (e.keyCode) {
case goog.events.KeyCodes.SHIFT:
case goog.events.KeyCodes.CTRL:
return false;
default:
return true;
}
};
/** @override */
goog.events.ImeHandler.prototype.disposeInternal = function() {
this.handler_.dispose();
this.keyUpHandler_.dispose();
this.el_ = null;
goog.events.ImeHandler.base(this, 'disposeInternal');
};
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
Author: nicksantos@google.com (Nick Santos) (Ported to Closure)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
JsUnit tests for goog.events.ImeHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.ImeHandlerTest');
</script>
</head>
<body>
<div id="sandbox">
</div>
<div>
<b>
Last 10 events:
</b>
<div id="logger" style="padding: 0.5em;">
</div>
</div>
</body>
</html>
@@ -0,0 +1,266 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.ImeHandlerTest');
goog.setTestOnly('goog.events.ImeHandlerTest');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.ImeHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var sandbox;
var imeHandler;
var eventsFired;
var stubs = new goog.testing.PropertyReplacer();
var eventTypes = goog.events.ImeHandler.EventType;
function setUp() {
sandbox = goog.dom.getElement('sandbox');
}
function initImeHandler() {
goog.events.ImeHandler.USES_COMPOSITION_EVENTS =
goog.userAgent.GECKO ||
(goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher(532));
imeHandler = new goog.events.ImeHandler(sandbox);
eventsFired = [];
goog.events.listen(
imeHandler,
goog.object.getValues(goog.events.ImeHandler.EventType),
function(e) {
eventsFired.push(e.type);
});
}
function tearDown() {
imeHandler.dispose();
imeHandler = null;
stubs.reset();
}
function tearDownPage() {
// Set up a test bed.
sandbox.innerHTML = '<div contentEditable="true">hello world</div>';
initImeHandler();
function unshiftEvent(e) {
last10Events.unshift(e.type + ':' + e.keyCode + ':' +
goog.string.htmlEscape(goog.dom.getTextContent(sandbox)));
last10Events.length = Math.min(last10Events.length, 10);
goog.dom.getElement('logger').innerHTML = last10Events.join('<br>');
}
var last10Events = [];
goog.events.listen(
imeHandler,
goog.object.getValues(goog.events.ImeHandler.EventType),
unshiftEvent);
goog.events.listen(
sandbox,
['keydown', 'textInput'],
unshiftEvent);
}
function assertEventsFired(var_args) {
assertArrayEquals(
goog.array.clone(arguments), eventsFired);
}
function fireInputEvent(type) {
return goog.testing.events.fireBrowserEvent(
new goog.testing.events.Event(type, sandbox));
}
function fireImeKeySequence() {
return fireKeySequence(goog.events.KeyCodes.WIN_IME);
}
function fireKeySequence(keyCode) {
return (
goog.testing.events.fireBrowserEvent(
new goog.testing.events.Event('textInput', sandbox)) &
goog.testing.events.fireKeySequence(
sandbox, keyCode));
}
function testHandleKeyDown_GeckoCompositionEvents() {
// This test verifies that our IME functions can dispatch IME events to
// InputHandler in the expected order on Gecko.
// Set the userAgent used for this test to Firefox.
setUserAgent('GECKO');
stubs.set(goog.userAgent, 'MAC', false);
initImeHandler();
fireInputEvent('compositionstart');
assertImeMode();
fireInputEvent('compositionupdate');
fireInputEvent('compositionupdate');
fireInputEvent('compositionend');
assertEventsFired(
eventTypes.START, eventTypes.UPDATE, eventTypes.UPDATE, eventTypes.END);
assertNotImeMode();
}
/**
* Verifies that our IME functions can dispatch IME events to the input handler
* in the expected order on Chrome. jsUnitFarm does not have Linux Chrome or
* Mac Chrome. So, we manually change the platform and run this test three
* times.
*/
function testChromeCompositionEventsLinux() {
runChromeCompositionEvents('LINUX');
}
function testChromeCompositionEventsMac() {
runChromeCompositionEvents('MAC');
}
function testChromeCompositionEventsWindows() {
runChromeCompositionEvents('WINDOWS');
}
function runChromeCompositionEvents(platform) {
setUserAgent('WEBKIT');
setVersion(532);
stubs.set(goog.userAgent, platform, true);
initImeHandler();
fireImeKeySequence();
fireInputEvent('compositionstart');
assertImeMode();
fireInputEvent('compositionupdate');
fireInputEvent('compositionupdate');
fireInputEvent('compositionend');
assertEventsFired(
eventTypes.START, eventTypes.UPDATE, eventTypes.UPDATE, eventTypes.END);
assertNotImeMode();
}
/**
* Ensures that the IME mode turn on/off correctly.
*/
function testHandlerKeyDownForIme_imeOnOff() {
setUserAgent('IE');
initImeHandler();
// Send a WIN_IME keyDown event and see whether IME mode turns on.
fireImeKeySequence();
assertImeMode();
// Send keyDown events which should not turn off IME mode and see whether
// IME mode holds on.
fireKeySequence(goog.events.KeyCodes.SHIFT);
assertImeMode();
fireKeySequence(goog.events.KeyCodes.CTRL);
assertImeMode();
// Send a keyDown event with keyCode = ENTER and see whether IME mode
// turns off.
fireKeySequence(goog.events.KeyCodes.ENTER);
assertNotImeMode();
assertEventsFired(
eventTypes.START, eventTypes.END);
}
/**
* Ensures that IME mode turns off when keyup events which are involved
* in commiting IME text occurred in Safari.
*/
function testHandleKeyUpForSafari() {
setUserAgent('WEBKIT');
setVersion(531);
initImeHandler();
fireImeKeySequence();
assertImeMode();
fireKeySequence(goog.events.KeyCodes.ENTER);
assertNotImeMode();
}
/**
* SCIM on Linux will fire WIN_IME keycodes for random characters.
* Fortunately, all Linux-based browsers use composition events.
* This test just verifies that we ignore the WIN_IME keycodes.
*/
function testScimFiresWinImeKeycodesGeckoLinux() {
setUserAgent('GECKO');
assertScimInputIgnored();
}
function testScimFiresWinImeKeycodesChromeLinux() {
setUserAgent('WEBKIT');
setVersion(532);
assertScimInputIgnored();
}
function assertScimInputIgnored() {
initImeHandler();
fireImeKeySequence();
assertNotImeMode();
fireInputEvent('compositionstart');
assertImeMode();
fireImeKeySequence();
assertImeMode();
fireInputEvent('compositionend');
assertNotImeMode();
}
var userAgents = ['IE', 'GECKO', 'WEBKIT'];
function setUserAgent(userAgent) {
for (var i = 0; i < userAgents.length; i++) {
stubs.set(goog.userAgent, userAgents[i], userAgents[i] == userAgent);
}
}
function setVersion(version) {
goog.userAgent.VERSION = version;
goog.userAgent.isVersionOrHigherCache_ = {};
}
function assertImeMode() {
assertTrue('Should be in IME mode.', imeHandler.isImeMode());
}
function assertNotImeMode() {
assertFalse('Should not be in IME mode.', imeHandler.isImeMode());
}
@@ -0,0 +1,220 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview An object that encapsulates text changed events for textareas
* and input element of type text and password. The event occurs after the value
* has been changed. The event does not occur if value was changed
* programmatically.<br>
* <br>
* Note: this does not guarantee the correctness of {@code keyCode} or
* {@code charCode}, or attempt to unify them across browsers. See
* {@code goog.events.KeyHandler} for that functionality<br>
* <br>
* Known issues:
* <ul>
* <li>Does not trigger for drop events on Opera due to browser bug.
* <li>IE doesn't have native support for input event. WebKit before version 531
* doesn't have support for textareas. For those browsers an emulation mode
* based on key, clipboard and drop events is used. Thus this event won't
* trigger in emulation mode if text was modified by context menu commands
* such as 'Undo' and 'Delete'.
* </ul>
* @author arv@google.com (Erik Arvidsson)
* @see ../demos/inputhandler.html
*/
goog.provide('goog.events.InputHandler');
goog.provide('goog.events.InputHandler.EventType');
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.KeyCodes');
goog.require('goog.userAgent');
/**
* This event handler will dispatch events when the user types into a text
* input, password input or a textarea
* @param {Element} element The element that you want to listen for input
* events on.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.events.InputHandler = function(element) {
goog.events.InputHandler.base(this, 'constructor');
/**
* Id of a timer used to postpone firing input event in emulation mode.
* @type {?number}
* @private
*/
this.timer_ = null;
/**
* The element that you want to listen for input events on.
* @type {Element}
* @private
*/
this.element_ = element;
// Determine whether input event should be emulated.
// IE8 doesn't support input events. We could use property change events but
// they are broken in many ways:
// - Fire even if value was changed programmatically.
// - Aren't always delivered. For example, if you change value or even width
// of input programmatically, next value change made by user won't fire an
// event.
// IE9 supports input events when characters are inserted, but not deleted.
// WebKit before version 531 did not support input events for textareas.
var emulateInputEvents = goog.userAgent.IE ||
(goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('531') &&
element.tagName == 'TEXTAREA');
/**
* @type {goog.events.EventHandler<!goog.events.InputHandler>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
// Even if input event emulation is enabled, still listen for input events
// since they may be partially supported by the browser (such as IE9).
// If the input event does fire, we will be able to dispatch synchronously.
// (InputHandler events being asynchronous for IE is a common issue for
// cases like auto-grow textareas where they result in a quick flash of
// scrollbars between the textarea content growing and it being resized to
// fit.)
this.eventHandler_.listen(
this.element_,
emulateInputEvents ?
['keydown', 'paste', 'cut', 'drop', 'input'] :
'input',
this);
};
goog.inherits(goog.events.InputHandler, goog.events.EventTarget);
/**
* Enum type for the events fired by the input handler
* @enum {string}
*/
goog.events.InputHandler.EventType = {
INPUT: 'input'
};
/**
* This handles the underlying events and dispatches a new event as needed.
* @param {goog.events.BrowserEvent} e The underlying browser event.
*/
goog.events.InputHandler.prototype.handleEvent = function(e) {
if (e.type == 'input') {
// http://stackoverflow.com/questions/18389732/changing-placeholder-triggers-input-event-in-ie-10
// IE 10+ fires an input event when there are inputs with placeholders.
// It fires the event with keycode 0, so if we detect it we don't
// propagate the input event.
if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher(10) &&
e.keyCode == 0 && e.charCode == 0) {
return;
}
// This event happens after all the other events we listen to, so cancel
// an asynchronous event dispatch if we have it queued up. Otherwise, we
// will end up firing an extra event.
this.cancelTimerIfSet_();
// Unlike other browsers, Opera fires an extra input event when an element
// is blurred after the user has input into it. Since Opera doesn't fire
// input event on drop, it's enough to check whether element still has focus
// to suppress bogus notification.
if (!goog.userAgent.OPERA || this.element_ ==
goog.dom.getOwnerDocument(this.element_).activeElement) {
this.dispatchEvent(this.createInputEvent_(e));
}
} else {
// Filter out key events that don't modify text.
if (e.type == 'keydown' &&
!goog.events.KeyCodes.isTextModifyingKeyEvent(e)) {
return;
}
// It is still possible that pressed key won't modify the value of an
// element. Storing old value will help us to detect modification but is
// also a little bit dangerous. If value is changed programmatically in
// another key down handler, we will detect it as user-initiated change.
var valueBeforeKey = e.type == 'keydown' ? this.element_.value : null;
// In IE on XP, IME the element's value has already changed when we get
// keydown events when the user is using an IME. In this case, we can't
// check the current value normally, so we assume that it's a modifying key
// event. This means that ENTER when used to commit will fire a spurious
// input event, but it's better to have a false positive than let some input
// slip through the cracks.
if (goog.userAgent.IE && e.keyCode == goog.events.KeyCodes.WIN_IME) {
valueBeforeKey = null;
}
// Create an input event now, because when we fire it on timer, the
// underlying event will already be disposed.
var inputEvent = this.createInputEvent_(e);
// Since key down, paste, cut and drop events are fired before actual value
// of the element has changed, we need to postpone dispatching input event
// until value is updated.
this.cancelTimerIfSet_();
this.timer_ = goog.Timer.callOnce(function() {
this.timer_ = null;
if (this.element_.value != valueBeforeKey) {
this.dispatchEvent(inputEvent);
}
}, 0, this);
}
};
/**
* Cancels timer if it is set, does nothing otherwise.
* @private
*/
goog.events.InputHandler.prototype.cancelTimerIfSet_ = function() {
if (this.timer_ != null) {
goog.Timer.clear(this.timer_);
this.timer_ = null;
}
};
/**
* Creates an input event from the browser event.
* @param {goog.events.BrowserEvent} be A browser event.
* @return {!goog.events.BrowserEvent} An input event.
* @private
*/
goog.events.InputHandler.prototype.createInputEvent_ = function(be) {
var e = new goog.events.BrowserEvent(be.getBrowserEvent());
e.type = goog.events.InputHandler.EventType.INPUT;
return e;
};
/** @override */
goog.events.InputHandler.prototype.disposeInternal = function() {
goog.events.InputHandler.base(this, 'disposeInternal');
this.eventHandler_.dispose();
this.cancelTimerIfSet_();
delete this.element_;
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Input Handler Test
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.InputHandlerTest');
</script>
</head>
<body>
<input type="text" placeholder="Foo" id="input-w-placeholder" />
</body>
</html>
@@ -0,0 +1,95 @@
// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.InputHandlerTest');
goog.setTestOnly('goog.events.InputHandlerTest');
goog.require('goog.dom');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.events.InputHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.require('goog.userAgent');
var inputHandler;
var eventHandler;
function setUp() {
eventHandler = new goog.events.EventHandler();
}
function tearDown() {
goog.dispose(inputHandler);
goog.dispose(eventHandler);
}
function testInputWithPlaceholder() {
var input = goog.dom.getElement('input-w-placeholder');
inputHandler = new goog.events.InputHandler(input);
var callback = listenToInput(inputHandler);
fireFakeInputEvent(input);
assertEquals(0, callback.getCallCount());
}
function testInputWithPlaceholder_withValue() {
var input = goog.dom.getElement('input-w-placeholder');
inputHandler = new goog.events.InputHandler(input);
var callback = listenToInput(inputHandler);
input.value = 'foo';
fireFakeInputEvent(input);
assertEquals(0, callback.getCallCount());
}
function testInputWithPlaceholder_someKeys() {
var input = goog.dom.getElement('input-w-placeholder');
inputHandler = new goog.events.InputHandler(input);
var callback = listenToInput(inputHandler);
input.focus();
input.value = 'foo';
fireInputEvent(input, goog.events.KeyCodes.M);
assertEquals(1, callback.getCallCount());
}
function listenToInput(inputHandler) {
var callback = goog.testing.recordFunction();
eventHandler.listen(
inputHandler,
goog.events.InputHandler.EventType.INPUT,
callback);
return callback;
}
function fireFakeInputEvent(input) {
// Simulate the input event that IE fires on focus when a placeholder
// is present.
input.focus();
if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher(10)) {
// IE fires an input event with keycode 0
fireInputEvent(input, 0);
}
}
function fireInputEvent(input, keyCode) {
var inputEvent = new goog.testing.events.Event(
goog.events.EventType.INPUT,
input);
inputEvent.keyCode = keyCode;
inputEvent.charCode = keyCode;
goog.testing.events.fireBrowserEvent(inputEvent);
}
@@ -0,0 +1,415 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Constant declarations for common key codes.
*
* @author eae@google.com (Emil A Eklund)
* @see ../demos/keyhandler.html
*/
goog.provide('goog.events.KeyCodes');
goog.require('goog.userAgent');
/**
* Key codes for common characters.
*
* This list is not localized and therefore some of the key codes are not
* correct for non US keyboard layouts. See comments below.
*
* @enum {number}
*/
goog.events.KeyCodes = {
WIN_KEY_FF_LINUX: 0,
MAC_ENTER: 3,
BACKSPACE: 8,
TAB: 9,
NUM_CENTER: 12, // NUMLOCK on FF/Safari Mac
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
PAUSE: 19,
CAPS_LOCK: 20,
ESC: 27,
SPACE: 32,
PAGE_UP: 33, // also NUM_NORTH_EAST
PAGE_DOWN: 34, // also NUM_SOUTH_EAST
END: 35, // also NUM_SOUTH_WEST
HOME: 36, // also NUM_NORTH_WEST
LEFT: 37, // also NUM_WEST
UP: 38, // also NUM_NORTH
RIGHT: 39, // also NUM_EAST
DOWN: 40, // also NUM_SOUTH
PRINT_SCREEN: 44,
INSERT: 45, // also NUM_INSERT
DELETE: 46, // also NUM_DELETE
ZERO: 48,
ONE: 49,
TWO: 50,
THREE: 51,
FOUR: 52,
FIVE: 53,
SIX: 54,
SEVEN: 55,
EIGHT: 56,
NINE: 57,
FF_SEMICOLON: 59, // Firefox (Gecko) fires this for semicolon instead of 186
FF_EQUALS: 61, // Firefox (Gecko) fires this for equals instead of 187
FF_DASH: 173, // Firefox (Gecko) fires this for dash instead of 189
QUESTION_MARK: 63, // needs localization
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
META: 91, // WIN_KEY_LEFT
WIN_KEY_RIGHT: 92,
CONTEXT_MENU: 93,
NUM_ZERO: 96,
NUM_ONE: 97,
NUM_TWO: 98,
NUM_THREE: 99,
NUM_FOUR: 100,
NUM_FIVE: 101,
NUM_SIX: 102,
NUM_SEVEN: 103,
NUM_EIGHT: 104,
NUM_NINE: 105,
NUM_MULTIPLY: 106,
NUM_PLUS: 107,
NUM_MINUS: 109,
NUM_PERIOD: 110,
NUM_DIVISION: 111,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
NUMLOCK: 144,
SCROLL_LOCK: 145,
// OS-specific media keys like volume controls and browser controls.
FIRST_MEDIA_KEY: 166,
LAST_MEDIA_KEY: 183,
SEMICOLON: 186, // needs localization
DASH: 189, // needs localization
EQUALS: 187, // needs localization
COMMA: 188, // needs localization
PERIOD: 190, // needs localization
SLASH: 191, // needs localization
APOSTROPHE: 192, // needs localization
TILDE: 192, // needs localization
SINGLE_QUOTE: 222, // needs localization
OPEN_SQUARE_BRACKET: 219, // needs localization
BACKSLASH: 220, // needs localization
CLOSE_SQUARE_BRACKET: 221, // needs localization
WIN_KEY: 224,
MAC_FF_META: 224, // Firefox (Gecko) fires this for the meta key instead of 91
MAC_WK_CMD_LEFT: 91, // WebKit Left Command key fired, same as META
MAC_WK_CMD_RIGHT: 93, // WebKit Right Command key fired, different from META
WIN_IME: 229,
// We've seen users whose machines fire this keycode at regular one
// second intervals. The common thread among these users is that
// they're all using Dell Inspiron laptops, so we suspect that this
// indicates a hardware/bios problem.
// http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
PHANTOM: 255
};
/**
* Returns true if the event contains a text modifying key.
* @param {goog.events.BrowserEvent} e A key event.
* @return {boolean} Whether it's a text modifying key.
*/
goog.events.KeyCodes.isTextModifyingKeyEvent = function(e) {
if (e.altKey && !e.ctrlKey ||
e.metaKey ||
// Function keys don't generate text
e.keyCode >= goog.events.KeyCodes.F1 &&
e.keyCode <= goog.events.KeyCodes.F12) {
return false;
}
// The following keys are quite harmless, even in combination with
// CTRL, ALT or SHIFT.
switch (e.keyCode) {
case goog.events.KeyCodes.ALT:
case goog.events.KeyCodes.CAPS_LOCK:
case goog.events.KeyCodes.CONTEXT_MENU:
case goog.events.KeyCodes.CTRL:
case goog.events.KeyCodes.DOWN:
case goog.events.KeyCodes.END:
case goog.events.KeyCodes.ESC:
case goog.events.KeyCodes.HOME:
case goog.events.KeyCodes.INSERT:
case goog.events.KeyCodes.LEFT:
case goog.events.KeyCodes.MAC_FF_META:
case goog.events.KeyCodes.META:
case goog.events.KeyCodes.NUMLOCK:
case goog.events.KeyCodes.NUM_CENTER:
case goog.events.KeyCodes.PAGE_DOWN:
case goog.events.KeyCodes.PAGE_UP:
case goog.events.KeyCodes.PAUSE:
case goog.events.KeyCodes.PHANTOM:
case goog.events.KeyCodes.PRINT_SCREEN:
case goog.events.KeyCodes.RIGHT:
case goog.events.KeyCodes.SCROLL_LOCK:
case goog.events.KeyCodes.SHIFT:
case goog.events.KeyCodes.UP:
case goog.events.KeyCodes.WIN_KEY:
case goog.events.KeyCodes.WIN_KEY_RIGHT:
return false;
case goog.events.KeyCodes.WIN_KEY_FF_LINUX:
return !goog.userAgent.GECKO;
default:
return e.keyCode < goog.events.KeyCodes.FIRST_MEDIA_KEY ||
e.keyCode > goog.events.KeyCodes.LAST_MEDIA_KEY;
}
};
/**
* Returns true if the key fires a keypress event in the current browser.
*
* Accoridng to MSDN [1] IE only fires keypress events for the following keys:
* - Letters: A - Z (uppercase and lowercase)
* - Numerals: 0 - 9
* - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
* - System: ESC, SPACEBAR, ENTER
*
* That's not entirely correct though, for instance there's no distinction
* between upper and lower case letters.
*
* [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
*
* Safari is similar to IE, but does not fire keypress for ESC.
*
* Additionally, IE6 does not fire keydown or keypress events for letters when
* the control or alt keys are held down and the shift key is not. IE7 does
* fire keydown in these cases, though, but not keypress.
*
* @param {number} keyCode A key code.
* @param {number=} opt_heldKeyCode Key code of a currently-held key.
* @param {boolean=} opt_shiftKey Whether the shift key is held down.
* @param {boolean=} opt_ctrlKey Whether the control key is held down.
* @param {boolean=} opt_altKey Whether the alt key is held down.
* @return {boolean} Whether it's a key that fires a keypress event.
*/
goog.events.KeyCodes.firesKeyPressEvent = function(keyCode, opt_heldKeyCode,
opt_shiftKey, opt_ctrlKey, opt_altKey) {
if (!goog.userAgent.IE &&
!(goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('525'))) {
return true;
}
if (goog.userAgent.MAC && opt_altKey) {
return goog.events.KeyCodes.isCharacterKey(keyCode);
}
// Alt but not AltGr which is represented as Alt+Ctrl.
if (opt_altKey && !opt_ctrlKey) {
return false;
}
// Saves Ctrl or Alt + key for IE and WebKit 525+, which won't fire keypress.
// Non-IE browsers and WebKit prior to 525 won't get this far so no need to
// check the user agent.
if (goog.isNumber(opt_heldKeyCode)) {
opt_heldKeyCode = goog.events.KeyCodes.normalizeKeyCode(opt_heldKeyCode);
}
if (!opt_shiftKey &&
(opt_heldKeyCode == goog.events.KeyCodes.CTRL ||
opt_heldKeyCode == goog.events.KeyCodes.ALT ||
goog.userAgent.MAC &&
opt_heldKeyCode == goog.events.KeyCodes.META)) {
return false;
}
// Some keys with Ctrl/Shift do not issue keypress in WEBKIT.
if (goog.userAgent.WEBKIT && opt_ctrlKey && opt_shiftKey) {
switch (keyCode) {
case goog.events.KeyCodes.BACKSLASH:
case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
case goog.events.KeyCodes.TILDE:
case goog.events.KeyCodes.SEMICOLON:
case goog.events.KeyCodes.DASH:
case goog.events.KeyCodes.EQUALS:
case goog.events.KeyCodes.COMMA:
case goog.events.KeyCodes.PERIOD:
case goog.events.KeyCodes.SLASH:
case goog.events.KeyCodes.APOSTROPHE:
case goog.events.KeyCodes.SINGLE_QUOTE:
return false;
}
}
// When Ctrl+<somekey> is held in IE, it only fires a keypress once, but it
// continues to fire keydown events as the event repeats.
if (goog.userAgent.IE && opt_ctrlKey && opt_heldKeyCode == keyCode) {
return false;
}
switch (keyCode) {
case goog.events.KeyCodes.ENTER:
return true;
case goog.events.KeyCodes.ESC:
return !goog.userAgent.WEBKIT;
}
return goog.events.KeyCodes.isCharacterKey(keyCode);
};
/**
* Returns true if the key produces a character.
* This does not cover characters on non-US keyboards (Russian, Hebrew, etc.).
*
* @param {number} keyCode A key code.
* @return {boolean} Whether it's a character key.
*/
goog.events.KeyCodes.isCharacterKey = function(keyCode) {
if (keyCode >= goog.events.KeyCodes.ZERO &&
keyCode <= goog.events.KeyCodes.NINE) {
return true;
}
if (keyCode >= goog.events.KeyCodes.NUM_ZERO &&
keyCode <= goog.events.KeyCodes.NUM_MULTIPLY) {
return true;
}
if (keyCode >= goog.events.KeyCodes.A &&
keyCode <= goog.events.KeyCodes.Z) {
return true;
}
// Safari sends zero key code for non-latin characters.
if (goog.userAgent.WEBKIT && keyCode == 0) {
return true;
}
switch (keyCode) {
case goog.events.KeyCodes.SPACE:
case goog.events.KeyCodes.QUESTION_MARK:
case goog.events.KeyCodes.NUM_PLUS:
case goog.events.KeyCodes.NUM_MINUS:
case goog.events.KeyCodes.NUM_PERIOD:
case goog.events.KeyCodes.NUM_DIVISION:
case goog.events.KeyCodes.SEMICOLON:
case goog.events.KeyCodes.FF_SEMICOLON:
case goog.events.KeyCodes.DASH:
case goog.events.KeyCodes.EQUALS:
case goog.events.KeyCodes.FF_EQUALS:
case goog.events.KeyCodes.COMMA:
case goog.events.KeyCodes.PERIOD:
case goog.events.KeyCodes.SLASH:
case goog.events.KeyCodes.APOSTROPHE:
case goog.events.KeyCodes.SINGLE_QUOTE:
case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
case goog.events.KeyCodes.BACKSLASH:
case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
return true;
default:
return false;
}
};
/**
* Normalizes key codes from OS/Browser-specific value to the general one.
* @param {number} keyCode The native key code.
* @return {number} The normalized key code.
*/
goog.events.KeyCodes.normalizeKeyCode = function(keyCode) {
if (goog.userAgent.GECKO) {
return goog.events.KeyCodes.normalizeGeckoKeyCode(keyCode);
} else if (goog.userAgent.MAC && goog.userAgent.WEBKIT) {
return goog.events.KeyCodes.normalizeMacWebKitKeyCode(keyCode);
} else {
return keyCode;
}
};
/**
* Normalizes key codes from their Gecko-specific value to the general one.
* @param {number} keyCode The native key code.
* @return {number} The normalized key code.
*/
goog.events.KeyCodes.normalizeGeckoKeyCode = function(keyCode) {
switch (keyCode) {
case goog.events.KeyCodes.FF_EQUALS:
return goog.events.KeyCodes.EQUALS;
case goog.events.KeyCodes.FF_SEMICOLON:
return goog.events.KeyCodes.SEMICOLON;
case goog.events.KeyCodes.FF_DASH:
return goog.events.KeyCodes.DASH;
case goog.events.KeyCodes.MAC_FF_META:
return goog.events.KeyCodes.META;
case goog.events.KeyCodes.WIN_KEY_FF_LINUX:
return goog.events.KeyCodes.WIN_KEY;
default:
return keyCode;
}
};
/**
* Normalizes key codes from their Mac WebKit-specific value to the general one.
* @param {number} keyCode The native key code.
* @return {number} The normalized key code.
*/
goog.events.KeyCodes.normalizeMacWebKitKeyCode = function(keyCode) {
switch (keyCode) {
case goog.events.KeyCodes.MAC_WK_CMD_RIGHT: // 93
return goog.events.KeyCodes.META; // 91
default:
return keyCode;
}
};
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
Author: nicksantos@google.com (Nick Santos)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
JsUnit tests for goog.events.KeyCodes
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.KeyCodesTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,174 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.KeyCodesTest');
goog.setTestOnly('goog.events.KeyCodesTest');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.KeyCodes');
goog.require('goog.object');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var KeyCodes;
var stubs;
function setUpPage() {
KeyCodes = goog.events.KeyCodes;
stubs = new goog.testing.PropertyReplacer();
}
function tearDown() {
stubs.reset();
}
function testTextModifyingKeys() {
var specialTextModifiers = goog.object.createSet(
KeyCodes.BACKSPACE,
KeyCodes.DELETE,
KeyCodes.ENTER,
KeyCodes.MAC_ENTER,
KeyCodes.TAB,
KeyCodes.WIN_IME);
if (!goog.userAgent.GECKO) {
specialTextModifiers[KeyCodes.WIN_KEY_FF_LINUX] = 1;
}
for (var keyId in KeyCodes) {
var key = KeyCodes[keyId];
if (goog.isFunction(key)) {
// skip static methods
continue;
}
var fakeEvent = createEventWithKeyCode(key);
if (KeyCodes.isCharacterKey(key) || (key in specialTextModifiers)) {
assertTrue('Expected key to modify text: ' + keyId,
KeyCodes.isTextModifyingKeyEvent(fakeEvent));
} else {
assertFalse('Expected key to not modify text: ' + keyId,
KeyCodes.isTextModifyingKeyEvent(fakeEvent));
}
}
for (var i = KeyCodes.FIRST_MEDIA_KEY; i <= KeyCodes.LAST_MEDIA_KEY; i++) {
var fakeEvent = createEventWithKeyCode(i);
assertFalse('Expected key to not modify text: ' + i,
KeyCodes.isTextModifyingKeyEvent(fakeEvent));
}
}
function testKeyCodeZero() {
var zeroEvent = createEventWithKeyCode(0);
assertEquals(
!goog.userAgent.GECKO,
KeyCodes.isTextModifyingKeyEvent(zeroEvent));
assertEquals(
goog.userAgent.WEBKIT,
KeyCodes.isCharacterKey(0));
}
function testPhantomKey() {
// KeyCode 255 deserves its own test to make sure this does not regress,
// because it's so weird. See the comments in the KeyCode enum.
var fakeEvent = createEventWithKeyCode(goog.events.KeyCodes.PHANTOM);
assertFalse('Expected phantom key to not modify text',
KeyCodes.isTextModifyingKeyEvent(fakeEvent));
assertFalse(KeyCodes.isCharacterKey(fakeEvent));
}
function testNonUsKeyboards() {
var fakeEvent = createEventWithKeyCode(1092 /* Russian a */);
assertTrue('Expected key to not modify text: 1092',
KeyCodes.isTextModifyingKeyEvent(fakeEvent));
}
function createEventWithKeyCode(i) {
var fakeEvent = new goog.events.BrowserEvent('keydown');
fakeEvent.keyCode = i;
return fakeEvent;
}
function testNormalizeGeckoKeyCode() {
stubs.set(goog.userAgent, 'GECKO', true);
// Test Gecko-specific key codes.
assertEquals(
goog.events.KeyCodes.normalizeGeckoKeyCode(KeyCodes.FF_EQUALS),
KeyCodes.EQUALS);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.FF_EQUALS),
KeyCodes.EQUALS);
assertEquals(
goog.events.KeyCodes.normalizeGeckoKeyCode(KeyCodes.FF_SEMICOLON),
KeyCodes.SEMICOLON);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.FF_SEMICOLON),
KeyCodes.SEMICOLON);
assertEquals(
goog.events.KeyCodes.normalizeGeckoKeyCode(KeyCodes.MAC_FF_META),
KeyCodes.META);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.MAC_FF_META),
KeyCodes.META);
assertEquals(
goog.events.KeyCodes.normalizeGeckoKeyCode(KeyCodes.WIN_KEY_FF_LINUX),
KeyCodes.WIN_KEY);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.WIN_KEY_FF_LINUX),
KeyCodes.WIN_KEY);
// Test general key codes.
assertEquals(
goog.events.KeyCodes.normalizeGeckoKeyCode(KeyCodes.COMMA),
KeyCodes.COMMA);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.COMMA),
KeyCodes.COMMA);
}
function testNormalizeMacWebKitKeyCode() {
stubs.set(goog.userAgent, 'GECKO', false);
stubs.set(goog.userAgent, 'MAC', true);
stubs.set(goog.userAgent, 'WEBKIT', true);
// Test Mac WebKit specific key codes.
assertEquals(
goog.events.KeyCodes.normalizeMacWebKitKeyCode(KeyCodes.MAC_WK_CMD_LEFT),
KeyCodes.META);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.MAC_WK_CMD_LEFT),
KeyCodes.META);
assertEquals(
goog.events.KeyCodes.normalizeMacWebKitKeyCode(KeyCodes.MAC_WK_CMD_RIGHT),
KeyCodes.META);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.MAC_WK_CMD_RIGHT),
KeyCodes.META);
// Test general key codes.
assertEquals(
goog.events.KeyCodes.normalizeMacWebKitKeyCode(KeyCodes.COMMA),
KeyCodes.COMMA);
assertEquals(
goog.events.KeyCodes.normalizeKeyCode(KeyCodes.COMMA),
KeyCodes.COMMA);
}
@@ -0,0 +1,556 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This file contains a class for working with keyboard events
* that repeat consistently across browsers and platforms. It also unifies the
* key code so that it is the same in all browsers and platforms.
*
* Different web browsers have very different keyboard event handling. Most
* importantly is that only certain browsers repeat keydown events:
* IE, Opera, FF/Win32, and Safari 3 repeat keydown events.
* FF/Mac and Safari 2 do not.
*
* For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit
* decided that they should try to match IE's key handling behavior.
* Safari 3.0.4, which shipped with Leopard (WebKit 523), has the
* Safari 2 behavior.
*
* Firefox, Safari, Opera prevent on keypress
*
* IE prevents on keydown
*
* Firefox does not fire keypress for shift, ctrl, alt
* Firefox does fire keydown for shift, ctrl, alt, meta
* Firefox does not repeat keydown for shift, ctrl, alt, meta
*
* Firefox does not fire keypress for up and down in an input
*
* Opera fires keypress for shift, ctrl, alt, meta
* Opera does not repeat keypress for shift, ctrl, alt, meta
*
* Safari 2 and 3 do not fire keypress for shift, ctrl, alt
* Safari 2 does not fire keydown for shift, ctrl, alt
* Safari 3 *does* fire keydown for shift, ctrl, alt
*
* IE provides the keycode for keyup/down events and the charcode (in the
* keycode field) for keypress.
*
* Mozilla provides the keycode for keyup/down and the charcode for keypress
* unless it's a non text modifying key in which case the keycode is provided.
*
* Safari 3 provides the keycode and charcode for all events.
*
* Opera provides the keycode for keyup/down event and either the charcode or
* the keycode (in the keycode field) for keypress events.
*
* Firefox x11 doesn't fire keydown events if a another key is already held down
* until the first key is released. This can cause a key event to be fired with
* a keyCode for the first key and a charCode for the second key.
*
* Safari in keypress
*
* charCode keyCode which
* ENTER: 13 13 13
* F1: 63236 63236 63236
* F8: 63243 63243 63243
* ...
* p: 112 112 112
* P: 80 80 80
*
* Firefox, keypress:
*
* charCode keyCode which
* ENTER: 0 13 13
* F1: 0 112 0
* F8: 0 119 0
* ...
* p: 112 0 112
* P: 80 0 80
*
* Opera, Mac+Win32, keypress:
*
* charCode keyCode which
* ENTER: undefined 13 13
* F1: undefined 112 0
* F8: undefined 119 0
* ...
* p: undefined 112 112
* P: undefined 80 80
*
* IE7, keydown
*
* charCode keyCode which
* ENTER: undefined 13 undefined
* F1: undefined 112 undefined
* F8: undefined 119 undefined
* ...
* p: undefined 80 undefined
* P: undefined 80 undefined
*
* @author arv@google.com (Erik Arvidsson)
* @author eae@google.com (Emil A Eklund)
* @see ../demos/keyhandler.html
*/
goog.provide('goog.events.KeyEvent');
goog.provide('goog.events.KeyHandler');
goog.provide('goog.events.KeyHandler.EventType');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.userAgent');
/**
* A wrapper around an element that you want to listen to keyboard events on.
* @param {Element|Document=} opt_element The element or document to listen on.
* @param {boolean=} opt_capture Whether to listen for browser events in
* capture phase (defaults to false).
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.events.KeyHandler = function(opt_element, opt_capture) {
goog.events.EventTarget.call(this);
if (opt_element) {
this.attach(opt_element, opt_capture);
}
};
goog.inherits(goog.events.KeyHandler, goog.events.EventTarget);
/**
* This is the element that we will listen to the real keyboard events on.
* @type {Element|Document|null}
* @private
*/
goog.events.KeyHandler.prototype.element_ = null;
/**
* The key for the key press listener.
* @type {goog.events.Key}
* @private
*/
goog.events.KeyHandler.prototype.keyPressKey_ = null;
/**
* The key for the key down listener.
* @type {goog.events.Key}
* @private
*/
goog.events.KeyHandler.prototype.keyDownKey_ = null;
/**
* The key for the key up listener.
* @type {goog.events.Key}
* @private
*/
goog.events.KeyHandler.prototype.keyUpKey_ = null;
/**
* Used to detect keyboard repeat events.
* @private
* @type {number}
*/
goog.events.KeyHandler.prototype.lastKey_ = -1;
/**
* Keycode recorded for key down events. As most browsers don't report the
* keycode in the key press event we need to record it in the key down phase.
* @private
* @type {number}
*/
goog.events.KeyHandler.prototype.keyCode_ = -1;
/**
* Alt key recorded for key down events. FF on Mac does not report the alt key
* flag in the key press event, we need to record it in the key down phase.
* @type {boolean}
* @private
*/
goog.events.KeyHandler.prototype.altKey_ = false;
/**
* Enum type for the events fired by the key handler
* @enum {string}
*/
goog.events.KeyHandler.EventType = {
KEY: 'key'
};
/**
* An enumeration of key codes that Safari 2 does incorrectly
* @type {Object}
* @private
*/
goog.events.KeyHandler.safariKey_ = {
'3': goog.events.KeyCodes.ENTER, // 13
'12': goog.events.KeyCodes.NUMLOCK, // 144
'63232': goog.events.KeyCodes.UP, // 38
'63233': goog.events.KeyCodes.DOWN, // 40
'63234': goog.events.KeyCodes.LEFT, // 37
'63235': goog.events.KeyCodes.RIGHT, // 39
'63236': goog.events.KeyCodes.F1, // 112
'63237': goog.events.KeyCodes.F2, // 113
'63238': goog.events.KeyCodes.F3, // 114
'63239': goog.events.KeyCodes.F4, // 115
'63240': goog.events.KeyCodes.F5, // 116
'63241': goog.events.KeyCodes.F6, // 117
'63242': goog.events.KeyCodes.F7, // 118
'63243': goog.events.KeyCodes.F8, // 119
'63244': goog.events.KeyCodes.F9, // 120
'63245': goog.events.KeyCodes.F10, // 121
'63246': goog.events.KeyCodes.F11, // 122
'63247': goog.events.KeyCodes.F12, // 123
'63248': goog.events.KeyCodes.PRINT_SCREEN, // 44
'63272': goog.events.KeyCodes.DELETE, // 46
'63273': goog.events.KeyCodes.HOME, // 36
'63275': goog.events.KeyCodes.END, // 35
'63276': goog.events.KeyCodes.PAGE_UP, // 33
'63277': goog.events.KeyCodes.PAGE_DOWN, // 34
'63289': goog.events.KeyCodes.NUMLOCK, // 144
'63302': goog.events.KeyCodes.INSERT // 45
};
/**
* An enumeration of key identifiers currently part of the W3C draft for DOM3
* and their mappings to keyCodes.
* http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set
* This is currently supported in Safari and should be platform independent.
* @type {Object}
* @private
*/
goog.events.KeyHandler.keyIdentifier_ = {
'Up': goog.events.KeyCodes.UP, // 38
'Down': goog.events.KeyCodes.DOWN, // 40
'Left': goog.events.KeyCodes.LEFT, // 37
'Right': goog.events.KeyCodes.RIGHT, // 39
'Enter': goog.events.KeyCodes.ENTER, // 13
'F1': goog.events.KeyCodes.F1, // 112
'F2': goog.events.KeyCodes.F2, // 113
'F3': goog.events.KeyCodes.F3, // 114
'F4': goog.events.KeyCodes.F4, // 115
'F5': goog.events.KeyCodes.F5, // 116
'F6': goog.events.KeyCodes.F6, // 117
'F7': goog.events.KeyCodes.F7, // 118
'F8': goog.events.KeyCodes.F8, // 119
'F9': goog.events.KeyCodes.F9, // 120
'F10': goog.events.KeyCodes.F10, // 121
'F11': goog.events.KeyCodes.F11, // 122
'F12': goog.events.KeyCodes.F12, // 123
'U+007F': goog.events.KeyCodes.DELETE, // 46
'Home': goog.events.KeyCodes.HOME, // 36
'End': goog.events.KeyCodes.END, // 35
'PageUp': goog.events.KeyCodes.PAGE_UP, // 33
'PageDown': goog.events.KeyCodes.PAGE_DOWN, // 34
'Insert': goog.events.KeyCodes.INSERT // 45
};
/**
* If true, the KeyEvent fires on keydown. Otherwise, it fires on keypress.
*
* @type {boolean}
* @private
*/
goog.events.KeyHandler.USES_KEYDOWN_ = goog.userAgent.IE ||
goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('525');
/**
* If true, the alt key flag is saved during the key down and reused when
* handling the key press. FF on Mac does not set the alt flag in the key press
* event.
* @type {boolean}
* @private
*/
goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_ = goog.userAgent.MAC &&
goog.userAgent.GECKO;
/**
* Records the keycode for browsers that only returns the keycode for key up/
* down events. For browser/key combinations that doesn't trigger a key pressed
* event it also fires the patched key event.
* @param {goog.events.BrowserEvent} e The key down event.
* @private
*/
goog.events.KeyHandler.prototype.handleKeyDown_ = function(e) {
// Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window
// before we've caught a key-up event. If the last-key was one of these we
// reset the state.
if (goog.userAgent.WEBKIT) {
if (this.lastKey_ == goog.events.KeyCodes.CTRL && !e.ctrlKey ||
this.lastKey_ == goog.events.KeyCodes.ALT && !e.altKey ||
goog.userAgent.MAC &&
this.lastKey_ == goog.events.KeyCodes.META && !e.metaKey) {
this.lastKey_ = -1;
this.keyCode_ = -1;
}
}
if (this.lastKey_ == -1) {
if (e.ctrlKey && e.keyCode != goog.events.KeyCodes.CTRL) {
this.lastKey_ = goog.events.KeyCodes.CTRL;
} else if (e.altKey && e.keyCode != goog.events.KeyCodes.ALT) {
this.lastKey_ = goog.events.KeyCodes.ALT;
} else if (e.metaKey && e.keyCode != goog.events.KeyCodes.META) {
this.lastKey_ = goog.events.KeyCodes.META;
}
}
if (goog.events.KeyHandler.USES_KEYDOWN_ &&
!goog.events.KeyCodes.firesKeyPressEvent(e.keyCode,
this.lastKey_, e.shiftKey, e.ctrlKey, e.altKey)) {
this.handleEvent(e);
} else {
this.keyCode_ = goog.events.KeyCodes.normalizeKeyCode(e.keyCode);
if (goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_) {
this.altKey_ = e.altKey;
}
}
};
/**
* Resets the stored previous values. Needed to be called for webkit which will
* not generate a key up for meta key operations. This should only be called
* when having finished with repeat key possiblities.
*/
goog.events.KeyHandler.prototype.resetState = function() {
this.lastKey_ = -1;
this.keyCode_ = -1;
};
/**
* Clears the stored previous key value, resetting the key repeat status. Uses
* -1 because the Safari 3 Windows beta reports 0 for certain keys (like Home
* and End.)
* @param {goog.events.BrowserEvent} e The keyup event.
* @private
*/
goog.events.KeyHandler.prototype.handleKeyup_ = function(e) {
this.resetState();
this.altKey_ = e.altKey;
};
/**
* Handles the events on the element.
* @param {goog.events.BrowserEvent} e The keyboard event sent from the
* browser.
*/
goog.events.KeyHandler.prototype.handleEvent = function(e) {
var be = e.getBrowserEvent();
var keyCode, charCode;
var altKey = be.altKey;
// IE reports the character code in the keyCode field for keypress events.
// There are two exceptions however, Enter and Escape.
if (goog.userAgent.IE && e.type == goog.events.EventType.KEYPRESS) {
keyCode = this.keyCode_;
charCode = keyCode != goog.events.KeyCodes.ENTER &&
keyCode != goog.events.KeyCodes.ESC ?
be.keyCode : 0;
// Safari reports the character code in the keyCode field for keypress
// events but also has a charCode field.
} else if (goog.userAgent.WEBKIT &&
e.type == goog.events.EventType.KEYPRESS) {
keyCode = this.keyCode_;
charCode = be.charCode >= 0 && be.charCode < 63232 &&
goog.events.KeyCodes.isCharacterKey(keyCode) ?
be.charCode : 0;
// Opera reports the keycode or the character code in the keyCode field.
} else if (goog.userAgent.OPERA) {
keyCode = this.keyCode_;
charCode = goog.events.KeyCodes.isCharacterKey(keyCode) ?
be.keyCode : 0;
// Mozilla reports the character code in the charCode field.
} else {
keyCode = be.keyCode || this.keyCode_;
charCode = be.charCode || 0;
if (goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_) {
altKey = this.altKey_;
}
// On the Mac, shift-/ triggers a question mark char code and no key code
// (normalized to WIN_KEY), so we synthesize the latter.
if (goog.userAgent.MAC &&
charCode == goog.events.KeyCodes.QUESTION_MARK &&
keyCode == goog.events.KeyCodes.WIN_KEY) {
keyCode = goog.events.KeyCodes.SLASH;
}
}
keyCode = goog.events.KeyCodes.normalizeKeyCode(keyCode);
var key = keyCode;
var keyIdentifier = be.keyIdentifier;
// Correct the key value for certain browser-specific quirks.
if (keyCode) {
if (keyCode >= 63232 && keyCode in goog.events.KeyHandler.safariKey_) {
// NOTE(nicksantos): Safari 3 has fixed this problem,
// this is only needed for Safari 2.
key = goog.events.KeyHandler.safariKey_[keyCode];
} else {
// Safari returns 25 for Shift+Tab instead of 9.
if (keyCode == 25 && e.shiftKey) {
key = 9;
}
}
} else if (keyIdentifier &&
keyIdentifier in goog.events.KeyHandler.keyIdentifier_) {
// This is needed for Safari Windows because it currently doesn't give a
// keyCode/which for non printable keys.
key = goog.events.KeyHandler.keyIdentifier_[keyIdentifier];
}
// If we get the same keycode as a keydown/keypress without having seen a
// keyup event, then this event was caused by key repeat.
var repeat = key == this.lastKey_;
this.lastKey_ = key;
var event = new goog.events.KeyEvent(key, charCode, repeat, be);
event.altKey = altKey;
this.dispatchEvent(event);
};
/**
* Returns the element listened on for the real keyboard events.
* @return {Element|Document|null} The element listened on for the real
* keyboard events.
*/
goog.events.KeyHandler.prototype.getElement = function() {
return this.element_;
};
/**
* Adds the proper key event listeners to the element.
* @param {Element|Document} element The element to listen on.
* @param {boolean=} opt_capture Whether to listen for browser events in
* capture phase (defaults to false).
*/
goog.events.KeyHandler.prototype.attach = function(element, opt_capture) {
if (this.keyUpKey_) {
this.detach();
}
this.element_ = element;
this.keyPressKey_ = goog.events.listen(this.element_,
goog.events.EventType.KEYPRESS,
this,
opt_capture);
// Most browsers (Safari 2 being the notable exception) doesn't include the
// keyCode in keypress events (IE has the char code in the keyCode field and
// Mozilla only included the keyCode if there's no charCode). Thus we have to
// listen for keydown to capture the keycode.
this.keyDownKey_ = goog.events.listen(this.element_,
goog.events.EventType.KEYDOWN,
this.handleKeyDown_,
opt_capture,
this);
this.keyUpKey_ = goog.events.listen(this.element_,
goog.events.EventType.KEYUP,
this.handleKeyup_,
opt_capture,
this);
};
/**
* Removes the listeners that may exist.
*/
goog.events.KeyHandler.prototype.detach = function() {
if (this.keyPressKey_) {
goog.events.unlistenByKey(this.keyPressKey_);
goog.events.unlistenByKey(this.keyDownKey_);
goog.events.unlistenByKey(this.keyUpKey_);
this.keyPressKey_ = null;
this.keyDownKey_ = null;
this.keyUpKey_ = null;
}
this.element_ = null;
this.lastKey_ = -1;
this.keyCode_ = -1;
};
/** @override */
goog.events.KeyHandler.prototype.disposeInternal = function() {
goog.events.KeyHandler.superClass_.disposeInternal.call(this);
this.detach();
};
/**
* This class is used for the goog.events.KeyHandler.EventType.KEY event and
* it overrides the key code with the fixed key code.
* @param {number} keyCode The adjusted key code.
* @param {number} charCode The unicode character code.
* @param {boolean} repeat Whether this event was generated by keyboard repeat.
* @param {Event} browserEvent Browser event object.
* @constructor
* @extends {goog.events.BrowserEvent}
* @final
*/
goog.events.KeyEvent = function(keyCode, charCode, repeat, browserEvent) {
goog.events.BrowserEvent.call(this, browserEvent);
this.type = goog.events.KeyHandler.EventType.KEY;
/**
* Keycode of key press.
* @type {number}
*/
this.keyCode = keyCode;
/**
* Unicode character code.
* @type {number}
*/
this.charCode = charCode;
/**
* True if this event was generated by keyboard auto-repeat (i.e., the user is
* holding the key down.)
* @type {boolean}
*/
this.repeat = repeat;
};
goog.inherits(goog.events.KeyEvent, goog.events.BrowserEvent);
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.KeyHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.KeyEventTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,728 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.KeyEventTest');
goog.setTestOnly('goog.events.KeyEventTest');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
function setUp() {
// Have this based on a fictitious DOCUMENT_MODE constant.
goog.userAgent.isDocumentMode = function(mode) {
return mode <= goog.userAgent.DOCUMENT_MODE;
};
}
/**
* Tests the key handler for the IE 8 and lower behavior.
*/
function testIe8StyleKeyHandling() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = true;
goog.userAgent.GECKO = false;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = false;
goog.userAgent.WINDOWS = true;
goog.userAgent.LINUX = false;
goog.userAgent.VERSION = 8;
goog.userAgent.DOCUMENT_MODE = 8;
goog.events.KeyHandler.USES_KEYDOWN_ = true;
assertIe8StyleKeyHandling();
}
/**
* Tests the key handler for the IE 8 and lower behavior.
*/
function testIe8StyleKeyHandlingInIe9DocumentMode() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = true;
goog.userAgent.GECKO = false;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = false;
goog.userAgent.WINDOWS = true;
goog.userAgent.LINUX = false;
goog.userAgent.VERSION = 9; // Try IE9 in IE8 document mode.
goog.userAgent.DOCUMENT_MODE = 8;
goog.events.KeyHandler.USES_KEYDOWN_ = true;
assertIe8StyleKeyHandling();
}
function assertIe8StyleKeyHandling() {
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, goog.events.KeyCodes.ENTER);
fireKeyPress(keyHandler, goog.events.KeyCodes.ENTER);
assertEquals('Enter should fire a key event with the keycode 13',
goog.events.KeyCodes.ENTER,
keyEvent.keyCode);
assertEquals('Enter should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.ESC);
fireKeyPress(keyHandler, goog.events.KeyCodes.ESC);
assertEquals('Esc should fire a key event with the keycode 27',
goog.events.KeyCodes.ESC,
keyEvent.keyCode);
assertEquals('Esc should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.UP);
assertEquals('Up should fire a key event with the keycode 38',
goog.events.KeyCodes.UP,
keyEvent.keyCode);
assertEquals('Up should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.SEVEN, undefined, undefined,
undefined, undefined, true);
fireKeyPress(keyHandler, 38, undefined, undefined, undefined, undefined,
true);
assertEquals('Shift+7 should fire a key event with the keycode 55',
goog.events.KeyCodes.SEVEN,
keyEvent.keyCode);
assertEquals('Shift+7 should fire a key event with the charcode 38',
38,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, 97);
assertEquals('Lower case a should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Lower case a should fire a key event with the charcode 97',
97,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, 65);
assertEquals('Upper case A should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Upper case A should fire a key event with the charcode 65',
65,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.DELETE);
assertEquals('Delete should fire a key event with the keycode 46',
goog.events.KeyCodes.DELETE,
keyEvent.keyCode);
assertEquals('Delete should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.PERIOD);
fireKeyPress(keyHandler, 46);
assertEquals('Period should fire a key event with the keycode 190',
goog.events.KeyCodes.PERIOD,
keyEvent.keyCode);
assertEquals('Period should fire a key event with the charcode 46',
46,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.CTRL);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
assertEquals('A with control down should fire a key event',
goog.events.KeyCodes.A,
keyEvent.keyCode);
// On IE, when Ctrl+<key> is held down, there is a KEYDOWN, a KEYPRESS, and
// then a series of KEYDOWN events for each repeat.
fireKeyDown(keyHandler, goog.events.KeyCodes.B, undefined, undefined, true);
fireKeyPress(keyHandler, goog.events.KeyCodes.B, undefined, undefined,
true);
assertEquals('B with control down should fire a key event',
goog.events.KeyCodes.B,
keyEvent.keyCode);
assertTrue('Ctrl should be down.', keyEvent.ctrlKey);
assertFalse('Should not have repeat=true on the first key press.',
keyEvent.repeat);
// Fire one repeated keydown event.
fireKeyDown(keyHandler, goog.events.KeyCodes.B, undefined, undefined, true);
assertEquals('A with control down should fire a key event',
goog.events.KeyCodes.B,
keyEvent.keyCode);
assertTrue('Should have repeat=true on key repeat.',
keyEvent.repeat);
assertTrue('Ctrl should be down.', keyEvent.ctrlKey);
}
/**
* Tests special cases for IE9.
*/
function testIe9StyleKeyHandling() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = true;
goog.userAgent.GECKO = false;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = false;
goog.userAgent.WINDOWS = true;
goog.userAgent.LINUX = false;
goog.userAgent.VERSION = 9;
goog.userAgent.DOCUMENT_MODE = 9;
goog.events.KeyHandler.USES_KEYDOWN_ = true;
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, goog.events.KeyCodes.ENTER);
fireKeyPress(keyHandler, goog.events.KeyCodes.ENTER);
assertEquals('Enter should fire a key event with the keycode 13',
goog.events.KeyCodes.ENTER,
keyEvent.keyCode);
assertEquals('Enter should fire a key event with the charcode 0',
0,
keyEvent.charCode);
}
/**
* Tests the key handler for the Gecko behavior.
*/
function testGeckoStyleKeyHandling() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = false;
goog.userAgent.GECKO = true;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = false;
goog.userAgent.WINDOWS = true;
goog.userAgent.LINUX = false;
goog.events.KeyHandler.USES_KEYDOWN_ = false;
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, goog.events.KeyCodes.ENTER);
fireKeyPress(keyHandler, goog.events.KeyCodes.ENTER);
assertEquals('Enter should fire a key event with the keycode 13',
goog.events.KeyCodes.ENTER,
keyEvent.keyCode);
assertEquals('Enter should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.ESC);
fireKeyPress(keyHandler, goog.events.KeyCodes.ESC);
assertEquals('Esc should fire a key event with the keycode 27',
goog.events.KeyCodes.ESC,
keyEvent.keyCode);
assertEquals('Esc should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.UP);
fireKeyPress(keyHandler, goog.events.KeyCodes.UP);
assertEquals('Up should fire a key event with the keycode 38',
goog.events.KeyCodes.UP,
keyEvent.keyCode);
assertEquals('Up should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.SEVEN, undefined, undefined,
undefined, undefined, true);
fireKeyPress(keyHandler, undefined, 38, undefined, undefined, undefined,
true);
assertEquals('Shift+7 should fire a key event with the keycode 55',
goog.events.KeyCodes.SEVEN,
keyEvent.keyCode);
assertEquals('Shift+7 should fire a key event with the charcode 38',
38,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, undefined, 97);
assertEquals('Lower case a should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Lower case a should fire a key event with the charcode 97',
97,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, undefined, 65);
assertEquals('Upper case A should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Upper case A should fire a key event with the charcode 65',
65,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.DELETE);
fireKeyPress(keyHandler, goog.events.KeyCodes.DELETE);
assertEquals('Delete should fire a key event with the keycode 46',
goog.events.KeyCodes.DELETE,
keyEvent.keyCode);
assertEquals('Delete should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.PERIOD);
fireKeyPress(keyHandler, undefined, 46);
assertEquals('Period should fire a key event with the keycode 190',
goog.events.KeyCodes.PERIOD,
keyEvent.keyCode);
assertEquals('Period should fire a key event with the charcode 46',
46,
keyEvent.charCode);
}
/**
* Tests the key handler for the Safari 3 behavior.
*/
function testSafari3StyleKeyHandling() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = false;
goog.userAgent.GECKO = false;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = true;
goog.userAgent.MAC = true;
goog.userAgent.WINDOWS = false;
goog.userAgent.LINUX = false;
goog.events.KeyHandler.USES_KEYDOWN_ = true;
goog.userAgent.VERSION = 525.3;
var keyEvent, keyHandler = new goog.events.KeyHandler();
// Make sure all events are caught while testing
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, goog.events.KeyCodes.ENTER);
fireKeyPress(keyHandler, goog.events.KeyCodes.ENTER);
assertEquals('Enter should fire a key event with the keycode 13',
goog.events.KeyCodes.ENTER,
keyEvent.keyCode);
assertEquals('Enter should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyUp(keyHandler, goog.events.KeyCodes.ENTER);
// Add a listener to ensure that an extra ENTER event is not dispatched
// by a subsequent keypress.
var enterCheck = goog.events.listen(keyHandler,
goog.events.KeyHandler.EventType.KEY,
function(e) {
assertNotEquals('Unexpected ENTER keypress dispatched',
e.keyCode, goog.events.KeyCodes.ENTER);
});
fireKeyDown(keyHandler, goog.events.KeyCodes.ESC);
assertEquals('Esc should fire a key event with the keycode 27',
goog.events.KeyCodes.ESC,
keyEvent.keyCode);
assertEquals('Esc should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyPress(keyHandler, goog.events.KeyCodes.ESC);
goog.events.unlistenByKey(enterCheck);
fireKeyDown(keyHandler, goog.events.KeyCodes.UP);
assertEquals('Up should fire a key event with the keycode 38',
goog.events.KeyCodes.UP,
keyEvent.keyCode);
assertEquals('Up should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.SEVEN, undefined, undefined,
undefined, undefined, true);
fireKeyPress(keyHandler, 38, 38, undefined, undefined, undefined, true);
assertEquals('Shift+7 should fire a key event with the keycode 55',
goog.events.KeyCodes.SEVEN,
keyEvent.keyCode);
assertEquals('Shift+7 should fire a key event with the charcode 38',
38,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, 97, 97);
assertEquals('Lower case a should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Lower case a should fire a key event with the charcode 97',
97,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, 65, 65);
assertEquals('Upper case A should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Upper case A should fire a key event with the charcode 65',
65,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.CTRL);
fireKeyDown(keyHandler, goog.events.KeyCodes.A, null, null, true /*ctrl*/);
assertEquals('A with control down should fire a key event',
goog.events.KeyCodes.A,
keyEvent.keyCode);
// Test that Alt-Tab outside the window doesn't break things.
fireKeyDown(keyHandler, goog.events.KeyCodes.ALT);
keyEvent.keyCode = -1; // Reset the event.
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
assertEquals('Should not have dispatched an Alt-A', -1, keyEvent.keyCode);
fireKeyPress(keyHandler, 65, 65);
assertEquals('Alt should be ignored since it isn\'t currently depressed',
goog.events.KeyCodes.A,
keyEvent.keyCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.DELETE);
assertEquals('Delete should fire a key event with the keycode 46',
goog.events.KeyCodes.DELETE,
keyEvent.keyCode);
assertEquals('Delete should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.PERIOD);
fireKeyPress(keyHandler, 46, 46);
assertEquals('Period should fire a key event with the keycode 190',
goog.events.KeyCodes.PERIOD,
keyEvent.keyCode);
assertEquals('Period should fire a key event with the charcode 46',
46,
keyEvent.charCode);
// Safari sends zero key code for non-latin characters.
fireKeyDown(keyHandler, 0, 0);
fireKeyPress(keyHandler, 1092, 1092);
assertEquals('Cyrillic small letter "Ef" should fire a key event with ' +
'the keycode 0',
0,
keyEvent.keyCode);
assertEquals('Cyrillic small letter "Ef" should fire a key event with ' +
'the charcode 1092',
1092,
keyEvent.charCode);
}
/**
* Tests the key handler for the Opera behavior.
*/
function testOperaStyleKeyHandling() {
goog.userAgent.OPERA = true;
goog.userAgent.IE = false;
goog.userAgent.GECKO = false;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = false;
goog.userAgent.WINDOWS = true;
goog.userAgent.LINUX = false;
goog.events.KeyHandler.USES_KEYDOWN_ = false;
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, goog.events.KeyCodes.ENTER);
fireKeyPress(keyHandler, goog.events.KeyCodes.ENTER);
assertEquals('Enter should fire a key event with the keycode 13',
goog.events.KeyCodes.ENTER,
keyEvent.keyCode);
assertEquals('Enter should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.ESC);
fireKeyPress(keyHandler, goog.events.KeyCodes.ESC);
assertEquals('Esc should fire a key event with the keycode 27',
goog.events.KeyCodes.ESC,
keyEvent.keyCode);
assertEquals('Esc should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.UP);
fireKeyPress(keyHandler, goog.events.KeyCodes.UP);
assertEquals('Up should fire a key event with the keycode 38',
goog.events.KeyCodes.UP,
keyEvent.keyCode);
assertEquals('Up should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.SEVEN, undefined, undefined,
undefined, undefined, true);
fireKeyPress(keyHandler, 38, undefined, undefined, undefined, undefined,
true);
assertEquals('Shift+7 should fire a key event with the keycode 55',
goog.events.KeyCodes.SEVEN,
keyEvent.keyCode);
assertEquals('Shift+7 should fire a key event with the charcode 38',
38,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, 97);
assertEquals('Lower case a should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Lower case a should fire a key event with the charcode 97',
97,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.A);
fireKeyPress(keyHandler, 65);
assertEquals('Upper case A should fire a key event with the keycode 65',
goog.events.KeyCodes.A,
keyEvent.keyCode);
assertEquals('Upper case A should fire a key event with the charcode 65',
65,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.DELETE);
fireKeyPress(keyHandler, goog.events.KeyCodes.DELETE);
assertEquals('Delete should fire a key event with the keycode 46',
goog.events.KeyCodes.DELETE,
keyEvent.keyCode);
assertEquals('Delete should fire a key event with the charcode 0',
0,
keyEvent.charCode);
fireKeyDown(keyHandler, goog.events.KeyCodes.PERIOD);
fireKeyPress(keyHandler, 46);
assertEquals('Period should fire a key event with the keycode 190',
goog.events.KeyCodes.PERIOD,
keyEvent.keyCode);
assertEquals('Period should fire a key event with the charcode 46',
46,
keyEvent.charCode);
}
function testGeckoOnMacAltHandling() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = false;
goog.userAgent.GECKO = true;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = true;
goog.userAgent.WINDOWS = false;
goog.userAgent.LINUX = false;
goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_ = true;
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, goog.events.KeyCodes.COMMA, 0, null, false,
true, false);
fireKeyPress(keyHandler, 0, 8804, null, false, false, false);
assertEquals('should fire a key event with COMMA',
goog.events.KeyCodes.COMMA,
keyEvent.keyCode);
assertEquals('should fire a key event with alt key set',
true,
keyEvent.altKey);
// Scenario: alt down, a down, a press, a up (should say alt is true),
// alt up.
keyEvent = undefined;
fireKeyDown(keyHandler, 18, 0, null, false, true, false);
fireKeyDown(keyHandler, goog.events.KeyCodes.A, 0, null, false, true,
false);
fireKeyPress(keyHandler, 0, 229, null, false, false, false);
assertEquals('should fire a key event with alt key set',
true,
keyEvent.altKey);
fireKeyUp(keyHandler, 0, 229, null, false, true, false);
assertEquals('alt key should still be set',
true,
keyEvent.altKey);
fireKeyUp(keyHandler, 18, 0, null, false, false, false);
}
function testGeckoEqualSign() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = false;
goog.userAgent.GECKO = true;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = false;
goog.userAgent.WINDOWS = true;
goog.userAgent.LINUX = false;
goog.events.KeyHandler.USES_KEYDOWN_ = false;
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, 61, 0);
fireKeyPress(keyHandler, 0, 61);
assertEquals('= should fire should fire a key event with the keyCode 187',
goog.events.KeyCodes.EQUALS,
keyEvent.keyCode);
assertEquals('= should fire a key event with the charCode 61',
goog.events.KeyCodes.FF_EQUALS,
keyEvent.charCode);
}
function testMacGeckoSlash() {
goog.userAgent.OPERA = false;
goog.userAgent.IE = false;
goog.userAgent.GECKO = true;
goog.userAgent.CAMINO = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.MAC = true;
goog.userAgent.WINDOWS = false;
goog.userAgent.LINUX = false;
goog.events.KeyHandler.USES_KEYDOWN_ = false;
var keyEvent, keyHandler = new goog.events.KeyHandler();
goog.events.listen(keyHandler, goog.events.KeyHandler.EventType.KEY,
function(e) { keyEvent = e; });
fireKeyDown(keyHandler, 0, 63, null, false, false, true);
fireKeyPress(keyHandler, 0, 63, null, false, false, true);
assertEquals('/ should fire a key event with the keyCode 191',
goog.events.KeyCodes.SLASH,
keyEvent.keyCode);
assertEquals('? should fire a key event with the charCode 63',
goog.events.KeyCodes.QUESTION_MARK,
keyEvent.charCode);
}
function testGetElement() {
var target = goog.dom.createDom('div');
var target2 = goog.dom.createDom('div');
var keyHandler = new goog.events.KeyHandler();
assertNull(keyHandler.getElement());
keyHandler.attach(target);
assertEquals(target, keyHandler.getElement());
keyHandler.attach(target2);
assertNotEquals(target, keyHandler.getElement());
assertEquals(target2, keyHandler.getElement());
var doc = goog.dom.getDocument();
keyHandler.attach(doc);
assertEquals(doc, keyHandler.getElement());
keyHandler = new goog.events.KeyHandler(doc);
assertEquals(doc, keyHandler.getElement());
keyHandler = new goog.events.KeyHandler(target);
assertEquals(target, keyHandler.getElement());
}
function testDetach() {
var target = goog.dom.createDom('div');
var keyHandler = new goog.events.KeyHandler(target);
assertEquals(target, keyHandler.getElement());
fireKeyDown(keyHandler, 0, 63, null, false, false, true);
fireKeyPress(keyHandler, 0, 63, null, false, false, true);
keyHandler.detach();
assertNull(keyHandler.getElement());
// All listeners should be cleared.
assertNull(keyHandler.keyDownKey_);
assertNull(keyHandler.keyPressKey_);
assertNull(keyHandler.keyUpKey_);
// All key related state should be cleared.
assertEquals('Last key should be -1', -1, keyHandler.lastKey_);
assertEquals('keycode should be -1', -1, keyHandler.keyCode_);
}
function testCapturePhase() {
var gotInCapturePhase;
var gotInBubblePhase;
var target = goog.dom.createDom('div');
goog.events.listen(
new goog.events.KeyHandler(target, false /* bubble */),
goog.events.KeyHandler.EventType.KEY,
function() {
gotInBubblePhase = true;
assertTrue(gotInCapturePhase);
});
goog.events.listen(
new goog.events.KeyHandler(target, true /* capture */),
goog.events.KeyHandler.EventType.KEY,
function() {
gotInCapturePhase = true;
});
goog.testing.events.fireKeySequence(target, goog.events.KeyCodes.ESC);
assertTrue(gotInBubblePhase);
}
function fireKeyDown(keyHandler, keyCode, opt_charCode, opt_keyIdentifier,
opt_ctrlKey, opt_altKey, opt_shiftKey) {
var fakeEvent = createFakeKeyEvent(
goog.events.EventType.KEYDOWN, keyCode, opt_charCode, opt_keyIdentifier,
opt_ctrlKey, opt_altKey, opt_shiftKey);
keyHandler.handleKeyDown_(fakeEvent);
return fakeEvent.returnValue_;
}
function fireKeyPress(keyHandler, keyCode, opt_charCode, opt_keyIdentifier,
opt_ctrlKey, opt_altKey, opt_shiftKey) {
var fakeEvent = createFakeKeyEvent(
goog.events.EventType.KEYPRESS, keyCode, opt_charCode,
opt_keyIdentifier, opt_ctrlKey, opt_altKey, opt_shiftKey);
keyHandler.handleEvent(fakeEvent);
return fakeEvent.returnValue_;
}
function fireKeyUp(keyHandler, keyCode, opt_charCode, opt_keyIdentifier,
opt_ctrlKey, opt_altKey, opt_shiftKey) {
var fakeEvent = createFakeKeyEvent(
goog.events.EventType.KEYUP, keyCode, opt_charCode,
opt_keyIdentifier, opt_ctrlKey, opt_altKey, opt_shiftKey);
keyHandler.handleKeyup_(fakeEvent);
return fakeEvent.returnValue_;
}
function createFakeKeyEvent(type, keyCode, opt_charCode, opt_keyIdentifier,
opt_ctrlKey, opt_altKey, opt_shiftKey) {
var event = {
type: type,
keyCode: keyCode,
charCode: opt_charCode || undefined,
keyIdentifier: opt_keyIdentifier || undefined,
ctrlKey: opt_ctrlKey || false,
altKey: opt_altKey || false,
shiftKey: opt_shiftKey || false,
timeStamp: goog.now()
};
return new goog.events.BrowserEvent(event);
}
@@ -0,0 +1,139 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Constant declarations for common key codes.
*
* @author eae@google.com (Emil A Eklund)
*/
goog.provide('goog.events.KeyNames');
/**
* Key names for common characters. These should be used with keyup/keydown
* events, since the .keyCode property on those is meant to indicate the
* *physical key* the user held down on the keyboard. Hence the mapping uses
* only the unshifted version of each key (e.g. no '#', since that's shift+3).
* Keypress events on the other hand generate (mostly) ASCII codes since they
* correspond to *characters* the user typed.
*
* For further reference: http://unixpapa.com/js/key.html
*
* This list is not localized and therefore some of the key codes are not
* correct for non-US keyboard layouts.
*
* @see goog.events.KeyCodes
* @enum {string}
*/
goog.events.KeyNames = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
19: 'pause',
20: 'caps-lock',
27: 'esc',
32: 'space',
33: 'pg-up',
34: 'pg-down',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'insert',
46: 'delete',
48: '0',
49: '1',
50: '2',
51: '3',
52: '4',
53: '5',
54: '6',
55: '7',
56: '8',
57: '9',
59: 'semicolon',
61: 'equals',
65: 'a',
66: 'b',
67: 'c',
68: 'd',
69: 'e',
70: 'f',
71: 'g',
72: 'h',
73: 'i',
74: 'j',
75: 'k',
76: 'l',
77: 'm',
78: 'n',
79: 'o',
80: 'p',
81: 'q',
82: 'r',
83: 's',
84: 't',
85: 'u',
86: 'v',
87: 'w',
88: 'x',
89: 'y',
90: 'z',
93: 'context',
96: 'num-0',
97: 'num-1',
98: 'num-2',
99: 'num-3',
100: 'num-4',
101: 'num-5',
102: 'num-6',
103: 'num-7',
104: 'num-8',
105: 'num-9',
106: 'num-multiply',
107: 'num-plus',
109: 'num-minus',
110: 'num-period',
111: 'num-division',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
186: 'semicolon',
187: 'equals',
189: 'dash',
188: ',',
190: '.',
191: '/',
192: '`',
219: 'open-square-bracket',
220: '\\',
221: 'close-square-bracket',
222: 'single-quote',
224: 'win'
};
@@ -0,0 +1,335 @@
// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview An interface for a listenable JavaScript object.
* @author chrishenry@google.com (Chris Henry)
*/
goog.provide('goog.events.Listenable');
goog.provide('goog.events.ListenableKey');
/** @suppress {extraRequire} */
goog.require('goog.events.EventId');
/**
* A listenable interface. A listenable is an object with the ability
* to dispatch/broadcast events to "event listeners" registered via
* listen/listenOnce.
*
* The interface allows for an event propagation mechanism similar
* to one offered by native browser event targets, such as
* capture/bubble mechanism, stopping propagation, and preventing
* default actions. Capture/bubble mechanism depends on the ancestor
* tree constructed via {@code #getParentEventTarget}; this tree
* must be directed acyclic graph. The meaning of default action(s)
* in preventDefault is specific to a particular use case.
*
* Implementations that do not support capture/bubble or can not have
* a parent listenable can simply not implement any ability to set the
* parent listenable (and have {@code #getParentEventTarget} return
* null).
*
* Implementation of this class can be used with or independently from
* goog.events.
*
* Implementation must call {@code #addImplementation(implClass)}.
*
* @interface
* @see goog.events
* @see http://www.w3.org/TR/DOM-Level-2-Events/events.html
*/
goog.events.Listenable = function() {};
/**
* An expando property to indicate that an object implements
* goog.events.Listenable.
*
* See addImplementation/isImplementedBy.
*
* @type {string}
* @const
*/
goog.events.Listenable.IMPLEMENTED_BY_PROP =
'closure_listenable_' + ((Math.random() * 1e6) | 0);
/**
* Marks a given class (constructor) as an implementation of
* Listenable, do that we can query that fact at runtime. The class
* must have already implemented the interface.
* @param {!Function} cls The class constructor. The corresponding
* class must have already implemented the interface.
*/
goog.events.Listenable.addImplementation = function(cls) {
cls.prototype[goog.events.Listenable.IMPLEMENTED_BY_PROP] = true;
};
/**
* @param {Object} obj The object to check.
* @return {boolean} Whether a given instance implements Listenable. The
* class/superclass of the instance must call addImplementation.
*/
goog.events.Listenable.isImplementedBy = function(obj) {
return !!(obj && obj[goog.events.Listenable.IMPLEMENTED_BY_PROP]);
};
/**
* Adds an event listener. A listener can only be added once to an
* object and if it is added again the key for the listener is
* returned. Note that if the existing listener is a one-off listener
* (registered via listenOnce), it will no longer be a one-off
* listener after a call to listen().
*
* @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
* @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
* method.
* @param {boolean=} opt_useCapture Whether to fire in capture phase
* (defaults to false).
* @param {SCOPE=} opt_listenerScope Object in whose scope to call the
* listener.
* @return {goog.events.ListenableKey} Unique key for the listener.
* @template SCOPE,EVENTOBJ
*/
goog.events.Listenable.prototype.listen;
/**
* Adds an event listener that is removed automatically after the
* listener fired once.
*
* If an existing listener already exists, listenOnce will do
* nothing. In particular, if the listener was previously registered
* via listen(), listenOnce() will not turn the listener into a
* one-off listener. Similarly, if there is already an existing
* one-off listener, listenOnce does not modify the listeners (it is
* still a once listener).
*
* @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
* @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
* method.
* @param {boolean=} opt_useCapture Whether to fire in capture phase
* (defaults to false).
* @param {SCOPE=} opt_listenerScope Object in whose scope to call the
* listener.
* @return {goog.events.ListenableKey} Unique key for the listener.
* @template SCOPE,EVENTOBJ
*/
goog.events.Listenable.prototype.listenOnce;
/**
* Removes an event listener which was added with listen() or listenOnce().
*
* @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
* @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
* method.
* @param {boolean=} opt_useCapture Whether to fire in capture phase
* (defaults to false).
* @param {SCOPE=} opt_listenerScope Object in whose scope to call
* the listener.
* @return {boolean} Whether any listener was removed.
* @template SCOPE,EVENTOBJ
*/
goog.events.Listenable.prototype.unlisten;
/**
* Removes an event listener which was added with listen() by the key
* returned by listen().
*
* @param {goog.events.ListenableKey} key The key returned by
* listen() or listenOnce().
* @return {boolean} Whether any listener was removed.
*/
goog.events.Listenable.prototype.unlistenByKey;
/**
* Dispatches an event (or event like object) and calls all listeners
* listening for events of this type. The type of the event is decided by the
* type property on the event object.
*
* If any of the listeners returns false OR calls preventDefault then this
* function will return false. If one of the capture listeners calls
* stopPropagation, then the bubble listeners won't fire.
*
* @param {goog.events.EventLike} e Event object.
* @return {boolean} If anyone called preventDefault on the event object (or
* if any of the listeners returns false) this will also return false.
*/
goog.events.Listenable.prototype.dispatchEvent;
/**
* Removes all listeners from this listenable. If type is specified,
* it will only remove listeners of the particular type. otherwise all
* registered listeners will be removed.
*
* @param {string=} opt_type Type of event to remove, default is to
* remove all types.
* @return {number} Number of listeners removed.
*/
goog.events.Listenable.prototype.removeAllListeners;
/**
* Returns the parent of this event target to use for capture/bubble
* mechanism.
*
* NOTE(chrishenry): The name reflects the original implementation of
* custom event target ({@code goog.events.EventTarget}). We decided
* that changing the name is not worth it.
*
* @return {goog.events.Listenable} The parent EventTarget or null if
* there is no parent.
*/
goog.events.Listenable.prototype.getParentEventTarget;
/**
* Fires all registered listeners in this listenable for the given
* type and capture mode, passing them the given eventObject. This
* does not perform actual capture/bubble. Only implementors of the
* interface should be using this.
*
* @param {string|!goog.events.EventId<EVENTOBJ>} type The type of the
* listeners to fire.
* @param {boolean} capture The capture mode of the listeners to fire.
* @param {EVENTOBJ} eventObject The event object to fire.
* @return {boolean} Whether all listeners succeeded without
* attempting to prevent default behavior. If any listener returns
* false or called goog.events.Event#preventDefault, this returns
* false.
* @template EVENTOBJ
*/
goog.events.Listenable.prototype.fireListeners;
/**
* Gets all listeners in this listenable for the given type and
* capture mode.
*
* @param {string|!goog.events.EventId} type The type of the listeners to fire.
* @param {boolean} capture The capture mode of the listeners to fire.
* @return {!Array<goog.events.ListenableKey>} An array of registered
* listeners.
* @template EVENTOBJ
*/
goog.events.Listenable.prototype.getListeners;
/**
* Gets the goog.events.ListenableKey for the event or null if no such
* listener is in use.
*
* @param {string|!goog.events.EventId<EVENTOBJ>} type The name of the event
* without the 'on' prefix.
* @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener The
* listener function to get.
* @param {boolean} capture Whether the listener is a capturing listener.
* @param {SCOPE=} opt_listenerScope Object in whose scope to call the
* listener.
* @return {goog.events.ListenableKey} the found listener or null if not found.
* @template SCOPE,EVENTOBJ
*/
goog.events.Listenable.prototype.getListener;
/**
* Whether there is any active listeners matching the specified
* signature. If either the type or capture parameters are
* unspecified, the function will match on the remaining criteria.
*
* @param {string|!goog.events.EventId<EVENTOBJ>=} opt_type Event type.
* @param {boolean=} opt_capture Whether to check for capture or bubble
* listeners.
* @return {boolean} Whether there is any active listeners matching
* the requested type and/or capture phase.
* @template EVENTOBJ
*/
goog.events.Listenable.prototype.hasListener;
/**
* An interface that describes a single registered listener.
* @interface
*/
goog.events.ListenableKey = function() {};
/**
* Counter used to create a unique key
* @type {number}
* @private
*/
goog.events.ListenableKey.counter_ = 0;
/**
* Reserves a key to be used for ListenableKey#key field.
* @return {number} A number to be used to fill ListenableKey#key
* field.
*/
goog.events.ListenableKey.reserveKey = function() {
return ++goog.events.ListenableKey.counter_;
};
/**
* The source event target.
* @type {!(Object|goog.events.Listenable|goog.events.EventTarget)}
*/
goog.events.ListenableKey.prototype.src;
/**
* The event type the listener is listening to.
* @type {string}
*/
goog.events.ListenableKey.prototype.type;
/**
* The listener function.
* @type {function(?):?|{handleEvent:function(?):?}|null}
*/
goog.events.ListenableKey.prototype.listener;
/**
* Whether the listener works on capture phase.
* @type {boolean}
*/
goog.events.ListenableKey.prototype.capture;
/**
* The 'this' object for the listener function's scope.
* @type {Object}
*/
goog.events.ListenableKey.prototype.handler;
/**
* A globally unique number to identify the key.
* @type {number}
*/
goog.events.ListenableKey.prototype.key;
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.Listenable
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.ListenableTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,29 @@
// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.ListenableTest');
goog.setTestOnly('goog.events.ListenableTest');
goog.require('goog.events.Listenable');
goog.require('goog.testing.jsunit');
function testIsImplementedBy() {
var ListenableClass = function() {};
goog.events.Listenable.addImplementation(ListenableClass);
var NonListenableClass = function() {};
assertTrue(goog.events.Listenable.isImplementedBy(new ListenableClass()));
assertFalse(goog.events.Listenable.isImplementedBy(new NonListenableClass()));
}
@@ -0,0 +1,131 @@
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Listener object.
* @see ../demos/events.html
*/
goog.provide('goog.events.Listener');
goog.require('goog.events.ListenableKey');
/**
* Simple class that stores information about a listener
* @param {!Function} listener Callback function.
* @param {Function} proxy Wrapper for the listener that patches the event.
* @param {EventTarget|goog.events.Listenable} src Source object for
* the event.
* @param {string} type Event type.
* @param {boolean} capture Whether in capture or bubble phase.
* @param {Object=} opt_handler Object in whose context to execute the callback.
* @implements {goog.events.ListenableKey}
* @constructor
*/
goog.events.Listener = function(
listener, proxy, src, type, capture, opt_handler) {
if (goog.events.Listener.ENABLE_MONITORING) {
this.creationStack = new Error().stack;
}
/**
* Callback function.
* @type {Function}
*/
this.listener = listener;
/**
* A wrapper over the original listener. This is used solely to
* handle native browser events (it is used to simulate the capture
* phase and to patch the event object).
* @type {Function}
*/
this.proxy = proxy;
/**
* Object or node that callback is listening to
* @type {EventTarget|goog.events.Listenable}
*/
this.src = src;
/**
* The event type.
* @const {string}
*/
this.type = type;
/**
* Whether the listener is being called in the capture or bubble phase
* @const {boolean}
*/
this.capture = !!capture;
/**
* Optional object whose context to execute the listener in
* @type {Object|undefined}
*/
this.handler = opt_handler;
/**
* The key of the listener.
* @const {number}
* @override
*/
this.key = goog.events.ListenableKey.reserveKey();
/**
* Whether to remove the listener after it has been called.
* @type {boolean}
*/
this.callOnce = false;
/**
* Whether the listener has been removed.
* @type {boolean}
*/
this.removed = false;
};
/**
* @define {boolean} Whether to enable the monitoring of the
* goog.events.Listener instances. Switching on the monitoring is only
* recommended for debugging because it has a significant impact on
* performance and memory usage. If switched off, the monitoring code
* compiles down to 0 bytes.
*/
goog.define('goog.events.Listener.ENABLE_MONITORING', false);
/**
* If monitoring the goog.events.Listener instances is enabled, stores the
* creation stack trace of the Disposable instance.
* @type {string}
*/
goog.events.Listener.prototype.creationStack;
/**
* Marks this listener as removed. This also remove references held by
* this listener object (such as listener and event source).
*/
goog.events.Listener.prototype.markAsRemoved = function() {
this.removed = true;
this.listener = null;
this.proxy = null;
this.src = null;
this.handler = null;
};
@@ -0,0 +1,308 @@
// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview A map of listeners that provides utility functions to
* deal with listeners on an event target. Used by
* {@code goog.events.EventTarget}.
*
* WARNING: Do not use this class from outside goog.events package.
*
* @visibility {//closure/goog/bin/sizetests:__pkg__}
* @visibility {//closure/goog/events:__pkg__}
* @visibility {//closure/goog/labs/events:__pkg__}
*/
goog.provide('goog.events.ListenerMap');
goog.require('goog.array');
goog.require('goog.events.Listener');
goog.require('goog.object');
/**
* Creates a new listener map.
* @param {EventTarget|goog.events.Listenable} src The src object.
* @constructor
* @final
*/
goog.events.ListenerMap = function(src) {
/** @type {EventTarget|goog.events.Listenable} */
this.src = src;
/**
* Maps of event type to an array of listeners.
* @type {Object<string, !Array<!goog.events.Listener>>}
*/
this.listeners = {};
/**
* The count of types in this map that have registered listeners.
* @private {number}
*/
this.typeCount_ = 0;
};
/**
* @return {number} The count of event types in this map that actually
* have registered listeners.
*/
goog.events.ListenerMap.prototype.getTypeCount = function() {
return this.typeCount_;
};
/**
* @return {number} Total number of registered listeners.
*/
goog.events.ListenerMap.prototype.getListenerCount = function() {
var count = 0;
for (var type in this.listeners) {
count += this.listeners[type].length;
}
return count;
};
/**
* Adds an event listener. A listener can only be added once to an
* object and if it is added again the key for the listener is
* returned.
*
* Note that a one-off listener will not change an existing listener,
* if any. On the other hand a normal listener will change existing
* one-off listener to become a normal listener.
*
* @param {string|!goog.events.EventId} type The listener event type.
* @param {!Function} listener This listener callback method.
* @param {boolean} callOnce Whether the listener is a one-off
* listener.
* @param {boolean=} opt_useCapture The capture mode of the listener.
* @param {Object=} opt_listenerScope Object in whose scope to call the
* listener.
* @return {goog.events.ListenableKey} Unique key for the listener.
*/
goog.events.ListenerMap.prototype.add = function(
type, listener, callOnce, opt_useCapture, opt_listenerScope) {
var typeStr = type.toString();
var listenerArray = this.listeners[typeStr];
if (!listenerArray) {
listenerArray = this.listeners[typeStr] = [];
this.typeCount_++;
}
var listenerObj;
var index = goog.events.ListenerMap.findListenerIndex_(
listenerArray, listener, opt_useCapture, opt_listenerScope);
if (index > -1) {
listenerObj = listenerArray[index];
if (!callOnce) {
// Ensure that, if there is an existing callOnce listener, it is no
// longer a callOnce listener.
listenerObj.callOnce = false;
}
} else {
listenerObj = new goog.events.Listener(
listener, null, this.src, typeStr, !!opt_useCapture, opt_listenerScope);
listenerObj.callOnce = callOnce;
listenerArray.push(listenerObj);
}
return listenerObj;
};
/**
* Removes a matching listener.
* @param {string|!goog.events.EventId} type The listener event type.
* @param {!Function} listener This listener callback method.
* @param {boolean=} opt_useCapture The capture mode of the listener.
* @param {Object=} opt_listenerScope Object in whose scope to call the
* listener.
* @return {boolean} Whether any listener was removed.
*/
goog.events.ListenerMap.prototype.remove = function(
type, listener, opt_useCapture, opt_listenerScope) {
var typeStr = type.toString();
if (!(typeStr in this.listeners)) {
return false;
}
var listenerArray = this.listeners[typeStr];
var index = goog.events.ListenerMap.findListenerIndex_(
listenerArray, listener, opt_useCapture, opt_listenerScope);
if (index > -1) {
var listenerObj = listenerArray[index];
listenerObj.markAsRemoved();
goog.array.removeAt(listenerArray, index);
if (listenerArray.length == 0) {
delete this.listeners[typeStr];
this.typeCount_--;
}
return true;
}
return false;
};
/**
* Removes the given listener object.
* @param {goog.events.ListenableKey} listener The listener to remove.
* @return {boolean} Whether the listener is removed.
*/
goog.events.ListenerMap.prototype.removeByKey = function(listener) {
var type = listener.type;
if (!(type in this.listeners)) {
return false;
}
var removed = goog.array.remove(this.listeners[type], listener);
if (removed) {
listener.markAsRemoved();
if (this.listeners[type].length == 0) {
delete this.listeners[type];
this.typeCount_--;
}
}
return removed;
};
/**
* Removes all listeners from this map. If opt_type is provided, only
* listeners that match the given type are removed.
* @param {string|!goog.events.EventId=} opt_type Type of event to remove.
* @return {number} Number of listeners removed.
*/
goog.events.ListenerMap.prototype.removeAll = function(opt_type) {
var typeStr = opt_type && opt_type.toString();
var count = 0;
for (var type in this.listeners) {
if (!typeStr || type == typeStr) {
var listenerArray = this.listeners[type];
for (var i = 0; i < listenerArray.length; i++) {
++count;
listenerArray[i].markAsRemoved();
}
delete this.listeners[type];
this.typeCount_--;
}
}
return count;
};
/**
* Gets all listeners that match the given type and capture mode. The
* returned array is a copy (but the listener objects are not).
* @param {string|!goog.events.EventId} type The type of the listeners
* to retrieve.
* @param {boolean} capture The capture mode of the listeners to retrieve.
* @return {!Array<goog.events.ListenableKey>} An array of matching
* listeners.
*/
goog.events.ListenerMap.prototype.getListeners = function(type, capture) {
var listenerArray = this.listeners[type.toString()];
var rv = [];
if (listenerArray) {
for (var i = 0; i < listenerArray.length; ++i) {
var listenerObj = listenerArray[i];
if (listenerObj.capture == capture) {
rv.push(listenerObj);
}
}
}
return rv;
};
/**
* Gets the goog.events.ListenableKey for the event or null if no such
* listener is in use.
*
* @param {string|!goog.events.EventId} type The type of the listener
* to retrieve.
* @param {!Function} listener The listener function to get.
* @param {boolean} capture Whether the listener is a capturing listener.
* @param {Object=} opt_listenerScope Object in whose scope to call the
* listener.
* @return {goog.events.ListenableKey} the found listener or null if not found.
*/
goog.events.ListenerMap.prototype.getListener = function(
type, listener, capture, opt_listenerScope) {
var listenerArray = this.listeners[type.toString()];
var i = -1;
if (listenerArray) {
i = goog.events.ListenerMap.findListenerIndex_(
listenerArray, listener, capture, opt_listenerScope);
}
return i > -1 ? listenerArray[i] : null;
};
/**
* Whether there is a matching listener. If either the type or capture
* parameters are unspecified, the function will match on the
* remaining criteria.
*
* @param {string|!goog.events.EventId=} opt_type The type of the listener.
* @param {boolean=} opt_capture The capture mode of the listener.
* @return {boolean} Whether there is an active listener matching
* the requested type and/or capture phase.
*/
goog.events.ListenerMap.prototype.hasListener = function(
opt_type, opt_capture) {
var hasType = goog.isDef(opt_type);
var typeStr = hasType ? opt_type.toString() : '';
var hasCapture = goog.isDef(opt_capture);
return goog.object.some(
this.listeners, function(listenerArray, type) {
for (var i = 0; i < listenerArray.length; ++i) {
if ((!hasType || listenerArray[i].type == typeStr) &&
(!hasCapture || listenerArray[i].capture == opt_capture)) {
return true;
}
}
return false;
});
};
/**
* Finds the index of a matching goog.events.Listener in the given
* listenerArray.
* @param {!Array<!goog.events.Listener>} listenerArray Array of listener.
* @param {!Function} listener The listener function.
* @param {boolean=} opt_useCapture The capture flag for the listener.
* @param {Object=} opt_listenerScope The listener scope.
* @return {number} The index of the matching listener within the
* listenerArray.
* @private
*/
goog.events.ListenerMap.findListenerIndex_ = function(
listenerArray, listener, opt_useCapture, opt_listenerScope) {
for (var i = 0; i < listenerArray.length; ++i) {
var listenerObj = listenerArray[i];
if (!listenerObj.removed &&
listenerObj.listener == listener &&
listenerObj.capture == !!opt_useCapture &&
listenerObj.handler == opt_listenerScope) {
return i;
}
}
return -1;
};
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.events.ListenerMap</title>
<script src="../base.js"></script>
<script>
goog.require('goog.events.ListenerMapTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,161 @@
// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Tests for listenermap.js.
*
* Most of this class functionality is already tested by
* goog.events.EventTarget tests. This test file only provides tests
* for features that are not direct duplicates of tests in
* goog.events.EventTarget.
*/
goog.provide('goog.events.ListenerMapTest');
goog.setTestOnly('goog.events.ListenerMapTest');
goog.require('goog.dispose');
goog.require('goog.events');
goog.require('goog.events.EventId');
goog.require('goog.events.EventTarget');
goog.require('goog.events.ListenerMap');
goog.require('goog.testing.jsunit');
var et, map;
var handler1 = function() {};
var handler2 = function() {};
var handler3 = function() {};
var CLICK_EVENT_ID = new goog.events.EventId(goog.events.getUniqueId('click'));
function setUp() {
et = new goog.events.EventTarget();
map = new goog.events.ListenerMap(et);
}
function tearDown() {
goog.dispose(et);
}
function testGetTypeCount() {
assertEquals(0, map.getTypeCount());
map.add('click', handler1, false);
assertEquals(1, map.getTypeCount());
map.remove('click', handler1);
assertEquals(0, map.getTypeCount());
map.add(CLICK_EVENT_ID, handler1, false);
assertEquals(1, map.getTypeCount());
map.remove(CLICK_EVENT_ID, handler1);
assertEquals(0, map.getTypeCount());
map.add('click', handler1, false, true);
assertEquals(1, map.getTypeCount());
map.remove('click', handler1, true);
assertEquals(0, map.getTypeCount());
map.add(CLICK_EVENT_ID, handler1, false, true);
assertEquals(1, map.getTypeCount());
map.remove(CLICK_EVENT_ID, handler1, true);
assertEquals(0, map.getTypeCount());
map.add('click', handler1, false);
map.add('click', handler1, false, true);
assertEquals(1, map.getTypeCount());
map.remove('click', handler1);
assertEquals(1, map.getTypeCount());
map.remove('click', handler1, true);
assertEquals(0, map.getTypeCount());
map.add(CLICK_EVENT_ID, handler1, false);
map.add(CLICK_EVENT_ID, handler1, false, true);
assertEquals(1, map.getTypeCount());
map.remove(CLICK_EVENT_ID, handler1);
assertEquals(1, map.getTypeCount());
map.remove(CLICK_EVENT_ID, handler1, true);
assertEquals(0, map.getTypeCount());
map.add('click', handler1, false);
map.add('touchstart', handler2, false);
map.add(CLICK_EVENT_ID, handler3, false);
assertEquals(3, map.getTypeCount());
map.remove(CLICK_EVENT_ID, handler3);
assertEquals(2, map.getTypeCount());
map.remove('touchstart', handler2);
assertEquals(1, map.getTypeCount());
map.remove('click', handler1);
assertEquals(0, map.getTypeCount());
}
function testGetListenerCount() {
assertEquals(0, map.getListenerCount());
map.add('click', handler1, false);
assertEquals(1, map.getListenerCount());
map.remove('click', handler1);
assertEquals(0, map.getListenerCount());
map.add(CLICK_EVENT_ID, handler1, false);
assertEquals(1, map.getListenerCount());
map.remove(CLICK_EVENT_ID, handler1);
assertEquals(0, map.getListenerCount());
map.add('click', handler1, false, true);
assertEquals(1, map.getListenerCount());
map.remove('click', handler1, true);
assertEquals(0, map.getListenerCount());
map.add(CLICK_EVENT_ID, handler1, false, true);
assertEquals(1, map.getListenerCount());
map.remove(CLICK_EVENT_ID, handler1, true);
assertEquals(0, map.getListenerCount());
map.add('click', handler1, false);
map.add('click', handler1, false, true);
assertEquals(2, map.getListenerCount());
map.remove('click', handler1);
map.remove('click', handler1, true);
assertEquals(0, map.getListenerCount());
map.add(CLICK_EVENT_ID, handler1, false);
map.add(CLICK_EVENT_ID, handler1, false, true);
assertEquals(2, map.getListenerCount());
map.remove(CLICK_EVENT_ID, handler1);
map.remove(CLICK_EVENT_ID, handler1, true);
assertEquals(0, map.getListenerCount());
map.add('click', handler1, false);
map.add('touchstart', handler2, false);
map.add(CLICK_EVENT_ID, handler3, false);
assertEquals(3, map.getListenerCount());
map.remove(CLICK_EVENT_ID, handler3);
map.remove('touchstart', handler2);
map.remove('click', handler1);
assertEquals(0, map.getListenerCount());
}
function testListenerSourceIsSetCorrectly() {
map.add('click', handler1, false);
var listener = map.getListener('click', handler1);
assertEquals(et, listener.src);
map.add(CLICK_EVENT_ID, handler2, false);
listener = map.getListener(CLICK_EVENT_ID, handler2);
assertEquals(et, listener.src);
}
@@ -0,0 +1,296 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This event wrapper will dispatch an event when the user uses
* the mouse wheel to scroll an element. You can get the direction by checking
* the deltaX and deltaY properties of the event.
*
* This class aims to smooth out inconsistencies between browser platforms with
* regards to mousewheel events, but we do not cover every possible
* software/hardware combination out there, some of which occasionally produce
* very large deltas in mousewheel events. If your application wants to guard
* against extremely large deltas, use the setMaxDeltaX and setMaxDeltaY APIs
* to set maximum values that make sense for your application.
*
* @author arv@google.com (Erik Arvidsson)
* @see ../demos/mousewheelhandler.html
*/
goog.provide('goog.events.MouseWheelEvent');
goog.provide('goog.events.MouseWheelHandler');
goog.provide('goog.events.MouseWheelHandler.EventType');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.math');
goog.require('goog.style');
goog.require('goog.userAgent');
/**
* This event handler allows you to catch mouse wheel events in a consistent
* manner.
* @param {Element|Document} element The element to listen to the mouse wheel
* event on.
* @param {boolean=} opt_capture Whether to handle the mouse wheel event in
* capture phase.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.events.MouseWheelHandler = function(element, opt_capture) {
goog.events.EventTarget.call(this);
/**
* This is the element that we will listen to the real mouse wheel events on.
* @type {Element|Document}
* @private
*/
this.element_ = element;
var rtlElement = goog.dom.isElement(this.element_) ?
/** @type {Element} */ (this.element_) :
(this.element_ ? /** @type {Document} */ (this.element_).body : null);
/**
* True if the element exists and is RTL, false otherwise.
* @type {boolean}
* @private
*/
this.isRtl_ = !!rtlElement && goog.style.isRightToLeft(rtlElement);
var type = goog.userAgent.GECKO ? 'DOMMouseScroll' : 'mousewheel';
/**
* The key returned from the goog.events.listen.
* @type {goog.events.Key}
* @private
*/
this.listenKey_ = goog.events.listen(this.element_, type, this, opt_capture);
};
goog.inherits(goog.events.MouseWheelHandler, goog.events.EventTarget);
/**
* Enum type for the events fired by the mouse wheel handler.
* @enum {string}
*/
goog.events.MouseWheelHandler.EventType = {
MOUSEWHEEL: 'mousewheel'
};
/**
* Optional maximum magnitude for x delta on each mousewheel event.
* @type {number|undefined}
* @private
*/
goog.events.MouseWheelHandler.prototype.maxDeltaX_;
/**
* Optional maximum magnitude for y delta on each mousewheel event.
* @type {number|undefined}
* @private
*/
goog.events.MouseWheelHandler.prototype.maxDeltaY_;
/**
* @param {number} maxDeltaX Maximum magnitude for x delta on each mousewheel
* event. Should be non-negative.
*/
goog.events.MouseWheelHandler.prototype.setMaxDeltaX = function(maxDeltaX) {
this.maxDeltaX_ = maxDeltaX;
};
/**
* @param {number} maxDeltaY Maximum magnitude for y delta on each mousewheel
* event. Should be non-negative.
*/
goog.events.MouseWheelHandler.prototype.setMaxDeltaY = function(maxDeltaY) {
this.maxDeltaY_ = maxDeltaY;
};
/**
* Handles the events on the element.
* @param {goog.events.BrowserEvent} e The underlying browser event.
*/
goog.events.MouseWheelHandler.prototype.handleEvent = function(e) {
var deltaX = 0;
var deltaY = 0;
var detail = 0;
var be = e.getBrowserEvent();
if (be.type == 'mousewheel') {
var wheelDeltaScaleFactor = 1;
if (goog.userAgent.IE ||
goog.userAgent.WEBKIT &&
(goog.userAgent.WINDOWS || goog.userAgent.isVersionOrHigher('532.0'))) {
// In IE we get a multiple of 120; we adjust to a multiple of 3 to
// represent number of lines scrolled (like Gecko).
// Newer versions of Webkit match IE behavior, and WebKit on
// Windows also matches IE behavior.
// See bug https://bugs.webkit.org/show_bug.cgi?id=24368
wheelDeltaScaleFactor = 40;
}
detail = goog.events.MouseWheelHandler.smartScale_(
-be.wheelDelta, wheelDeltaScaleFactor);
if (goog.isDef(be.wheelDeltaX)) {
// Webkit has two properties to indicate directional scroll, and
// can scroll both directions at once.
deltaX = goog.events.MouseWheelHandler.smartScale_(
-be.wheelDeltaX, wheelDeltaScaleFactor);
deltaY = goog.events.MouseWheelHandler.smartScale_(
-be.wheelDeltaY, wheelDeltaScaleFactor);
} else {
deltaY = detail;
}
// Historical note: Opera (pre 9.5) used to negate the detail value.
} else { // Gecko
// Gecko returns multiple of 3 (representing the number of lines scrolled)
detail = be.detail;
// Gecko sometimes returns really big values if the user changes settings to
// scroll a whole page per scroll
if (detail > 100) {
detail = 3;
} else if (detail < -100) {
detail = -3;
}
// Firefox 3.1 adds an axis field to the event to indicate direction of
// scroll. See https://developer.mozilla.org/en/Gecko-Specific_DOM_Events
if (goog.isDef(be.axis) && be.axis === be.HORIZONTAL_AXIS) {
deltaX = detail;
} else {
deltaY = detail;
}
}
if (goog.isNumber(this.maxDeltaX_)) {
deltaX = goog.math.clamp(deltaX, -this.maxDeltaX_, this.maxDeltaX_);
}
if (goog.isNumber(this.maxDeltaY_)) {
deltaY = goog.math.clamp(deltaY, -this.maxDeltaY_, this.maxDeltaY_);
}
// Don't clamp 'detail', since it could be ambiguous which axis it refers to
// and because it's informally deprecated anyways.
// For horizontal scrolling we need to flip the value for RTL grids.
if (this.isRtl_) {
deltaX = -deltaX;
}
var newEvent = new goog.events.MouseWheelEvent(detail, be, deltaX, deltaY);
this.dispatchEvent(newEvent);
};
/**
* Helper for scaling down a mousewheel delta by a scale factor, if appropriate.
* @param {number} mouseWheelDelta Delta from a mouse wheel event. Expected to
* be an integer.
* @param {number} scaleFactor Factor to scale the delta down by. Expected to
* be an integer.
* @return {number} Scaled-down delta value, or the original delta if the
* scaleFactor does not appear to be applicable.
* @private
*/
goog.events.MouseWheelHandler.smartScale_ = function(mouseWheelDelta,
scaleFactor) {
// The basic problem here is that in Webkit on Mac and Linux, we can get two
// very different types of mousewheel events: from continuous devices
// (touchpads, Mighty Mouse) or non-continuous devices (normal wheel mice).
//
// Non-continuous devices in Webkit get their wheel deltas scaled up to
// behave like IE. Continuous devices return much smaller unscaled values
// (which most of the time will not be cleanly divisible by the IE scale
// factor), so we should not try to normalize them down.
//
// Detailed discussion:
// https://bugs.webkit.org/show_bug.cgi?id=29601
// http://trac.webkit.org/browser/trunk/WebKit/chromium/src/mac/WebInputEventFactory.mm#L1063
if (goog.userAgent.WEBKIT &&
(goog.userAgent.MAC || goog.userAgent.LINUX) &&
(mouseWheelDelta % scaleFactor) != 0) {
return mouseWheelDelta;
} else {
return mouseWheelDelta / scaleFactor;
}
};
/** @override */
goog.events.MouseWheelHandler.prototype.disposeInternal = function() {
goog.events.MouseWheelHandler.superClass_.disposeInternal.call(this);
goog.events.unlistenByKey(this.listenKey_);
this.listenKey_ = null;
};
/**
* A base class for mouse wheel events. This is used with the
* MouseWheelHandler.
*
* @param {number} detail The number of rows the user scrolled.
* @param {Event} browserEvent Browser event object.
* @param {number} deltaX The number of rows the user scrolled in the X
* direction.
* @param {number} deltaY The number of rows the user scrolled in the Y
* direction.
* @constructor
* @extends {goog.events.BrowserEvent}
* @final
*/
goog.events.MouseWheelEvent = function(detail, browserEvent, deltaX, deltaY) {
goog.events.BrowserEvent.call(this, browserEvent);
this.type = goog.events.MouseWheelHandler.EventType.MOUSEWHEEL;
/**
* The number of lines the user scrolled
* @type {number}
* NOTE: Informally deprecated. Use deltaX and deltaY instead, they provide
* more information.
*/
this.detail = detail;
/**
* The number of "lines" scrolled in the X direction.
*
* Note that not all browsers provide enough information to distinguish
* horizontal and vertical scroll events, so for these unsupported browsers,
* we will always have a deltaX of 0, even if the user scrolled their mouse
* wheel or trackpad sideways.
*
* Currently supported browsers are Webkit and Firefox 3.1 or later.
*
* @type {number}
*/
this.deltaX = deltaX;
/**
* The number of lines scrolled in the Y direction.
* @type {number}
*/
this.deltaY = deltaY;
};
goog.inherits(goog.events.MouseWheelEvent, goog.events.BrowserEvent);
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.MouseWheelHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.MouseWheelHandlerTest');
</script>
</head>
<body>
<div id="foo">
</div>
<div id="fooRtl" dir="rtl">
</div>
<div id="log" style="position:absolute;right:0;top:0">
Logged events:
</div>
</body>
</html>
@@ -0,0 +1,384 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.MouseWheelHandlerTest');
goog.setTestOnly('goog.events.MouseWheelHandlerTest');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.MouseWheelEvent');
goog.require('goog.events.MouseWheelHandler');
goog.require('goog.functions');
goog.require('goog.string');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var log;
var stubs = new goog.testing.PropertyReplacer();
var DEFAULT_TYPE = 'mousewheel';
var GECKO_TYPE = 'DOMMouseScroll';
var HORIZONTAL = 'h';
var VERTICAL = 'v';
var mouseWheelEvent;
var mouseWheelEventRtl;
var mouseWheelHandler;
var mouseWheelHandlerRtl;
function setUpPage() {
log = goog.dom.getElement('log');
}
function setUp() {
stubs.remove(goog, 'userAgent');
}
function tearDown() {
stubs.reset();
goog.dispose(mouseWheelHandler);
goog.dispose(mouseWheelHandlerRtl);
mouseWheelHandlerRtl = null;
mouseWheelHandler = null;
mouseWheelEvent = null;
mouseWheelEventRtl = null;
}
function tearDownPage() {
// Create interactive demo.
mouseWheelHandler = new goog.events.MouseWheelHandler(document.body);
goog.events.listen(mouseWheelHandler,
goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
function(e) {
log.innerHTML += goog.string.subs('<br />(deltaX, deltaY): (%s, %s)',
e.deltaX, e.deltaY);
});
}
function testIeStyleMouseWheel() {
goog.userAgent = {
OPERA: false,
IE: true,
GECKO: false,
CAMINO: false,
WEBKIT: false
};
createHandlerAndListen();
// Non-gecko, non-webkit events get wheelDelta divided by -40 to get detail.
handleEvent(createFakeMouseWheelEvent(DEFAULT_TYPE, 120));
assertMouseWheelEvent(-3, 0, -3);
handleEvent(createFakeMouseWheelEvent(DEFAULT_TYPE, -120));
assertMouseWheelEvent(3, 0, 3);
handleEvent(createFakeMouseWheelEvent(DEFAULT_TYPE, 1200));
assertMouseWheelEvent(-30, 0, -30);
}
function testNullBody() {
goog.userAgent = {
OPERA: false,
IE: true,
GECKO: false,
CAMINO: false,
WEBKIT: false
};
var documentObjectWithNoBody = { };
goog.testing.events.mixinListenable(documentObjectWithNoBody);
mouseWheelHandler =
new goog.events.MouseWheelHandler(documentObjectWithNoBody);
}
function testGeckoStyleMouseWheel() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: true,
CAMINO: false,
WEBKIT: false
};
createHandlerAndListen();
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, 3));
assertMouseWheelEvent(3, 0, 3);
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, -12));
assertMouseWheelEvent(-12, 0, -12);
// Really big values should get truncated to +-3.
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, 1200));
assertMouseWheelEvent(3, 0, 3);
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, -1200));
assertMouseWheelEvent(-3, 0, -3);
// Test scrolling with the additional axis property.
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, 3, VERTICAL));
assertMouseWheelEvent(3, 0, 3);
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, 3, HORIZONTAL));
assertMouseWheelEvent(3, 3, 0);
handleEvent(createFakeMouseWheelEvent(GECKO_TYPE, null, -3, HORIZONTAL));
assertMouseWheelEvent(-3, -3, 0);
}
function testWebkitStyleMouseWheel_ieStyle() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: false,
CAMINO: false,
WEBKIT: true,
WINDOWS: true
};
createHandlerAndListen();
// IE-style Webkit events get wheelDelta divided by -40 to get detail.
handleEvent(createFakeWebkitMouseWheelEvent(-40, 0));
assertMouseWheelEvent(1, 1, 0);
handleEvent(createFakeWebkitMouseWheelEvent(120, 0));
assertMouseWheelEvent(-3, -3, 0);
handleEvent(createFakeWebkitMouseWheelEvent(0, 120));
assertMouseWheelEvent(-3, 0, -3);
handleEvent(createFakeWebkitMouseWheelEvent(0, -40));
assertMouseWheelEvent(1, 0, 1);
handleEvent(createFakeWebkitMouseWheelEvent(80, -40));
assertMouseWheelEvent(-2, -2, 1);
}
function testWebkitStyleMouseWheel_ieStyleOnLinux() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: false,
CAMINO: false,
WEBKIT: true,
WINDOWS: false,
LINUX: true
};
runWebKitContinousAndDiscreteEventsTest();
}
function testWebkitStyleMouseWheel_ieStyleOnMac() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: false,
CAMINO: false,
WEBKIT: true,
WINDOWS: false,
MAC: true
};
runWebKitContinousAndDiscreteEventsTest();
}
function runWebKitContinousAndDiscreteEventsTest() {
goog.userAgent.isVersionOrHigher = goog.functions.TRUE;
createHandlerAndListen();
// IE-style wheel events.
handleEvent(createFakeWebkitMouseWheelEvent(0, -40));
assertMouseWheelEvent(1, 0, 1);
handleEvent(createFakeWebkitMouseWheelEvent(80, -40));
assertMouseWheelEvent(-2, -2, 1);
// Even in Webkit versions that usually behave in IE style, sometimes wheel
// events don't behave; this has been observed for instance with Macbook
// and Chrome OS touchpads in Webkit 534.10+.
handleEvent(createFakeWebkitMouseWheelEvent(-3, 5));
assertMouseWheelEvent(-5, 3, -5);
handleEvent(createFakeWebkitMouseWheelEvent(4, -7));
assertMouseWheelEvent(7, -4, 7);
}
function testWebkitStyleMouseWheel_nonIeStyle() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: false,
CAMINO: false,
WEBKIT: true,
WINDOWS: false
};
goog.userAgent.isVersionOrHigher = goog.functions.FALSE;
createHandlerAndListen();
// non-IE-style Webkit events do not get wheelDelta scaled
handleEvent(createFakeWebkitMouseWheelEvent(-1, 0));
assertMouseWheelEvent(1, 1, 0);
handleEvent(createFakeWebkitMouseWheelEvent(3, 0));
assertMouseWheelEvent(-3, -3, 0);
handleEvent(createFakeWebkitMouseWheelEvent(0, 3));
assertMouseWheelEvent(-3, 0, -3);
handleEvent(createFakeWebkitMouseWheelEvent(0, -1));
assertMouseWheelEvent(1, 0, 1);
handleEvent(createFakeWebkitMouseWheelEvent(2, -1));
assertMouseWheelEvent(-2, -2, 1);
}
function testMaxDeltaX() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: false,
CAMINO: false,
WEBKIT: true,
WINDOWS: true
};
createHandlerAndListen();
// IE-style Webkit events get wheelDelta divided by -40 to get detail.
handleEvent(createFakeWebkitMouseWheelEvent(-120, 0));
assertMouseWheelEvent(3, 3, 0);
mouseWheelHandler.setMaxDeltaX(3);
mouseWheelHandlerRtl.setMaxDeltaX(3);
handleEvent(createFakeWebkitMouseWheelEvent(-120, 0));
assertMouseWheelEvent(3, 3, 0);
mouseWheelHandler.setMaxDeltaX(2);
mouseWheelHandlerRtl.setMaxDeltaX(2);
handleEvent(createFakeWebkitMouseWheelEvent(-120, 0));
assertMouseWheelEvent(3, 2, 0);
handleEvent(createFakeWebkitMouseWheelEvent(0, -120));
assertMouseWheelEvent(3, 0, 3);
}
function testMaxDeltaY() {
goog.userAgent = {
OPERA: false,
IE: false,
GECKO: false,
CAMINO: false,
WEBKIT: true,
WINDOWS: true
};
createHandlerAndListen();
// IE-style Webkit events get wheelDelta divided by -40 to get detail.
handleEvent(createFakeWebkitMouseWheelEvent(0, -120));
assertMouseWheelEvent(3, 0, 3);
mouseWheelHandler.setMaxDeltaY(3);
mouseWheelHandlerRtl.setMaxDeltaY(3);
handleEvent(createFakeWebkitMouseWheelEvent(0, -120));
assertMouseWheelEvent(3, 0, 3);
mouseWheelHandler.setMaxDeltaY(2);
mouseWheelHandlerRtl.setMaxDeltaY(2);
handleEvent(createFakeWebkitMouseWheelEvent(0, -120));
assertMouseWheelEvent(3, 0, 2);
handleEvent(createFakeWebkitMouseWheelEvent(-120, 0));
assertMouseWheelEvent(3, 3, 0);
}
// Be sure to call this after setting up goog.userAgent mock and not before.
function createHandlerAndListen() {
mouseWheelHandler = new goog.events.MouseWheelHandler(
goog.dom.getElement('foo'));
goog.events.listen(mouseWheelHandler,
goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
function(e) { mouseWheelEvent = e; });
mouseWheelHandlerRtl = new goog.events.MouseWheelHandler(
goog.dom.getElement('fooRtl'));
goog.events.listen(mouseWheelHandlerRtl,
goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
function(e) { mouseWheelEventRtl = e; });
}
function handleEvent(event) {
mouseWheelHandler.handleEvent(event);
mouseWheelHandlerRtl.handleEvent(event);
}
function assertMouseWheelEvent(expectedDetail, expectedDeltaX,
expectedDeltaY) {
assertTrue('event should be non-null', !!mouseWheelEvent);
assertTrue('event should have correct JS type',
mouseWheelEvent instanceof goog.events.MouseWheelEvent);
assertEquals('event should have correct detail property',
expectedDetail, mouseWheelEvent.detail);
assertEquals('event should have correct deltaX property',
expectedDeltaX, mouseWheelEvent.deltaX);
assertEquals('event should have correct deltaY property',
expectedDeltaY, mouseWheelEvent.deltaY);
// RTL
assertTrue('event should be non-null', !!mouseWheelEventRtl);
assertTrue('event should have correct JS type',
mouseWheelEventRtl instanceof goog.events.MouseWheelEvent);
assertEquals('event should have correct detail property',
expectedDetail, mouseWheelEventRtl.detail);
assertEquals('event should have correct deltaX property',
-expectedDeltaX, mouseWheelEventRtl.deltaX);
assertEquals('event should have correct deltaY property',
expectedDeltaY, mouseWheelEventRtl.deltaY);
}
function createFakeMouseWheelEvent(type, opt_wheelDelta, opt_detail,
opt_axis, opt_wheelDeltaX, opt_wheelDeltaY) {
var event = {
type: type,
wheelDelta: goog.isDef(opt_wheelDelta) ? opt_wheelDelta : undefined,
detail: goog.isDef(opt_detail) ? opt_detail : undefined,
axis: opt_axis || undefined,
wheelDeltaX: goog.isDef(opt_wheelDeltaX) ? opt_wheelDeltaX : undefined,
wheelDeltaY: goog.isDef(opt_wheelDeltaY) ? opt_wheelDeltaY : undefined,
// These two are constants defined on the event in FF3.1 and later.
// It doesn't matter exactly what they are, and it doesn't affect
// our simulations of other browsers.
HORIZONTAL_AXIS: HORIZONTAL,
VERTICAL_AXIS: VERTICAL
};
return new goog.events.BrowserEvent(event);
}
function createFakeWebkitMouseWheelEvent(wheelDeltaX, wheelDeltaY) {
return createFakeMouseWheelEvent(DEFAULT_TYPE,
Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) ?
wheelDeltaX : wheelDeltaY,
undefined, undefined, wheelDeltaX, wheelDeltaY);
}
@@ -0,0 +1,159 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This event handler will dispatch events when
* {@code navigator.onLine} changes. HTML5 defines two events, online and
* offline that is fired on the window. As of today 3 browsers support these
* events: Firefox 3 (Gecko 1.9), Opera 9.5, and IE8. If we have any of these
* we listen to the 'online' and 'offline' events on the current window
* object. Otherwise we poll the navigator.onLine property to detect changes.
*
* Note that this class only reflects what the browser tells us and this usually
* only reflects changes to the File -> Work Offline menu item.
*
* @author arv@google.com (Erik Arvidsson)
* @see ../demos/onlinehandler.html
*/
// TODO(arv): We should probably implement some kind of polling service and/or
// a poll for changes event handler that can be used to fire events when a state
// changes.
goog.provide('goog.events.OnlineHandler');
goog.provide('goog.events.OnlineHandler.EventType');
goog.require('goog.Timer');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.net.NetworkStatusMonitor');
/**
* Basic object for detecting whether the online state changes.
* @constructor
* @extends {goog.events.EventTarget}
* @implements {goog.net.NetworkStatusMonitor}
*/
goog.events.OnlineHandler = function() {
goog.events.OnlineHandler.base(this, 'constructor');
/**
* @private {goog.events.EventHandler<!goog.events.OnlineHandler>}
*/
this.eventHandler_ = new goog.events.EventHandler(this);
// Some browsers do not support navigator.onLine and therefore we don't
// bother setting up events or timers.
if (!goog.events.BrowserFeature.HAS_NAVIGATOR_ONLINE_PROPERTY) {
return;
}
if (goog.events.BrowserFeature.HAS_HTML5_NETWORK_EVENT_SUPPORT) {
var target =
goog.events.BrowserFeature.HTML5_NETWORK_EVENTS_FIRE_ON_BODY ?
document.body : window;
this.eventHandler_.listen(target,
[goog.events.EventType.ONLINE, goog.events.EventType.OFFLINE],
this.handleChange_);
} else {
this.online_ = this.isOnline();
this.timer_ = new goog.Timer(goog.events.OnlineHandler.POLL_INTERVAL_);
this.eventHandler_.listen(this.timer_, goog.Timer.TICK, this.handleTick_);
this.timer_.start();
}
};
goog.inherits(goog.events.OnlineHandler, goog.events.EventTarget);
/**
* Enum for the events dispatched by the OnlineHandler.
* @enum {string}
* @deprecated Use goog.net.NetworkStatusMonitor.EventType instead.
*/
goog.events.OnlineHandler.EventType = goog.net.NetworkStatusMonitor.EventType;
/**
* The time to wait before checking the {@code navigator.onLine} again.
* @type {number}
* @private
*/
goog.events.OnlineHandler.POLL_INTERVAL_ = 250;
/**
* Stores the last value of the online state so we can detect if this has
* changed.
* @type {boolean}
* @private
*/
goog.events.OnlineHandler.prototype.online_;
/**
* The timer object used to poll the online state.
* @type {goog.Timer}
* @private
*/
goog.events.OnlineHandler.prototype.timer_;
/** @override */
goog.events.OnlineHandler.prototype.isOnline = function() {
return goog.events.BrowserFeature.HAS_NAVIGATOR_ONLINE_PROPERTY ?
navigator.onLine : true;
};
/**
* Called every time the timer ticks to see if the state has changed and when
* the online state changes the method handleChange_ is called.
* @private
*/
goog.events.OnlineHandler.prototype.handleTick_ = function() {
var online = this.isOnline();
if (online != this.online_) {
this.online_ = online;
this.handleChange_();
}
};
/**
* Called when the online state changes. This dispatches the
* {@code ONLINE} and {@code OFFLINE} events respectively.
* @private
*/
goog.events.OnlineHandler.prototype.handleChange_ = function() {
var type = this.isOnline() ?
goog.net.NetworkStatusMonitor.EventType.ONLINE :
goog.net.NetworkStatusMonitor.EventType.OFFLINE;
this.dispatchEvent(type);
};
/** @override */
goog.events.OnlineHandler.prototype.disposeInternal = function() {
goog.events.OnlineHandler.base(this, 'disposeInternal');
this.eventHandler_.dispose();
this.eventHandler_ = null;
if (this.timer_) {
this.timer_.dispose();
this.timer_ = null;
}
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: arv@google.com (Erik Arvidsson)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.OnlineListener
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.OnlineHandlerTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,154 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.OnlineHandlerTest');
goog.setTestOnly('goog.events.OnlineHandlerTest');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.OnlineHandler');
goog.require('goog.net.NetworkStatusMonitor');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var stubs = new goog.testing.PropertyReplacer();
var clock = new goog.testing.MockClock();
var online = true;
var onlineCount;
var offlineCount;
function listenToEvents(oh) {
onlineCount = 0;
offlineCount = 0;
goog.events.listen(oh, goog.net.NetworkStatusMonitor.EventType.ONLINE,
function(e) {
assertTrue(oh.isOnline());
onlineCount++;
});
goog.events.listen(oh, goog.net.NetworkStatusMonitor.EventType.OFFLINE,
function(e) {
assertFalse(oh.isOnline());
offlineCount++;
});
}
function setUp() {
stubs.set(goog.events.OnlineHandler.prototype, 'isOnline', function() {
return online;
});
}
function tearDown() {
stubs.reset();
clock.uninstall();
}
function testConstructAndDispose() {
var oh = new goog.events.OnlineHandler();
oh.dispose();
}
function testNoOnlineProperty() {
stubs.set(goog.events.BrowserFeature,
'HAS_NAVIGATOR_ONLINE_PROPERTY', false);
stubs.set(goog.events.EventHandler.prototype, 'listen',
goog.testing.recordFunction());
var oh = new goog.events.OnlineHandler();
assertEquals(0, oh.eventHandler_.listen.getCallCount());
oh.dispose();
}
function testNonHtml5() {
clock.install();
stubs.set(goog.events.BrowserFeature,
'HAS_HTML5_NETWORK_EVENT_SUPPORT', false);
var oh = new goog.events.OnlineHandler();
listenToEvents(oh);
clock.tick(500);
online = false;
clock.tick(500);
assertEquals(0, onlineCount);
assertEquals(1, offlineCount);
online = true;
clock.tick(500);
assertEquals(1, onlineCount);
assertEquals(1, offlineCount);
oh.dispose();
clock.dispose();
}
function testHtml5() {
stubs.set(goog.events.BrowserFeature,
'HAS_HTML5_NETWORK_EVENT_SUPPORT', true);
// Test for browsers that fire network events on document.body.
stubs.set(goog.events.BrowserFeature,
'HTML5_NETWORK_EVENTS_FIRE_ON_BODY', true);
var oh = new goog.events.OnlineHandler();
listenToEvents(oh);
online = false;
var e = new goog.events.Event('offline');
goog.events.fireListeners(document.body, e.type, false, e);
assertEquals(0, onlineCount);
assertEquals(1, offlineCount);
online = true;
e = new goog.events.Event('online');
goog.events.fireListeners(document.body, e.type, false, e);
assertEquals(1, onlineCount);
assertEquals(1, offlineCount);
oh.dispose();
// Test for browsers that fire network events on window.
stubs.set(goog.events.BrowserFeature,
'HTML5_NETWORK_EVENTS_FIRE_ON_BODY', false);
oh = new goog.events.OnlineHandler();
listenToEvents(oh);
online = false;
e = new goog.events.Event('offline');
goog.events.fireListeners(window, e.type, false, e);
assertEquals(0, onlineCount);
assertEquals(1, offlineCount);
online = true;
e = new goog.events.Event('online');
goog.events.fireListeners(window, e.type, false, e);
assertEquals(1, onlineCount);
assertEquals(1, offlineCount);
oh.dispose();
}
@@ -0,0 +1,517 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides a 'paste' event detector that works consistently
* across different browsers.
*
* IE5, IE6, IE7, Safari3.0 and FF3.0 all fire 'paste' events on textareas.
* FF2 doesn't. This class uses 'paste' events when they are available
* and uses heuristics to detect the 'paste' event when they are not available.
*
* Known issue: will not detect paste events in FF2 if you pasted exactly the
* same existing text.
* Known issue: Opera + Mac doesn't work properly because of the meta key. We
* can probably fix that. TODO(user): {@link KeyboardShortcutHandler} does not
* work either very well with opera + mac. fix that.
*
* @supported IE5, IE6, IE7, Safari3.0, Chrome, FF2.0 (linux) and FF3.0 and
* Opera (mac and windows).
*
* @see ../demos/pastehandler.html
*/
goog.provide('goog.events.PasteHandler');
goog.provide('goog.events.PasteHandler.EventType');
goog.provide('goog.events.PasteHandler.State');
goog.require('goog.Timer');
goog.require('goog.async.ConditionalDelay');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.log');
goog.require('goog.userAgent');
/**
* A paste event detector. Gets an {@code element} as parameter and fires
* {@code goog.events.PasteHandler.EventType.PASTE} events when text is
* pasted in the {@code element}. Uses heuristics to detect paste events in FF2.
* See more details of the heuristic on {@link #handleEvent_}.
*
* @param {Element} element The textarea element we are listening on.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.events.PasteHandler = function(element) {
goog.events.EventTarget.call(this);
/**
* The element that you want to listen for paste events on.
* @type {Element}
* @private
*/
this.element_ = element;
/**
* The last known value of the element. Kept to check if things changed. See
* more details on {@link #handleEvent_}.
* @type {string}
* @private
*/
this.oldValue_ = this.element_.value;
/**
* Handler for events.
* @type {goog.events.EventHandler<!goog.events.PasteHandler>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* The last time an event occurred on the element. Kept to check whether the
* last event was generated by two input events or by multiple fast key events
* that got swallowed. See more details on {@link #handleEvent_}.
* @type {number}
* @private
*/
this.lastTime_ = goog.now();
if (goog.userAgent.WEBKIT ||
goog.userAgent.IE ||
goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9')) {
// Most modern browsers support the paste event.
this.eventHandler_.listen(element, goog.events.EventType.PASTE,
this.dispatch_);
} else {
// But FF2 and Opera doesn't. we listen for a series of events to try to
// find out if a paste occurred. We enumerate and cover all known ways to
// paste text on textareas. See more details on {@link #handleEvent_}.
var events = [
goog.events.EventType.KEYDOWN,
goog.events.EventType.BLUR,
goog.events.EventType.FOCUS,
goog.events.EventType.MOUSEOVER,
'input'
];
this.eventHandler_.listen(element, events, this.handleEvent_);
}
/**
* ConditionalDelay used to poll for changes in the text element once users
* paste text. Browsers fire paste events BEFORE the text is actually present
* in the element.value property.
* @type {goog.async.ConditionalDelay}
* @private
*/
this.delay_ = new goog.async.ConditionalDelay(
goog.bind(this.checkUpdatedText_, this));
};
goog.inherits(goog.events.PasteHandler, goog.events.EventTarget);
/**
* The types of events fired by this class.
* @enum {string}
*/
goog.events.PasteHandler.EventType = {
/**
* Dispatched as soon as the paste event is detected, but before the pasted
* text has been added to the text element we're listening to.
*/
PASTE: 'paste',
/**
* Dispatched after detecting a change to the value of text element
* (within 200msec of receiving the PASTE event).
*/
AFTER_PASTE: 'after_paste'
};
/**
* The mandatory delay we expect between two {@code input} events, used to
* differentiated between non key paste events and key events.
* @type {number}
*/
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER =
400;
/**
* The period between each time we check whether the pasted text appears in the
* text element or not.
* @type {number}
* @private
*/
goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_ = 50;
/**
* The maximum amount of time we want to poll for changes.
* @type {number}
* @private
*/
goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_ = 200;
/**
* The states that this class can be found, on the paste detection algorithm.
* @enum {string}
*/
goog.events.PasteHandler.State = {
INIT: 'init',
FOCUSED: 'focused',
TYPING: 'typing'
};
/**
* The initial state of the paste detection algorithm.
* @type {goog.events.PasteHandler.State}
* @private
*/
goog.events.PasteHandler.prototype.state_ =
goog.events.PasteHandler.State.INIT;
/**
* The previous event that caused us to be on the current state.
* @type {?string}
* @private
*/
goog.events.PasteHandler.prototype.previousEvent_;
/**
* A logger, used to help us debug the algorithm.
* @type {goog.log.Logger}
* @private
*/
goog.events.PasteHandler.prototype.logger_ =
goog.log.getLogger('goog.events.PasteHandler');
/** @override */
goog.events.PasteHandler.prototype.disposeInternal = function() {
goog.events.PasteHandler.superClass_.disposeInternal.call(this);
this.eventHandler_.dispose();
this.eventHandler_ = null;
this.delay_.dispose();
this.delay_ = null;
};
/**
* Returns the current state of the paste detection algorithm. Used mostly for
* testing.
* @return {goog.events.PasteHandler.State} The current state of the class.
*/
goog.events.PasteHandler.prototype.getState = function() {
return this.state_;
};
/**
* Returns the event handler.
* @return {goog.events.EventHandler<T>} The event handler.
* @protected
* @this T
* @template T
*/
goog.events.PasteHandler.prototype.getEventHandler = function() {
return this.eventHandler_;
};
/**
* Checks whether the element.value property was updated, and if so, dispatches
* the event that let clients know that the text is available.
* @return {boolean} Whether the polling should stop or not, based on whether
* we found a text change or not.
* @private
*/
goog.events.PasteHandler.prototype.checkUpdatedText_ = function() {
if (this.oldValue_ == this.element_.value) {
return false;
}
goog.log.info(this.logger_, 'detected textchange after paste');
this.dispatchEvent(goog.events.PasteHandler.EventType.AFTER_PASTE);
return true;
};
/**
* Dispatches the paste event.
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
goog.events.PasteHandler.prototype.dispatch_ = function(e) {
var event = new goog.events.BrowserEvent(e.getBrowserEvent());
event.type = goog.events.PasteHandler.EventType.PASTE;
this.dispatchEvent(event);
// Starts polling for updates in the element.value property so we can tell
// when do dispatch the AFTER_PASTE event. (We do an initial check after an
// async delay of 0 msec since some browsers update the text right away and
// our poller will always wait one period before checking).
goog.Timer.callOnce(function() {
if (!this.checkUpdatedText_()) {
this.delay_.start(
goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_,
goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_);
}
}, 0, this);
};
/**
* The main event handler which implements a state machine.
*
* To handle FF2, we enumerate and cover all the known ways a user can paste:
*
* 1) ctrl+v, shift+insert, cmd+v
* 2) right click -> paste
* 3) edit menu -> paste
* 4) drag and drop
* 5) middle click
*
* (1) is easy and can be detected by listening for key events and finding out
* which keys are pressed. (2), (3), (4) and (5) do not generate a key event,
* so we need to listen for more than that. (2-5) all generate 'input' events,
* but so does key events. So we need to have some sort of 'how did the input
* event was generated' history algorithm.
*
* (2) is an interesting case in Opera on a Mac: since Macs does not have two
* buttons, right clicking involves pressing the CTRL key. Even more interesting
* is the fact that opera does NOT set the e.ctrlKey bit. Instead, it sets
* e.keyCode = 0.
* {@link http://www.quirksmode.org/js/keys.html}
*
* (1) is also an interesting case in Opera on a Mac: Opera is the only browser
* covered by this class that can detect the cmd key (FF2 can't apparently). And
* it fires e.keyCode = 17, which is the CTRL key code.
* {@link http://www.quirksmode.org/js/keys.html}
*
* NOTE(user, pbarry): There is an interesting thing about (5): on Linux, (5)
* pastes the last thing that you highlighted, not the last thing that you
* ctrl+c'ed. This code will still generate a {@code PASTE} event though.
*
* We enumerate all the possible steps a user can take to paste text and we
* implemented the transition between the steps in a state machine. The
* following is the design of the state machine:
*
* matching paths:
*
* (1) happens on INIT -> FOCUSED -> TYPING -> [e.ctrlKey & e.keyCode = 'v']
* (2-3) happens on INIT -> FOCUSED -> [input event happened]
* (4) happens on INIT -> [mouseover && text changed]
*
* non matching paths:
*
* user is typing normally
* INIT -> FOCUS -> TYPING -> INPUT -> INIT
*
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
goog.events.PasteHandler.prototype.handleEvent_ = function(e) {
// transition between states happen at each browser event, and depend on the
// current state, the event that led to this state, and the event input.
switch (this.state_) {
case goog.events.PasteHandler.State.INIT: {
this.handleUnderInit_(e);
break;
}
case goog.events.PasteHandler.State.FOCUSED: {
this.handleUnderFocused_(e);
break;
}
case goog.events.PasteHandler.State.TYPING: {
this.handleUnderTyping_(e);
break;
}
default: {
goog.log.error(this.logger_, 'invalid ' + this.state_ + ' state');
}
}
this.lastTime_ = goog.now();
this.oldValue_ = this.element_.value;
goog.log.info(this.logger_, e.type + ' -> ' + this.state_);
this.previousEvent_ = e.type;
};
/**
* {@code goog.events.PasteHandler.EventType.INIT} is the first initial state
* the textarea is found. You can only leave this state by setting focus on the
* textarea, which is how users will input text. You can also paste things using
* drag and drop, which will not generate a {@code goog.events.EventType.FOCUS}
* event, but will generate a {@code goog.events.EventType.MOUSEOVER}.
*
* For browsers that support the 'paste' event, we match it and stay on the same
* state.
*
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
goog.events.PasteHandler.prototype.handleUnderInit_ = function(e) {
switch (e.type) {
case goog.events.EventType.BLUR: {
this.state_ = goog.events.PasteHandler.State.INIT;
break;
}
case goog.events.EventType.FOCUS: {
this.state_ = goog.events.PasteHandler.State.FOCUSED;
break;
}
case goog.events.EventType.MOUSEOVER: {
this.state_ = goog.events.PasteHandler.State.INIT;
if (this.element_.value != this.oldValue_) {
goog.log.info(this.logger_, 'paste by dragdrop while on init!');
this.dispatch_(e);
}
break;
}
default: {
goog.log.error(this.logger_,
'unexpected event ' + e.type + 'during init');
}
}
};
/**
* {@code goog.events.PasteHandler.EventType.FOCUSED} is typically the second
* state the textarea will be, which is followed by the {@code INIT} state. On
* this state, users can paste in three different ways: edit -> paste,
* right click -> paste and drag and drop.
*
* The latter will generate a {@code goog.events.EventType.MOUSEOVER} event,
* which we match by making sure the textarea text changed. The first two will
* generate an 'input', which we match by making sure it was NOT generated by a
* key event (which also generates an 'input' event).
*
* Unfortunately, in Firefox, if you type fast, some KEYDOWN events are
* swallowed but an INPUT event may still happen. That means we need to
* differentiate between two consecutive INPUT events being generated either by
* swallowed key events OR by a valid edit -> paste -> edit -> paste action. We
* do this by checking a minimum time between the two events. This heuristic
* seems to work well, but it is obviously a heuristic :).
*
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
goog.events.PasteHandler.prototype.handleUnderFocused_ = function(e) {
switch (e.type) {
case 'input' : {
// there are two different events that happen in practice that involves
// consecutive 'input' events. we use a heuristic to differentiate
// between the one that generates a valid paste action and the one that
// doesn't.
// @see testTypingReallyFastDispatchesTwoInputEventsBeforeTheKEYDOWNEvent
// and
// @see testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents
// Notice that an 'input' event may be also triggered by a 'middle click'
// paste event, which is described in
// @see testMiddleClickWithoutFocusTriggersPasteEvent
var minimumMilisecondsBetweenInputEvents = this.lastTime_ +
goog.events.PasteHandler.
MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER;
if (goog.now() > minimumMilisecondsBetweenInputEvents ||
this.previousEvent_ == goog.events.EventType.FOCUS) {
goog.log.info(this.logger_, 'paste by textchange while focused!');
this.dispatch_(e);
}
break;
}
case goog.events.EventType.BLUR: {
this.state_ = goog.events.PasteHandler.State.INIT;
break;
}
case goog.events.EventType.KEYDOWN: {
goog.log.info(this.logger_, 'key down ... looking for ctrl+v');
// Opera + MAC does not set e.ctrlKey. Instead, it gives me e.keyCode = 0.
// http://www.quirksmode.org/js/keys.html
if (goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 0 ||
goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 17) {
break;
}
this.state_ = goog.events.PasteHandler.State.TYPING;
break;
}
case goog.events.EventType.MOUSEOVER: {
if (this.element_.value != this.oldValue_) {
goog.log.info(this.logger_, 'paste by dragdrop while focused!');
this.dispatch_(e);
}
break;
}
default: {
goog.log.error(this.logger_,
'unexpected event ' + e.type + ' during focused');
}
}
};
/**
* {@code goog.events.PasteHandler.EventType.TYPING} is the third state
* this class can be. It exists because each KEYPRESS event will ALSO generate
* an INPUT event (because the textarea value changes), and we need to
* differentiate between an INPUT event generated by a key event and an INPUT
* event generated by edit -> paste actions.
*
* This is the state that we match the ctrl+v pattern.
*
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
goog.events.PasteHandler.prototype.handleUnderTyping_ = function(e) {
switch (e.type) {
case 'input' : {
this.state_ = goog.events.PasteHandler.State.FOCUSED;
break;
}
case goog.events.EventType.BLUR: {
this.state_ = goog.events.PasteHandler.State.INIT;
break;
}
case goog.events.EventType.KEYDOWN: {
if (e.ctrlKey && e.keyCode == goog.events.KeyCodes.V ||
e.shiftKey && e.keyCode == goog.events.KeyCodes.INSERT ||
e.metaKey && e.keyCode == goog.events.KeyCodes.V) {
goog.log.info(this.logger_, 'paste by ctrl+v while keypressed!');
this.dispatch_(e);
}
break;
}
case goog.events.EventType.MOUSEOVER: {
if (this.element_.value != this.oldValue_) {
goog.log.info(this.logger_, 'paste by dragdrop while keypressed!');
this.dispatch_(e);
}
break;
}
default: {
goog.log.error(this.logger_,
'unexpected event ' + e.type + ' during keypressed');
}
}
};
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.PasteHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.PasteHandlerTest');
</script>
</head>
<body>
<textarea id="foo">
</textarea>
</body>
</html>
@@ -0,0 +1,409 @@
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.PasteHandlerTest');
goog.setTestOnly('goog.events.PasteHandlerTest');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.PasteHandler');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.MockUserAgent');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
function setUp() {
// TODO(user): fix {@code goog.testing.MockUserAgent} to do the right thing.
// the code doesn't seem to be updating the variables with
// goog.userAgent.init_(), which means it is not allowing me to mock the
// user agent variables.
goog.userAgent.GECKO = true;
goog.userAgent.IE = false;
goog.userAgent.WEBKIT = false;
goog.userAgent.VERSION = '1.8';
textarea = new goog.events.EventTarget();
textarea.value = '';
clock = new goog.testing.MockClock(true);
mockUserAgent = new goog.testing.MockUserAgent();
handler = new goog.events.PasteHandler(textarea);
pasted = false;
goog.events.listen(handler, goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
}
function tearDown() {
textarea.dispose();
handler.dispose();
clock.dispose();
mockUserAgent.dispose();
}
function newBrowserEvent(type) {
if (goog.isString(type)) {
return new goog.events.BrowserEvent({type: type});
} else {
return new goog.events.BrowserEvent(type);
}
}
function testDispatchingPasteEventSupportedByAFewBrowsersWork() {
goog.userAgent.IE = true;
var handlerThatSupportsPasteEvents =
new goog.events.PasteHandler(textarea);
// user clicks on the textarea and give it focus
goog.events.listen(handlerThatSupportsPasteEvents,
goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
textarea.dispatchEvent(newBrowserEvent('paste'));
assertTrue(pasted);
}
function testJustTypingDoesntFirePasteEvent() {
// user clicks on the textarea and give it focus
textarea.dispatchEvent(newBrowserEvent(goog.events.EventType.FOCUS));
assertFalse(pasted);
// user starts typing
textarea.dispatchEvent(newBrowserEvent({
type: goog.events.EventType.KEYDOWN,
keyCode: goog.events.KeyCodes.A
}));
textarea.value = 'a';
assertFalse(pasted);
// still typing
textarea.dispatchEvent({
type: goog.events.EventType.KEYDOWN,
keyCode: goog.events.KeyCodes.B
});
textarea.value = 'ab';
assertFalse(pasted);
// ends typing
textarea.dispatchEvent({
type: goog.events.EventType.KEYDOWN,
keyCode: goog.events.KeyCodes.C
});
textarea.value = 'abc';
assertFalse(pasted);
}
function testStartsOnInitialState() {
assertTrue(handler.getState() == goog.events.PasteHandler.State.INIT);
assertFalse(pasted);
}
function testBlurOnInit() {
textarea.dispatchEvent(goog.events.EventType.BLUR);
assertTrue(handler.getState() == goog.events.PasteHandler.State.INIT);
assertFalse(pasted);
}
function testFocusOnInit() {
textarea.dispatchEvent(goog.events.EventType.FOCUS);
assertTrue(handler.getState() == goog.events.PasteHandler.State.FOCUSED);
assertFalse(pasted);
}
function testInputOnFocus() {
// user clicks on the textarea
textarea.dispatchEvent(newBrowserEvent(goog.events.EventType.FOCUS));
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
// and right click -> paste a text!
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(handler.getState() == goog.events.PasteHandler.State.FOCUSED);
// make sure we detected it
assertTrue(pasted);
}
function testKeyPressOnFocus() {
// user clicks on the textarea
textarea.dispatchEvent(newBrowserEvent(goog.events.EventType.FOCUS));
// starts typing something
textarea.dispatchEvent(newBrowserEvent({
type: goog.events.EventType.KEYDOWN,
keyCode: goog.events.KeyCodes.A
}));
assertTrue(handler.getState() == goog.events.PasteHandler.State.TYPING);
assertFalse(pasted);
// and then presses ctrl+v
textarea.dispatchEvent(newBrowserEvent({
type: goog.events.EventType.KEYDOWN,
keyCode: goog.events.KeyCodes.V,
ctrlKey: true
}));
assertTrue(handler.getState() == goog.events.PasteHandler.State.TYPING);
// makes sure we detected it
assertTrue(pasted);
}
function testMouseOverOnInit() {
// user has something on the events
textarea.value = 'pasted string';
// and right click -> paste it on the textarea, WITHOUT giving focus
textarea.dispatchEvent(newBrowserEvent(goog.events.EventType.MOUSEOVER));
assertTrue(handler.getState() == goog.events.PasteHandler.State.INIT);
// makes sure we detect it
assertTrue(pasted);
pasted = false;
// user normaly mouseovers the textarea, with no text change
textarea.dispatchEvent(goog.events.EventType.MOUSEOVER);
assertTrue(handler.getState() == goog.events.PasteHandler.State.INIT);
// text area value doesnt change
assertFalse(pasted);
}
function testMouseOverAfterTyping() {
textarea.dispatchEvent(goog.events.EventType.FOCUS);
assertFalse(pasted);
textarea.dispatchEvent(
{type: goog.events.EventType.KEYDOWN, keyCode: goog.events.KeyCodes.A});
assertFalse(pasted);
textarea.value = 'a';
textarea.dispatchEvent('input');
assertFalse(pasted);
assertEquals('a', handler.oldValue_);
textarea.dispatchEvent(goog.events.EventType.MOUSEOVER);
assertFalse(pasted);
}
function testTypingAndThenRightClickPaste() {
textarea.dispatchEvent(goog.events.EventType.FOCUS);
textarea.dispatchEvent(
{type: goog.events.EventType.KEYDOWN, keyCode: goog.events.KeyCodes.A});
assertFalse(pasted);
textarea.value = 'a';
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
textarea.dispatchEvent('input');
assertFalse(pasted);
assertEquals('a', handler.oldValue_);
textarea.value = 'ab';
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(pasted);
}
function testTypingReallyFastDispatchesTwoInputEventsBeforeTheKeyDownEvent() {
textarea.dispatchEvent(goog.events.EventType.FOCUS);
// keydown and input events seems to be fired indepently: even though input
// should happen after the key event, it doens't if the user types fast
// enough. FF2 + linux doesn't fire keydown events for every key pressed when
// you type fast enough. if one of the keydown events gets swallowed, two
// input events are fired consecutively. notice that there is a similar
// scenario, that actually does produce a valid paste action.
// {@see testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents}
textarea.dispatchEvent(
{type: goog.events.EventType.KEYDOWN, keyCode: goog.events.KeyCodes.A});
assertFalse(pasted);
textarea.value = 'a';
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER -
1);
textarea.dispatchEvent('input');
assertFalse(pasted);
// second key down events gets fired on a different order
textarea.value = 'ab';
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER -
1);
textarea.dispatchEvent('input');
assertFalse(pasted);
}
function testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents() {
textarea.dispatchEvent(goog.events.EventType.FOCUS);
// there is also another case that two consecutive INPUT events are fired,
// but in a valid paste action: if the user edit -> paste -> edit -> paste,
// it is a valid paste action.
textarea.value = 'a';
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(pasted);
// second key down events gets fired on a different order
textarea.value = 'ab';
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(pasted);
}
function testMiddleClickWithoutFocusTriggersPasteEvent() {
// if the textarea is NOT selected, and then we use the middle button,
// FF2+linux pastes what was last highlighted, causing a paste action.
textarea.dispatchEvent(goog.events.EventType.FOCUS);
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(pasted);
}
function testMacRightClickPasteRequiresCtrlBecauseItHasOneButton() {
// Macs don't have two buttons mouse: this means that you need to press
// ctrl + click to get to the menu, and then you can click paste.
// The sequences of events fired on Opera are:
// focus -> keydown (keyCode == 0, not e.ctrlKey) -> input
goog.userAgent.OPERA = true;
goog.userAgent.MAC = true;
var handler = new goog.events.PasteHandler(textarea);
// user clicks on the textarea and give it focus
goog.events.listen(handler,
goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
textarea.dispatchEvent(goog.events.EventType.FOCUS);
assertFalse(pasted);
textarea.dispatchEvent({type: goog.events.EventType.KEYDOWN, keyCode: 0});
assertFalse(pasted);
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(pasted);
}
function testOperaMacFiresKeyCode17WhenAppleKeyPressedButDoesNotFireKeyDown() {
// Opera on Macs fires keycode 17 when apple key is pressed, and then it does
// not fire a keydown event when the V is pressed.
goog.userAgent.OPERA = true;
goog.userAgent.MAC = true;
var handler = new goog.events.PasteHandler(textarea);
// user clicks on the textarea and give it focus
goog.events.listen(handler,
goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
textarea.dispatchEvent(goog.events.EventType.FOCUS);
assertFalse(pasted);
// apple key is pressed, generating a keydown event
textarea.dispatchEvent({type: goog.events.EventType.KEYDOWN, keyCode: 17});
assertFalse(pasted);
clock.tick(
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER +
1);
// and then text is added magically without any extra keydown events.
textarea.dispatchEvent(newBrowserEvent('input'));
assertTrue(pasted);
}
function testScriptingDoesntTriggerPasteEvents() {
var handlerUsedToListenForScriptingChanges =
new goog.events.PasteHandler(textarea);
pasted = false;
// user clicks on the textarea and give it focus
goog.events.listen(handlerUsedToListenForScriptingChanges,
goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
goog.dom.getElement('foo').value = 'dear paste handler,';
assertFalse(pasted);
goog.dom.getElement('foo').value = 'please dont misunderstand script changes';
assertFalse(pasted);
goog.dom.getElement('foo').value = 'with user generated paste events';
assertFalse(pasted);
goog.dom.getElement('foo').value = 'thanks!';
assertFalse(pasted);
}
function testAfterPaste() {
goog.userAgent.IE = true;
var handlerThatSupportsPasteEvents =
new goog.events.PasteHandler(textarea);
pasted = false;
goog.events.listen(handlerThatSupportsPasteEvents,
goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
var afterPasteFired = false;
goog.events.listen(handlerThatSupportsPasteEvents,
goog.events.PasteHandler.EventType.AFTER_PASTE,
function() {
afterPasteFired = true;
});
// Initial paste event comes before AFTER_PASTE has fired.
textarea.dispatchEvent(newBrowserEvent('paste'));
assertTrue(pasted);
assertFalse(afterPasteFired);
// Once text is pasted, it takes a bit to detect it, at which point
// AFTER_PASTE is fired.
clock.tick(goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_);
textarea.value = 'text';
clock.tick(goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_);
assertTrue(afterPasteFired);
}
function testAfterPasteNotFiredIfDelayTooLong() {
goog.userAgent.IE = true;
var handlerThatSupportsPasteEvents =
new goog.events.PasteHandler(textarea);
pasted = false;
goog.events.listen(handlerThatSupportsPasteEvents,
goog.events.PasteHandler.EventType.PASTE,
function() {
pasted = true;
});
var afterPasteFired = false;
goog.events.listen(handlerThatSupportsPasteEvents,
goog.events.PasteHandler.EventType.AFTER_PASTE,
function() {
afterPasteFired = true;
});
// Initial paste event comes before AFTER_PASTE has fired.
textarea.dispatchEvent(newBrowserEvent('paste'));
assertTrue(pasted);
assertFalse(afterPasteFired);
// If the new text doesn't show up in time, we never fire AFTER_PASTE.
clock.tick(goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_);
textarea.value = 'text';
clock.tick(goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_);
assertFalse(afterPasteFired);
}
@@ -0,0 +1,169 @@
// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This class aims to smooth out inconsistencies between browser
* handling of wheel events by providing an event that is similar to that
* defined in the standard, but also easier to consume.
*
* It is based upon the WheelEvent, which allows for up to 3 dimensional
* scrolling events that come in units of either pixels, lines or pages.
* http://www.w3.org/TR/2014/WD-DOM-Level-3-Events-20140925/#interface-WheelEvent
*
* The significant difference here is that it also provides reasonable pixel
* deltas for clients that do not want to treat line and page scrolling events
* specially.
*
* Clients of this code should be aware that some input devices only fire a few
* discrete events (such as a mouse wheel without acceleration) whereas some can
* generate a large number of events for a single interaction (such as a
* touchpad with acceleration). There is no signal in the events to reliably
* distinguish between these.
*
* @see ../demos/wheelhandler.html
*/
goog.provide('goog.events.WheelEvent');
goog.require('goog.asserts');
goog.require('goog.events.BrowserEvent');
/**
* A common class for wheel events. This is used with the WheelHandler.
*
* @param {Event} browserEvent Browser event object.
* @param {goog.events.WheelEvent.DeltaMode} deltaMode The delta mode units of
* the wheel event.
* @param {number} deltaX The number of delta units the user in the X axis.
* @param {number} deltaY The number of delta units the user in the Y axis.
* @param {number} deltaZ The number of delta units the user in the Z axis.
* @constructor
* @extends {goog.events.BrowserEvent}
* @final
*/
goog.events.WheelEvent = function(
browserEvent, deltaMode, deltaX, deltaY, deltaZ) {
goog.events.WheelEvent.base(this, 'constructor', browserEvent);
goog.asserts.assert(browserEvent, 'Expecting a non-null browserEvent');
/** @type {goog.events.WheelEvent.EventType} */
this.type = goog.events.WheelEvent.EventType.WHEEL;
/**
* An enum corresponding to the units of this event.
* @type {goog.events.WheelEvent.DeltaMode}
*/
this.deltaMode = deltaMode;
/**
* The number of delta units in the X axis.
* @type {number}
*/
this.deltaX = deltaX;
/**
* The number of delta units in the Y axis.
* @type {number}
*/
this.deltaY = deltaY;
/**
* The number of delta units in the Z axis.
* @type {number}
*/
this.deltaZ = deltaZ;
// Ratio between delta and pixel values.
var pixelRatio = 1; // Value for DeltaMode.PIXEL
switch (deltaMode) {
case goog.events.WheelEvent.DeltaMode.PAGE:
pixelRatio *= goog.events.WheelEvent.PIXELS_PER_PAGE_;
break;
case goog.events.WheelEvent.DeltaMode.LINE:
pixelRatio *= goog.events.WheelEvent.PIXELS_PER_LINE_;
break;
}
/**
* The number of delta pixels in the X axis. Code that doesn't want to handle
* different deltaMode units can just look here.
* @type {number}
*/
this.pixelDeltaX = this.deltaX * pixelRatio;
/**
* The number of pixels in the Y axis. Code that doesn't want to
* handle different deltaMode units can just look here.
* @type {number}
*/
this.pixelDeltaY = this.deltaY * pixelRatio;
/**
* The number of pixels scrolled in the Z axis. Code that doesn't want to
* handle different deltaMode units can just look here.
* @type {number}
*/
this.pixelDeltaZ = this.deltaZ * pixelRatio;
};
goog.inherits(goog.events.WheelEvent, goog.events.BrowserEvent);
/**
* Enum type for the events fired by the wheel handler.
* @enum {string}
*/
goog.events.WheelEvent.EventType = {
/** The user has provided wheel-based input. */
WHEEL: 'wheel'
};
/**
* Units for the deltas in a WheelEvent.
* @enum {number}
*/
goog.events.WheelEvent.DeltaMode = {
/** The units are in pixels. From DOM_DELTA_PIXEL. */
PIXEL: 0,
/** The units are in lines. From DOM_DELTA_LINE. */
LINE: 1,
/** The units are in pages. From DOM_DELTA_PAGE. */
PAGE: 2
};
/**
* A conversion number between line scroll units and pixel scroll units. The
* actual value per line can vary a lot between devices and font sizes. This
* number can not be perfect, but it should be reasonable for converting lines
* scroll events into pixels.
* @const {number}
* @private
*/
goog.events.WheelEvent.PIXELS_PER_LINE_ = 15;
/**
* A conversion number between page scroll units and pixel scroll units. The
* actual value per page can vary a lot as many different devices have different
* screen sizes, and the window might not be taking up the full screen. This
* number can not be perfect, but it should be reasonable for converting page
* scroll events into pixels.
* @const {number}
* @private
*/
goog.events.WheelEvent.PIXELS_PER_PAGE_ = 30 *
goog.events.WheelEvent.PIXELS_PER_LINE_;
@@ -0,0 +1,159 @@
// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview This event wrapper will dispatch an event when the user uses
* the wheel on an element. The event provides details of the unit type (pixel /
* line / page) and deltas in those units in up to 3 dimensions. Additionally,
* simplified pixel deltas are provided for code that doesn't need to handle the
* different units differently. This is not to be confused with the scroll
* event, where an element in the dom can report that it was scrolled.
*
* This class aims to smooth out inconsistencies between browser platforms with
* regards to wheel events, but we do not cover every possible software/hardware
* combination out there, some of which occasionally produce very large deltas
* in wheel events, especially when the device supports acceleration.
*
* Relevant standard:
* http://www.w3.org/TR/2014/WD-DOM-Level-3-Events-20140925/#interface-WheelEvent
*
* Clients of this code should be aware that some input devices only fire a few
* discrete events (such as a mouse wheel without acceleration) whereas some can
* generate a large number of events for a single interaction (such as a
* touchpad with acceleration). There is no signal in the events to reliably
* distinguish between these.
*
* @author arv@google.com (Erik Arvidsson)
* @see ../demos/wheelhandler.html
*/
goog.provide('goog.events.WheelHandler');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.WheelEvent');
goog.require('goog.style');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');
goog.require('goog.userAgent.product.isVersion');
/**
* This event handler allows you to catch wheel events in a consistent manner.
* @param {!Element|!Document} element The element to listen to the wheel event
* on.
* @param {boolean=} opt_capture Whether to handle the wheel event in capture
* phase.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.events.WheelHandler = function(element, opt_capture) {
goog.events.WheelHandler.base(this, 'constructor');
/**
* This is the element that we will listen to the real wheel events on.
* @private {!Element|!Document}
*/
this.element_ = element;
var rtlElement = goog.dom.isElement(this.element_) ?
/** @type {!Element} */ (this.element_) :
/** @type {!Document} */ (this.element_).body;
/**
* True if the element exists and is RTL, false otherwise.
* @private {boolean}
*/
this.isRtl_ = !!rtlElement && goog.style.isRightToLeft(rtlElement);
/**
* The key returned from the goog.events.listen.
* @private {goog.events.Key}
*/
this.listenKey_ = goog.events.listen(
this.element_, goog.events.WheelHandler.getDomEventType(),
this, opt_capture);
};
goog.inherits(goog.events.WheelHandler, goog.events.EventTarget);
/**
* Returns the dom event type.
* @return {string} The dom event type.
*/
goog.events.WheelHandler.getDomEventType = function() {
// Prefer to use wheel events whenever supported.
if (goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher(17) ||
goog.userAgent.IE && goog.userAgent.isVersionOrHigher(9) ||
goog.userAgent.product.CHROME && goog.userAgent.product.isVersion(31)) {
return 'wheel';
}
// Legacy events. Still the best we have on Opera and Safari.
return goog.userAgent.GECKO ? 'DOMMouseScroll' : 'mousewheel';
};
/**
* Handles the events on the element.
* @param {!goog.events.BrowserEvent} e The underlying browser event.
*/
goog.events.WheelHandler.prototype.handleEvent = function(e) {
var deltaMode = goog.events.WheelEvent.DeltaMode.PIXEL;
var deltaX = 0;
var deltaY = 0;
var deltaZ = 0;
var be = e.getBrowserEvent();
if (be.type == 'wheel') {
deltaMode = be.deltaMode;
deltaX = be.deltaX;
deltaY = be.deltaY;
deltaZ = be.deltaZ;
} else if (be.type == 'mousewheel') {
// Assume that these are still comparable to pixels. This may not be true
// for all old browsers.
if (goog.isDef(be.wheelDeltaX)) {
deltaX = -be.wheelDeltaX;
deltaY = -be.wheelDeltaY;
} else {
deltaY = -be.wheelDelta;
}
} else { // Historical Gecko
// Gecko returns multiple of 3 (representing the number of lines)
deltaMode = goog.events.WheelEvent.DeltaMode.LINE;
// Firefox 3.1 adds an axis field to the event to indicate axis.
if (goog.isDef(be.axis) && be.axis === be.HORIZONTAL_AXIS) {
deltaX = be.detail;
} else {
deltaY = be.detail;
}
}
// For horizontal deltas we need to flip the value for RTL grids.
if (this.isRtl_) {
deltaX = -deltaX;
}
var newEvent = new goog.events.WheelEvent(
be, deltaMode, deltaX, deltaY, deltaZ);
this.dispatchEvent(newEvent);
};
/** @override */
goog.events.WheelHandler.prototype.disposeInternal = function() {
goog.events.WheelHandler.superClass_.disposeInternal.call(this);
goog.events.unlistenByKey(this.listenKey_);
this.listenKey_ = null;
};
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2014 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.events.WheelHandler
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.events.WheelHandlerTest');
</script>
</head>
<body>
<div id="foo">
</div>
<div id="fooRtl" dir="rtl">
</div>
<div id="log" style="position:absolute;right:0;top:0">
Logged events:
</div>
</body>
</html>
@@ -0,0 +1,296 @@
// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.events.WheelHandlerTest');
goog.setTestOnly('goog.events.WheelHandlerTest');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.WheelEvent');
goog.require('goog.events.WheelHandler');
goog.require('goog.string');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');
var log;
var stubs = new goog.testing.PropertyReplacer();
var PREFERRED_TYPE = 'wheel';
var LEGACY_TYPE = 'mousewheel';
var GECKO_TYPE = 'DOMMouseScroll';
var HORIZONTAL = 'h';
var VERTICAL = 'v';
var DeltaMode = goog.events.WheelEvent.DeltaMode;
var mouseWheelEvent;
var mouseWheelEventRtl;
var mouseWheelHandler;
var mouseWheelHandlerRtl;
function setUpPage() {
log = goog.dom.getElement('log');
}
function setUp() {
stubs.remove(goog, 'userAgent');
goog.userAgent = {
product: {
CHROME: false,
version: 0,
isVersion: function(version) {
return goog.string.compareVersions(this.version, version) >= 0;
}
},
GECKO: false,
IE: false,
version: 0,
isVersionOrHigher: function(version) {
return goog.string.compareVersions(this.version, version) >= 0;
}
};
}
function tearDown() {
stubs.reset();
goog.dispose(mouseWheelHandler);
goog.dispose(mouseWheelHandlerRtl);
mouseWheelHandlerRtl = null;
mouseWheelHandler = null;
mouseWheelEvent = null;
mouseWheelEventRtl = null;
}
function tearDownPage() {
// Create interactive demo.
mouseWheelHandler = new goog.events.WheelHandler(document.body);
goog.events.listen(mouseWheelHandler,
goog.events.WheelEvent.EventType.WHEEL,
function(e) {
log.innerHTML += goog.string.subs('<br />(deltaX, deltaY): (%s, %s)',
e.deltaX, e.deltaY);
});
}
function testGetDomEventType() {
// Defaults to legacy non-gecko event.
assertEquals(LEGACY_TYPE, goog.events.WheelHandler.getDomEventType());
// Gecko start to support wheel with version 17.
goog.userAgent.GECKO = true;
goog.userAgent.version = 16;
assertEquals(GECKO_TYPE, goog.events.WheelHandler.getDomEventType());
goog.userAgent.version = 17;
assertEquals(PREFERRED_TYPE, goog.events.WheelHandler.getDomEventType());
goog.userAgent.GECKO = false;
// IE started with version 9.
goog.userAgent.IE = true;
goog.userAgent.version = 8;
assertEquals(LEGACY_TYPE, goog.events.WheelHandler.getDomEventType());
goog.userAgent.version = 9;
assertEquals(PREFERRED_TYPE, goog.events.WheelHandler.getDomEventType());
goog.userAgent.IE = false;
// Chrome started with version 31.
goog.userAgent.product.CHROME = true;
goog.userAgent.product.version = 30;
assertEquals(LEGACY_TYPE, goog.events.WheelHandler.getDomEventType());
goog.userAgent.product.version = 31;
assertEquals(PREFERRED_TYPE, goog.events.WheelHandler.getDomEventType());
goog.userAgent.product.CHROME = false;
}
function testPreferredStyleWheel() {
// Enable 'wheel'
goog.userAgent.IE = true;
goog.userAgent.version = 9;
createHandlerAndListen();
handleEvent(createFakePreferredEvent(DeltaMode.PIXEL, 10, 20, 30));
assertWheelEvent(DeltaMode.PIXEL, 10, 20, 30);
assertPixelDeltas(1);
handleEvent(createFakePreferredEvent(DeltaMode.LINE, 10, 20, 30));
assertWheelEvent(DeltaMode.LINE, 10, 20, 30);
assertPixelDeltas(15);
handleEvent(createFakePreferredEvent(DeltaMode.PAGE, 10, 20, 30));
assertWheelEvent(DeltaMode.PAGE, 10, 20, 30);
assertPixelDeltas(30 * 15);
}
function testLegacyStyleWheel() {
// 'mousewheel' enabled by default
createHandlerAndListen();
// Test one dimensional.
handleEvent(createFakeLegacyEvent(10));
assertWheelEvent(DeltaMode.PIXEL, 0, -10, 0);
assertPixelDeltas(1);
// Test two dimensional.
handleEvent(createFakeLegacyEvent(/* ignored */ 10, 20, 30));
assertWheelEvent(DeltaMode.PIXEL, -20, -30, 0);
assertPixelDeltas(1);
}
function testLegacyGeckoStyleWheel() {
goog.userAgent.GECKO = true;
createHandlerAndListen();
// Test no axis.
handleEvent(createFakeGeckoEvent(10));
assertWheelEvent(DeltaMode.LINE, 0, 10, 0);
assertPixelDeltas(15);
// Vertical axis.
handleEvent(createFakeGeckoEvent(10, VERTICAL));
assertWheelEvent(DeltaMode.LINE, 0, 10, 0);
assertPixelDeltas(15);
// Horizontal axis.
handleEvent(createFakeGeckoEvent(10, HORIZONTAL));
assertWheelEvent(DeltaMode.LINE, 10, 0, 0);
assertPixelDeltas(15);
}
function testLegacyIeStyleWheel() {
goog.userAgent.IE = true;
createHandlerAndListen();
// Non-gecko, non-webkit events get wheelDelta divided by -40 to get detail.
handleEvent(createFakeLegacyEvent(120));
assertWheelEvent(DeltaMode.PIXEL, 0, -120, 0);
handleEvent(createFakeLegacyEvent(-120));
assertWheelEvent(DeltaMode.PIXEL, 0, 120, 0);
handleEvent(createFakeLegacyEvent(1200));
assertWheelEvent(DeltaMode.PIXEL, 0, -1200, 0);
}
function testNullBody() {
goog.userAgent.IE = true;
var documentObjectWithNoBody = { };
goog.testing.events.mixinListenable(documentObjectWithNoBody);
mouseWheelHandler =
new goog.events.WheelHandler(documentObjectWithNoBody);
}
// Be sure to call this after setting up goog.userAgent mock and not before.
function createHandlerAndListen() {
mouseWheelHandler = new goog.events.WheelHandler(
goog.dom.getElement('foo'));
goog.events.listen(mouseWheelHandler,
goog.events.WheelEvent.EventType.WHEEL,
function(e) { mouseWheelEvent = e; });
mouseWheelHandlerRtl = new goog.events.WheelHandler(
goog.dom.getElement('fooRtl'));
goog.events.listen(mouseWheelHandlerRtl,
goog.events.WheelEvent.EventType.WHEEL,
function(e) { mouseWheelEventRtl = e; });
}
function handleEvent(event) {
mouseWheelHandler.handleEvent(event);
mouseWheelHandlerRtl.handleEvent(event);
}
function assertWheelEvent(deltaMode, deltaX, deltaY, deltaZ) {
assertTrue('event should be non-null', !!mouseWheelEvent);
assertTrue('event should have correct JS type',
mouseWheelEvent instanceof goog.events.WheelEvent);
assertEquals('event should have correct deltaMode property',
deltaMode, mouseWheelEvent.deltaMode);
assertEquals('event should have correct deltaX property',
deltaX, mouseWheelEvent.deltaX);
assertEquals('event should have correct deltaY property',
deltaY, mouseWheelEvent.deltaY);
assertEquals('event should have correct deltaZ property',
deltaZ, mouseWheelEvent.deltaZ);
// RTL
assertTrue('event should be non-null', !!mouseWheelEventRtl);
assertTrue('event should have correct JS type',
mouseWheelEventRtl instanceof goog.events.WheelEvent);
assertEquals('event should have correct deltaMode property',
deltaMode, mouseWheelEventRtl.deltaMode);
assertEquals('event should have correct deltaX property',
-deltaX, mouseWheelEventRtl.deltaX);
assertEquals('event should have correct deltaY property',
deltaY, mouseWheelEventRtl.deltaY);
assertEquals('event should have correct deltaZ property',
deltaZ, mouseWheelEventRtl.deltaZ);
}
function assertPixelDeltas(scale) {
assertEquals(mouseWheelEvent.deltaX * scale, mouseWheelEvent.pixelDeltaX);
assertEquals(mouseWheelEvent.deltaY * scale, mouseWheelEvent.pixelDeltaY);
assertEquals(mouseWheelEvent.deltaZ * scale, mouseWheelEvent.pixelDeltaZ);
// RTL
assertEquals(mouseWheelEventRtl.deltaX * scale,
mouseWheelEventRtl.pixelDeltaX);
assertEquals(mouseWheelEventRtl.deltaY * scale,
mouseWheelEventRtl.pixelDeltaY);
assertEquals(mouseWheelEventRtl.deltaZ * scale,
mouseWheelEventRtl.pixelDeltaZ);
}
function createFakePreferredEvent(
opt_deltaMode, opt_deltaX, opt_deltaY, opt_deltaZ) {
var event = {
type: PREFERRED_TYPE,
deltaMode: opt_deltaMode,
deltaX: opt_deltaX,
deltaY: opt_deltaY,
deltaZ: opt_deltaZ
};
return new goog.events.BrowserEvent(event);
}
function createFakeLegacyEvent(
opt_wheelDelta, opt_wheelDeltaX, opt_wheelDeltaY) {
var event = {
type: LEGACY_TYPE,
wheelDelta: opt_wheelDelta,
wheelDeltaX: opt_wheelDeltaX,
wheelDeltaY: opt_wheelDeltaY
};
return new goog.events.BrowserEvent(event);
}
function createFakeGeckoEvent(opt_detail, opt_axis) {
var event = {
type: GECKO_TYPE,
detail: opt_detail,
axis: opt_axis,
HORIZONTAL_AXIS: HORIZONTAL,
VERTICAL_AXIS: VERTICAL
};
return new goog.events.BrowserEvent(event);
}