// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview The file contains an abstraction of a touch screen19* for simulating atomic touchscreen actions.20*/2122goog.provide('bot.Touchscreen');2324goog.require('bot');25goog.require('bot.Device');26goog.require('bot.Error');27goog.require('bot.ErrorCode');28goog.require('bot.dom');29goog.require('bot.events');30goog.require('bot.userAgent');31goog.require('goog.dom.TagName');32goog.require('goog.math.Coordinate');33goog.require('goog.userAgent.product');34goog.require('goog.utils');35363738/**39* A TouchScreen that provides atomic touch actions. The metaphor40* for this abstraction is a finger moving above the touchscreen that41* can press and then release the touchscreen when specified.42*43* The touchscreen supports three actions: press, release, and move.44*45* @constructor46* @extends {bot.Device}47*/48bot.Touchscreen = function () {49bot.Device.call(this);5051/** @private {!goog.math.Coordinate} */52this.clientXY_ = new goog.math.Coordinate(0, 0);5354/** @private {!goog.math.Coordinate} */55this.clientXY2_ = new goog.math.Coordinate(0, 0);56};57goog.utils.inherits(bot.Touchscreen, bot.Device);585960/** @private {boolean} */61bot.Touchscreen.prototype.fireMouseEventsOnRelease_ = true;626364/** @private {boolean} */65bot.Touchscreen.prototype.cancelled_ = false;666768/** @private {number} */69bot.Touchscreen.prototype.touchIdentifier_ = 0;707172/** @private {number} */73bot.Touchscreen.prototype.touchIdentifier2_ = 0;747576/** @private {number} */77bot.Touchscreen.prototype.touchCounter_ = 2;787980/**81* Press the touch screen. Pressing before moving results in an exception.82* Pressing while already pressed also results in an exception.83*84* @param {boolean=} opt_press2 Whether or not press the second finger during85* the press. If not defined or false, only the primary finger will be86* pressed.87*/88bot.Touchscreen.prototype.press = function (opt_press2) {89if (this.isPressed()) {90throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,91'Cannot press touchscreen when already pressed.');92}9394this.touchIdentifier_ = this.touchCounter_++;95if (opt_press2) {96this.touchIdentifier2_ = this.touchCounter_++;97}9899if (bot.userAgent.IE_DOC_10) {100this.fireMouseEventsOnRelease_ = true;101this.firePointerEvents_(bot.Touchscreen.fireSinglePressPointer_);102} else {103this.fireMouseEventsOnRelease_ = this.fireTouchEvent_(104bot.events.EventType.TOUCHSTART);105}106};107108109/**110* Releases an element on a touchscreen. Releasing an element that is not111* pressed results in an exception.112*/113bot.Touchscreen.prototype.release = function () {114if (!this.isPressed()) {115throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,116'Cannot release touchscreen when not already pressed.');117}118119if (!bot.userAgent.IE_DOC_10) {120this.fireTouchReleaseEvents_();121} else if (!this.cancelled_) {122this.firePointerEvents_(bot.Touchscreen.fireSingleReleasePointer_);123}124bot.Device.clearPointerMap();125this.touchIdentifier_ = 0;126this.touchIdentifier2_ = 0;127this.cancelled_ = false;128};129130131/**132* Moves finger along the touchscreen.133*134* @param {!Element} element Element that is being pressed.135* @param {!goog.math.Coordinate} coords Coordinates relative to136* currentElement.137* @param {goog.math.Coordinate=} opt_coords2 Coordinates relative to138* currentElement.139*/140bot.Touchscreen.prototype.move = function (element, coords, opt_coords2) {141// The target element for touch actions is the original element. Hence, the142// element is set only when the touchscreen is not currently being pressed.143// The exception is IE10 which fire events on the moved to element.144var originalElement = this.getElement();145if (!this.isPressed() || bot.userAgent.IE_DOC_10) {146this.setElement(element);147}148149var rect = bot.dom.getClientRect(element);150this.clientXY_.x = coords.x + rect.left;151this.clientXY_.y = coords.y + rect.top;152153if (opt_coords2 !== undefined) {154this.clientXY2_.x = opt_coords2.x + rect.left;155this.clientXY2_.y = opt_coords2.y + rect.top;156}157158if (this.isPressed()) {159if (!bot.userAgent.IE_DOC_10) {160this.fireMouseEventsOnRelease_ = false;161this.fireTouchEvent_(bot.events.EventType.TOUCHMOVE);162} else if (!this.cancelled_) {163if (element != originalElement) {164this.fireMouseEventsOnRelease_ = false;165}166if (bot.Touchscreen.hasMsTouchActionsEnabled_(element)) {167this.firePointerEvents_(bot.Touchscreen.fireSingleMovePointer_);168} else {169this.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1,170this.touchIdentifier_, MSPointerEvent.MSPOINTER_TYPE_TOUCH, true);171this.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0);172this.fireMSPointerEvent(bot.events.EventType.MSPOINTERCANCEL, coords, 0,173this.touchIdentifier_, MSPointerEvent.MSPOINTER_TYPE_TOUCH, true);174this.cancelled_ = true;175bot.Device.clearPointerMap();176}177}178}179};180181182/**183* Returns whether the touchscreen is currently pressed.184*185* @return {boolean} Whether the touchscreen is pressed.186*/187bot.Touchscreen.prototype.isPressed = function () {188return !!this.touchIdentifier_;189};190191192/**193* A helper function to fire touch events.194*195* @param {!bot.events.EventFactory_} type Event type.196* @return {boolean} Whether the event fired successfully or was cancelled.197* @private198*/199bot.Touchscreen.prototype.fireTouchEvent_ = function (type) {200if (!this.isPressed()) {201throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,202'Should never fire event when touchscreen is not pressed.');203}204var touchIdentifier2;205var coords2;206if (this.touchIdentifier2_) {207touchIdentifier2 = this.touchIdentifier2_;208coords2 = this.clientXY2_;209}210return this.fireTouchEvent(type, this.touchIdentifier_, this.clientXY_,211touchIdentifier2, coords2);212};213214215/**216* A helper function to fire touch events that occur on a release.217*218* @private219*/220bot.Touchscreen.prototype.fireTouchReleaseEvents_ = function () {221var touchendSuccess = this.fireTouchEvent_(bot.events.EventType.TOUCHEND);222223// In general, TouchScreen.Release will fire the legacy mouse events:224// mousemove, mousedown, mouseup, and click after the touch events have been225// fired. The click button should be zero and only one mousemove should fire.226// Under the following cases, mouse events should not be fired:227// 1. Movement has occurred since press.228// 2. Any event handler for touchstart has called preventDefault().229// 3. Any event handler for touchend has called preventDefault(), and browser230// is Mobile Safari or Chrome.231var fireMouseEvents =232this.fireMouseEventsOnRelease_ &&233(touchendSuccess || !(bot.userAgent.IOS ||234goog.userAgent.product.CHROME));235236if (fireMouseEvents) {237this.fireMouseEvent(bot.events.EventType.MOUSEMOVE, this.clientXY_, 0);238var performFocus = this.fireMouseEvent(bot.events.EventType.MOUSEDOWN,239this.clientXY_, 0);240// Element gets focus after the mousedown event only if the mousedown was241// not cancelled.242if (performFocus) {243this.focusOnElement();244}245this.maybeToggleOption();246247// If a mouseup event is dispatched to an interactable event, and that248// mouseup would complete a click, then the click event must be dispatched249// even if the element becomes non-interactable after the mouseup.250var elementInteractableBeforeMouseup =251bot.dom.isInteractable(this.getElement());252this.fireMouseEvent(bot.events.EventType.MOUSEUP, this.clientXY_, 0);253254// Special click logic to follow links and to perform form actions.255if (!(bot.userAgent.WINDOWS_PHONE &&256bot.dom.isElement(this.getElement(), goog.dom.TagName.OPTION))) {257this.clickElement(this.clientXY_,258/* button */ 0,259/* opt_force */ elementInteractableBeforeMouseup);260}261}262};263264265/**266* A helper function to fire a sequence of Pointer events.267* @param {function(!bot.Touchscreen, !Element, !goog.math.Coordinate, number,268* boolean)} fireSinglePointer A function that fires a set of events for one269* finger.270* @private271*/272bot.Touchscreen.prototype.firePointerEvents_ = function (fireSinglePointer) {273fireSinglePointer(this, this.getElement(), this.clientXY_,274this.touchIdentifier_, true);275if (this.touchIdentifier2_ &&276bot.Touchscreen.hasMsTouchActionsEnabled_(this.getElement())) {277fireSinglePointer(this, this.getElement(),278this.clientXY2_, this.touchIdentifier2_, false);279}280};281282283/**284* A helper function to fire Pointer events related to a press.285*286* @param {!bot.Touchscreen} ts A touchscreen object.287* @param {!Element} element Element that is being pressed.288* @param {!goog.math.Coordinate} coords Coordinates relative to289* currentElement.290* @param {number} id The touch identifier.291* @param {boolean} isPrimary Whether the pointer represents the primary point292* of contact.293* @private294*/295bot.Touchscreen.fireSinglePressPointer_ = function (ts, element, coords, id,296isPrimary) {297// Fire a mousemove event.298ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0);299300// Fire a MSPointerOver and mouseover events.301ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROVER, coords, 0, id,302MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);303ts.fireMouseEvent(bot.events.EventType.MOUSEOVER, coords, 0);304305// Fire a MSPointerDown and mousedown events.306ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERDOWN, coords, 0, id,307MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);308309// Element gets focus after the mousedown event.310if (ts.fireMouseEvent(bot.events.EventType.MOUSEDOWN, coords, 0)) {311// For selectable elements, IE 10 fires a MSGotPointerCapture event.312if (bot.dom.isSelectable(element)) {313ts.fireMSPointerEvent(bot.events.EventType.MSGOTPOINTERCAPTURE, coords, 0,314id, MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);315}316ts.focusOnElement();317}318};319320321/**322* A helper function to fire Pointer events related to a release.323*324* @param {!bot.Touchscreen} ts A touchscreen object.325* @param {!Element} element Element that is being released.326* @param {!goog.math.Coordinate} coords Coordinates relative to327* currentElement.328* @param {number} id The touch identifier.329* @param {boolean} isPrimary Whether the pointer represents the primary point330* of contact.331* @private332*/333bot.Touchscreen.fireSingleReleasePointer_ = function (ts, element, coords, id,334isPrimary) {335// Fire a MSPointerUp and mouseup events.336ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERUP, coords, 0, id,337MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);338339// If a mouseup event is dispatched to an interactable event, and that mouseup340// would complete a click, then the click event must be dispatched even if the341// element becomes non-interactable after the mouseup.342var elementInteractableBeforeMouseup =343bot.dom.isInteractable(ts.getElement());344ts.fireMouseEvent(bot.events.EventType.MOUSEUP, coords, 0, null, 0, false,345id);346347// Fire a click.348if (ts.fireMouseEventsOnRelease_) {349ts.maybeToggleOption();350if (!(bot.userAgent.WINDOWS_PHONE &&351bot.dom.isElement(element, goog.dom.TagName.OPTION))) {352ts.clickElement(ts.clientXY_,353/* button */ 0,354/* opt_force */ elementInteractableBeforeMouseup,355id);356}357}358359if (bot.dom.isSelectable(element)) {360// For selectable elements, IE 10 fires a MSLostPointerCapture event.361ts.fireMSPointerEvent(bot.events.EventType.MSLOSTPOINTERCAPTURE,362new goog.math.Coordinate(0, 0), 0, id,363MSPointerEvent.MSPOINTER_TYPE_TOUCH, false);364}365366// Fire a MSPointerOut and mouseout events.367ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1, id,368MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);369ts.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0, null, 0, false,370id);371};372373374/**375* A helper function to fire Pointer events related to a move.376*377* @param {!bot.Touchscreen} ts A touchscreen object.378* @param {!Element} element Element that is being moved.379* @param {!goog.math.Coordinate} coords Coordinates relative to380* currentElement.381* @param {number} id The touch identifier.382* @param {boolean} isPrimary Whether the pointer represents the primary point383* of contact.384* @private385*/386bot.Touchscreen.fireSingleMovePointer_ = function (ts, element, coords, id,387isPrimary) {388// Fire a MSPointerMove and mousemove events.389ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERMOVE, coords, -1, id,390MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);391ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0, null, 0, false,392id);393};394395396/**397* A function that determines whether an element can be manipulated by the user.398* The msTouchAction style is queried and an element can be manipulated if the399* style value is none. If an element cannot be manipulated, then move gestures400* will result in a cancellation and multi-touch events will be prevented. Tap401* gestures will still be allowed. If not on IE 10, the function returns true.402*403* @param {!Element} element The element being manipulated.404* @return {boolean} Whether the element can be manipulated.405* @private406*/407bot.Touchscreen.hasMsTouchActionsEnabled_ = function (element) {408if (!bot.userAgent.IE_DOC_10) {409throw new Error('hasMsTouchActionsEnable should only be called from IE 10');410}411412// Although this particular element may have a style indicating that it cannot413// receive javascript events, its parent may indicate otherwise.414if (bot.dom.getEffectiveStyle(element, 'ms-touch-action') == 'none') {415return true;416} else {417var parent = bot.dom.getParentElement(element);418return !!parent && bot.Touchscreen.hasMsTouchActionsEnabled_(parent);419}420};421422423