touch.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. import * as DomUtils from './dom.js';
  12. import * as arrays from '../common/arrays.js';
  13. import { memoize } from '../common/decorators.js';
  14. import { Disposable } from '../common/lifecycle.js';
  15. export var EventType;
  16. (function (EventType) {
  17. EventType.Tap = '-monaco-gesturetap';
  18. EventType.Change = '-monaco-gesturechange';
  19. EventType.Start = '-monaco-gesturestart';
  20. EventType.End = '-monaco-gesturesend';
  21. EventType.Contextmenu = '-monaco-gesturecontextmenu';
  22. })(EventType || (EventType = {}));
  23. export class Gesture extends Disposable {
  24. constructor() {
  25. super();
  26. this.dispatched = false;
  27. this.activeTouches = {};
  28. this.handle = null;
  29. this.targets = [];
  30. this.ignoreTargets = [];
  31. this._lastSetTapCountTime = 0;
  32. this._register(DomUtils.addDisposableListener(document, 'touchstart', (e) => this.onTouchStart(e), { passive: false }));
  33. this._register(DomUtils.addDisposableListener(document, 'touchend', (e) => this.onTouchEnd(e)));
  34. this._register(DomUtils.addDisposableListener(document, 'touchmove', (e) => this.onTouchMove(e), { passive: false }));
  35. }
  36. static addTarget(element) {
  37. if (!Gesture.isTouchDevice()) {
  38. return Disposable.None;
  39. }
  40. if (!Gesture.INSTANCE) {
  41. Gesture.INSTANCE = new Gesture();
  42. }
  43. Gesture.INSTANCE.targets.push(element);
  44. return {
  45. dispose: () => {
  46. Gesture.INSTANCE.targets = Gesture.INSTANCE.targets.filter(t => t !== element);
  47. }
  48. };
  49. }
  50. static ignoreTarget(element) {
  51. if (!Gesture.isTouchDevice()) {
  52. return Disposable.None;
  53. }
  54. if (!Gesture.INSTANCE) {
  55. Gesture.INSTANCE = new Gesture();
  56. }
  57. Gesture.INSTANCE.ignoreTargets.push(element);
  58. return {
  59. dispose: () => {
  60. Gesture.INSTANCE.ignoreTargets = Gesture.INSTANCE.ignoreTargets.filter(t => t !== element);
  61. }
  62. };
  63. }
  64. static isTouchDevice() {
  65. // `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
  66. // `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
  67. return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
  68. }
  69. dispose() {
  70. if (this.handle) {
  71. this.handle.dispose();
  72. this.handle = null;
  73. }
  74. super.dispose();
  75. }
  76. onTouchStart(e) {
  77. let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
  78. if (this.handle) {
  79. this.handle.dispose();
  80. this.handle = null;
  81. }
  82. for (let i = 0, len = e.targetTouches.length; i < len; i++) {
  83. let touch = e.targetTouches.item(i);
  84. this.activeTouches[touch.identifier] = {
  85. id: touch.identifier,
  86. initialTarget: touch.target,
  87. initialTimeStamp: timestamp,
  88. initialPageX: touch.pageX,
  89. initialPageY: touch.pageY,
  90. rollingTimestamps: [timestamp],
  91. rollingPageX: [touch.pageX],
  92. rollingPageY: [touch.pageY]
  93. };
  94. let evt = this.newGestureEvent(EventType.Start, touch.target);
  95. evt.pageX = touch.pageX;
  96. evt.pageY = touch.pageY;
  97. this.dispatchEvent(evt);
  98. }
  99. if (this.dispatched) {
  100. e.preventDefault();
  101. e.stopPropagation();
  102. this.dispatched = false;
  103. }
  104. }
  105. onTouchEnd(e) {
  106. let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
  107. let activeTouchCount = Object.keys(this.activeTouches).length;
  108. for (let i = 0, len = e.changedTouches.length; i < len; i++) {
  109. let touch = e.changedTouches.item(i);
  110. if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
  111. console.warn('move of an UNKNOWN touch', touch);
  112. continue;
  113. }
  114. let data = this.activeTouches[touch.identifier], holdTime = Date.now() - data.initialTimeStamp;
  115. if (holdTime < Gesture.HOLD_DELAY
  116. && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
  117. && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {
  118. let evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
  119. evt.pageX = arrays.tail(data.rollingPageX);
  120. evt.pageY = arrays.tail(data.rollingPageY);
  121. this.dispatchEvent(evt);
  122. }
  123. else if (holdTime >= Gesture.HOLD_DELAY
  124. && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
  125. && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {
  126. let evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
  127. evt.pageX = arrays.tail(data.rollingPageX);
  128. evt.pageY = arrays.tail(data.rollingPageY);
  129. this.dispatchEvent(evt);
  130. }
  131. else if (activeTouchCount === 1) {
  132. let finalX = arrays.tail(data.rollingPageX);
  133. let finalY = arrays.tail(data.rollingPageY);
  134. let deltaT = arrays.tail(data.rollingTimestamps) - data.rollingTimestamps[0];
  135. let deltaX = finalX - data.rollingPageX[0];
  136. let deltaY = finalY - data.rollingPageY[0];
  137. // We need to get all the dispatch targets on the start of the inertia event
  138. const dispatchTo = this.targets.filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
  139. this.inertia(dispatchTo, timestamp, // time now
  140. Math.abs(deltaX) / deltaT, // speed
  141. deltaX > 0 ? 1 : -1, // x direction
  142. finalX, // x now
  143. Math.abs(deltaY) / deltaT, // y speed
  144. deltaY > 0 ? 1 : -1, // y direction
  145. finalY // y now
  146. );
  147. }
  148. this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
  149. // forget about this touch
  150. delete this.activeTouches[touch.identifier];
  151. }
  152. if (this.dispatched) {
  153. e.preventDefault();
  154. e.stopPropagation();
  155. this.dispatched = false;
  156. }
  157. }
  158. newGestureEvent(type, initialTarget) {
  159. let event = document.createEvent('CustomEvent');
  160. event.initEvent(type, false, true);
  161. event.initialTarget = initialTarget;
  162. event.tapCount = 0;
  163. return event;
  164. }
  165. dispatchEvent(event) {
  166. if (event.type === EventType.Tap) {
  167. const currentTime = (new Date()).getTime();
  168. let setTapCount = 0;
  169. if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
  170. setTapCount = 1;
  171. }
  172. else {
  173. setTapCount = 2;
  174. }
  175. this._lastSetTapCountTime = currentTime;
  176. event.tapCount = setTapCount;
  177. }
  178. else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
  179. // tap is canceled by scrolling or context menu
  180. this._lastSetTapCountTime = 0;
  181. }
  182. for (let i = 0; i < this.ignoreTargets.length; i++) {
  183. if (event.initialTarget instanceof Node && this.ignoreTargets[i].contains(event.initialTarget)) {
  184. return;
  185. }
  186. }
  187. this.targets.forEach(target => {
  188. if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) {
  189. target.dispatchEvent(event);
  190. this.dispatched = true;
  191. }
  192. });
  193. }
  194. inertia(dispatchTo, t1, vX, dirX, x, vY, dirY, y) {
  195. this.handle = DomUtils.scheduleAtNextAnimationFrame(() => {
  196. let now = Date.now();
  197. // velocity: old speed + accel_over_time
  198. let deltaT = now - t1, delta_pos_x = 0, delta_pos_y = 0, stopped = true;
  199. vX += Gesture.SCROLL_FRICTION * deltaT;
  200. vY += Gesture.SCROLL_FRICTION * deltaT;
  201. if (vX > 0) {
  202. stopped = false;
  203. delta_pos_x = dirX * vX * deltaT;
  204. }
  205. if (vY > 0) {
  206. stopped = false;
  207. delta_pos_y = dirY * vY * deltaT;
  208. }
  209. // dispatch translation event
  210. let evt = this.newGestureEvent(EventType.Change);
  211. evt.translationX = delta_pos_x;
  212. evt.translationY = delta_pos_y;
  213. dispatchTo.forEach(d => d.dispatchEvent(evt));
  214. if (!stopped) {
  215. this.inertia(dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
  216. }
  217. });
  218. }
  219. onTouchMove(e) {
  220. let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
  221. for (let i = 0, len = e.changedTouches.length; i < len; i++) {
  222. let touch = e.changedTouches.item(i);
  223. if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
  224. console.warn('end of an UNKNOWN touch', touch);
  225. continue;
  226. }
  227. let data = this.activeTouches[touch.identifier];
  228. let evt = this.newGestureEvent(EventType.Change, data.initialTarget);
  229. evt.translationX = touch.pageX - arrays.tail(data.rollingPageX);
  230. evt.translationY = touch.pageY - arrays.tail(data.rollingPageY);
  231. evt.pageX = touch.pageX;
  232. evt.pageY = touch.pageY;
  233. this.dispatchEvent(evt);
  234. // only keep a few data points, to average the final speed
  235. if (data.rollingPageX.length > 3) {
  236. data.rollingPageX.shift();
  237. data.rollingPageY.shift();
  238. data.rollingTimestamps.shift();
  239. }
  240. data.rollingPageX.push(touch.pageX);
  241. data.rollingPageY.push(touch.pageY);
  242. data.rollingTimestamps.push(timestamp);
  243. }
  244. if (this.dispatched) {
  245. e.preventDefault();
  246. e.stopPropagation();
  247. this.dispatched = false;
  248. }
  249. }
  250. }
  251. Gesture.SCROLL_FRICTION = -0.005;
  252. Gesture.HOLD_DELAY = 700;
  253. Gesture.CLEAR_TAP_COUNT_TIME = 400; // ms
  254. __decorate([
  255. memoize
  256. ], Gesture, "isTouchDevice", null);