Index: chrome/browser/resources/touch_ntp/grabber.js |
diff --git a/chrome/browser/resources/touch_ntp/grabber.js b/chrome/browser/resources/touch_ntp/grabber.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3cc74b85b2f240c07f0c21f394c56b9c026d9130 |
--- /dev/null |
+++ b/chrome/browser/resources/touch_ntp/grabber.js |
@@ -0,0 +1,387 @@ |
+// 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). |
+ */ |
+ |
+/** |
+ * 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(); |
+ |
+ // TODO: Switch to getter syntax for properties once there is an official |
arv (Not doing code reviews)
2011/03/11 20:08:47
Sad
Rick Byers
2011/03/15 21:47:54
Fixed
|
+ // JSCompiler build that supports the syntax (via --language_in=ECMASCRIPT5) |
+ Object.defineProperty(this, 'element', { |
+ value: element, |
+ writable: false, |
+ enumerable: true |
+ }); |
+ |
arv (Not doing code reviews)
2011/03/11 20:08:47
We can skip this.element_ now and replace all usag
Rick Byers
2011/03/15 21:47:54
Done.
|
+ // |
+ // 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 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 = { |
+ |
Rick Byers
2011/03/11 02:44:33
Darn - reitveld did a poor job of matching this di
|
+ /** |
+ * Clean up all event handlers (eg. if the underlying element will be removed) |
+ * @this {Grabber} |
+ */ |
+ 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 |
+ * @this {Grabber} |
+ * @param {!CustomEvent} 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.detail.enableDrag = true; |
+ }, |
+ |
+ /** |
+ * Invoked whenever the element stops being touched. |
+ * Can be called explicitly to cleanup any active touch. |
+ * @this {Grabber} |
+ * @param {!CustomEvent=} 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.grabbed_ = false; |
+ |
+ this.dispatchEvent_(Grabber.EventType.RELEASE); |
+ } |
+ else |
+ { |
arv (Not doing code reviews)
2011/03/11 20:08:47
} else {
Rick Byers
2011/03/15 21:47:54
Done.
|
+ this.element_.classList.remove(Grabber.PRESSED_CLASS); |
+ } |
+ }, |
+ |
+ /** |
+ * Handler for TouchHandler's LONG_PRESS event |
+ * Invoked when the element is held (without being dragged) |
+ * @this {Grabber} |
+ * @param {!CustomEvent} 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); |
+ this.grabbed_ = true; |
+ |
+ this.dispatchEvent_(Grabber.EventType.GRAB); |
+ }, |
+ |
+ /** |
+ * Invoked when the element is dragged. |
+ * @this {Grabber} |
+ * @param {!CustomEvent} 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.dispatchEvent_(Grabber.EventType.DRAG_START); |
+ e.detail.enableDrag = true; |
+ this.dragging_ = true; |
+ } else { |
+ // Hasn't been grabbed - don't drag, just unpress |
+ this.element_.classList.remove(Grabber.PRESSED_CLASS); |
+ e.detail.enableDrag = false; |
+ } |
+ }, |
+ |
+ /** |
+ * Invoked when a grabbed element is being dragged |
+ * @this {Grabber} |
+ * @param {!CustomEvent} e The TouchHandler event. |
+ * @private |
+ */ |
+ onDragMove_: function(e) { |
+ assert(this.grabbed_ && this.dragging_); |
+ |
+ this.translateTo_(e.detail.dragDeltaX, e.detail.dragDeltaY); |
+ |
+ var target = this.getCoveredElement_(e.detail); |
+ if (target && target != this.lastEnter_) { |
+ // Send the events |
+ this.sendDragLeave_(e); |
+ this.dispatchEventTo_(Grabber.EventType.DRAG_ENTER, target); |
+ } |
+ this.lastEnter_ = target; |
+ }, |
+ |
+ /** |
+ * Send DRAG_LEAVE to the element last sent a DRAG_ENTER if any. |
+ * @this {Grabber} |
+ * @param {!CustomEvent} e The event triggering this DRAG_LEAVE. |
+ * @private |
+ */ |
+ sendDragLeave_: function(e) { |
+ if (this.lastEnter_) { |
+ this.dispatchEventTo_(Grabber.EventType.DRAG_LEAVE, this.lastEnter_); |
+ this.lastEnter_ = undefined; |
+ } |
+ }, |
+ |
+ /** |
+ * Moves the element to the specified position. |
+ * @this {Grabber} |
+ * @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, ' + |
arv (Not doing code reviews)
2011/03/11 20:08:47
WebkitTransform for consistency with the name conv
Rick Byers
2011/03/15 21:47:54
Done.
|
+ y + 'px, 0) ' + this.baseTransform_; |
+ }, |
+ |
+ /** |
+ * Get the element being covered by a given touch. |
+ * @this {Grabber} |
+ * @param {TouchHandler.EventDetail} touch The details of the touch event |
+ * indicating the position to check. |
+ * @return {Element} The element under the touch or null. |
+ * @private |
+ */ |
+ getCoveredElement_: function(touch) { |
+ // Ensure the element being dragged doesn't get in the way |
+ // It's unfortunate that there's not a better way to do this (such as a |
+ // 'elementsFromPoint' API that returns the elements in stack order). |
+ // Relying on pointerEvents (instead of display or zIndex for example) |
+ // should ensure that we don't trigger a relayout. |
arv (Not doing code reviews)
2011/03/11 20:08:47
Actually, why don't you set "pointer-events: none"
Rick Byers
2011/03/15 21:47:54
Nice, I like it. It complicates my TouchHandler a
|
+ var origEvents = this.element_.style.pointerEvents || ''; |
+ this.element_.style.pointerEvents = 'none'; |
+ var target = document.elementFromPoint(touch.clientX, touch.clientY); |
+ this.element_.style.pointerEvents = origEvents; |
+ |
+ return target; |
+ }, |
+ |
+ /** |
+ * Invoked when the element is no longer being dragged. |
+ * @this {Grabber} |
+ * @param {CustomEvent} 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; |
+ |
+ // Try to determine what element is underneath us |
+ var target = this.getCoveredElement_(e.detail); |
+ if (target) |
+ this.dispatchEventTo_(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 |
+ * @this {Grabber} |
+ * @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.dispatchEvent_(Grabber.EventType.DRAG_END); |
+ }, |
+ |
+ /** |
+ * Send a Grabber event to a specific element |
+ * @this {Grabber} |
+ * @param {string} eventType The type of event to send. |
+ * @param {!Element} target The element to send the event to. |
+ * @private |
+ */ |
+ dispatchEventTo_: function(eventType, target) { |
+ var event = document.createEvent('Event'); |
+ event.initEvent(eventType, true, true); |
+ event.sender = this; |
+ target.dispatchEvent(event); |
+ }, |
+ |
+ /** |
+ * Send a Grabber event to the grabbed element |
+ * @this {Grabber} |
+ * @param {string} eventType The type of event to send. |
+ * @private |
+ */ |
+ dispatchEvent_: function(eventType) { |
+ this.dispatchEventTo_(eventType, this.element_); |
+ }, |
+ |
+ /** |
+ * 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 |
+}; |
+ |