Draggable.js (3232B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 createRef, 9 Component, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 const { 14 canPointerEventDrag, 15 } = require("resource://devtools/client/shared/events.js"); 16 17 class Draggable extends Component { 18 static get propTypes() { 19 return { 20 onMove: PropTypes.func.isRequired, 21 onDoubleClick: PropTypes.func, 22 onStart: PropTypes.func, 23 onStop: PropTypes.func, 24 style: PropTypes.object, 25 title: PropTypes.string, 26 className: PropTypes.string, 27 }; 28 } 29 30 constructor(props) { 31 super(props); 32 33 this.draggableEl = createRef(); 34 35 this.startDragging = this.startDragging.bind(this); 36 this.stopDragging = this.stopDragging.bind(this); 37 this.onDoubleClick = this.onDoubleClick.bind(this); 38 this.onMove = this.onMove.bind(this); 39 40 this.mouseX = 0; 41 this.mouseY = 0; 42 } 43 startDragging(ev) { 44 if (!canPointerEventDrag(ev)) { 45 return; 46 } 47 48 const xDiff = Math.abs(this.mouseX - ev.clientX); 49 const yDiff = Math.abs(this.mouseY - ev.clientY); 50 51 // This allows for double-click. 52 if (this.props.onDoubleClick && xDiff + yDiff <= 1) { 53 return; 54 } 55 this.mouseX = ev.clientX; 56 this.mouseY = ev.clientY; 57 58 if (this.isDragging) { 59 return; 60 } 61 this.isDragging = true; 62 63 // "pointermove" is fired when the button state is changed too. Therefore, 64 // we should listen to "mousemove" to handle the pointer position changes. 65 this.draggableEl.current.addEventListener("mousemove", this.onMove); 66 this.draggableEl.current.setPointerCapture(ev.pointerId); 67 this.draggableEl.current.addEventListener( 68 "mousedown", 69 event => event.preventDefault(), 70 { once: true } 71 ); 72 73 this.props.onStart && this.props.onStart(); 74 } 75 76 onDoubleClick() { 77 if (this.props.onDoubleClick) { 78 this.props.onDoubleClick(); 79 } 80 } 81 82 onMove(ev) { 83 if (!this.isDragging) { 84 return; 85 } 86 87 ev.preventDefault(); 88 // Use viewport coordinates so, moving mouse over iframes 89 // doesn't mangle (relative) coordinates. 90 this.props.onMove(ev.clientX, ev.clientY); 91 } 92 93 stopDragging() { 94 if (!this.isDragging) { 95 return; 96 } 97 this.isDragging = false; 98 this.draggableEl.current.removeEventListener("mousemove", this.onMove); 99 this.draggableEl.current.addEventListener( 100 "mouseup", 101 event => event.preventDefault(), 102 { once: true } 103 ); 104 this.props.onStop && this.props.onStop(); 105 } 106 107 render() { 108 return dom.div({ 109 ref: this.draggableEl, 110 role: "presentation", 111 style: this.props.style, 112 title: this.props.title, 113 className: this.props.className, 114 onPointerDown: this.startDragging, 115 onPointerUp: this.stopDragging, 116 onDoubleClick: this.onDoubleClick, 117 }); 118 } 119 } 120 121 module.exports = Draggable;