summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorestade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-03-19 00:49:24 +0000
committerestade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-03-19 00:49:24 +0000
commitd5dcfb3c3346d6f65b6265ff3c0a11e968c84e16 (patch)
treeda084e13915d4387df88654ca4fd33fe05e9c8a9
parent5d3b29bf92fd075a51106e6b2da7a574ecae2f45 (diff)
downloadchromium_src-d5dcfb3c3346d6f65b6265ff3c0a11e968c84e16.zip
chromium_src-d5dcfb3c3346d6f65b6265ff3c0a11e968c84e16.tar.gz
chromium_src-d5dcfb3c3346d6f65b6265ff3c0a11e968c84e16.tar.bz2
Add ntp4 flag.
Code is copied from touchntp, minus standalone hack. BUG=76706 TEST=manual Review URL: http://codereview.chromium.org/6708032 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@78783 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/app/generated_resources.grd6
-rw-r--r--chrome/browser/about_flags.cc7
-rw-r--r--chrome/browser/browser_resources.grd3
-rw-r--r--chrome/browser/resources/ntp4/card_slider.js336
-rw-r--r--chrome/browser/resources/ntp4/event_tracker.js99
-rw-r--r--chrome/browser/resources/ntp4/grabber.js383
-rw-r--r--chrome/browser/resources/ntp4/new_tab.css217
-rw-r--r--chrome/browser/resources/ntp4/new_tab.html56
-rw-r--r--chrome/browser/resources/ntp4/new_tab.js799
-rwxr-xr-xchrome/browser/resources/ntp4/tools/check.sh57
-rw-r--r--chrome/browser/resources/ntp4/tools/externs.js40
-rw-r--r--chrome/browser/resources/ntp4/touch_handler.js850
-rw-r--r--chrome/browser/resources/shared/images/trash-open.png (renamed from chrome/browser/resources/touch_ntp/trash-open.png)bin2886 -> 2886 bytes
-rw-r--r--chrome/browser/resources/shared/images/trash.png (renamed from chrome/browser/resources/touch_ntp/trash.png)bin2330 -> 2330 bytes
-rw-r--r--chrome/browser/resources/touch_ntp/newtab.css4
-rw-r--r--chrome/browser/ui/webui/ntp_resource_cache.cc11
-rw-r--r--chrome/common/chrome_switches.cc3
-rw-r--r--chrome/common/chrome_switches.h1
18 files changed, 2866 insertions, 6 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 1cad060..3430c44 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -4179,6 +4179,12 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_FLAGS_ENABLE_HISTORY_QUICK_PROVIDER_DESCRIPTION" desc="Description of the 'Enable better visit history matching in the omnibox' lab.">
Enables substring and multi-fragment matching within URLs from history.
</message>
+ <message name="IDS_FLAGS_NEW_TAB_PAGE_4_NAME" desc="Name of the new tab page 4 lab.">
+ Experimental new tab page
+ </message>
+ <message name="IDS_FLAGS_NEW_TAB_PAGE_4_DESCRIPTION" desc="Description of the new tab page 4 lab.">
+ Enables an in-development redesign of the new tab page.
+ </message>
<!-- Crashes -->
<message name="IDS_CRASHES_TITLE" desc="Title for the chrome://crashes page.">
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 516b7cf..fc8f0d8 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -288,6 +288,13 @@ const Experiment kExperiments[] = {
kOsAll,
SINGLE_VALUE_TYPE(switches::kFocusExistingTabOnOpen)
},
+ {
+ "new-tab-page-4",
+ IDS_FLAGS_NEW_TAB_PAGE_4_NAME,
+ IDS_FLAGS_NEW_TAB_PAGE_4_DESCRIPTION,
+ kOsAll,
+ SINGLE_VALUE_TYPE(switches::kNewTabPage4)
+ },
};
const Experiment* experiments = kExperiments;
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index e66a3bf..9e95704 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -56,7 +56,8 @@ without changes to the corresponding grd file. etaa -->
<include name="IDR_NEW_TAB_HTML" file="resources\touch_ntp\newtab.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_NEW_TAB_THEME_CSS" file="resources\touch_ntp\newtab.css" flattenhtml="true" type="BINDATA" />
</if>
- <include name="IDR_TOUCH_NEW_TAB_HTML" file="resources\touch_ntp\newtab.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_NEW_TAB_4_HTML" file="resources\ntp4\new_tab.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_NEW_TAB_4_THEME_CSS" file="resources\ntp4\new_tab.css" flattenhtml="true" type="BINDATA" />
<include name="IDR_NOTIFICATION_1LINE_HTML" file="resources\notification_1line.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_NOTIFICATION_2LINE_HTML" file="resources\notification_2line.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_NOTIFICATION_ICON_HTML" file="resources\notification_icon.html" type="BINDATA" />
diff --git a/chrome/browser/resources/ntp4/card_slider.js b/chrome/browser/resources/ntp4/card_slider.js
new file mode 100644
index 0000000..b0779ed
--- /dev/null
+++ b/chrome/browser/resources/ntp4/card_slider.js
@@ -0,0 +1,336 @@
+// 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 Card slider implementation. Allows you to create interactions
+ * that have items that can slide left to right to reveal additional items.
+ * Works by adding the necessary event handlers to a specific DOM structure
+ * including a frame, container and cards.
+ * - The frame defines the boundary of one item. Each card will be expanded to
+ * fill the width of the frame. This element is also overflow hidden so that
+ * the additional items left / right do not trigger horizontal scrolling.
+ * - The container is what all the touch events are attached to. This element
+ * will be expanded to be the width of all cards.
+ * - The cards are the individual viewable items. There should be one card for
+ * each item in the list. Only one card will be visible at a time. Two cards
+ * will be visible while you are transitioning between cards.
+ *
+ * This class is designed to work well on any hardware-accelerated touch device.
+ * It should still work on pre-hardware accelerated devices it just won't feel
+ * very good. It should also work well with a mouse.
+ */
+
+
+// Use an anonymous function to enable strict mode just for this file (which
+// will be concatenated with other files when embedded in Chrome
+var CardSlider = (function() {
+ 'use strict';
+
+ /**
+ * @constructor
+ * @param {!Element} frame The bounding rectangle that cards are visible in.
+ * @param {!Element} container The surrounding element that will have event
+ * listeners attached to it.
+ * @param {!Array.<!Element>} cards The individual viewable cards.
+ * @param {number} currentCard The index of the card that is currently
+ * visible.
+ * @param {number} cardWidth The width of each card should have.
+ */
+ function CardSlider(frame, container, cards, currentCard, cardWidth) {
+ /**
+ * @type {!Element}
+ * @private
+ */
+ this.frame_ = frame;
+
+ /**
+ * @type {!Element}
+ * @private
+ */
+ this.container_ = container;
+
+ /**
+ * @type {!Array.<!Element>}
+ * @private
+ */
+ this.cards_ = cards;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ this.currentCard_ = currentCard;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ this.cardWidth_ = cardWidth;
+
+ /**
+ * @type {!TouchHandler}
+ * @private
+ */
+ this.touchHandler_ = new TouchHandler(this.container_);
+ }
+
+
+ /**
+ * Events fired by the slider.
+ * Events are fired at the container.
+ */
+ CardSlider.EventType = {
+ // Fired when the user slides to another card.
+ CARD_CHANGED: 'cardSlider:card_changed'
+ };
+
+
+ /**
+ * The time to transition between cards when animating. Measured in ms.
+ * @type {number}
+ * @private
+ * @const
+ */
+ CardSlider.TRANSITION_TIME_ = 200;
+
+
+ /**
+ * The minimum velocity required to transition cards if they did not drag past
+ * the halfway point between cards. Measured in pixels / ms.
+ * @type {number}
+ * @private
+ * @const
+ */
+ CardSlider.TRANSITION_VELOCITY_THRESHOLD_ = 0.2;
+
+
+ CardSlider.prototype = {
+ /**
+ * The current left offset of the container relative to the frame.
+ * @type {number}
+ * @private
+ */
+ currentLeft_: 0,
+
+ /**
+ * Initialize all elements and event handlers. Must call after construction
+ * and before usage.
+ */
+ initialize: function() {
+ var view = this.container_.ownerDocument.defaultView;
+ assert(view.getComputedStyle(this.container_).display == '-webkit-box',
+ 'Container should be display -webkit-box.');
+ assert(view.getComputedStyle(this.frame_).overflow == 'hidden',
+ 'Frame should be overflow hidden.');
+ assert(view.getComputedStyle(this.container_).position == 'static',
+ 'Container should be position static.');
+ for (var i = 0, card; card = this.cards_[i]; i++) {
+ assert(view.getComputedStyle(card).position == 'static',
+ 'Cards should be position static.');
+ }
+
+ this.updateCardWidths_();
+ this.transformToCurrentCard_();
+
+ this.container_.addEventListener(TouchHandler.EventType.TOUCH_START,
+ this.onTouchStart_.bind(this));
+ this.container_.addEventListener(TouchHandler.EventType.DRAG_START,
+ this.onDragStart_.bind(this));
+ this.container_.addEventListener(TouchHandler.EventType.DRAG_MOVE,
+ this.onDragMove_.bind(this));
+ this.container_.addEventListener(TouchHandler.EventType.DRAG_END,
+ this.onDragEnd_.bind(this));
+
+ this.touchHandler_.enable(/* opt_capture */ false);
+ },
+
+ /**
+ * Use in cases where the width of the frame has changed in order to update
+ * the width of cards. For example should be used when orientation changes
+ * in full width sliders.
+ * @param {number} newCardWidth Width all cards should have, in pixels.
+ */
+ resize: function(newCardWidth) {
+ if (newCardWidth != this.cardWidth_) {
+ this.cardWidth_ = newCardWidth;
+
+ this.updateCardWidths_();
+
+ // Must upate the transform on the container to show the correct card.
+ this.transformToCurrentCard_();
+ }
+ },
+
+ /**
+ * Sets the cards used. Can be called more than once to switch card sets.
+ * @param {!Array.<!Element>} cards The individual viewable cards.
+ * @param {number} index Index of the card to in the new set of cards to
+ * navigate to.
+ */
+ setCards: function(cards, index) {
+ assert(index >= 0 && index < cards.length,
+ 'Invalid index in CardSlider#setCards');
+ this.cards_ = cards;
+
+ this.updateCardWidths_();
+
+ // Jump to the given card index.
+ this.selectCard(index);
+ },
+
+ /**
+ * Updates the width of each card.
+ * @private
+ */
+ updateCardWidths_: function() {
+ for (var i = 0, card; card = this.cards_[i]; i++)
+ card.style.width = this.cardWidth_ + 'px';
+ },
+
+ /**
+ * Returns the index of the current card.
+ * @return {number} index of the current card.
+ */
+ get currentCard() {
+ return this.currentCard_;
+ },
+
+ /**
+ * Clear any transition that is in progress and enable dragging for the
+ * touch.
+ * @param {!TouchHandler.Event} e The TouchHandler event.
+ * @private
+ */
+ onTouchStart_: function(e) {
+ this.container_.style.WebkitTransition = '';
+ e.enableDrag = true;
+ },
+
+
+ /**
+ * Tell the TouchHandler that dragging is acceptable when the user begins by
+ * scrolling horizontally.
+ * @param {!TouchHandler.Event} e The TouchHandler event.
+ * @private
+ */
+ onDragStart_: function(e) {
+ e.enableDrag = Math.abs(e.dragDeltaX) > Math.abs(e.dragDeltaY);
+ },
+
+ /**
+ * On each drag move event reposition the container appropriately so the
+ * cards look like they are sliding.
+ * @param {!TouchHandler.Event} e The TouchHandler event.
+ * @private
+ */
+ onDragMove_: function(e) {
+ var deltaX = e.dragDeltaX;
+ // If dragging beyond the first or last card then apply a backoff so the
+ // dragging feels stickier than usual.
+ if (!this.currentCard && deltaX > 0 ||
+ this.currentCard == (this.cards_.length - 1) && deltaX < 0) {
+ deltaX /= 2;
+ }
+ this.translateTo_(this.currentLeft_ + deltaX);
+ },
+
+ /**
+ * Moves the view to the specified position.
+ * @param {number} x Horizontal position to move to.
+ * @private
+ */
+ translateTo_: function(x) {
+ // We use a webkitTransform to slide because this is GPU accelerated on
+ // Chrome and iOS. Once Chrome does GPU acceleration on the position
+ // fixed-layout elements we could simply set the element's position to
+ // fixed and modify 'left' instead.
+ this.container_.style.WebkitTransform = 'translate3d(' + x + 'px, 0, 0)';
+ },
+
+ /**
+ * On drag end events we may want to transition to another card, depending
+ * on the ending position of the drag and the velocity of the drag.
+ * @param {!TouchHandler.Event} e The TouchHandler event.
+ * @private
+ */
+ onDragEnd_: function(e) {
+ var deltaX = e.dragDeltaX;
+ var velocity = this.touchHandler_.getEndVelocity().x;
+ var newX = this.currentLeft_ + deltaX;
+ var newCardIndex = Math.round(-newX / this.cardWidth_);
+
+ if (newCardIndex == this.currentCard && Math.abs(velocity) >
+ CardSlider.TRANSITION_VELOCITY_THRESHOLD_) {
+ // If the drag wasn't far enough to change cards but the velocity was
+ // high enough to transition anyways. If the velocity is to the left
+ // (negative) then the user wishes to go right (card +1).
+ newCardIndex += velocity > 0 ? -1 : 1;
+ }
+
+ this.selectCard(newCardIndex, /* animate */ true);
+ },
+
+ /**
+ * Cancel any current touch/slide as if we saw a touch end
+ */
+ cancelTouch: function() {
+ // Stop listening to any current touch
+ this.touchHandler_.cancelTouch();
+
+ // Ensure we're at a card bounary
+ this.transformToCurrentCard_(true);
+ },
+
+ /**
+ * Selects a new card, ensuring that it is a valid index, transforming the
+ * view and possibly calling the change card callback.
+ * @param {number} newCardIndex Index of card to show.
+ * @param {boolean=} opt_animate If true will animate transition from
+ * current position to new position.
+ */
+ selectCard: function(newCardIndex, opt_animate) {
+ var isChangingCard = newCardIndex >= 0 &&
+ newCardIndex < this.cards_.length &&
+ newCardIndex != this.currentCard;
+ if (isChangingCard) {
+ // If we have a new card index and it is valid then update the left
+ // position and current card index.
+ this.currentCard_ = newCardIndex;
+ }
+
+ this.transformToCurrentCard_(opt_animate);
+
+ if (isChangingCard) {
+ var event = document.createEvent('Event');
+ event.initEvent(CardSlider.EventType.CARD_CHANGED, true, true);
+ event.cardSlider = this;
+ this.container_.dispatchEvent(event);
+ }
+ },
+
+ /**
+ * Centers the view on the card denoted by this.currentCard. Can either
+ * animate to that card or snap to it.
+ * @param {boolean=} opt_animate If true will animate transition from
+ * current position to new position.
+ * @private
+ */
+ transformToCurrentCard_: function(opt_animate) {
+ this.currentLeft_ = -this.currentCard * this.cardWidth_;
+
+ // Animate to the current card, which will either transition if the
+ // current card is new, or reset the existing card if we didn't drag
+ // enough to change cards.
+ var transition = '';
+ if (opt_animate) {
+ transition = '-webkit-transform ' + CardSlider.TRANSITION_TIME_ +
+ 'ms ease-in-out';
+ }
+ this.container_.style.WebkitTransition = transition;
+ this.translateTo_(this.currentLeft_);
+ }
+ };
+
+ return CardSlider;
+})();
diff --git a/chrome/browser/resources/ntp4/event_tracker.js b/chrome/browser/resources/ntp4/event_tracker.js
new file mode 100644
index 0000000..bb76f5e
--- /dev/null
+++ b/chrome/browser/resources/ntp4/event_tracker.js
@@ -0,0 +1,99 @@
+// 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 EventTracker is a simple class that manages the addition and
+ * removal of DOM event listeners. In particular, it keeps track of all
+ * listeners that have been added and makes it easy to remove some or all of
+ * them without requiring all the information again. This is particularly
+ * handy when the listener is a generated function such as a lambda or the
+ * result of calling Function.bind. The goal of this class is to make it
+ * easier to avoid memory leaks caused by DOM<->JS cycles - removing event
+ * listeners breakes the DOM->JS part of the cycle.
+ */
+
+// Use an anonymous function to enable strict mode just for this file (which
+// will be concatenated with other files when embedded in Chrome)
+var EventTracker = (function() {
+ 'use strict';
+
+ /**
+ * Create an EventTracker to track a set of events.
+ * EventTracker instances are typically tied 1:1 with other objects or
+ * DOM elements whose listeners should be removed when the object is disposed
+ * or the corresponding elements are removed from the DOM.
+ * @constructor
+ */
+ function EventTracker() {
+ /**
+ * @type {Array.<EventTracker.Entry>}
+ * @private
+ */
+ this.listeners_ = [];
+ }
+
+ /**
+ * The type of the internal tracking entry.
+ * @typedef {{node: !Node,
+ * eventType: string,
+ * listener: Function,
+ * capture: boolean}}
+ */
+ EventTracker.Entry;
+
+ EventTracker.prototype = {
+ /**
+ * Add an event listener - replacement for Node.addEventListener.
+ * @param {!Node} node The DOM node to add a listener to.
+ * @param {string} eventType The type of event to subscribe to.
+ * @param {Function} listener The listener to add.
+ * @param {boolean} capture Whether to invoke during the capture phase.
+ */
+ add: function(node, eventType, listener, capture) {
+ var h = {
+ node: node,
+ eventType: eventType,
+ listener: listener,
+ capture: capture
+ };
+ this.listeners_.push(h);
+ node.addEventListener(eventType, listener, capture);
+ },
+
+ /**
+ * Remove any specified event listeners added with this EventTracker.
+ * @param {!Node} node The DOM node to remove a listener from.
+ * @param {string} eventType The type of event to remove.
+ */
+ remove: function(node, eventType) {
+ this.listeners_ = this.listeners_.filter(function(h) {
+ if (h.node == node && h.eventType == eventType) {
+ EventTracker.removeEventListener_(h);
+ return false;
+ }
+ return true;
+ });
+ },
+
+ /**
+ * Remove all event listeners added with this EventTracker.
+ */
+ removeAll: function() {
+ this.listeners_.forEach(EventTracker.removeEventListener_);
+ this.listeners_ = [];
+ }
+ };
+
+ /**
+ * Remove a single event listener given it's tracker entry. It's up to the
+ * caller to ensure the entry is removed from listeners_.
+ * @param {EventTracker.Entry} h The entry describing the listener to remove.
+ * @private
+ */
+ EventTracker.removeEventListener_ = function(h) {
+ h.node.removeEventListener(h.eventType, h.listener, h.capture);
+ };
+
+ return EventTracker;
+})();
+
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;
+})();
diff --git a/chrome/browser/resources/ntp4/new_tab.css b/chrome/browser/resources/ntp4/new_tab.css
new file mode 100644
index 0000000..079c4e1
--- /dev/null
+++ b/chrome/browser/resources/ntp4/new_tab.css
@@ -0,0 +1,217 @@
+/* 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.
+ *
+ * This is the stylesheet used by the touch-enabled new tab page
+ */
+
+html {
+ /* It's necessary to put this here instead of in body in order to get the
+ background-size of 100% to work properly */
+ height: 100%;
+}
+
+body {
+ /* This newer linear-gradient form works on Chrome but not mobile Safari */
+ /*background: -webkit-linear-gradient(top,#252c39,#3e485b,#252c39); */
+ background: -webkit-gradient(linear, left top, left bottom, from(#252c39),
+ color-stop(0.5,#3e485b), to(#252c39));
+ background-size: auto 100%;
+ font-family: segoe ui, arial, helvetica, sans-serif;
+ font-size: 14px;
+ color: white;
+ margin: 0;
+ /* Don't highlight links when they're tapped. Safari has bugs here that
+ show up as flicker when dragging in some situations */
+ -webkit-tap-highlight-color: transparent;
+ /* Don't allow selecting text - can occur when dragging */
+ -webkit-user-select: none;
+}
+
+/* The frame is what the slider fits into
+ */
+#apps-frame {
+ /* We want this to fill the window except for the region used
+ by footer
+ */
+ position: fixed;
+ width: 100%;
+ top: 0;
+ bottom: 60px; /* must match #footer height */
+ overflow: hidden;
+}
+
+/* The list holds all the pages and is what the touch events are attached to
+*/
+#apps-page-list {
+ /* fill the apps-frame */
+ height: 100%;
+ display: -webkit-box;
+}
+
+/* The apps-page is the slider card that is moved.
+ */
+.apps-page {
+ -webkit-box-sizing: border-box;
+ padding: 29px;
+ /* TODO(rbyers): Don't want the final row centered, but would like all rows
+ * centered. Really I want the page-content width determined by the boxes
+ * inside of it, but perhaps webkit-box doesn't support that.
+ * Note that instead of display:inline-block for the apps, I could use
+ * float:left and have a .app-container:first-child { clear:both; } rule,
+ * but I'd have to figure out some way to get the vertical position reset.
+ text-align:center; */
+}
+
+.app-container {
+ width: 128px;
+ height: 128px;
+ padding: 16px;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.app {
+ text-align: center;
+ width: 128px;
+ height: 128px;
+ /* Animate effects to/from the grabbed state, and the position when drop
+ is cancelled. I'd like to also animate movement when an app is
+ re-positioned, but since chrome is doing the layout there is no property
+ to animate.
+ TODO(rbyers): Should we take over responsibility for app layout ourself
+ like the classic NTP's most-visited icons? Or should we extend webkit
+ somehow to support animation of the position of browser laid-out
+ elements. */
+ -webkit-transition-property: -webkit-transform, opacity, zIndex;
+ -webkit-transition-duration: 200ms;
+ /* Don't offer the context menu on long-press. */
+ -webkit-touch-callout: none;
+ /* Work-around regression bug 74802 */
+ -webkit-transform: scale3d(1, 1, 1);
+}
+
+.app span {
+ text-decoration: none;
+ /* TODO(rbyers): why isn't ellipsis working? */
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ color: white;
+}
+
+.app img {
+ display: block;
+ width: 96px;
+ height: 96px;
+ margin-left: auto;
+ margin-right: auto;
+ /* -webkit-mask-image set by JavaScript to the image source */
+ -webkit-mask-size: 100% 100%;
+}
+
+/* Pressed is set when an app is first touched.
+ By using the mask, pressing causes a darkening effect of just the image */
+.app.grabber-pressed img {
+ opacity: 0.8;
+}
+
+/* Grabbed is set (and pressed is cleared) when the app has been held. */
+.grabber-grabbed {
+ opacity: 0.8;
+ -webkit-transform: scale3d(1.4, 1.4, 1);
+}
+
+/* Dragging is set (without grabbed being cleared) when a grabbed app is
+ moved */
+.grabber-dragging {
+ /* We need to ensure there is no animation applied to its position
+ (or the drag touch may stop being over top of it */
+ -webkit-transition: none !important;
+
+ /* Ensure the element has a large z-index so that we can get events
+ for it as it moves over other elements. This is animated as the
+ element flys back, so we want a large value that will stay large until
+ its almost home. */
+ z-index: 100;
+}
+
+#footer {
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ height: 60px; /* must match #apps-frame bottom */
+ overflow: hidden;
+}
+
+#dot-list {
+ text-align: center;
+ margin: 0;
+ padding: 0;
+ bottom: 0;
+ list-style-type: none;
+ margin-top: 20px;
+}
+
+.dot {
+ display: inline-block;
+ margin: 10px;
+ width: 10px;
+ height: 10px;
+ background-color: #3d465f;
+ -webkit-box-sizing: border-box;
+ border: 1px solid black;
+ -webkit-border-radius: 2px;
+ -webkit-transition-property: width, height, margin, -webkit-transform;
+ -webkit-transition-duration: 500ms;
+ /* Work-around regression bug 74802 */
+ -webkit-transform: translate3d(0, 0, 0);
+}
+
+#footer.rearrange-mode .dot {
+ margin: 0px 20px;
+ width: 30px;
+ height: 30px;
+}
+
+.dot.selected {
+ background-color: #b3bbd3;
+}
+
+.dot.new {
+ -webkit-transform: translate3d(0, 40px, 0);
+}
+
+#trash {
+ position: absolute;
+ width: 110px;
+ height: 100%;
+ right: 0px;
+ bottom: 0px;
+ background-image: url('../shared/images/trash.png');
+ background-size: 40px 40px;
+ background-repeat: no-repeat;
+ background-position: 40px 12px;
+ /* Work-around chromium bug 74730 by using translate instead of the
+ GPU-accelerated translate3d */
+ -webkit-transform: translate(80px, 0);
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transition-duration: 500ms;
+}
+
+#trash.hover {
+ background-image: url('../shared/images/trash-open.png');
+}
+
+.app.trashing img {
+ opacity: 0.3;
+}
+
+#footer.rearrange-mode #trash {
+ -webkit-transform: translate(0, 0);
+}
+
+/* Ensure template items are never drawn when the page initially loads */
+#app-template {
+ display: none;
+}
diff --git a/chrome/browser/resources/ntp4/new_tab.html b/chrome/browser/resources/ntp4/new_tab.html
new file mode 100644
index 0000000..b4d05c3
--- /dev/null
+++ b/chrome/browser/resources/ntp4/new_tab.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title i18n-content="title"></title>
+ <link rel="stylesheet" href="new_tab.css">
+ <!-- Don't scale the viewport in either portrait or landscape mode.
+ Note that this means apps will be reflowed when rotated (like iPad).
+ If we wanted to maintain position we could remove 'maximum-scale' so
+ that we'd zoom out in portrait mode, but then there would be a bunch
+ of unusable space at the bottom.
+ -->
+ <meta name="viewport" content="user-scalable=no, width=device-width, maximum-scale=1.0">
+</head>
+<body>
+ <div id="apps-frame">
+ <div id="apps-page-list">
+ <div class="apps-page">
+ <!-- This is used as a template which JS copies and fills in. The
+ template itself is never actually displayed.
+ An app-container is always a direct child of an apps-page, and is
+ expected to have a single 'app' child element holding the visible
+ content, and 'img' and 'span' tags as descendants somewhere.
+ -->
+ <div class="app-container" id="app-template">
+ <div class="app">
+ <img>
+ <span>App name</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="footer">
+ <ul id="dot-list">
+ <li class="dot"></li>
+ </ul>
+ <div id="trash">
+ </div>
+ </div>
+ <!-- Substitute localized strings for all elements with il8n-content
+ attributes. -->
+ <script src="../shared/js/i18n_template.js"></script>
+ <!-- template data placeholder -->
+ <script>
+ i18nTemplate.process(document, templateData);
+ </script>
+
+ <script src="../shared/js/util.js"></script>
+ <script src="event_tracker.js"></script>
+ <script src="touch_handler.js"></script>
+ <script src="card_slider.js"></script>
+ <script src="grabber.js"></script>
+ <script src="new_tab.js"></script>
+</body>
+</html>
diff --git a/chrome/browser/resources/ntp4/new_tab.js b/chrome/browser/resources/ntp4/new_tab.js
new file mode 100644
index 0000000..bac18ae
--- /dev/null
+++ b/chrome/browser/resources/ntp4/new_tab.js
@@ -0,0 +1,799 @@
+// 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 Touch-based new tab page
+ * This is the main code for the new tab page used by touch-enabled Chrome
+ * browsers. For now this is still a prototype.
+ */
+
+// Use an anonymous function to enable strict mode just for this file (which
+// will be concatenated with other files when embedded in Chrome
+var ntp = (function() {
+ 'use strict';
+
+ /**
+ * The CardSlider object to use for changing app pages.
+ * @type {CardSlider|undefined}
+ */
+ var cardSlider;
+
+ /**
+ * Template to use for creating new 'apps-page' elements
+ * @type {!Element|undefined}
+ */
+ var appsPageTemplate;
+
+ /**
+ * Template to use for creating new 'app-container' elements
+ * @type {!Element|undefined}
+ */
+ var appTemplate;
+
+ /**
+ * Template to use for creating new 'dot' elements
+ * @type {!Element|undefined}
+ */
+ var dotTemplate;
+
+ /**
+ * The 'apps-page-list' element.
+ * @type {!Element}
+ */
+ var appsPageList = getRequiredElement('apps-page-list');
+
+ /**
+ * A list of all 'apps-page' elements.
+ * @type {!NodeList|undefined}
+ */
+ var appsPages;
+
+ /**
+ * The 'dots-list' element.
+ * @type {!Element}
+ */
+ var dotList = getRequiredElement('dot-list');
+
+ /**
+ * A list of all 'dots' elements.
+ * @type {!NodeList|undefined}
+ */
+ var dots;
+
+ /**
+ * The 'trash' element. Note that technically this is unnecessary,
+ * JavaScript creates the object for us based on the id. But I don't want
+ * to rely on the ID being the same, and JSCompiler doesn't know about it.
+ * @type {!Element}
+ */
+ var trash = getRequiredElement('trash');
+
+ /**
+ * The time in milliseconds for most transitions. This should match what's
+ * in new_tab.css. Unfortunately there's no better way to try to time
+ * something to occur until after a transition has completed.
+ * @type {number}
+ * @const
+ */
+ var DEFAULT_TRANSITION_TIME = 500;
+
+ /**
+ * All the Grabber objects currently in use on the page
+ * @type {Array.<Grabber>}
+ */
+ var grabbers = [];
+
+ /**
+ * Holds all event handlers tied to apps (and so subject to removal when the
+ * app list is refreshed)
+ * @type {!EventTracker}
+ */
+ var appEvents = new EventTracker();
+
+ /**
+ * Invoked at startup once the DOM is available to initialize the app.
+ */
+ function initializeNtp() {
+ // Request data on the apps so we can fill them in.
+ // Note that this is kicked off asynchronously. 'getAppsCallback' will be
+ // invoked at some point after this function returns.
+ chrome.send('getApps');
+
+ // Prevent touch events from triggering any sort of native scrolling
+ document.addEventListener('touchmove', function(e) {
+ e.preventDefault();
+ }, true);
+
+ // Get the template elements and remove them from the DOM. Things are
+ // simpler if we start with 0 pages and 0 apps and don't leave hidden
+ // template elements behind in the DOM.
+ appTemplate = getRequiredElement('app-template');
+ appTemplate.id = null;
+
+ appsPages = appsPageList.getElementsByClassName('apps-page');
+ assert(appsPages.length == 1,
+ 'Expected exactly one apps-page in the apps-page-list.');
+ appsPageTemplate = appsPages[0];
+ appsPageList.removeChild(appsPages[0]);
+
+ dots = dotList.getElementsByClassName('dot');
+ assert(dots.length == 1,
+ 'Expected exactly one dot in the dots-list.');
+ dotTemplate = dots[0];
+ dotList.removeChild(dots[0]);
+
+ // Initialize the cardSlider without any cards at the moment
+ var appsFrame = getRequiredElement('apps-frame');
+ cardSlider = new CardSlider(appsFrame, appsPageList, [], 0,
+ appsFrame.offsetWidth);
+ cardSlider.initialize();
+
+ // Ensure the slider is resized appropriately with the window
+ window.addEventListener('resize', function() {
+ cardSlider.resize(appsFrame.offsetWidth);
+ });
+
+ // Handle the page being changed
+ appsPageList.addEventListener(
+ CardSlider.EventType.CARD_CHANGED,
+ function(e) {
+ // Update the active dot
+ var curDot = dotList.getElementsByClassName('selected')[0];
+ if (curDot)
+ curDot.classList.remove('selected');
+ var newPageIndex = e.cardSlider.currentCard;
+ dots[newPageIndex].classList.add('selected');
+ // If an app was being dragged, move it to the end of the new page
+ if (draggingAppContainer)
+ appsPages[newPageIndex].appendChild(draggingAppContainer);
+ });
+
+ // Add a drag handler to the body (for drags that don't land on an existing
+ // app)
+ document.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter);
+
+ // Handle dropping an app anywhere other than on the trash
+ document.addEventListener(Grabber.EventType.DROP, appDrop);
+
+ // Add handles to manage the transition into/out-of rearrange mode
+ // Note that we assume here that we only use a Grabber for moving apps,
+ // so ANY GRAB event means we're enterring rearrange mode.
+ appsFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode);
+ appsFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode);
+
+ // Add handlers for the tash can
+ trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) {
+ trash.classList.add('hover');
+ e.grabbedElement.classList.add('trashing');
+ e.stopPropagation();
+ });
+ trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) {
+ e.grabbedElement.classList.remove('trashing');
+ trash.classList.remove('hover');
+ });
+ trash.addEventListener(Grabber.EventType.DROP, appTrash);
+ }
+
+ /**
+ * Simple common assertion API
+ * @param {*} condition The condition to test. Note that this may be used to
+ * test whether a value is defined or not, and we don't want to force a
+ * cast to Boolean.
+ * @param {string=} opt_message A message to use in any error.
+ */
+ function assert(condition, opt_message) {
+ 'use strict';
+ if (!condition) {
+ var msg = 'Assertion failed';
+ if (opt_message)
+ msg = msg + ': ' + opt_message;
+ throw new Error(msg);
+ }
+ }
+
+ /**
+ * Get an element that's known to exist by its ID. We use this instead of just
+ * calling getElementById and not checking the result because this lets us
+ * satisfy the JSCompiler type system.
+ * @param {string} id The identifier name.
+ * @return {!Element} the Element.
+ */
+ function getRequiredElement(id) {
+ var element = document.getElementById(id);
+ assert(element, 'Missing required element: ' + id);
+ return element;
+ }
+
+ /**
+ * Remove all children of an element which have a given class in
+ * their classList.
+ * @param {!Element} element The parent element to examine.
+ * @param {string} className The class to look for.
+ */
+ function removeChildrenByClassName(element, className) {
+ for (var child = element.firstElementChild; child;) {
+ var prev = child;
+ child = child.nextElementSibling;
+ if (prev.classList.contains(className))
+ element.removeChild(prev);
+ }
+ }
+
+ /**
+ * Callback invoked by chrome with the apps available.
+ *
+ * Note that calls to this function can occur at any time, not just in
+ * response to a getApps request. For example, when a user installs/uninstalls
+ * an app on another synchronized devices.
+ * @param {Object} data An object with all the data on available
+ * applications.
+ */
+ function getAppsCallback(data)
+ {
+ // Clean up any existing grabber objects - cancelling any outstanding drag.
+ // Ideally an async app update wouldn't disrupt an active drag but
+ // that would require us to re-use existing elements and detect how the apps
+ // have changed, which would be a lot of work.
+ // Note that we have to explicitly clean up the grabber objects so they stop
+ // listening to events and break the DOM<->JS cycles necessary to enable
+ // collection of all these objects.
+ grabbers.forEach(function(g) {
+ // Note that this may raise DRAG_END/RELEASE events to clean up an
+ // oustanding drag.
+ g.dispose();
+ });
+ assert(!draggingAppContainer && !draggingAppOriginalPosition &&
+ !draggingAppOriginalPage);
+ grabbers = [];
+ appEvents.removeAll();
+
+ // Clear any existing apps pages and dots.
+ // TODO(rbyers): It might be nice to preserve animation of dots after an
+ // uninstall. Could we re-use the existing page and dot elements? It seems
+ // unfortunate to have Chrome send us the entire apps list after an
+ // uninstall.
+ removeChildrenByClassName(appsPageList, 'apps-page');
+ removeChildrenByClassName(dotList, 'dot');
+
+ // Get the array of apps and add any special synthesized entries
+ var apps = data.apps;
+ apps.push(makeWebstoreApp());
+
+ // Sort by launch index
+ apps.sort(function(a, b) {
+ return a.app_launch_index - b.app_launch_index;
+ });
+
+ // Add the apps, creating pages as necessary
+ for (var i = 0; i < apps.length; i++) {
+ var app = apps[i];
+ var pageIndex = (app.page_index || 0);
+ while (pageIndex >= appsPages.length) {
+ var origPageCount = appsPages.length;
+ createAppPage();
+ // Confirm that appsPages is a live object, updated when a new page is
+ // added (otherwise we'd have an infinite loop)
+ assert(appsPages.length == origPageCount + 1, 'expected new page');
+ }
+ appendApp(appsPages[pageIndex], app);
+ }
+
+ // Tell the slider about the pages
+ updateSliderCards();
+
+ // Mark the current page
+ dots[cardSlider.currentCard].classList.add('selected');
+ }
+
+ /**
+ * Make a synthesized app object representing the chrome web store. It seems
+ * like this could just as easily come from the back-end, and then would
+ * support being rearranged, etc.
+ * @return {Object} The app object as would be sent from the webui back-end.
+ */
+ function makeWebstoreApp() {
+ return {
+ id: '', // Empty ID signifies this is a special synthesized app
+ page_index: 0,
+ app_launch_index: -1, // always first
+ name: templateData.web_store_title,
+ launch_url: templateData.web_store_url,
+ icon_big: getThemeUrl('IDR_WEBSTORE_ICON')
+ };
+ }
+
+ /**
+ * Given a theme resource name, construct a URL for it.
+ * @param {string} resourceName The name of the resource.
+ * @return {string} A url which can be used to load the resource.
+ */
+ function getThemeUrl(resourceName) {
+ return 'chrome://theme/' + resourceName;
+ }
+
+ /**
+ * Callback invoked by chrome whenever an app preference changes.
+ * The normal NTP uses this to keep track of the current launch-type of an
+ * app, updating the choices in the context menu. We don't have such a menu
+ * so don't use this at all (but it still needs to be here for chrome to
+ * call).
+ * @param {Object} data An object with all the data on available
+ * applications.
+ */
+ function appsPrefChangeCallback(data) {
+ }
+
+ /**
+ * Invoked whenever the pages in apps-page-list have changed so that
+ * the Slider knows about the new elements.
+ */
+ function updateSliderCards() {
+ var pageNo = cardSlider.currentCard;
+ if (pageNo >= appsPages.length)
+ pageNo = appsPages.length - 1;
+ var pageArray = [];
+ for (var i = 0; i < appsPages.length; i++)
+ pageArray[i] = appsPages[i];
+ cardSlider.setCards(pageArray, pageNo);
+ }
+
+ /**
+ * Create a new app element and attach it to the end of the specified app
+ * page.
+ * @param {!Element} parent The element where the app should be inserted.
+ * @param {!Object} app The application object to create an app for.
+ */
+ function appendApp(parent, app) {
+ // Make a deep copy of the template and clear its ID
+ var containerElement = appTemplate.cloneNode(true);
+ var appElement = containerElement.getElementsByClassName('app')[0];
+ assert(appElement, 'Expected app-template to have an app child');
+ assert(typeof(app.id) == 'string',
+ 'Expected every app to have an ID or empty string');
+ appElement.setAttribute('app-id', app.id);
+
+ // Find the span element (if any) and fill it in with the app name
+ var span = appElement.querySelector('span');
+ if (span)
+ span.textContent = app.name;
+
+ // Fill in the image
+ // We use a mask of the same image so CSS rules can highlight just the image
+ // when it's touched.
+ var appImg = appElement.querySelector('img');
+ if (appImg) {
+ appImg.src = app.icon_big;
+ appImg.style.webkitMaskImage = url(app.icon_big);
+ // We put a click handler just on the app image - so clicking on the
+ // margins between apps doesn't do anything
+ if (app.id) {
+ appEvents.add(appImg, 'click', appClick, false);
+ } else {
+ // Special case of synthesized apps - can't launch directly so just
+ // change the URL as if we clicked a link. We may want to eventually
+ // support tracking clicks with ping messages, but really it seems it
+ // would be better for the back-end to just create virtual apps for such
+ // cases.
+ appEvents.add(appImg, 'click', function(e) {
+ window.location = app.launch_url;
+ }, false);
+ }
+ }
+
+ // Only real apps with back-end storage (for their launch index, etc.) can
+ // be rearranged.
+ if (app.id) {
+ // Create a grabber to support moving apps around
+ // Note that we move the app rather than the container. This is so that an
+ // element remains in the original position so we can detect when an app
+ // is dropped in its starting location.
+ var grabber = new Grabber(appElement);
+ grabbers.push(grabber);
+
+ // Register to be made aware of when we are dragged
+ appEvents.add(appElement, Grabber.EventType.DRAG_START, appDragStart,
+ false);
+ appEvents.add(appElement, Grabber.EventType.DRAG_END, appDragEnd,
+ false);
+
+ // Register to be made aware of any app drags on top of our container
+ appEvents.add(containerElement, Grabber.EventType.DRAG_ENTER,
+ appDragEnter, false);
+ } else {
+ // Prevent any built-in drag-and-drop support from activating for the
+ // element.
+ appEvents.add(appElement, 'dragstart', function(e) {
+ e.preventDefault();
+ }, true);
+ }
+
+ // Insert at the end of the provided page
+ parent.appendChild(containerElement);
+ }
+
+ /**
+ * Creates a new page for apps
+ *
+ * @return {!Element} The apps-page element created.
+ * @param {boolean=} opt_animate If true, add the class 'new' to the created
+ * dot.
+ */
+ function createAppPage(opt_animate)
+ {
+ // Make a shallow copy of the app page template.
+ var newPage = appsPageTemplate.cloneNode(false);
+ appsPageList.appendChild(newPage);
+
+ // Make a deep copy of the dot template to add a new one.
+ var dotCount = dots.length;
+ var newDot = dotTemplate.cloneNode(true);
+ if (opt_animate)
+ newDot.classList.add('new');
+ dotList.appendChild(newDot);
+
+ // Add click handler to the dot to change the page.
+ // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we
+ // don't rely on synthesized click events, and the change takes effect
+ // before releasing). However, click events seems to be synthesized for a
+ // region outside the border, and a 10px box is too small to require touch
+ // events to fall inside of. We could get around this by adding a box around
+ // the dot for accepting the touch events.
+ function switchPage(e) {
+ cardSlider.selectCard(dotCount, true);
+ e.stopPropagation();
+ }
+ appEvents.add(newDot, 'click', switchPage, false);
+
+ // Change pages whenever an app is dragged over a dot.
+ appEvents.add(newDot, Grabber.EventType.DRAG_ENTER, switchPage, false);
+
+ return newPage;
+ }
+
+ /**
+ * Invoked when an app is clicked
+ * @param {Event} e The click event.
+ */
+ function appClick(e) {
+ var target = e.currentTarget;
+ var app = getParentByClassName(target, 'app');
+ assert(app, 'appClick should have been on a descendant of an app');
+
+ var appId = app.getAttribute('app-id');
+ assert(appId, 'unexpected app without appId');
+
+ // Tell chrome to launch the app.
+ var NTP_APPS_MAXIMIZED = 0;
+ chrome.send('launchApp', [appId, NTP_APPS_MAXIMIZED]);
+
+ // Don't allow the click to trigger a link or anything
+ e.preventDefault();
+ }
+
+ /**
+ * Search an elements ancestor chain for the nearest element that is a member
+ * of the specified class.
+ * @param {!Element} element The element to start searching from.
+ * @param {string} className The name of the class to locate.
+ * @return {Element} The first ancestor of the specified class or null.
+ */
+ function getParentByClassName(element, className)
+ {
+ for (var e = element; e; e = e.parentElement) {
+ if (e.classList.contains(className))
+ return e;
+ }
+ return null;
+ }
+
+ /**
+ * The container where the app currently being dragged came from.
+ * @type {!Element|undefined}
+ */
+ var draggingAppContainer;
+
+ /**
+ * The apps-page that the app currently being dragged camed from.
+ * @type {!Element|undefined}
+ */
+ var draggingAppOriginalPage;
+
+ /**
+ * The element that was originally after the app currently being dragged (or
+ * null if it was the last on the page).
+ * @type {!Element|undefined}
+ */
+ var draggingAppOriginalPosition;
+
+ /**
+ * Invoked when app dragging begins.
+ * @param {Grabber.Event} e The event from the Grabber indicating the drag.
+ */
+ function appDragStart(e) {
+ // Pull the element out to the appsFrame using fixed positioning. This
+ // ensures that the app is not affected (remains under the finger) if the
+ // slider changes cards and is translated. An alternate approach would be
+ // to use fixed positioning for the slider (so that changes to its position
+ // don't affect children that aren't positioned relative to it), but we
+ // don't yet have GPU acceleration for this. Note that we use the appsFrame
+ var element = e.grabbedElement;
+
+ var pos = element.getBoundingClientRect();
+ element.style.webkitTransform = '';
+
+ element.style.position = 'fixed';
+ // Don't want to zoom around the middle since the left/top co-ordinates
+ // are post-transform values.
+ element.style.webkitTransformOrigin = 'left top';
+ element.style.left = pos.left + 'px';
+ element.style.top = pos.top + 'px';
+
+ // Keep track of what app is being dragged and where it came from
+ assert(!draggingAppContainer, 'got DRAG_START without DRAG_END');
+ draggingAppContainer = element.parentNode;
+ assert(draggingAppContainer.classList.contains('app-container'));
+ draggingAppOriginalPosition = draggingAppContainer.nextSibling;
+ draggingAppOriginalPage = draggingAppContainer.parentNode;
+
+ // Move the app out of the container
+ // Note that appendChild also removes the element from its current parent.
+ getRequiredElement('apps-frame').appendChild(element);
+ }
+
+ /**
+ * Invoked when app dragging terminates (either successfully or not)
+ * @param {Grabber.Event} e The event from the Grabber.
+ */
+ function appDragEnd(e) {
+ // Stop floating the app
+ var appBeingDragged = e.grabbedElement;
+ assert(appBeingDragged.classList.contains('app'));
+ appBeingDragged.style.position = '';
+ appBeingDragged.style.webkitTransformOrigin = '';
+ appBeingDragged.style.left = '';
+ appBeingDragged.style.top = '';
+
+ // Ensure the trash can is not active (we won't necessarily get a DRAG_LEAVE
+ // for it - eg. if we drop on it, or the drag is cancelled)
+ trash.classList.remove('hover');
+ appBeingDragged.classList.remove('trashing');
+
+ // If we have an active drag (i.e. it wasn't aborted by an app update)
+ if (draggingAppContainer) {
+ // Put the app back into it's container
+ if (appBeingDragged.parentNode != draggingAppContainer)
+ draggingAppContainer.appendChild(appBeingDragged);
+
+ // If we care about the container's original position
+ if (draggingAppOriginalPage)
+ {
+ // Then put the container back where it came from
+ if (draggingAppOriginalPosition) {
+ draggingAppOriginalPage.insertBefore(draggingAppContainer,
+ draggingAppOriginalPosition);
+ } else {
+ draggingAppOriginalPage.appendChild(draggingAppContainer);
+ }
+ }
+ }
+
+ draggingAppContainer = undefined;
+ draggingAppOriginalPage = undefined;
+ draggingAppOriginalPosition = undefined;
+ }
+
+ /**
+ * Invoked when an app is dragged over another app. Updates the DOM to affect
+ * the rearrangement (but doesn't commit the change until the app is dropped).
+ * @param {Grabber.Event} e The event from the Grabber indicating the drag.
+ */
+ function appDragEnter(e)
+ {
+ assert(draggingAppContainer, 'expected stored container');
+ var sourceContainer = draggingAppContainer;
+
+ // Ensure enter events delivered to an app-container don't also get
+ // delivered to the document.
+ e.stopPropagation();
+
+ var curPage = appsPages[cardSlider.currentCard];
+ var followingContainer = null;
+
+ // If we dragged over a specific app, determine which one to insert before
+ if (e.currentTarget != document) {
+
+ // Start by assuming we'll insert the app before the one dragged over
+ followingContainer = e.currentTarget;
+ assert(followingContainer.classList.contains('app-container'),
+ 'expected drag over container');
+ assert(followingContainer.parentNode == curPage);
+ if (followingContainer == draggingAppContainer)
+ return;
+
+ // But if it's after the current container position then we'll need to
+ // move ahead by one to account for the container being removed.
+ if (curPage == draggingAppContainer.parentNode) {
+ for (var c = draggingAppContainer; c; c = c.nextElementSibling) {
+ if (c == followingContainer) {
+ followingContainer = followingContainer.nextElementSibling;
+ break;
+ }
+ }
+ }
+ }
+
+ // Move the container to the appropriate place on the page
+ curPage.insertBefore(draggingAppContainer, followingContainer);
+ }
+
+ /**
+ * Invoked when an app is dropped on the trash
+ * @param {Grabber.Event} e The event from the Grabber indicating the drop.
+ */
+ function appTrash(e) {
+ var appElement = e.grabbedElement;
+ assert(appElement.classList.contains('app'));
+ var appId = appElement.getAttribute('app-id');
+ assert(appId);
+
+ // Mark this drop as handled so that the catch-all drop handler
+ // on the document doesn't see this event.
+ e.stopPropagation();
+
+ // Tell chrome to uninstall the app (prompting the user)
+ chrome.send('uninstallApp', [appId]);
+ }
+
+ /**
+ * Called when an app is dropped anywhere other than the trash can. Commits
+ * any movement that has occurred.
+ * @param {Grabber.Event} e The event from the Grabber indicating the drop.
+ */
+ function appDrop(e) {
+ if (!draggingAppContainer)
+ // Drag was aborted (eg. due to an app update) - do nothing
+ return;
+
+ // If the app is dropped back into it's original position then do nothing
+ assert(draggingAppOriginalPage);
+ if (draggingAppContainer.parentNode == draggingAppOriginalPage &&
+ draggingAppContainer.nextSibling == draggingAppOriginalPosition)
+ return;
+
+ // Determine which app was being dragged
+ var appElement = e.grabbedElement;
+ assert(appElement.classList.contains('app'));
+ var appId = appElement.getAttribute('app-id');
+ assert(appId);
+
+ // Update the page index for the app if it's changed. This doesn't trigger
+ // a call to getAppsCallback so we want to do it before reorderApps
+ var pageIndex = cardSlider.currentCard;
+ assert(pageIndex >= 0 && pageIndex < appsPages.length,
+ 'page number out of range');
+ if (appsPages[pageIndex] != draggingAppOriginalPage)
+ chrome.send('setPageIndex', [appId, pageIndex]);
+
+ // Put the app being dragged back into it's container
+ draggingAppContainer.appendChild(appElement);
+
+ // Create a list of all appIds in the order now present in the DOM
+ var appIds = [];
+ for (var page = 0; page < appsPages.length; page++) {
+ var appsOnPage = appsPages[page].getElementsByClassName('app');
+ for (var i = 0; i < appsOnPage.length; i++) {
+ var id = appsOnPage[i].getAttribute('app-id');
+ if (id)
+ appIds.push(id);
+ }
+ }
+
+ // We are going to commit this repositioning - clear the original position
+ draggingAppOriginalPage = undefined;
+ draggingAppOriginalPosition = undefined;
+
+ // Tell chrome to update its database to persist this new order of apps This
+ // will cause getAppsCallback to be invoked and the apps to be redrawn.
+ chrome.send('reorderApps', [appId, appIds]);
+ appMoved = true;
+ }
+
+ /**
+ * Set to true if we're currently in rearrange mode and an app has
+ * been successfully dropped to a new location. This indicates that
+ * a getAppsCallback call is pending and we can rely on the DOM being
+ * updated by that.
+ * @type {boolean}
+ */
+ var appMoved = false;
+
+ /**
+ * Invoked whenever some app is grabbed
+ * @param {Grabber.Event} e The Grabber Grab event.
+ */
+ function enterRearrangeMode(e)
+ {
+ // Stop the slider from sliding for this touch
+ cardSlider.cancelTouch();
+
+ // Add an extra blank page in case the user wants to create a new page
+ createAppPage(true);
+ var pageAdded = appsPages.length - 1;
+ window.setTimeout(function() {
+ dots[pageAdded].classList.remove('new');
+ }, 0);
+
+ updateSliderCards();
+
+ // Cause the dot-list to grow
+ getRequiredElement('footer').classList.add('rearrange-mode');
+
+ assert(!appMoved, 'appMoved should not be set yet');
+ }
+
+ /**
+ * Invoked whenever some app is released
+ * @param {Grabber.Event} e The Grabber RELEASE event.
+ */
+ function leaveRearrangeMode(e)
+ {
+ // Return the dot-list to normal
+ getRequiredElement('footer').classList.remove('rearrange-mode');
+
+ // If we didn't successfully re-arrange an app, then we won't be
+ // refreshing the app view in getAppCallback and need to explicitly remove
+ // the extra empty page we added. We don't want to do this in the normal
+ // case because if we did actually drop an app there, we want to retain that
+ // page as our current page number.
+ if (!appMoved) {
+ assert(appsPages[appsPages.length - 1].
+ getElementsByClassName('app-container').length == 0,
+ 'Last app page should be empty');
+ removePage(appsPages.length - 1);
+ }
+ appMoved = false;
+ }
+
+ /**
+ * Remove the page with the specified index and update the slider.
+ * @param {number} pageNo The index of the page to remove.
+ */
+ function removePage(pageNo)
+ {
+ var page = appsPages[pageNo];
+
+ // Remove the page from the DOM
+ page.parentNode.removeChild(page);
+
+ // Remove the corresponding dot
+ // Need to give it a chance to animate though
+ var dot = dots[pageNo];
+ dot.classList.add('new');
+ window.setTimeout(function() {
+ // If we've re-created the apps (eg. because an app was uninstalled) then
+ // we will have removed the old dots from the document already, so skip.
+ if (dot.parentNode)
+ dot.parentNode.removeChild(dot);
+ }, DEFAULT_TRANSITION_TIME);
+
+ updateSliderCards();
+ }
+
+ // Return an object with all the exports
+ return {
+ assert: assert,
+ appsPrefChangeCallback: appsPrefChangeCallback,
+ getAppsCallback: getAppsCallback,
+ initialize: initializeNtp
+ };
+})();
+
+// publish ntp globals
+var assert = ntp.assert;
+var getAppsCallback = ntp.getAppsCallback;
+var appsPrefChangeCallback = ntp.appsPrefChangeCallback;
+
+// Initialize immediately once globals are published (there doesn't seem to be
+// any need to wait for DOMContentLoaded)
+ntp.initialize();
diff --git a/chrome/browser/resources/ntp4/tools/check.sh b/chrome/browser/resources/ntp4/tools/check.sh
new file mode 100755
index 0000000..6283e9d
--- /dev/null
+++ b/chrome/browser/resources/ntp4/tools/check.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+# Copyright (c) 2008 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.
+
+# This script checks the touch_ntp code for common errors and style
+# problems using the closure compiler (jscompiler) and closure linter
+# (gjslint) - both of which must be on the path.
+# See http://code.google.com/closure/compiler/ and
+# http://code.google.com/closure/utilities/ for details on these tools.
+
+SOURCES="event_tracker.js touch_handler.js card_slider.js new_tab.js grabber.js "
+
+# First run the closure compiler looking for syntactic issues.
+# Note that we throw away the output from jscompiler since it's use
+# is not yet common in Chromium and for now we want it to be an optional
+# tool for helping to find bugs, not something that actually changes
+# the embedded JavaScript (making it harder to debug, for example).
+
+# I used to run with '--warning_level VERBOSE' to get full type checking
+# but there are enough limitations in the language and compiler that
+# it doesn't seem worth the benefit (spent more time trying to apease
+# the compiler and reviewers of my code than the compiler saved me).
+
+# Enable support for property get/set syntax as added in ecmascript5.
+# Note that this requires a build of JSCompiler that is newer than
+# Feb 2011.
+CARGS="--language_in=ECMASCRIPT5_STRICT"
+
+CARGS+=" --js_output_file /dev/null"
+for S in $SOURCES tools/externs.js; do
+ CARGS+=" --js $S"
+done
+
+cd `dirname $0`/..
+
+echo jscompiler $CARGS
+jscompiler $CARGS || exit 1
+
+# Now run the closure linter looking for style issues.
+
+# GJSLint can't follow the more concice syntax for prototype members and
+# complains about missing @this annotations (filed as bug 4073735). To
+# cope for now I just just off all missing-JSDoc warnings.
+LARGS="--nojsdoc"
+
+# Verify extra rules like spacing and indentation
+LARGS+=" --strict"
+
+# Might as well check the bit of JS we have embedded in HTML too
+LARGS+=" --check_html new_tab.html"
+
+LARGS+=" $SOURCES"
+
+echo gjslint $LARGS
+gjslint $LARGS || exit 1
diff --git a/chrome/browser/resources/ntp4/tools/externs.js b/chrome/browser/resources/ntp4/tools/externs.js
new file mode 100644
index 0000000..f86d21a
--- /dev/null
+++ b/chrome/browser/resources/ntp4/tools/externs.js
@@ -0,0 +1,40 @@
+// 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.
+
+// externs.js contains variable declarations for the closure compiler
+// This isn't actually used when running the code.
+
+// JSCompiler doesn't know about this new Element property
+Element.prototype.classList = {};
+/** @param {string} c */
+Element.prototype.classList.remove = function(c) {};
+/** @param {string} c */
+Element.prototype.classList.add = function(c) {};
+/** @param {string} c */
+Element.prototype.classList.contains = function(c) {};
+
+/**
+ * @constructor
+ * @extends {Event}
+ */
+var CustomEvent = function() {};
+CustomEvent.prototype.initCustomEvent =
+ function(eventType, bubbles, cancellable, detail) {};
+/** @type {TouchHandler.EventDetail} */
+CustomEvent.prototype.detail;
+
+
+/** @param {string} s
+ * @return {string}
+ */
+var url = function(s) {};
+
+/**
+ * @param {string} type
+ * @param {EventListener|function(Event):(boolean|undefined)} listener
+ * @param {boolean=} opt_useCapture
+ * @return {undefined}
+ * @suppress {checkTypes}
+ */
+Window.prototype.addEventListener = function(type, listener, opt_useCapture) {};
diff --git a/chrome/browser/resources/ntp4/touch_handler.js b/chrome/browser/resources/ntp4/touch_handler.js
new file mode 100644
index 0000000..927bebb
--- /dev/null
+++ b/chrome/browser/resources/ntp4/touch_handler.js
@@ -0,0 +1,850 @@
+// 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 Touch Handler. Class that handles all touch events and
+ * uses them to interpret higher level gestures and behaviors. TouchEvent is a
+ * built in mobile safari type:
+ * http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html.
+ * This class is intended to work with all webkit browsers, tested on Chrome and
+ * iOS.
+ *
+ * The following types of gestures are currently supported. See the definition
+ * of TouchHandler.EventType for details.
+ *
+ * Single Touch:
+ * This provides simple single-touch events. Any secondary touch is
+ * ignored.
+ *
+ * Drag:
+ * A single touch followed by some movement. This behavior will handle all
+ * of the required events and report the properties of the drag to you
+ * while the touch is happening and at the end of the drag sequence. This
+ * behavior will NOT perform the actual dragging (redrawing the element)
+ * for you, this responsibility is left to the client code.
+ *
+ * Long press:
+ * When your element is touched and held without any drag occuring, the
+ * LONG_PRESS event will fire.
+ */
+
+// Use an anonymous function to enable strict mode just for this file (which
+// will be concatenated with other files when embedded in Chrome)
+var TouchHandler = (function() {
+ 'use strict';
+
+ /**
+ * A TouchHandler attaches to an Element, listents for low-level touch (or
+ * mouse) events and dispatching higher-level events on the element.
+ * @param {!Element} element The element to listen on and fire events
+ * for.
+ * @constructor
+ */
+ function TouchHandler(element) {
+ /**
+ * @type {!Element}
+ * @private
+ */
+ this.element_ = element;
+
+ /**
+ * The absolute sum of all touch y deltas.
+ * @type {number}
+ * @private
+ */
+ this.totalMoveY_ = 0;
+
+ /**
+ * The absolute sum of all touch x deltas.
+ * @type {number}
+ * @private
+ */
+ this.totalMoveX_ = 0;
+
+ /**
+ * An array of tuples where the first item is the horizontal component of a
+ * recent relevant touch and the second item is the touch's time stamp. Old
+ * touches are removed based on the max tracking time and when direction
+ * changes.
+ * @type {!Array.<number>}
+ * @private
+ */
+ this.recentTouchesX_ = [];
+
+ /**
+ * An array of tuples where the first item is the vertical component of a
+ * recent relevant touch and the second item is the touch's time stamp. Old
+ * touches are removed based on the max tracking time and when direction
+ * changes.
+ * @type {!Array.<number>}
+ * @private
+ */
+ this.recentTouchesY_ = [];
+
+ /**
+ * Used to keep track of all events we subscribe to so we can easily clean
+ * up
+ * @type {EventTracker}
+ * @private
+ */
+ this.events_ = new EventTracker();
+ }
+
+
+ /**
+ * DOM Events that may be fired by the TouchHandler at the element
+ */
+ TouchHandler.EventType = {
+ // Fired whenever the element is touched as the only touch to the device.
+ // enableDrag defaults to false, set to true to permit dragging.
+ TOUCH_START: 'touchHandler:touch_start',
+
+ // Fired when an element is held for a period of time. Prevents dragging
+ // from occuring (even if enableDrag was set to true).
+ LONG_PRESS: 'touchHandler:long_press',
+
+ // If enableDrag was set to true at TOUCH_START, DRAG_START will fire when
+ // the touch first moves sufficient distance. enableDrag is set to true but
+ // can be reset to false to cancel the drag.
+ DRAG_START: 'touchHandler:drag_start',
+
+ // If enableDrag was true after DRAG_START, DRAG_MOVE will fire whenever the
+ // touch is moved.
+ DRAG_MOVE: 'touchHandler:drag_move',
+
+ // Fired just before TOUCH_END when a drag is released. Correlates 1:1 with
+ // a DRAG_START.
+ DRAG_END: 'touchHandler:drag_end',
+
+ // Fired whenever a touch that is being tracked has been released.
+ // Correlates 1:1 with a TOUCH_START.
+ TOUCH_END: 'touchHandler:touch_end'
+ };
+
+
+ /**
+ * The type of event sent by TouchHandler
+ * @constructor
+ * @param {string} type The type of event (one of Grabber.EventType).
+ * @param {boolean} bubbles Whether or not the event should bubble.
+ * @param {number} clientX The X location of the touch.
+ * @param {number} clientY The Y location of the touch.
+ * @param {!Element} touchedElement The element at the current location of the
+ * touch.
+ */
+ TouchHandler.Event = function(type, bubbles, clientX, clientY,
+ touchedElement) {
+ var event = document.createEvent('Event');
+ event.initEvent(type, bubbles, true);
+ event.__proto__ = TouchHandler.Event.prototype;
+
+ /**
+ * The X location of the touch affected
+ * @type {number}
+ */
+ event.clientX = clientX;
+
+ /**
+ * The Y location of the touch affected
+ * @type {number}
+ */
+ event.clientY = clientY;
+
+ /**
+ * The element at the current location of the touch.
+ * @type {!Element}
+ */
+ event.touchedElement = touchedElement;
+
+ return event;
+ };
+
+ TouchHandler.Event.prototype = {
+ __proto__: Event.prototype,
+
+ /**
+ * For TOUCH_START and DRAG START events, set to true to enable dragging or
+ * false to disable dragging.
+ * @type {boolean|undefined}
+ */
+ enableDrag: undefined,
+
+ /**
+ * For DRAG events, provides the horizontal component of the
+ * drag delta. Drag delta is defined as the delta of the start touch
+ * position and the current drag position.
+ * @type {number|undefined}
+ */
+ dragDeltaX: undefined,
+
+ /**
+ * For DRAG events, provides the vertical component of the
+ * drag delta.
+ * @type {number|undefined}
+ */
+ dragDeltaY: undefined
+ };
+
+ /**
+ * Minimum movement of touch required to be considered a drag.
+ * @type {number}
+ * @private
+ */
+ TouchHandler.MIN_TRACKING_FOR_DRAG_ = 8;
+
+
+ /**
+ * The maximum number of ms to track a touch event. After an event is older
+ * than this value, it will be ignored in velocity calculations.
+ * @type {number}
+ * @private
+ */
+ TouchHandler.MAX_TRACKING_TIME_ = 250;
+
+
+ /**
+ * The maximum number of touches to track.
+ * @type {number}
+ * @private
+ */
+ TouchHandler.MAX_TRACKING_TOUCHES_ = 5;
+
+
+ /**
+ * The maximum velocity to return, in pixels per millisecond, that is used
+ * to guard against errors in calculating end velocity of a drag. This is a
+ * very fast drag velocity.
+ * @type {number}
+ * @private
+ */
+ TouchHandler.MAXIMUM_VELOCITY_ = 5;
+
+
+ /**
+ * The velocity to return, in pixel per millisecond, when the time stamps on
+ * the events are erroneous. The browser can return bad time stamps if the
+ * thread is blocked for the duration of the drag. This is a low velocity to
+ * prevent the content from moving quickly after a slow drag. It is less
+ * jarring if the content moves slowly after a fast drag.
+ * @type {number}
+ * @private
+ */
+ TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ = 1;
+
+ /**
+ * The time, in milliseconds, that a touch must be held to be considered
+ * 'long'.
+ * @type {number}
+ * @private
+ */
+ TouchHandler.TIME_FOR_LONG_PRESS_ = 500;
+
+ TouchHandler.prototype = {
+ /**
+ * If defined, the identifer of the single touch that is active. Note that
+ * 0 is a valid touch identifier - it should not be treated equivalently to
+ * undefined.
+ * @type {number|undefined}
+ * @private
+ */
+ activeTouch_: undefined,
+
+ /**
+ * @type {boolean|undefined}
+ * @private
+ */
+ tracking_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ startTouchX_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ startTouchY_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ endTouchX_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ endTouchY_: undefined,
+
+ /**
+ * Time of the touchstart event.
+ * @type {number|undefined}
+ * @private
+ */
+ startTime_: undefined,
+
+ /**
+ * The time of the touchend event.
+ * @type {number|undefined}
+ * @private
+ */
+ endTime_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ lastTouchX_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ lastTouchY_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ lastMoveX_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ lastMoveY_: undefined,
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ longPressTimeout_: undefined,
+
+ /**
+ * If defined and true, the next click event should be swallowed
+ * @type {boolean|undefined}
+ * @private
+ */
+ swallowNextClick_: undefined,
+
+ /**
+ * Start listenting for events.
+ * @param {boolean=} opt_capture True if the TouchHandler should listen to
+ * during the capture phase.
+ */
+ enable: function(opt_capture) {
+ var capture = !!opt_capture;
+
+ // Just listen to start events for now. When a touch is occuring we'll
+ // want to be subscribed to move and end events on the document, but we
+ // don't want to incur the cost of lots of no-op handlers on the document.
+ this.events_.add(this.element_, 'touchstart', this.onStart_.bind(this),
+ capture);
+ this.events_.add(this.element_, 'mousedown',
+ this.mouseToTouchCallback_(this.onStart_.bind(this)),
+ capture);
+
+ // If the element is long-pressed, we may need to swallow a click
+ this.events_.add(this.element_, 'click', this.onClick_.bind(this), true);
+ },
+
+ /**
+ * Stop listening to all events.
+ */
+ disable: function() {
+ this.stopTouching_();
+ this.events_.removeAll();
+ },
+
+ /**
+ * Wraps a callback with translations of mouse events to touch events.
+ * NOTE: These types really should be function(Event) but then we couldn't
+ * use this with bind (which operates on any type of function). Doesn't
+ * JSDoc support some sort of polymorphic types?
+ * @param {Function} callback The event callback.
+ * @return {Function} The wrapping callback.
+ * @private
+ */
+ mouseToTouchCallback_: function(callback) {
+ return function(e) {
+ // Note that there may be synthesizes mouse events caused by touch
+ // events (a mouseDown after a touch-click). We leave it up to the
+ // client to worry about this if it matters to them (typically a short
+ // mouseDown/mouseUp without a click is no big problem and it's not
+ // obvious how we identify such synthesized events in a general way).
+ var touch = {
+ // any fixed value will do for the identifier - there will only
+ // ever be a single active 'touch' when using the mouse.
+ identifier: 0,
+ clientX: e.clientX,
+ clientY: e.clientY,
+ target: e.target
+ };
+ e.touches = [];
+ e.targetTouches = [];
+ e.changedTouches = [touch];
+ if (e.type != 'mouseup') {
+ e.touches[0] = touch;
+ e.targetTouches[0] = touch;
+ }
+ callback(e);
+ };
+ },
+
+ /**
+ * Begin tracking the touchable element, it is eligible for dragging.
+ * @private
+ */
+ beginTracking_: function() {
+ this.tracking_ = true;
+ },
+
+ /**
+ * Stop tracking the touchable element, it is no longer dragging.
+ * @private
+ */
+ endTracking_: function() {
+ this.tracking_ = false;
+ this.dragging_ = false;
+ this.totalMoveY_ = 0;
+ this.totalMoveX_ = 0;
+ },
+
+ /**
+ * Reset the touchable element as if we never saw the touchStart
+ * Doesn't dispatch any end events - be careful of existing listeners.
+ */
+ cancelTouch: function() {
+ this.stopTouching_();
+ this.endTracking_();
+ // If clients needed to be aware of this, we could fire a cancel event
+ // here.
+ },
+
+ /**
+ * Record that touching has stopped
+ * @private
+ */
+ stopTouching_: function() {
+ // Mark as no longer being touched
+ this.activeTouch_ = undefined;
+
+ // If we're waiting for a long press, stop
+ window.clearTimeout(this.longPressTimeout_);
+
+ // Stop listening for move/end events until there's another touch.
+ // We don't want to leave handlers piled up on the document.
+ // Note that there's no harm in removing handlers that weren't added, so
+ // rather than track whether we're using mouse or touch we do both.
+ this.events_.remove(document, 'touchmove');
+ this.events_.remove(document, 'touchend');
+ this.events_.remove(document, 'touchcancel');
+ this.events_.remove(document, 'mousemove');
+ this.events_.remove(document, 'mouseup');
+ },
+
+ /**
+ * Touch start handler.
+ * @param {!TouchEvent} e The touchstart event.
+ * @private
+ */
+ onStart_: function(e) {
+ // Only process single touches. If there is already a touch happening, or
+ // two simultaneous touches then just ignore them.
+ if (e.touches.length > 1)
+ // Note that we could cancel an active touch here. That would make
+ // simultaneous touch behave similar to near-simultaneous. However, if
+ // the user is dragging something, an accidental second touch could be
+ // quite disruptive if it cancelled their drag. Better to just ignore
+ // it.
+ return;
+
+ // It's still possible there could be an active "touch" if the user is
+ // simultaneously using a mouse and a touch input.
+ if (this.activeTouch_ !== undefined)
+ return;
+
+ var touch = e.targetTouches[0];
+ this.activeTouch_ = touch.identifier;
+
+ // We've just started touching so shouldn't swallow any upcoming click
+ if (this.swallowNextClick_)
+ this.swallowNextClick_ = false;
+
+ // Sign up for end/cancel notifications for this touch.
+ // Note that we do this on the document so that even if the user drags
+ // their finger off the element, we'll still know what they're doing.
+ if (e.type == 'mousedown') {
+ this.events_.add(document, 'mouseup',
+ this.mouseToTouchCallback_(this.onEnd_.bind(this)), false);
+ } else {
+ this.events_.add(document, 'touchend', this.onEnd_.bind(this), false);
+ this.events_.add(document, 'touchcancel', this.onEnd_.bind(this),
+ false);
+ }
+
+ // This timeout is cleared on touchEnd and onDrag
+ // If we invoke the function then we have a real long press
+ window.clearTimeout(this.longPressTimeout_);
+ this.longPressTimeout_ = window.setTimeout(
+ this.onLongPress_.bind(this),
+ TouchHandler.TIME_FOR_LONG_PRESS_);
+
+ // Dispatch the TOUCH_START event
+ if (!this.dispatchEvent_(TouchHandler.EventType.TOUCH_START, touch))
+ // Dragging was not enabled, nothing more to do
+ return;
+
+ // We want dragging notifications
+ if (e.type == 'mousedown') {
+ this.events_.add(document, 'mousemove',
+ this.mouseToTouchCallback_(this.onMove_.bind(this)), false);
+ } else {
+ this.events_.add(document, 'touchmove', this.onMove_.bind(this), false);
+ }
+
+ this.startTouchX_ = this.lastTouchX_ = touch.clientX;
+ this.startTouchY_ = this.lastTouchY_ = touch.clientY;
+ this.startTime_ = e.timeStamp;
+
+ this.recentTouchesX_ = [];
+ this.recentTouchesY_ = [];
+ this.recentTouchesX_.push(touch.clientX, e.timeStamp);
+ this.recentTouchesY_.push(touch.clientY, e.timeStamp);
+
+ this.beginTracking_();
+ },
+
+ /**
+ * Given a list of Touches, find the one matching our activeTouch
+ * identifier. Note that Chrome currently always uses 0 as the identifier.
+ * In that case we'll end up always choosing the first element in the list.
+ * @param {TouchList} touches The list of Touch objects to search.
+ * @return {!Touch|undefined} The touch matching our active ID if any.
+ * @private
+ */
+ findActiveTouch_: function(touches) {
+ assert(this.activeTouch_ !== undefined, 'Expecting an active touch');
+ // A TouchList isn't actually an array, so we shouldn't use
+ // Array.prototype.filter/some, etc.
+ for (var i = 0; i < touches.length; i++) {
+ if (touches[i].identifier == this.activeTouch_)
+ return touches[i];
+ }
+ return undefined;
+ },
+
+ /**
+ * Touch move handler.
+ * @param {!TouchEvent} e The touchmove event.
+ * @private
+ */
+ onMove_: function(e) {
+ if (!this.tracking_)
+ return;
+
+ // Our active touch should always be in the list of touches still active
+ assert(this.findActiveTouch_(e.touches), 'Missing touchEnd');
+
+ var that = this;
+ var touch = this.findActiveTouch_(e.changedTouches);
+ if (!touch)
+ return;
+
+ var clientX = touch.clientX;
+ var clientY = touch.clientY;
+
+ var moveX = this.lastTouchX_ - clientX;
+ var moveY = this.lastTouchY_ - clientY;
+ this.totalMoveX_ += Math.abs(moveX);
+ this.totalMoveY_ += Math.abs(moveY);
+ this.lastTouchX_ = clientX;
+ this.lastTouchY_ = clientY;
+
+ if (!this.dragging_ && (this.totalMoveY_ >
+ TouchHandler.MIN_TRACKING_FOR_DRAG_ ||
+ this.totalMoveX_ >
+ TouchHandler.MIN_TRACKING_FOR_DRAG_)) {
+ // If we're waiting for a long press, stop
+ window.clearTimeout(this.longPressTimeout_);
+
+ // Dispatch the DRAG_START event and record whether dragging should be
+ // allowed or not. Note that this relies on the current value of
+ // startTouchX/Y - handlers may use the initial drag delta to determine
+ // if dragging should be permitted.
+ this.dragging_ = this.dispatchEvent_(
+ TouchHandler.EventType.DRAG_START, touch);
+
+ if (this.dragging_) {
+ // Update the start position here so that drag deltas have better
+ // values but don't touch the recent positions so that velocity
+ // calculations can still use touchstart position in the time and
+ // distance delta.
+ this.startTouchX_ = clientX;
+ this.startTouchY_ = clientY;
+ this.startTime_ = e.timeStamp;
+ } else {
+ this.endTracking_();
+ }
+ }
+
+ if (this.dragging_) {
+ this.dispatchEvent_(TouchHandler.EventType.DRAG_MOVE, touch);
+
+ this.removeTouchesInWrongDirection_(this.recentTouchesX_,
+ this.lastMoveX_, moveX);
+ this.removeTouchesInWrongDirection_(this.recentTouchesY_,
+ this.lastMoveY_, moveY);
+ this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
+ this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
+ this.recentTouchesX_.push(clientX, e.timeStamp);
+ this.recentTouchesY_.push(clientY, e.timeStamp);
+ }
+
+ this.lastMoveX_ = moveX;
+ this.lastMoveY_ = moveY;
+ },
+
+ /**
+ * Filters the provided recent touches array to remove all touches except
+ * the last if the move direction has changed.
+ * @param {!Array.<number>} recentTouches An array of tuples where the first
+ * item is the x or y component of the recent touch and the second item
+ * is the touch time stamp.
+ * @param {number|undefined} lastMove The x or y component of the previous
+ * move.
+ * @param {number} recentMove The x or y component of the most recent move.
+ * @private
+ */
+ removeTouchesInWrongDirection_: function(recentTouches, lastMove,
+ recentMove) {
+ if (lastMove && recentMove && recentTouches.length > 2 &&
+ (lastMove > 0 ^ recentMove > 0)) {
+ recentTouches.splice(0, recentTouches.length - 2);
+ }
+ },
+
+ /**
+ * Filters the provided recent touches array to remove all touches older
+ * than the max tracking time or the 5th most recent touch.
+ * @param {!Array.<number>} recentTouches An array of tuples where the first
+ * item is the x or y component of the recent touch and the second item
+ * is the touch time stamp.
+ * @param {number} recentTime The time of the most recent event.
+ * @private
+ */
+ removeOldTouches_: function(recentTouches, recentTime) {
+ while (recentTouches.length && recentTime - recentTouches[1] >
+ TouchHandler.MAX_TRACKING_TIME_ ||
+ recentTouches.length >
+ TouchHandler.MAX_TRACKING_TOUCHES_ * 2) {
+ recentTouches.splice(0, 2);
+ }
+ },
+
+ /**
+ * Touch end handler.
+ * @param {!TouchEvent} e The touchend event.
+ * @private
+ */
+ onEnd_: function(e) {
+ var that = this;
+ assert(this.activeTouch_ !== undefined, 'Expect to already be touching');
+
+ // If the touch we're tracking isn't changing here, ignore this touch end.
+ var touch = this.findActiveTouch_(e.changedTouches);
+ if (!touch) {
+ // In most cases, our active touch will be in the 'touches' collection,
+ // but we can't assert that because occasionally two touchend events can
+ // occur at almost the same time with both having empty 'touches' lists.
+ // I.e., 'touches' seems like it can be a bit more up-to-date than the
+ // current event.
+ return;
+ }
+
+ // This is touchEnd for the touch we're monitoring
+ assert(!this.findActiveTouch_(e.touches),
+ 'Touch ended also still active');
+
+ // Indicate that touching has finished
+ this.stopTouching_();
+
+ if (this.tracking_) {
+ var clientX = touch.clientX;
+ var clientY = touch.clientY;
+
+ if (this.dragging_) {
+ this.endTime_ = e.timeStamp;
+ this.endTouchX_ = clientX;
+ this.endTouchY_ = clientY;
+
+ this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
+ this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
+
+ this.dispatchEvent_(TouchHandler.EventType.DRAG_END, touch);
+
+ // Note that in some situations we can get a click event here as well.
+ // For now this isn't a problem, but we may want to consider having
+ // some logic that hides clicks that appear to be caused by a touchEnd
+ // used for dragging.
+ }
+
+ this.endTracking_();
+ }
+
+ // Note that we dispatch the touchEnd event last so that events at
+ // different levels of semantics nest nicely (similar to how DOM
+ // drag-and-drop events are nested inside of the mouse events that trigger
+ // them).
+ this.dispatchEvent_(TouchHandler.EventType.TOUCH_END, touch);
+ },
+
+ /**
+ * Get end velocity of the drag. This method is specific to drag behavior,
+ * so if touch behavior and drag behavior is split then this should go with
+ * drag behavior. End velocity is defined as deltaXY / deltaTime where
+ * deltaXY is the difference between endPosition and the oldest recent
+ * position, and deltaTime is the difference between endTime and the oldest
+ * recent time stamp.
+ * @return {Object} The x and y velocity.
+ */
+ getEndVelocity: function() {
+ // Note that we could move velocity to just be an end-event parameter.
+ var velocityX = this.recentTouchesX_.length ?
+ (this.endTouchX_ - this.recentTouchesX_[0]) /
+ (this.endTime_ - this.recentTouchesX_[1]) : 0;
+ var velocityY = this.recentTouchesY_.length ?
+ (this.endTouchY_ - this.recentTouchesY_[0]) /
+ (this.endTime_ - this.recentTouchesY_[1]) : 0;
+
+ velocityX = this.correctVelocity_(velocityX);
+ velocityY = this.correctVelocity_(velocityY);
+
+ return {
+ x: velocityX,
+ y: velocityY
+ };
+ },
+
+ /**
+ * Correct erroneous velocities by capping the velocity if we think it's too
+ * high, or setting it to a default velocity if know that the event data is
+ * bad.
+ * @param {number} velocity The x or y velocity component.
+ * @return {number} The corrected velocity.
+ * @private
+ */
+ correctVelocity_: function(velocity) {
+ var absVelocity = Math.abs(velocity);
+
+ // We add to recent touches for each touchstart and touchmove. If we have
+ // fewer than 3 touches (6 entries), we assume that the thread was blocked
+ // for the duration of the drag and we received events in quick succession
+ // with the wrong time stamps.
+ if (absVelocity > TouchHandler.MAXIMUM_VELOCITY_) {
+ absVelocity = this.recentTouchesY_.length < 3 ?
+ TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ :
+ TouchHandler.MAXIMUM_VELOCITY_;
+ }
+ return absVelocity * (velocity < 0 ? -1 : 1);
+ },
+
+ /**
+ * Handler when an element has been pressed for a long time
+ * @private
+ */
+ onLongPress_: function() {
+ // Swallow any click that occurs on this element without an intervening
+ // touch start event. This simple click-busting technique should be
+ // sufficient here since a real click should have a touchstart first.
+ this.swallowNextClick_ = true;
+
+ // Dispatch to the LONG_PRESS
+ this.dispatchEventXY_(TouchHandler.EventType.LONG_PRESS, this.element_,
+ this.startTouchX_, this.startTouchY_);
+ },
+
+ /**
+ * Click handler - used to swallow clicks after a long-press
+ * @param {!Event} e The click event.
+ * @private
+ */
+ onClick_: function(e) {
+ if (this.swallowNextClick_) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.swallowNextClick_ = false;
+ }
+ },
+
+ /**
+ * Dispatch a TouchHandler event to the element
+ * @param {string} eventType The event to dispatch.
+ * @param {Touch} touch The touch triggering this event.
+ * @return {boolean|undefined} The value of enableDrag after dispatching
+ * the event.
+ * @private
+ */
+ dispatchEvent_: function(eventType, touch) {
+
+ // Determine which element was touched. For mouse events, this is always
+ // the event/touch target. But for touch events, the target is always the
+ // target of the touchstart (and it's unlikely we can change this
+ // since the common implementation of touch dragging relies on it). Since
+ // touch is our primary scenario (which we want to emulate with mouse),
+ // we'll treat both cases the same and not depend on the target.
+ var touchedElement;
+ if (eventType == TouchHandler.EventType.TOUCH_START) {
+ touchedElement = touch.target;
+ } else {
+ touchedElement = this.element_.ownerDocument.
+ elementFromPoint(touch.clientX, touch.clientY);
+ }
+
+ return this.dispatchEventXY_(eventType, touchedElement, touch.clientX,
+ touch.clientY);
+ },
+
+ /**
+ * Dispatch a TouchHandler event to the element
+ * @param {string} eventType The event to dispatch.
+ @param {number} clientX The X location for the event.
+ @param {number} clientY The Y location for the event.
+ * @return {boolean|undefined} The value of enableDrag after dispatching
+ * the event.
+ * @private
+ */
+ dispatchEventXY_: function(eventType, touchedElement, clientX, clientY) {
+ var isDrag = (eventType == TouchHandler.EventType.DRAG_START ||
+ eventType == TouchHandler.EventType.DRAG_MOVE ||
+ eventType == TouchHandler.EventType.DRAG_END);
+
+ // Drag events don't bubble - we're really just dragging the element,
+ // not affecting its parent at all.
+ var bubbles = !isDrag;
+
+ var event = new TouchHandler.Event(eventType, bubbles, clientX, clientY,
+ touchedElement);
+
+ // Set enableDrag when it can be overridden
+ if (eventType == TouchHandler.EventType.TOUCH_START)
+ event.enableDrag = false;
+ else if (eventType == TouchHandler.EventType.DRAG_START)
+ event.enableDrag = true;
+
+ if (isDrag) {
+ event.dragDeltaX = clientX - this.startTouchX_;
+ event.dragDeltaY = clientY - this.startTouchY_;
+ }
+
+ this.element_.dispatchEvent(event);
+ return event.enableDrag;
+ }
+ };
+
+ return TouchHandler;
+})();
diff --git a/chrome/browser/resources/touch_ntp/trash-open.png b/chrome/browser/resources/shared/images/trash-open.png
index b0ccc1b..b0ccc1b 100644
--- a/chrome/browser/resources/touch_ntp/trash-open.png
+++ b/chrome/browser/resources/shared/images/trash-open.png
Binary files differ
diff --git a/chrome/browser/resources/touch_ntp/trash.png b/chrome/browser/resources/shared/images/trash.png
index 8b43f1c..8b43f1c 100644
--- a/chrome/browser/resources/touch_ntp/trash.png
+++ b/chrome/browser/resources/shared/images/trash.png
Binary files differ
diff --git a/chrome/browser/resources/touch_ntp/newtab.css b/chrome/browser/resources/touch_ntp/newtab.css
index 455e610..079c4e1 100644
--- a/chrome/browser/resources/touch_ntp/newtab.css
+++ b/chrome/browser/resources/touch_ntp/newtab.css
@@ -188,7 +188,7 @@ body {
height: 100%;
right: 0px;
bottom: 0px;
- background-image: url('trash.png');
+ background-image: url('../shared/images/trash.png');
background-size: 40px 40px;
background-repeat: no-repeat;
background-position: 40px 12px;
@@ -200,7 +200,7 @@ body {
}
#trash.hover {
- background-image: url('trash-open.png');
+ background-image: url('../shared/images/trash-open.png');
}
.app.trashing img {
diff --git a/chrome/browser/ui/webui/ntp_resource_cache.cc b/chrome/browser/ui/webui/ntp_resource_cache.cc
index 7c2ac1c..501e89a 100644
--- a/chrome/browser/ui/webui/ntp_resource_cache.cc
+++ b/chrome/browser/ui/webui/ntp_resource_cache.cc
@@ -395,8 +395,11 @@ void NTPResourceCache::CreateNewTabHTML() {
// do here (all of the template data, etc.), but we keep the back end
// consistent across builds, supporting the union of all NTP front-ends
// for simplicity.
+ int ntp_resource_id =
+ CommandLine::ForCurrentProcess()->HasSwitch(switches::kNewTabPage4) ?
+ IDR_NEW_TAB_4_HTML : IDR_NEW_TAB_HTML;
base::StringPiece new_tab_html(ResourceBundle::GetSharedInstance().
- GetRawDataResource(IDR_NEW_TAB_HTML));
+ GetRawDataResource(ntp_resource_id));
// Inject the template data into the HTML so that it is available before any
// layout is needed.
@@ -547,11 +550,13 @@ void NTPResourceCache::CreateNewTabCSS() {
SkColorSetA(color_section_header_rule, 0))); // $$$2
subst3.push_back(SkColorToRGBAString(color_text_light)); // $$$3
-
// Get our template.
+ int ntp_css_resource_id =
+ CommandLine::ForCurrentProcess()->HasSwitch(switches::kNewTabPage4) ?
+ IDR_NEW_TAB_4_THEME_CSS : IDR_NEW_TAB_THEME_CSS;
static const base::StringPiece new_tab_theme_css(
ResourceBundle::GetSharedInstance().GetRawDataResource(
- IDR_NEW_TAB_THEME_CSS));
+ ntp_css_resource_id));
// Create the string from our template and the replacements.
std::string css_string;
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index 5077c4e..366b99b 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -747,6 +747,9 @@ const char kNaClBrokerProcess[] = "nacl-broker";
// Causes the Native Client process to display a dialog on launch.
const char kNaClStartupDialog[] = "nacl-startup-dialog";
+// Use the latest incarnation of the new tab page.
+const char kNewTabPage4[] = "new-tab-page-4";
+
// Disables the default browser check. Useful for UI/browser tests where we
// want to avoid having the default browser info-bar displayed.
const char kNoDefaultBrowserCheck[] = "no-default-browser-check";
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index b7f8e64..894aad6 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -216,6 +216,7 @@ extern const char kNaClDebugIP[];
extern const char kNaClDebugPorts[];
extern const char kNaClBrokerProcess[];
extern const char kNaClStartupDialog[];
+extern const char kNewTabPage4[];
extern const char kNoDefaultBrowserCheck[];
extern const char kNoEvents[];
extern const char kNoExperiments[];