// 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'); goog.require('bot.userAgent'); goog.require('goog.dom.TagName'); goog.require('goog.math.Coordinate'); goog.require('goog.userAgent.product'); goog.require('goog.utils'); /** * 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 () { bot.Device.call(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.utils.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 (opt_coords2 !== undefined) { 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.EventFactory_} 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); } };