diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index a831fae65b6..fa16eed1ee3 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -7,6 +7,7 @@ import arrays = require('vs/base/common/arrays'); import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import DomUtils = require('vs/base/browser/dom'); +import { memoize } from 'vs/base/common/decorators'; export namespace EventType { export const Tap = '-monaco-gesturetap'; @@ -63,56 +64,52 @@ interface TouchEvent extends Event { changedTouches: TouchList; } -export const IS_TOUCH_DEVICE = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0; -const HOLD_DELAY = 700; - -function newGestureEvent(type: string): GestureEvent { - let event = (document.createEvent('CustomEvent')); - event.initEvent(type, false, true); - return event; -} export class Gesture implements IDisposable { private static readonly SCROLL_FRICTION = -0.005; + private static INSTANCE: Gesture; + private static HOLD_DELAY = 700; - private targetElement: HTMLElement; - private callOnTarget: IDisposable[]; + private targets: HTMLElement[]; + private toDispose: IDisposable[]; private handle: IDisposable; private activeTouches: { [id: number]: TouchData; }; - constructor(target: HTMLElement) { - this.callOnTarget = []; + private constructor() { + this.toDispose = []; this.activeTouches = {}; - this.target = target; this.handle = null; + this.toDispose.push(DomUtils.addDisposableListener(document, 'touchstart', (e) => this.onTouchStart(e))); + this.toDispose.push(DomUtils.addDisposableListener(document, 'touchend', (e) => this.onTouchEnd(e))); + this.toDispose.push(DomUtils.addDisposableListener(document, 'touchmove', (e) => this.onTouchMove(e))); + } + + public static addTarget(element: HTMLElement): void { + if (!Gesture.isTouchDevice()) { + return; + } + if (!Gesture.INSTANCE) { + Gesture.INSTANCE = new Gesture(); + } + + Gesture.INSTANCE.targets.push(element); + } + + @memoize + private static isTouchDevice(): boolean { + return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0; } public dispose(): void { - this.target = null; if (this.handle) { this.handle.dispose(); + dispose(this.toDispose); this.handle = null; } } - public set target(element: HTMLElement) { - this.callOnTarget = dispose(this.callOnTarget); - - this.activeTouches = {}; - - this.targetElement = element; - - if (!this.targetElement || !IS_TOUCH_DEVICE) { - return; - } - - this.callOnTarget.push(DomUtils.addDisposableListener(this.targetElement, 'touchstart', (e) => this.onTouchStart(e))); - this.callOnTarget.push(DomUtils.addDisposableListener(this.targetElement, 'touchend', (e) => this.onTouchEnd(e))); - this.callOnTarget.push(DomUtils.addDisposableListener(this.targetElement, 'touchmove', (e) => this.onTouchMove(e))); - } - private onTouchStart(e: TouchEvent): void { let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based. e.preventDefault(); @@ -136,10 +133,10 @@ export class Gesture implements IDisposable { rollingPageY: [touch.pageY] }; - let evt = newGestureEvent(EventType.Start); + let evt = this.newGestureEvent(EventType.Start); evt.pageX = touch.pageX; evt.pageY = touch.pageY; - this.targetElement.dispatchEvent(evt); + this.dispatchEvent(evt); } } @@ -162,25 +159,25 @@ export class Gesture implements IDisposable { let data = this.activeTouches[touch.identifier], holdTime = Date.now() - data.initialTimeStamp; - if (holdTime < HOLD_DELAY + if (holdTime < Gesture.HOLD_DELAY && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30 && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) { - let evt = newGestureEvent(EventType.Tap); + let evt = this.newGestureEvent(EventType.Tap); evt.initialTarget = data.initialTarget; evt.pageX = arrays.tail(data.rollingPageX); evt.pageY = arrays.tail(data.rollingPageY); - this.dispatch(evt); + this.dispatchEvent(evt); - } else if (holdTime >= HOLD_DELAY + } else if (holdTime >= Gesture.HOLD_DELAY && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30 && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) { - let evt = newGestureEvent(EventType.Contextmenu); + let evt = this.newGestureEvent(EventType.Contextmenu); evt.initialTarget = data.initialTarget; evt.pageX = arrays.tail(data.rollingPageX); evt.pageY = arrays.tail(data.rollingPageY); - this.dispatch(evt); + this.dispatchEvent(evt); } else if (activeTouchCount === 1) { let finalX = arrays.tail(data.rollingPageX); @@ -205,8 +202,18 @@ export class Gesture implements IDisposable { } } - protected dispatch(event: GestureEvent): void { - this.targetElement.dispatchEvent(event); + private newGestureEvent(type: string): GestureEvent { + let event = (document.createEvent('CustomEvent')); + event.initEvent(type, false, true); + return event; + } + + private dispatchEvent(event: GestureEvent): void { + this.targets.forEach(target => { + if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) { + target.dispatchEvent(event); + } + }); } private inertia(t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void { @@ -232,10 +239,10 @@ export class Gesture implements IDisposable { } // dispatch translation event - let evt = newGestureEvent(EventType.Change); + let evt = this.newGestureEvent(EventType.Change); evt.translationX = delta_pos_x; evt.translationY = delta_pos_y; - this.targetElement.dispatchEvent(evt); + this.dispatchEvent(evt); if (!stopped) { this.inertia(now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y); @@ -259,12 +266,12 @@ export class Gesture implements IDisposable { let data = this.activeTouches[touch.identifier]; - let evt = newGestureEvent(EventType.Change); + let evt = this.newGestureEvent(EventType.Change); evt.translationX = touch.pageX - arrays.tail(data.rollingPageX); evt.translationY = touch.pageY - arrays.tail(data.rollingPageY); evt.pageX = touch.pageX; evt.pageY = touch.pageY; - this.targetElement.dispatchEvent(evt); + this.dispatchEvent(evt); // only keep a few data points, to average the final speed if (data.rollingPageX.length > 3) { @@ -279,82 +286,3 @@ export class Gesture implements IDisposable { } } } - -export class SimpleGesture implements IDisposable { - - private toDispose: IDisposable[]; - private static INSTANCE: SimpleGesture; - private targets: HTMLElement[]; - - private activeTouches: { [id: number]: TouchData; }; - - private constructor() { - this.toDispose = []; - this.activeTouches = {}; - this.targets = []; - this.toDispose.push(DomUtils.addDisposableListener(document, 'touchend', (e) => this.onTouchEnd(e))); - } - - public static addTarget(element: HTMLElement): void { - if (!SimpleGesture.INSTANCE) { - SimpleGesture.INSTANCE = new SimpleGesture(); - } - - if (IS_TOUCH_DEVICE) { - SimpleGesture.INSTANCE.targets.push(element); - } - } - - public dispose(): void { - dispose(this.toDispose); - } - - private onTouchEnd(e: TouchEvent): void { - e.preventDefault(); - e.stopPropagation(); - - for (let i = 0, len = e.changedTouches.length; i < len; i++) { - - let touch = e.changedTouches.item(i); - - if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) { - console.warn('move of an UNKNOWN touch', touch); - continue; - } - - let data = this.activeTouches[touch.identifier], - holdTime = Date.now() - data.initialTimeStamp; - - if (holdTime < HOLD_DELAY - && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30 - && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) { - - let evt = newGestureEvent(EventType.Tap); - evt.initialTarget = data.initialTarget; - evt.pageX = arrays.tail(data.rollingPageX); - evt.pageY = arrays.tail(data.rollingPageY); - this.dispatch(evt); - } else if (holdTime >= HOLD_DELAY - && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30 - && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) { - - let evt = newGestureEvent(EventType.Contextmenu); - evt.initialTarget = data.initialTarget; - evt.pageX = arrays.tail(data.rollingPageX); - evt.pageY = arrays.tail(data.rollingPageY); - this.dispatch(evt); - } - - // forget about this touch - delete this.activeTouches[touch.identifier]; - } - } - - protected dispatch(event: GestureEvent): void { - this.targets.forEach(target => { - if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) { - target.dispatchEvent(event); - } - }); - } -} diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index c2256bee0a9..41eb283271e 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -14,7 +14,7 @@ import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, IRunEvent } from 'vs/base/common/actions'; import DOM = require('vs/base/browser/dom'); import types = require('vs/base/common/types'); -import { EventType, SimpleGesture } from 'vs/base/browser/touch'; +import { EventType, Gesture } from 'vs/base/browser/touch'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import Event, { Emitter } from 'vs/base/common/event'; @@ -105,7 +105,7 @@ export class BaseActionItem implements IActionItem { public render(container: HTMLElement): void { this.builder = $(container); - SimpleGesture.addTarget(container); + Gesture.addTarget(container); const enableDragging = this.options && this.options.draggable; if (enableDragging) { diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 87af1748c82..d6d67657887 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -8,7 +8,7 @@ import 'vs/css!./dropdown'; import { Builder, $ } from 'vs/base/browser/builder'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SimpleGesture, EventType as GestureEventType } from 'vs/base/browser/touch'; +import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -67,7 +67,7 @@ export class BaseDropdown extends ActionRunner { this._toDispose.push(cleanupFn); } - SimpleGesture.addTarget(this.$label.getHTMLElement()); + Gesture.addTarget(this.$label.getHTMLElement()); } public get toDispose(): IDisposable[] { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index f7a875c47f2..9fb829a2bb0 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -106,7 +106,7 @@ export class ListView implements ISpliceable, IDisposable { this.rowsContainer = document.createElement('div'); this.rowsContainer.className = 'monaco-list-rows'; - this.gesture = new Gesture(this.rowsContainer); + Gesture.addTarget(this.rowsContainer); this.scrollableElement = new ScrollableElement(this.rowsContainer, { alwaysConsumeMouseWheel: true, diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 76b81d40038..7d17583573d 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -378,7 +378,6 @@ export class TreeView extends HeightMap { private styleElement: HTMLStyleElement; private rowsContainer: HTMLElement; private scrollableElement: ScrollableElement; - private wrapperGesture: Touch.Gesture; private msGesture: MSGesture; private lastPointerType: string; private lastClickTimeStamp: number = 0; @@ -475,7 +474,7 @@ export class TreeView extends HeightMap { this.wrapper.style.msTouchAction = 'none'; this.wrapper.style.msContentZooming = 'none'; } else { - this.wrapperGesture = new Touch.Gesture(this.wrapper); + Touch.Gesture.addTarget(this.wrapper); } this.rowsContainer = document.createElement('div'); @@ -1642,11 +1641,6 @@ export class TreeView extends HeightMap { } this.domNode = null; - if (this.wrapperGesture) { - this.wrapperGesture.dispose(); - this.wrapperGesture = null; - } - if (this.context.cache) { this.context.cache.dispose(); this.context.cache = null; diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index 2c7548ff84b..d8a44df6837 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -188,12 +188,10 @@ class StandardPointerHandler extends MouseHandler implements IDisposable { class TouchHandler extends MouseHandler { - private gesture: Gesture; - constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) { super(context, viewController, viewHelper); - this.gesture = new Gesture(this.viewHelper.linesContentDomNode); + Gesture.addTarget(this.viewHelper.linesContentDomNode); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e))); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e))); @@ -202,7 +200,6 @@ class TouchHandler extends MouseHandler { } public dispose(): void { - this.gesture.dispose(); super.dispose(); } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 51bc218f8ff..72aeecf038b 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -13,7 +13,7 @@ import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { Verbosity } from 'vs/platform/editor/common/editor'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; -import { EventType as TouchEventType, GestureEvent, SimpleGesture } from 'vs/base/browser/touch'; +import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; export class NoTabsTitleControl extends TitleControl { private titleContainer: HTMLElement; @@ -31,7 +31,7 @@ export class NoTabsTitleControl extends TitleControl { this.titleContainer = parent; // Gesture Support - SimpleGesture.addTarget(this.titleContainer); + Gesture.addTarget(this.titleContainer); // Pin on double click this.toUnbind.push(DOM.addDisposableListener(this.titleContainer, DOM.EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e))); diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 054b32740ac..96b0fda1ca2 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -519,7 +519,7 @@ export class TabsTitleControl extends TitleControl { DOM.addClass(tabContainer, 'tab'); // Gesture Support - const gestureSupport = new Gesture(tabContainer); + Gesture.addTarget(tabContainer); // Tab Editor Label const editorLabel = this.instantiationService.createInstance(ResourceLabel, tabContainer, void 0); @@ -536,7 +536,7 @@ export class TabsTitleControl extends TitleControl { // Eventing const disposable = this.hookTabListeners(tabContainer, index); - this.tabDisposeables.push(combinedDisposable([disposable, bar, editorLabel, gestureSupport])); + this.tabDisposeables.push(combinedDisposable([disposable, bar, editorLabel])); return tabContainer; }