1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// The delegate interface: 6// dragContainer --> 7// element containing the draggable items 8// 9// transitionsDuration --> 10// length of time of transitions in ms 11// 12// dragItem --> 13// get / set property containing the item being dragged 14// 15// getItem(e) --> 16// get's the item that is under the mouse event |e| 17// 18// canDropOn(coordinates) --> 19// returns true if the coordinates (relative to the drag container) 20// point to a valid place to drop an item 21// 22// setDragPlaceholder(coordinates) --> 23// tells the delegate that the dragged item is currently above 24// the specified coordinates. 25// 26// saveDrag(draggedItem) --> 27// tells the delegate that the drag is done. move the item to the 28// position last specified by setDragPlaceholder (e.g., commit changes). 29// draggedItem was the item being dragged. 30// 31 32// The distance, in px, that the mouse must move before initiating a drag. 33var DRAG_THRESHOLD = 35; 34 35function DragAndDropController(delegate) { 36 this.delegate_ = delegate; 37 38 // Install the 'mousedown' handler, the entry point to drag and drop. 39 var el = this.delegate_.dragContainer; 40 el.addEventListener('mousedown', this.handleMouseDown_.bind(this)); 41} 42 43DragAndDropController.prototype = { 44 isDragging_: false, 45 startItem_: null, 46 startItemXY_: null, 47 startMouseXY_: null, 48 mouseXY_: null, 49 50 // Enables the handlers that are only active during a drag. 51 enableHandlers_: function() { 52 // Record references to the generated functions so we can 53 // remove the listeners later. 54 this.mouseMoveListener_ = this.handleMouseMove_.bind(this); 55 this.mouseUpListener_ = this.handleMouseUp_.bind(this); 56 this.scrollListener_ = this.handleScroll_.bind(this); 57 58 document.addEventListener('mousemove', this.mouseMoveListener_, true); 59 document.addEventListener('mouseup', this.mouseUpListener_, true); 60 document.addEventListener('scroll', this.scrollListener_, true); 61 }, 62 63 disableHandlers_: function() { 64 document.removeEventListener('mousemove', this.mouseMoveListener_, true); 65 document.removeEventListener('mouseup', this.mouseUpListener_, true); 66 document.removeEventListener('scroll', this.scrollListener_, true); 67 }, 68 69 isDragging: function() { 70 return this.isDragging_; 71 }, 72 73 distance_: function(p1, p2) { 74 var x2 = Math.pow(p1.x - p2.x, 2); 75 var y2 = Math.pow(p1.y - p2.y, 2); 76 return Math.sqrt(x2 + y2); 77 }, 78 79 // Shifts the client coordinates, |xy|, so they are relative to the top left 80 // of the drag container. 81 getCoordinates_: function(xy) { 82 var rect = this.delegate_.dragContainer.getBoundingClientRect(); 83 var coordinates = { 84 x: xy.x - rect.left, 85 y: xy.y - rect.top 86 }; 87 88 // If we're in an RTL language, reflect the coordinates so the delegate 89 // doesn't need to worry about it. 90 if (isRtl()) 91 coordinates.x = this.delegate_.dragContainer.offsetWidth - coordinates.x; 92 93 return coordinates; 94 }, 95 96 // Listen to mousedown to get the relative position of the cursor when 97 // starting drag and drop. 98 handleMouseDown_: function(e) { 99 var item = this.delegate_.getItem(e); 100 101 // This can't be a drag & drop event if it's not the left mouse button 102 // or if the mouse is not above an item. We also bail out if the dragging 103 // flag is still set (the flag remains around for a bit so that 'click' 104 // event handlers can distinguish between a click and drag). 105 if (!item || e.button != 0 || this.isDragging()) 106 return; 107 108 this.startItem_ = item; 109 this.startItemXY_ = {x: item.offsetLeft, y: item.offsetTop}; 110 this.startMouseXY_ = {x: e.clientX, y: e.clientY}; 111 this.startScrollXY_ = {x: window.scrollX, y: window.scrollY}; 112 113 this.enableHandlers_(); 114 }, 115 116 handleMouseMove_: function(e) { 117 this.mouseXY_ = {x: e.clientX, y: e.clientY}; 118 119 if (this.isDragging()) { 120 this.handleDrag_(); 121 return; 122 } 123 124 // Initiate the drag if the mouse has moved far enough. 125 if (this.distance_(this.startMouseXY_, this.mouseXY_) >= DRAG_THRESHOLD) 126 this.handleDragStart_(); 127 }, 128 129 handleMouseUp_: function() { 130 this.handleDrop_(); 131 }, 132 133 handleScroll_: function(e) { 134 if (this.isDragging()) 135 this.handleDrag_(); 136 }, 137 138 handleDragStart_: function() { 139 // Use the item that the mouse was above when 'mousedown' fired. 140 var item = this.startItem_; 141 if (!item) 142 return; 143 144 this.isDragging_ = true; 145 this.delegate_.dragItem = item; 146 item.classList.add('dragging'); 147 item.style.zIndex = 2; 148 }, 149 150 handleDragOver_: function() { 151 var coordinates = this.getCoordinates_(this.mouseXY_); 152 if (!this.delegate_.canDropOn(coordinates)) 153 return; 154 155 this.delegate_.setDragPlaceholder(coordinates); 156 }, 157 158 handleDrop_: function() { 159 this.disableHandlers_(); 160 161 var dragItem = this.delegate_.dragItem; 162 if (!dragItem) 163 return; 164 165 this.delegate_.dragItem = this.startItem_ = null; 166 this.delegate_.saveDrag(dragItem); 167 dragItem.classList.remove('dragging'); 168 169 setTimeout(function() { 170 // Keep the flag around a little so other 'mouseup' and 'click' 171 // listeners know the event is from a drag operation. 172 this.isDragging_ = false; 173 dragItem.style.zIndex = 0; 174 }.bind(this), this.delegate_.transitionsDuration); 175 }, 176 177 handleDrag_: function() { 178 // Moves the drag item making sure that it is not displayed outside the 179 // drag container. 180 var dragItem = this.delegate_.dragItem; 181 var dragContainer = this.delegate_.dragContainer; 182 var rect = dragContainer.getBoundingClientRect(); 183 184 // First, move the item the same distance the mouse has moved. 185 var x = this.startItemXY_.x + this.mouseXY_.x - this.startMouseXY_.x + 186 window.scrollX - this.startScrollXY_.x; 187 var y = this.startItemXY_.y + this.mouseXY_.y - this.startMouseXY_.y + 188 window.scrollY - this.startScrollXY_.y; 189 190 var w = this.delegate_.dimensions.width; 191 var h = this.delegate_.dimensions.height; 192 193 var offset = parseInt(getComputedStyle(dragContainer).marginLeft); 194 195 // The position of the item is relative to the drag container. We 196 // want to make sure that half of the item's width or height is within 197 // the container. 198 x = Math.max(x, - w / 2 - offset); 199 x = Math.min(x, rect.width + w / 2 - offset); 200 201 y = Math.max(- h / 2, y); 202 y = Math.min(y, rect.height - h / 2); 203 204 dragItem.style.left = x + 'px'; 205 dragItem.style.top = y + 'px'; 206 207 // Update the layouts and positions based on the new drag location. 208 this.handleDragOver_(); 209 210 this.delegate_.scrollPage(this.mouseXY_); 211 } 212}; 213