// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // The delegate interface: // dragContainer --> // element containing the draggable items // // transitionsDuration --> // length of time of transitions in ms // // dragItem --> // get / set property containing the item being dragged // // getItem(e) --> // get's the item that is under the mouse event |e| // // canDropOn(coordinates) --> // returns true if the coordinates (relative to the drag container) // point to a valid place to drop an item // // setDragPlaceholder(coordinates) --> // tells the delegate that the dragged item is currently above // the specified coordinates. // // saveDrag(draggedItem) --> // tells the delegate that the drag is done. move the item to the // position last specified by setDragPlaceholder (e.g., commit changes). // draggedItem was the item being dragged. // // The distance, in px, that the mouse must move before initiating a drag. var DRAG_THRESHOLD = 35; function DragAndDropController(delegate) { this.delegate_ = delegate; // Install the 'mousedown' handler, the entry point to drag and drop. var el = this.delegate_.dragContainer; el.addEventListener('mousedown', this.handleMouseDown_.bind(this)); } DragAndDropController.prototype = { isDragging_: false, startItem_: null, startItemXY_: null, startMouseXY_: null, mouseXY_: null, // Enables the handlers that are only active during a drag. enableHandlers_: function() { // Record references to the generated functions so we can // remove the listeners later. this.mouseMoveListener_ = this.handleMouseMove_.bind(this); this.mouseUpListener_ = this.handleMouseUp_.bind(this); this.scrollListener_ = this.handleScroll_.bind(this); document.addEventListener('mousemove', this.mouseMoveListener_, true); document.addEventListener('mouseup', this.mouseUpListener_, true); document.addEventListener('scroll', this.scrollListener_, true); }, disableHandlers_: function() { document.removeEventListener('mousemove', this.mouseMoveListener_, true); document.removeEventListener('mouseup', this.mouseUpListener_, true); document.removeEventListener('scroll', this.scrollListener_, true); }, isDragging: function() { return this.isDragging_; }, distance_: function(p1, p2) { var x2 = Math.pow(p1.x - p2.x, 2); var y2 = Math.pow(p1.y - p2.y, 2); return Math.sqrt(x2 + y2); }, // Shifts the client coordinates, |xy|, so they are relative to the top left // of the drag container. getCoordinates_: function(xy) { var rect = this.delegate_.dragContainer.getBoundingClientRect(); var coordinates = { x: xy.x - rect.left, y: xy.y - rect.top }; // If we're in an RTL language, reflect the coordinates so the delegate // doesn't need to worry about it. if (isRtl()) coordinates.x = this.delegate_.dragContainer.offsetWidth - coordinates.x; return coordinates; }, // Listen to mousedown to get the relative position of the cursor when // starting drag and drop. handleMouseDown_: function(e) { var item = this.delegate_.getItem(e); // This can't be a drag & drop event if it's not the left mouse button // or if the mouse is not above an item. We also bail out if the dragging // flag is still set (the flag remains around for a bit so that 'click' // event handlers can distinguish between a click and drag). if (!item || e.button != 0 || this.isDragging()) return; this.startItem_ = item; this.startItemXY_ = {x: item.offsetLeft, y: item.offsetTop}; this.startMouseXY_ = {x: e.clientX, y: e.clientY}; this.startScrollXY_ = {x: window.scrollX, y: window.scrollY}; this.enableHandlers_(); }, handleMouseMove_: function(e) { this.mouseXY_ = {x: e.clientX, y: e.clientY}; if (this.isDragging()) { this.handleDrag_(); return; } // Initiate the drag if the mouse has moved far enough. if (this.distance_(this.startMouseXY_, this.mouseXY_) >= DRAG_THRESHOLD) this.handleDragStart_(); }, handleMouseUp_: function() { this.handleDrop_(); }, handleScroll_: function(e) { if (this.isDragging()) this.handleDrag_(); }, handleDragStart_: function() { // Use the item that the mouse was above when 'mousedown' fired. var item = this.startItem_; if (!item) return; this.isDragging_ = true; this.delegate_.dragItem = item; item.classList.add('dragging'); item.style.zIndex = 2; }, handleDragOver_: function() { var coordinates = this.getCoordinates_(this.mouseXY_); if (!this.delegate_.canDropOn(coordinates)) return; this.delegate_.setDragPlaceholder(coordinates); }, handleDrop_: function() { this.disableHandlers_(); var dragItem = this.delegate_.dragItem; if (!dragItem) return; this.delegate_.dragItem = this.startItem_ = null; this.delegate_.saveDrag(dragItem); dragItem.classList.remove('dragging'); setTimeout(function() { // Keep the flag around a little so other 'mouseup' and 'click' // listeners know the event is from a drag operation. this.isDragging_ = false; dragItem.style.zIndex = 0; }.bind(this), this.delegate_.transitionsDuration); }, handleDrag_: function() { // Moves the drag item making sure that it is not displayed outside the // drag container. var dragItem = this.delegate_.dragItem; var dragContainer = this.delegate_.dragContainer; var rect = dragContainer.getBoundingClientRect(); // First, move the item the same distance the mouse has moved. var x = this.startItemXY_.x + this.mouseXY_.x - this.startMouseXY_.x + window.scrollX - this.startScrollXY_.x; var y = this.startItemXY_.y + this.mouseXY_.y - this.startMouseXY_.y + window.scrollY - this.startScrollXY_.y; var w = this.delegate_.dimensions.width; var h = this.delegate_.dimensions.height; var offset = parseInt(getComputedStyle(dragContainer).marginLeft); // The position of the item is relative to the drag container. We // want to make sure that half of the item's width or height is within // the container. x = Math.max(x, - w / 2 - offset); x = Math.min(x, rect.width + w / 2 - offset); y = Math.max(- h / 2, y); y = Math.min(y, rect.height - h / 2); dragItem.style.left = x + 'px'; dragItem.style.top = y + 'px'; // Update the layouts and positions based on the new drag location. this.handleDragOver_(); this.delegate_.scrollPage(this.mouseXY_); } };