diff options
Diffstat (limited to 'chrome/browser/resources/ntp4/grabber.js')
-rw-r--r-- | chrome/browser/resources/ntp4/grabber.js | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/chrome/browser/resources/ntp4/grabber.js b/chrome/browser/resources/ntp4/grabber.js new file mode 100644 index 0000000..ffca317 --- /dev/null +++ b/chrome/browser/resources/ntp4/grabber.js @@ -0,0 +1,383 @@ +// 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; +})(); |