// 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.EventType');30goog.require('goog.dom.TagName');31goog.require('goog.math.Coordinate');32goog.require('goog.userAgent.product');33343536/**37* A TouchScreen that provides atomic touch actions. The metaphor38* for this abstraction is a finger moving above the touchscreen that39* can press and then release the touchscreen when specified.40*41* The touchscreen supports three actions: press, release, and move.42*43* @constructor44* @extends {bot.Device}45*/46bot.Touchscreen = function () {47goog.base(this);4849/** @private {!goog.math.Coordinate} */50this.clientXY_ = new goog.math.Coordinate(0, 0);5152/** @private {!goog.math.Coordinate} */53this.clientXY2_ = new goog.math.Coordinate(0, 0);54};55goog.inherits(bot.Touchscreen, bot.Device);565758/** @private {boolean} */59bot.Touchscreen.prototype.fireMouseEventsOnRelease_ = true;606162/** @private {boolean} */63bot.Touchscreen.prototype.cancelled_ = false;646566/** @private {number} */67bot.Touchscreen.prototype.touchIdentifier_ = 0;686970/** @private {number} */71bot.Touchscreen.prototype.touchIdentifier2_ = 0;727374/** @private {number} */75bot.Touchscreen.prototype.touchCounter_ = 2;767778/**79* Press the touch screen. Pressing before moving results in an exception.80* Pressing while already pressed also results in an exception.81*82* @param {boolean=} opt_press2 Whether or not press the second finger during83* the press. If not defined or false, only the primary finger will be84* pressed.85*/86bot.Touchscreen.prototype.press = function (opt_press2) {87if (this.isPressed()) {88throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,89'Cannot press touchscreen when already pressed.');90}9192this.touchIdentifier_ = this.touchCounter_++;93if (opt_press2) {94this.touchIdentifier2_ = this.touchCounter_++;95}9697if (bot.userAgent.IE_DOC_10) {98this.fireMouseEventsOnRelease_ = true;99this.firePointerEvents_(bot.Touchscreen.fireSinglePressPointer_);100} else {101this.fireMouseEventsOnRelease_ = this.fireTouchEvent_(102bot.events.EventType.TOUCHSTART);103}104};105106107/**108* Releases an element on a touchscreen. Releasing an element that is not109* pressed results in an exception.110*/111bot.Touchscreen.prototype.release = function () {112if (!this.isPressed()) {113throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,114'Cannot release touchscreen when not already pressed.');115}116117if (!bot.userAgent.IE_DOC_10) {118this.fireTouchReleaseEvents_();119} else if (!this.cancelled_) {120this.firePointerEvents_(bot.Touchscreen.fireSingleReleasePointer_);121}122bot.Device.clearPointerMap();123this.touchIdentifier_ = 0;124this.touchIdentifier2_ = 0;125this.cancelled_ = false;126};127128129/**130* Moves finger along the touchscreen.131*132* @param {!Element} element Element that is being pressed.133* @param {!goog.math.Coordinate} coords Coordinates relative to134* currentElement.135* @param {goog.math.Coordinate=} opt_coords2 Coordinates relative to136* currentElement.137*/138bot.Touchscreen.prototype.move = function (element, coords, opt_coords2) {139// The target element for touch actions is the original element. Hence, the140// element is set only when the touchscreen is not currently being pressed.141// The exception is IE10 which fire events on the moved to element.142var originalElement = this.getElement();143if (!this.isPressed() || bot.userAgent.IE_DOC_10) {144this.setElement(element);145}146147var rect = bot.dom.getClientRect(element);148this.clientXY_.x = coords.x + rect.left;149this.clientXY_.y = coords.y + rect.top;150151if (goog.isDef(opt_coords2)) {152this.clientXY2_.x = opt_coords2.x + rect.left;153this.clientXY2_.y = opt_coords2.y + rect.top;154}155156if (this.isPressed()) {157if (!bot.userAgent.IE_DOC_10) {158this.fireMouseEventsOnRelease_ = false;159this.fireTouchEvent_(bot.events.EventType.TOUCHMOVE);160} else if (!this.cancelled_) {161if (element != originalElement) {162this.fireMouseEventsOnRelease_ = false;163}164if (bot.Touchscreen.hasMsTouchActionsEnabled_(element)) {165this.firePointerEvents_(bot.Touchscreen.fireSingleMovePointer_);166} else {167this.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1,168this.touchIdentifier_, MSPointerEvent.MSPOINTER_TYPE_TOUCH, true);169this.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0);170this.fireMSPointerEvent(bot.events.EventType.MSPOINTERCANCEL, coords, 0,171this.touchIdentifier_, MSPointerEvent.MSPOINTER_TYPE_TOUCH, true);172this.cancelled_ = true;173bot.Device.clearPointerMap();174}175}176}177};178179180/**181* Returns whether the touchscreen is currently pressed.182*183* @return {boolean} Whether the touchscreen is pressed.184*/185bot.Touchscreen.prototype.isPressed = function () {186return !!this.touchIdentifier_;187};188189190/**191* A helper function to fire touch events.192*193* @param {bot.events.EventType} type Event type.194* @return {boolean} Whether the event fired successfully or was cancelled.195* @private196*/197bot.Touchscreen.prototype.fireTouchEvent_ = function (type) {198if (!this.isPressed()) {199throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,200'Should never fire event when touchscreen is not pressed.');201}202var touchIdentifier2;203var coords2;204if (this.touchIdentifier2_) {205touchIdentifier2 = this.touchIdentifier2_;206coords2 = this.clientXY2_;207}208return this.fireTouchEvent(type, this.touchIdentifier_, this.clientXY_,209touchIdentifier2, coords2);210};211212213/**214* A helper function to fire touch events that occur on a release.215*216* @private217*/218bot.Touchscreen.prototype.fireTouchReleaseEvents_ = function () {219var touchendSuccess = this.fireTouchEvent_(bot.events.EventType.TOUCHEND);220221// In general, TouchScreen.Release will fire the legacy mouse events:222// mousemove, mousedown, mouseup, and click after the touch events have been223// fired. The click button should be zero and only one mousemove should fire.224// Under the following cases, mouse events should not be fired:225// 1. Movement has occurred since press.226// 2. Any event handler for touchstart has called preventDefault().227// 3. Any event handler for touchend has called preventDefault(), and browser228// is Mobile Safari or Chrome.229var fireMouseEvents =230this.fireMouseEventsOnRelease_ &&231(touchendSuccess || !(bot.userAgent.IOS ||232goog.userAgent.product.CHROME));233234if (fireMouseEvents) {235this.fireMouseEvent(bot.events.EventType.MOUSEMOVE, this.clientXY_, 0);236var performFocus = this.fireMouseEvent(bot.events.EventType.MOUSEDOWN,237this.clientXY_, 0);238// Element gets focus after the mousedown event only if the mousedown was239// not cancelled.240if (performFocus) {241this.focusOnElement();242}243this.maybeToggleOption();244245// If a mouseup event is dispatched to an interactable event, and that246// mouseup would complete a click, then the click event must be dispatched247// even if the element becomes non-interactable after the mouseup.248var elementInteractableBeforeMouseup =249bot.dom.isInteractable(this.getElement());250this.fireMouseEvent(bot.events.EventType.MOUSEUP, this.clientXY_, 0);251252// Special click logic to follow links and to perform form actions.253if (!(bot.userAgent.WINDOWS_PHONE &&254bot.dom.isElement(this.getElement(), goog.dom.TagName.OPTION))) {255this.clickElement(this.clientXY_,256/* button */ 0,257/* opt_force */ elementInteractableBeforeMouseup);258}259}260};261262263/**264* A helper function to fire a sequence of Pointer events.265* @param {function(!bot.Touchscreen, !Element, !goog.math.Coordinate, number,266* boolean)} fireSinglePointer A function that fires a set of events for one267* finger.268* @private269*/270bot.Touchscreen.prototype.firePointerEvents_ = function (fireSinglePointer) {271fireSinglePointer(this, this.getElement(), this.clientXY_,272this.touchIdentifier_, true);273if (this.touchIdentifier2_ &&274bot.Touchscreen.hasMsTouchActionsEnabled_(this.getElement())) {275fireSinglePointer(this, this.getElement(),276this.clientXY2_, this.touchIdentifier2_, false);277}278};279280281/**282* A helper function to fire Pointer events related to a press.283*284* @param {!bot.Touchscreen} ts A touchscreen object.285* @param {!Element} element Element that is being pressed.286* @param {!goog.math.Coordinate} coords Coordinates relative to287* currentElement.288* @param {number} id The touch identifier.289* @param {boolean} isPrimary Whether the pointer represents the primary point290* of contact.291* @private292*/293bot.Touchscreen.fireSinglePressPointer_ = function (ts, element, coords, id,294isPrimary) {295// Fire a mousemove event.296ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0);297298// Fire a MSPointerOver and mouseover events.299ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROVER, coords, 0, id,300MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);301ts.fireMouseEvent(bot.events.EventType.MOUSEOVER, coords, 0);302303// Fire a MSPointerDown and mousedown events.304ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERDOWN, coords, 0, id,305MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);306307// Element gets focus after the mousedown event.308if (ts.fireMouseEvent(bot.events.EventType.MOUSEDOWN, coords, 0)) {309// For selectable elements, IE 10 fires a MSGotPointerCapture event.310if (bot.dom.isSelectable(element)) {311ts.fireMSPointerEvent(bot.events.EventType.MSGOTPOINTERCAPTURE, coords, 0,312id, MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);313}314ts.focusOnElement();315}316};317318319/**320* A helper function to fire Pointer events related to a release.321*322* @param {!bot.Touchscreen} ts A touchscreen object.323* @param {!Element} element Element that is being released.324* @param {!goog.math.Coordinate} coords Coordinates relative to325* currentElement.326* @param {number} id The touch identifier.327* @param {boolean} isPrimary Whether the pointer represents the primary point328* of contact.329* @private330*/331bot.Touchscreen.fireSingleReleasePointer_ = function (ts, element, coords, id,332isPrimary) {333// Fire a MSPointerUp and mouseup events.334ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERUP, coords, 0, id,335MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);336337// If a mouseup event is dispatched to an interactable event, and that mouseup338// would complete a click, then the click event must be dispatched even if the339// element becomes non-interactable after the mouseup.340var elementInteractableBeforeMouseup =341bot.dom.isInteractable(ts.getElement());342ts.fireMouseEvent(bot.events.EventType.MOUSEUP, coords, 0, null, 0, false,343id);344345// Fire a click.346if (ts.fireMouseEventsOnRelease_) {347ts.maybeToggleOption();348if (!(bot.userAgent.WINDOWS_PHONE &&349bot.dom.isElement(element, goog.dom.TagName.OPTION))) {350ts.clickElement(ts.clientXY_,351/* button */ 0,352/* opt_force */ elementInteractableBeforeMouseup,353id);354}355}356357if (bot.dom.isSelectable(element)) {358// For selectable elements, IE 10 fires a MSLostPointerCapture event.359ts.fireMSPointerEvent(bot.events.EventType.MSLOSTPOINTERCAPTURE,360new goog.math.Coordinate(0, 0), 0, id,361MSPointerEvent.MSPOINTER_TYPE_TOUCH, false);362}363364// Fire a MSPointerOut and mouseout events.365ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1, id,366MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);367ts.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0, null, 0, false,368id);369};370371372/**373* A helper function to fire Pointer events related to a move.374*375* @param {!bot.Touchscreen} ts A touchscreen object.376* @param {!Element} element Element that is being moved.377* @param {!goog.math.Coordinate} coords Coordinates relative to378* currentElement.379* @param {number} id The touch identifier.380* @param {boolean} isPrimary Whether the pointer represents the primary point381* of contact.382* @private383*/384bot.Touchscreen.fireSingleMovePointer_ = function (ts, element, coords, id,385isPrimary) {386// Fire a MSPointerMove and mousemove events.387ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERMOVE, coords, -1, id,388MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);389ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0, null, 0, false,390id);391};392393394/**395* A function that determines whether an element can be manipulated by the user.396* The msTouchAction style is queried and an element can be manipulated if the397* style value is none. If an element cannot be manipulated, then move gestures398* will result in a cancellation and multi-touch events will be prevented. Tap399* gestures will still be allowed. If not on IE 10, the function returns true.400*401* @param {!Element} element The element being manipulated.402* @return {boolean} Whether the element can be manipulated.403* @private404*/405bot.Touchscreen.hasMsTouchActionsEnabled_ = function (element) {406if (!bot.userAgent.IE_DOC_10) {407throw new Error('hasMsTouchActionsEnable should only be called from IE 10');408}409410// Although this particular element may have a style indicating that it cannot411// receive javascript events, its parent may indicate otherwise.412if (bot.dom.getEffectiveStyle(element, 'ms-touch-action') == 'none') {413return true;414} else {415var parent = bot.dom.getParentElement(element);416return !!parent && bot.Touchscreen.hasMsTouchActionsEnabled_(parent);417}418};419420421