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 15export class DragGestureHandler implements Disposable { 16 private readonly boundOnMouseDown = this.onMouseDown.bind(this); 17 private readonly boundOnMouseMove = this.onMouseMove.bind(this); 18 private readonly boundOnMouseUp = this.onMouseUp.bind(this); 19 private clientRect?: DOMRect; 20 private pendingMouseDownEvent?: MouseEvent; 21 private _isDragging = false; 22 23 constructor( 24 private element: HTMLElement, 25 private onDrag: (x: number, y: number) => void, 26 private onDragStarted: (x: number, y: number) => void = () => {}, 27 private onDragFinished = () => {}, 28 ) { 29 element.addEventListener('mousedown', this.boundOnMouseDown); 30 } 31 32 private onMouseDown(e: MouseEvent) { 33 this._isDragging = true; 34 document.body.addEventListener('mousemove', this.boundOnMouseMove); 35 document.body.addEventListener('mouseup', this.boundOnMouseUp); 36 this.pendingMouseDownEvent = e; 37 } 38 39 // We don't start the drag gesture on mouse down, instead we wait until 40 // the mouse has moved at least 1px. This prevents accidental drags that 41 // were meant to be clicks. 42 private startDragGesture(e: MouseEvent) { 43 this.clientRect = this.element.getBoundingClientRect(); 44 this.onDragStarted( 45 e.clientX - this.clientRect.left, 46 e.clientY - this.clientRect.top, 47 ); 48 } 49 50 private onMouseMove(e: MouseEvent) { 51 if (e.buttons === 0) { 52 return this.onMouseUp(); 53 } 54 if ( 55 this.pendingMouseDownEvent && 56 (Math.abs(e.clientX - this.pendingMouseDownEvent.clientX) > 1 || 57 Math.abs(e.clientY - this.pendingMouseDownEvent.clientY) > 1) 58 ) { 59 this.startDragGesture(this.pendingMouseDownEvent); 60 this.pendingMouseDownEvent = undefined; 61 } 62 if (!this.pendingMouseDownEvent) { 63 this.onDrag( 64 e.clientX - this.clientRect!.left, 65 e.clientY - this.clientRect!.top, 66 ); 67 } 68 } 69 70 private onMouseUp() { 71 this._isDragging = false; 72 document.body.removeEventListener('mousemove', this.boundOnMouseMove); 73 document.body.removeEventListener('mouseup', this.boundOnMouseUp); 74 if (!this.pendingMouseDownEvent) { 75 this.onDragFinished(); 76 } 77 } 78 79 get isDragging() { 80 return this._isDragging; 81 } 82 83 [Symbol.dispose]() { 84 if (this._isDragging) { 85 this.onMouseUp(); 86 } 87 document.body.removeEventListener('mousedown', this.boundOnMouseDown); 88 document.body.removeEventListener('mousemove', this.boundOnMouseMove); 89 document.body.removeEventListener('mouseup', this.boundOnMouseUp); 90 } 91} 92