blob: 56dea9197f148896c963a76bd89578c792275b00 [file] [log] [blame] [edit]
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you 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 The file contains an abstraction of a touch screen
* for simulating atomic touchscreen actions.
*/
goog.provide('bot.Touchscreen');
goog.require('bot');
goog.require('bot.Device');
goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('bot.dom');
goog.require('bot.events.EventType');
goog.require('goog.dom.TagName');
goog.require('goog.math.Coordinate');
goog.require('goog.userAgent.product');
/**
* A TouchScreen that provides atomic touch actions. The metaphor
* for this abstraction is a finger moving above the touchscreen that
* can press and then release the touchscreen when specified.
*
* The touchscreen supports three actions: press, release, and move.
*
* @constructor
* @extends {bot.Device}
*/
bot.Touchscreen = function () {
goog.base(this);
/** @private {!goog.math.Coordinate} */
this.clientXY_ = new goog.math.Coordinate(0, 0);
/** @private {!goog.math.Coordinate} */
this.clientXY2_ = new goog.math.Coordinate(0, 0);
};
goog.inherits(bot.Touchscreen, bot.Device);
/** @private {boolean} */
bot.Touchscreen.prototype.fireMouseEventsOnRelease_ = true;
/** @private {boolean} */
bot.Touchscreen.prototype.cancelled_ = false;
/** @private {number} */
bot.Touchscreen.prototype.touchIdentifier_ = 0;
/** @private {number} */
bot.Touchscreen.prototype.touchIdentifier2_ = 0;
/** @private {number} */
bot.Touchscreen.prototype.touchCounter_ = 2;
/**
* Press the touch screen. Pressing before moving results in an exception.
* Pressing while already pressed also results in an exception.
*
* @param {boolean=} opt_press2 Whether or not press the second finger during
* the press. If not defined or false, only the primary finger will be
* pressed.
*/
bot.Touchscreen.prototype.press = function (opt_press2) {
if (this.isPressed()) {
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
'Cannot press touchscreen when already pressed.');
}
this.touchIdentifier_ = this.touchCounter_++;
if (opt_press2) {
this.touchIdentifier2_ = this.touchCounter_++;
}
if (bot.userAgent.IE_DOC_10) {
this.fireMouseEventsOnRelease_ = true;
this.firePointerEvents_(bot.Touchscreen.fireSinglePressPointer_);
} else {
this.fireMouseEventsOnRelease_ = this.fireTouchEvent_(
bot.events.EventType.TOUCHSTART);
}
};
/**
* Releases an element on a touchscreen. Releasing an element that is not
* pressed results in an exception.
*/
bot.Touchscreen.prototype.release = function () {
if (!this.isPressed()) {
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
'Cannot release touchscreen when not already pressed.');
}
if (!bot.userAgent.IE_DOC_10) {
this.fireTouchReleaseEvents_();
} else if (!this.cancelled_) {
this.firePointerEvents_(bot.Touchscreen.fireSingleReleasePointer_);
}
bot.Device.clearPointerMap();
this.touchIdentifier_ = 0;
this.touchIdentifier2_ = 0;
this.cancelled_ = false;
};
/**
* Moves finger along the touchscreen.
*
* @param {!Element} element Element that is being pressed.
* @param {!goog.math.Coordinate} coords Coordinates relative to
* currentElement.
* @param {goog.math.Coordinate=} opt_coords2 Coordinates relative to
* currentElement.
*/
bot.Touchscreen.prototype.move = function (element, coords, opt_coords2) {
// The target element for touch actions is the original element. Hence, the
// element is set only when the touchscreen is not currently being pressed.
// The exception is IE10 which fire events on the moved to element.
var originalElement = this.getElement();
if (!this.isPressed() || bot.userAgent.IE_DOC_10) {
this.setElement(element);
}
var rect = bot.dom.getClientRect(element);
this.clientXY_.x = coords.x + rect.left;
this.clientXY_.y = coords.y + rect.top;
if (goog.isDef(opt_coords2)) {
this.clientXY2_.x = opt_coords2.x + rect.left;
this.clientXY2_.y = opt_coords2.y + rect.top;
}
if (this.isPressed()) {
if (!bot.userAgent.IE_DOC_10) {
this.fireMouseEventsOnRelease_ = false;
this.fireTouchEvent_(bot.events.EventType.TOUCHMOVE);
} else if (!this.cancelled_) {
if (element != originalElement) {
this.fireMouseEventsOnRelease_ = false;
}
if (bot.Touchscreen.hasMsTouchActionsEnabled_(element)) {
this.firePointerEvents_(bot.Touchscreen.fireSingleMovePointer_);
} else {
this.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1,
this.touchIdentifier_, MSPointerEvent.MSPOINTER_TYPE_TOUCH, true);
this.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0);
this.fireMSPointerEvent(bot.events.EventType.MSPOINTERCANCEL, coords, 0,
this.touchIdentifier_, MSPointerEvent.MSPOINTER_TYPE_TOUCH, true);
this.cancelled_ = true;
bot.Device.clearPointerMap();
}
}
}
};
/**
* Returns whether the touchscreen is currently pressed.
*
* @return {boolean} Whether the touchscreen is pressed.
*/
bot.Touchscreen.prototype.isPressed = function () {
return !!this.touchIdentifier_;
};
/**
* A helper function to fire touch events.
*
* @param {bot.events.EventType} type Event type.
* @return {boolean} Whether the event fired successfully or was cancelled.
* @private
*/
bot.Touchscreen.prototype.fireTouchEvent_ = function (type) {
if (!this.isPressed()) {
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
'Should never fire event when touchscreen is not pressed.');
}
var touchIdentifier2;
var coords2;
if (this.touchIdentifier2_) {
touchIdentifier2 = this.touchIdentifier2_;
coords2 = this.clientXY2_;
}
return this.fireTouchEvent(type, this.touchIdentifier_, this.clientXY_,
touchIdentifier2, coords2);
};
/**
* A helper function to fire touch events that occur on a release.
*
* @private
*/
bot.Touchscreen.prototype.fireTouchReleaseEvents_ = function () {
var touchendSuccess = this.fireTouchEvent_(bot.events.EventType.TOUCHEND);
// In general, TouchScreen.Release will fire the legacy mouse events:
// mousemove, mousedown, mouseup, and click after the touch events have been
// fired. The click button should be zero and only one mousemove should fire.
// Under the following cases, mouse events should not be fired:
// 1. Movement has occurred since press.
// 2. Any event handler for touchstart has called preventDefault().
// 3. Any event handler for touchend has called preventDefault(), and browser
// is Mobile Safari or Chrome.
var fireMouseEvents =
this.fireMouseEventsOnRelease_ &&
(touchendSuccess || !(bot.userAgent.IOS ||
goog.userAgent.product.CHROME));
if (fireMouseEvents) {
this.fireMouseEvent(bot.events.EventType.MOUSEMOVE, this.clientXY_, 0);
var performFocus = this.fireMouseEvent(bot.events.EventType.MOUSEDOWN,
this.clientXY_, 0);
// Element gets focus after the mousedown event only if the mousedown was
// not cancelled.
if (performFocus) {
this.focusOnElement();
}
this.maybeToggleOption();
// If a mouseup event is dispatched to an interactable event, and that
// mouseup would complete a click, then the click event must be dispatched
// even if the element becomes non-interactable after the mouseup.
var elementInteractableBeforeMouseup =
bot.dom.isInteractable(this.getElement());
this.fireMouseEvent(bot.events.EventType.MOUSEUP, this.clientXY_, 0);
// Special click logic to follow links and to perform form actions.
if (!(bot.userAgent.WINDOWS_PHONE &&
bot.dom.isElement(this.getElement(), goog.dom.TagName.OPTION))) {
this.clickElement(this.clientXY_,
/* button */ 0,
/* opt_force */ elementInteractableBeforeMouseup);
}
}
};
/**
* A helper function to fire a sequence of Pointer events.
* @param {function(!bot.Touchscreen, !Element, !goog.math.Coordinate, number,
* boolean)} fireSinglePointer A function that fires a set of events for one
* finger.
* @private
*/
bot.Touchscreen.prototype.firePointerEvents_ = function (fireSinglePointer) {
fireSinglePointer(this, this.getElement(), this.clientXY_,
this.touchIdentifier_, true);
if (this.touchIdentifier2_ &&
bot.Touchscreen.hasMsTouchActionsEnabled_(this.getElement())) {
fireSinglePointer(this, this.getElement(),
this.clientXY2_, this.touchIdentifier2_, false);
}
};
/**
* A helper function to fire Pointer events related to a press.
*
* @param {!bot.Touchscreen} ts A touchscreen object.
* @param {!Element} element Element that is being pressed.
* @param {!goog.math.Coordinate} coords Coordinates relative to
* currentElement.
* @param {number} id The touch identifier.
* @param {boolean} isPrimary Whether the pointer represents the primary point
* of contact.
* @private
*/
bot.Touchscreen.fireSinglePressPointer_ = function (ts, element, coords, id,
isPrimary) {
// Fire a mousemove event.
ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0);
// Fire a MSPointerOver and mouseover events.
ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROVER, coords, 0, id,
MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
ts.fireMouseEvent(bot.events.EventType.MOUSEOVER, coords, 0);
// Fire a MSPointerDown and mousedown events.
ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERDOWN, coords, 0, id,
MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
// Element gets focus after the mousedown event.
if (ts.fireMouseEvent(bot.events.EventType.MOUSEDOWN, coords, 0)) {
// For selectable elements, IE 10 fires a MSGotPointerCapture event.
if (bot.dom.isSelectable(element)) {
ts.fireMSPointerEvent(bot.events.EventType.MSGOTPOINTERCAPTURE, coords, 0,
id, MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
}
ts.focusOnElement();
}
};
/**
* A helper function to fire Pointer events related to a release.
*
* @param {!bot.Touchscreen} ts A touchscreen object.
* @param {!Element} element Element that is being released.
* @param {!goog.math.Coordinate} coords Coordinates relative to
* currentElement.
* @param {number} id The touch identifier.
* @param {boolean} isPrimary Whether the pointer represents the primary point
* of contact.
* @private
*/
bot.Touchscreen.fireSingleReleasePointer_ = function (ts, element, coords, id,
isPrimary) {
// Fire a MSPointerUp and mouseup events.
ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERUP, coords, 0, id,
MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
// If a mouseup event is dispatched to an interactable event, and that mouseup
// would complete a click, then the click event must be dispatched even if the
// element becomes non-interactable after the mouseup.
var elementInteractableBeforeMouseup =
bot.dom.isInteractable(ts.getElement());
ts.fireMouseEvent(bot.events.EventType.MOUSEUP, coords, 0, null, 0, false,
id);
// Fire a click.
if (ts.fireMouseEventsOnRelease_) {
ts.maybeToggleOption();
if (!(bot.userAgent.WINDOWS_PHONE &&
bot.dom.isElement(element, goog.dom.TagName.OPTION))) {
ts.clickElement(ts.clientXY_,
/* button */ 0,
/* opt_force */ elementInteractableBeforeMouseup,
id);
}
}
if (bot.dom.isSelectable(element)) {
// For selectable elements, IE 10 fires a MSLostPointerCapture event.
ts.fireMSPointerEvent(bot.events.EventType.MSLOSTPOINTERCAPTURE,
new goog.math.Coordinate(0, 0), 0, id,
MSPointerEvent.MSPOINTER_TYPE_TOUCH, false);
}
// Fire a MSPointerOut and mouseout events.
ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1, id,
MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
ts.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0, null, 0, false,
id);
};
/**
* A helper function to fire Pointer events related to a move.
*
* @param {!bot.Touchscreen} ts A touchscreen object.
* @param {!Element} element Element that is being moved.
* @param {!goog.math.Coordinate} coords Coordinates relative to
* currentElement.
* @param {number} id The touch identifier.
* @param {boolean} isPrimary Whether the pointer represents the primary point
* of contact.
* @private
*/
bot.Touchscreen.fireSingleMovePointer_ = function (ts, element, coords, id,
isPrimary) {
// Fire a MSPointerMove and mousemove events.
ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERMOVE, coords, -1, id,
MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0, null, 0, false,
id);
};
/**
* A function that determines whether an element can be manipulated by the user.
* The msTouchAction style is queried and an element can be manipulated if the
* style value is none. If an element cannot be manipulated, then move gestures
* will result in a cancellation and multi-touch events will be prevented. Tap
* gestures will still be allowed. If not on IE 10, the function returns true.
*
* @param {!Element} element The element being manipulated.
* @return {boolean} Whether the element can be manipulated.
* @private
*/
bot.Touchscreen.hasMsTouchActionsEnabled_ = function (element) {
if (!bot.userAgent.IE_DOC_10) {
throw new Error('hasMsTouchActionsEnable should only be called from IE 10');
}
// Although this particular element may have a style indicating that it cannot
// receive javascript events, its parent may indicate otherwise.
if (bot.dom.getEffectiveStyle(element, 'ms-touch-action') == 'none') {
return true;
} else {
var parent = bot.dom.getParentElement(element);
return !!parent && bot.Touchscreen.hasMsTouchActionsEnabled_(parent);
}
};