1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Disposable} from '../base/disposable'; 16 17export class DragGestureHandler implements Disposable { 18 private readonly boundOnMouseDown = this.onMouseDown.bind(this); 19 private readonly boundOnMouseMove = this.onMouseMove.bind(this); 20 private readonly boundOnMouseUp = this.onMouseUp.bind(this); 21 private clientRect?: DOMRect; 22 private pendingMouseDownEvent?: MouseEvent; 23 private _isDragging = false; 24 25 constructor( 26 private element: HTMLElement, 27 private onDrag: (x: number, y: number) => void, 28 private onDragStarted: (x: number, y: number) => void = () => {}, 29 private onDragFinished = () => {}, 30 ) { 31 element.addEventListener('mousedown', this.boundOnMouseDown); 32 } 33 34 private onMouseDown(e: MouseEvent) { 35 this._isDragging = true; 36 document.body.addEventListener('mousemove', this.boundOnMouseMove); 37 document.body.addEventListener('mouseup', this.boundOnMouseUp); 38 this.pendingMouseDownEvent = e; 39 } 40 41 // We don't start the drag gesture on mouse down, instead we wait until 42 // the mouse has moved at least 1px. This prevents accidental drags that 43 // were meant to be clicks. 44 private startDragGesture(e: MouseEvent) { 45 this.clientRect = this.element.getBoundingClientRect(); 46 this.onDragStarted( 47 e.clientX - this.clientRect.left, 48 e.clientY - this.clientRect.top, 49 ); 50 } 51 52 private onMouseMove(e: MouseEvent) { 53 if (e.buttons === 0) { 54 return this.onMouseUp(); 55 } 56 if ( 57 this.pendingMouseDownEvent && 58 (Math.abs(e.clientX - this.pendingMouseDownEvent.clientX) > 1 || 59 Math.abs(e.clientY - this.pendingMouseDownEvent.clientY) > 1) 60 ) { 61 this.startDragGesture(this.pendingMouseDownEvent); 62 this.pendingMouseDownEvent = undefined; 63 } 64 if (!this.pendingMouseDownEvent) { 65 this.onDrag( 66 e.clientX - this.clientRect!.left, 67 e.clientY - this.clientRect!.top, 68 ); 69 } 70 } 71 72 private onMouseUp() { 73 this._isDragging = false; 74 document.body.removeEventListener('mousemove', this.boundOnMouseMove); 75 document.body.removeEventListener('mouseup', this.boundOnMouseUp); 76 if (!this.pendingMouseDownEvent) { 77 this.onDragFinished(); 78 } 79 } 80 81 get isDragging() { 82 return this._isDragging; 83 } 84 85 dispose() { 86 if (this._isDragging) { 87 this.onMouseUp(); 88 } 89 document.body.removeEventListener('mousedown', this.boundOnMouseDown); 90 document.body.removeEventListener('mousemove', this.boundOnMouseMove); 91 document.body.removeEventListener('mouseup', this.boundOnMouseUp); 92 } 93} 94