touch.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2016 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Touch handling for Blockly.
  22. * @author fenichel@google.com (Rachel Fenichel)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Touch');
  26. goog.require('goog.events');
  27. goog.require('goog.events.BrowserFeature');
  28. goog.require('goog.string');
  29. /**
  30. * Which touch events are we currently paying attention to?
  31. * @type {DOMString}
  32. * @private
  33. */
  34. Blockly.Touch.touchIdentifier_ = null;
  35. /**
  36. * Wrapper function called when a touch mouseUp occurs during a drag operation.
  37. * @type {Array.<!Array>}
  38. * @private
  39. */
  40. Blockly.Touch.onTouchUpWrapper_ = null;
  41. /**
  42. * The TOUCH_MAP lookup dictionary specifies additional touch events to fire,
  43. * in conjunction with mouse events.
  44. * @type {Object}
  45. */
  46. Blockly.Touch.TOUCH_MAP = {};
  47. if (goog.events.BrowserFeature.TOUCH_ENABLED) {
  48. Blockly.Touch.TOUCH_MAP = {
  49. 'mousedown': ['touchstart'],
  50. 'mousemove': ['touchmove'],
  51. 'mouseup': ['touchend', 'touchcancel']
  52. };
  53. }
  54. /**
  55. * PID of queued long-press task.
  56. * @private
  57. */
  58. Blockly.longPid_ = 0;
  59. /**
  60. * Context menus on touch devices are activated using a long-press.
  61. * Unfortunately the contextmenu touch event is currently (2015) only suported
  62. * by Chrome. This function is fired on any touchstart event, queues a task,
  63. * which after about a second opens the context menu. The tasks is killed
  64. * if the touch event terminates early.
  65. * @param {!Event} e Touch start event.
  66. * @param {!Blockly.Block|!Blockly.WorkspaceSvg} uiObject The block or workspace
  67. * under the touchstart event.
  68. * @private
  69. */
  70. Blockly.longStart_ = function(e, uiObject) {
  71. Blockly.longStop_();
  72. Blockly.longPid_ = setTimeout(function() {
  73. e.button = 2; // Simulate a right button click.
  74. uiObject.onMouseDown_(e);
  75. }, Blockly.LONGPRESS);
  76. };
  77. /**
  78. * Nope, that's not a long-press. Either touchend or touchcancel was fired,
  79. * or a drag hath begun. Kill the queued long-press task.
  80. * @private
  81. */
  82. Blockly.longStop_ = function() {
  83. if (Blockly.longPid_) {
  84. clearTimeout(Blockly.longPid_);
  85. Blockly.longPid_ = 0;
  86. }
  87. };
  88. /**
  89. * Handle a mouse-up anywhere on the page.
  90. * @param {!Event} e Mouse up event.
  91. * @private
  92. */
  93. Blockly.onMouseUp_ = function(e) {
  94. var workspace = Blockly.getMainWorkspace();
  95. if (workspace.dragMode_ == Blockly.DRAG_NONE) {
  96. return;
  97. }
  98. Blockly.Touch.clearTouchIdentifier();
  99. Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
  100. workspace.dragMode_ = Blockly.DRAG_NONE;
  101. // Unbind the touch event if it exists.
  102. if (Blockly.Touch.onTouchUpWrapper_) {
  103. Blockly.unbindEvent_(Blockly.Touch.onTouchUpWrapper_);
  104. Blockly.Touch.onTouchUpWrapper_ = null;
  105. }
  106. if (Blockly.onMouseMoveWrapper_) {
  107. Blockly.unbindEvent_(Blockly.onMouseMoveWrapper_);
  108. Blockly.onMouseMoveWrapper_ = null;
  109. }
  110. };
  111. /**
  112. * Handle a mouse-move on SVG drawing surface.
  113. * @param {!Event} e Mouse move event.
  114. * @private
  115. */
  116. Blockly.onMouseMove_ = function(e) {
  117. var workspace = Blockly.getMainWorkspace();
  118. if (workspace.dragMode_ != Blockly.DRAG_NONE) {
  119. var dx = e.clientX - workspace.startDragMouseX;
  120. var dy = e.clientY - workspace.startDragMouseY;
  121. var metrics = workspace.startDragMetrics;
  122. var x = workspace.startScrollX + dx;
  123. var y = workspace.startScrollY + dy;
  124. x = Math.min(x, -metrics.contentLeft);
  125. y = Math.min(y, -metrics.contentTop);
  126. x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
  127. metrics.contentWidth);
  128. y = Math.max(y, metrics.viewHeight - metrics.contentTop -
  129. metrics.contentHeight);
  130. // Move the scrollbars and the page will scroll automatically.
  131. workspace.scrollbar.set(-x - metrics.contentLeft,
  132. -y - metrics.contentTop);
  133. // Cancel the long-press if the drag has moved too far.
  134. if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
  135. Blockly.longStop_();
  136. workspace.dragMode_ = Blockly.DRAG_FREE;
  137. }
  138. e.stopPropagation();
  139. e.preventDefault();
  140. }
  141. };
  142. /**
  143. * Clear the touch identifier that tracks which touch stream to pay attention
  144. * to. This ends the current drag/gesture and allows other pointers to be
  145. * captured.
  146. */
  147. Blockly.Touch.clearTouchIdentifier = function() {
  148. Blockly.Touch.touchIdentifier_ = null;
  149. };
  150. /**
  151. * Decide whether Blockly should handle or ignore this event.
  152. * Mouse and touch events require special checks because we only want to deal
  153. * with one touch stream at a time. All other events should always be handled.
  154. * @param {!Event} e The event to check.
  155. * @return {boolean} True if this event should be passed through to the
  156. * registered handler; false if it should be blocked.
  157. */
  158. Blockly.Touch.shouldHandleEvent = function(e) {
  159. return !Blockly.Touch.isMouseOrTouchEvent(e) ||
  160. Blockly.Touch.checkTouchIdentifier(e);
  161. };
  162. /**
  163. * Check whether the touch identifier on the event matches the current saved
  164. * identifier. If there is no identifier, that means it's a mouse event and
  165. * we'll use the identifier "mouse". This means we won't deal well with
  166. * multiple mice being used at the same time. That seems okay.
  167. * If the current identifier was unset, save the identifier from the
  168. * event. This starts a drag/gesture, during which touch events with other
  169. * identifiers will be silently ignored.
  170. * @param {!Event} e Mouse event or touch event.
  171. * @return {boolean} Whether the identifier on the event matches the current
  172. * saved identifier.
  173. */
  174. Blockly.Touch.checkTouchIdentifier = function(e) {
  175. var identifier = (e.changedTouches && e.changedTouches[0] &&
  176. e.changedTouches[0].identifier != undefined &&
  177. e.changedTouches[0].identifier != null) ?
  178. e.changedTouches[0].identifier : 'mouse';
  179. // if (Blockly.touchIdentifier_ )is insufficient because android touch
  180. // identifiers may be zero.
  181. if (Blockly.Touch.touchIdentifier_ != undefined &&
  182. Blockly.Touch.touchIdentifier_ != null) {
  183. // We're already tracking some touch/mouse event. Is this from the same
  184. // source?
  185. return Blockly.Touch.touchIdentifier_ == identifier;
  186. }
  187. if (e.type == 'mousedown' || e.type == 'touchstart') {
  188. // No identifier set yet, and this is the start of a drag. Set it and
  189. // return.
  190. Blockly.Touch.touchIdentifier_ = identifier;
  191. return true;
  192. }
  193. // There was no identifier yet, but this wasn't a start event so we're going
  194. // to ignore it. This probably means that another drag finished while this
  195. // pointer was down.
  196. return false;
  197. };
  198. /**
  199. * Set an event's clientX and clientY from its first changed touch. Use this to
  200. * make a touch event work in a mouse event handler.
  201. * @param {!Event} e A touch event.
  202. */
  203. Blockly.Touch.setClientFromTouch = function(e) {
  204. if (goog.string.startsWith(e.type, 'touch')) {
  205. // Map the touch event's properties to the event.
  206. var touchPoint = e.changedTouches[0];
  207. e.clientX = touchPoint.clientX;
  208. e.clientY = touchPoint.clientY;
  209. }
  210. };
  211. /**
  212. * Check whether a given event is a mouse or touch event.
  213. * @param {!Event} e An event.
  214. * @return {boolean} true if it is a mouse or touch event; false otherwise.
  215. */
  216. Blockly.Touch.isMouseOrTouchEvent = function(e) {
  217. return goog.string.startsWith(e.type, 'touch') ||
  218. goog.string.startsWith(e.type, 'mouse');
  219. };
  220. /**
  221. * Split an event into an array of events, one per changed touch or mouse
  222. * point.
  223. * @param {!Event} e A mouse event or a touch event with one or more changed
  224. * touches.
  225. * @return {!Array.<!Event>} An array of mouse or touch events. Each touch
  226. * event will have exactly one changed touch.
  227. */
  228. Blockly.Touch.splitEventByTouches = function(e) {
  229. var events = [];
  230. if (e.changedTouches) {
  231. for (var i = 0; i < e.changedTouches.length; i++) {
  232. var newEvent = {
  233. type: e.type,
  234. changedTouches: [e.changedTouches[i]],
  235. target: e.target,
  236. stopPropagation: function(){ e.stopPropagation(); },
  237. preventDefault: function(){ e.preventDefault(); }
  238. };
  239. events[i] = newEvent;
  240. }
  241. } else {
  242. events.push(e);
  243. }
  244. return events;
  245. };