Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isidorn/touch #38946

Merged
merged 9 commits into from
Nov 22, 2017
11 changes: 7 additions & 4 deletions src/vs/base/browser/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,16 @@ class DomListener implements IDisposable {
private _node: Element | Window | Document;
private readonly _type: string;
private readonly _useCapture: boolean;
private readonly _passive: boolean;

constructor(node: Element | Window | Document, type: string, handler: (e: any) => void, useCapture: boolean) {
constructor(node: Element | Window | Document, type: string, handler: (e: any) => void, useCapture: boolean, passive: boolean) {
this._node = node;
this._type = type;
this._handler = handler;
this._useCapture = (useCapture || false);
this._node.addEventListener(this._type, this._handler, this._useCapture);
this._passive = passive;
// TODO@Isidor remove any cast once we update our lib.d.ts
this._node.addEventListener(this._type, this._handler, <any>{ capture: this._useCapture, passive: this._passive });
}

public dispose(): void {
Expand All @@ -202,8 +205,8 @@ class DomListener implements IDisposable {
}
}

export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return new DomListener(node, type, handler, useCapture);
export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean, passive?: boolean): IDisposable {
return new DomListener(node, type, handler, useCapture, passive);
}

export interface IAddStandardDisposableListenerSignature {
Expand Down
105 changes: 59 additions & 46 deletions src/vs/base/browser/touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -35,8 +36,6 @@ export interface GestureEvent extends MouseEvent {
pageY: number;
}

export const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0;

interface Touch {
identifier: number;
screenX: number;
Expand Down Expand Up @@ -65,52 +64,51 @@ interface TouchEvent extends Event {
changedTouches: TouchList;
}


export class Gesture implements IDisposable {

private static readonly HOLD_DELAY = 700;
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.targets = [];
this.toDispose.push(DomUtils.addDisposableListener(document, 'touchstart', (e) => this.onTouchStart(e), false, false));
this.toDispose.push(DomUtils.addDisposableListener(document, 'touchend', (e) => this.onTouchEnd(e), false, false));
this.toDispose.push(DomUtils.addDisposableListener(document, 'touchmove', (e) => this.onTouchMove(e), false, false));
}

public dispose(): void {
this.target = null;
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
}

public set target(element: HTMLElement) {
this.callOnTarget = dispose(this.callOnTarget);

this.activeTouches = {};

this.targetElement = element;

if (!this.targetElement) {
public static addTarget(element: HTMLElement): void {
if (!Gesture.isTouchDevice()) {
return;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = new Gesture();
}

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)));
Gesture.INSTANCE.targets.push(element);
}

private static newGestureEvent(type: string): GestureEvent {
let event = <GestureEvent>(<any>document.createEvent('CustomEvent'));
event.initEvent(type, false, true);
return event;
@memoize
private static isTouchDevice(): boolean {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0;
}

public dispose(): void {
if (this.handle) {
this.handle.dispose();
dispose(this.toDispose);
this.handle = null;
}
}

private onTouchStart(e: TouchEvent): void {
Expand All @@ -136,10 +134,10 @@ export class Gesture implements IDisposable {
rollingPageY: [touch.pageY]
};

let evt = Gesture.newGestureEvent(EventType.Start);
let evt = this.newGestureEvent(EventType.Start, touch.target);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.targetElement.dispatchEvent(evt);
this.dispatchEvent(evt);
}
}

Expand All @@ -166,21 +164,19 @@ export class Gesture implements IDisposable {
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {

let evt = Gesture.newGestureEvent(EventType.Tap);
evt.initialTarget = data.initialTarget;
let evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX);
evt.pageY = arrays.tail(data.rollingPageY);
this.targetElement.dispatchEvent(evt);
this.dispatchEvent(evt);

} 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 = Gesture.newGestureEvent(EventType.Contextmenu);
evt.initialTarget = data.initialTarget;
let evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX);
evt.pageY = arrays.tail(data.rollingPageY);
this.targetElement.dispatchEvent(evt);
this.dispatchEvent(evt);

} else if (activeTouchCount === 1) {
let finalX = arrays.tail(data.rollingPageX);
Expand All @@ -190,7 +186,9 @@ export class Gesture implements IDisposable {
let deltaX = finalX - data.rollingPageX[0];
let deltaY = finalY - data.rollingPageY[0];

this.inertia(timestamp, // time now
// We need to get all the dispatch targets on the start of the inertia event
const dispatchTo = this.targets.filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
this.inertia(dispatchTo, timestamp, // time now
Math.abs(deltaX) / deltaT, // speed
deltaX > 0 ? 1 : -1, // x direction
finalX, // x now
Expand All @@ -205,7 +203,22 @@ export class Gesture implements IDisposable {
}
}

private inertia(t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
private newGestureEvent(type: string, intialTarget?: EventTarget): GestureEvent {
let event = <GestureEvent>(<any>document.createEvent('CustomEvent'));
event.initEvent(type, false, true);
event.initialTarget = intialTarget;
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(dispatchTo: EventTarget[], t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
this.handle = DomUtils.scheduleAtNextAnimationFrame(() => {
let now = Date.now();

Expand All @@ -228,13 +241,13 @@ export class Gesture implements IDisposable {
}

// dispatch translation event
let evt = Gesture.newGestureEvent(EventType.Change);
let evt = this.newGestureEvent(EventType.Change);
evt.translationX = delta_pos_x;
evt.translationY = delta_pos_y;
this.targetElement.dispatchEvent(evt);
dispatchTo.forEach(d => d.dispatchEvent(evt));

if (!stopped) {
this.inertia(now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
this.inertia(dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
}
});
}
Expand All @@ -255,12 +268,12 @@ export class Gesture implements IDisposable {

let data = this.activeTouches[touch.identifier];

let evt = Gesture.newGestureEvent(EventType.Change);
let evt = this.newGestureEvent(EventType.Change, data.initialTarget);
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) {
Expand Down
14 changes: 3 additions & 11 deletions src/vs/base/browser/ui/actionbar/actionbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Gesture, EventType, isTouchDevice } 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';
Expand All @@ -41,7 +41,6 @@ export class BaseActionItem implements IActionItem {
public _context: any;
public _action: IAction;

private gesture: Gesture;
private _actionRunner: IActionRunner;

constructor(context: any, action: IAction, protected options?: IBaseActionItemOptions) {
Expand Down Expand Up @@ -106,16 +105,14 @@ export class BaseActionItem implements IActionItem {

public render(container: HTMLElement): void {
this.builder = $(container);
this.gesture = new Gesture(container);
Gesture.addTarget(container);

const enableDragging = this.options && this.options.draggable;
if (enableDragging) {
container.draggable = true;
}

if (isTouchDevice) {
this.builder.on(EventType.Tap, e => this.onClick(e));
}
this.builder.on(EventType.Tap, e => this.onClick(e));

this.builder.on(DOM.EventType.MOUSE_DOWN, (e) => {
if (!enableDragging) {
Expand Down Expand Up @@ -203,11 +200,6 @@ export class BaseActionItem implements IActionItem {
this.builder = null;
}

if (this.gesture) {
this.gesture.dispose();
this.gesture = null;
}

this._callOnDispose = lifecycle.dispose(this._callOnDispose);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/base/browser/ui/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class BaseDropdown extends ActionRunner {
this._toDispose.push(cleanupFn);
}

this._toDispose.push(new Gesture(this.$label.getHTMLElement()));
Gesture.addTarget(this.$label.getHTMLElement());
}

public get toDispose(): IDisposable[] {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/base/browser/ui/list/listView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class ListView<T> implements ISpliceable<T>, 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,
Expand Down
8 changes: 1 addition & 7 deletions src/vs/base/parts/tree/browser/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 1 addition & 4 deletions src/vs/editor/browser/controller/pointerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand All @@ -202,7 +200,6 @@ class TouchHandler extends MouseHandler {
}

public dispose(): void {
this.gesture.dispose();
super.dispose();
}

Expand Down
11 changes: 2 additions & 9 deletions src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/brow
export class NoTabsTitleControl extends TitleControl {
private titleContainer: HTMLElement;
private editorLabel: ResourceLabel;
private titleTouchSupport: Gesture;

public setContext(group: IEditorGroup): void {
super.setContext(group);
Expand All @@ -32,7 +31,7 @@ export class NoTabsTitleControl extends TitleControl {
this.titleContainer = parent;

// Gesture Support
this.titleTouchSupport = new Gesture(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)));
Expand Down Expand Up @@ -162,10 +161,4 @@ export class NoTabsTitleControl extends TitleControl {
default: return Verbosity.MEDIUM;
}
}

public dispose(): void {
super.dispose();

this.titleTouchSupport.dispose();
}
}
}
4 changes: 2 additions & 2 deletions src/vs/workbench/browser/parts/editor/tabsTitleControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down