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 m from 'mithril'; 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 m.redraw(); 51 } 52 53 private onMouseMove(e: MouseEvent) { 54 if (e.buttons === 0) { 55 return this.onMouseUp(); 56 } 57 if ( 58 this.pendingMouseDownEvent && 59 (Math.abs(e.clientX - this.pendingMouseDownEvent.clientX) > 1 || 60 Math.abs(e.clientY - this.pendingMouseDownEvent.clientY) > 1) 61 ) { 62 this.startDragGesture(this.pendingMouseDownEvent); 63 this.pendingMouseDownEvent = undefined; 64 } 65 if (!this.pendingMouseDownEvent) { 66 this.onDrag( 67 e.clientX - this.clientRect!.left, 68 e.clientY - this.clientRect!.top, 69 ); 70 } 71 } 72 73 private onMouseUp() { 74 this._isDragging = false; 75 document.body.removeEventListener('mousemove', this.boundOnMouseMove); 76 document.body.removeEventListener('mouseup', this.boundOnMouseUp); 77 if (!this.pendingMouseDownEvent) { 78 this.onDragFinished(); 79 m.redraw(); 80 } 81 } 82 83 get isDragging() { 84 return this._isDragging; 85 } 86 87 [Symbol.dispose]() { 88 if (this._isDragging) { 89 this.onMouseUp(); 90 } 91 document.body.removeEventListener('mousedown', this.boundOnMouseDown); 92 document.body.removeEventListener('mousemove', this.boundOnMouseMove); 93 document.body.removeEventListener('mouseup', this.boundOnMouseUp); 94 } 95} 96