summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordhollowa@chromium.org <dhollowa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-06-26 00:34:20 +0000
committerdhollowa@chromium.org <dhollowa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-06-26 00:34:20 +0000
commit4a44f6610eb14a728a22a9dea00d6707d2e616e1 (patch)
tree0e76609c4c38a88943cb2f9dd4bf7b81644f140b
parent50b6601e6fbe1862226c072952d52eae1556a6ae (diff)
downloadchromium_src-4a44f6610eb14a728a22a9dea00d6707d2e616e1.zip
chromium_src-4a44f6610eb14a728a22a9dea00d6707d2e616e1.tar.gz
chromium_src-4a44f6610eb14a728a22a9dea00d6707d2e616e1.tar.bz2
Add alternate New Tab Page for Instant Extended work
Adds alternate New Tab Page for Instant Extended work. This is a duplicate of the chrome/browser/resources/ntp4/ directory. This alternate page is enabled with the --enable-instant-extended-api flag. BUG=133529 TEST=Manual R=estade@chromium.org Review URL: https://chromiumcodereview.appspot.com/10636031 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@144073 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/browser_resources.grd4
-rw-r--r--chrome/browser/resources/ntp_search/OWNERS4
-rw-r--r--chrome/browser/resources/ntp_search/apps_page.css176
-rw-r--r--chrome/browser/resources/ntp_search/apps_page.js917
-rw-r--r--chrome/browser/resources/ntp_search/dot_list.js80
-rw-r--r--chrome/browser/resources/ntp_search/footer_menu.css160
-rw-r--r--chrome/browser/resources/ntp_search/images/app_promo_button.pngbin0 -> 1286 bytes
-rw-r--r--chrome/browser/resources/ntp_search/images/closed_window.pngbin0 -> 266 bytes
-rw-r--r--chrome/browser/resources/ntp_search/images/disclosure_triangle_mask.pngbin0 -> 94 bytes
-rw-r--r--chrome/browser/resources/ntp_search/logging.js32
-rw-r--r--chrome/browser/resources/ntp_search/most_visited_page.css185
-rw-r--r--chrome/browser/resources/ntp_search/most_visited_page.js470
-rw-r--r--chrome/browser/resources/ntp_search/nav_dot.css90
-rw-r--r--chrome/browser/resources/ntp_search/nav_dot.js276
-rw-r--r--chrome/browser/resources/ntp_search/new_tab.css447
-rw-r--r--chrome/browser/resources/ntp_search/new_tab.html206
-rw-r--r--chrome/browser/resources/ntp_search/new_tab.js595
-rw-r--r--chrome/browser/resources/ntp_search/new_tab_theme.css109
-rw-r--r--chrome/browser/resources/ntp_search/other_sessions.js361
-rw-r--r--chrome/browser/resources/ntp_search/page_list_view.js767
-rw-r--r--chrome/browser/resources/ntp_search/page_switcher.js107
-rw-r--r--chrome/browser/resources/ntp_search/recently_closed.js114
-rw-r--r--chrome/browser/resources/ntp_search/suggestions_page.css109
-rw-r--r--chrome/browser/resources/ntp_search/suggestions_page.js471
-rw-r--r--chrome/browser/resources/ntp_search/tile_page.css194
-rw-r--r--chrome/browser/resources/ntp_search/tile_page.js1347
-rw-r--r--chrome/browser/resources/ntp_search/trash.js83
-rw-r--r--chrome/browser/ui/webui/ntp/new_tab_ui.cc24
-rw-r--r--chrome/browser/ui/webui/ntp/ntp_resource_cache.cc8
29 files changed, 7329 insertions, 7 deletions
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index 227f2ab..3f6ffb1 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -89,6 +89,8 @@
<include name="IDR_NEW_INCOGNITO_TAB_THEME_CSS" file="resources\new_incognito_tab_theme.css" 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_theme.css" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_NEW_TAB_SEARCH_HTML" file="resources\ntp_search\new_tab.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_NEW_TAB_SEARCH_THEME_CSS" file="resources\ntp_search\new_tab_theme.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" />
@@ -138,6 +140,8 @@
<include name="IDR_SUGGESTIONS_INTERNALS_JS" file="resources\suggestions_internals\suggestions_internals.js" type="BINDATA" />
<include name="IDR_SUGGESTIONS_PAGE_CSS" file="resources\ntp4\suggestions_page.css" type="BINDATA" />
<include name="IDR_SUGGESTIONS_PAGE_JS" file="resources\ntp4\suggestions_page.js" type="BINDATA" />
+ <include name="IDR_SUGGESTIONS_PAGE_SEARCH_CSS" file="resources\ntp_search\suggestions_page.css" type="BINDATA" />
+ <include name="IDR_SUGGESTIONS_PAGE_SEARCH_JS" file="resources\ntp_search\suggestions_page.js" type="BINDATA" />
<include name="IDR_SSL_ERROR_HTML" file="resources\ssl_error.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_SSL_ROAD_BLOCK_HTML" file="resources\ssl_roadblock.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_TRACING_HTML" file="..\..\third_party\trace-viewer\src\about_tracing.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
diff --git a/chrome/browser/resources/ntp_search/OWNERS b/chrome/browser/resources/ntp_search/OWNERS
new file mode 100644
index 0000000..a295d1b
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/OWNERS
@@ -0,0 +1,4 @@
+set noparent
+estade@chromium.org
+rbyers@chromium.org
+dbeam@chromium.org
diff --git a/chrome/browser/resources/ntp_search/apps_page.css b/chrome/browser/resources/ntp_search/apps_page.css
new file mode 100644
index 0000000..b0f7c79
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/apps_page.css
@@ -0,0 +1,176 @@
+/* Copyright (c) 2012 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. */
+
+.app {
+ outline: none;
+ position: absolute;
+ text-align: center;
+}
+
+.app-contents {
+ -webkit-transition: -webkit-transform 100ms;
+}
+
+.app-contents:active:not(.suppress-active),
+.app:not(.click-focus):focus .app-contents:not(.suppress-active),
+.drag-representation:not(.placing) .app-contents {
+ -webkit-transform: scale(1.1);
+}
+
+/* Don't animate the initial scaling. */
+.app-contents:active:not(.suppress-active),
+/* Active gets applied right before .suppress-active, so to avoid flicker
+ * we need to make the scale go back to normal without an animation. */
+.app-contents.suppress-active {
+ -webkit-transition-duration: 0;
+}
+
+.app-contents > span {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.app-img-container {
+ /* -webkit-mask-image set by JavaScript to the image source. */
+ -webkit-mask-size: 100% 100%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.app-img-container > * {
+ height: 100%;
+ width: 100%;
+}
+
+.app-icon-div {
+ -webkit-box-align: center;
+ -webkit-box-pack: center;
+ background-color: white;
+ border: 1px solid #d5d5d5;
+ border-radius: 5px;
+ display: -webkit-box;
+ margin-left: auto;
+ margin-right: auto;
+ position: relative;
+ vertical-align: middle;
+ z-index: 0;
+}
+
+.app-icon-div .app-img-container {
+ bottom: 10px;
+ left: 10px;
+ position: absolute;
+}
+
+.app-icon-div .color-stripe {
+ border-bottom-left-radius: 5px 5px;
+ border-bottom-right-radius: 5px 5px;
+ bottom: 0;
+ height: 3px;
+ opacity: 1.0;
+ position: absolute;
+ width: 100%;
+ z-index: 100;
+}
+
+.app-context-menu > button:first-child {
+ font-weight: bold;
+}
+
+.app-context-menu {
+ z-index: 1000;
+}
+
+.launch-click-target {
+ cursor: pointer;
+}
+
+/* Notifications */
+
+.app-notification {
+ -webkit-transition: color 150ms linear;
+ color: #999;
+ display: block;
+ font-size: 0.9em;
+ white-space: nowrap;
+}
+
+.app-notification:hover {
+ text-decoration: underline;
+}
+
+/* Promo */
+/* Show the promo if the webstore has a promo and is the only app on the page.
+ */
+.tile:only-of-type > .has-promo > .app-contents > span {
+ display: none;
+}
+
+.tile:only-of-type > .has-promo .app-img-container > .apps-promo-logo {
+ display: block;
+}
+
+.apps-promo-logo {
+ display: none;
+ height: 128px;
+ width: 128px;
+}
+
+.tile:only-of-type > .has-promo .app-img-container > img:first-child {
+ display: none;
+}
+
+.app-img-container > img:first-child {
+ display: block;
+}
+
+/* TODO(estade): animation? */
+.tile:only-of-type > .has-promo > .apps-promo-extras {
+ display: block;
+}
+
+.apps-promo-extras {
+ display: none;
+ /* 128 * 5/4 */
+ left: 160px;
+ position: absolute;
+ text-align: left;
+ top: 0;
+}
+
+html[dir='rtl'] .apps-promo-extras {
+ left: auto;
+ right: 160px;
+}
+
+.apps-promo-heading {
+ -webkit-margin-start: 3px;
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.g-button-basic {
+ -webkit-border-image: url('images/app_promo_button.png') 6 10 12 6;
+ border-width: 6px 10px 12px 6px;
+ color: #fff !important;
+ display: inline-block;
+ font-size: 1.3em;
+ font-weight: bold;
+ padding: 2px 10px;
+ text-align: center;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.app .invisible {
+ visibility: hidden;
+}
+
+/* Move the notification lower on apps pages to account for the 16px of
+ * transparency each app icon should have. */
+.apps-page #notification-container {
+ bottom: 15px;
+}
diff --git a/chrome/browser/resources/ntp_search/apps_page.js b/chrome/browser/resources/ntp_search/apps_page.js
new file mode 100644
index 0000000..6a41162
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/apps_page.js
@@ -0,0 +1,917 @@
+// Copyright (c) 2012 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.
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ var APP_LAUNCH = {
+ // The histogram buckets (keep in sync with extension_constants.h).
+ NTP_APPS_MAXIMIZED: 0,
+ NTP_APPS_COLLAPSED: 1,
+ NTP_APPS_MENU: 2,
+ NTP_MOST_VISITED: 3,
+ NTP_RECENTLY_CLOSED: 4,
+ NTP_APP_RE_ENABLE: 16,
+ NTP_WEBSTORE_FOOTER: 18,
+ NTP_WEBSTORE_PLUS_ICON: 19,
+ };
+
+ // Histogram buckets for UMA tracking of where a DnD drop came from.
+ var DRAG_SOURCE = {
+ SAME_APPS_PANE: 0,
+ OTHER_APPS_PANE: 1,
+ MOST_VISITED_PANE: 2,
+ BOOKMARKS_PANE: 3,
+ OUTSIDE_NTP: 4
+ };
+ var DRAG_SOURCE_LIMIT = DRAG_SOURCE.OUTSIDE_NTP + 1;
+
+ /**
+ * App context menu. The class is designed to be used as a singleton with
+ * the app that is currently showing a context menu stored in this.app_.
+ * @constructor
+ */
+ function AppContextMenu() {
+ this.__proto__ = AppContextMenu.prototype;
+ this.initialize();
+ }
+ cr.addSingletonGetter(AppContextMenu);
+
+ AppContextMenu.prototype = {
+ initialize: function() {
+ var menu = new cr.ui.Menu;
+ cr.ui.decorate(menu, cr.ui.Menu);
+ menu.classList.add('app-context-menu');
+ this.menu = menu;
+
+ this.launch_ = this.appendMenuItem_();
+ this.launch_.addEventListener('activate', this.onLaunch_.bind(this));
+
+ menu.appendChild(cr.ui.MenuItem.createSeparator());
+ this.launchRegularTab_ = this.appendMenuItem_('applaunchtyperegular');
+ this.launchPinnedTab_ = this.appendMenuItem_('applaunchtypepinned');
+ if (!cr.isMac)
+ this.launchNewWindow_ = this.appendMenuItem_('applaunchtypewindow');
+ this.launchFullscreen_ = this.appendMenuItem_('applaunchtypefullscreen');
+
+ var self = this;
+ this.forAllLaunchTypes_(function(launchTypeButton, id) {
+ launchTypeButton.addEventListener('activate',
+ self.onLaunchTypeChanged_.bind(self));
+ });
+
+ menu.appendChild(cr.ui.MenuItem.createSeparator());
+ this.options_ = this.appendMenuItem_('appoptions');
+ this.disableNotifications_ =
+ this.appendMenuItem_('appdisablenotifications');
+ this.uninstall_ = this.appendMenuItem_('appuninstall');
+ this.options_.addEventListener('activate',
+ this.onShowOptions_.bind(this));
+ this.disableNotifications_.addEventListener(
+ 'activate', this.onDisableNotifications_.bind(this));
+ this.uninstall_.addEventListener('activate',
+ this.onUninstall_.bind(this));
+
+ if (!cr.isMac && !cr.isChromeOS) {
+ menu.appendChild(cr.ui.MenuItem.createSeparator());
+ this.createShortcut_ = this.appendMenuItem_('appcreateshortcut');
+ this.createShortcut_.addEventListener(
+ 'activate', this.onCreateShortcut_.bind(this));
+ }
+
+ document.body.appendChild(menu);
+ },
+
+ /**
+ * Appends a menu item to |this.menu|.
+ * @param {?String} textId If non-null, the ID for the localized string
+ * that acts as the item's label.
+ */
+ appendMenuItem_: function(textId) {
+ var button = cr.doc.createElement('button');
+ this.menu.appendChild(button);
+ cr.ui.decorate(button, cr.ui.MenuItem);
+ if (textId)
+ button.textContent = loadTimeData.getString(textId);
+ return button;
+ },
+
+ /**
+ * Iterates over all the launch type menu items.
+ * @param {function(cr.ui.MenuItem, number)} f The function to call for each
+ * menu item. The parameters to the function include the menu item and
+ * the associated launch ID.
+ */
+ forAllLaunchTypes_: function(f) {
+ // Order matters: index matches launchType id.
+ var launchTypes = [this.launchPinnedTab_,
+ this.launchRegularTab_,
+ this.launchFullscreen_,
+ this.launchNewWindow_];
+
+ for (var i = 0; i < launchTypes.length; ++i) {
+ if (!launchTypes[i])
+ continue;
+
+ f(launchTypes[i], i);
+ }
+ },
+
+ /**
+ * Does all the necessary setup to show the menu for the given app.
+ * @param {App} app The App object that will be showing a context menu.
+ */
+ setupForApp: function(app) {
+ this.app_ = app;
+
+ this.launch_.textContent = app.appData.title;
+
+ this.forAllLaunchTypes_(function(launchTypeButton, id) {
+ launchTypeButton.disabled = false;
+ launchTypeButton.checked = app.appData.launch_type == id;
+ });
+
+ this.options_.disabled = !app.appData.optionsUrl || !app.appData.enabled;
+ this.uninstall_.disabled = !app.appData.mayDisable;
+
+ this.disableNotifications_.hidden = true;
+ var notificationsDisabled = app.appData.notifications_disabled;
+ if (typeof notificationsDisabled != 'undefined') {
+ this.disableNotifications_.hidden = false;
+ this.disableNotifications_.checked = notificationsDisabled;
+ }
+ },
+
+ /**
+ * Handlers for menu item activation.
+ * @param {Event} e The activation event.
+ * @private
+ */
+ onLaunch_: function(e) {
+ chrome.send('launchApp', [this.app_.appId, APP_LAUNCH.NTP_APPS_MENU]);
+ },
+ onLaunchTypeChanged_: function(e) {
+ var pressed = e.currentTarget;
+ var app = this.app_;
+ this.forAllLaunchTypes_(function(launchTypeButton, id) {
+ if (launchTypeButton == pressed) {
+ chrome.send('setLaunchType', [app.appId, id]);
+ // Manually update the launch type. We will only get
+ // appsPrefChangeCallback calls after changes to other NTP instances.
+ app.appData.launch_type = id;
+ }
+ });
+ },
+ onShowOptions_: function(e) {
+ window.location = this.app_.appData.optionsUrl;
+ },
+ onDisableNotifications_: function(e) {
+ var app = this.app_;
+ app.removeBubble();
+ // Toggle the current disable setting.
+ var newSetting = !this.disableNotifications_.checked;
+ app.appData.notifications_disabled = newSetting;
+ chrome.send('setNotificationsDisabled', [app.appData.id, newSetting]);
+ },
+ onUninstall_: function(e) {
+ chrome.send('uninstallApp', [this.app_.appData.id]);
+ },
+ onCreateShortcut_: function(e) {
+ chrome.send('createAppShortcut', [this.app_.appData.id]);
+ },
+ };
+
+ /**
+ * Creates a new App object.
+ * @param {Object} appData The data object that describes the app.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ function App(appData) {
+ var el = cr.doc.createElement('div');
+ el.__proto__ = App.prototype;
+ el.initialize(appData);
+
+ return el;
+ }
+
+ App.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ /**
+ * Initialize the app object.
+ * @param {Object} appData The data object that describes the app.
+ */
+ initialize: function(appData) {
+ this.appData = appData;
+ assert(this.appData_.id, 'Got an app without an ID');
+ this.id = this.appData_.id;
+
+ this.className = 'app focusable';
+
+ if (!this.appData_.icon_big_exists && this.appData_.icon_small_exists)
+ this.useSmallIcon_ = true;
+
+ this.appContents_ = this.useSmallIcon_ ?
+ $('app-small-icon-template').cloneNode(true) :
+ $('app-large-icon-template').cloneNode(true);
+ this.appContents_.id = '';
+ this.appendChild(this.appContents_);
+
+ this.appImgContainer_ = this.querySelector('.app-img-container');
+ this.appImg_ = this.appImgContainer_.querySelector('img');
+ this.setIcon();
+
+ if (this.useSmallIcon_) {
+ this.imgDiv_ = this.querySelector('.app-icon-div');
+ this.addLaunchClickTarget_(this.imgDiv_);
+ this.imgDiv_.title = this.appData_.title;
+ chrome.send('getAppIconDominantColor', [this.id]);
+ } else {
+ this.addLaunchClickTarget_(this.appImgContainer_);
+ this.appImgContainer_.title = this.appData_.title;
+ }
+
+ var appSpan = this.appContents_.querySelector('.title');
+ appSpan.textContent = appSpan.title = this.appData_.title;
+ this.addLaunchClickTarget_(appSpan);
+
+ var notification = this.appData_.notification;
+ var hasNotification = typeof notification != 'undefined' &&
+ typeof notification['title'] != 'undefined' &&
+ typeof notification['body'] != 'undefined' &&
+ !this.appData_.notifications_disabled;
+ if (hasNotification)
+ this.setupNotification_(notification);
+
+ this.addEventListener('keydown', cr.ui.contextMenuHandler);
+ this.addEventListener('keyup', cr.ui.contextMenuHandler);
+
+ // This hack is here so that appContents.contextMenu will be the same as
+ // this.contextMenu.
+ var self = this;
+ this.appContents_.__defineGetter__('contextMenu', function() {
+ return self.contextMenu;
+ });
+ this.appContents_.addEventListener('contextmenu',
+ cr.ui.contextMenuHandler);
+
+ if (this.appData_.is_webstore)
+ this.createAppsPromoExtras_();
+
+ this.addEventListener('mousedown', this.onMousedown_, true);
+ this.addEventListener('keydown', this.onKeydown_);
+ this.addEventListener('blur', this.onBlur_, true);
+ },
+
+ /**
+ * Sets the color of the favicon dominant color bar.
+ * @param {string} color The css-parsable value for the color.
+ */
+ set stripeColor(color) {
+ this.querySelector('.color-stripe').style.backgroundColor = color;
+ },
+
+ /**
+ * Removes the app tile from the page. Should be called after the app has
+ * been uninstalled.
+ */
+ remove: function(opt_animate) {
+ // Unset the ID immediately, because the app is already gone. But leave
+ // the tile on the page as it animates out.
+ this.id = '';
+ this.tile.doRemove(opt_animate);
+ },
+
+ /**
+ * Set the URL of the icon from |appData_|. This won't actually show the
+ * icon until loadIcon() is called (for performance reasons; we don't want
+ * to load icons until we have to).
+ */
+ setIcon: function() {
+ var src = this.useSmallIcon_ ? this.appData_.icon_small :
+ this.appData_.icon_big;
+ if (!this.appData_.enabled ||
+ (!this.appData_.offlineEnabled && !navigator.onLine)) {
+ src += '?grayscale=true';
+ }
+
+ this.appImgSrc_ = src;
+ this.classList.add('icon-loading');
+ },
+
+ /**
+ * Shows the icon for the app. That is, it causes chrome to load the app
+ * icon resource.
+ */
+ loadIcon: function() {
+ if (this.appImgSrc_) {
+ this.appImg_.src = this.appImgSrc_;
+ this.appImg_.classList.remove('invisible');
+ this.appImgSrc_ = null;
+ }
+
+ this.classList.remove('icon-loading');
+ },
+
+ /**
+ * Creates a bubble node.
+ * @param {Object} notification The notification to show in the bubble.
+ * @param {boolean} full Whether we want the headline or just the content.
+ * @private
+ */
+ createBubbleNode_: function(notification, full) {
+ if (!full) {
+ var titleItem = this.ownerDocument.createElement('span');
+ titleItem.textContent = notification['title'];
+ return titleItem;
+ } else {
+ var container = this.ownerDocument.createElement('div');
+
+ var messageItem = this.ownerDocument.createElement('div');
+ messageItem.textContent = notification['body'];
+ container.appendChild(messageItem);
+
+ if (notification['linkUrl'] && notification['linkText']) {
+ var anchor = this.ownerDocument.createElement('a');
+ anchor.href = notification['linkUrl'];
+ anchor.textContent = notification['linkText'];
+ container.appendChild(anchor);
+ }
+
+ return container;
+ }
+ },
+
+ /**
+ * Sets up a notification for the app icon.
+ * @param {Object} notification The notification to show in the bubble.
+ * @private
+ */
+ setupNotification_: function(notification) {
+ if (notification) {
+ var infoBubble;
+ if (!this.currentBubbleShowing_) {
+ // Create a new bubble.
+ infoBubble = new cr.ui.ExpandableBubble;
+ infoBubble.anchorNode = this;
+ infoBubble.appId = this.appData_.id;
+ infoBubble.handleCloseEvent = function() {
+ chrome.send('closeNotification', [this.appId]);
+ infoBubble.hide();
+ };
+ } else {
+ // Reuse the old bubble instead of popping up a new bubble over
+ // the old one.
+ infoBubble = this.currentBubbleShowing_;
+ infoBubble.collapseBubble_();
+ }
+ infoBubble.contentTitle = this.createBubbleNode_(notification, false);
+ infoBubble.content = this.createBubbleNode_(notification, true);
+ infoBubble.show();
+ infoBubble.resizeAndReposition();
+
+ this.currentBubbleShowing_ = infoBubble;
+ }
+ },
+
+ /**
+ * Removes the info bubble if there is one.
+ */
+ removeBubble: function() {
+ if (this.currentBubbleShowing_) {
+ this.currentBubbleShowing_.hide();
+ this.currentBubbleShowing_ = null;
+ }
+ },
+
+ /**
+ * Creates the apps-promo section of the app (should only be called for the
+ * webstore app).
+ * @private
+ */
+ createAppsPromoExtras_: function() {
+ this.classList.add('webstore');
+
+ this.appsPromoExtras_ = $('apps-promo-extras-template').cloneNode(true);
+ this.appsPromoExtras_.id = '';
+ this.appsPromoHeading_ =
+ this.appsPromoExtras_.querySelector('.apps-promo-heading');
+ this.appsPromoLink_ =
+ this.appsPromoExtras_.querySelector('.apps-promo-link');
+ this.appsPromoLink_.addEventListener('click', this.onClick_.bind(this));
+
+ this.appsPromoLogo_ = this.ownerDocument.createElement('img');
+ this.appsPromoLogo_.className = 'apps-promo-logo';
+ this.appImgContainer_.appendChild(this.appsPromoLogo_);
+
+ this.appendChild(this.appsPromoExtras_);
+ },
+
+ /**
+ * Sets the apps promo appearance. If |data| is null, there is no promo. If
+ * |data| is non-null, it contains strings to be shown for the promo. The
+ * promo is only shown when the webstore app icon is alone on a page.
+ * @param {Object} data A dictionary that contains apps promo strings.
+ */
+ setAppsPromoData: function(data) {
+ if (data) {
+ this.classList.add('has-promo');
+ } else {
+ this.classList.remove('has-promo');
+ return;
+ }
+
+ this.appsPromoHeading_.textContent = data.promoHeader;
+ this.appsPromoLink_.href = data.promoLink;
+ this.appsPromoLink_.textContent = data.promoButton;
+ this.appsPromoLogo_.src = data.promoLogo;
+ },
+
+ /**
+ * Set the size and position of the app tile.
+ * @param {number} size The total size of |this|.
+ * @param {number} x The x-position.
+ * @param {number} y The y-position.
+ * animate.
+ */
+ setBounds: function(size, x, y) {
+ var imgSize = size * APP_IMG_SIZE_FRACTION;
+ this.appImgContainer_.style.width = this.appImgContainer_.style.height =
+ toCssPx(this.useSmallIcon_ ? 16 : imgSize);
+ if (this.useSmallIcon_) {
+ // 3/4 is the ratio of 96px to 128px (the used height and full height
+ // of icons in apps).
+ var iconSize = imgSize * 3 / 4;
+ // The -2 is for the div border to improve the visual alignment for the
+ // icon div.
+ this.imgDiv_.style.width = this.imgDiv_.style.height =
+ toCssPx(iconSize - 2);
+ // Margins set to get the icon placement right and the text to line up.
+ this.imgDiv_.style.marginTop = this.imgDiv_.style.marginBottom =
+ toCssPx((imgSize - iconSize) / 2);
+ }
+
+ this.style.width = this.style.height = toCssPx(size);
+ this.style.left = toCssPx(x);
+ this.style.right = toCssPx(x);
+ this.style.top = toCssPx(y);
+
+ if (this.currentBubbleShowing_)
+ this.currentBubbleShowing_.resizeAndReposition();
+ },
+
+ /**
+ * Invoked when an app is clicked.
+ * @param {Event} e The click event.
+ * @private
+ */
+ onClick_: function(e) {
+ var is_promo = this.appsPromoExtras_ &&
+ window.getComputedStyle(this.appsPromoExtras_).display != 'none';
+ var url = !this.appData_.is_webstore ? '' :
+ is_promo ? this.appsPromoLink_.href :
+ appendParam(this.appData_.url,
+ 'utm_source',
+ 'chrome-ntp-icon');
+
+ chrome.send('launchApp',
+ [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, url,
+ e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
+
+ // Don't allow the click to trigger a link or anything
+ e.preventDefault();
+ },
+
+ /**
+ * Invoked when the user presses a key while the app is focused.
+ * @param {Event} e The key event.
+ * @private
+ */
+ onKeydown_: function(e) {
+ if (e.keyIdentifier == 'Enter') {
+ chrome.send('launchApp',
+ [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, '',
+ 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ /**
+ * Adds a node to the list of targets that will launch the app. This list
+ * is also used in onMousedown to determine whether the app contents should
+ * be shown as active (if we don't do this, then clicking anywhere in
+ * appContents, even a part that is outside the ideally clickable region,
+ * will cause the app icon to look active).
+ * @param {HTMLElement} node The node that should be clickable.
+ */
+ addLaunchClickTarget_: function(node) {
+ node.classList.add('launch-click-target');
+ node.addEventListener('click', this.onClick_.bind(this));
+ },
+
+ /**
+ * Handler for mousedown on the App. Adds a class that allows us to
+ * not display as :active for right clicks and clicks on app notifications
+ * (specifically, don't pulse on these occasions). Also, we don't pulse
+ * for clicks that aren't within the clickable regions.
+ * @param {Event} e The mousedown event.
+ */
+ onMousedown_: function(e) {
+ if (e.button == 2 ||
+ !findAncestorByClass(e.target, 'launch-click-target')) {
+ this.appContents_.classList.add('suppress-active');
+ } else {
+ this.appContents_.classList.remove('suppress-active');
+ }
+
+ // This class is here so we don't show the focus state for apps that
+ // gain keyboard focus via mouse clicking.
+ this.classList.add('click-focus');
+ },
+
+ /**
+ * This app is losing keyboard focus.
+ * @param {Event} e The event.
+ */
+ onBlur_: function(e) {
+ this.classList.remove('click-focus');
+ },
+
+ /**
+ * Change the appData and update the appearance of the app.
+ * @param {Object} appData The new data object that describes the app.
+ */
+ replaceAppData: function(appData) {
+ this.appData_ = appData;
+ this.setIcon();
+ this.loadIcon();
+ },
+
+ /**
+ * The data and preferences for this app.
+ * @type {Object}
+ */
+ set appData(data) {
+ this.appData_ = data;
+ },
+ get appData() {
+ return this.appData_;
+ },
+
+ get appId() {
+ return this.appData_.id;
+ },
+
+ /**
+ * Returns a pointer to the context menu for this app. All apps share the
+ * singleton AppContextMenu. This function is called by the
+ * ContextMenuHandler in response to the 'contextmenu' event.
+ * @type {cr.ui.Menu}
+ */
+ get contextMenu() {
+ var menu = AppContextMenu.getInstance();
+ menu.setupForApp(this);
+ return menu.menu;
+ },
+
+ /**
+ * Returns whether this element can be 'removed' from chrome (i.e. whether
+ * the user can drag it onto the trash and expect something to happen).
+ * @return {boolean} True if the app can be uninstalled.
+ */
+ canBeRemoved: function() {
+ return this.appData_.mayDisable;
+ },
+
+ /**
+ * Uninstalls the app after it's been dropped on the trash.
+ */
+ removeFromChrome: function() {
+ chrome.send('uninstallApp', [this.appData_.id, true]);
+ this.tile.tilePage.removeTile(this.tile, true);
+ if (this.currentBubbleShowing_)
+ currentBubbleShowing_.hide();
+ },
+
+ /**
+ * Called when a drag is starting on the tile. Updates dataTransfer with
+ * data for this tile.
+ */
+ setDragData: function(dataTransfer) {
+ dataTransfer.setData('Text', this.appData_.title);
+ dataTransfer.setData('URL', this.appData_.url);
+ },
+ };
+
+ var TilePage = ntp.TilePage;
+
+ // The fraction of the app tile size that the icon uses.
+ var APP_IMG_SIZE_FRACTION = 4 / 5;
+
+ var appsPageGridValues = {
+ // The fewest tiles we will show in a row.
+ minColCount: 3,
+ // The most tiles we will show in a row.
+ maxColCount: 6,
+
+ // The smallest a tile can be.
+ minTileWidth: 64 / APP_IMG_SIZE_FRACTION,
+ // The biggest a tile can be.
+ maxTileWidth: 128 / APP_IMG_SIZE_FRACTION,
+
+ // The padding between tiles, as a fraction of the tile width.
+ tileSpacingFraction: 1 / 8,
+ };
+ TilePage.initGridValues(appsPageGridValues);
+
+ /**
+ * Creates a new AppsPage object.
+ * @constructor
+ * @extends {TilePage}
+ */
+ function AppsPage() {
+ var el = new TilePage(appsPageGridValues);
+ el.__proto__ = AppsPage.prototype;
+ el.initialize();
+
+ return el;
+ }
+
+ AppsPage.prototype = {
+ __proto__: TilePage.prototype,
+
+ initialize: function() {
+ this.classList.add('apps-page');
+
+ this.addEventListener('cardselected', this.onCardSelected_);
+ // Add event listeners for two events, so we can temporarily suppress
+ // the app notification bubbles when the app card slides in and out of
+ // view.
+ this.addEventListener('carddeselected', this.onCardDeselected_);
+ this.addEventListener('cardSlider:card_change_ended',
+ this.onCardChangeEnded_);
+
+ this.addEventListener('tilePage:tile_added', this.onTileAdded_);
+
+ this.content_.addEventListener('scroll', this.onScroll_.bind(this));
+ },
+
+ /**
+ * Highlight a newly installed app as it's added to the NTP.
+ * @param {Object} appData The data object that describes the app.
+ */
+ insertAndHighlightApp: function(appData) {
+ ntp.getCardSlider().selectCardByValue(this);
+ this.content_.scrollTop = this.content_.scrollHeight;
+ this.insertApp(appData, true);
+ },
+
+ /**
+ * Similar to appendApp, but it respects the app_launch_ordinal field of
+ * |appData|.
+ * @param {Object} appData The data that describes the app.
+ * @param {boolean} animate Whether to animate the insertion.
+ */
+ insertApp: function(appData, animate) {
+ var index = this.tileElements_.length;
+ for (var i = 0; i < this.tileElements_.length; i++) {
+ if (appData.app_launch_ordinal <
+ this.tileElements_[i].firstChild.appData.app_launch_ordinal) {
+ index = i;
+ break;
+ }
+ }
+
+ this.addTileAt(new App(appData), index, animate);
+ },
+
+ /**
+ * Handler for 'cardselected' event, fired when |this| is selected. The
+ * first time this is called, we load all the app icons.
+ * @private
+ */
+ onCardSelected_: function(e) {
+ var apps = this.querySelectorAll('.app.icon-loading');
+ for (var i = 0; i < apps.length; i++) {
+ apps[i].loadIcon();
+ if (apps[i].currentBubbleShowing_)
+ apps[i].currentBubbleShowing_.suppressed = false;
+ }
+ },
+
+ /**
+ * Handler for tile additions to this page.
+ * @param {Event} e The tilePage:tile_added event.
+ */
+ onTileAdded_: function(e) {
+ assert(e.currentTarget == this);
+ assert(e.addedTile.firstChild instanceof App);
+ if (this.classList.contains('selected-card'))
+ e.addedTile.firstChild.loadIcon();
+ },
+
+ /**
+ * Handler for the when this.cardSlider ends change its card. If animated,
+ * this happens when the -webkit-transition is done, otherwise happens
+ * immediately (but after cardSlider:card_changed).
+ * @private
+ */
+ onCardChangeEnded_: function(e) {
+ for (var i = 0; i < this.tileElements_.length; i++) {
+ var app = this.tileElements_[i].firstChild;
+ assert(app instanceof App);
+ if (app.currentBubbleShowing_)
+ app.currentBubbleShowing_.suppressed = false;
+ }
+ },
+
+ /**
+ * Handler for the 'carddeselected' event, fired when the user switches
+ * to another 'card' than the App 'card' on the NTP (|this| gets
+ * deselected).
+ * @private
+ */
+ onCardDeselected_: function(e) {
+ for (var i = 0; i < this.tileElements_.length; i++) {
+ var app = this.tileElements_[i].firstChild;
+ assert(app instanceof App);
+ if (app.currentBubbleShowing_)
+ app.currentBubbleShowing_.suppressed = true;
+ }
+ },
+
+ /**
+ * A handler for when the apps page is scrolled (then we need to reposition
+ * the bubbles.
+ * @private
+ */
+ onScroll_: function(e) {
+ if (!this.selected)
+ return;
+ for (var i = 0; i < this.tileElements_.length; i++) {
+ var app = this.tileElements_[i].firstChild;
+ assert(app instanceof App);
+ if (app.currentBubbleShowing_)
+ app.currentBubbleShowing_.resizeAndReposition();
+ }
+ },
+
+ /** @inheritDoc */
+ doDragOver: function(e) {
+ // Only animatedly re-arrange if the user is currently dragging an app.
+ var tile = ntp.getCurrentlyDraggingTile();
+ if (tile && tile.querySelector('.app')) {
+ TilePage.prototype.doDragOver.call(this, e);
+ } else {
+ e.preventDefault();
+ this.setDropEffect(e.dataTransfer);
+ }
+ },
+
+ /** @inheritDoc */
+ shouldAcceptDrag: function(e) {
+ if (ntp.getCurrentlyDraggingTile())
+ return true;
+ if (!e.dataTransfer || !e.dataTransfer.types)
+ return false;
+ return Array.prototype.indexOf.call(e.dataTransfer.types,
+ 'text/uri-list') != -1;
+ },
+
+ /** @inheritDoc */
+ addDragData: function(dataTransfer, index) {
+ var sourceId = -1;
+ var currentlyDraggingTile = ntp.getCurrentlyDraggingTile();
+ if (currentlyDraggingTile) {
+ var tileContents = currentlyDraggingTile.firstChild;
+ if (tileContents.classList.contains('app')) {
+ var originalPage = currentlyDraggingTile.tilePage;
+ var samePageDrag = originalPage == this;
+ sourceId = samePageDrag ? DRAG_SOURCE.SAME_APPS_PANE :
+ DRAG_SOURCE.OTHER_APPS_PANE;
+ this.tileGrid_.insertBefore(currentlyDraggingTile,
+ this.tileElements_[index]);
+ this.tileMoved(currentlyDraggingTile);
+ if (!samePageDrag) {
+ originalPage.fireRemovedEvent(currentlyDraggingTile, index, true);
+ this.fireAddedEvent(currentlyDraggingTile, index, true);
+ }
+ } else if (currentlyDraggingTile.querySelector('.most-visited')) {
+ this.generateAppForLink(tileContents.data);
+ sourceId = DRAG_SOURCE.MOST_VISITED_PANE;
+ }
+ } else {
+ this.addOutsideData_(dataTransfer);
+ sourceId = DRAG_SOURCE.OUTSIDE_NTP;
+ }
+
+ assert(sourceId != -1);
+ chrome.send('metricsHandler:recordInHistogram',
+ ['NewTabPage.AppsPageDragSource', sourceId, DRAG_SOURCE_LIMIT]);
+ },
+
+ /**
+ * Adds drag data that has been dropped from a source that is not a tile.
+ * @param {Object} dataTransfer The data transfer object that holds drop
+ * data.
+ * @private
+ */
+ addOutsideData_: function(dataTransfer) {
+ var url = dataTransfer.getData('url');
+ assert(url);
+
+ // If the dataTransfer has html data, use that html's text contents as the
+ // title of the new link.
+ var html = dataTransfer.getData('text/html');
+ var title;
+ if (html) {
+ // It's important that we don't attach this node to the document
+ // because it might contain scripts.
+ var node = this.ownerDocument.createElement('div');
+ node.innerHTML = html;
+ title = node.textContent;
+ }
+
+ // Make sure title is >=1 and <=45 characters for Chrome app limits.
+ if (!title)
+ title = url;
+ if (title.length > 45)
+ title = title.substring(0, 45);
+ var data = {url: url, title: title};
+
+ // Synthesize an app.
+ this.generateAppForLink(data);
+ },
+
+ /**
+ * Creates a new crx-less app manifest and installs it.
+ * @param {Object} data The data object describing the link. Must have |url|
+ * and |title| members.
+ */
+ generateAppForLink: function(data) {
+ assert(data.url != undefined);
+ assert(data.title != undefined);
+ var pageIndex = ntp.getAppsPageIndex(this);
+ chrome.send('generateAppForLink', [data.url, data.title, pageIndex]);
+ },
+
+ /** @inheritDoc */
+ tileMoved: function(draggedTile) {
+ if (!(draggedTile.firstChild instanceof App))
+ return;
+
+ var pageIndex = ntp.getAppsPageIndex(this);
+ chrome.send('setPageIndex', [draggedTile.firstChild.appId, pageIndex]);
+
+ var appIds = [];
+ for (var i = 0; i < this.tileElements_.length; i++) {
+ var tileContents = this.tileElements_[i].firstChild;
+ if (tileContents instanceof App)
+ appIds.push(tileContents.appId);
+ }
+
+ chrome.send('reorderApps', [draggedTile.firstChild.appId, appIds]);
+ },
+
+ /** @inheritDoc */
+ setDropEffect: function(dataTransfer) {
+ var tile = ntp.getCurrentlyDraggingTile();
+ if (tile && tile.querySelector('.app'))
+ ntp.setCurrentDropEffect(dataTransfer, 'move');
+ else
+ ntp.setCurrentDropEffect(dataTransfer, 'copy');
+ },
+ };
+
+ AppsPage.setPromo = function(data) {
+ var store = document.querySelector('.webstore');
+ if (store)
+ store.setAppsPromoData(data);
+ };
+
+ /**
+ * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
+ * histogram. This should only be invoked from the AppLauncherHandler.
+ * @param {String} appID The ID of the app.
+ */
+ function launchAppAfterEnable(appId) {
+ chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
+ }
+
+ function appNotificationChanged(id, notification) {
+ var app = $(id);
+ // The app might have been uninstalled, or notifications might be disabled.
+ if (app && !app.appData.notifications_disabled)
+ app.setupNotification_(notification);
+ }
+
+ return {
+ APP_LAUNCH: APP_LAUNCH,
+ appNotificationChanged: appNotificationChanged,
+ AppsPage: AppsPage,
+ launchAppAfterEnable: launchAppAfterEnable,
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/dot_list.js b/chrome/browser/resources/ntp_search/dot_list.js
new file mode 100644
index 0000000..b8e0f11
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/dot_list.js
@@ -0,0 +1,80 @@
+// Copyright (c) 2012 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 DotList implementation
+ */
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ /**
+ * Live list of the navigation dots.
+ * @type {!NodeList|undefined}
+ */
+ var navDots;
+
+ /**
+ * Creates a new DotList object.
+ * @constructor
+ * @extends {HTMLUListElement}
+ */
+ var DotList = cr.ui.define('ul');
+
+ DotList.prototype = {
+ __proto__: HTMLUListElement.prototype,
+
+ /** @inheritDoc */
+ decorate: function() {
+ this.addEventListener('keydown', this.onKeyDown_.bind(this));
+ navDots = this.getElementsByClassName('dot');
+ },
+
+ /**
+ * Live list of the navigation dots.
+ * @type {!NodeList|undefined}
+ */
+ get dots() {
+ return navDots;
+ },
+
+ /**
+ * Handler for key events on the dot list. These keys will change the focus
+ * element.
+ * @param {Event} e The KeyboardEvent.
+ */
+ onKeyDown_: function(e) {
+ if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey)
+ return;
+
+ var direction = 0;
+ if (e.keyIdentifier == 'Left')
+ direction = -1;
+ else if (e.keyIdentifier == 'Right')
+ direction = 1;
+ else
+ return;
+
+ var focusDot = this.querySelector('.dot:focus');
+ if (!focusDot)
+ return;
+ var focusIndex = Array.prototype.indexOf.call(navDots, focusDot);
+ var newFocusIndex = focusIndex + direction;
+ if (focusIndex == newFocusIndex)
+ return;
+
+ newFocusIndex = (newFocusIndex + navDots.length) % navDots.length;
+ navDots[newFocusIndex].tabIndex = 3;
+ navDots[newFocusIndex].focus();
+ focusDot.tabIndex = -1;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+
+ return {
+ DotList: DotList
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/footer_menu.css b/chrome/browser/resources/ntp_search/footer_menu.css
new file mode 100644
index 0000000..96c6fae
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/footer_menu.css
@@ -0,0 +1,160 @@
+/* Copyright (c) 2012 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. */
+
+.footer-menu-button {
+ -webkit-appearance: none;
+ -webkit-padding-end: 15px;
+ -webkit-padding-start: 9px;
+ -webkit-transition: opacity 200ms;
+ -webkit-transition-delay: 100ms;
+ background: none;
+ border: 0;
+ color: inherit;
+ cursor: pointer;
+ display: block;
+ font: inherit;
+ height: 100%;
+ margin: 0;
+ /* The padding increases the clickable area. */
+ padding-bottom: 0;
+ padding-top: 0;
+ white-space: nowrap;
+}
+
+.footer-menu-button:hover:not([menu-shown]) {
+ color: #666;
+}
+
+.footer-menu-button:hover:not([menu-shown]) .disclosure-triangle {
+ background-color: #666;
+}
+
+.footer-menu-button[menu-shown] {
+ color: #555;
+}
+
+.footer-menu-button[menu-shown] .disclosure-triangle {
+ background-color: #555;
+}
+
+.footer-menu {
+ -webkit-margin-end: 10px;
+ max-height: 400px;
+ overflow: auto;
+ padding: 6px 8px;
+ /* Needs to be above #footer. */
+ z-index: 10;
+}
+
+.footer-menu,
+.footer-menu-context-menu {
+ min-width: 150px;
+}
+
+/* TODO(dubroy): Unify this with tile page scrollbar. */
+.footer-menu::-webkit-scrollbar-button {
+ display: none;
+}
+
+.footer-menu::-webkit-scrollbar {
+ width: 8px;
+}
+
+.footer-menu::-webkit-scrollbar-thumb {
+ background-color: #D9D9D9;
+ border: 2px solid white;
+}
+
+.footer-menu-item {
+ -webkit-margin-end: 8px;
+ -webkit-margin-start: 0;
+ -webkit-padding-end: 0;
+ -webkit-padding-start: 22px;
+ background: no-repeat 0 50%;
+ background-color: transparent !important;
+ background-size: 16px 16px;
+ box-sizing: border-box;
+ display: block;
+ line-height: 1.5em;
+ margin-bottom: 0.5em;
+ margin-top: 0.5em;
+ max-width: 450px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.footer-menu-item:not(:hover) {
+ text-decoration: none;
+}
+
+.footer-menu-item:first-of-type {
+ margin-top: 0.25em;
+}
+
+.footer-menu-item:last-of-type {
+ margin-bottom: 0.25em;
+}
+
+.footer-menu section {
+ padding: 0;
+}
+
+.footer-menu section h3 {
+ color: black;
+ font-size: 1em;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+}
+
+.footer-menu section:first-of-type h3 {
+ margin-top: 0.25em;
+}
+
+/* Used to add additional details to a section header */
+.footer-menu section h3 .details {
+ color: rgb(151, 156, 160);
+ font-style: italic;
+ font-weight: normal;
+}
+
+.footer-menu section h3 .details:before {
+ content: '\2013'; /* En-dash character. */
+ margin: 0 0.5em;
+}
+
+html[dir='rtl'] .footer-menu-item {
+ background: no-repeat 100% 50%;
+}
+
+.recent-window {
+ background-image: url('images/closed_window.png');
+}
+
+/* TODO(estade): find a better color for active. */
+.footer-menu-item:active,
+.footer-menu-item:visited,
+.footer-menu-item:link {
+ color: hsl(213, 90%, 24%) !important;
+}
+
+.disclosure-triangle {
+ -webkit-margin-start: 2px;
+ -webkit-mask-image: url('images/disclosure_triangle_mask.png');
+ background-color: #7F7F7F;
+ display: inline-block;
+ height: 9px;
+ width: 9px;
+}
+
+.footer-menu-context-menu {
+ /* Needs to be above .footer-menu. */
+ z-index: 11;
+}
+
+.footer-menu hr {
+ background-color: rgb(217, 217, 217);
+ border: 0;
+ height: 1px;
+}
diff --git a/chrome/browser/resources/ntp_search/images/app_promo_button.png b/chrome/browser/resources/ntp_search/images/app_promo_button.png
new file mode 100644
index 0000000..9625a7f
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/images/app_promo_button.png
Binary files differ
diff --git a/chrome/browser/resources/ntp_search/images/closed_window.png b/chrome/browser/resources/ntp_search/images/closed_window.png
new file mode 100644
index 0000000..ba0c05a
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/images/closed_window.png
Binary files differ
diff --git a/chrome/browser/resources/ntp_search/images/disclosure_triangle_mask.png b/chrome/browser/resources/ntp_search/images/disclosure_triangle_mask.png
new file mode 100644
index 0000000..b034d57
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/images/disclosure_triangle_mask.png
Binary files differ
diff --git a/chrome/browser/resources/ntp_search/logging.js b/chrome/browser/resources/ntp_search/logging.js
new file mode 100644
index 0000000..dfd0b14
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/logging.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2012 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
+ * Logging info for benchmarking purposes. Should be the first js file included.
+ */
+
+/* Stack of events that has been logged. */
+var eventLog = [];
+
+/**
+ * Logs an event.
+ * @param {String} name The name of the event (can be any string).
+ * @param {boolean} shouldLogTime If true, the event is used for benchmarking
+ * and the time is logged. Otherwise, just push the event on the event
+ * stack.
+ */
+function logEvent(name, shouldLogTime) {
+ if (shouldLogTime)
+ chrome.send('metricsHandler:logEventTime', [name]);
+ eventLog.push([name, Date.now()]);
+}
+
+logEvent('Tab.NewTabScriptStart', true);
+window.addEventListener('load', function(e) {
+ logEvent('Tab.NewTabOnload', true);
+});
+document.addEventListener('DOMContentLoaded', function(e) {
+ logEvent('Tab.NewTabDOMContentLoaded', true);
+});
diff --git a/chrome/browser/resources/ntp_search/most_visited_page.css b/chrome/browser/resources/ntp_search/most_visited_page.css
new file mode 100644
index 0000000..a8c39f9
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/most_visited_page.css
@@ -0,0 +1,185 @@
+/* Copyright (c) 2012 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. */
+
+.most-visited {
+ position: absolute;
+ z-index: 0;
+}
+
+.most-visited {
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ position: absolute;
+ text-decoration: none;
+}
+
+.most-visited:focus {
+ outline: none;
+}
+
+.fills-parent {
+ bottom: 0;
+ display: -webkit-box;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+/* filler mode: hide everything except the thumbnail --- leave a grey rectangle
+ * in its place. */
+.filler * {
+ visibility: hidden;
+}
+
+.filler {
+ pointer-events: none;
+}
+
+.most-visited .close-button {
+ -webkit-transition: opacity 150ms;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 5;
+}
+
+html[dir=rtl] .most-visited .close-button {
+ left: 0;
+ right: auto;
+}
+
+.most-visited:hover .close-button {
+ -webkit-transition-delay: 500ms;
+ opacity: 1;
+}
+
+.most-visited .close-button:hover {
+ -webkit-transition: none;
+}
+
+.most-visited .favicon {
+ -webkit-margin-start: 5px;
+ background: no-repeat left 50%;
+ bottom: 7px;
+ box-sizing: border-box;
+ display: block;
+ height: 16px;
+ position: absolute;
+ width: 16px;
+}
+
+html[dir='rtl'] .most-visited .favicon {
+ background-position-x: right;
+}
+
+.most-visited .color-stripe {
+ border-bottom-left-radius: 3px 3px;
+ border-bottom-right-radius: 3px 3px;
+ /* Matches height of title. */
+ bottom: 23px;
+ height: 3px;
+ /* Matches padding-top of the title. */
+ margin-bottom: 8px;
+ position: absolute;
+ width: 100%;
+ z-index: 10;
+}
+
+.most-visited .title {
+ display: block;
+ height: 23px;
+ overflow: hidden;
+ padding-top: 8px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.thumbnail {
+ -webkit-transition: opacity 150ms;
+ background: no-repeat;
+ /* This shows for missing thumbnails. */
+ background-color: #eee;
+ background-size: 100%;
+ border-radius: 3px;
+ /* These max dimensions are not necessary, as the sizing logic in the .js
+ * should be sufficient, but they're here for extra insurance. We never want
+ * to scale a thumbnail larger than this size. */
+ max-height: 132px;
+ max-width: 212px;
+}
+
+.filler .thumbnail {
+ /* TODO(estade): there seems to be a webkit bug where this border is not
+ * always removed when it should be. Investigate. */
+ border: 1px solid;
+ visibility: visible;
+}
+
+.thumbnail-shield {
+ background: -webkit-linear-gradient(rgba(255, 255, 255, 0),
+ rgba(255, 255, 255, 0) 50%,
+ rgba(255, 255, 255, 0.9));
+ border-radius: 3px;
+}
+
+/* TODO(dbeam): Remove this when printing of -webkit-linear-gradient() works. */
+@media print {
+ .thumbnail-shield {
+ background: none;
+ }
+}
+
+.most-visited:focus .thumbnail,
+.most-visited:hover .thumbnail {
+ opacity: 0.95;
+}
+
+.most-visited:focus .thumbnail-shield,
+.most-visited:hover .thumbnail-shield,
+.most-visited:active .thumbnail-shield {
+ background: -webkit-linear-gradient(rgba(255, 255, 255, 0),
+ rgba(255, 255, 255, 0) 80%,
+ rgba(255, 255, 255, 0.9));
+}
+
+/* The thumbnail gets lighter when clicked, but not when the click is on the
+ * close button. */
+.most-visited:active .close-button:not(:active) + .thumbnail {
+ opacity: 0.9;
+}
+
+/* The thumbnail gets a shadow when clicked, but not when the click is on the
+ * close button. */
+.most-visited:active .close-button:not(:active) + .thumbnail .thumbnail-shield {
+ -webkit-box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.2);
+}
+
+.thumbnail-wrapper {
+ -webkit-box-flex: 1;
+ -webkit-transition: background-color 150ms;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ display: block;
+ position: relative;
+ z-index: 5;
+}
+
+.filler .thumbnail-wrapper {
+ visibility: visible;
+}
+
+/* 'finishing-drag' is the state we are in after dropping on the trash can.
+ * Override opacity of the tile to 1, so that the new tile animation
+ * occurs simultaneously with the trash animation. */
+.tile.dragging.finishing-drag {
+ opacity: 1;
+}
+
+/* Don't display the new tile until there's something to show. */
+.blacklisted {
+ opacity: 0;
+}
diff --git a/chrome/browser/resources/ntp_search/most_visited_page.js b/chrome/browser/resources/ntp_search/most_visited_page.js
new file mode 100644
index 0000000..8b005b9
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/most_visited_page.js
@@ -0,0 +1,470 @@
+// Copyright (c) 2012 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.
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ var TilePage = ntp.TilePage;
+
+ /**
+ * A counter for generating unique tile IDs.
+ */
+ var tileID = 0;
+
+ /**
+ * Creates a new Most Visited object for tiling.
+ * @constructor
+ * @extends {HTMLAnchorElement}
+ */
+ function MostVisited() {
+ var el = cr.doc.createElement('a');
+ el.__proto__ = MostVisited.prototype;
+ el.initialize();
+
+ return el;
+ }
+
+ MostVisited.prototype = {
+ __proto__: HTMLAnchorElement.prototype,
+
+ initialize: function() {
+ this.reset();
+
+ this.addEventListener('click', this.handleClick_);
+ this.addEventListener('keydown', this.handleKeyDown_);
+ },
+
+ get index() {
+ assert(this.tile);
+ return this.tile.index;
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ /**
+ * Clears the DOM hierarchy for this node, setting it back to the default
+ * for a blank thumbnail.
+ */
+ reset: function() {
+ this.className = 'most-visited filler real';
+ this.innerHTML =
+ '<span class="thumbnail-wrapper fills-parent">' +
+ '<div class="close-button"></div>' +
+ '<span class="thumbnail fills-parent">' +
+ // thumbnail-shield provides a gradient fade effect.
+ '<div class="thumbnail-shield fills-parent"></div>' +
+ '</span>' +
+ '<span class="favicon"></span>' +
+ '</span>' +
+ '<div class="color-stripe"></div>' +
+ '<span class="title"></span>';
+
+ this.querySelector('.close-button').title =
+ loadTimeData.getString('removethumbnailtooltip');
+
+ this.tabIndex = -1;
+ this.data_ = null;
+ this.removeAttribute('id');
+ this.title = '';
+ },
+
+ /**
+ * Update the appearance of this tile according to |data|.
+ * @param {Object} data A dictionary of relevant data for the page.
+ */
+ updateForData: function(data) {
+ if (this.classList.contains('blacklisted') && data) {
+ // Animate appearance of new tile.
+ this.classList.add('new-tile-contents');
+ }
+ this.classList.remove('blacklisted');
+
+ if (!data || data.filler) {
+ if (this.data_)
+ this.reset();
+ return;
+ }
+
+ var id = tileID++;
+ this.id = 'most-visited-tile-' + id;
+ this.data_ = data;
+ this.classList.add('focusable');
+
+ var faviconDiv = this.querySelector('.favicon');
+ var faviconUrl = 'chrome://favicon/size/16/' + data.url;
+ faviconDiv.style.backgroundImage = url(faviconUrl);
+ chrome.send('getFaviconDominantColor', [faviconUrl, this.id]);
+
+ var title = this.querySelector('.title');
+ title.textContent = data.title;
+ title.dir = data.direction;
+
+ // Sets the tooltip.
+ this.title = data.title;
+
+ var thumbnailUrl = 'chrome://thumb/' + data.url;
+ this.querySelector('.thumbnail').style.backgroundImage =
+ url(thumbnailUrl);
+
+ this.href = data.url;
+
+ this.classList.remove('filler');
+ },
+
+ /**
+ * Sets the color of the favicon dominant color bar.
+ * @param {string} color The css-parsable value for the color.
+ */
+ set stripeColor(color) {
+ this.querySelector('.color-stripe').style.backgroundColor = color;
+ },
+
+ /**
+ * Handles a click on the tile.
+ * @param {Event} e The click event.
+ */
+ handleClick_: function(e) {
+ if (e.target.classList.contains('close-button')) {
+ this.blacklist_();
+ e.preventDefault();
+ } else {
+ // Records an app launch from the most visited page (Chrome will decide
+ // whether the url is an app). TODO(estade): this only works for clicks;
+ // other actions like "open in new tab" from the context menu won't be
+ // recorded. Can this be fixed?
+ chrome.send('recordAppLaunchByURL',
+ [encodeURIComponent(this.href),
+ ntp.APP_LAUNCH.NTP_MOST_VISITED]);
+ // Records the index of this tile.
+ chrome.send('metricsHandler:recordInHistogram',
+ ['NewTabPage.MostVisited', this.index, 8]);
+ chrome.send('mostVisitedAction',
+ [ntp.NtpFollowAction.CLICKED_TILE]);
+ }
+ },
+
+ /**
+ * Allow blacklisting most visited site using the keyboard.
+ */
+ handleKeyDown_: function(e) {
+ if (!cr.isMac && e.keyCode == 46 || // Del
+ cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
+ this.blacklist_();
+ }
+ },
+
+ /**
+ * Permanently removes a page from Most Visited.
+ */
+ blacklist_: function() {
+ this.showUndoNotification_();
+ chrome.send('blacklistURLFromMostVisited', [this.data_.url]);
+ this.reset();
+ chrome.send('getMostVisited');
+ this.classList.add('blacklisted');
+ },
+
+ showUndoNotification_: function() {
+ var data = this.data_;
+ var self = this;
+ var doUndo = function() {
+ chrome.send('removeURLsFromMostVisitedBlacklist', [data.url]);
+ self.updateForData(data);
+ }
+
+ var undo = {
+ action: doUndo,
+ text: loadTimeData.getString('undothumbnailremove'),
+ };
+
+ var undoAll = {
+ action: function() {
+ chrome.send('clearMostVisitedURLsBlacklist');
+ },
+ text: loadTimeData.getString('restoreThumbnailsShort'),
+ };
+
+ ntp.showNotification(
+ loadTimeData.getString('thumbnailremovednotification'),
+ [undo, undoAll]);
+ },
+
+ /**
+ * Set the size and position of the most visited tile.
+ * @param {number} size The total size of |this|.
+ * @param {number} x The x-position.
+ * @param {number} y The y-position.
+ * animate.
+ */
+ setBounds: function(size, x, y) {
+ this.style.width = toCssPx(size);
+ this.style.height = toCssPx(heightForWidth(size));
+
+ this.style.left = toCssPx(x);
+ this.style.right = toCssPx(x);
+ this.style.top = toCssPx(y);
+ },
+
+ /**
+ * Returns whether this element can be 'removed' from chrome (i.e. whether
+ * the user can drag it onto the trash and expect something to happen).
+ * @return {boolean} True, since most visited pages can always be
+ * blacklisted.
+ */
+ canBeRemoved: function() {
+ return true;
+ },
+
+ /**
+ * Removes this element from chrome, i.e. blacklists it.
+ */
+ removeFromChrome: function() {
+ this.blacklist_();
+ this.parentNode.classList.add('finishing-drag');
+ },
+
+ /**
+ * Called when a drag of this tile has ended (after all animations have
+ * finished).
+ */
+ finalizeDrag: function() {
+ this.parentNode.classList.remove('finishing-drag');
+ },
+
+ /**
+ * Called when a drag is starting on the tile. Updates dataTransfer with
+ * data for this tile (for dragging outside of the NTP).
+ */
+ setDragData: function(dataTransfer) {
+ dataTransfer.setData('Text', this.data_.title);
+ dataTransfer.setData('URL', this.data_.url);
+ },
+ };
+
+ var mostVisitedPageGridValues = {
+ // The fewest tiles we will show in a row.
+ minColCount: 2,
+ // The most tiles we will show in a row.
+ maxColCount: 4,
+
+ // The smallest a tile can be.
+ minTileWidth: 122,
+ // The biggest a tile can be. 212 (max thumbnail width) + 2.
+ maxTileWidth: 214,
+
+ // The padding between tiles, as a fraction of the tile width.
+ tileSpacingFraction: 1 / 8,
+ };
+ TilePage.initGridValues(mostVisitedPageGridValues);
+
+ /**
+ * Calculates the height for a Most Visited tile for a given width. The size
+ * is based on the thumbnail, which should have a 212:132 ratio.
+ * @return {number} The height.
+ */
+ function heightForWidth(width) {
+ // The 2s are for borders, the 31 is for the title.
+ return (width - 2) * 132 / 212 + 2 + 31;
+ }
+
+ var THUMBNAIL_COUNT = 8;
+
+ /**
+ * Creates a new MostVisitedPage object.
+ * @constructor
+ * @extends {TilePage}
+ */
+ function MostVisitedPage() {
+ var el = new TilePage(mostVisitedPageGridValues);
+ el.__proto__ = MostVisitedPage.prototype;
+ el.initialize();
+
+ return el;
+ }
+
+ MostVisitedPage.prototype = {
+ __proto__: TilePage.prototype,
+
+ initialize: function() {
+ this.classList.add('most-visited-page');
+ this.data_ = null;
+ this.mostVisitedTiles_ = this.getElementsByClassName('most-visited real');
+
+ this.addEventListener('carddeselected', this.handleCardDeselected_);
+ this.addEventListener('cardselected', this.handleCardSelected_);
+ },
+
+ /**
+ * Create blank (filler) tiles.
+ * @private
+ */
+ createTiles_: function() {
+ for (var i = 0; i < THUMBNAIL_COUNT; i++) {
+ this.appendTile(new MostVisited());
+ }
+ },
+
+ /**
+ * Update the tiles after a change to |data_|.
+ */
+ updateTiles_: function() {
+ for (var i = 0; i < THUMBNAIL_COUNT; i++) {
+ var page = this.data_[i];
+ var tile = this.mostVisitedTiles_[i];
+
+ if (i >= this.data_.length)
+ tile.reset();
+ else
+ tile.updateForData(page);
+ }
+ },
+
+ /**
+ * Handles the 'card deselected' event (i.e. the user clicked to another
+ * pane).
+ * @param {Event} e The CardChanged event.
+ */
+ handleCardDeselected_: function(e) {
+ if (!document.documentElement.classList.contains('starting-up')) {
+ chrome.send('mostVisitedAction',
+ [ntp.NtpFollowAction.CLICKED_OTHER_NTP_PANE]);
+ }
+ },
+
+ /**
+ * Handles the 'card selected' event (i.e. the user clicked to select the
+ * Most Visited pane).
+ * @param {Event} e The CardChanged event.
+ */
+ handleCardSelected_: function(e) {
+ if (!document.documentElement.classList.contains('starting-up'))
+ chrome.send('mostVisitedSelected');
+ },
+
+ /**
+ * Array of most visited data objects.
+ * @type {Array}
+ */
+ get data() {
+ return this.data_;
+ },
+ set data(data) {
+ var startTime = Date.now();
+
+ // The first time data is set, create the tiles.
+ if (!this.data_) {
+ this.createTiles_();
+ this.data_ = data.slice(0, THUMBNAIL_COUNT);
+ } else {
+ this.data_ = refreshData(this.data_, data);
+ }
+
+ this.updateTiles_();
+ logEvent('mostVisited.layout: ' + (Date.now() - startTime));
+ },
+
+ /** @inheritDoc */
+ shouldAcceptDrag: function(e) {
+ return false;
+ },
+
+ /** @inheritDoc */
+ heightForWidth: heightForWidth,
+ };
+
+ /**
+ * Executed once the NTP has loaded. Checks if the Most Visited pane is
+ * shown or not. If it is shown, the 'mostVisitedSelected' message is sent
+ * to the C++ code, to record the fact that the user has seen this pane.
+ */
+ MostVisitedPage.onLoaded = function() {
+ if (ntp.getCardSlider() &&
+ ntp.getCardSlider().currentCardValue &&
+ ntp.getCardSlider().currentCardValue.classList
+ .contains('most-visited-page')) {
+ chrome.send('mostVisitedSelected');
+ }
+ }
+
+ /**
+ * We've gotten additional Most Visited data. Update our old data with the
+ * new data. The ordering of the new data is not important, except when a
+ * page is pinned. Thus we try to minimize re-ordering.
+ * @param {Array} oldData The current Most Visited page list.
+ * @param {Array} newData The new Most Visited page list.
+ * @return {Array} The merged page list that should replace the current page
+ * list.
+ */
+ function refreshData(oldData, newData) {
+ oldData = oldData.slice(0, THUMBNAIL_COUNT);
+ newData = newData.slice(0, THUMBNAIL_COUNT);
+
+ // Copy over pinned sites directly.
+ for (var j = 0; j < newData.length; j++) {
+ if (newData[j].pinned) {
+ oldData[j] = newData[j];
+ // Mark the entry as 'updated' so we don't try to update again.
+ oldData[j].updated = true;
+ // Mark the newData page as 'used' so we don't try to re-use it.
+ newData[j].used = true;
+ }
+ }
+
+ // Look through old pages; if they exist in the newData list, keep them
+ // where they are.
+ for (var i = 0; i < oldData.length; i++) {
+ if (!oldData[i] || oldData[i].updated)
+ continue;
+
+ for (var j = 0; j < newData.length; j++) {
+ if (newData[j].used)
+ continue;
+
+ if (newData[j].url == oldData[i].url) {
+ // The background image and other data may have changed.
+ oldData[i] = newData[j];
+ oldData[i].updated = true;
+ newData[j].used = true;
+ break;
+ }
+ }
+ }
+
+ // Look through old pages that haven't been updated yet; replace them.
+ for (var i = 0; i < oldData.length; i++) {
+ if (oldData[i] && oldData[i].updated)
+ continue;
+
+ for (var j = 0; j < newData.length; j++) {
+ if (newData[j].used)
+ continue;
+
+ oldData[i] = newData[j];
+ oldData[i].updated = true;
+ newData[j].used = true;
+ break;
+ }
+
+ if (oldData[i] && !oldData[i].updated)
+ oldData[i] = null;
+ }
+
+ // Clear 'updated' flags so this function will work next time it's called.
+ for (var i = 0; i < THUMBNAIL_COUNT; i++) {
+ if (oldData[i])
+ oldData[i].updated = false;
+ }
+
+ return oldData;
+ };
+
+ return {
+ MostVisitedPage: MostVisitedPage,
+ refreshData: refreshData,
+ };
+});
+
+document.addEventListener('ntpLoaded', ntp.MostVisitedPage.onLoaded);
diff --git a/chrome/browser/resources/ntp_search/nav_dot.css b/chrome/browser/resources/ntp_search/nav_dot.css
new file mode 100644
index 0000000..820aec3
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/nav_dot.css
@@ -0,0 +1,90 @@
+/* Copyright (c) 2012 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. */
+
+/* TODO(estade): handle overflow better? I tried overflow-x: hidden and
+ overflow-y: visible (for the new dot animation), but this makes a scroll
+ bar appear */
+#dot-list {
+ /* Expand to take up all available horizontal space. */
+ -webkit-box-flex: 1;
+ /* Center child dots. */
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+html.starting-up #dot-list {
+ display: none;
+}
+
+.dot {
+ -webkit-box-flex: 1;
+ -webkit-margin-end: 10px;
+ -webkit-padding-start: 2px;
+ -webkit-transition: max-width 250ms, -webkit-margin-end 250ms;
+ box-sizing: border-box;
+ cursor: pointer;
+ /* max-width: Set in new_tab.js. See measureNavDots() */
+ outline: none;
+ text-align: left;
+}
+
+.dot:last-child {
+ -webkit-margin-end: 0;
+}
+
+.dot.small {
+ -webkit-margin-end: 0;
+ max-width: 0;
+}
+
+.dot .selection-bar {
+ -webkit-transition: border-color 200ms;
+ border-bottom: 5px solid;
+ border-color: rgba(0, 0, 0, 0.1);
+ height: 10px;
+}
+
+.dot input {
+ -webkit-appearance: caret;
+ -webkit-margin-start: 2px;
+ -webkit-transition: color 200ms;
+ background-color: transparent;
+ cursor: inherit;
+ font: inherit;
+ height: auto;
+ margin-top: 2px;
+ padding: 1px 0;
+ width: 90%;
+}
+
+.dot input:focus {
+ cursor: auto;
+}
+
+/* Everything below here should be themed but we don't have appropriate colors
+ * yet.
+ */
+.dot input {
+ color: #b2b2b2;
+}
+
+.dot:focus input,
+.dot:hover input,
+.dot.selected input {
+ color: #7f7f7f;
+}
+
+.dot:focus .selection-bar,
+.dot:hover .selection-bar,
+.dot.drag-target .selection-bar {
+ border-color: #b2b2b2;
+}
+
+.dot.selected .selection-bar {
+ border-color: #7f7f7f;
+}
diff --git a/chrome/browser/resources/ntp_search/nav_dot.js b/chrome/browser/resources/ntp_search/nav_dot.js
new file mode 100644
index 0000000..72a2fbc
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/nav_dot.js
@@ -0,0 +1,276 @@
+// Copyright (c) 2012 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 Nav dot
+ * This is the class for the navigation controls that appear along the bottom
+ * of the NTP.
+ */
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ /**
+ * Creates a new navigation dot.
+ * @param {TilePage} page The associated TilePage.
+ * @param {string} title The title of the navigation dot.
+ * @param {bool} titleIsEditable If true, the title can be changed.
+ * @param {bool} animate If true, animates into existence.
+ * @constructor
+ * @extends {HTMLLIElement}
+ */
+ function NavDot(page, title, titleIsEditable, animate) {
+ var dot = cr.doc.createElement('li');
+ dot.__proto__ = NavDot.prototype;
+ dot.initialize(page, title, titleIsEditable, animate);
+
+ return dot;
+ }
+
+ NavDot.prototype = {
+ __proto__: HTMLLIElement.prototype,
+
+ initialize: function(page, title, titleIsEditable, animate) {
+ this.className = 'dot';
+ this.setAttribute('role', 'button');
+
+ this.page_ = page;
+
+ var selectionBar = this.ownerDocument.createElement('div');
+ selectionBar.className = 'selection-bar';
+ this.appendChild(selectionBar);
+
+ // TODO(estade): should there be some limit to the number of characters?
+ this.input_ = this.ownerDocument.createElement('input');
+ this.input_.setAttribute('spellcheck', false);
+ this.input_.value = title;
+ // Take the input out of the tab-traversal focus order.
+ this.input_.disabled = true;
+ this.appendChild(this.input_);
+
+ this.displayTitle = title;
+ this.titleIsEditable_ = titleIsEditable;
+
+ this.addEventListener('keydown', this.onKeyDown_);
+ this.addEventListener('click', this.onClick_);
+ this.addEventListener('dblclick', this.onDoubleClick_);
+ this.dragWrapper_ = new cr.ui.DragWrapper(this, this);
+ this.addEventListener('webkitTransitionEnd', this.onTransitionEnd_);
+
+ this.input_.addEventListener('blur', this.onInputBlur_.bind(this));
+ this.input_.addEventListener('mousedown',
+ this.onInputMouseDown_.bind(this));
+ this.input_.addEventListener('keydown', this.onInputKeyDown_.bind(this));
+
+ if (animate) {
+ this.classList.add('small');
+ var self = this;
+ window.setTimeout(function() {
+ self.classList.remove('small');
+ }, 0);
+ }
+ },
+
+ /**
+ * @return {TilePage} The associated TilePage.
+ */
+ get page() {
+ return this.page_;
+ },
+
+ /**
+ * Sets/gets the display title.
+ * @type {String} title The display name for this nav dot.
+ */
+ get displayTitle() {
+ return this.title;
+ },
+ set displayTitle(title) {
+ this.title = this.input_.value = title;
+ },
+
+ /**
+ * Removes the dot from the page. If |opt_animate| is truthy, we first
+ * transition the element to 0 width.
+ * @param {boolean=} opt_animate Whether to animate the removal or not.
+ */
+ remove: function(opt_animate) {
+ if (opt_animate)
+ this.classList.add('small');
+ else
+ this.parentNode.removeChild(this);
+ },
+
+ /**
+ * Navigates the card slider to the page for this dot.
+ */
+ switchToPage: function() {
+ ntp.getCardSlider().selectCardByValue(this.page_, true);
+ },
+
+ /**
+ * Handler for keydown event on the dot.
+ * @param {Event} e The KeyboardEvent.
+ */
+ onKeyDown_: function(e) {
+ if (e.keyIdentifier == 'Enter') {
+ this.onClick_(e);
+ e.stopPropagation();
+ }
+ },
+
+ /**
+ * Clicking causes the associated page to show.
+ * @param {Event} e The click event.
+ * @private
+ */
+ onClick_: function(e) {
+ this.switchToPage();
+ // The explicit focus call is necessary because of overriding the default
+ // handling in onInputMouseDown_.
+ if (this.ownerDocument.activeElement != this.input_)
+ this.focus();
+
+ chrome.send('introMessageDismissed');
+ e.stopPropagation();
+ },
+
+ /**
+ * Double clicks allow the user to edit the page title.
+ * @param {Event} e The click event.
+ * @private
+ */
+ onDoubleClick_: function(e) {
+ if (this.titleIsEditable_) {
+ this.input_.disabled = false;
+ this.input_.focus();
+ this.input_.select();
+ }
+ },
+
+ /**
+ * Prevent mouse down on the input from selecting it.
+ * @param {Event} e The click event.
+ * @private
+ */
+ onInputMouseDown_: function(e) {
+ if (this.ownerDocument.activeElement != this.input_)
+ e.preventDefault();
+ },
+
+ /**
+ * Handle keypresses on the input.
+ * @param {Event} e The click event.
+ * @private
+ */
+ onInputKeyDown_: function(e) {
+ switch (e.keyIdentifier) {
+ case 'U+001B': // Escape cancels edits.
+ this.input_.value = this.displayTitle;
+ case 'Enter': // Fall through.
+ this.input_.blur();
+ break;
+ }
+ },
+
+ /**
+ * When the input blurs, commit the edited changes.
+ * @param {Event} e The blur event.
+ * @private
+ */
+ onInputBlur_: function(e) {
+ window.getSelection().removeAllRanges();
+ this.displayTitle = this.input_.value;
+ ntp.saveAppPageName(this.page_, this.displayTitle);
+ this.input_.disabled = true;
+ },
+
+ shouldAcceptDrag: function(e) {
+ return this.page_.shouldAcceptDrag(e);
+ },
+
+ /**
+ * A drag has entered the navigation dot. If the user hovers long enough,
+ * we will navigate to the relevant page.
+ * @param {Event} e The MouseOver event for the drag.
+ * @private
+ */
+ doDragEnter: function(e) {
+ var self = this;
+ function navPageClearTimeout() {
+ self.switchToPage();
+ self.dragNavTimeout = null;
+ }
+ this.dragNavTimeout = window.setTimeout(navPageClearTimeout, 500);
+
+ this.doDragOver(e);
+ },
+
+ /**
+ * A dragged element has moved over the navigation dot. Show the correct
+ * indicator and prevent default handling so the <input> won't act as a drag
+ * target.
+ * @param {Event} e The MouseOver event for the drag.
+ * @private
+ */
+ doDragOver: function(e) {
+ e.preventDefault();
+
+ if (!this.dragWrapper_.isCurrentDragTarget)
+ ntp.setCurrentDropEffect(e.dataTransfer, 'none');
+ else
+ this.page_.setDropEffect(e.dataTransfer);
+ },
+
+ /**
+ * A dragged element has been dropped on the navigation dot. Tell the page
+ * to append it.
+ * @param {Event} e The MouseOver event for the drag.
+ * @private
+ */
+ doDrop: function(e) {
+ e.stopPropagation();
+ var tile = ntp.getCurrentlyDraggingTile();
+ if (tile && tile.tilePage != this.page_)
+ this.page_.appendDraggingTile();
+ // TODO(estade): handle non-tile drags.
+
+ this.cancelDelayedSwitch_();
+ },
+
+ /**
+ * The drag has left the navigation dot.
+ * @param {Event} e The MouseOver event for the drag.
+ * @private
+ */
+ doDragLeave: function(e) {
+ this.cancelDelayedSwitch_();
+ },
+
+ /**
+ * Cancels the timer for page switching.
+ * @private
+ */
+ cancelDelayedSwitch_: function() {
+ if (this.dragNavTimeout) {
+ window.clearTimeout(this.dragNavTimeout);
+ this.dragNavTimeout = null;
+ }
+ },
+
+ /**
+ * A transition has ended.
+ * @param {Event} e The transition end event.
+ * @private
+ */
+ onTransitionEnd_: function(e) {
+ if (e.propertyName === 'max-width' && this.classList.contains('small'))
+ this.parentNode.removeChild(this);
+ },
+ };
+
+ return {
+ NavDot: NavDot,
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/new_tab.css b/chrome/browser/resources/ntp_search/new_tab.css
new file mode 100644
index 0000000..cdaabae
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/new_tab.css
@@ -0,0 +1,447 @@
+/* Copyright (c) 2012 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. */
+
+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%;
+ overflow: hidden;
+}
+
+body {
+ /* 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;
+ background-size: auto 100%;
+ margin: 0;
+}
+
+/* [hidden] does display:none, but its priority is too low in some cases. */
+[hidden] {
+ display: none !important;
+}
+
+#notification-container {
+ -webkit-transition: opacity 200ms, margin-left 200ms;
+ bottom: 31px;
+ display: block;
+ float: left;
+ position: relative;
+ text-align: start;
+ z-index: 15;
+}
+
+html[dir='rtl'] #notification-container {
+ float: right;
+}
+
+#notification-container.card-changed {
+ -webkit-transition: none;
+ opacity: 0;
+}
+
+#notification-container.inactive {
+ -webkit-transition: opacity 200ms;
+ opacity: 0;
+}
+
+#notification {
+ display: inline-block;
+ font-weight: bold;
+}
+
+#notification > div > div,
+#notification > div {
+ display: inline-block;
+}
+
+/* NOTE: This is in the probable case that we start stuffing 16x16 data URI'd
+ * icons in the promo notification responses. */
+#notification > span > img {
+ margin-bottom: -3px;
+}
+
+#notification .close-button {
+ -webkit-margin-start: 0.5em;
+ vertical-align: middle;
+}
+
+.close-button {
+ background: no-repeat;
+ background-color: transparent;
+ /* TODO(estade): this should animate between states. */
+ background-image: -webkit-image-set(
+ url('../../../../ui/resources/default_100_percent/close_bar.png') 1x,
+ url('../../../../ui/resources/default_200_percent/close_bar.png') 2x);
+ border: 0;
+ cursor: default;
+ display: inline-block;
+ height: 16px;
+ padding: 0;
+ width: 16px;
+}
+
+.close-button:hover,
+.close-button:focus {
+ background-image: -webkit-image-set(
+ url('../../../../ui/resources/default_100_percent/close_bar_hover.png')
+ 1x,
+ url('../../../../ui/resources/default_200_percent/close_bar_hover.png')
+ 2x);
+}
+
+.close-button:active {
+ background-image: -webkit-image-set(
+ url('../../../../ui/resources/default_100_percent/close_bar_pressed.png')
+ 1x,
+ url('../../../../ui/resources/default_200_percent/close_bar_pressed.png')
+ 2x);
+}
+
+.link-button {
+ -webkit-margin-start: 0.5em;
+}
+
+#card-slider-frame {
+ /* Must match #footer height. */
+ bottom: 50px;
+ overflow: hidden;
+ /* We want this to fill the window except for the region used
+ by footer */
+ position: fixed;
+ top: 0;
+ width: 100%;
+}
+
+body.bare-minimum #card-slider-frame {
+ bottom: 0;
+}
+
+#page-list {
+ /* fill the apps-frame */
+ display: -webkit-box;
+ height: 100%;
+}
+
+#attribution {
+ bottom: 0;
+ left: auto;
+ margin-left: 8px;
+ /* Leave room for the scrollbar. */
+ margin-right: 13px;
+ position: absolute;
+ right: 0;
+ text-align: left;
+ z-index: -5;
+}
+
+/* For themes that right-align their images, we flip the attribution to the
+ * left to avoid conflicts. We also do this for bare-minimum mode since there
+ * can be conflicts with the recently closed menu. */
+html[themegravity='right'] #attribution,
+body.bare-minimum #attribution,
+html[dir='rtl'] #attribution {
+ left: 0;
+ right: auto;
+ text-align: right;
+}
+
+#attribution > span {
+ display: block;
+}
+
+#footer {
+ background-image: -webkit-linear-gradient(
+ rgba(242, 242, 242, 0.9), rgba(222, 222, 222, 0.9));
+ bottom: 0;
+ color: #7F7F7F;
+ font-size: 0.9em;
+ font-weight: bold;
+ overflow: hidden;
+ position: fixed;
+ width: 100%;
+ z-index: 5;
+}
+
+/* TODO(estade): remove this border hack and replace with a webkit-gradient
+ * border-image on #footer once WebKit supports border-image-slice.
+ * See https://bugs.webkit.org/show_bug.cgi?id=20127 */
+#footer-border {
+ height: 1px;
+}
+
+#footer-content {
+ -webkit-box-align: center;
+ display: -webkit-box;
+ height: 49px;
+}
+
+#footer-content > * {
+ margin: 0 9px;
+}
+
+#logo-img {
+ margin-top: 4px;
+}
+
+body.bare-minimum #footer {
+ background: transparent;
+ bottom: auto;
+ font-weight: normal;
+ position: absolute;
+ right: 0;
+}
+
+html[dir='rtl'] body.bare-minimum #footer {
+ left: 0;
+ right: auto;
+}
+
+body.bare-minimum #footer-border,
+body.bare-minimum #logo-img,
+body.bare-minimum #dot-list {
+ visibility: hidden;
+}
+
+.starting-up * {
+ -webkit-transition: none !important;
+}
+
+/* Login Status. **************************************************************/
+
+#login-container {
+ -webkit-box-shadow: none;
+ background: transparent none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ /* Leave room for the scrollbar. */
+ margin-left: 13px;
+ margin-right: 13px;
+ margin-top: 5px;
+ padding: 0;
+ position: fixed;
+ right: 0;
+ text-align: right;
+ top: 0;
+ z-index: 10;
+}
+
+html[dir='rtl'] #login-container {
+ left: 0;
+ right: auto;
+}
+
+.login-status-icon {
+ -webkit-padding-end: 37px;
+ background-position: right center;
+ background-repeat: no-repeat;
+ min-height: 27px;
+}
+
+html[dir='rtl'] .login-status-icon {
+ background-position-x: left;
+}
+
+.profile-name:hover,
+.link-span {
+ text-decoration: underline;
+}
+
+#login-status-bubble-contents {
+ font-size: 1.1em;
+}
+
+#login-status-message-container {
+ margin-bottom: 13px;
+}
+
+#login-status-learn-more {
+ display: inline-block;
+}
+
+.login-status-row {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: end;
+ display: -webkit-box;
+}
+
+#login-status-advanced-container {
+ -webkit-box-flex: 1;
+}
+
+#login-status-dismiss {
+ min-width: 6em;
+}
+
+/* Trash. *********************************************************************/
+
+#trash {
+ -webkit-transition: top 200ms, opacity 0;
+ -webkit-transition-delay: 0, 200ms;
+ color: #222;
+ height: 100%;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 50px;
+ width: auto;
+}
+
+html[dir='rtl'] #trash {
+ left: 0;
+ right: auto;
+}
+
+#footer.showing-trash-mode #trash {
+ -webkit-transition-delay: 0, 0;
+ -webkit-transition-duration: 0, 200ms;
+ opacity: 0.75;
+ top: 0;
+}
+
+#footer.showing-trash-mode #trash.drag-target {
+ opacity: 1;
+}
+
+#trash > .trash-text {
+ -webkit-padding-end: 7px;
+ -webkit-padding-start: 30px;
+ border: 1px dashed #7f7f7f;
+ border-radius: 4px;
+ display: inline-block;
+ padding-bottom: 9px;
+ padding-top: 10px;
+ position: relative;
+ top: 7px;
+}
+
+#trash > .lid,
+#trash > .can {
+ top: 18px;
+}
+
+#footer.showing-trash-mode #trash.drag-target .lid {
+ -webkit-transform: rotate(-45deg);
+}
+
+html[dir='rtl'] #footer.showing-trash-mode #trash.drag-target .lid {
+ -webkit-transform: rotate(45deg);
+}
+
+#fontMeasuringDiv {
+ /* The font attributes match the nav inputs. */
+ font-size: 0.9em;
+ font-weight: bold;
+ pointer-events: none;
+ position: absolute;
+ visibility: hidden;
+}
+
+/* Page switcher buttons. *****************************************************/
+
+.page-switcher {
+ -webkit-transition: width 150ms, right 150ms, background-color 150ms;
+ background-color: transparent;
+ border: none;
+ bottom: 0;
+ font-size: 40px;
+ margin: 0;
+ max-width: 150px;
+ min-width: 90px;
+ outline: none;
+ padding: 0;
+ position: absolute;
+ top: 0;
+ z-index: 5;
+}
+
+#chrome-web-store-link {
+ -webkit-padding-end: 12px;
+ /* Match transition delay of recently closed button. */
+ -webkit-transition-delay: 100ms;
+ color: inherit;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+#chrome-web-store-title {
+ -webkit-padding-end: 36px;
+ -webkit-padding-start: 15px;
+ background: url('chrome://theme/IDR_WEBSTORE_ICON_24') right 50% no-repeat;
+ display: inline-block;
+ line-height: 49px;
+}
+
+#chrome-web-store-link:hover {
+ color: #666;
+}
+
+html[dir='rtl'] #chrome-web-store-title {
+ background-position-x: left;
+}
+
+#vertical-separator {
+ -webkit-box-ordinal-group: 3;
+ background-color: rgb(178, 178, 178);
+ display: none;
+ height: 20px;
+ margin: 0;
+ vertical-align: middle;
+ width: 1px;
+}
+
+/* Show the separator only if one of the menus is visible. */
+.footer-menu-button:not(.invisible) ~ #vertical-separator {
+ display: block;
+}
+
+/* In trash mode, hide the menus and web store link. */
+#footer.showing-trash-mode #chrome-web-store-link,
+#footer.showing-trash-mode .menu-container {
+ -webkit-transition-delay: 0;
+ opacity: 0;
+ visibility: hidden;
+}
+
+#footer .menu-container {
+ -webkit-box-align: center;
+ /* Put menus in a box so the order can easily be swapped. */
+ display: -webkit-box;
+ height: 100%;
+ margin: 0;
+}
+
+#recently-closed-menu-button.invisible {
+ visibility: hidden;
+}
+
+#recently-closed-menu-button:not(.invisible) {
+ /* Put the recently closed menu to the right when it's visible. */
+ -webkit-box-ordinal-group: 2;
+}
+
+#other-sessions-menu-button.invisible {
+ display: none;
+}
+
+.other-sessions-promo-message {
+ display: none;
+ padding: 0;
+}
+
+.other-sessions-promo-message:only-child {
+ display: block;
+}
+
+.other-sessions-promo-message p {
+ margin: 0;
+}
diff --git a/chrome/browser/resources/ntp_search/new_tab.html b/chrome/browser/resources/ntp_search/new_tab.html
new file mode 100644
index 0000000..3b31e19
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/new_tab.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html i18n-values="
+ dir:textdirection;
+ hasattribution:hasattribution;
+ themegravity:themegravity;
+ bookmarkbarattached:bookmarkbarattached;"
+ class="starting-up">
+<head>
+<meta charset="utf-8">
+<title i18n-content="title"></title>
+<!-- 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">
+
+<!-- It's important that this be the first script loaded. -->
+<script src="logging.js"></script>
+
+<link rel="stylesheet" href="../shared/css/bubble.css">
+<link rel="stylesheet" href="../shared/css/expandable_bubble.css">
+<link rel="stylesheet" href="../shared/css/menu.css">
+<link rel="stylesheet" href="../shared/css/menu_button.css">
+<link rel="stylesheet" href="../shared/css/trash.css">
+<link rel="stylesheet" href="../shared/css/widgets.css">
+<link rel="stylesheet" href="apps_page.css">
+<link rel="stylesheet" href="chrome://newtab/suggestions_page.css">
+<link rel="stylesheet" href="most_visited_page.css">
+<link rel="stylesheet" href="nav_dot.css">
+<link rel="stylesheet" href="new_tab.css">
+<link rel="stylesheet" href="footer_menu.css">
+<link rel="stylesheet" href="tile_page.css">
+<link id="themecss" rel="stylesheet">
+
+<script src="../shared/js/event_tracker.js"></script>
+<script src="../shared/js/load_time_data.js"></script>
+<script src="../shared/js/parse_html_subset.js"></script>
+<script src="../shared/js/util.js"></script>
+
+<script src="../shared/js/cr.js"></script>
+<script src="../shared/js/cr/ui.js"></script>
+<script src="../shared/js/cr/ui/bubble.js"></script>
+<script src="../shared/js/cr/ui/card_slider.js"></script>
+<script src="../shared/js/cr/ui/context_menu_handler.js"></script>
+<script src="../shared/js/cr/ui/drag_wrapper.js"></script>
+<script src="../shared/js/cr/ui/expandable_bubble.js"></script>
+<script src="../shared/js/cr/ui/menu.js"></script>
+<script src="../shared/js/cr/ui/menu_item.js"></script>
+<script src="../shared/js/cr/ui/position_util.js"></script>
+<script src="../shared/js/cr/ui/menu_button.js"></script>
+<script src="../shared/js/cr/ui/context_menu_button.js"></script>
+<script src="../shared/js/cr/ui/touch_handler.js"></script>
+
+<script src="tile_page.js"></script>
+<script src="apps_page.js"></script>
+<script src="dot_list.js"></script>
+<script src="most_visited_page.js"></script>
+<script src="trash.js"></script>
+<script src="page_list_view.js"></script>
+<script src="page_switcher.js"></script>
+
+<script src="nav_dot.js"></script>
+<script src="new_tab.js"></script>
+<script src="recently_closed.js"></script>
+<script src="other_sessions.js"></script>
+</head>
+
+<body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize">
+ <button id="login-container" class="custom-appearance" hidden>
+ <div id="login-status-header-container" class="login-status-row">
+ <div id="login-status-header"></div>
+ </div>
+ <div id="login-status-sub-header"></div>
+ </button>
+
+ <div id="notification-container" class="inactive" hidden>
+ <div id="notification">
+ <span></span>
+ <div id="notificationLinks"></div>
+ <button class="close-button custom-appearance" class="custom-appearance">
+ </button>
+ </div>
+ </div>
+
+ <div id="card-slider-frame">
+ <button id="page-switcher-start" class="page-switcher custom-appearance"
+ tabindex="2" hidden>‹
+ </button>
+ <div id="page-list"></div>
+ <button id="page-switcher-end" class="page-switcher custom-appearance"
+ tabindex="2" hidden>›
+ </button>
+ <div id="attribution">
+ <span i18n-content="attributionintro"></span>
+ <img id="attribution-img">
+ </div>
+ </div>
+
+ <div id="footer">
+ <div id="footer-border"></div>
+ <div id="footer-content">
+ <img id="logo-img" src="chrome://theme/IDR_PRODUCT_LOGO">
+
+ <ul id="dot-list">
+ </ul>
+
+ <div class="menu-container">
+ <button id="recently-closed-menu-button"
+ class="footer-menu-button custom-appearance">
+ <span i18n-content="recentlyclosed"></span>
+ <div class="disclosure-triangle"></div>
+ </button>
+
+ <button id="other-sessions-menu-button"
+ class="footer-menu-button custom-appearance invisible">
+ <span i18n-content="otherSessions"></span>
+ <div class="disclosure-triangle"></div>
+ </button>
+
+ <div id="vertical-separator"></div>
+ </div>
+
+ <a id="chrome-web-store-link">
+ <span id="chrome-web-store-title" i18n-content="webStoreTitleShort">
+ </span>
+ </a>
+
+ <div id="trash" class="trash">
+ <span class="lid"></span>
+ <span class="can"></span>
+ <span class="trash-text" i18n-content="appuninstall"></span>
+ </div>
+ </div>
+ </div>
+</body>
+
+<!-- A div to hold all the templates, and in the darkness bind them. -->
+<div hidden>
+
+<!-- NTP4 intro bubble -->
+<div id="ntp4-intro-bubble-contents">
+ <div></div>
+ <a i18n-content="learn_more" target="_blank"></a>
+</div>
+
+<!-- Login status bubble -->
+<div id="login-status-bubble-contents">
+ <div id="login-status-message-container">
+ <span i18n-content="login_status_message"></span>
+ <a id="login-status-learn-more" i18n-content="learn_more"
+ i18n-values="href:login_status_url" target="_blank"></a>
+ </div>
+ <div class="login-status-row">
+ <div id="login-status-advanced-container">
+ <a id="login-status-advanced"
+ i18n-content="login_status_advanced" href="#"></a>
+ </div>
+ <button id="login-status-dismiss" i18n-content="login_status_dismiss">
+ </button>
+ </div>
+</div>
+
+<!-- Apps promo. -->
+<div id="apps-promo-extras-template" class="apps-promo-extras">
+ <h3 class="apps-promo-heading"></h3>
+ <a class="apps-promo-link g-button-basic"></a>
+</div>
+
+<!-- App Contents w/ Large Icon -->
+<div id="app-large-icon-template" class="app-contents">
+ <div class="app-img-container">
+ <img class="invisible">
+ </div>
+ <span class="title"></span>
+</div>
+
+<!-- App Contents w/ Small Icon -->
+<div id="app-small-icon-template" class="app-contents">
+ <div class="app-icon-div">
+ <div class="app-img-container">
+ <img class="invisible">
+ </div>
+ <div class="color-stripe"></div>
+ </div>
+ <span class="title"></span>
+</div>
+
+<!-- Message shown in the other sessions menu when the user is signed in but
+ there is no session data (e.g. they have tab sync turned off). -->
+<div id="other-sessions-promo-template" class="other-sessions-promo-message">
+ <span i18n-content="otherSessionsEmpty"></span>
+ <p>
+ <a i18n-values="href:otherSessionsLearnMoreUrl" i18n-content="learnMore">
+ </a>
+ </p>
+</div>
+
+</div>
+
+<!-- This is used to measure text in the current locale. It is not visible. -->
+<div id="fontMeasuringDiv"></div>
+
+</html>
diff --git a/chrome/browser/resources/ntp_search/new_tab.js b/chrome/browser/resources/ntp_search/new_tab.js
new file mode 100644
index 0000000..57c2a68
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/new_tab.js
@@ -0,0 +1,595 @@
+// Copyright (c) 2012 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 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
+cr.define('ntp', function() {
+ 'use strict';
+
+ /**
+ * NewTabView instance.
+ * @type {!Object|undefined}
+ */
+ var newTabView;
+
+ /**
+ * The 'notification-container' element.
+ * @type {!Element|undefined}
+ */
+ var notificationContainer;
+
+ /**
+ * If non-null, an info bubble for showing messages to the user. It points at
+ * the Most Visited label, and is used to draw more attention to the
+ * navigation dot UI.
+ * @type {!Element|undefined}
+ */
+ var infoBubble;
+
+ /**
+ * If non-null, an bubble confirming that the user has signed into sync. It
+ * points at the login status at the top of the page.
+ * @type {!Element|undefined}
+ */
+ var loginBubble;
+
+ /**
+ * true if |loginBubble| should be shown.
+ * @type {Boolean}
+ */
+ var shouldShowLoginBubble = false;
+
+ /**
+ * The 'other-sessions-menu-button' element.
+ * @type {!Element|undefined}
+ */
+ var otherSessionsButton;
+
+ /**
+ * 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;
+
+ /**
+ * See description for these values in ntp_stats.h.
+ * @enum {number}
+ */
+ var NtpFollowAction = {
+ CLICKED_TILE: 11,
+ CLICKED_OTHER_NTP_PANE: 12,
+ OTHER: 13
+ };
+
+ /**
+ * Creates a NewTabView object. NewTabView extends PageListView with
+ * new tab UI specific logics.
+ * @constructor
+ * @extends {PageListView}
+ */
+ function NewTabView() {
+ var pageSwitcherStart = null;
+ var pageSwitcherEnd = null;
+ if (loadTimeData.getValue('showApps')) {
+ pageSwitcherStart = getRequiredElement('page-switcher-start');
+ pageSwitcherEnd = getRequiredElement('page-switcher-end');
+ }
+ this.initialize(getRequiredElement('page-list'),
+ getRequiredElement('dot-list'),
+ getRequiredElement('card-slider-frame'),
+ getRequiredElement('trash'),
+ pageSwitcherStart, pageSwitcherEnd);
+ }
+
+ NewTabView.prototype = {
+ __proto__: ntp.PageListView.prototype,
+
+ /** @inheritDoc */
+ appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
+ ntp.PageListView.prototype.appendTilePage.apply(this, arguments);
+
+ if (infoBubble)
+ window.setTimeout(infoBubble.reposition.bind(infoBubble), 0);
+ }
+ };
+
+ /**
+ * Invoked at startup once the DOM is available to initialize the app.
+ */
+ function onLoad() {
+ sectionsToWaitFor = loadTimeData.getBoolean('showApps') ? 2 : 1;
+ if (loadTimeData.getBoolean('isSuggestionsPageEnabled'))
+ sectionsToWaitFor++;
+ measureNavDots();
+
+ // Load the current theme colors.
+ themeChanged();
+
+ newTabView = new NewTabView();
+
+ notificationContainer = getRequiredElement('notification-container');
+ notificationContainer.addEventListener(
+ 'webkitTransitionEnd', onNotificationTransitionEnd);
+
+ cr.ui.decorate($('recently-closed-menu-button'), ntp.RecentMenuButton);
+ chrome.send('getRecentlyClosedTabs');
+
+ if (loadTimeData.getBoolean('showOtherSessionsMenu')) {
+ otherSessionsButton = getRequiredElement('other-sessions-menu-button');
+ cr.ui.decorate(otherSessionsButton, ntp.OtherSessionsMenuButton);
+ otherSessionsButton.initialize(loadTimeData.getBoolean('isUserSignedIn'));
+ }
+
+ var mostVisited = new ntp.MostVisitedPage();
+ // Move the footer into the most visited page if we are in "bare minimum"
+ // mode.
+ if (document.body.classList.contains('bare-minimum'))
+ mostVisited.appendFooter(getRequiredElement('footer'));
+ newTabView.appendTilePage(mostVisited,
+ loadTimeData.getString('mostvisited'),
+ false);
+ chrome.send('getMostVisited');
+
+ if (loadTimeData.getBoolean('isSuggestionsPageEnabled')) {
+ var suggestions_script = document.createElement('script');
+ suggestions_script.src = 'suggestions_page.js';
+ suggestions_script.onload = function() {
+ newTabView.appendTilePage(new ntp.SuggestionsPage(),
+ loadTimeData.getString('suggestions'),
+ false,
+ (newTabView.appsPages.length > 0) ?
+ newTabView.appsPages[0] : null);
+ chrome.send('getSuggestions');
+ cr.dispatchSimpleEvent(document, 'sectionready', true, true);
+ };
+ document.querySelector('head').appendChild(suggestions_script);
+ }
+
+ var webStoreLink = loadTimeData.getString('webStoreLink');
+ var url = appendParam(webStoreLink, 'utm_source', 'chrome-ntp-launcher');
+ $('chrome-web-store-link').href = url;
+ $('chrome-web-store-link').addEventListener('click',
+ onChromeWebStoreButtonClick);
+
+ if (loadTimeData.getString('login_status_message')) {
+ loginBubble = new cr.ui.Bubble;
+ loginBubble.anchorNode = $('login-container');
+ loginBubble.setArrowLocation(cr.ui.ArrowLocation.TOP_END);
+ loginBubble.bubbleAlignment =
+ cr.ui.BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE;
+ loginBubble.deactivateToDismissDelay = 2000;
+ loginBubble.setCloseButtonVisible(false);
+
+ $('login-status-advanced').onclick = function() {
+ chrome.send('showAdvancedLoginUI');
+ };
+ $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
+
+ var bubbleContent = $('login-status-bubble-contents');
+ loginBubble.content = bubbleContent;
+
+ // The anchor node won't be updated until updateLogin is called so don't
+ // show the bubble yet.
+ shouldShowLoginBubble = true;
+ }
+
+ var loginContainer = getRequiredElement('login-container');
+ loginContainer.addEventListener('click', showSyncLoginUI);
+ chrome.send('initializeSyncLogin');
+
+ doWhenAllSectionsReady(function() {
+ // Tell the slider about the pages.
+ newTabView.updateSliderCards();
+ // Mark the current page.
+ newTabView.cardSlider.currentCardValue.navigationDot.classList.add(
+ 'selected');
+
+ if (loadTimeData.valueExists('serverpromo')) {
+ var promo = loadTimeData.getString('serverpromo');
+ var tags = ['IMG'];
+ var attrs = {
+ src: function(node, value) {
+ return node.tagName == 'IMG' &&
+ /^data\:image\/(?:png|gif|jpe?g)/.test(value);
+ },
+ };
+ showNotification(parseHtmlSubset(promo, tags, attrs), [], function() {
+ chrome.send('closeNotificationPromo');
+ }, 60000);
+ chrome.send('notificationPromoViewed');
+ }
+
+ cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
+ document.documentElement.classList.remove('starting-up');
+ });
+ }
+
+ /**
+ * Launches the chrome web store app with the chrome-ntp-launcher
+ * source.
+ * @param {Event} e The click event.
+ */
+ function onChromeWebStoreButtonClick(e) {
+ chrome.send('recordAppLaunchByURL',
+ [encodeURIComponent(this.href),
+ ntp.APP_LAUNCH.NTP_WEBSTORE_FOOTER]);
+ }
+
+ /*
+ * The number of sections to wait on.
+ * @type {number}
+ */
+ var sectionsToWaitFor = -1;
+
+ /**
+ * Queued callbacks which lie in wait for all sections to be ready.
+ * @type {array}
+ */
+ var readyCallbacks = [];
+
+ /**
+ * Fired as each section of pages becomes ready.
+ * @param {Event} e Each page's synthetic DOM event.
+ */
+ document.addEventListener('sectionready', function(e) {
+ if (--sectionsToWaitFor <= 0) {
+ while (readyCallbacks.length) {
+ readyCallbacks.shift()();
+ }
+ }
+ });
+
+ /**
+ * This is used to simulate a fire-once event (i.e. $(document).ready() in
+ * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback
+ * is fired right away. If all pages are not ready yet, the function is queued
+ * for later execution.
+ * @param {function} callback The work to be done when ready.
+ */
+ function doWhenAllSectionsReady(callback) {
+ assert(typeof callback == 'function');
+ if (sectionsToWaitFor > 0)
+ readyCallbacks.push(callback);
+ else
+ window.setTimeout(callback, 0); // Do soon after, but asynchronously.
+ }
+
+ /**
+ * Fills in an invisible div with the 'Most Visited' string so that
+ * its length may be measured and the nav dots sized accordingly.
+ */
+ function measureNavDots() {
+ var measuringDiv = $('fontMeasuringDiv');
+ measuringDiv.textContent = loadTimeData.getString('mostvisited');
+ // The 4 is for border and padding.
+ var pxWidth = Math.max(measuringDiv.clientWidth * 1.15 + 4, 80);
+
+ var styleElement = document.createElement('style');
+ styleElement.type = 'text/css';
+ // max-width is used because if we run out of space, the nav dots will be
+ // shrunk.
+ styleElement.textContent = '.dot { max-width: ' + pxWidth + 'px; }';
+ document.querySelector('head').appendChild(styleElement);
+ }
+
+ function themeChanged(opt_hasAttribution) {
+ $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
+
+ if (typeof opt_hasAttribution != 'undefined') {
+ document.documentElement.setAttribute('hasattribution',
+ opt_hasAttribution);
+ }
+
+ updateAttribution();
+ }
+
+ function setBookmarkBarAttached(attached) {
+ document.documentElement.setAttribute('bookmarkbarattached', attached);
+ }
+
+ /**
+ * Attributes the attribution image at the bottom left.
+ */
+ function updateAttribution() {
+ var attribution = $('attribution');
+ if (document.documentElement.getAttribute('hasattribution') == 'true') {
+ $('attribution-img').src =
+ 'chrome://theme/IDR_THEME_NTP_ATTRIBUTION?' + Date.now();
+ attribution.hidden = false;
+ } else {
+ attribution.hidden = true;
+ }
+ }
+
+ /**
+ * Timeout ID.
+ * @type {number}
+ */
+ var notificationTimeout = 0;
+
+ /**
+ * Shows the notification bubble.
+ * @param {string|Node} message The notification message or node to use as
+ * message.
+ * @param {Array.<{text: string, action: function()}>} links An array of
+ * records describing the links in the notification. Each record should
+ * have a 'text' attribute (the display string) and an 'action' attribute
+ * (a function to run when the link is activated).
+ * @param {Function} opt_closeHandler The callback invoked if the user
+ * manually dismisses the notification.
+ */
+ function showNotification(message, links, opt_closeHandler, opt_timeout) {
+ window.clearTimeout(notificationTimeout);
+
+ var span = document.querySelector('#notification > span');
+ if (typeof message == 'string') {
+ span.textContent = message;
+ } else {
+ span.textContent = ''; // Remove all children.
+ span.appendChild(message);
+ }
+
+ var linksBin = $('notificationLinks');
+ linksBin.textContent = '';
+ for (var i = 0; i < links.length; i++) {
+ var link = linksBin.ownerDocument.createElement('div');
+ link.textContent = links[i].text;
+ link.action = links[i].action;
+ link.onclick = function() {
+ this.action();
+ hideNotification();
+ };
+ link.setAttribute('role', 'button');
+ link.setAttribute('tabindex', 0);
+ link.className = 'link-button';
+ linksBin.appendChild(link);
+ }
+
+ function closeFunc(e) {
+ if (opt_closeHandler)
+ opt_closeHandler();
+ hideNotification();
+ }
+
+ document.querySelector('#notification button').onclick = closeFunc;
+ document.addEventListener('dragstart', closeFunc);
+
+ notificationContainer.hidden = false;
+ showNotificationOnCurrentPage();
+
+ newTabView.cardSlider.frame.addEventListener(
+ 'cardSlider:card_change_ended', onCardChangeEnded);
+
+ var timeout = opt_timeout || 10000;
+ notificationTimeout = window.setTimeout(hideNotification, timeout);
+ }
+
+ /**
+ * Hide the notification bubble.
+ */
+ function hideNotification() {
+ notificationContainer.classList.add('inactive');
+
+ newTabView.cardSlider.frame.removeEventListener(
+ 'cardSlider:card_change_ended', onCardChangeEnded);
+ }
+
+ /**
+ * Happens when 1 or more consecutive card changes end.
+ * @param {Event} e The cardSlider:card_change_ended event.
+ */
+ function onCardChangeEnded(e) {
+ // If we ended on the same page as we started, ignore.
+ if (newTabView.cardSlider.currentCardValue.notification)
+ return;
+
+ // Hide the notification the old page.
+ notificationContainer.classList.add('card-changed');
+
+ showNotificationOnCurrentPage();
+ }
+
+ /**
+ * Move and show the notification on the current page.
+ */
+ function showNotificationOnCurrentPage() {
+ var page = newTabView.cardSlider.currentCardValue;
+ doWhenAllSectionsReady(function() {
+ if (page != newTabView.cardSlider.currentCardValue)
+ return;
+
+ // NOTE: This moves the notification to inside of the current page.
+ page.notification = notificationContainer;
+
+ // Reveal the notification and instruct it to hide itself if ignored.
+ notificationContainer.classList.remove('inactive');
+
+ // Gives the browser time to apply this rule before we remove it (causing
+ // a transition).
+ window.setTimeout(function() {
+ notificationContainer.classList.remove('card-changed');
+ }, 0);
+ });
+ }
+
+ /**
+ * When done fading out, set hidden to true so the notification can't be
+ * tabbed to or clicked.
+ * @param {Event} e The webkitTransitionEnd event.
+ */
+ function onNotificationTransitionEnd(e) {
+ if (notificationContainer.classList.contains('inactive'))
+ notificationContainer.hidden = true;
+ }
+
+ function setRecentlyClosedTabs(dataItems) {
+ $('recently-closed-menu-button').dataItems = dataItems;
+ }
+
+ function setMostVisitedPages(data, hasBlacklistedUrls) {
+ newTabView.mostVisitedPage.data = data;
+ cr.dispatchSimpleEvent(document, 'sectionready', true, true);
+ }
+
+ function setSuggestionsPages(data, hasBlacklistedUrls) {
+ newTabView.suggestionsPage.data = data;
+ }
+
+ /**
+ * Set the dominant color for a node. This will be called in response to
+ * getFaviconDominantColor. The node represented by |id| better have a setter
+ * for stripeColor.
+ * @param {string} id The ID of a node.
+ * @param {string} color The color represented as a CSS string.
+ */
+ function setStripeColor(id, color) {
+ var node = $(id);
+ if (node)
+ node.stripeColor = color;
+ }
+
+ /**
+ * Updates the text displayed in the login container. If there is no text then
+ * the login container is hidden.
+ * @param {string} loginHeader The first line of text.
+ * @param {string} loginSubHeader The second line of text.
+ * @param {string} iconURL The url for the login status icon. If this is null
+ then the login status icon is hidden.
+ * @param {boolean} isUserSignedIn Indicates if the user is signed in or not.
+ */
+ function updateLogin(loginHeader, loginSubHeader, iconURL, isUserSignedIn) {
+ if (loginHeader || loginSubHeader) {
+ $('login-container').hidden = false;
+ $('login-status-header').innerHTML = loginHeader;
+ $('login-status-sub-header').innerHTML = loginSubHeader;
+ $('card-slider-frame').classList.add('showing-login-area');
+
+ if (iconURL) {
+ $('login-status-header-container').style.backgroundImage = url(iconURL);
+ $('login-status-header-container').classList.add('login-status-icon');
+ } else {
+ $('login-status-header-container').style.backgroundImage = 'none';
+ $('login-status-header-container').classList.remove(
+ 'login-status-icon');
+ }
+ } else {
+ $('login-container').hidden = true;
+ $('card-slider-frame').classList.remove('showing-login-area');
+ }
+ if (shouldShowLoginBubble) {
+ window.setTimeout(loginBubble.show.bind(loginBubble), 0);
+ chrome.send('loginMessageSeen');
+ shouldShowLoginBubble = false;
+ } else if (loginBubble) {
+ loginBubble.reposition();
+ }
+ if (otherSessionsButton)
+ otherSessionsButton.updateSignInState(isUserSignedIn);
+ }
+
+ /**
+ * Show the sync login UI.
+ * @param {Event} e The click event.
+ */
+ function showSyncLoginUI(e) {
+ var rect = e.currentTarget.getBoundingClientRect();
+ chrome.send('showSyncLoginUI',
+ [rect.left, rect.top, rect.width, rect.height]);
+ }
+
+ /**
+ * Wrappers to forward the callback to corresponding PageListView member.
+ */
+ function appAdded() {
+ return newTabView.appAdded.apply(newTabView, arguments);
+ }
+
+ function appMoved() {
+ return newTabView.appMoved.apply(newTabView, arguments);
+ }
+
+ function appRemoved() {
+ return newTabView.appRemoved.apply(newTabView, arguments);
+ }
+
+ function appsPrefChangeCallback() {
+ return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
+ }
+
+ function appsReordered() {
+ return newTabView.appsReordered.apply(newTabView, arguments);
+ }
+
+ function enterRearrangeMode() {
+ return newTabView.enterRearrangeMode.apply(newTabView, arguments);
+ }
+
+ function setForeignSessions(sessionList, isTabSyncEnabled) {
+ if (otherSessionsButton)
+ otherSessionsButton.setForeignSessions(sessionList, isTabSyncEnabled);
+ }
+
+ function getAppsCallback() {
+ return newTabView.getAppsCallback.apply(newTabView, arguments);
+ }
+
+ function getAppsPageIndex() {
+ return newTabView.getAppsPageIndex.apply(newTabView, arguments);
+ }
+
+ function getCardSlider() {
+ return newTabView.cardSlider;
+ }
+
+ function leaveRearrangeMode() {
+ return newTabView.leaveRearrangeMode.apply(newTabView, arguments);
+ }
+
+ function saveAppPageName() {
+ return newTabView.saveAppPageName.apply(newTabView, arguments);
+ }
+
+ function setAppToBeHighlighted(appId) {
+ newTabView.highlightAppId = appId;
+ }
+
+ // Return an object with all the exports
+ return {
+ appAdded: appAdded,
+ appMoved: appMoved,
+ appRemoved: appRemoved,
+ appsPrefChangeCallback: appsPrefChangeCallback,
+ enterRearrangeMode: enterRearrangeMode,
+ getAppsCallback: getAppsCallback,
+ getAppsPageIndex: getAppsPageIndex,
+ getCardSlider: getCardSlider,
+ onLoad: onLoad,
+ leaveRearrangeMode: leaveRearrangeMode,
+ NtpFollowAction: NtpFollowAction,
+ saveAppPageName: saveAppPageName,
+ setAppToBeHighlighted: setAppToBeHighlighted,
+ setBookmarkBarAttached: setBookmarkBarAttached,
+ setForeignSessions: setForeignSessions,
+ setMostVisitedPages: setMostVisitedPages,
+ setSuggestionsPages: setSuggestionsPages,
+ setRecentlyClosedTabs: setRecentlyClosedTabs,
+ setStripeColor: setStripeColor,
+ showNotification: showNotification,
+ themeChanged: themeChanged,
+ updateLogin: updateLogin
+ };
+});
+
+document.addEventListener('DOMContentLoaded', ntp.onLoad);
+
+var toCssPx = cr.ui.toCssPx;
diff --git a/chrome/browser/resources/ntp_search/new_tab_theme.css b/chrome/browser/resources/ntp_search/new_tab_theme.css
new file mode 100644
index 0000000..8f268f3
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/new_tab_theme.css
@@ -0,0 +1,109 @@
+/* Copyright (c) 2012 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. */
+
+html {
+ background-attachment: fixed;
+ background-color: $2; /* COLOR_NTP_BACKGROUND */
+ background-image: url(chrome://theme/IDR_THEME_NTP_BACKGROUND?$1);
+ background-position: $3;
+ background-repeat: $5;
+}
+
+html[bookmarkbarattached='true'] {
+ background-position: $4;
+}
+
+body {
+ color: $8; /* COLOR_NTP_TEXT */
+ height: 100%;
+ overflow: auto;
+}
+
+#attribution,
+.link-button,
+.link-span {
+ color: $21; /* COLOR_NTP_TEXT_LIGHT */
+}
+
+.link-button:active {
+ color: $8; /* COLOR_NTP_TEXT */
+}
+
+.page-switcher {
+ color: rgba($23, 0.5); /* COLOR_NTP_TEXT */
+}
+
+.page-switcher:hover,
+.page-switcher:focus,
+.page-switcher.drag-target {
+ background-color: rgba($23, 0.06); /* COLOR_NTP_TEXT */
+}
+
+/* Only change the background to a gradient when a promo is showing. */
+.showing-login-area #page-switcher-end:hover,
+.showing-login-area #page-switcher-end:focus,
+.showing-login-area #page-switcher-end.drag-target {
+ background:
+ -webkit-linear-gradient(top, rgba($23, 0) 0,
+ rgba($23, .01) 60px,
+ rgba($23, .06) 183px); /* COLOR_NTP_TEXT */
+}
+
+.tile-page-scrollbar {
+ background-color: $21; /* COLOR_NTP_TEXT_LIGHT */
+}
+
+/* Footer *********************************************************************/
+
+#footer-border {
+ background: -webkit-linear-gradient(left,
+ rgba($22, 0.2), /* COLOR_NTP_SECTION_BORDER */
+ rgba($22, 0.3) 20%, /* COLOR_NTP_SECTION_BORDER */
+ rgba($22, 0.3) 80%, /* COLOR_NTP_SECTION_BORDER */
+ rgba($22, 0.2)); /* COLOR_NTP_SECTION_BORDER */
+}
+
+.dot input:focus {
+ background-color: $2; /* COLOR_NTP_BACKGROUND */
+}
+
+.bare-minimum #footer {
+ color: $8; /* COLOR_NTP_TEXT */
+}
+
+.bare-minimum .disclosure-triangle {
+ background-color: $8; /* COLOR_NTP_TEXT */
+}
+
+.bare-minimum .footer-menu-button:hover,
+.bare-minimum .footer-menu-button[menu-shown],
+.bare-minimum #chrome-web-store-link:hover {
+ color: rgba($23, 0.85); /* COLOR_NTP_TEXT */
+}
+
+.bare-minimum .footer-menu-button:hover .disclosure-triangle,
+.bare-minimum .footer-menu-button[menu-shown] .disclosure-triangle {
+ background-color: rgba($23, 0.85); /* COLOR_NTP_TEXT */
+}
+
+/* Most Visited ***************************************************************/
+
+.most-visited,
+.most-visited > .title {
+ color: $8; /* COLOR_NTP_TEXT */
+}
+
+.most-visited:focus:not(.filler) .thumbnail-wrapper,
+.most-visited:hover:not(.filler) .thumbnail-wrapper {
+ background-color: $6; /* COLOR_NTP_HEADER */
+}
+
+.thumbnail-wrapper {
+ /* This shows through at the (rounded) thumbnail's corners. */
+ background-color: $11; /* COLOR_NTP_SECTION_BORDER */
+}
+
+.filler .thumbnail {
+ border-color: $2; /* COLOR_NTP_BACKGROUND */
+}
diff --git a/chrome/browser/resources/ntp_search/other_sessions.js b/chrome/browser/resources/ntp_search/other_sessions.js
new file mode 100644
index 0000000..1a0f60d
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/other_sessions.js
@@ -0,0 +1,361 @@
+// Copyright (c) 2012 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 The menu that shows tabs from sessions on other devices.
+ */
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ /** @const */ var ContextMenuButton = cr.ui.ContextMenuButton;
+ /** @const */ var Menu = cr.ui.Menu;
+ /** @const */ var MenuItem = cr.ui.MenuItem;
+ /** @const */ var MenuButton = cr.ui.MenuButton;
+ /** @const */ var OtherSessionsMenuButton = cr.ui.define('button');
+
+ // Histogram buckets for UMA tracking of menu usage.
+ /** @const */ var HISTOGRAM_EVENT = {
+ INITIALIZED: 0,
+ SHOW_MENU: 1,
+ LINK_CLICKED: 2,
+ LINK_RIGHT_CLICKED: 3,
+ SESSION_NAME_RIGHT_CLICKED: 4,
+ SHOW_SESSION_MENU: 5,
+ COLLAPSE_SESSION: 6,
+ EXPAND_SESSION: 7,
+ OPEN_ALL: 8
+ };
+ /** @const */ var HISTOGRAM_EVENT_LIMIT =
+ HISTOGRAM_EVENT.OPEN_ALL + 1;
+
+ /**
+ * Record an event in the UMA histogram.
+ * @param {Number} eventId The id of the event to be recorded.
+ * @private
+ */
+ function recordUmaEvent_(eventId) {
+ chrome.send('metricsHandler:recordInHistogram',
+ ['NewTabPage.OtherSessionsMenu', eventId, HISTOGRAM_EVENT_LIMIT]);
+ }
+
+ OtherSessionsMenuButton.prototype = {
+ __proto__: MenuButton.prototype,
+
+ decorate: function() {
+ MenuButton.prototype.decorate.call(this);
+ this.menu = new Menu;
+ cr.ui.decorate(this.menu, Menu);
+ this.menu.classList.add('footer-menu');
+ this.menu.addEventListener('contextmenu',
+ this.onContextMenu_.bind(this), true);
+ document.body.appendChild(this.menu);
+
+ // Create the context menu that appears when the user right clicks
+ // on a device name.
+ this.deviceContextMenu_ = DeviceContextMenuController.getInstance().menu;
+ document.body.appendChild(this.deviceContextMenu_);
+
+ this.promoMessage_ = $('other-sessions-promo-template').cloneNode(true);
+ this.promoMessage_.removeAttribute('id'); // Prevent a duplicate id.
+
+ this.sessions_ = [];
+ this.anchorType = cr.ui.AnchorType.ABOVE;
+ this.invertLeftRight = true;
+
+ // Initialize the images for the drop-down buttons that appear beside the
+ // session names.
+ MenuButton.createDropDownArrows();
+
+ recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED);
+ },
+
+ /**
+ * Initialize this element.
+ * @param {boolean} signedIn Is the current user signed in?
+ */
+ initialize: function(signedIn) {
+ this.updateSignInState(signedIn);
+ },
+
+ /**
+ * Handle a context menu event for an object in the menu's DOM subtree.
+ */
+ onContextMenu_: function(e) {
+ // Only record the action if it occurred in one of the menu items or
+ // on one of the session headings.
+ if (findAncestorByClass(e.target, 'footer-menu-item')) {
+ recordUmaEvent_(HISTOGRAM_EVENT.LINK_RIGHT_CLICKED);
+ } else {
+ var heading = findAncestorByClass(e.target, 'session-heading');
+ if (heading) {
+ recordUmaEvent_(HISTOGRAM_EVENT.SESSION_NAME_RIGHT_CLICKED);
+
+ // Let the context menu know which session it was invoked on,
+ // since they all share the same instance of the menu.
+ DeviceContextMenuController.getInstance().setSession(
+ heading.sessionData_);
+ }
+ }
+ },
+
+ /**
+ * Hides the menu.
+ * @override
+ */
+ hideMenu: function() {
+ // Don't hide if the device context menu is currently showing.
+ if (this.deviceContextMenu_.hidden)
+ MenuButton.prototype.hideMenu.call(this);
+ },
+
+ /**
+ * Shows the menu, first rebuilding it if necessary.
+ * TODO(estade): the right of the menu should align with the right of the
+ * button.
+ * @override
+ */
+ showMenu: function() {
+ if (this.sessions_.length == 0)
+ chrome.send('getForeignSessions');
+ recordUmaEvent_(HISTOGRAM_EVENT.SHOW_MENU);
+ MenuButton.prototype.showMenu.call(this);
+
+ // Work around https://bugs.webkit.org/show_bug.cgi?id=85884.
+ this.menu.scrollTop = 0;
+ },
+
+ /**
+ * Reset the menu contents to the default state.
+ * @private
+ */
+ resetMenuContents_: function() {
+ this.menu.innerHTML = '';
+ this.menu.appendChild(this.promoMessage_);
+ },
+
+ /**
+ * Create a custom click handler for a link, so that clicking on a link
+ * restores the session (including back stack) rather than just opening
+ * the URL.
+ */
+ makeClickHandler_: function(sessionTag, windowId, tabId) {
+ var self = this;
+ return function(e) {
+ recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED);
+ chrome.send('openForeignSession', [sessionTag, windowId, tabId,
+ e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
+ e.preventDefault();
+ };
+ },
+
+ /**
+ * Add the UI for a foreign session to the menu.
+ * @param {Object} session Object describing the foreign session.
+ */
+ addSession_: function(session) {
+ var doc = this.ownerDocument;
+
+ var section = doc.createElement('section');
+ this.menu.appendChild(section);
+
+ var heading = doc.createElement('h3');
+ heading.className = 'session-heading';
+ heading.textContent = session.name;
+ heading.sessionData_ = session;
+ section.appendChild(heading);
+
+ var dropDownButton = new ContextMenuButton;
+ dropDownButton.classList.add('drop-down');
+ // Keep track of the drop down that triggered the menu, so we know
+ // which element to apply the command to.
+ function handleDropDownFocus(e) {
+ DeviceContextMenuController.getInstance().setSession(session);
+ }
+ dropDownButton.addEventListener('mousedown', handleDropDownFocus);
+ dropDownButton.addEventListener('focus', handleDropDownFocus);
+ heading.appendChild(dropDownButton);
+
+ var timeSpan = doc.createElement('span');
+ timeSpan.className = 'details';
+ timeSpan.textContent = session.modifiedTime;
+ heading.appendChild(timeSpan);
+
+ cr.ui.contextMenuHandler.setContextMenu(heading,
+ this.deviceContextMenu_);
+
+ if (!session.collapsed)
+ section.appendChild(this.createSessionContents_(session));
+ },
+
+ /**
+ * Create the DOM tree representing the tabs and windows in a session.
+ * @param {Object} session The session model object.
+ * @return {Element} A single div containing the list of tabs & windows.
+ * @private
+ */
+ createSessionContents_: function(session) {
+ var doc = this.ownerDocument;
+ var contents = doc.createElement('div');
+
+ for (var i = 0; i < session.windows.length; i++) {
+ var window = session.windows[i];
+
+ // Show a separator between multiple windows in the same session.
+ if (i > 0)
+ contents.appendChild(doc.createElement('hr'));
+
+ for (var j = 0; j < window.tabs.length; j++) {
+ var tab = window.tabs[j];
+ var a = doc.createElement('a');
+ a.className = 'footer-menu-item';
+ a.textContent = tab.title;
+ a.href = tab.url;
+ a.style.backgroundImage = url('chrome://session-favicon/' + tab.url);
+
+ var clickHandler = this.makeClickHandler_(
+ session.tag, String(window.sessionId), String(tab.sessionId));
+ a.addEventListener('click', clickHandler);
+ contents.appendChild(a);
+ }
+ }
+
+ return contents;
+ },
+
+ /**
+ * Sets the menu model data. An empty list means that either there are no
+ * foreign sessions, or tab sync is disabled for this profile.
+ * |isTabSyncEnabled| makes it possible to distinguish between the cases.
+ *
+ * @param {Array} sessionList Array of objects describing the sessions
+ * from other devices.
+ * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
+ */
+ setForeignSessions: function(sessionList, isTabSyncEnabled) {
+ this.sessions_ = sessionList;
+ this.resetMenuContents_();
+ if (sessionList.length > 0) {
+ // Rebuild the menu with the new data.
+ for (var i = 0; i < sessionList.length; i++) {
+ this.addSession_(sessionList[i]);
+ }
+ }
+
+ // The menu button is shown iff tab sync is enabled.
+ if (isTabSyncEnabled)
+ this.classList.remove('invisible');
+ else
+ this.classList.add('invisible');
+ },
+
+ /**
+ * Called when this element is initialized, and from the new tab page when
+ * the user's signed in state changes,
+ * @param {boolean} signedIn Is the user currently signed in?
+ */
+ updateSignInState: function(signedIn) {
+ if (signedIn)
+ chrome.send('getForeignSessions');
+ else
+ this.classList.add('invisible');
+ },
+ };
+
+ /**
+ * Controller for the context menu for device names in the list of sessions.
+ * This class is designed to be used as a singleton.
+ *
+ * @constructor
+ */
+ function DeviceContextMenuController() {
+ this.__proto__ = DeviceContextMenuController.prototype;
+ this.initialize();
+ }
+ cr.addSingletonGetter(DeviceContextMenuController);
+
+ DeviceContextMenuController.prototype = {
+
+ initialize: function() {
+ var menu = new cr.ui.Menu;
+ cr.ui.decorate(menu, cr.ui.Menu);
+ menu.classList.add('device-context-menu');
+ menu.classList.add('footer-menu-context-menu');
+ this.menu = menu;
+ this.collapseItem_ = this.appendMenuItem_('collapseSessionMenuItemText');
+ this.collapseItem_.addEventListener('activate',
+ this.onCollapseOrExpand_.bind(this));
+ this.expandItem_ = this.appendMenuItem_('expandSessionMenuItemText');
+ this.expandItem_.addEventListener('activate',
+ this.onCollapseOrExpand_.bind(this));
+ this.openAllItem_ = this.appendMenuItem_('restoreSessionMenuItemText');
+ this.openAllItem_.addEventListener('activate',
+ this.onOpenAll_.bind(this));
+ },
+
+ /**
+ * Appends a menu item to |this.menu|.
+ * @param {String} textId The ID for the localized string that acts as
+ * the item's label.
+ */
+ appendMenuItem_: function(textId) {
+ var button = cr.doc.createElement('button');
+ this.menu.appendChild(button);
+ cr.ui.decorate(button, cr.ui.MenuItem);
+ button.textContent = loadTimeData.getString(textId);
+ return button;
+ },
+
+ /**
+ * Handler for the 'Collapse' and 'Expand' menu items.
+ * @param {Event} e The activation event.
+ * @private
+ */
+ onCollapseOrExpand_: function(e) {
+ this.session_.collapsed = !this.session_.collapsed;
+ this.updateMenuItems_();
+ chrome.send('setForeignSessionCollapsed',
+ [this.session_.tag, this.session_.collapsed]);
+ chrome.send('getForeignSessions'); // Refresh the list.
+
+ var eventId = this.session_.collapsed ?
+ HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION;
+ recordUmaEvent_(eventId);
+ },
+
+ /**
+ * Handler for the 'Open all' menu item.
+ * @param {Event} e The activation event.
+ * @private
+ */
+ onOpenAll_: function(e) {
+ chrome.send('openForeignSession', [this.session_.tag]);
+ recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL);
+ },
+
+ /**
+ * Set the session data for the session the context menu was invoked on.
+ * This should never be called when the menu is visible.
+ * @param {Object} session The model object for the session.
+ */
+ setSession: function(session) {
+ this.session_ = session;
+ this.updateMenuItems_();
+ },
+
+ /**
+ * Set the visibility of the Expand/Collapse menu items based on the state
+ * of the session that this menu is currently associated with.
+ * @private
+ */
+ updateMenuItems_: function() {
+ this.collapseItem_.hidden = this.session_.collapsed;
+ this.expandItem_.hidden = !this.session_.collapsed;
+ }
+ };
+
+ return {
+ OtherSessionsMenuButton: OtherSessionsMenuButton,
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/page_list_view.js b/chrome/browser/resources/ntp_search/page_list_view.js
new file mode 100644
index 0000000..0eb37be
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/page_list_view.js
@@ -0,0 +1,767 @@
+// Copyright (c) 2012 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 PageListView implementation.
+ * PageListView manages page list, dot list, switcher buttons and handles apps
+ * pages callbacks from backend.
+ *
+ * Note that you need to have AppLauncherHandler in your WebUI to use this code.
+ */
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ /**
+ * Creates a PageListView object.
+ * @constructor
+ * @extends {Object}
+ */
+ function PageListView() {
+ }
+
+ PageListView.prototype = {
+ /**
+ * The CardSlider object to use for changing app pages.
+ * @type {CardSlider|undefined}
+ */
+ cardSlider: undefined,
+
+ /**
+ * The frame div for this.cardSlider.
+ * @type {!Element|undefined}
+ */
+ sliderFrame: undefined,
+
+ /**
+ * The 'page-list' element.
+ * @type {!Element|undefined}
+ */
+ pageList: undefined,
+
+ /**
+ * A list of all 'tile-page' elements.
+ * @type {!NodeList|undefined}
+ */
+ tilePages: undefined,
+
+ /**
+ * A list of all 'apps-page' elements.
+ * @type {!NodeList|undefined}
+ */
+ appsPages: undefined,
+
+ /**
+ * The Suggestions page.
+ * @type {!Element|undefined}
+ */
+ suggestionsPage: undefined,
+
+ /**
+ * The Most Visited page.
+ * @type {!Element|undefined}
+ */
+ mostVisitedPage: undefined,
+
+ /**
+ * The 'dots-list' element.
+ * @type {!Element|undefined}
+ */
+ dotList: undefined,
+
+ /**
+ * The left and right paging buttons.
+ * @type {!Element|undefined}
+ */
+ pageSwitcherStart: undefined,
+ pageSwitcherEnd: undefined,
+
+ /**
+ * 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|undefined}
+ */
+ trash: undefined,
+
+ /**
+ * The type of page that is currently shown. The value is a numerical ID.
+ * @type {number}
+ */
+ shownPage: 0,
+
+ /**
+ * The index of the page that is currently shown, within the page type.
+ * For example if the third Apps page is showing, this will be 2.
+ * @type {number}
+ */
+ shownPageIndex: 0,
+
+ /**
+ * EventTracker for managing event listeners for page events.
+ * @type {!EventTracker}
+ */
+ eventTracker: new EventTracker,
+
+ /**
+ * If non-null, this is the ID of the app to highlight to the user the next
+ * time getAppsCallback runs. "Highlight" in this case means to switch to
+ * the page and run the new tile animation.
+ * @type {?string}
+ */
+ highlightAppId: null,
+
+ /**
+ * Initializes page list view.
+ * @param {!Element} pageList A DIV element to host all pages.
+ * @param {!Element} dotList An UL element to host nav dots. Each dot
+ * represents a page.
+ * @param {!Element} cardSliderFrame The card slider frame that hosts
+ * pageList and switcher buttons.
+ * @param {!Element|undefined} opt_trash Optional trash element.
+ * @param {!Element|undefined} opt_pageSwitcherStart Optional start page
+ * switcher button.
+ * @param {!Element|undefined} opt_pageSwitcherEnd Optional end page
+ * switcher button.
+ */
+ initialize: function(pageList, dotList, cardSliderFrame, opt_trash,
+ opt_pageSwitcherStart, opt_pageSwitcherEnd) {
+ this.pageList = pageList;
+
+ this.dotList = dotList;
+ cr.ui.decorate(this.dotList, ntp.DotList);
+
+ this.trash = opt_trash;
+ if (this.trash)
+ new ntp.Trash(this.trash);
+
+ this.pageSwitcherStart = opt_pageSwitcherStart;
+ if (this.pageSwitcherStart)
+ ntp.initializePageSwitcher(this.pageSwitcherStart);
+
+ this.pageSwitcherEnd = opt_pageSwitcherEnd;
+ if (this.pageSwitcherEnd)
+ ntp.initializePageSwitcher(this.pageSwitcherEnd);
+
+ this.shownPage = loadTimeData.getInteger('shown_page_type');
+ this.shownPageIndex = loadTimeData.getInteger('shown_page_index');
+
+ if (loadTimeData.getBoolean('showApps')) {
+ // 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');
+ } else {
+ // No apps page.
+ if (this.shownPage == loadTimeData.getInteger('apps_page_id')) {
+ this.setShownPage_(
+ loadTimeData.getInteger('most_visited_page_id'), 0);
+ }
+
+ document.body.classList.add('bare-minimum');
+ }
+
+ document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
+ // Prevent touch events from triggering any sort of native scrolling.
+ document.addEventListener('touchmove', function(e) {
+ e.preventDefault();
+ }, true);
+
+ this.tilePages = this.pageList.getElementsByClassName('tile-page');
+ this.appsPages = this.pageList.getElementsByClassName('apps-page');
+
+ // Initialize the cardSlider without any cards at the moment.
+ this.sliderFrame = cardSliderFrame;
+ this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList,
+ this.sliderFrame.offsetWidth);
+
+ // Handle mousewheel events anywhere in the card slider, so that wheel
+ // events on the page switchers will still scroll the page.
+ // This listener must be added before the card slider is initialized,
+ // because it needs to be called before the card slider's handler.
+ var cardSlider = this.cardSlider;
+ cardSliderFrame.addEventListener('mousewheel', function(e) {
+ if (cardSlider.currentCardValue.handleMouseWheel(e)) {
+ e.preventDefault(); // Prevent default scroll behavior.
+ e.stopImmediatePropagation(); // Prevent horizontal card flipping.
+ }
+ });
+
+ this.cardSlider.initialize(
+ loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));
+
+ // Handle events from the card slider.
+ this.pageList.addEventListener('cardSlider:card_changed',
+ this.onCardChanged_.bind(this));
+ this.pageList.addEventListener('cardSlider:card_added',
+ this.onCardAdded_.bind(this));
+ this.pageList.addEventListener('cardSlider:card_removed',
+ this.onCardRemoved_.bind(this));
+
+ // Ensure the slider is resized appropriately with the window.
+ window.addEventListener('resize', this.onWindowResize_.bind(this));
+
+ // Update apps when online state changes.
+ window.addEventListener('online',
+ this.updateOfflineEnabledApps_.bind(this));
+ window.addEventListener('offline',
+ this.updateOfflineEnabledApps_.bind(this));
+ },
+
+ /**
+ * Appends a tile page.
+ *
+ * @param {TilePage} page The page element.
+ * @param {string} title The title of the tile page.
+ * @param {bool} titleIsEditable If true, the title can be changed.
+ * @param {TilePage} opt_refNode Optional reference node to insert in front
+ * of.
+ * When opt_refNode is falsey, |page| will just be appended to the end of
+ * the page list.
+ */
+ appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
+ if (opt_refNode) {
+ var refIndex = this.getTilePageIndex(opt_refNode);
+ this.cardSlider.addCardAtIndex(page, refIndex);
+ } else {
+ this.cardSlider.appendCard(page);
+ }
+
+ // Remember special MostVisitedPage.
+ if (typeof ntp.MostVisitedPage != 'undefined' &&
+ page instanceof ntp.MostVisitedPage) {
+ assert(this.tilePages.length == 1,
+ 'MostVisitedPage should be added as first tile page');
+ this.mostVisitedPage = page;
+ }
+
+ if (typeof ntp.SuggestionsPage != 'undefined' &&
+ page instanceof ntp.SuggestionsPage) {
+ this.suggestionsPage = page;
+ }
+
+ // If we're appending an AppsPage and it's a temporary page, animate it.
+ var animate = page instanceof ntp.AppsPage &&
+ page.classList.contains('temporary');
+ // Make a deep copy of the dot template to add a new one.
+ var newDot = new ntp.NavDot(page, title, titleIsEditable, animate);
+ page.navigationDot = newDot;
+ this.dotList.insertBefore(newDot,
+ opt_refNode ? opt_refNode.navigationDot : null);
+ // Set a tab index on the first dot.
+ if (this.dotList.dots.length == 1)
+ newDot.tabIndex = 3;
+
+ this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
+ },
+
+ /**
+ * Called by chrome when an app has changed positions.
+ * @param {Object} appData The data for the app. This contains page and
+ * position indices.
+ */
+ appMoved: function(appData) {
+ assert(loadTimeData.getBoolean('showApps'));
+
+ var app = $(appData.id);
+ assert(app, 'trying to move an app that doesn\'t exist');
+ app.remove(false);
+
+ this.appsPages[appData.page_index].insertApp(appData, false);
+ },
+
+ /**
+ * Called by chrome when an existing app has been disabled or
+ * removed/uninstalled from chrome.
+ * @param {Object} appData A data structure full of relevant information for
+ * the app.
+ * @param {boolean} isUninstall True if the app is being uninstalled;
+ * false if the app is being disabled.
+ * @param {boolean} fromPage True if the removal was from the current page.
+ */
+ appRemoved: function(appData, isUninstall, fromPage) {
+ assert(loadTimeData.getBoolean('showApps'));
+
+ var app = $(appData.id);
+ assert(app, 'trying to remove an app that doesn\'t exist');
+
+ if (!isUninstall)
+ app.replaceAppData(appData);
+ else
+ app.remove(!!fromPage);
+ },
+
+ /**
+ * @return {boolean} If the page is still starting up.
+ * @private
+ */
+ isStartingUp_: function() {
+ return document.documentElement.classList.contains('starting-up');
+ },
+
+ /**
+ * Tracks whether apps have been loaded at least once.
+ * @type {boolean}
+ * @private
+ */
+ appsLoaded_: false,
+
+ /**
+ * 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.
+ */
+ getAppsCallback: function(data) {
+ assert(loadTimeData.getBoolean('showApps'));
+
+ var startTime = Date.now();
+
+ // Remember this to select the correct card when done rebuilding.
+ var prevCurrentCard = this.cardSlider.currentCard;
+
+ // Make removal of pages and dots as quick as possible with less DOM
+ // operations, reflows, or repaints. We set currentCard = 0 and remove
+ // from the end to not encounter any auto-magic card selections in the
+ // process and we hide the card slider throughout.
+ this.cardSlider.currentCard = 0;
+
+ // 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.
+ while (this.appsPages.length > 0)
+ this.removeTilePageAndDot_(this.appsPages[this.appsPages.length - 1]);
+
+ // Get the array of apps and add any special synthesized entries
+ var apps = data.apps;
+
+ // Get a list of page names
+ var pageNames = data.appPageNames;
+
+ function stringListIsEmpty(list) {
+ for (var i = 0; i < list.length; i++) {
+ if (list[i])
+ return false;
+ }
+ return true;
+ }
+
+ // Sort by launch ordinal
+ apps.sort(function(a, b) {
+ return a.app_launch_ordinal > b.app_launch_ordinal ? 1 :
+ a.app_launch_ordinal < b.app_launch_ordinal ? -1 : 0;
+ });
+
+ // An app to animate (in case it was just installed).
+ var highlightApp;
+
+ // If there are any pages after the apps, add new pages before them.
+ var lastAppsPage = (this.appsPages.length > 0) ?
+ this.appsPages[this.appsPages.length - 1] : null;
+ var lastAppsPageIndex = (lastAppsPage != null) ?
+ Array.prototype.indexOf.call(this.tilePages, lastAppsPage) : -1;
+ var nextPageAfterApps = lastAppsPageIndex != -1 ?
+ this.tilePages[lastAppsPageIndex + 1] : null;
+
+ // 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 >= this.appsPages.length) {
+ var pageName = loadTimeData.getString('appDefaultPageName');
+ if (this.appsPages.length < pageNames.length)
+ pageName = pageNames[this.appsPages.length];
+
+ var origPageCount = this.appsPages.length;
+ this.appendTilePage(new ntp.AppsPage(), pageName, true,
+ nextPageAfterApps);
+ // Confirm that appsPages is a live object, updated when a new page is
+ // added (otherwise we'd have an infinite loop)
+ assert(this.appsPages.length == origPageCount + 1,
+ 'expected new page');
+ }
+
+ if (app.id == this.highlightAppId)
+ highlightApp = app;
+ else
+ this.appsPages[pageIndex].insertApp(app, false);
+ }
+
+ ntp.AppsPage.setPromo(data.showPromo ? data : null);
+
+ this.cardSlider.currentCard = prevCurrentCard;
+
+ if (highlightApp)
+ this.appAdded(highlightApp, true);
+
+ logEvent('apps.layout: ' + (Date.now() - startTime));
+
+ // Tell the slider about the pages and mark the current page.
+ this.updateSliderCards();
+ this.cardSlider.currentCardValue.navigationDot.classList.add('selected');
+
+ if (!this.appsLoaded_) {
+ this.appsLoaded_ = true;
+ cr.dispatchSimpleEvent(document, 'sectionready', true, true);
+ }
+ },
+
+ /**
+ * Called by chrome when a new app has been added to chrome or has been
+ * enabled if previously disabled.
+ * @param {Object} appData A data structure full of relevant information for
+ * the app.
+ * @param {boolean=} opt_highlight Whether the app about to be added should
+ * be highlighted.
+ */
+ appAdded: function(appData, opt_highlight) {
+ assert(loadTimeData.getBoolean('showApps'));
+
+ if (appData.id == this.highlightAppId) {
+ opt_highlight = true;
+ this.highlightAppId = null;
+ }
+
+ var pageIndex = appData.page_index || 0;
+
+ if (pageIndex >= this.appsPages.length) {
+ while (pageIndex >= this.appsPages.length) {
+ this.appendTilePage(new ntp.AppsPage(),
+ loadTimeData.getString('appDefaultPageName'),
+ true);
+ }
+ this.updateSliderCards();
+ }
+
+ var page = this.appsPages[pageIndex];
+ var app = $(appData.id);
+ if (app) {
+ app.replaceAppData(appData);
+ } else if (opt_highlight) {
+ page.insertAndHighlightApp(appData);
+ this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
+ appData.page_index);
+ } else {
+ page.insertApp(appData, false);
+ }
+ },
+
+ /**
+ * Callback invoked by chrome whenever an app preference changes.
+ * @param {Object} data An object with all the data on available
+ * applications.
+ */
+ appsPrefChangedCallback: function(data) {
+ assert(loadTimeData.getBoolean('showApps'));
+
+ for (var i = 0; i < data.apps.length; ++i) {
+ $(data.apps[i].id).appData = data.apps[i];
+ }
+
+ // Set the App dot names. Skip the first dot (Most Visited).
+ var dots = this.dotList.getElementsByClassName('dot');
+ var start = this.mostVisitedPage ? 1 : 0;
+ for (var i = start; i < dots.length; ++i) {
+ dots[i].displayTitle = data.appPageNames[i - start] || '';
+ }
+ },
+
+ /**
+ * Invoked whenever the pages in apps-page-list have changed so that
+ * the Slider knows about the new elements.
+ */
+ updateSliderCards: function() {
+ var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard,
+ this.tilePages.length - 1));
+ this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
+ pageNo);
+ switch (this.shownPage) {
+ case loadTimeData.getInteger('apps_page_id'):
+ this.cardSlider.selectCardByValue(
+ this.appsPages[Math.min(this.shownPageIndex,
+ this.appsPages.length - 1)]);
+ break;
+ case loadTimeData.getInteger('most_visited_page_id'):
+ if (this.mostVisitedPage)
+ this.cardSlider.selectCardByValue(this.mostVisitedPage);
+ break;
+ case loadTimeData.getInteger('suggestions_page_id'):
+ if (this.suggestionsPage)
+ this.cardSlider.selectCardByValue(this.suggestionsPage);
+ break;
+ }
+ },
+
+ /**
+ * Called whenever tiles should be re-arranging themselves out of the way
+ * of a moving or insert tile.
+ */
+ enterRearrangeMode: function() {
+ if (loadTimeData.getBoolean('showApps')) {
+ var tempPage = new ntp.AppsPage();
+ tempPage.classList.add('temporary');
+ var pageName = loadTimeData.getString('appDefaultPageName');
+ this.appendTilePage(tempPage, pageName, true);
+ }
+
+ if (ntp.getCurrentlyDraggingTile().firstChild.canBeRemoved())
+ $('footer').classList.add('showing-trash-mode');
+
+ document.documentElement.classList.add('dragging-mode');
+ },
+
+ /**
+ * Invoked whenever some app is released
+ */
+ leaveRearrangeMode: function() {
+ var tempPage = document.querySelector('.tile-page.temporary');
+ if (tempPage) {
+ var dot = tempPage.navigationDot;
+ if (!tempPage.tileCount &&
+ tempPage != this.cardSlider.currentCardValue) {
+ this.removeTilePageAndDot_(tempPage, true);
+ } else {
+ tempPage.classList.remove('temporary');
+ this.saveAppPageName(tempPage,
+ loadTimeData.getString('appDefaultPageName'));
+ }
+ }
+
+ $('footer').classList.remove('showing-trash-mode');
+ document.documentElement.classList.remove('dragging-mode');
+ },
+
+ /**
+ * Callback for the 'pagelayout' event.
+ * @param {Event} e The event.
+ */
+ onPageLayout_: function(e) {
+ if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
+ this.cardSlider.currentCard) {
+ return;
+ }
+
+ this.updatePageSwitchers();
+ },
+
+ /**
+ * Adjusts the size and position of the page switchers according to the
+ * layout of the current card.
+ */
+ updatePageSwitchers: function() {
+ if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
+ return;
+
+ var page = this.cardSlider.currentCardValue;
+
+ this.pageSwitcherStart.hidden = !page ||
+ (this.cardSlider.currentCard == 0);
+ this.pageSwitcherEnd.hidden = !page ||
+ (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);
+
+ if (!page)
+ return;
+
+ var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd :
+ this.pageSwitcherStart;
+ var pageSwitcherRight = isRTL() ? this.pageSwitcherStart :
+ this.pageSwitcherEnd;
+ var scrollbarWidth = page.scrollbarWidth;
+ pageSwitcherLeft.style.width =
+ (page.sideMargin + 13) + 'px';
+ pageSwitcherLeft.style.left = '0';
+ pageSwitcherRight.style.width =
+ (page.sideMargin - scrollbarWidth + 13) + 'px';
+ pageSwitcherRight.style.right = scrollbarWidth + 'px';
+
+ var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
+ pageSwitcherLeft.style.top = offsetTop;
+ pageSwitcherRight.style.top = offsetTop;
+ pageSwitcherLeft.style.paddingBottom = offsetTop;
+ pageSwitcherRight.style.paddingBottom = offsetTop;
+ },
+
+ /**
+ * Returns the index of the given apps page.
+ * @param {AppsPage} page The AppsPage we wish to find.
+ * @return {number} The index of |page| or -1 if it is not in the
+ * collection.
+ */
+ getAppsPageIndex: function(page) {
+ return Array.prototype.indexOf.call(this.appsPages, page);
+ },
+
+ /**
+ * Handler for cardSlider:card_changed events from this.cardSlider.
+ * @param {Event} e The cardSlider:card_changed event.
+ * @private
+ */
+ onCardChanged_: function(e) {
+ var page = e.cardSlider.currentCardValue;
+
+ // Don't change shownPage until startup is done (and page changes actually
+ // reflect user actions).
+ if (!this.isStartingUp_()) {
+ if (page.classList.contains('apps-page')) {
+ this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
+ this.getAppsPageIndex(page));
+ } else if (page.classList.contains('most-visited-page')) {
+ this.setShownPage_(
+ loadTimeData.getInteger('most_visited_page_id'), 0);
+ } else if (page.classList.contains('suggestions-page')) {
+ this.setShownPage_(loadTimeData.getInteger('suggestions_page_id'), 0);
+ } else {
+ console.error('unknown page selected');
+ }
+ }
+
+ // Update the active dot
+ var curDot = this.dotList.getElementsByClassName('selected')[0];
+ if (curDot)
+ curDot.classList.remove('selected');
+ page.navigationDot.classList.add('selected');
+ this.updatePageSwitchers();
+ },
+
+ /**
+ * Saves/updates the newly selected page to open when first loading the NTP.
+ * @type {number} shownPage The new shown page type.
+ * @type {number} shownPageIndex The new shown page index.
+ * @private
+ */
+ setShownPage_: function(shownPage, shownPageIndex) {
+ assert(shownPageIndex >= 0);
+ this.shownPage = shownPage;
+ this.shownPageIndex = shownPageIndex;
+ chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
+ },
+
+ /**
+ * Listen for card additions to update the page switchers or the current
+ * card accordingly.
+ * @param {Event} e A card removed or added event.
+ */
+ onCardAdded_: function(e) {
+ // When the second arg passed to insertBefore is falsey, it acts just like
+ // appendChild.
+ this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]);
+ this.onCardAddedOrRemoved_();
+ },
+
+ /**
+ * Listen for card removals to update the page switchers or the current card
+ * accordingly.
+ * @param {Event} e A card removed or added event.
+ */
+ onCardRemoved_: function(e) {
+ e.removedCard.parentNode.removeChild(e.removedCard);
+ this.onCardAddedOrRemoved_();
+ },
+
+ /**
+ * Called when a card is removed or added.
+ * @private
+ */
+ onCardAddedOrRemoved_: function() {
+ if (this.isStartingUp_())
+ return;
+
+ // Without repositioning there were issues - http://crbug.com/133457.
+ this.cardSlider.repositionFrame();
+ this.updatePageSwitchers();
+ },
+
+ /**
+ * Save the name of an apps page.
+ * Store the apps page name into the preferences store.
+ * @param {AppsPage} appsPage The app page for which we wish to save.
+ * @param {string} name The name of the page.
+ */
+ saveAppPageName: function(appPage, name) {
+ var index = this.getAppsPageIndex(appPage);
+ assert(index != -1);
+ chrome.send('saveAppPageName', [name, index]);
+ },
+
+ /**
+ * Window resize handler.
+ * @private
+ */
+ onWindowResize_: function(e) {
+ this.cardSlider.resize(this.sliderFrame.offsetWidth);
+ this.updatePageSwitchers();
+ },
+
+ /**
+ * Listener for offline status change events. Updates apps that are
+ * not offline-enabled to be grayscale if the browser is offline.
+ * @private
+ */
+ updateOfflineEnabledApps_: function() {
+ var apps = document.querySelectorAll('.app');
+ for (var i = 0; i < apps.length; ++i) {
+ if (apps[i].appData.enabled && !apps[i].appData.offline_enabled) {
+ apps[i].setIcon();
+ apps[i].loadIcon();
+ }
+ }
+ },
+
+ /**
+ * Handler for key events on the page. Ctrl-Arrow will switch the visible
+ * page.
+ * @param {Event} e The KeyboardEvent.
+ * @private
+ */
+ onDocKeyDown_: function(e) {
+ if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
+ return;
+
+ var direction = 0;
+ if (e.keyIdentifier == 'Left')
+ direction = -1;
+ else if (e.keyIdentifier == 'Right')
+ direction = 1;
+ else
+ return;
+
+ var cardIndex =
+ (this.cardSlider.currentCard + direction +
+ this.cardSlider.cardCount) % this.cardSlider.cardCount;
+ this.cardSlider.selectCard(cardIndex, true);
+
+ e.stopPropagation();
+ },
+
+ /**
+ * Returns the index of a given tile page.
+ * @param {TilePage} page The TilePage we wish to find.
+ * @return {number} The index of |page| or -1 if it is not in the
+ * collection.
+ */
+ getTilePageIndex: function(page) {
+ return Array.prototype.indexOf.call(this.tilePages, page);
+ },
+
+ /**
+ * Removes a page and navigation dot (if the navdot exists).
+ * @param {TilePage} page The page to be removed.
+ * @param {boolean=} opt_animate If the removal should be animated.
+ */
+ removeTilePageAndDot_: function(page, opt_animate) {
+ if (page.navigationDot)
+ page.navigationDot.remove(opt_animate);
+ this.cardSlider.removeCard(page);
+ },
+ };
+
+ return {
+ PageListView: PageListView
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/page_switcher.js b/chrome/browser/resources/ntp_search/page_switcher.js
new file mode 100644
index 0000000..2ae7f75
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/page_switcher.js
@@ -0,0 +1,107 @@
+// Copyright (c) 2012 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 Page switcher
+ * This is the class for the left and right navigation arrows that switch
+ * between pages.
+ */
+cr.define('ntp', function() {
+
+ function PageSwitcher() {
+ }
+
+ PageSwitcher.template = {
+ __proto__: HTMLButtonElement.prototype,
+
+ decorate: function(el) {
+ el.__proto__ = PageSwitcher.template;
+
+ el.addEventListener('click', el.activate_);
+
+ el.direction_ = el.id == 'page-switcher-start' ? -1 : 1;
+
+ el.dragWrapper_ = new cr.ui.DragWrapper(el, el);
+ },
+
+ /**
+ * Activate the switcher (go to the next card).
+ * @private
+ */
+ activate_: function() {
+ var cardSlider = ntp.getCardSlider();
+ var index = cardSlider.currentCard + this.direction_;
+ var numCards = cardSlider.cardCount - 1;
+ cardSlider.selectCard(Math.max(0, Math.min(index, numCards)), true);
+ },
+
+ shouldAcceptDrag: function(e) {
+ // We allow all drags to trigger the page switching effect.
+ return true;
+ },
+
+ doDragEnter: function(e) {
+ this.scheduleDelayedSwitch_();
+ this.doDragOver(e);
+ },
+
+ doDragLeave: function(e) {
+ this.cancelDelayedSwitch_();
+ },
+
+ doDragOver: function(e) {
+ e.preventDefault();
+ var targetPage = ntp.getCardSlider().currentCardValue;
+ if (targetPage.shouldAcceptDrag(e))
+ targetPage.setDropEffect(e.dataTransfer);
+ },
+
+ doDrop: function(e) {
+ e.stopPropagation();
+ this.cancelDelayedSwitch_();
+
+ var tile = ntp.getCurrentlyDraggingTile();
+ if (!tile)
+ return;
+
+ var sourcePage = tile.tilePage;
+ var targetPage = ntp.getCardSlider().currentCardValue;
+ if (targetPage == sourcePage || !targetPage.shouldAcceptDrag(e))
+ return;
+
+ targetPage.appendDraggingTile();
+ },
+
+ /**
+ * Starts a timer to activate the switcher. The timer repeats until
+ * cancelled by cancelDelayedSwitch_.
+ * @private
+ */
+ scheduleDelayedSwitch_: function() {
+ var self = this;
+ function navPageClearTimeout() {
+ self.activate_();
+ self.dragNavTimeout_ = null;
+ self.scheduleDelayedSwitch_();
+ }
+ this.dragNavTimeout_ = window.setTimeout(navPageClearTimeout, 500);
+ },
+
+ /**
+ * Cancels the timer that activates the switcher while dragging.
+ * @private
+ */
+ cancelDelayedSwitch_: function() {
+ if (this.dragNavTimeout_) {
+ window.clearTimeout(this.dragNavTimeout_);
+ this.dragNavTimeout_ = null;
+ }
+ },
+
+ };
+
+ return {
+ initializePageSwitcher: PageSwitcher.template.decorate
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/recently_closed.js b/chrome/browser/resources/ntp_search/recently_closed.js
new file mode 100644
index 0000000..6ece9de
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/recently_closed.js
@@ -0,0 +1,114 @@
+// Copyright (c) 2012 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 The recently closed menu: button, model data, and menu.
+ */
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ /**
+ * Returns the text used for a recently closed window.
+ * @param {number} numTabs Number of tabs in the window.
+ * @return {string} The text to use.
+ */
+ function formatTabsText(numTabs) {
+ if (numTabs == 1)
+ return loadTimeData.getString('closedwindowsingle');
+ return loadTimeData.getStringF('closedwindowmultiple', numTabs);
+ }
+
+ var Menu = cr.ui.Menu;
+ var MenuItem = cr.ui.MenuItem;
+ var MenuButton = cr.ui.MenuButton;
+ var RecentMenuButton = cr.ui.define('button');
+
+ RecentMenuButton.prototype = {
+ __proto__: MenuButton.prototype,
+
+ decorate: function() {
+ MenuButton.prototype.decorate.call(this);
+ this.menu = new Menu;
+ cr.ui.decorate(this.menu, Menu);
+ this.menu.classList.add('footer-menu');
+ document.body.appendChild(this.menu);
+
+ this.needsRebuild_ = true;
+ this.classList.add('invisible');
+ this.anchorType = cr.ui.AnchorType.ABOVE;
+ this.invertLeftRight = true;
+ },
+
+ /**
+ * Shows the menu, first rebuilding it if necessary.
+ * TODO(estade): the right of the menu should align with the right of the
+ * button.
+ * @override
+ */
+ showMenu: function() {
+ if (this.needsRebuild_) {
+ this.menu.textContent = '';
+ this.dataItems_.forEach(this.addItem_, this);
+ this.needsRebuild_ = false;
+ }
+
+ MenuButton.prototype.showMenu.call(this);
+ },
+
+ /**
+ * Sets the menu model data.
+ * @param {Array} dataItems Array of objects that describe the apps.
+ */
+ set dataItems(dataItems) {
+ this.dataItems_ = dataItems;
+ this.needsRebuild_ = true;
+ if (dataItems.length)
+ this.classList.remove('invisible');
+ else
+ this.classList.add('invisible');
+ },
+
+ /**
+ * Adds an app to the menu.
+ * @param {Object} data An object encapsulating all data about the app.
+ * @private
+ */
+ addItem_: function(data) {
+ var isWindow = data.type == 'window';
+ var a = this.ownerDocument.createElement('a');
+ a.className = 'footer-menu-item';
+ if (isWindow) {
+ a.href = '';
+ a.classList.add('recent-window');
+ a.textContent = formatTabsText(data.tabs.length);
+ a.title = data.tabs.map(function(tab) { return tab.title; }).join('\n');
+ } else {
+ a.href = data.url;
+ a.style.backgroundImage = 'url(chrome://favicon/' + data.url + ')';
+ a.textContent = data.title;
+ }
+
+ function onClick(e) {
+ chrome.send('recordAppLaunchByURL',
+ [encodeURIComponent(data.url),
+ ntp.APP_LAUNCH.NTP_RECENTLY_CLOSED]);
+ var index = Array.prototype.indexOf.call(a.parentNode.children, a);
+ chrome.send('reopenTab', [data.sessionId, index,
+ e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
+ // We are likely deleted by this point!
+
+ e.preventDefault();
+ }
+ a.addEventListener('click', onClick);
+
+ this.menu.appendChild(a);
+ cr.ui.decorate(a, MenuItem);
+ },
+ };
+
+ return {
+ RecentMenuButton: RecentMenuButton,
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/suggestions_page.css b/chrome/browser/resources/ntp_search/suggestions_page.css
new file mode 100644
index 0000000..747d206
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/suggestions_page.css
@@ -0,0 +1,109 @@
+/* Copyright (c) 2012 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. */
+
+.suggestions {
+ position: absolute;
+ z-index: 0;
+}
+
+.suggestions {
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ position: absolute;
+ text-decoration: none;
+}
+
+.suggestions:focus {
+ outline: none;
+}
+
+.suggestions .close-button {
+ -webkit-transition: opacity 150ms;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 5;
+}
+
+html[dir=rtl] .suggestions .close-button {
+ left: 0;
+ right: auto;
+}
+
+.suggestions:hover .close-button {
+ -webkit-transition-delay: 500ms;
+ opacity: 1;
+}
+
+.suggestions .close-button:hover {
+ -webkit-transition: none;
+}
+
+.suggestions .favicon {
+ -webkit-margin-start: 5px;
+ background: no-repeat left 50%;
+ bottom: 7px;
+ box-sizing: border-box;
+ display: block;
+ height: 16px;
+ position: absolute;
+ width: 16px;
+}
+
+html[dir='rtl'] .suggestions .favicon {
+ background-position-x: right;
+}
+
+.suggestions .color-stripe {
+ border-bottom-left-radius: 3px 3px;
+ border-bottom-right-radius: 3px 3px;
+ /* Matches height of title plus height of score. */
+ bottom: 36px;
+ height: 3px;
+ position: absolute;
+ width: 100%;
+ z-index: 10;
+}
+
+.suggestions .title {
+ display: block;
+ height: 18px;
+ overflow: hidden;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.suggestions .score {
+ display: block;
+ height: 18px;
+ overflow: hidden;
+ text-align: center;
+}
+
+.suggestions:focus .thumbnail,
+.suggestions:hover .thumbnail {
+ opacity: 0.95;
+}
+
+.suggestions:focus .thumbnail-shield,
+.suggestions:hover .thumbnail-shield,
+.suggestions:active .thumbnail-shield {
+ background: -webkit-linear-gradient(rgba(255, 255, 255, 0),
+ rgba(255, 255, 255, 0) 80%,
+ rgba(255, 255, 255, 0.9));
+}
+
+/* The thumbnail gets lighter when clicked, but not when the click is on the
+ * close button. */
+.suggestions:active .close-button:not(:active) + .thumbnail {
+ opacity: 0.9;
+}
+
+/* The thumbnail gets a shadow when clicked, but not when the click is on the
+ * close button. */
+.suggestions:active .close-button:not(:active) + .thumbnail .thumbnail-shield {
+ -webkit-box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.2);
+}
diff --git a/chrome/browser/resources/ntp_search/suggestions_page.js b/chrome/browser/resources/ntp_search/suggestions_page.js
new file mode 100644
index 0000000..4e4f0d6
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/suggestions_page.js
@@ -0,0 +1,471 @@
+// Copyright (c) 2012 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.
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ var TilePage = ntp.TilePage;
+
+ /**
+ * A counter for generating unique tile IDs.
+ */
+ var tileID = 0;
+
+ /**
+ * Creates a new Suggestions page object for tiling.
+ * @constructor
+ * @extends {HTMLAnchorElement}
+ */
+ function Suggestion() {
+ var el = cr.doc.createElement('a');
+ el.__proto__ = Suggestion.prototype;
+ el.initialize();
+
+ return el;
+ }
+
+ Suggestion.prototype = {
+ __proto__: HTMLAnchorElement.prototype,
+
+ initialize: function() {
+ this.reset();
+
+ this.addEventListener('click', this.handleClick_);
+ this.addEventListener('keydown', this.handleKeyDown_);
+ },
+
+ get index() {
+ assert(this.tile);
+ return this.tile.index;
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ /**
+ * Clears the DOM hierarchy for this node, setting it back to the default
+ * for a blank thumbnail. TODO(georgey) make it a template.
+ */
+ reset: function() {
+ this.className = 'suggestions filler real';
+ this.innerHTML =
+ '<span class="thumbnail-wrapper fills-parent">' +
+ '<div class="close-button"></div>' +
+ '<span class="thumbnail fills-parent">' +
+ // thumbnail-shield provides a gradient fade effect.
+ '<div class="thumbnail-shield fills-parent"></div>' +
+ '</span>' +
+ '<span class="favicon"></span>' +
+ '</span>' +
+ '<div class="color-stripe"></div>' +
+ '<span class="title"></span>' +
+ '<span class="score"></span>';
+
+ this.querySelector('.close-button').title =
+ loadTimeData.getString('removethumbnailtooltip');
+
+ this.tabIndex = -1;
+ this.data_ = null;
+ this.removeAttribute('id');
+ this.title = '';
+ },
+
+ /**
+ * Update the appearance of this tile according to |data|.
+ * @param {Object} data A dictionary of relevant data for the page.
+ */
+ updateForData: function(data) {
+ if (this.classList.contains('blacklisted') && data) {
+ // Animate appearance of new tile.
+ this.classList.add('new-tile-contents');
+ }
+ this.classList.remove('blacklisted');
+
+ if (!data || data.filler) {
+ if (this.data_)
+ this.reset();
+ return;
+ }
+
+ var id = tileID++;
+ this.id = 'suggestions-tile-' + id;
+ this.data_ = data;
+ this.classList.add('focusable');
+
+ var faviconDiv = this.querySelector('.favicon');
+ var faviconUrl = 'chrome://favicon/size/16/' + data.url;
+ faviconDiv.style.backgroundImage = url(faviconUrl);
+ chrome.send('getFaviconDominantColor', [faviconUrl, this.id]);
+
+ var title = this.querySelector('.title');
+ title.textContent = data.title;
+ title.dir = data.direction;
+
+ var score = this.querySelector('.score');
+ score.textContent = data.score;
+
+ // Sets the tooltip.
+ this.title = data.title;
+
+ var thumbnailUrl = 'chrome://thumb/' + data.url;
+ this.querySelector('.thumbnail').style.backgroundImage =
+ url(thumbnailUrl);
+
+ this.href = data.url;
+
+ this.classList.remove('filler');
+ },
+
+ /**
+ * Sets the color of the favicon dominant color bar.
+ * @param {string} color The css-parsable value for the color.
+ */
+ set stripeColor(color) {
+ this.querySelector('.color-stripe').style.backgroundColor = color;
+ },
+
+ /**
+ * Handles a click on the tile.
+ * @param {Event} e The click event.
+ */
+ handleClick_: function(e) {
+ if (e.target.classList.contains('close-button')) {
+ this.blacklist_();
+ e.preventDefault();
+ } else {
+ // Records the index of this tile.
+ chrome.send('metricsHandler:recordInHistogram',
+ ['NewTabPage.SuggestedSite', this.index, 8]);
+ chrome.send('suggestedSitesAction',
+ [ntp.NtpFollowAction.CLICKED_TILE]);
+ }
+ },
+
+ /**
+ * Allow blacklisting suggestions site using the keyboard.
+ * @param {Event} e The keydown event.
+ */
+ handleKeyDown_: function(e) {
+ if (!cr.isMac && e.keyCode == 46 || // Del
+ cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
+ this.blacklist_();
+ }
+ },
+
+ /**
+ * Permanently removes a page from Suggestions.
+ */
+ blacklist_: function() {
+ this.showUndoNotification_();
+ chrome.send('blacklistURLFromSuggestions', [this.data_.url]);
+ this.reset();
+ chrome.send('getSuggestions');
+ this.classList.add('blacklisted');
+ },
+
+ /**
+ * Shows notification that you can undo blacklisting.
+ */
+ showUndoNotification_: function() {
+ var data = this.data_;
+ var self = this;
+ var doUndo = function() {
+ chrome.send('removeURLsFromSuggestionsBlacklist', [data.url]);
+ self.updateForData(data);
+ };
+
+ var undo = {
+ action: doUndo,
+ text: loadTimeData.getString('undothumbnailremove'),
+ };
+
+ var undoAll = {
+ action: function() {
+ chrome.send('clearSuggestionsURLsBlacklist');
+ },
+ text: loadTimeData.getString('restoreThumbnailsShort'),
+ };
+
+ ntp.showNotification(
+ loadTimeData.getString('thumbnailremovednotification'),
+ [undo, undoAll]);
+ },
+
+ /**
+ * Set the size and position of the suggestions tile.
+ * @param {number} size The total size of |this|.
+ * @param {number} x The x-position.
+ * @param {number} y The y-position.
+ */
+ setBounds: function(size, x, y) {
+ this.style.width = size + 'px';
+ this.style.height = heightForWidth(size) + 'px';
+
+ this.style.left = x + 'px';
+ this.style.right = x + 'px';
+ this.style.top = y + 'px';
+ },
+
+ /**
+ * Returns whether this element can be 'removed' from chrome (i.e. whether
+ * the user can drag it onto the trash and expect something to happen).
+ * @return {boolean} True, since suggestions pages can always be
+ * blacklisted.
+ */
+ canBeRemoved: function() {
+ return true;
+ },
+
+ /**
+ * Removes this element from chrome, i.e. blacklists it.
+ */
+ removeFromChrome: function() {
+ this.blacklist_();
+ this.parentNode.classList.add('finishing-drag');
+ },
+
+ /**
+ * Called when a drag of this tile has ended (after all animations have
+ * finished).
+ */
+ finalizeDrag: function() {
+ this.parentNode.classList.remove('finishing-drag');
+ },
+
+ /**
+ * Called when a drag is starting on the tile. Updates dataTransfer with
+ * data for this tile (for dragging outside of the NTP).
+ * @param {Event.DataTransfer} dataTransfer The drag event data store.
+ */
+ setDragData: function(dataTransfer) {
+ dataTransfer.setData('Text', this.data_.title);
+ dataTransfer.setData('URL', this.data_.url);
+ },
+ };
+
+ var suggestionsPageGridValues = {
+ // The fewest tiles we will show in a row.
+ minColCount: 2,
+ // The suggestions we will show in a row.
+ maxColCount: 4,
+
+ // The smallest a tile can be.
+ minTileWidth: 122,
+ // The biggest a tile can be. 212 (max thumbnail width) + 2.
+ maxTileWidth: 214,
+
+ // The padding between tiles, as a fraction of the tile width.
+ tileSpacingFraction: 1 / 8,
+ };
+ TilePage.initGridValues(suggestionsPageGridValues);
+
+ /**
+ * Calculates the height for a Suggestion tile for a given width. The size
+ * is based on the thumbnail, which should have a 212:132 ratio.
+ * @return {number} The height.
+ */
+ function heightForWidth(width) {
+ // The 2s are for borders, the 36 is for the title and score.
+ return (width - 2) * 132 / 212 + 2 + 36;
+ }
+
+ var THUMBNAIL_COUNT = 8;
+
+ /**
+ * Creates a new SuggestionsPage object.
+ * @constructor
+ * @extends {TilePage}
+ */
+ function SuggestionsPage() {
+ var el = new TilePage(suggestionsPageGridValues);
+ el.__proto__ = SuggestionsPage.prototype;
+ el.initialize();
+
+ return el;
+ }
+
+ SuggestionsPage.prototype = {
+ __proto__: TilePage.prototype,
+
+ initialize: function() {
+ this.classList.add('suggestions-page');
+ this.data_ = null;
+ this.suggestionsTiles_ = this.getElementsByClassName('suggestions real');
+
+ this.addEventListener('carddeselected', this.handleCardDeselected_);
+ this.addEventListener('cardselected', this.handleCardSelected_);
+ },
+
+ /**
+ * Create blank (filler) tiles.
+ * @private
+ */
+ createTiles_: function() {
+ for (var i = 0; i < THUMBNAIL_COUNT; i++) {
+ this.appendTile(new Suggestion());
+ }
+ },
+
+ /**
+ * Update the tiles after a change to |this.data_|.
+ */
+ updateTiles_: function() {
+ for (var i = 0; i < THUMBNAIL_COUNT; i++) {
+ var page = this.data_[i];
+ var tile = this.suggestionsTiles_[i];
+
+ if (i >= this.data_.length)
+ tile.reset();
+ else
+ tile.updateForData(page);
+ }
+ },
+
+ /**
+ * Handles the 'card deselected' event (i.e. the user clicked to another
+ * pane).
+ * @param {Event} e The CardChanged event.
+ */
+ handleCardDeselected_: function(e) {
+ if (!document.documentElement.classList.contains('starting-up')) {
+ chrome.send('suggestedSitesAction',
+ [ntp.NtpFollowAction.CLICKED_OTHER_NTP_PANE]);
+ }
+ },
+
+ /**
+ * Handles the 'card selected' event (i.e. the user clicked to select the
+ * Suggested pane).
+ * @param {Event} e The CardChanged event.
+ */
+ handleCardSelected_: function(e) {
+ if (!document.documentElement.classList.contains('starting-up'))
+ chrome.send('suggestedSitesSelected');
+ },
+
+ /**
+ * Array of suggestions data objects.
+ * @type {Array}
+ */
+ get data() {
+ return this.data_;
+ },
+ set data(data) {
+ var startTime = Date.now();
+
+ // The first time data is set, create the tiles.
+ if (!this.data_) {
+ this.createTiles_();
+ this.data_ = data.slice(0, THUMBNAIL_COUNT);
+ } else {
+ this.data_ = refreshData(this.data_, data);
+ }
+
+ this.updateTiles_();
+ logEvent('suggestions.layout: ' + (Date.now() - startTime));
+ },
+
+ /** @inheritDoc */
+ shouldAcceptDrag: function(e) {
+ return false;
+ },
+
+ /** @inheritDoc */
+ heightForWidth: heightForWidth,
+ };
+
+ /**
+ * Executed once the NTP has loaded. Checks if the Suggested pane is
+ * shown or not. If it is shown, the 'suggestedSitesSelected' message is sent
+ * to the C++ code, to record the fact that the user has seen this pane.
+ */
+ SuggestionsPage.onLoaded = function() {
+ if (ntp.getCardSlider() &&
+ ntp.getCardSlider().currentCardValue &&
+ ntp.getCardSlider().currentCardValue.classList
+ .contains('suggestions-page')) {
+ chrome.send('suggestedSitesSelected');
+ }
+ }
+
+ /**
+ * We've gotten additional data for Suggestions page. Update our old data with
+ * the new data. The ordering of the new data is not important, except when a
+ * page is pinned. Thus we try to minimize re-ordering.
+ * @param {Array} oldData The current Suggestions page list.
+ * @param {Array} newData The new Suggestions page list.
+ * @return {Array} The merged page list that should replace the current page
+ * list.
+ */
+ function refreshData(oldData, newData) {
+ oldData = oldData.slice(0, THUMBNAIL_COUNT);
+ newData = newData.slice(0, THUMBNAIL_COUNT);
+
+ // Copy over pinned sites directly.
+ for (var i = 0; i < newData.length; i++) {
+ if (newData[i].pinned) {
+ oldData[i] = newData[i];
+ // Mark the entry as 'updated' so we don't try to update again.
+ oldData[i].updated = true;
+ // Mark the newData page as 'used' so we don't try to re-use it.
+ newData[i].used = true;
+ }
+ }
+
+ // Look through old pages; if they exist in the newData list, keep them
+ // where they are.
+ for (var i = 0; i < oldData.length; i++) {
+ if (!oldData[i] || oldData[i].updated)
+ continue;
+
+ for (var j = 0; j < newData.length; j++) {
+ if (newData[j].used)
+ continue;
+
+ if (newData[j].url == oldData[i].url) {
+ // The background image and other data may have changed.
+ oldData[i] = newData[j];
+ oldData[i].updated = true;
+ newData[j].used = true;
+ break;
+ }
+ }
+ }
+
+ // Look through old pages that haven't been updated yet; replace them.
+ for (var i = 0; i < oldData.length; i++) {
+ if (oldData[i] && oldData[i].updated)
+ continue;
+
+ for (var j = 0; j < newData.length; j++) {
+ if (newData[j].used)
+ continue;
+
+ oldData[i] = newData[j];
+ oldData[i].updated = true;
+ newData[j].used = true;
+ break;
+ }
+
+ if (oldData[i] && !oldData[i].updated)
+ oldData[i] = null;
+ }
+
+ // Clear 'updated' flags so this function will work next time it's called.
+ for (var i = 0; i < THUMBNAIL_COUNT; i++) {
+ if (oldData[i])
+ oldData[i].updated = false;
+ }
+
+ return oldData;
+ }
+
+ return {
+ SuggestionsPage: SuggestionsPage,
+ refreshData: refreshData,
+ };
+});
+
+document.addEventListener('ntpLoaded', ntp.SuggestionsPage.onLoaded);
diff --git a/chrome/browser/resources/ntp_search/tile_page.css b/chrome/browser/resources/ntp_search/tile_page.css
new file mode 100644
index 0000000..08ff22b
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/tile_page.css
@@ -0,0 +1,194 @@
+/* Copyright (c) 2012 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. */
+
+.tile-page {
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ height: 100%;
+ position: relative;
+ width: 100%;
+}
+
+.tile-page-scrollbar {
+ -webkit-box-sizing: border-box;
+ margin: 0 4px;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ width: 5px;
+ z-index: 5;
+}
+
+.tile-page-content {
+ -webkit-box-flex: 1;
+ /* Don't apply clip mask to padding. */
+ -webkit-mask-clip: content-box;
+ /* TODO(estade): this mask is disabled for technical reasons. It negatively
+ * impacts performance of page switching, also it causes problems with Mac
+ * text: http://crbug.com/86955
+ -webkit-mask-image: -webkit-linear-gradient(bottom, transparent, black 30px);
+ */
+ /* The following four properties are necessary so that the mask won't clip
+ * the scrollbar. */
+ box-sizing: border-box;
+ overflow-y: scroll;
+ /* Scrollbar width(13px) + balance right padding. */
+ padding-left: 93px;
+ padding-right: 80px;
+ /* This value is mirrored in TilePage.updateTopMargin_ */
+ padding-top: 60px;
+ position: relative;
+ text-align: center;
+ width: 100%;
+}
+
+.top-margin {
+ /* The only reason height is set to 1px, rather than left at 0, is that
+ * otherwise webkit collapses the top and bottom margins. */
+ height: 1px;
+}
+
+.tile-grid {
+ position: relative;
+ width: 100%;
+}
+
+.tile {
+ -webkit-print-color-adjust: exact;
+ /* Don't offer the context menu on long-press. */
+ -webkit-touch-callout: none;
+ -webkit-user-drag: element;
+ display: inline-block;
+ position: absolute;
+}
+
+/* NOTE: Dopplegangers nest themselves inside of other tiles, so don't
+ * accidentally double apply font-size to them. */
+.tile:not(.doppleganger) {
+ font-size: 1.2em;
+}
+
+/* Not real but not a doppleganger: show nothing. This state exists for a
+ * webstore tile that's on the same page as a [+]. */
+.tile:not(.real):not(.doppleganger) {
+ display: none;
+}
+
+/* I don't know why this is necessary. -webkit-user-drag: element on .tile
+ * should be enough. If we don't do this, we get 2 drag representations for
+ * the image. */
+.tile img {
+ -webkit-user-drag: none;
+}
+
+.doppleganger {
+ left: 0 !important;
+ right: 0 !important;
+ top: 0 !important;
+}
+
+.tile.dragging {
+ opacity: 0;
+}
+
+.tile.drag-representation {
+ -webkit-transition: opacity 200ms;
+ pointer-events: none;
+ position: fixed;
+ z-index: 3;
+}
+
+.tile.drag-representation.placing > * {
+ -webkit-transition: -webkit-transform 200ms;
+}
+
+/* When a drag finishes while we're not showing the page where the tile
+ * belongs, the tile shrinks to a dot. */
+.tile.drag-representation.dropped-on-other-page > * {
+ -webkit-transform: scale(0) rotate(0);
+}
+
+.tile.drag-representation.deleting > * {
+ -webkit-transform: scale(0) rotate(360deg);
+ -webkit-transition: -webkit-transform 600ms;
+}
+
+.animating-tile-page .tile,
+.tile.drag-representation.placing {
+ -webkit-transition: left 200ms, right 200ms, top 200ms;
+}
+
+.hovering-on-trash {
+ opacity: 0.6;
+}
+
+.animating-tile-page .top-margin {
+ -webkit-transition: margin-bottom 200ms;
+}
+
+.animating-tile-page #notification-container {
+ -webkit-transition: margin 200ms, opacity 200ms;
+}
+
+@-webkit-keyframes bounce {
+ 0% {
+ -webkit-transform: scale(0, 0);
+ }
+
+ 60% {
+ -webkit-transform: scale(1.2, 1.2);
+ }
+
+ 100% {
+ -webkit-transform: scale(1, 1);
+ }
+}
+
+.tile > .new-tile-contents {
+ -webkit-animation: bounce 500ms ease-in-out;
+}
+
+@-webkit-keyframes blipout {
+ 0% {
+ -webkit-transform: scale(1, 1);
+ }
+
+ 60% {
+ -webkit-animation-timing-function: ease-in;
+ -webkit-transform: scale(1.3, 0.02);
+ opacity: 1;
+ }
+
+ 90% {
+ -webkit-animation-timing-function: default;
+ -webkit-transform: scale(0.3, 0.02);
+ opacity: 0.7;
+ }
+
+ 100% {
+ -webkit-animation-timing-function: linear;
+ -webkit-transform: scale(0.3, 0.02);
+ opacity: 0;
+ }
+}
+
+.tile > .removing-tile-contents {
+ -webkit-animation: blipout 300ms;
+ -webkit-animation-fill-mode: forwards;
+ pointer-events: none;
+}
+
+.tile-page:not(.selected-card) * {
+ -webkit-transition: none !important;
+}
+
+/** Scrollbars ****************************************************************/
+
+.tile-page-content::-webkit-scrollbar {
+ width: 13px;
+}
+
+.tile-page-content::-webkit-scrollbar-button {
+ display: none;
+}
diff --git a/chrome/browser/resources/ntp_search/tile_page.js b/chrome/browser/resources/ntp_search/tile_page.js
new file mode 100644
index 0000000..33baa7f
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/tile_page.js
@@ -0,0 +1,1347 @@
+// Copyright (c) 2012 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.
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ // We can't pass the currently dragging tile via dataTransfer because of
+ // http://crbug.com/31037
+ var currentlyDraggingTile = null;
+ function getCurrentlyDraggingTile() {
+ return currentlyDraggingTile;
+ }
+ function setCurrentlyDraggingTile(tile) {
+ currentlyDraggingTile = tile;
+ if (tile)
+ ntp.enterRearrangeMode();
+ else
+ ntp.leaveRearrangeMode();
+ }
+
+ /**
+ * Changes the current dropEffect of a drag. This modifies the native cursor
+ * and serves as an indicator of what we should do at the end of the drag as
+ * well as give indication to the user if a drop would succeed if they let go.
+ * @param {DataTransfer} dataTransfer A dataTransfer object from a drag event.
+ * @param {string} effect A drop effect to change to (i.e. copy, move, none).
+ */
+ function setCurrentDropEffect(dataTransfer, effect) {
+ dataTransfer.dropEffect = effect;
+ if (currentlyDraggingTile)
+ currentlyDraggingTile.lastDropEffect = dataTransfer.dropEffect;
+ }
+
+ /**
+ * Creates a new Tile object. Tiles wrap content on a TilePage, providing
+ * some styling and drag functionality.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ function Tile(contents) {
+ var tile = cr.doc.createElement('div');
+ tile.__proto__ = Tile.prototype;
+ tile.initialize(contents);
+
+ return tile;
+ }
+
+ Tile.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ initialize: function(contents) {
+ // 'real' as opposed to doppleganger.
+ this.className = 'tile real';
+ this.appendChild(contents);
+ contents.tile = this;
+
+ this.addEventListener('dragstart', this.onDragStart_);
+ this.addEventListener('drag', this.onDragMove_);
+ this.addEventListener('dragend', this.onDragEnd_);
+
+ this.firstChild.addEventListener(
+ 'webkitAnimationEnd', this.onContentsAnimationEnd_.bind(this));
+
+ this.eventTracker = new EventTracker();
+ },
+
+ get index() {
+ return Array.prototype.indexOf.call(this.tilePage.tileElements_, this);
+ },
+
+ get tilePage() {
+ return findAncestorByClass(this, 'tile-page');
+ },
+
+ /**
+ * Position the tile at |x, y|, and store this as the grid location, i.e.
+ * where the tile 'belongs' when it's not being dragged.
+ * @param {number} x The x coordinate, in pixels.
+ * @param {number} y The y coordinate, in pixels.
+ */
+ setGridPosition: function(x, y) {
+ this.gridX = x;
+ this.gridY = y;
+ this.moveTo(x, y);
+ },
+
+ /**
+ * Position the tile at |x, y|.
+ * @param {number} x The x coordinate, in pixels.
+ * @param {number} y The y coordinate, in pixels.
+ */
+ moveTo: function(x, y) {
+ // left overrides right in LTR, and right takes precedence in RTL.
+ this.style.left = toCssPx(x);
+ this.style.right = toCssPx(x);
+ this.style.top = toCssPx(y);
+ },
+
+ /**
+ * The handler for dragstart events fired on |this|.
+ * @param {Event} e The event for the drag.
+ * @private
+ */
+ onDragStart_: function(e) {
+ // The user may start dragging again during a previous drag's finishing
+ // animation.
+ if (this.classList.contains('dragging'))
+ this.finalizeDrag_();
+
+ setCurrentlyDraggingTile(this);
+
+ e.dataTransfer.effectAllowed = 'copyMove';
+ this.firstChild.setDragData(e.dataTransfer);
+
+ // The drag clone is the node we use as a representation during the drag.
+ // It's attached to the top level document element so that it floats above
+ // image masks.
+ this.dragClone = this.cloneNode(true);
+ this.dragClone.style.right = '';
+ this.dragClone.classList.add('drag-representation');
+ $('card-slider-frame').appendChild(this.dragClone);
+ this.eventTracker.add(this.dragClone, 'webkitTransitionEnd',
+ this.onDragCloneTransitionEnd_.bind(this));
+
+ this.classList.add('dragging');
+ // offsetLeft is mirrored in RTL. Un-mirror it.
+ var offsetLeft = isRTL() ?
+ this.parentNode.clientWidth - this.offsetLeft :
+ this.offsetLeft;
+ this.dragOffsetX = e.x - offsetLeft - this.parentNode.offsetLeft;
+ this.dragOffsetY = e.y - this.offsetTop -
+ // Unlike offsetTop, this value takes scroll position into account.
+ this.parentNode.getBoundingClientRect().top;
+
+ this.onDragMove_(e);
+ },
+
+ /**
+ * The handler for drag events fired on |this|.
+ * @param {Event} e The event for the drag.
+ * @private
+ */
+ onDragMove_: function(e) {
+ if (e.view != window || (e.x == 0 && e.y == 0)) {
+ this.dragClone.hidden = true;
+ return;
+ }
+
+ this.dragClone.hidden = false;
+ this.dragClone.style.left = toCssPx(e.x - this.dragOffsetX);
+ this.dragClone.style.top = toCssPx(e.y - this.dragOffsetY);
+ },
+
+ /**
+ * The handler for dragend events fired on |this|.
+ * @param {Event} e The event for the drag.
+ * @private
+ */
+ onDragEnd_: function(e) {
+ this.dragClone.hidden = false;
+ this.dragClone.classList.add('placing');
+
+ setCurrentlyDraggingTile(null);
+
+ // tilePage will be null if we've already been removed.
+ var tilePage = this.tilePage;
+ if (tilePage)
+ tilePage.positionTile_(this.index);
+
+ // Take an appropriate action with the drag clone.
+ if (this.landedOnTrash) {
+ this.dragClone.classList.add('deleting');
+ } else if (tilePage) {
+ // TODO(dbeam): Until we fix dropEffect to the correct behavior it will
+ // differ on windows - crbug.com/39399. That's why we use the custom
+ // this.lastDropEffect instead of e.dataTransfer.dropEffect.
+ if (tilePage.selected && this.lastDropEffect != 'copy') {
+ // The drag clone can still be hidden from the last drag move event.
+ this.dragClone.hidden = false;
+ // The tile's contents may have moved following the respositioning;
+ // adjust for that.
+ var contentDiffX = this.dragClone.firstChild.offsetLeft -
+ this.firstChild.offsetLeft;
+ var contentDiffY = this.dragClone.firstChild.offsetTop -
+ this.firstChild.offsetTop;
+ this.dragClone.style.left =
+ toCssPx(this.gridX + this.parentNode.offsetLeft -
+ contentDiffX);
+ this.dragClone.style.top =
+ toCssPx(this.gridY +
+ this.parentNode.getBoundingClientRect().top -
+ contentDiffY);
+ } else if (this.dragClone.hidden) {
+ this.finalizeDrag_();
+ } else {
+ // The CSS3 transitions spec intentionally leaves it up to individual
+ // user agents to determine when styles should be applied. On some
+ // platforms (at the moment, Windows), when you apply both classes
+ // immediately a transition may not occur correctly. That's why we're
+ // using a setTimeout here to queue adding the class until the
+ // previous class (currently: .placing) sets up a transition.
+ // http://dev.w3.org/csswg/css3-transitions/#starting
+ window.setTimeout(function() {
+ if (this.dragClone)
+ this.dragClone.classList.add('dropped-on-other-page');
+ }.bind(this), 0);
+ }
+ }
+
+ delete this.lastDropEffect;
+ this.landedOnTrash = false;
+ },
+
+ /**
+ * Creates a clone of this node offset by the coordinates. Used for the
+ * dragging effect where a tile appears to float off one side of the grid
+ * and re-appear on the other.
+ * @param {number} x x-axis offset, in pixels.
+ * @param {number} y y-axis offset, in pixels.
+ */
+ showDoppleganger: function(x, y) {
+ // We always have to clear the previous doppleganger to make sure we get
+ // style updates for the contents of this tile.
+ this.clearDoppleganger();
+
+ var clone = this.cloneNode(true);
+ clone.classList.remove('real');
+ clone.classList.add('doppleganger');
+ var clonelets = clone.querySelectorAll('.real');
+ for (var i = 0; i < clonelets.length; i++) {
+ clonelets[i].classList.remove('real');
+ }
+
+ this.appendChild(clone);
+ this.doppleganger_ = clone;
+
+ if (isRTL())
+ x *= -1;
+
+ this.doppleganger_.style.WebkitTransform = 'translate(' + x + 'px, ' +
+ y + 'px)';
+ },
+
+ /**
+ * Destroys the current doppleganger.
+ */
+ clearDoppleganger: function() {
+ if (this.doppleganger_) {
+ this.removeChild(this.doppleganger_);
+ this.doppleganger_ = null;
+ }
+ },
+
+ /**
+ * Returns status of doppleganger.
+ * @return {boolean} True if there is a doppleganger showing for |this|.
+ */
+ hasDoppleganger: function() {
+ return !!this.doppleganger_;
+ },
+
+ /**
+ * Cleans up after the drag is over. This is either called when the
+ * drag representation finishes animating to the final position, or when
+ * the next drag starts (if the user starts a 2nd drag very quickly).
+ * @private
+ */
+ finalizeDrag_: function() {
+ assert(this.classList.contains('dragging'));
+
+ var clone = this.dragClone;
+ this.dragClone = null;
+
+ clone.parentNode.removeChild(clone);
+ this.eventTracker.remove(clone, 'webkitTransitionEnd');
+ this.classList.remove('dragging');
+ if (this.firstChild.finalizeDrag)
+ this.firstChild.finalizeDrag();
+ },
+
+ /**
+ * Called when the drag representation node is done migrating to its final
+ * resting spot.
+ * @param {Event} e The transition end event.
+ */
+ onDragCloneTransitionEnd_: function(e) {
+ if (this.classList.contains('dragging') &&
+ (e.propertyName == 'left' || e.propertyName == 'top' ||
+ e.propertyName == '-webkit-transform')) {
+ this.finalizeDrag_();
+ }
+ },
+
+ /**
+ * Called when an app is removed from Chrome. Animates its disappearance.
+ * @param {boolean=} opt_animate Whether the animation should be animated.
+ */
+ doRemove: function(opt_animate) {
+ if (opt_animate)
+ this.firstChild.classList.add('removing-tile-contents');
+ else
+ this.tilePage.removeTile(this, false);
+ },
+
+ /**
+ * Callback for the webkitAnimationEnd event on the tile's contents.
+ * @param {Event} e The event object.
+ */
+ onContentsAnimationEnd_: function(e) {
+ if (this.firstChild.classList.contains('new-tile-contents'))
+ this.firstChild.classList.remove('new-tile-contents');
+ if (this.firstChild.classList.contains('removing-tile-contents'))
+ this.tilePage.removeTile(this, true);
+ },
+ };
+
+ /**
+ * Gives the proportion of the row width that is devoted to a single icon.
+ * @param {number} rowTileCount The number of tiles in a row.
+ * @param {number} tileSpacingFraction The proportion of the tile width which
+ * will be used as spacing between tiles.
+ * @return {number} The ratio between icon width and row width.
+ */
+ function tileWidthFraction(rowTileCount, tileSpacingFraction) {
+ return rowTileCount + (rowTileCount - 1) * tileSpacingFraction;
+ }
+
+ /**
+ * Calculates an assortment of tile-related values for a grid with the
+ * given dimensions.
+ * @param {number} width The pixel width of the grid.
+ * @param {number} numRowTiles The number of tiles in a row.
+ * @param {number} tileSpacingFraction The proportion of the tile width which
+ * will be used as spacing between tiles.
+ * @return {Object} A mapping of pixel values.
+ */
+ function tileValuesForGrid(width, numRowTiles, tileSpacingFraction) {
+ var tileWidth = width / tileWidthFraction(numRowTiles, tileSpacingFraction);
+ var offsetX = tileWidth * (1 + tileSpacingFraction);
+ var interTileSpacing = offsetX - tileWidth;
+
+ return {
+ tileWidth: tileWidth,
+ offsetX: offsetX,
+ interTileSpacing: interTileSpacing,
+ };
+ }
+
+ // The smallest amount of horizontal blank space to display on the sides when
+ // displaying a wide arrangement. There is an additional 26px of margin from
+ // the tile page padding.
+ var MIN_WIDE_MARGIN = 18;
+
+ /**
+ * Creates a new TilePage object. This object contains tiles and controls
+ * their layout.
+ * @param {Object} gridValues Pixel values that define the size and layout
+ * of the tile grid.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ function TilePage(gridValues) {
+ var el = cr.doc.createElement('div');
+ el.gridValues_ = gridValues;
+ el.__proto__ = TilePage.prototype;
+ el.initialize();
+
+ return el;
+ }
+
+ /**
+ * Takes a collection of grid layout pixel values and updates them with
+ * additional tiling values that are calculated from TilePage constants.
+ * @param {Object} grid The grid layout pixel values to update.
+ */
+ TilePage.initGridValues = function(grid) {
+ // The amount of space we need to display a narrow grid (all narrow grids
+ // are this size).
+ grid.narrowWidth =
+ grid.minTileWidth * tileWidthFraction(grid.minColCount,
+ grid.tileSpacingFraction);
+ // The minimum amount of space we need to display a wide grid.
+ grid.minWideWidth =
+ grid.minTileWidth * tileWidthFraction(grid.maxColCount,
+ grid.tileSpacingFraction);
+ // The largest we will ever display a wide grid.
+ grid.maxWideWidth =
+ grid.maxTileWidth * tileWidthFraction(grid.maxColCount,
+ grid.tileSpacingFraction);
+ // Tile-related pixel values for the narrow display.
+ grid.narrowTileValues = tileValuesForGrid(grid.narrowWidth,
+ grid.minColCount,
+ grid.tileSpacingFraction);
+ // Tile-related pixel values for the minimum narrow display.
+ grid.wideTileValues = tileValuesForGrid(grid.minWideWidth,
+ grid.maxColCount,
+ grid.tileSpacingFraction);
+ };
+
+ TilePage.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ initialize: function() {
+ this.className = 'tile-page';
+
+ // Div that acts as a custom scrollbar. The scrollbar has to live
+ // outside the content div so it doesn't flicker when scrolling (due to
+ // repainting after the scroll, then repainting again when moved in the
+ // onScroll handler). |scrollbar_| is only aesthetic, and it only
+ // represents the thumb. Actual events are still handled by the invisible
+ // native scrollbars. This div gives us more flexibility with the visuals.
+ this.scrollbar_ = this.ownerDocument.createElement('div');
+ this.scrollbar_.className = 'tile-page-scrollbar';
+ this.scrollbar_.hidden = true;
+ this.appendChild(this.scrollbar_);
+
+ // This contains everything but the scrollbar.
+ this.content_ = this.ownerDocument.createElement('div');
+ this.content_.className = 'tile-page-content';
+ this.appendChild(this.content_);
+
+ // Div that sets the vertical position of the tile grid.
+ this.topMargin_ = this.ownerDocument.createElement('div');
+ this.topMargin_.className = 'top-margin';
+ this.content_.appendChild(this.topMargin_);
+
+ // Div that holds the tiles.
+ this.tileGrid_ = this.ownerDocument.createElement('div');
+ this.tileGrid_.className = 'tile-grid';
+ this.tileGrid_.style.minWidth = this.gridValues_.narrowWidth + 'px';
+ this.content_.appendChild(this.tileGrid_);
+
+ // Ordered list of our tiles.
+ this.tileElements_ = this.tileGrid_.getElementsByClassName('tile real');
+ // Ordered list of the elements which want to accept keyboard focus. These
+ // elements will not be a part of the normal tab order; the tile grid
+ // initially gets focused and then these elements can be focused via the
+ // arrow keys.
+ this.focusableElements_ =
+ this.tileGrid_.getElementsByClassName('focusable');
+
+ // These are properties used in updateTopMargin.
+ this.animatedTopMarginPx_ = 0;
+ this.topMarginPx_ = 0;
+
+ this.eventTracker = new EventTracker();
+ this.eventTracker.add(window, 'resize', this.onResize_.bind(this));
+
+ this.addEventListener('DOMNodeInsertedIntoDocument',
+ this.onNodeInsertedIntoDocument_);
+
+ this.content_.addEventListener('scroll', this.onScroll_.bind(this));
+
+ this.dragWrapper_ = new cr.ui.DragWrapper(this.tileGrid_, this);
+
+ this.addEventListener('cardselected', this.handleCardSelection_);
+ this.addEventListener('carddeselected', this.handleCardDeselection_);
+ this.addEventListener('focus', this.handleFocus_);
+ this.addEventListener('keydown', this.handleKeyDown_);
+ this.addEventListener('mousedown', this.handleMouseDown_);
+
+ this.focusElementIndex_ = -1;
+ },
+
+ get tiles() {
+ return this.tileElements_;
+ },
+
+ get tileCount() {
+ return this.tileElements_.length;
+ },
+
+ get selected() {
+ return Array.prototype.indexOf.call(this.parentNode.children, this) ==
+ ntp.getCardSlider().currentCard;
+ },
+
+ /**
+ * The size of the margin (unused space) on the sides of the tile grid, in
+ * pixels.
+ * @type {number}
+ */
+ get sideMargin() {
+ return this.layoutValues_.leftMargin;
+ },
+
+ /**
+ * Returns the width of the scrollbar, in pixels, if it is active, or 0
+ * otherwise.
+ * @type {number}
+ */
+ get scrollbarWidth() {
+ return this.scrollbar_.hidden ? 0 : 13;
+ },
+
+ /**
+ * Returns any extra padding to insert to the bottom of a tile page. By
+ * default there is none, but subclasses can override.
+ * @type {number}
+ */
+ get extraBottomPadding() {
+ return 0;
+ },
+
+ /**
+ * The notification content of this tile (if any, otherwise null).
+ * @type {!HTMLElement}
+ */
+ get notification() {
+ return this.topMargin_.nextElementSibling.id == 'notification-container' ?
+ this.topMargin_.nextElementSibling : null;
+ },
+ /**
+ * The notification content of this tile (if any, otherwise null).
+ * @type {!HTMLElement}
+ */
+ set notification(node) {
+ assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!');
+ // NOTE: Implicitly removes from DOM if |node| is inside it.
+ this.content_.insertBefore(node, this.topMargin_.nextElementSibling);
+ this.positionNotification_();
+ },
+
+ /**
+ * Fetches the size, in pixels, of the padding-top of the tile contents.
+ * @type {number}
+ */
+ get contentPadding() {
+ if (typeof this.contentPadding_ == 'undefined') {
+ this.contentPadding_ =
+ parseInt(getComputedStyle(this.content_).paddingTop, 10);
+ }
+ return this.contentPadding_;
+ },
+
+ /**
+ * Removes the tilePage from the DOM and cleans up event handlers.
+ */
+ remove: function() {
+ // This checks arguments.length as most remove functions have a boolean
+ // |opt_animate| argument, but that's not necesarilly applicable to
+ // removing a tilePage. Selecting a different card in an animated way and
+ // deleting the card afterward is probably a better choice.
+ assert(typeof arguments[0] != 'boolean',
+ 'This function takes no |opt_animate| argument.');
+ this.tearDown_();
+ this.parentNode.removeChild(this);
+ },
+
+ /**
+ * Cleans up resources that are no longer needed after this TilePage
+ * instance is removed from the DOM.
+ * @private
+ */
+ tearDown_: function() {
+ this.eventTracker.removeAll();
+ },
+
+ /**
+ * Appends a tile to the end of the tile grid.
+ * @param {HTMLElement} tileElement The contents of the tile.
+ * @param {boolean} animate If true, the append will be animated.
+ * @protected
+ */
+ appendTile: function(tileElement, animate) {
+ this.addTileAt(tileElement, this.tileElements_.length, animate);
+ },
+
+ /**
+ * Adds the given element to the tile grid.
+ * @param {Node} tileElement The tile object/node to insert.
+ * @param {number} index The location in the tile grid to insert it at.
+ * @param {boolean} animate If true, the tile in question will be
+ * animated (other tiles, if they must reposition, do not animate).
+ * @protected
+ */
+ addTileAt: function(tileElement, index, animate) {
+ this.classList.remove('animating-tile-page');
+ if (animate)
+ tileElement.classList.add('new-tile-contents');
+
+ // Make sure the index is positive and either in the the bounds of
+ // this.tileElements_ or at the end (meaning append).
+ assert(index >= 0 && index <= this.tileElements_.length);
+
+ var wrapperDiv = new Tile(tileElement);
+ // If is out of the bounds of the tile element list, .insertBefore() will
+ // act just like appendChild().
+ this.tileGrid_.insertBefore(wrapperDiv, this.tileElements_[index]);
+ this.calculateLayoutValues_();
+ this.heightChanged_();
+
+ this.repositionTiles_();
+ this.fireAddedEvent(wrapperDiv, index, animate);
+ },
+
+ /**
+ * Notify interested subscribers that a tile has been removed from this
+ * page.
+ * @param {Tile} tile The newly added tile.
+ * @param {number} index The index of the tile that was added.
+ * @param {boolean} wasAnimated Whether the removal was animated.
+ */
+ fireAddedEvent: function(tile, index, wasAnimated) {
+ var e = document.createEvent('Event');
+ e.initEvent('tilePage:tile_added', true, true);
+ e.addedIndex = index;
+ e.addedTile = tile;
+ e.wasAnimated = wasAnimated;
+ this.dispatchEvent(e);
+ },
+
+ /**
+ * Removes the given tile and animates the repositioning of the other tiles.
+ * @param {boolean=} opt_animate Whether the removal should be animated.
+ * @param {boolean=} opt_dontNotify Whether a page should be removed if the
+ * last tile is removed from it.
+ */
+ removeTile: function(tile, opt_animate, opt_dontNotify) {
+ if (opt_animate)
+ this.classList.add('animating-tile-page');
+ var index = tile.index;
+ tile.parentNode.removeChild(tile);
+ this.calculateLayoutValues_();
+ this.cleanupDrag();
+
+ if (!opt_dontNotify)
+ this.fireRemovedEvent(tile, index, !!opt_animate);
+ },
+
+ /**
+ * Notify interested subscribers that a tile has been removed from this
+ * page.
+ * @param {Tile} tile The tile that was removed.
+ * @param {number} oldIndex Where the tile was positioned before removal.
+ * @param {boolean} wasAnimated Whether the removal was animated.
+ */
+ fireRemovedEvent: function(tile, oldIndex, wasAnimated) {
+ var e = document.createEvent('Event');
+ e.initEvent('tilePage:tile_removed', true, true);
+ e.removedIndex = oldIndex;
+ e.removedTile = tile;
+ e.wasAnimated = wasAnimated;
+ this.dispatchEvent(e);
+ },
+
+ /**
+ * Removes all tiles from the page.
+ */
+ removeAllTiles: function() {
+ this.tileGrid_.innerHTML = '';
+ },
+
+ /**
+ * Called when the page is selected (in the card selector).
+ * @param {Event} e A custom cardselected event.
+ * @private
+ */
+ handleCardSelection_: function(e) {
+ this.tabIndex = 1;
+
+ // When we are selected, we re-calculate the layout values. (See comment
+ // in doDrop.)
+ this.calculateLayoutValues_();
+ },
+
+ /**
+ * Called when the page loses selection (in the card selector).
+ * @param {Event} e A custom carddeselected event.
+ * @private
+ */
+ handleCardDeselection_: function(e) {
+ this.tabIndex = -1;
+ if (this.currentFocusElement_)
+ this.currentFocusElement_.tabIndex = -1;
+ },
+
+ /**
+ * When we get focus, pass it on to the focus element.
+ * @param {Event} e The focus event.
+ * @private
+ */
+ handleFocus_: function(e) {
+ if (this.focusableElements_.length == 0)
+ return;
+
+ this.updateFocusElement_();
+ },
+
+ /**
+ * Since we are doing custom focus handling, we have to manually
+ * set focusability on click (as well as keyboard nav above).
+ * @param {Event} e The focus event.
+ * @private
+ */
+ handleMouseDown_: function(e) {
+ var focusable = findAncestorByClass(e.target, 'focusable');
+ if (focusable) {
+ this.focusElementIndex_ =
+ Array.prototype.indexOf.call(this.focusableElements_,
+ focusable);
+ this.updateFocusElement_();
+ } else {
+ // This prevents the tile page from getting focus when the user clicks
+ // inside the grid but outside of any tile.
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Handle arrow key focus nav.
+ * @param {Event} e The focus event.
+ * @private
+ */
+ handleKeyDown_: function(e) {
+ // We only handle up, down, left, right without control keys.
+ if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey)
+ return;
+
+ // Wrap the given index to |this.focusableElements_|.
+ var wrap = function(idx) {
+ return (idx + this.focusableElements_.length) %
+ this.focusableElements_.length;
+ }.bind(this);
+
+ switch (e.keyIdentifier) {
+ case 'Right':
+ case 'Left':
+ var direction = e.keyIdentifier == 'Right' ? 1 : -1;
+ this.focusElementIndex_ = wrap(this.focusElementIndex_ + direction);
+ break;
+ case 'Up':
+ case 'Down':
+ // Look through all focusable elements. Find the first one that is
+ // in the same column.
+ var direction = e.keyIdentifier == 'Up' ? -1 : 1;
+ var currentIndex =
+ Array.prototype.indexOf.call(this.focusableElements_,
+ this.currentFocusElement_);
+ var newFocusIdx = wrap(currentIndex + direction);
+ var tile = this.currentFocusElement_.parentNode;
+ for (;; newFocusIdx = wrap(newFocusIdx + direction)) {
+ var newTile = this.focusableElements_[newFocusIdx].parentNode;
+ var rowTiles = this.layoutValues_.numRowTiles;
+ if ((newTile.index - tile.index) % rowTiles == 0)
+ break;
+ }
+
+ this.focusElementIndex_ = newFocusIdx;
+ break;
+
+ default:
+ return;
+ }
+
+ this.updateFocusElement_();
+
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ /**
+ * Focuses the element for |this.focusElementIndex_|. Makes the current
+ * focus element, if any, no longer eligible for focus.
+ * @private
+ */
+ updateFocusElement_: function() {
+ this.focusElementIndex_ = Math.min(this.focusableElements_.length - 1,
+ this.focusElementIndex_);
+ this.focusElementIndex_ = Math.max(0, this.focusElementIndex_);
+
+ var newFocusElement = this.focusableElements_[this.focusElementIndex_];
+ var lastFocusElement = this.currentFocusElement_;
+ if (lastFocusElement && lastFocusElement != newFocusElement)
+ lastFocusElement.tabIndex = -1;
+
+ newFocusElement.tabIndex = 1;
+ newFocusElement.focus();
+ this.tabIndex = -1;
+ },
+
+ /**
+ * The current focus element is that element which is eligible for focus.
+ * @type {HTMLElement} The node.
+ * @private
+ */
+ get currentFocusElement_() {
+ return this.querySelector('.focusable[tabindex="1"]');
+ },
+
+ /**
+ * Makes some calculations for tile layout. These change depending on
+ * height, width, and the number of tiles.
+ * TODO(estade): optimize calls to this function. Do nothing if the page is
+ * hidden, but call before being shown.
+ * @private
+ */
+ calculateLayoutValues_: function() {
+ var grid = this.gridValues_;
+ var availableSpace = this.tileGrid_.clientWidth - 2 * MIN_WIDE_MARGIN;
+ var wide = availableSpace >= grid.minWideWidth;
+ var numRowTiles = wide ? grid.maxColCount : grid.minColCount;
+
+ var effectiveGridWidth = wide ?
+ Math.min(Math.max(availableSpace, grid.minWideWidth),
+ grid.maxWideWidth) :
+ grid.narrowWidth;
+ var realTileValues = tileValuesForGrid(effectiveGridWidth, numRowTiles,
+ grid.tileSpacingFraction);
+
+ // leftMargin centers the grid within the avaiable space.
+ var minMargin = wide ? MIN_WIDE_MARGIN : 0;
+ var leftMargin =
+ Math.max(minMargin,
+ (this.tileGrid_.clientWidth - effectiveGridWidth) / 2);
+
+ var rowHeight = this.heightForWidth(realTileValues.tileWidth) +
+ realTileValues.interTileSpacing;
+
+ this.layoutValues_ = {
+ numRowTiles: numRowTiles,
+ leftMargin: leftMargin,
+ colWidth: realTileValues.offsetX,
+ rowHeight: rowHeight,
+ tileWidth: realTileValues.tileWidth,
+ wide: wide,
+ };
+
+ // We need to update the top margin as well.
+ this.updateTopMargin_();
+
+ this.firePageLayoutEvent_();
+ },
+
+ /**
+ * Dispatches the custom pagelayout event.
+ * @private
+ */
+ firePageLayoutEvent_: function() {
+ cr.dispatchSimpleEvent(this, 'pagelayout', true, true);
+ },
+
+ /**
+ * Calculates the x/y coordinates for an element and moves it there.
+ * @param {number} index The index of the element to be positioned.
+ * @param {number} indexOffset If provided, this is added to |index| when
+ * positioning the tile. The effect is that the tile will be positioned
+ * in a non-default location.
+ * @private
+ */
+ positionTile_: function(index, indexOffset) {
+ var grid = this.gridValues_;
+ var layout = this.layoutValues_;
+
+ indexOffset = typeof indexOffset != 'undefined' ? indexOffset : 0;
+ // Add the offset _after_ the modulus division. We might want to show the
+ // tile off the side of the grid.
+ var col = index % layout.numRowTiles + indexOffset;
+ var row = Math.floor(index / layout.numRowTiles);
+ // Calculate the final on-screen position for the tile.
+ var realX = col * layout.colWidth + layout.leftMargin;
+ var realY = row * layout.rowHeight;
+
+ // Calculate the portion of the tile's position that should be animated.
+ var animatedTileValues = layout.wide ?
+ grid.wideTileValues : grid.narrowTileValues;
+ // Animate the difference between three-wide and six-wide.
+ var animatedLeftMargin = layout.wide ?
+ 0 : (grid.minWideWidth - MIN_WIDE_MARGIN - grid.narrowWidth) / 2;
+ var animatedX = col * animatedTileValues.offsetX + animatedLeftMargin;
+ var animatedY = row * (this.heightForWidth(animatedTileValues.tileWidth) +
+ animatedTileValues.interTileSpacing);
+
+ var tile = this.tileElements_[index];
+ tile.setGridPosition(animatedX, animatedY);
+ tile.firstChild.setBounds(layout.tileWidth,
+ realX - animatedX,
+ realY - animatedY);
+
+ // This code calculates whether the tile needs to show a clone of itself
+ // wrapped around the other side of the tile grid.
+ var offTheRight = col == layout.numRowTiles ||
+ (col == layout.numRowTiles - 1 && tile.hasDoppleganger());
+ var offTheLeft = col == -1 || (col == 0 && tile.hasDoppleganger());
+ if (this.isCurrentDragTarget && (offTheRight || offTheLeft)) {
+ var sign = offTheRight ? 1 : -1;
+ tile.showDoppleganger(-layout.numRowTiles * layout.colWidth * sign,
+ layout.rowHeight * sign);
+ } else {
+ tile.clearDoppleganger();
+ }
+
+ if (index == this.tileElements_.length - 1) {
+ this.tileGrid_.style.height = (realY + layout.rowHeight) + 'px';
+ this.queueUpdateScrollbars_();
+ }
+ },
+
+ /**
+ * Gets the index of the tile that should occupy coordinate (x, y). Note
+ * that this function doesn't care where the tiles actually are, and will
+ * return an index even for the space between two tiles. This function is
+ * effectively the inverse of |positionTile_|.
+ * @param {number} x The x coordinate, in pixels, relative to the left of
+ * |this|.
+ * @param {number} y The y coordinate, in pixels, relative to the top of
+ * |this|.
+ * @private
+ */
+ getWouldBeIndexForPoint_: function(x, y) {
+ var grid = this.gridValues_;
+ var layout = this.layoutValues_;
+
+ var gridClientRect = this.tileGrid_.getBoundingClientRect();
+ var col = Math.floor((x - gridClientRect.left - layout.leftMargin) /
+ layout.colWidth);
+ if (col < 0 || col >= layout.numRowTiles)
+ return -1;
+
+ if (isRTL())
+ col = layout.numRowTiles - 1 - col;
+
+ var row = Math.floor((y - gridClientRect.top) / layout.rowHeight);
+ return row * layout.numRowTiles + col;
+ },
+
+ /**
+ * Window resize event handler. Window resizes may trigger re-layouts.
+ * @param {Object} e The resize event.
+ */
+ onResize_: function(e) {
+ if (this.lastWidth_ == this.clientWidth &&
+ this.lastHeight_ == this.clientHeight) {
+ return;
+ }
+
+ this.calculateLayoutValues_();
+
+ this.lastWidth_ = this.clientWidth;
+ this.lastHeight_ = this.clientHeight;
+ this.classList.add('animating-tile-page');
+ this.heightChanged_();
+
+ this.positionNotification_();
+ this.repositionTiles_();
+ },
+
+ /**
+ * The tile grid has an image mask which fades at the edges. We only show
+ * the mask when there is an active drag; it obscures doppleganger tiles
+ * as they enter or exit the grid.
+ * @private
+ */
+ updateMask_: function() {
+ if (!this.isCurrentDragTarget) {
+ this.tileGrid_.style.WebkitMaskBoxImage = '';
+ return;
+ }
+
+ var leftMargin = this.layoutValues_.leftMargin;
+ // The fade distance is the space between tiles.
+ var fadeDistance = (this.gridValues_.tileSpacingFraction *
+ this.layoutValues_.tileWidth);
+ fadeDistance = Math.min(leftMargin, fadeDistance);
+ // On Skia we don't use any fade because it works very poorly. See
+ // http://crbug.com/99373
+ if (!cr.isMac)
+ fadeDistance = 1;
+ var gradient =
+ '-webkit-linear-gradient(left,' +
+ 'transparent, ' +
+ 'transparent ' + (leftMargin - fadeDistance) + 'px, ' +
+ 'black ' + leftMargin + 'px, ' +
+ 'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' +
+ 'transparent ' + (this.tileGrid_.clientWidth - leftMargin +
+ fadeDistance) + 'px, ' +
+ 'transparent)';
+ this.tileGrid_.style.WebkitMaskBoxImage = gradient;
+ },
+
+ updateTopMargin_: function() {
+ var layout = this.layoutValues_;
+
+ // The top margin is set so that the vertical midpoint of the grid will
+ // be 1/3 down the page.
+ var numTiles = this.tileCount +
+ (this.isCurrentDragTarget && !this.withinPageDrag_ ? 1 : 0);
+ var numRows = Math.max(1, Math.ceil(numTiles / layout.numRowTiles));
+ var usedHeight = layout.rowHeight * numRows;
+ var newMargin = document.documentElement.clientHeight / 3 -
+ usedHeight / 3 - this.contentPadding;
+ // The 'height' style attribute of topMargin is non-zero to work around
+ // webkit's collapsing margin behavior, so we have to factor that into
+ // our calculations here.
+ newMargin = Math.max(newMargin, 0) - this.topMargin_.offsetHeight;
+
+ // |newMargin| is the final margin we actually want to show. However,
+ // part of that should be animated and part should not (for the same
+ // reason as with leftMargin). The approach is to consider differences
+ // when the layout changes from wide to narrow or vice versa as
+ // 'animatable'. These differences accumulate in animatedTopMarginPx_,
+ // while topMarginPx_ caches the real (total) margin. Either of these
+ // calculations may come out to be negative, so we use margins as the
+ // css property.
+
+ if (typeof this.topMarginIsForWide_ == 'undefined')
+ this.topMarginIsForWide_ = layout.wide;
+ if (this.topMarginIsForWide_ != layout.wide) {
+ this.animatedTopMarginPx_ += newMargin - this.topMarginPx_;
+ this.topMargin_.style.marginBottom = toCssPx(this.animatedTopMarginPx_);
+ }
+
+ this.topMarginIsForWide_ = layout.wide;
+ this.topMarginPx_ = newMargin;
+ this.topMargin_.style.marginTop =
+ toCssPx(this.topMarginPx_ - this.animatedTopMarginPx_);
+ },
+
+ /**
+ * Position the notification if there's one showing.
+ */
+ positionNotification_: function() {
+ if (this.notification && !this.notification.hidden) {
+ this.notification.style.margin =
+ -this.notification.offsetHeight + 'px ' +
+ this.layoutValues_.leftMargin + 'px 0';
+ }
+ },
+
+ /**
+ * Handles final setup that can only happen after |this| is inserted into
+ * the page.
+ * @private
+ */
+ onNodeInsertedIntoDocument_: function(e) {
+ this.calculateLayoutValues_();
+ this.heightChanged_();
+ },
+
+ /**
+ * Called when the height of |this| has changed: update the size of
+ * tileGrid.
+ * @private
+ */
+ heightChanged_: function() {
+ // The tile grid will expand to the bottom footer, or enough to hold all
+ // the tiles, whichever is greater. It would be nicer if tilePage were
+ // a flex box, and the tile grid could be box-flex: 1, but this exposes a
+ // bug where repositioning tiles will cause the scroll position to reset.
+ this.tileGrid_.style.minHeight = (this.clientHeight -
+ this.tileGrid_.offsetTop - this.content_.offsetTop -
+ this.extraBottomPadding -
+ (this.footerNode_ ? this.footerNode_.clientHeight : 0)) + 'px';
+ },
+
+ /**
+ * Places an element at the bottom of the content div. Used in bare-minimum
+ * mode to hold #footer.
+ * @param {HTMLElement} footerNode The node to append to content.
+ */
+ appendFooter: function(footerNode) {
+ this.footerNode_ = footerNode;
+ this.content_.appendChild(footerNode);
+ },
+
+ /**
+ * Scrolls the page in response to an mousewheel event, although the event
+ * may have been triggered on a different element. Return true if the
+ * event triggered scrolling, and false otherwise.
+ * This is called explicitly, which allows a consistent experience whether
+ * the user scrolls on the page or on the page switcher, because this
+ * function provides a common conversion factor between wheel delta and
+ * scroll delta.
+ * @param {Event} e The mousewheel event.
+ */
+ handleMouseWheel: function(e) {
+ if (e.wheelDeltaY == 0)
+ return false;
+
+ this.content_.scrollTop -= e.wheelDeltaY / 3;
+ return true;
+ },
+
+ /**
+ * Handler for the 'scroll' event on |content_|.
+ * @param {Event} e The scroll event.
+ * @private
+ */
+ onScroll_: function(e) {
+ this.queueUpdateScrollbars_();
+ },
+
+ /**
+ * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued.
+ * @private
+ */
+ scrollbarUpdate_: 0,
+
+ /**
+ * Queues an update on the custom scrollbar. Used for two reasons: first,
+ * coalescing of multiple updates, and second, because action like
+ * repositioning a tile can require a delay before they affect values
+ * like clientHeight.
+ * @private
+ */
+ queueUpdateScrollbars_: function() {
+ if (this.scrollbarUpdate_)
+ return;
+
+ this.scrollbarUpdate_ = window.setTimeout(
+ this.doUpdateScrollbars_.bind(this), 0);
+ },
+
+ /**
+ * Does the work of calculating the visibility, height and position of the
+ * scrollbar thumb (there is no track or buttons).
+ * @private
+ */
+ doUpdateScrollbars_: function() {
+ this.scrollbarUpdate_ = 0;
+
+ var content = this.content_;
+
+ // Adjust scroll-height to account for possible header-bar.
+ var adjustedScrollHeight = content.scrollHeight - content.offsetTop;
+
+ if (adjustedScrollHeight <= content.clientHeight) {
+ this.scrollbar_.hidden = true;
+ return;
+ } else {
+ this.scrollbar_.hidden = false;
+ }
+
+ var thumbTop = content.offsetTop +
+ content.scrollTop / adjustedScrollHeight * content.clientHeight;
+ var thumbHeight = content.clientHeight / adjustedScrollHeight *
+ this.clientHeight;
+
+ this.scrollbar_.style.top = thumbTop + 'px';
+ this.scrollbar_.style.height = thumbHeight + 'px';
+ this.firePageLayoutEvent_();
+ },
+
+ /**
+ * Get the height for a tile of a certain width. Override this function to
+ * get non-square tiles.
+ * @param {number} width The pixel width of a tile.
+ * @return {number} The height for |width|.
+ */
+ heightForWidth: function(width) {
+ return width;
+ },
+
+ /** Dragging **/
+
+ get isCurrentDragTarget() {
+ return this.dragWrapper_.isCurrentDragTarget;
+ },
+
+ /**
+ * Thunk for dragleave events fired on |tileGrid_|.
+ * @param {Event} e A MouseEvent for the drag.
+ */
+ doDragLeave: function(e) {
+ this.cleanupDrag();
+ },
+
+ /**
+ * Performs all actions necessary when a drag enters the tile page.
+ * @param {Event} e A mouseover event for the drag enter.
+ */
+ doDragEnter: function(e) {
+ // Applies the mask so doppleganger tiles disappear into the fog.
+ this.updateMask_();
+
+ this.classList.add('animating-tile-page');
+ this.withinPageDrag_ = this.contains(currentlyDraggingTile);
+ this.dragItemIndex_ = this.withinPageDrag_ ?
+ currentlyDraggingTile.index : this.tileElements_.length;
+ this.currentDropIndex_ = this.dragItemIndex_;
+
+ // The new tile may change the number of rows, hence the top margin
+ // will change.
+ if (!this.withinPageDrag_)
+ this.updateTopMargin_();
+
+ this.doDragOver(e);
+ },
+
+ /**
+ * Performs all actions necessary when the user moves the cursor during
+ * a drag over the tile page.
+ * @param {Event} e A mouseover event for the drag over.
+ */
+ doDragOver: function(e) {
+ e.preventDefault();
+
+ this.setDropEffect(e.dataTransfer);
+ var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY);
+ if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length)
+ newDragIndex = this.dragItemIndex_;
+ this.updateDropIndicator_(newDragIndex);
+ },
+
+ /**
+ * Performs all actions necessary when the user completes a drop.
+ * @param {Event} e A mouseover event for the drag drop.
+ */
+ doDrop: function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ var index = this.currentDropIndex_;
+ // Only change data if this was not a 'null drag'.
+ if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) {
+ var adjustedIndex = this.currentDropIndex_ +
+ (index > this.dragItemIndex_ ? 1 : 0);
+ if (this.withinPageDrag_) {
+ this.tileGrid_.insertBefore(
+ currentlyDraggingTile,
+ this.tileElements_[adjustedIndex]);
+ this.tileMoved(currentlyDraggingTile, this.dragItemIndex_);
+ } else {
+ var originalPage = currentlyDraggingTile ?
+ currentlyDraggingTile.tilePage : null;
+ this.addDragData(e.dataTransfer, adjustedIndex);
+ if (originalPage)
+ originalPage.cleanupDrag();
+ }
+
+ // Dropping the icon may cause topMargin to change, but changing it
+ // now would cause everything to move (annoying), so we leave it
+ // alone. The top margin will be re-calculated next time the window is
+ // resized or the page is selected.
+ }
+
+ this.classList.remove('animating-tile-page');
+ this.cleanupDrag();
+ },
+
+ /**
+ * Appends the currently dragged tile to the end of the page. Called
+ * from outside the page, e.g. when dropping on a nav dot.
+ */
+ appendDraggingTile: function() {
+ var originalPage = currentlyDraggingTile.tilePage;
+ if (originalPage == this)
+ return;
+
+ this.addDragData(null, this.tileElements_.length);
+ if (originalPage)
+ originalPage.cleanupDrag();
+ },
+
+ /**
+ * Makes sure all the tiles are in the right place after a drag is over.
+ */
+ cleanupDrag: function() {
+ this.repositionTiles_(currentlyDraggingTile);
+ // Remove the drag mask.
+ this.updateMask_();
+ },
+
+ /**
+ * Reposition all the tiles (possibly ignoring one).
+ * @param {?Node} ignoreNode An optional node to ignore.
+ * @private
+ */
+ repositionTiles_: function(ignoreNode) {
+ for (var i = 0; i < this.tileElements_.length; i++) {
+ if (!ignoreNode || ignoreNode !== this.tileElements_[i])
+ this.positionTile_(i);
+ }
+ },
+
+ /**
+ * Updates the visual indicator for the drop location for the active drag.
+ * @param {Event} e A MouseEvent for the drag.
+ * @private
+ */
+ updateDropIndicator_: function(newDragIndex) {
+ var oldDragIndex = this.currentDropIndex_;
+ if (newDragIndex == oldDragIndex)
+ return;
+
+ var repositionStart = Math.min(newDragIndex, oldDragIndex);
+ var repositionEnd = Math.max(newDragIndex, oldDragIndex);
+
+ for (var i = repositionStart; i <= repositionEnd; i++) {
+ if (i == this.dragItemIndex_)
+ continue;
+ else if (i > this.dragItemIndex_)
+ var adjustment = i <= newDragIndex ? -1 : 0;
+ else
+ var adjustment = i >= newDragIndex ? 1 : 0;
+
+ this.positionTile_(i, adjustment);
+ }
+ this.currentDropIndex_ = newDragIndex;
+ },
+
+ /**
+ * Checks if a page can accept a drag with the given data.
+ * @param {Event} e The drag event if the drag object. Implementations will
+ * likely want to check |e.dataTransfer|.
+ * @return {boolean} True if this page can handle the drag.
+ */
+ shouldAcceptDrag: function(e) {
+ return false;
+ },
+
+ /**
+ * Called to accept a drag drop. Will not be called for in-page drops.
+ * @param {Object} dataTransfer The data transfer object that holds the drop
+ * data. This should only be used if currentlyDraggingTile is null.
+ * @param {number} index The tile index at which the drop occurred.
+ */
+ addDragData: function(dataTransfer, index) {
+ assert(false);
+ },
+
+ /**
+ * Called when a tile has been moved (via dragging). Override this to make
+ * backend updates.
+ * @param {Node} draggedTile The tile that was dropped.
+ * @param {number} prevIndex The previous index of the tile.
+ */
+ tileMoved: function(draggedTile, prevIndex) {
+ },
+
+ /**
+ * Sets the drop effect on |dataTransfer| to the desired value (e.g.
+ * 'copy').
+ * @param {Object} dataTransfer The drag event dataTransfer object.
+ */
+ setDropEffect: function(dataTransfer) {
+ assert(false);
+ },
+ };
+
+ return {
+ getCurrentlyDraggingTile: getCurrentlyDraggingTile,
+ setCurrentDropEffect: setCurrentDropEffect,
+ TilePage: TilePage,
+ };
+});
diff --git a/chrome/browser/resources/ntp_search/trash.js b/chrome/browser/resources/ntp_search/trash.js
new file mode 100644
index 0000000..c15eb93
--- /dev/null
+++ b/chrome/browser/resources/ntp_search/trash.js
@@ -0,0 +1,83 @@
+// Copyright (c) 2012 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 Trash
+ * This is the class for the trash can that appears when dragging an app.
+ */
+
+cr.define('ntp', function() {
+ 'use strict';
+
+ function Trash(trash) {
+ trash.__proto__ = Trash.prototype;
+ trash.initialize();
+ return trash;
+ }
+
+ Trash.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ initialize: function(element) {
+ this.dragWrapper_ = new cr.ui.DragWrapper(this, this);
+ },
+
+ /**
+ * Determines whether we are interested in the drag data for |e|.
+ * @param {Event} e The event from drag enter.
+ * @return {boolean} True if we are interested in the drag data for |e|.
+ */
+ shouldAcceptDrag: function(e) {
+ var tile = ntp.getCurrentlyDraggingTile();
+ if (!tile)
+ return false;
+
+ return tile.firstChild.canBeRemoved();
+ },
+
+ /**
+ * Drag over handler.
+ * @param {Event} e The drag event.
+ */
+ doDragOver: function(e) {
+ ntp.getCurrentlyDraggingTile().dragClone.classList.add(
+ 'hovering-on-trash');
+ ntp.setCurrentDropEffect(e.dataTransfer, 'move');
+ e.preventDefault();
+ },
+
+ /**
+ * Drag enter handler.
+ * @param {Event} e The drag event.
+ */
+ doDragEnter: function(e) {
+ this.doDragOver(e);
+ },
+
+ /**
+ * Drop handler.
+ * @param {Event} e The drag event.
+ */
+ doDrop: function(e) {
+ e.preventDefault();
+
+ var tile = ntp.getCurrentlyDraggingTile();
+ tile.firstChild.removeFromChrome();
+ tile.landedOnTrash = true;
+ },
+
+ /**
+ * Drag leave handler.
+ * @param {Event} e The drag event.
+ */
+ doDragLeave: function(e) {
+ ntp.getCurrentlyDraggingTile().dragClone.classList.remove(
+ 'hovering-on-trash');
+ },
+ };
+
+ return {
+ Trash: Trash,
+ };
+});
diff --git a/chrome/browser/ui/webui/ntp/new_tab_ui.cc b/chrome/browser/ui/webui/ntp/new_tab_ui.cc
index ef71746..49bf9ff 100644
--- a/chrome/browser/ui/webui/ntp/new_tab_ui.cc
+++ b/chrome/browser/ui/webui/ntp/new_tab_ui.cc
@@ -26,6 +26,7 @@
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/search/search.h"
#include "chrome/browser/ui/webui/chrome_url_data_manager.h"
#include "chrome/browser/ui/webui/metrics_handler.h"
#include "chrome/browser/ui/webui/ntp/favicon_webui_handler.h"
@@ -139,13 +140,26 @@ NewTabUI::NewTabUI(content::WebUI* web_ui)
InitializeCSSCaches();
NewTabHTMLSource* html_source =
new NewTabHTMLSource(GetProfile()->GetOriginalProfile());
+
// These two resources should be loaded only if suggestions NTP is enabled.
- html_source->AddResource("suggestions_page.css", "text/css",
- NewTabUI::IsSuggestionsPageEnabled() ? IDR_SUGGESTIONS_PAGE_CSS : 0);
- if (NewTabUI::IsSuggestionsPageEnabled()) {
- html_source->AddResource("suggestions_page.js", "application/javascript",
- IDR_SUGGESTIONS_PAGE_JS);
+ const bool kSuggestEnabled = NewTabUI::IsSuggestionsPageEnabled();
+ const bool kInstantEnabled =
+ chrome::search::IsInstantExtendedAPIEnabled(GetProfile());
+ html_source->AddResource("suggestions_page.css",
+ "text/css",
+ kSuggestEnabled ?
+ kInstantEnabled ?
+ IDR_SUGGESTIONS_PAGE_SEARCH_CSS :
+ IDR_SUGGESTIONS_PAGE_CSS :
+ 0);
+ if (kSuggestEnabled) {
+ html_source->AddResource("suggestions_page.js",
+ "application/javascript",
+ kInstantEnabled ?
+ IDR_SUGGESTIONS_PAGE_SEARCH_JS :
+ IDR_SUGGESTIONS_PAGE_JS);
}
+
// ChromeURLDataManager assumes the ownership of the html_source and in some
// tests immediately deletes it, so html_source should not be accessed after
// this call.
diff --git a/chrome/browser/ui/webui/ntp/ntp_resource_cache.cc b/chrome/browser/ui/webui/ntp/ntp_resource_cache.cc
index da04a88..8b49198 100644
--- a/chrome/browser/ui/webui/ntp/ntp_resource_cache.cc
+++ b/chrome/browser/ui/webui/ntp/ntp_resource_cache.cc
@@ -22,6 +22,7 @@
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
+#include "chrome/browser/ui/search/search.h"
#include "chrome/browser/ui/webui/chrome_url_data_manager.h"
#include "chrome/browser/ui/webui/ntp/new_tab_page_handler.h"
#include "chrome/browser/ui/webui/ntp/new_tab_ui.h"
@@ -425,7 +426,8 @@ void NTPResourceCache::CreateNewTabHTML() {
// Load the new tab page appropriate for this build
base::StringPiece new_tab_html(ResourceBundle::GetSharedInstance().
- GetRawDataResource(IDR_NEW_TAB_4_HTML,
+ GetRawDataResource(chrome::search::IsInstantExtendedAPIEnabled(profile_) ?
+ IDR_NEW_TAB_SEARCH_HTML : IDR_NEW_TAB_4_HTML,
ui::SCALE_FACTOR_NONE));
jstemplate_builder::UseVersion2 version2;
std::string full_html =
@@ -552,7 +554,9 @@ void NTPResourceCache::CreateNewTabCSS() {
// Get our template.
static const base::StringPiece new_tab_theme_css(
ResourceBundle::GetSharedInstance().GetRawDataResource(
- IDR_NEW_TAB_4_THEME_CSS, ui::SCALE_FACTOR_NONE));
+ chrome::search::IsInstantExtendedAPIEnabled(profile_) ?
+ IDR_NEW_TAB_SEARCH_THEME_CSS : IDR_NEW_TAB_4_THEME_CSS,
+ ui::SCALE_FACTOR_NONE));
// Create the string from our template and the replacements.
std::string css_string;