// 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.
/**
* @fileoverview Grabber implementation.
* Allows you to pick up objects (with a long-press) and drag them around the
* screen.
*
* Note: This should perhaps really use standard drag-and-drop events, but there
* is no standard for them on touch devices. We could define a model for
* activating touch-based dragging of elements (programatically and/or with
* CSS attributes) and use it here (even have a JS library to generate such
* events when the browser doesn't support them).
*/
// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome)
var Grabber = (function() {
'use strict';
/**
* Create a Grabber object to enable grabbing and dragging a given element.
* @constructor
* @param {!Element} element The element that can be grabbed and moved.
*/
function Grabber(element) {
/**
* The element the grabber is attached to.
* @type {!Element}
* @private
*/
this.element_ = element;
/**
* The TouchHandler responsible for firing lower-level touch events when the
* element is manipulated.
* @type {!TouchHandler}
* @private
*/
this.touchHandler_ = new TouchHandler(this.element);
/**
* Tracks all event listeners we have created.
* @type {EventTracker}
* @private
*/
this.events_ = new EventTracker();
// Enable the generation of events when the element is touched (but no need
// to use the early capture phase of event processing).
this.touchHandler_.enable(/* opt_capture */ false);
// Prevent any built-in drag-and-drop support from activating for the
// element. Note that we don't want details of how we're implementing
// dragging here to leak out of this file (eg. we may switch to using webkit
// drag-and-drop).
this.events_.add(this.element, 'dragstart', function(e) {
e.preventDefault();
}, true);
// Add our TouchHandler event listeners
this.events_.add(this.element, TouchHandler.EventType.TOUCH_START,
this.onTouchStart_.bind(this), false);
this.events_.add(this.element, TouchHandler.EventType.LONG_PRESS,
this.onLongPress_.bind(this), false);
this.events_.add(this.element, TouchHandler.EventType.DRAG_START,
this.onDragStart_.bind(this), false);
this.events_.add(this.element, TouchHandler.EventType.DRAG_MOVE,
this.onDragMove_.bind(this), false);
this.events_.add(this.element, TouchHandler.EventType.DRAG_END,
this.onDragEnd_.bind(this), false);
this.events_.add(this.element, TouchHandler.EventType.TOUCH_END,
this.onTouchEnd_.bind(this), false);
}
/**
* Events fired by the grabber.
* Events are fired at the element affected (not the element being dragged).
* @enum {string}
*/
Grabber.EventType = {
// Fired at the grabber element when it is first grabbed
GRAB: 'grabber:grab',
// Fired at the grabber element when dragging begins (after GRAB)
DRAG_START: 'grabber:dragstart',
// Fired at an element when something is dragged over top of it.
DRAG_ENTER: 'grabber:dragenter',
// Fired at an element when something is no longer over top of it.
// Not fired at all in the case of a DROP
DRAG_LEAVE: 'grabber:drag',
// Fired at an element when something is dropped on top of it.
DROP: 'grabber:drop',
// Fired at the grabber element when dragging ends (successfully or not) -
// after any DROP or DRAG_LEAVE
DRAG_END: 'grabber:dragend',
// Fired at the grabber element when it is released (even if no drag
// occured) - after any DRAG_END event.
RELEASE: 'grabber:release'
};
/**
* The type of Event sent by Grabber
* @constructor
* @param {string} type The type of event (one of Grabber.EventType).
* @param {Element!} grabbedElement The element being dragged.
*/
Grabber.Event = function(type, grabbedElement) {
var event = document.createEvent('Event');
event.initEvent(type, true, true);
event.__proto__ = Grabber.Event.prototype;
/**
* The element which is being dragged. For some events this will be the
* same as 'target', but for events like DROP that are fired at another
* element it will be different.
* @type {!Element}
*/
event.grabbedElement = grabbedElement;
return event;
};
Grabber.Event.prototype = {
__proto__: Event.prototype
};
/**
* The CSS class to apply when an element is touched but not yet
* grabbed.
* @type {string}
*/
Grabber.PRESSED_CLASS = 'grabber-pressed';
/**
* The class to apply when an element has been held (including when it is
* being dragged.
* @type {string}
*/
Grabber.GRAB_CLASS = 'grabber-grabbed';
/**
* The class to apply when a grabbed element is being dragged.
* @type {string}
*/
Grabber.DRAGGING_CLASS = 'grabber-dragging';
Grabber.prototype = {
/**
* @return {!Element} The element that can be grabbed.
*/
get element() {
return this.element_;
},
/**
* Clean up all event handlers (eg. if the underlying element will be
* removed)
*/
dispose: function() {
this.touchHandler_.disable();
this.events_.removeAll();
// Clean-up any active touch/drag
if (this.dragging_)
this.stopDragging_();
this.onTouchEnd_();
},
/**
* Invoked whenever this element is first touched
* @param {!TouchHandler.Event} e The TouchHandler event.
* @private
*/
onTouchStart_: function(e) {
this.element.classList.add(Grabber.PRESSED_CLASS);
// Always permit the touch to perhaps trigger a drag
e.enableDrag = true;
},
/**
* Invoked whenever the element stops being touched.
* Can be called explicitly to cleanup any active touch.
* @param {!TouchHandler.Event=} opt_e The TouchHandler event.
* @private
*/
onTouchEnd_: function(opt_e) {
if (this.grabbed_) {
// Mark this element as no longer being grabbed
this.element.classList.remove(Grabber.GRAB_CLASS);
this.element.style.pointerEvents = '';
this.grabbed_ = false;
this.sendEvent_(Grabber.EventType.RELEASE, this.element);
} else {
this.element.classList.remove(Grabber.PRESSED_CLASS);
}
},
/**
* Handler for TouchHandler's LONG_PRESS event
* Invoked when the element is held (without being dragged)
* @param {!TouchHandler.Event} e The TouchHandler event.
* @private
*/
onLongPress_: function(e) {
assert(!this.grabbed_, 'Got longPress while still being held');
this.element.classList.remove(Grabber.PRESSED_CLASS);
this.element.classList.add(Grabber.GRAB_CLASS);
// Disable mouse events from the element - we care only about what's
// under the element after it's grabbed (since we're getting move events
// from the body - not the element itself). Note that we can't wait until
// onDragStart to do this because it won't have taken effect by the first
// onDragMove.
this.element.style.pointerEvents = 'none';
this.grabbed_ = true;
this.sendEvent_(Grabber.EventType.GRAB, this.element);
},
/**
* Invoked when the element is dragged.
* @param {!TouchHandler.Event} e The TouchHandler event.
* @private
*/
onDragStart_: function(e) {
assert(!this.lastEnter_, 'only expect one drag to occur at a time');
assert(!this.dragging_);
// We only want to drag the element if its been grabbed
if (this.grabbed_) {
// Mark the item as being dragged
// Ensures our translate transform won't be animated and cancels any
// outstanding animations.
this.element.classList.add(Grabber.DRAGGING_CLASS);
// Determine the webkitTransform currently applied to the element.
// Note that it's important that we do this AFTER cancelling animation,
// otherwise we could see an intermediate value.
// We'll assume this value will be constant for the duration of the drag
// so that we can combine it with our translate3d transform.
this.baseTransform_ = this.element.ownerDocument.defaultView.
getComputedStyle(this.element).webkitTransform;
this.sendEvent_(Grabber.EventType.DRAG_START, this.element);
e.enableDrag = true;
this.dragging_ = true;
} else {
// Hasn't been grabbed - don't drag, just unpress
this.element.classList.remove(Grabber.PRESSED_CLASS);
e.enableDrag = false;
}
},
/**
* Invoked when a grabbed element is being dragged
* @param {!TouchHandler.Event} e The TouchHandler event.
* @private
*/
onDragMove_: function(e) {
assert(this.grabbed_ && this.dragging_);
this.translateTo_(e.dragDeltaX, e.dragDeltaY);
var target = e.touchedElement;
if (target && target != this.lastEnter_) {
// Send the events
this.sendDragLeave_(e);
this.sendEvent_(Grabber.EventType.DRAG_ENTER, target);
}
this.lastEnter_ = target;
},
/**
* Send DRAG_LEAVE to the element last sent a DRAG_ENTER if any.
* @param {!TouchHandler.Event} e The event triggering this DRAG_LEAVE.
* @private
*/
sendDragLeave_: function(e) {
if (this.lastEnter_) {
this.sendEvent_(Grabber.EventType.DRAG_LEAVE, this.lastEnter_);
this.lastEnter_ = undefined;
}
},
/**
* Moves the element to the specified position.
* @param {number} x Horizontal position to move to.
* @param {number} y Vertical position to move to.
* @private
*/
translateTo_: function(x, y) {
// Order is important here - we want to translate before doing the zoom
this.element.style.WebkitTransform = 'translate3d(' + x + 'px, ' +
y + 'px, 0) ' + this.baseTransform_;
},
/**
* Invoked when the element is no longer being dragged.
* @param {TouchHandler.Event} e The TouchHandler event.
* @private
*/
onDragEnd_: function(e) {
// We should get this before the onTouchEnd. Don't change
// this.grabbed_ - it's onTouchEnd's responsibility to clear it.
assert(this.grabbed_ && this.dragging_);
var event;
// Send the drop event to the element underneath the one we're dragging.
var target = e.touchedElement;
if (target)
this.sendEvent_(Grabber.EventType.DROP, target);
// Cleanup and send DRAG_END
// Note that like HTML5 DND, we don't send DRAG_LEAVE on drop
this.stopDragging_();
},
/**
* Clean-up the active drag and send DRAG_LEAVE
* @private
*/
stopDragging_: function() {
assert(this.dragging_);
this.lastEnter_ = undefined;
// Mark the element as no longer being dragged
this.element.classList.remove(Grabber.DRAGGING_CLASS);
this.element.style.webkitTransform = '';
this.dragging_ = false;
this.sendEvent_(Grabber.EventType.DRAG_END, this.element);
},
/**
* Send a Grabber event to a specific element
* @param {string} eventType The type of event to send.
* @param {!Element} target The element to send the event to.
* @private
*/
sendEvent_: function(eventType, target) {
var event = new Grabber.Event(eventType, this.element);
target.dispatchEvent(event);
},
/**
* Whether or not the element is currently grabbed.
* @type {boolean}
* @private
*/
grabbed_: false,
/**
* Whether or not the element is currently being dragged.
* @type {boolean}
* @private
*/
dragging_: false,
/**
* The webkitTransform applied to the element when it first started being
* dragged.
* @type {string|undefined}
* @private
*/
baseTransform_: undefined,
/**
* The element for which a DRAG_ENTER event was last fired
* @type {Element|undefined}
* @private
*/
lastEnter_: undefined
};
return Grabber;
})();