Merge pull request #38946 from Microsoft/isidorn/touch

Isidorn/touch
This commit is contained in:
Isidor Nikolic 2017-11-22 12:45:59 +01:00 committed by GitHub
commit 9069bdc91f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 77 additions and 85 deletions

View file

@ -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 {
@ -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 {

View file

@ -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';
@ -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;
@ -65,54 +64,53 @@ 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 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) {
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 static newGestureEvent(type: string): GestureEvent {
let event = <GestureEvent>(<any>document.createEvent('CustomEvent'));
event.initEvent(type, false, true);
return event;
}
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 +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);
}
}
@ -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);
@ -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
@ -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();
@ -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);
}
});
}
@ -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) {

View file

@ -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';
@ -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) {
@ -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) {
@ -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);
}
}

View file

@ -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[] {

View file

@ -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,

View file

@ -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;

View file

@ -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();
}

View file

@ -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);
@ -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)));
@ -162,10 +161,4 @@ export class NoTabsTitleControl extends TitleControl {
default: return Verbosity.MEDIUM;
}
}
public dispose(): void {
super.dispose();
this.titleTouchSupport.dispose();
}
}
}

View file

@ -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;
}