diff options
author | estade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-03-19 00:49:24 +0000 |
---|---|---|
committer | estade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-03-19 00:49:24 +0000 |
commit | d5dcfb3c3346d6f65b6265ff3c0a11e968c84e16 (patch) | |
tree | da084e13915d4387df88654ca4fd33fe05e9c8a9 | |
parent | 5d3b29bf92fd075a51106e6b2da7a574ecae2f45 (diff) | |
download | chromium_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
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 Binary files differindex b0ccc1b..b0ccc1b 100644 --- a/chrome/browser/resources/touch_ntp/trash-open.png +++ b/chrome/browser/resources/shared/images/trash-open.png diff --git a/chrome/browser/resources/touch_ntp/trash.png b/chrome/browser/resources/shared/images/trash.png Binary files differindex 8b43f1c..8b43f1c 100644 --- a/chrome/browser/resources/touch_ntp/trash.png +++ b/chrome/browser/resources/shared/images/trash.png 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[]; |