Adding mapbox-gl branch
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user