diff options
author | arv@google.com <arv@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-06-22 23:53:09 +0000 |
---|---|---|
committer | arv@google.com <arv@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-06-22 23:53:09 +0000 |
commit | 60287ffd30023b51d7f183abb6948a63f9f1eb90 (patch) | |
tree | a640367374681d389b091b4443d9b43ff4822983 /chrome/browser | |
parent | 930d1f1a0dd0bdfa65822af55d4717c6ab62515e (diff) | |
download | chromium_src-60287ffd30023b51d7f183abb6948a63f9f1eb90.zip chromium_src-60287ffd30023b51d7f183abb6948a63f9f1eb90.tar.gz chromium_src-60287ffd30023b51d7f183abb6948a63f9f1eb90.tar.bz2 |
Update the New New Tab Page. There are still a lot of things that are
not implemented but it is time to get this submitted and iron out the
remaining issues.
BUG=13362
TEST=Start chrome with --new-new-tab-page and look at play arouind with
the new tab page.
Review URL: http://codereview.chromium.org/132027
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@18989 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/browser_resources.grd | 2 | ||||
-rw-r--r-- | chrome/browser/dom_ui/new_tab_ui.cc | 40 | ||||
-rw-r--r-- | chrome/browser/dom_ui/shown_sections_handler.cc | 54 | ||||
-rw-r--r-- | chrome/browser/dom_ui/shown_sections_handler.h | 40 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.css | 665 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.html | 1516 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.js | 891 |
7 files changed, 1838 insertions, 1370 deletions
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd index 69733b7..e8938c4 100644 --- a/chrome/browser/browser_resources.grd +++ b/chrome/browser/browser_resources.grd @@ -20,6 +20,8 @@ without changes to the corresponding grd file. --> <include name="IDR_NEW_TAB_HTML" file="resources\new_tab.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_NEW_TAB_THEME_CSS" file="resources\new_tab_theme.css" flattenhtml="true" type="BINDATA" /> <include name="IDR_NEW_NEW_TAB_HTML" file="resources\new_new_tab.html" flattenhtml="true" type="BINDATA" /> + <include name="IDR_NEW_NEW_TAB_CSS" file="resources\new_new_tab.css" type="BINDATA" /> + <include name="IDR_NEW_NEW_TAB_JS" file="resources\new_new_tab.js" type="BINDATA" /> <include name="IDR_SAFE_BROWSING_MALWARE_BLOCK" file="resources\safe_browsing_malware_block.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_SAFE_BROWSING_PHISHING_BLOCK" file="resources\safe_browsing_phishing_block.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_SAFE_BROWSING_MULTIPLE_THREAT_BLOCK" file="resources\safe_browsing_multiple_threat_block.html" flattenhtml="true" type="BINDATA" /> diff --git a/chrome/browser/dom_ui/new_tab_ui.cc b/chrome/browser/dom_ui/new_tab_ui.cc index e1dd96e..3bdb738 100644 --- a/chrome/browser/dom_ui/new_tab_ui.cc +++ b/chrome/browser/dom_ui/new_tab_ui.cc @@ -22,6 +22,7 @@ #include "chrome/browser/dom_ui/dom_ui_theme_source.h" #include "chrome/browser/dom_ui/downloads_dom_handler.h" #include "chrome/browser/dom_ui/history_ui.h" +#include "chrome/browser/dom_ui/shown_sections_handler.h" #include "chrome/browser/dom_ui/web_resource_handler.h" #include "chrome/browser/history/page_usage_data.h" #include "chrome/browser/metrics/user_metrics.h" @@ -294,6 +295,38 @@ void NewTabHTMLSource::StartDataRequest(const std::string& path, l10n_util::GetString(IDS_NEW_TAB_ATTRIBUTION_INTRO)); localized_strings.SetString(L"resourcecache", l10n_util::GetString(IDS_NEW_TAB_WEB_RESOURCE_CACHE)); + localized_strings.SetString(L"editthumbnail", + l10n_util::GetString(IDS_NEW_TAB_EDIT_THUMBNAIL)); + localized_strings.SetString(L"recentactivities", + l10n_util::GetString(IDS_NEW_TAB_RECENT_ACTIVITIES)); + localized_strings.SetString(L"downloads", + l10n_util::GetString(IDS_NEW_TAB_DOWNLOADS)); + localized_strings.SetString(L"viewfullhistory", + l10n_util::GetString(IDS_NEW_TAB_VIEW_FULL_HISTORY)); + localized_strings.SetString(L"viewalldownloads", + l10n_util::GetString(IDS_NEW_TAB_VIEW_ALL_DOWNLOADS)); + localized_strings.SetString(L"showthumbnails", + l10n_util::GetString(IDS_NEW_TAB_SHOW_THUMBNAILS)); + localized_strings.SetString(L"hidethumbnails", + l10n_util::GetString(IDS_NEW_TAB_HIDE_THUMBNAILS)); + localized_strings.SetString(L"showlist", + l10n_util::GetString(IDS_NEW_TAB_SHOW_LIST)); + localized_strings.SetString(L"hidelist", + l10n_util::GetString(IDS_NEW_TAB_HIDE_LIST)); + localized_strings.SetString(L"showrecent", + l10n_util::GetString(IDS_NEW_TAB_SHOW_RECENT)); + localized_strings.SetString(L"hiderecent", + l10n_util::GetString(IDS_NEW_TAB_HIDE_RECENT)); + localized_strings.SetString(L"showrecommendations", + l10n_util::GetString(IDS_NEW_TAB_SHOW_RECOMMENDATIONS)); + localized_strings.SetString(L"hiderecommendations", + l10n_util::GetString(IDS_NEW_TAB_HIDE_RECOMMENDATIONS)); + localized_strings.SetString(L"thumbnailremovednotification", + l10n_util::GetString(IDS_NEW_TAB_THUMBNAIL_REMOVED_NOTIFICATION)); + localized_strings.SetString(L"undothumbnailremove", + l10n_util::GetString(IDS_NEW_TAB_UNDO_THUMBNAIL_REMOVE)); + localized_strings.SetString(L"otrmessage", + l10n_util::GetString(IDS_NEW_TAB_OTR_MESSAGE)); SetFontAndTextDirection(&localized_strings); @@ -1376,11 +1409,10 @@ NewTabUI::NewTabUI(TabContents* contents) DownloadManager* dlm = GetProfile()->GetDownloadManager(); DownloadsDOMHandler* downloads_handler = new DownloadsDOMHandler(this, dlm); - AddMessageHandler(downloads_handler); - AddMessageHandler(new BrowsingHistoryHandler(this)); - downloads_handler->Init(); + + AddMessageHandler(new ShownSectionsHandler(this)); } if (EnableWebResources()) @@ -1443,6 +1475,8 @@ void NewTabUI::RegisterUserPrefs(PrefService* prefs) { MostVisitedHandler::RegisterUserPrefs(prefs); if (NewTabUI::EnableWebResources()) WebResourceHandler::RegisterUserPrefs(prefs); + if (NewTabUI::EnableNewNewTabPage()) + ShownSectionsHandler::RegisterUserPrefs(prefs); } // static diff --git a/chrome/browser/dom_ui/shown_sections_handler.cc b/chrome/browser/dom_ui/shown_sections_handler.cc new file mode 100644 index 0000000..d9c3488 --- /dev/null +++ b/chrome/browser/dom_ui/shown_sections_handler.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2009 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. + +#include "chrome/browser/dom_ui/shown_sections_handler.h" + +#include "base/string_util.h" +#include "base/values.h" +#include "chrome/browser/profile.h" +#include "chrome/common/pref_names.h" + +ShownSectionsHandler::ShownSectionsHandler(DOMUI* dom_ui) + : DOMMessageHandler(dom_ui), + dom_ui_(dom_ui) { + dom_ui->RegisterMessageCallback("getShownSections", + NewCallback(this, &ShownSectionsHandler::HandleGetShownSections)); + dom_ui->RegisterMessageCallback("setShownSections", + NewCallback(this, &ShownSectionsHandler::HandleSetShownSections)); +} + +void ShownSectionsHandler::HandleGetShownSections(const Value* value) { + const int mode = dom_ui_->GetProfile()->GetPrefs()-> + GetInteger(prefs::kNTPShownSections); + FundamentalValue* mode_value = new FundamentalValue(mode); + dom_ui_->CallJavascriptFunction(L"onShownSections", *mode_value); +} + +void ShownSectionsHandler::HandleSetShownSections(const Value* value) { + if (!value->IsType(Value::TYPE_LIST)) { + NOTREACHED(); + return; + } + + const ListValue* list = static_cast<const ListValue*>(value); + std::string mode_string; + + if (list->GetSize() < 1) { + NOTREACHED() << "setShownSections called with too few arguments"; + return; + } + + bool r = list->GetString(0, &mode_string); + DCHECK(r) << "Missing value in setShownSections from the NTP Most Visited."; + + dom_ui_->GetProfile()->GetPrefs()->SetInteger( + prefs::kNTPShownSections, StringToInt(mode_string)); +} + +// static +void ShownSectionsHandler::RegisterUserPrefs(PrefService* prefs) { + prefs->RegisterIntegerPref(prefs::kNTPShownSections, + THUMB | RECENT | RECOMMENDATIONS); +} + diff --git a/chrome/browser/dom_ui/shown_sections_handler.h b/chrome/browser/dom_ui/shown_sections_handler.h new file mode 100644 index 0000000..d70ba1c --- /dev/null +++ b/chrome/browser/dom_ui/shown_sections_handler.h @@ -0,0 +1,40 @@ +// Copyright (c) 2006-2009 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. + +#ifndef CHROME_BROWSER_DOM_UI_SHOWN_SECTIONS_HANDLER_H_ +#define CHROME_BROWSER_DOM_UI_SHOWN_SECTIONS_HANDLER_H_ + +#include "chrome/browser/dom_ui/dom_ui.h" + +class DOMUI; +class Value; +class PrefService; + +// Use for the shown sections bitmask. +enum Section { + THUMB = 1, + LIST = 2, + RECENT = 4, + RECOMMENDATIONS = 8 +}; + +class ShownSectionsHandler : public DOMMessageHandler { + public: + explicit ShownSectionsHandler(DOMUI* dom_ui); + + // Callback for "getShownSections" message. + void HandleGetShownSections(const Value* value); + + // Callback for "setShownSections" message. + void HandleSetShownSections(const Value* value); + + static void RegisterUserPrefs(PrefService* prefs); + + private: + DOMUI* dom_ui_; + + DISALLOW_COPY_AND_ASSIGN(ShownSectionsHandler); +}; + +#endif // CHROME_BROWSER_DOM_UI_SHOWN_SECTIONS_HANDLER_H_ diff --git a/chrome/browser/resources/new_new_tab.css b/chrome/browser/resources/new_new_tab.css new file mode 100644 index 0000000..9aae5b4 --- /dev/null +++ b/chrome/browser/resources/new_new_tab.css @@ -0,0 +1,665 @@ +html { + /* This is needed because of chrome://theme/css/new_tab.css */ + height: 100%; +} + +body { + margin: 0; +} + +#main { + background: url(chrome://theme/product_logo) no-repeat 0 0px; + position: relative; + margin: 0 auto; + width: 940px; + -webkit-transition: width .5s; +} + +html[dir='rtl'] #main { + background-position-x: 100%; +} + +.small #main { + width: 692px; +} + +html[anim='false'], +.no-anim, .no-anim *, +.loading * { + -webkit-transition: none !important; + -webkit-animation: none !important; +} + +/* Most Visited */ + +#most-visited { + position: relative; + padding: 0; + margin-bottom: 34px; + -webkit-user-select: none; + -webkit-transition: height .5s, opacity .5s; +} + +@-webkit-keyframes 'thumbnail-enter' { + /* 2.5s */ + 0%, 20% { + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, .5); + -webkit-border-radius: 4px; + } + + 100% { + -webkit-border-top-left-radius: 0; + -webkit-border-top-right-radius: 0; + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, 0); + } +} + +@-webkit-keyframes 'edit-mode-border-enter' { + /* 2.5s */ + 0%, 20% { + background-color: hsla(213, 66%, 57%, 0); + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, 0); + } + + 100% { + background-color: hsla(213, 66%, 57%, 1); + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, .5); + } +} + +@-webkit-keyframes 'edit-bar-enter' { + /* 2.5s */ + 0%, 20% { + opacity: 0; + pointer-events: none; + } + + 100% { + opacity: 1; + pointer-events: inherit; + } +} + +.thumbnail-container { + position: absolute; + -webkit-transition: top .5s, left .5s; + color: black; + text-decoration: none; + -webkit-transition: left .5s, top .5s; +} + +.thumbnail, +.thumbnail-container .title { + width: 212px; /* natural size is 196 */ + height: 132px; /* 136 */ + -webkit-transition: width .5s, height .5s; +} + +.small .thumbnail, +.small .thumbnail-container .title { + width: 150px; + height: 93px; +} + +.thumbnail-wrapper { + display: block; + -webkit-background-size: 212px 132px; + background: no-repeat 4px 4px; + background-color: white; + -webkit-border-radius: 5px; + -webkit-transition: -webkit-background-size .5s; +} + +.small .thumbnail-wrapper { + -webkit-background-size: 150px 93px; +} + +.filler * { + visibility: hidden; +} + +.filler { + pointer-events: none; +} + +.filler .thumbnail-wrapper { + visibility: inherit; + border: 1px solid hsl(0, 0%, 90%); + background-color: hsl(0, 0%, 95%); + -webkit-border-radius: 2px; + display: block; +} + +.filler .thumbnail-wrapper .thumbnail { + visibility: visible; + border-color: hsl(0, 0%, 90%); + background-color: hsl(0, 0%, 95%); +} + +.edit-bar { + opacity: 0; + pointer-events: none; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: stretch; + padding: 3px; + padding-bottom: 0; + height: 17px; /* 23 - 2 * 3 */ + -webkit-transition: opacity .5s; + cursor: move; + font-size: 100%; + line-height: 17px; + background-image: -webkit-gradient(linear, left top, left bottom, + from(hsl(213, 87%, 67%)), + to(hsl(213, 66%, 57%))); + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; +} + +.edit-bar > * { + display: block; + position: relative; +} + +.thumbnail-container:hover .edit-bar { + -webkit-animation: 'edit-bar-enter' 2.5s; + opacity: 1; + pointer-events: inherit; +} + +.edit-bar { + +} + +.edit-bar > .edit-link { + padding: 0 5px; + color: hsl(213, 88%, 94%); + font-weight: bold; +} + +.edit-bar > .spacer { + -webkit-box-flex: 1; +} + +.edit-bar > .pin, +.edit-bar > .remove { + width: 16px; + height: 16px; + cursor: pointer; + background-image: no-repeat 50% 50%; +} + +.edit-bar > .pin { + background-image: url(chrome://theme/newtab_pin_off); +} + +.edit-bar > .pin:hover { + background-image: url(chrome://theme/newtab_pin_off_h); +} + +.edit-bar > .pin:active { + background-image: url(chrome://theme/newtab_pin_off_p); +} + +.pinned .edit-bar > .pin { + background-image: url(chrome://theme/newtab_pin_on); +} + +.pinned .edit-bar > .pin:hover { + background-image: url(chrome://theme/newtab_pin_on_h); +} + +.pinned .edit-bar > .pin:active { + background-image: url(chrome://theme/newtab_pin_on_p); +} + +.edit-bar > .remove { + background-image: url(chrome://theme/newtab_close); +} + +.edit-bar > .remove:hover { + background-image: url(chrome://theme/newtab_close_h); +} + +.edit-bar > .remove:active { + background-image: url(chrome://theme/newtab_close_p); +} + +:link, +:visited, +.link, +.thumbnail-container a { + cursor: pointer; + text-decoration: underline; + color: hsl(213, 90%, 24%); +} + +.thumbnail-container .title, +.small .thumbnail-container .title { + line-height: 16px; + height: 16px; + margin: 0; + margin-top: 4px; + font-size: 100%; + font-weight:normal; + padding: 0 3px; + opacity: 1; + -webkit-transition: opacity .5s; + color: black; +} + +.thumbnail-container .title div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + background: no-repeat 0 50%; + -webkit-background-size: 16px; + padding-left: 20px; /* we cannot use padding start here because even if we set + the direction we always want the icon on the same side + */ + padding-right: 0; +} + +html[dir=rtl] .thumbnail-container .title div { + background-position-x: 100%; + padding-left: 0; + padding-right: 20px; + text-align: right; +} + +.thumbnail { + border: 3px solid hsla(213, 63%, 93%, 1); + padding: 1px; + -webkit-border-radius: 5px; + display: block; + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, 0); + -webkit-transition: width .5s, height .5s, border-color .5s, + -webkit-border-radius .5s, -webkit-box-shadow .5s; +} + +.edit-mode-border { + -webkit-border-radius: 4px; + background-color: hsla(213, 54%, 95%, 0); +} + +.thumbnail-container:hover .thumbnail { + -webkit-animation: 'thumbnail-enter' 2.5s; + border-color: hsla(213, 66%, 57%, 1); + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, 0); + -webkit-border-top-left-radius: 0; + -webkit-border-top-right-radius: 0; + + background-image: -webkit-gradient(linear, left top, left bottom, + from(hsla(0, 0%, 0%, 0)), + color-stop(0.85, hsla(0, 0%, 47%, 0)), + to(hsla(0, 0%, 47%, 0.2)) + ); +} + +.thumbnail-container:hover .edit-mode-border { + -webkit-animation: 'edit-mode-border-enter' 2.5s; + background-color: hsla(213, 66%, 57%, 1); + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, .5); +} + +.dragging { + -webkit-transition: none; +} + +.dragging .title { + opacity: 0; +} + +.list .dragging .title { + opacity: 1; +} + +.hide { + opacity: 0 !important; + visibility: hidden !important; +} + +@-webkit-keyframes 'fade-in' { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.fade-in { + -webkit-animation: 'fade-in' 2.5s; +} + +/* Notification */ + +#notification { + position: relative; + background-color: hsl(52, 100%, 80%); + -webkit-border-radius: 4px; + padding: 8px 16px; + white-space: nowrap; + display: table; + margin: 10px auto; + font-weight: bold; + opacity: 0; + visibility: hidden; + -webkit-transition: opacity 1s; + z-index: 1; +} + +#notification > * { + display: table-cell; + max-width: 500px; + overflow: hidden; + text-overflow: ellipsis; +} + +.small #notification > * { + max-width: 300px; +} + +#notification:hover, +#notification.show { + opacity: 1; + visibility: inherit; +} + +#notification .link { + -webkit-margin-start: 20px; +} + +/* List mode */ + +.list .thumbnail, +.list .edit-bar { + display: none; +} + +.list .title, +.small .list .title { + width: auto; +} + +.list .thumbnail-container { + -webkit-box-sizing: border-box; +} + +.list .title, +.small .list .title { + font-size: 140%; + line-height: 40px; + height: 40px; + color: hsl(213, 27%, 68%); + text-decoration: underline; +} + +.list .title div { + color: rgb(6, 45, 117); +} + +.section { + background: white; + -webkit-border-radius: 4px; + white-space: nowrap; + text-overflow: ellipsis; + border: 1px solid hsl(213, 65%, 92%); + padding-bottom: 5px; + overflow: hidden; + min-height: 175px; + vertical-align: top; + margin: 0; +} + +.section > h2 { + background-image: -webkit-gradient(linear, left top, left bottom, + from(hsl(213, 87%, 67%)), + to(hsl(213, 66%, 57%))); + -webkit-border-top-left-radius: 2px; + -webkit-border-top-right-radius: 2px; + padding: 4px 8px; + color: white; + font-size: 100%; + margin: 0px; +} + +.item { + background: no-repeat 0% 50%; + padding: 2px; + -webkit-padding-start: 18px; + -webkit-background-size: 16px; + background-color: hsla(213, 63%, 93%, 0); + display: block; + line-height: 20px; + -webkit-box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-decoration: none; + font-size: 100%; +} + +html[dir='rtl'] .item { + background-position-x: 100%; +} + +.window { + position: relative; + overflow: visible; /* We use visible so that the menu can be a child and shown + on :hover. To get this to work we have to set visibility + to visible which unfortunately breaks the ellipsis for t + he window items */ + background-image: url(chrome://theme/newtab_closed_window); +} + +.window-menu { + position: absolute; + display: none; + border: 1px solid #999; + -webkit-box-shadow: 5px 5px 10px hsla(0, 0%, 0%, .3); + background-color: white; + width: 157px; + left: 0; + white-space: nowrap; + opacity: .9; + z-index: 1; + pointer-events: none; +} + +.item:hover > .window-menu, +.item:focus > .window-menu { + display: block; +} + +.item:hover .window-menu .item, +.item:focus .window-menu .item { + background-color: transparent; + text-decoration: none; + margin: 0; + font-size: 100%; +} + +.hbox { + display: -webkit-box; + -webkit-box-orient: horizontal; +} + +#recent-tabs, +#downloads { + -webkit-box-flex: 1; + width: 50%; +} + +.section h3 { + padding: 8px 5px; + padding-bottom: 0; + font-size: 80%; + font-weight: normal; + margin: 0; + -webkit-margin-start: 2px; + overflow: hidden; + text-overflow: ellipsis; +} + +.item-container { + margin: 0 5px; + text-decoration: underline; + color: hsl(213, 27%, 68%); +} + +.item.nav:after { + content: '\00bb'; /* raque gets flipped automatically in rtl */ + font-size: 120%; + -webkit-padding-start: 2px; +} + +#recent-activities { + -webkit-transition: width .5s, opacity .5s; +} + +.section { + -webkit-box-flex: 1; + width: 50%; + display: inline-block; +} + +#view-toolbar { + -webkit-user-select: none; + text-align: right; + position: relative; + top: 35px; +} + +html[dir='rtl'] #view-toolbar { + text-align: left; +} + +#view-toolbar input { + -webkit-appearance: none; + background-color: transparent; + width: 19px; + height: 17px; + margin: 0; + border: 0; + padding: 0; + vertical-align: top; + -webkit-margin-start: 5px; +} + +#lower-sections { + position: relative; + overflow: hidden; + -webkit-transition: height .5s, width .5s, opacity .5s; + white-space: nowrap; +} + +#lower-sections .section { + -webkit-transition: width .5s, opacity .5s, left .5s; + -webkit-box-sizing: border-box; +} + +#lower-sections .spacer { + width: 20px; + -webkit-transition: width .5s; + display: inline-block; +} + +.loading * { + -webkit-transition: none; +} + +#thumb-checkbox { + background-image: url(chrome://theme/newtab_thumb_off); +} + +#thumb-checkbox:hover { + background-image: url(chrome://theme/newtab_thumb_off_h); +} + +#thumb-checkbox:active { + background-image: url(chrome://theme/newtab_thumb_off_p); +} + +#thumb-checkbox:checked { + background-image: url(chrome://theme/newtab_thumb_on); +} + +#thumb-checkbox:checked:hover { + background-image: url(chrome://theme/newtab_thumb_on_h); +} + +#thumb-checkbox:checked:active { + background-image: url(chrome://theme/newtab_thumb_on_p); +} + +#list-checkbox { + background-image: url(chrome://theme/newtab_list_off); +} + +#list-checkbox:hover { + background-image: url(chrome://theme/newtab_list_off_h); +} + +#list-checkbox:active { + background-image: url(chrome://theme/newtab_list_off_p); +} + +#list-checkbox:checked { + background-image: url(chrome://theme/newtab_list_on); +} + +#list-checkbox:checked:hover { + background-image: url(chrome://theme/newtab_list_on_h); +} + +#list-checkbox:checked:active { + background-image: url(chrome://theme/newtab_list_on_p); +} + +#option-button { + background-image: url(chrome://theme/newtab_option); +} + +#option-button:hover { + background-image: url(chrome://theme/newtab_option_h); +} + +#option-button:active { + background-image: url(chrome://theme/newtab_option_p); +} + +#option-menu { + -webkit-user-select: none; + position: absolute; + right: 0; + left: auto; + top: 53px; /* Position this below the option button. The option button + is positioned 35px from the top and has a height of 17px. We add + one to get some spacing there as well: 35 + 17 + 1 = 53 */ + cursor: default; + pointer-events: all; + min-width: 175px; + outline: 0; +} + +html[dir='rtl'] #option-menu { + right: auto; + left: 0; +} + +#option-menu > div { + padding: 3px 8px; + margin: 1px; + overflow: hidden; + text-overflow: ellipsis; +} + +#option-menu > div:hover, +#option-menu > div:focus { + background-color: hsla(213, 66%, 57%, 1); + color: white; + outline: none; +} diff --git a/chrome/browser/resources/new_new_tab.html b/chrome/browser/resources/new_new_tab.html index 7897c44..0372260 100644 --- a/chrome/browser/resources/new_new_tab.html +++ b/chrome/browser/resources/new_new_tab.html @@ -1,1402 +1,184 @@ -<!DOCTYPE HTML> -<html id="t" jsvalues="dir:textdirection;firstview:firstview;anim:anim"> -<!-- - This page is optimized for perceived performance. Our enemies are the time - taken for the backend to generate our data, and the time taken to parse - and render the starting HTML/CSS content of the page. This page is - designed to let Chrome do both of those things in parallel. +<!DOCTYPE html> +<html id="t" jsvalues="dir:textdirection;firstview:firstview;bookmarkbarattached:bookmarkbarattached;hasattribution:hasattribution;anim:anim"> - 1. Defines temporary content callback functions - 2. Fires off requests for content (these can come back 20-150ms later) - 3. Defines basic functions (handlers) - 4. Renders a fast-parse hard-coded version of itself (this can take 20-50ms) - 5. Defines the full content-rendering functions - - If the requests for content come back before the content-rendering functions - are defined, the data is held until those functions are defined. ---> -<script src="local_strings.js"></script> +<meta charset="utf-8"> +<title jscontent="title"></title> <script> - // Logging info for benchmarking purposes. var log = []; function logEvent(name) { log.push([name, Date.now()]); } -// Basic functions to send, receive, store and process the data from our -// backend. -var unprocessedData = { - mostVisitedPages: false, - searchURLs: false -}; - -var recent = { - MAX_ITEMS: 20, - entries: [], - entriesById: {}, - dirty: true, - appendData: function(data, constr) { - var self = this; - data.forEach(function(d) { - self.add(new constr(d)); - }); - }, - - add: function(entry) { - if (!(entry instanceof Entry)) { - alert('Not an instance of Entry:\n\n' + JSON.stringify(entry)); - } - // Don't add if already in there. - var id = entry.id; - if (!(id in this.entriesById)) { - this.entries.push(entry); - this.entriesById[id] = entry; - this.dirty = true; - } - }, - - remove: function(entry) { - var id = entry.id; - if (id in this.entriesById) { - entry.remove(); - delete this.entriesById[id]; - this.dirty = true; - } - }, - - sortEntries: function() { - this.entries.sort(function(e1, e2) { - return e2.time - e1.time; - }); - } -}; - -var renderFunctionsDefined = false; - -var localStrings; - -// The list of URLs that have been blacklisted (so that thumbnails are not -// shown) in the last set of change. Used to revert changes when the Cancel -// button is pressed. -var blacklistedURLs = []; - -function $(o) {return document.getElementById(o);} - -/////////////////////////////////////////////////////////////////////////////// - - -function Entry(data) { - this.data = data; -} - -Entry.prototype = { - get url() { - return this.data.url; - }, - get title() { - return this.data.title; - }, - get icon() { - return 'chrome://favicon/' + this.data.url; - }, - get time() { - return this.data.time; - }, - get id() { - return this.type + '-' + this.url + '-' + this.time; - }, - createDom: function() { - var data = this.data; - var link = DOM('a', { - title: this.title, - href: data.url, - textContent: this.title - }); - var div = DOM('div', { - id: this.id, - className: 'recent-item ' + this.type - }); - - link.style.backgroundImage = 'url("' + this.icon + '")'; - - // Set the title's directionality independently of the page, see comment - // about setting div_title.style.direction above for details. - link.style.direction = data.direction; - - // The following if statement is a temporary workaround for - // http://crbug.com/7252 and http://crbug.com/7697. It should be removed - // before closing these bugs. - if (data.direction == 'rtl') { - link.style.textOverflow = 'clip'; - } - - this.addListeners(div, link); - div.appendChild(link); - - return this.element = div; - }, - addListeners: function(div, link) { - }, - remove: function() { - this.removed = true; - var div = $(this.id); - if (!div || !div.parentNode) return; - div.parentNode.removeChild(div); - } -}; - -function DownloadEntry(data) { - Entry.call(this, data); -} - -DownloadEntry.prototype = { - __proto__: Entry.prototype, - type: 'download', - typeImageUrl: '', - get title() { - return this.data.file_name; - }, - get icon() { - return 'chrome://fileicon/' + this.data.file_path; - }, - get time() { - return this.data.started; - }, - addListeners: function(div, link) { - // TODO(arv): What about non safe files? - // TODO(arv): What about non finished files? - var id = this.data.id; - link.onclick = function(e) { - chrome.send("openFile", [String(id)]); - e.preventDefault(); - } - } -}; - -function HistoryEntry(data) { - Entry.call(this, data); -} -HistoryEntry.prototype = { - __proto__: Entry.prototype, - type: 'history' -}; - -function BookmarkEntry(data) { - Entry.call(this, data); -} -BookmarkEntry.prototype = { - __proto__: Entry.prototype, - type: 'bookmark', - addListeners: function(div, link) { - var index = this.data.index; - link.addEventListener('mousedown', function(event) { - chrome.send('metrics', ['NTP_Bookmark' + index]); - }, false); - } -}; - -function TabEntry(data) { - if (data.type == 'window') { - return new WindowEntry(data); - } - Entry.call(this, data); -} -TabEntry.prototype = { - __proto__: Entry.prototype, - type: 'tab', - typeImageUrl: '', - - // Neither closed tabs or windows have a time stamp. Use now as the time - get time() { - return this.time_ || (this.time_ = Math.floor(Date.now() / 1000)); - }, - addListeners: function(div, link) { - var sessionId = this.data.sessionId; - var index = this.data.index; - link.onclick = function(e) { - chrome.send('metrics', ['NTP_TabRestored' + index]); - // This is a hack because chrome.send is hardcoded to only - // accept arrays of strings. - chrome.send('reopenTab', [String(sessionId)]); - e.preventDefault(); - }; - }, - get id() { - return this.type + '-' + this.data.sessionId; - } -}; - -function WindowEntry(data) { - // This actually extends TabEntry but the tab entry constructor redirects to - // this constructor so don't call it here because that would cause an infinite - // loop. - Entry.call(this, data); -} -WindowEntry.prototype = { - __proto__: TabEntry.prototype, - type: 'window', - url: '', - createDom: function() { - var entry = this.data; - var div = DOM('div', { - id: this.id, - className: 'recent-window-container' - }); - - var linkSpan = DOM('span', { - textContent: ' ' - }); - for (var windowIndex = 0; windowIndex < entry.tabs.length; windowIndex++) { - var tab = entry.tabs[windowIndex]; - var tabImg = DOM('img', { - src: 'url("chrome://favicon/' + tab.url + '")', - width: 16, - height: 16, - onmousedown: function() { - return false; - } - }); - linkSpan.appendChild(tabImg); - } - - var link = DOM('span', { - className: 'recently-closed-window-link', - tabIndex: 0 - }); - var windowSpan = DOM('span', { - className: 'recently-close-window-text', - textContent: recentlyClosedWindowText(entry.tabs.length) - }); - link.appendChild(windowSpan); - link.appendChild(linkSpan); - div.appendChild(link); - - - // The card takes care of appending itself to the DOM, so no need to - // keep a reference to it. - new RecentlyClosedHoverCard(link, entry); - - this.addListeners(div, link); - - return this.element = div; - } -}; +var global = this; /** - * If the functions that can render content are defined, render - * the content for any data we've received so far. + * Registers a callback function so that if the backend calls it too early it + * will get delayed until DOMContentLoaded is fired. + * @param {string} name The name of the global function that the backend calls. */ -function processData() { - // This is ugly. Can we refactor this? - if (renderFunctionsDefined) { - if (unprocessedData.mostVisitedPages) { - renderMostVisitedPages(unprocessedData.mostVisitedPages); - unprocessedData.mostVisitedPages = false; - } - if (unprocessedData.searchURLs) { - //renderSearchURLs(unprocessedData.searchURLs); - renderRecentItems(); - unprocessedData.searchURLs = false; - } - - if (recent.dirty && recent.entries.length) { - renderRecentItems(); - } - } -} - -function mostVisitedPages(data) { - logEvent('received most visited pages'); - unprocessedData.mostVisitedPages = data; - processData(); -} - -function searchURLs(data) { - logEvent('received search URLs'); - unprocessedData.searchURLs = data; - processData(); -} - -function recentlyBookmarked(data) { - logEvent('received recently bookmarked data'); - replaceData(data, BookmarkEntry); -} - -function replaceData(data, ctor) { - var oldEntries = {}; - recent.entries.forEach(function(entry) { - if (entry instanceof ctor) { - oldEntries[entry.id] = entry; - } - }); - - var entriesToAdd = {}; - data.map(function(d) { - var newEntry = new ctor(d); - var id = newEntry.id; - if (id in oldEntries && !oldEntries[id].removed) { - // Already present. No need to add or remove. - delete oldEntries[id]; - } else { - recent.add(newEntry); +function registerCallback(name) { + var f = function(var_args) { + var args = Array.prototype.slice.call(arguments); + // If we still have the temporary function we delay until the dom is ready. + if (global[name] == f) { + logEvent(name + ' is not yet ready. Waiting for DOMContentLoaded'); + document.addEventListener('DOMContentLoaded', function() { + logEvent('Calling the new ' + name); + global[name].apply(null, args); + }); } - }); - - for (var id in oldEntries) { - recent.remove(oldEntries[id]); - } - - processData(); -} - -function recentlyClosedTabs(data) { - logEvent('received recently closed tabs'); - // Add index to the data. - var i = 0; - data.forEach(function(d) { - d.index = i++; - }); - replaceData(data, TabEntry); -} - -function historyResult(info, results) { - logEvent('received recent history'); - // TODO(arv): If we have more history and we want to show more recent items - // we should fetch more items from the history. - recent.appendData(results, HistoryEntry); - if (!info.finished && results.length < recent.MAX_ITEMS) { - getMoreHistory(); - } - processData(); -} - -function downloadsList(data) { - logEvent('received downloads'); - recent.appendData(data, DownloadEntry); - processData(); -} - -function resizeP13N(new_height) { - var childf = $('p13n'); - if (new_height < 1) { - childf.style.display = "none"; - return; - } - childf.height = new_height; - childf.style.display = "block"; -} - -var currentHistoryDay = 0; -function getMoreHistory() { - chrome.send('getHistory', [String(currentHistoryDay++)]); -} - -function handleWindowResize() { - var body = document.body; - if (!body || body.clientWidth < 10) { - // We're probably a background tab, so don't do anything. - return; - } - - if (body.className == 'small' && body.clientWidth >= 885) { - body.className = ''; - } else if (body.className == '' && body.clientWidth <= 865) { - body.className = 'small'; - } + }; + global[name] = f; } -function handleDOMContentLoaded() { - logEvent('domcontentloaded fired'); -} +chrome.send('getShownSections'); +chrome.send('getMostVisited'); +chrome.send('getDownloads'); +chrome.send('getRecentlyClosedTabs'); -chrome.send("getMostVisited"); -//chrome.send("getMostSearched"); -chrome.send("getRecentlyBookmarked"); -chrome.send("getRecentlyClosedTabs"); -getMoreHistory(); -chrome.send('getDownloads', ['']); +registerCallback('onShownSections'); +registerCallback('mostVisitedPages'); +registerCallback('downloadsList'); +registerCallback('recentlyClosedTabs'); logEvent('log start'); -</script> -<head> -<meta charset="utf-8"> -<title jscontent="title"></title> -<style> -body { - background-color:white; - margin:0px; - background-image:url(chrome://theme/theme_newtab_background); - background-repeat:repeat-x; - background-attachment: fixed; -} -html[firstview='true'] #main { - opacity:0.0; - -webkit-transition:all 0.4s; -} -html[firstview='true'] #main.visible { - opacity:1.0; -} -html[anim='false'] * { - -webkit-transition-property: none !important -} -#main { - margin-left:auto; - margin-right:auto; - margin-top:10px; - width: 838px; - -webkit-transition:width .12s; -} -.small #main { - width: 658px; /* 4 * 217 */ - -} -form { - padding: 0; - margin: 0; -} -.section { - padding:3px 0px 5px 0px; - margin-bottom:30px; -} -.section-title { - color:#000; - line-height:19pt; - font-size:110%; - font-weight:bold; - margin-bottom:4px; -} -#recent-section { - margin-top: 1.5em; -} -#recent-section > .section-title { - margin-bottom: 1em; -} -#mostvisitedsection { - margin:0px 5px 0px 0px; -} -#mostvisited td { - padding:0px 10px 10px 0px; -} -html[dir='rtl'] #mostvisited td { - padding:0px 0px 10px 10px; -} -.most-visited-text { - position:absolute; - left:100px; - right:100px; - padding:20px; - margin:15px; - background-color:white; - -webkit-box-shadow: 5px 5px 10px #ccc; - -webkit-transition:all .12s; - z-index: 1; -} -.thumbnail-title { - display:block; - width:195px; /* thumbnail */ - margin-top:6px; /* line up favicons with search favicons */ - padding:1px; - overflow: hidden; - text-overflow: ellipsis; - text-decoration:none; - -webkit-transition:all .12s; - -webkit-box-sizing:border-box; -} -html[dir='rtl'] .thumbnail-title { - background-position:right; - padding-left:0px; - padding-right:22px; - text-align:right; -} -.thumbnail { - width:195px; - height:136px; - border:1px solid #ccc; - background-color:#eee; - -webkit-transition:all .12s; - position: relative; - -webkit-box-shadow:#ccc 1px 1px 5px; -} -a.thumbnail { - border:1px solid #abe; -} -.thumbnail-icon { - right: -5px; - bottom: -5px; - width: 32px; - height: 32px; - position: absolute; - /* a shadow (non box shadow) might make the icon stand out more - -webkit-box-shadow: 3px 3px 5px #ccc; - */ -} - -.small .thumbnail-title { - width:127px; -} -.small .thumbnail { - width:150px; - height:113px; -} -.small .most-visited-text { - width:430px; - padding:15px; - margin:12px; -} -.recent-item { - margin:3px 0px 3px 0px; - height:16pt; - line-height:16px; - overflow: hidden; - text-overflow: ellipsis; - background: no-repeat center left; - padding-left: 22px; -} -.recent-item.history { - background-image: url('../../app/theme/o2_history.png'); -} -.recent-item.bookmark { - background-image: url('../../app/theme/o2_star.png'); -} -.recent-item > a { - background-repeat:no-repeat; - -webkit-background-size:16px; - background-position:center left; - padding:1px 0px 0px 22px; -} - -.recent-bookmark { - display:block; - background-repeat:no-repeat; - background-size:16px; - background-position:0px 1px; - padding:1px 0px 0px 22px; - margin:3px 0px 3px 0px; - min-height:16pt; - line-height:16px; - overflow: hidden; - text-overflow: ellipsis; - text-decoration:underline; -} -.recent-window-container { - line-height:16px; - display:block; - position: relative; - margin:0 3px; - padding-left: 22px; - margin-left: 22px; - - /* TODO(arv): Remove hard coded URL */ - background-image: url('../../app/theme/closed_window.png') !important; -} -.recent-window-container img { - margin:0 3px -2px 3px; -} -.recent-window-hover-container { - position:absolute; - border:1px solid #999; - -webkit-box-shadow: 5px 5px 10px #ccc; - background-color:white; - width: 157px; - left: 20px; - white-space:nowrap; - opacity:.9; -} -.recent-window-hover-container .recent-bookmark { - text-decoration:none; - text-overflow:ellipsis; - overflow:hidden; - margin: 3px 0 0 5px; -} -.recently-closed-window-link { - 'text-decoration:none'; -} -.recently-closed-window-link:hover { - cursor:pointer; -} -.recently-close-window-text { - text-decoration:underline; - color: #0000cc; -} - -html[dir='rtl'] .recent-bookmark { - background-position:right; - padding-left:0px; - padding-right:22px; -} -a { - color:#0000cc; - text-decoration:underline; - white-space: nowrap; -} -a.manage { - color:#77c; - margin-left: 5px; - margin-right: 5px; - line-height:19pt; - text-decoration:underline; -} -html[dir='rtl'] #managesearcheslink { - float: left; -} -.sidebar { - width: 207px; - padding:3px 10px 3px 9px; - -webkit-border-radius:5px 5px; - margin-bottom:10px; -} -#searches { - background-color:#e1ecfe; -} -#recentlyBookmarked { - background-color:#e1ecfe; -} -html[dir='rtl'] #recentlyBookmarkedContainer { - text-align:right; -} -#recentlyClosedContainer { - position:relative; -} -html[dir='rtl'] #recentlyClosedContainer { - text-align:right; -} -#searches input { - border:1px solid #7f9db9; - background-repeat: no-repeat; - background-position:4px center; - padding-left: 23px; - min-height:24px; - width:182px; - margin-bottom:8px; - display:block; -} -html[dir='rtl'] #searches input { - background-position: right; - padding-left:0px; - padding-right: 23px; -} -#searches input:-webkit-input-placeholder-mode { - color: #aaa; -} -.footer { - border-top:1px solid #ccc; - padding-top:4px; - font-size:8pt; -} -.edit-visible { - display: none; -} -.edit-mode .edit-visible { - display: inline; -} -.non-edit-visible { - display: inline; -} -.edit-mode .non-edit-visible { - display: none; -} -.most-visited-container { - position: relative; - left: 0px; - top: 0px; -} -.edit-mode .disabled-on-edit { - opacity: 0.5; - pointer-events: none; /* Disable clicks */ -} -</style> +</script> +<link rel="stylesheet" href="new_new_tab.css"> +<link id="themecss" rel="stylesheet" href="chrome://theme/css/newtab.css"> </head> -<body onload="logEvent('body onload fired');" +<body class="loading" jsvalues=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> -<script> -// We apply the size class here so that we don't trigger layout animations onload. -handleWindowResize(); -window.addEventListener('resize', handleWindowResize, true); -document.addEventListener('DOMContentLoaded', handleDOMContentLoaded); -</script> <div id="main"> - <div id="mostvisitedsection" class="section"> - <div id="mostvisited" style="position:relative;"> - <div> - <span class="section-title non-edit-visible" jscontent="mostvisited"></span> - <span class="section-title edit-visible" jseval="this.innerHTML = $this.editmodeheading;"></span> - </div> - <div id="mostvisitedintro" style="display:none;"> - <div class="most-visited-text" jseval="this.innerHTML = $this.mostvisitedintro;"></div> - <table> - <tr> - <td><div class="thumbnail"> </div></td> - <td><div class="thumbnail"></div></td> - <td><div class="thumbnail"> </div></td> - <td><div class="thumbnail"></div></td> - </tr> - <tr> - <td><div class="thumbnail"> </div></td> - <td><div class="thumbnail"></div></td> - <td><div class="thumbnail"> </div></td> - <td><div class="thumbnail"></div></td> - </tr> - </table> - </div> - <table id="mostvisitedtable"> - <!-- This content forces the view to the correct width and provides a - preview of what's to load to reduce white-flash. Users who get - the mostvisitedintro will see a brief flash of this content. We - only use one row so that we may avoid flashing extra rows when - the user has only one row of items --> - <tr> - <td> - <div class="thumbnail-title"> </div> - <div class="thumbnail"></div> - </td> - <td> - <div class="thumbnail-title"> </div> - <div class="thumbnail"></div> - </td> - <td> - <div class="thumbnail-title"> </div> - <div class="thumbnail"></div> - </td> - <td> - <div class="thumbnail-title"> </div> - <div class="thumbnail"></div> - </td> - </tr> - </table> - </div> - <a href="#" - class="manage non-edit-visible" - onClick="enterEditMode(); return false"> - <span jscontent="editthumbnails"></span></a> - <button type="button" class="edit-visible" onClick="exitEditMode();" - jscontent="doneediting"></button> - <button type="button" class="edit-visible" onClick="cancelEdits();" - jscontent="cancelediting"></button> - <a href="#" - class="manage edit-visible" - onClick="restoreThumbnails(); return false"> - <span jscontent="restorethumbnails"></span></a> - <a href="#" - jsvalues="href:showhistoryurl" - class="manage non-edit-visible"> - <span jscontent="showhistory"></span> »</a> - </div> - - <div id="recent-section"> - <div class="section-title" jscontent="recent"></div> - <div id="recent-items"></div> - </div> - - <div style="display:none"> - <div align="right"> - <img src="../../app/theme/%DISTRIBUTION%/product_logo.png" - width="145" height="52" style="padding-bottom:8px;" /> - </div> - <iframe id="p13n" frameborder="0" width="100%" scrolling="no" height="0" - jsdisplay="p13nsrc" style="display:none;" - jsvalues="src:p13nsrc"></iframe> - <div id="searches" class="sidebar"> - <div class="section-title" jscontent="searches"></div> - <form onsubmit="chrome.send('searchHistoryPage', [this.search.value]); return false;"> - <input type="text" class="hint" - name="search" - style="background-image:url(chrome://favicon/);" - jsvalues="placeholder:searchhistory"> - </form> - <div id='searches-entries'></div> - </div> - - <div id="recentlyBookmarked" class="sidebar" style="display:none"> - <span class="section-title" jscontent="bookmarks"></span> - <div id="recentlyBookmarkedContainer"></div> - </div> - - <div id="recentlyClosedTabs" class="sidebar" style="display:none"> - <div class="section-title" jscontent="recentlyclosed"></div> - <div id="recentlyClosedContainer"></div> - </div> - - </div> + <div id="view-toolbar"><input type=checkbox id="thumb-checkbox" + ><input type=checkbox id="list-checkbox" + ><input type="button" id="option-button"></div> + + +<div id="option-menu" class="window-menu" tabindex="0"> + <div section="THUMB" show="true" jscontent="showthumbnails"></div> + <div section="THUMB" show="false" jscontent="hidethumbnails"></div> + <div section="LIST" show="true" jscontent="showlist"></div> + <div section="LIST" show="false" jscontent="hidelist"></div> + <div section="RECENT" show="true" jscontent="showrecent"></div> + <div section="RECENT" show="false" jscontent="hiderecent"></div> + <div section="RECOMMENDATIONS" show="true" + jscontent="showrecommendations"></div> + <div section="RECOMMENDATIONS" show="false" + jscontent="hiderecommendations"></div> </div> -<script> -logEvent('start of second script block'); - -/* Return a DOM element with tag name |elem| and attributes |attrs|. */ -function DOM(elem, attrs) { - var elem = document.createElement(elem); - for (var attr in attrs) { - elem[attr] = attrs[attr]; - } - return elem; -} - -/** - * Partially applies this function to a particular 'this object' and zero or - * more arguments. The result is a new function with some arguments of the first - * function pre-filled and the value of |this| 'pre-specified'.<br><br> - * - * Remaining arguments specified at call-time are appended to the pre- - * specified ones.<br><br> - * - * @param {Function} fn A function to partially apply. - * @param {Object} selfObj Specifies the object which |this| should point to - * when the function is run. - * @param {Object} var_args Additional arguments that are partially - * applied to the function. - * - * @return {!Function} A partially-applied form of the function bind() was - * invoked as a method of. - */ -function bind(fn, selfObj, var_args) { - var boundArgs = Array.prototype.slice.call(arguments, 2); - return function() { - var args = Array.prototype.slice.call(arguments); - args.unshift.apply(args, boundArgs); - return fn.apply(selfObj, args); - } -} + <div id="notification"> + <span> </span> + <span><span class="link" tabindex="0"></span></span> + </div> -/* Return the DOM element for a "most visited" entry. - |page| should be an object with "title", "url", and "direction" fields. */ -function makeMostVisitedDOM(page, number) { - /* The HTML we want looks like this: - <a class="disabled-on-edit" href="URL" title="gmail.com"> - <div class="thumbnail-title disabled-on-edit" - style="direction:ltr">gmail.com</div> - <img class="thumbnail disabled-on-edit" - style="background-image:url(thumbnailurl);"> - <img class="thumbnail-icon disabled-on-edit" src="chrome://faviconurl"> + <div id="most-visited" jsskip="!processing"> + <a class="thumbnail-container" style="display:none" id="thumbnail-template"> + <div class="edit-mode-border"> + <div class="edit-bar"> + <div class="pin"></div> + <div class="spacer"></div> + <div class="edit-link link"></div> + <div class="remove"></div> + </div> + <span class="thumbnail-wrapper"> + <span class="thumbnail"></span> + </span> + </div> + <div class="title"> + <div></div> + </div> </a> - */ - var root; - if (page.url) { - root = DOM('a', {className:'disabled-on-edit', - href:page.url, - title:page.title}); - root.addEventListener("mousedown", function(event) { - chrome.send("metrics", ["NTP_MostVisited" + number]) - }, false); - } else { - // Something went wrong; don't make it clickable. - root = DOM('span'); - } - - /* Create the thumbnail */ - var img_thumbnail = DOM('img', {className:'thumbnail disabled-on-edit'}); - img_thumbnail.setAttribute('onload', "logEvent('image loaded');"); - img_thumbnail.src = 'chrome://thumb/' + page.url; - - /* Create the title */ - var div_title = DOM('div', {className:'thumbnail-title disabled-on-edit'}); - /* Set the title's directionality independently of the overall page - directionality. We need to do this since a purely LTR title should always - have it's direction set as ltr. We only set the title direction to rtl if - it contains a strong RTL character. Please refer to http://crbug.com/5926 - for more information. - */ - div_title.style.direction = page.direction; - /* The following if statement is a temporary workaround for - http://crbug.com/7252 and http://crbug.com/7697. It should be removed - before closing these bugs. - */ - if (page.direction == 'rtl') { - div_title.style.textOverflow = 'clip'; - } - if (page.title) { - div_title.appendChild(document.createTextNode(page.title)); - } else { - // Make the empty title at least push down the icon. - div_title.innerHTML = ' '; - } - - // Create icon - var img_icon = DOM('img', { - className: 'thumbnail-icon disabled-on-edit', - src: 'chrome://favicon/' + page.url - }); - - root.appendChild(div_title); - root.appendChild(img_thumbnail); - root.appendChild(img_icon); - - return root; -} - -/* Return the DOM element for the cross that should be displayed in edit-mode - over a "most visited" entry. */ -function makeCrossImageDOM(url) { - var cross = DOM('div', {className:'edit-cross'}); - cross.addEventListener("mousedown", - function(event) { - if (event.which == 1) // Left click only. - blacklistURL(url); }, - false); - return cross; -} - -/* This function is called by the browser with the most visited pages list. - |pages| is a list of page objects, which have url, title, and direction - attributes. */ -function renderMostVisitedPages(pages) { - logEvent('renderMostVisitedPages called: ' + pages.length); - - // TODO(arv): Only send 8 pages - pages = pages.slice(0, 8); - - var table = $("main"); - // If we were in edit-mode, stay in that mode as this means this is a - // refresh triggered by thumbnails editing. - if (table.className.indexOf('edit-mode') != -1) - table.className = 'visible edit-mode'; - else - table.className = 'visible'; - var table = $("mostvisitedtable"); - table.innerHTML = ''; - - // Show the most visited helptext if most visited is still useless. This is - // a crappy heuristic. - if (pages.length < 4) { - $("mostvisitedintro").style.display = "block"; - return; - } - - $('mostvisitedintro').style.display = 'none'; - - // Create the items and add them to rows. - var rows = []; - var rowNum = -1; - for (var i = 0, page; page = pages[i]; ++i) { - if (i % 4 == 0) { - rowNum += 1; - rows[rowNum] = DOM('tr', {}); - } - var cell = DOM('td'); - var container = DOM('div', { className: "most-visited-container"}); - container.appendChild(makeCrossImageDOM(page.url)); - container.appendChild(makeMostVisitedDOM(page, i)); - cell.appendChild(container); - - rows[rowNum].appendChild(cell); - - logEvent('mostVisitedPage : ' + i); - } - - // Add the rows to the table. - for (var i = 0, row; row = rows[i]; i++) { - table.appendChild(row); - } - - logEvent('renderMostVisitedPages done'); -} - -function makeSearchURL(url) { - /* The HTML we want looks like this: - <form> - <input type="text" class="hint" - style="background-image:url(chrome://favicon/"+url+");" - placeholder="Search Wikipedia"> - </form> - */ - var input = DOM('input', {type:'text', - className: 'hint'}); - // There is no DOM property for placeholder. - input.setAttribute('placeholder', url.short_name); - input.keyword = url.keyword; - - if (url.favIconURL) { - input.style.backgroundImage = - 'url("chrome://favicon/iconurl/' + url.favIconURL + '")'; - } else { - input.style.backgroundImage = - 'url("chrome://favicon/http://' + url.short_name + '")'; - } - - var form = DOM('form'); - form.onsubmit = function() { - chrome.send('doSearch', [input.keyword, input.value]); - return false; - }; - form.appendChild(input); - - return form; -} - -/* This function is called by the browser when the list of search URLs is - available. |urls| is a list of objects with |name| attributes. */ -function renderSearchURLs(urls) { - logEvent('renderSearchURLs called: ' + urls.length); - var container = $('searches-entries'); - container.innerHTML = ''; // Clear out any previous contents. - if (urls.length > 0) { - $('searches').style.display = 'block'; - for (var i = 0; i < urls.length; ++i) { - container.appendChild(makeSearchURL(urls[i])); - } - } - - logEvent('renderSearchURLs done'); -} - -/** - * Renders recent history items, bookmarks, searches and tabs. - */ -function renderRecentItems() { - var recentSection = $('recent-section'); - var containerEl = $('recent-items'); - - // Filter out subsequent entries from the same domain as well as remove exact - // duplicates from earlier (based on url). - var lastDomain; - var seenUrls = {}; - recent.entries = recent.entries.filter(function(entry) { - // Mark the item as hidden if we have seen the domain before - var url = entry.url; - // Closed windows don't have an URL but we never filter them out. - if (!url) { - return true; - } - - var m = url.match(/^[^:]+:\/*[^\/]+/); - var previousDomain = lastDomain; - lastDomain = m && m[0]; - return previousDomain != lastDomain; - }); - - recent.sortEntries(); - - recentSection.style.display = recent.entries.length ? 'block' : 'none'; - - containerEl.innerHTML = ''; - - var last; - for (var i = 0; i < recent.entries.length; i++) { - var entry = recent.entries[i]; - - if (i > recent.MAX_ITEMS) { - entry.remove(); - continue; - } - - if (entry.removed) { - continue; - } - - last = entry.time; - - if (!entry.element) { - entry.createDom(); - } - - containerEl.appendChild(entry.element); - } - - recent.entries = recent.entries.filter(function(d) { - return !d.removed; - }); -} - -/** - * This function adds incoming information about tabs to the new tab UI. - */ -function renderRecentlyClosedTabs(entries) { - logEvent('renderRecentlyClosedTabs begin'); - var section = $('recentlyClosedTabs'); - var container = $('recentlyClosedContainer'); - - // recentlyClosedTabs is called on every internal event which - // affects tab history to make sure things are up to - // date. Therefore, reset the recentlyClosedTabs state on every - // call. - section.style.display = 'none'; - container.innerHTML = ''; - - if (entries.length > 0) { - section.style.display = 'block'; - - for (var i = 0; entry = entries[i]; ++i) { - var link; - - if (entry.type == "tab") { - // Closed tab. - link = createRecentBookmark('a', entry); - container.appendChild(link); - } else { - // Closed window. - var linkSpanContainer = DOM('div', {className: 'recent-window-container'}); - - var linkSpan = DOM('span'); - linkSpan.textContent = ' '; - for (var windowIndex = 0; windowIndex < entry.tabs.length; windowIndex++) { - var tab = entry.tabs[windowIndex]; - var tabImg = DOM('img', { - src:'url("chrome://favicon/' + tab.url + '")', - width:16, - height:16}); - tabImg.onmousedown = function() { return false; } - linkSpan.appendChild(tabImg); - } - - link = DOM('span', { className: 'recently-closed-window-link' } ); - windowSpan = DOM('span', {className: 'recently-close-window-text'}); - windowSpan.appendChild(document.createTextNode( - recentlyClosedWindowText(entry.tabs.length))); - link.appendChild(windowSpan); - link.appendChild(linkSpan); - linkSpanContainer.appendChild(link); - container.appendChild(linkSpanContainer); - - // The card takes care of appending itself to the DOM, so no need to - // keep a reference to it. - new RecentlyClosedHoverCard(linkSpanContainer, entry); - } - - link.onclick = function(sessionId) { - return function() { - chrome.send("metrics", ["NTP_TabRestored" + i]); - // This is a hack because chrome.send is hardcoded to only - // accept arrays of strings. - chrome.send('reopenTab', [sessionId.toString()]); - return false; - } - }(entry.sessionId); - } - } - - logEvent('renderRecentlyClosedTabs done'); -} - -/** - * Returns the text used for a recently closed window. - * - * @param numTabs number of tabs in the window - * - * @return the text to use - */ -function recentlyClosedWindowText(numTabs) { - if (numTabs == 1) - return localStrings.getString('closedwindowsingle'); - // TODO(arv): This should use localStrings.formatString but we need to update - // the grd file for that. - return localStrings.getString('closedwindowmultiple').replace('%', numTabs); -} - -/** - * Creates an item to go in the recent bookmarks or recently closed lists. - * - * @param {String} tagName Tagname for the DOM element to create. - * @param {Object} data Object with title, url, and direction to popuplate the element. - * - * @return {Node} The element containing the bookmark. - */ -function createRecentBookmark(tagName, data) { - var link = DOM(tagName, {className:'recent-bookmark', title:data.title}); - if (tagName == 'a') - link.href = data.url; - link.style.backgroundImage = 'url("chrome://favicon/' + data.url + '")'; - // Set the title's directionality independently of the page, see comment - // about setting div_title.style.direction above for details. - link.style.direction = data.direction; - // The following if statement is a temporary workaround for - // http://crbug.com/7252 and http://crbug.com/7697. It should be removed - // before closing these bugs. - if (data.direction == 'rtl') { - link.style.textOverflow = 'clip'; - } - - link.appendChild(document.createTextNode(data.title)); - return link; -} - -/** - * A hover card for windows in the recently closed list to show more details. - * - * @param {Node} target The element the hover card is for. - * @param {Object} data Object containing all the data for the card. - */ -function RecentlyClosedHoverCard(target, data) { - this.target_ = target; - this.data_ = data; - this.target_.onmouseover = bind(this.setShowTimeout_, this); - this.target_.onmouseout = bind(this.setHideTimeout_, this); -} - -/** Timeout set when closing the card. */ -RecentlyClosedHoverCard.closeTimeout_; - -/** Timeout set when opening the card. */ -RecentlyClosedHoverCard.openTimeout_; - -/** - * Clears the timer for hiding the card. - */ -RecentlyClosedHoverCard.clearHideTimeout_ = function() { - clearTimeout(RecentlyClosedHoverCard.closeTimeout_); -}; - -/** - * Clears the timer for opening the card. - */ -RecentlyClosedHoverCard.clearOpenTimeout_ = function() { - clearTimeout(RecentlyClosedHoverCard.openTimeout_); -}; - -/** - * Creates and shows the card. - */ -RecentlyClosedHoverCard.prototype.show_ = function() { - if (!this.container_) { - this.container_ = DOM('div', {className: 'recent-window-hover-container'}); - for (var i = 0; i < this.data_.tabs.length; i++) { - var tab = this.data_.tabs[i]; - var item = createRecentBookmark('span', tab); - this.container_.appendChild(item); - } - this.target_.parentNode.insertBefore(this.container_, - this.target_.nextSibling); - this.container_.onmouseover = RecentlyClosedHoverCard.clearHideTimeout_; - this.container_.onmouseout = bind(this.setHideTimeout_, this); - } - this.container_.style.display = ''; -}; - -/** - * Hides the card. - */ -RecentlyClosedHoverCard.prototype.hide_ = function() { - this.container_.style.display = 'none'; -}; - -/** - * Clears any open timers and sets the open timer. - * If the card is already showing then we only need to clear - * the hide timer. - */ -RecentlyClosedHoverCard.prototype.setShowTimeout_ = function() { - if (this.container && this.container_.style.display != 'none') { - // If we're already showing the hovercard, make sure we don't hide it again - // onmouseover. - RecentlyClosedHoverCard.clearHideTimeout_(); - return; - } - - RecentlyClosedHoverCard.clearOpenTimeout_(); - RecentlyClosedHoverCard.openTimeout_ = - setTimeout(bind(this.show_, this), 200); -}; - -/** - * Clears the open timer and sets the close one. - */ -RecentlyClosedHoverCard.prototype.setHideTimeout_ = function() { - RecentlyClosedHoverCard.clearOpenTimeout_(); - RecentlyClosedHoverCard.closeTimeout_ = - setTimeout(bind(this.hide_, this), 200); -}; - -/** - * Switches to thumbnails editing mode. - */ -function enterEditMode() { - // If the cross-image in the heading has not been added yet, do it. - // Note that we have to insert the image node explicitly because the - // heading is localized and therefore set at run-time, and we need - // the image to be static so that it can be inlined at build-time. - var crossImageDiv = $('cross-image-container'); - if (crossImageDiv && !crossImageDiv.hasChildNodes()) { - var image = $('small-cross-image'); - image.parentNode.removeChild(image); - crossImageDiv.appendChild(image); - image.style.display = 'inline'; - crossImageDiv.style.display = 'inline'; - } - $('main').className = 'visible edit-mode'; -} - -function exitEditMode() { - $('main').className = 'visible'; - blacklistedURLs = []; -} - -function cancelEdits() { - if (blacklistedURLs.length > 0) - chrome.send("removeURLsFromMostVisitedBlacklist", blacklistedURLs); - exitEditMode(); -} - -function blacklistURL(url) { - blacklistedURLs.push(url); - chrome.send("blacklistURLFromMostVisited", [url]); -} - -function restoreThumbnails() { - exitEditMode(); - chrome.send('clearMostVisitedURLsBlacklist'); -} - -function viewLog() { - var lines = []; - var start = log[0][1]; - - for (var i = 0; i < log.length; i++) { - lines.push((log[i][1] - start) + ': ' + log[i][0]); - } - - var lognode = document.createElement('pre'); - lognode.appendChild(document.createTextNode(lines.join("\n"))); - document.body.appendChild(lognode); -} + </div> -logEvent('end of second script block'); + <div id="lower-sections"> + + <div id="recent-activities" class="section"> + <h2 jscontent="recentactivities"></h2> + + <div class="hbox"> + + <div id="recent-tabs"> + <h3 jscontent="recentlyclosed"></h3> + + <div class="item-container"> + <div id="tab-items" jsskip="!processing"> + <div jsselect="$this"> + <a class="item" + jsdisplay="type == 'tab'" + jsvalues="href:url; + title:title; + .style.backgroundImage:'url(chrome://favicon/' + url + + ')'; + .style.direction:direction; + .sessionId:sessionId" + jscontent="title"></a> + <div jsdisplay="type == 'window'" + class="item link window" + jsvalues=".sessionId:sessionId" + tabindex="0"> + <span jscontent="formatTabsText($this.tabs.length)"></span> + <div class="window-menu"> + <span jsselect="tabs"> + <span class="item" + jsdisplay="type == 'tab'" + jsvalues="title:title; + .style.backgroundImage:'url(chrome://favicon/' + + url + ')'; + .style.direction:direction;" + jscontent="title"></span> + </span> + </div> + </div> + </div> + </div> + + <div> + <a href="chrome://history/" class="item nav" + jscontent="viewfullhistory"></a> + </div> + + </div> + </div> + + <div id="downloads"> + <h3 jscontent="downloads"></h3> + <div class="item-container"> + <div id="download-items" jsskip="!processing"> + <a class="item" jsselect="$this" + jsdisplay="state != 'CANCELLED'" + jsvalues="href:file_path;title:url; + .style.backgroundImage:'url(chrome://fileicon/' + + file_path + ')'; + .fileId:id" + jscontent="file_name"></a> + </div> + + <div> + <a href="chrome://downloads/" class="item nav" + jscontent="viewalldownloads"></a> + </div> + + </div> + </div> -localStrings = new LocalStrings(); + </div> -// We've got all the JS we need, render any unprocessed data. -renderFunctionsDefined = true; -processData(); + </div><div class="spacer"> -// In case renderMostVisitedItems doesn't come back quickly enough, begin -// the first-run fade-in. If it has started or if this is not a first -// run new tab, this will be a no-op. -setTimeout(function(){$('main').className = 'visible'}, - 1000); -</script> + </div><div id="recommendations" class="section"> + <h2>Even more</h2> + <p style="margin:10px">What will we put here? + </div> + </div> -<img id="small-cross-image" style="display: none; vertical-align:middle;" alt="X" - src="../../app/theme/ntp_x_icon_small.png"/> -</body> +</div> <!-- main --> -<style> -/* This CSS code is located at the end of file so it does not slow-down the page - loading, as it contains inlined images. -*/ -.edit-mode div.edit-cross { - position: absolute; - z-index: 10; - width: 81px; - height: 81px; - left: 60px; - top: 47px; - background: url('../../app/theme/ntp_x_icon.png'); -} -.edit-mode div.edit-cross:hover { - background: url('../../app/theme/ntp_x_icon_hover.png'); -} -.edit-mode div.edit-cross:active { - background: url('../../app/theme/ntp_x_icon_active.png'); -} -.recent-window-container { - background: url('../../app/theme/closed_window.png'); - background-repeat: no-repeat; -} -html[dir='rtl'] .recent-window-container { - background-position: right; - padding-right: 22px; -} -</style> +<script src="local_strings.js"></script> +<script src="new_new_tab.js"></script> </html> diff --git a/chrome/browser/resources/new_new_tab.js b/chrome/browser/resources/new_new_tab.js new file mode 100644 index 0000000..b62aefc --- /dev/null +++ b/chrome/browser/resources/new_new_tab.js @@ -0,0 +1,891 @@ + +// Helpers + +function $(id) { + return document.getElementById(id); +} + +// TODO(arv): Remove these when classList is available in HTML5. +// https://bugs.webkit.org/show_bug.cgi?id=20709 +function hasClass(el, name) { + return el.nodeType == 1 && el.className.split(/\s+/).indexOf(name) != -1; +} + +function addClass(el, name) { + el.className += ' ' + name; +} + +function removeClass(el, name) { + var names = el.className.split(/\s+/); + el.className = names.filter(function(n) { + return name != n; + }).join(' '); +} + +function findAncestorByClass(el, className) { + return findAncestor(el, function(el) { + return hasClass(el, className); + }); +} + +function findAncestor(el, predicate) { + while (el != null && !predicate(el)) { + el = el.parentNode; + } + return el; +} + +// WebKit does not have Node.prototype.swapNode +// https://bugs.webkit.org/show_bug.cgi?id=26525 +function swapDomNodes(a, b) { + var afterA = a.nextSibling; + if (afterA == b) { + swapDomNodes(b, a); + return; + } + var aParent = a.parentNode; + b.parentNode.replaceChild(a, b); + aParent.insertBefore(b, afterA); +} + +function bind(fn, selfObj, var_args) { + var boundArgs = Array.prototype.slice.call(arguments, 2); + return function() { + var args = Array.prototype.slice.call(arguments); + args.unshift.apply(args, boundArgs); + return fn.apply(selfObj, args); + } +} + +var mostVisitedData = []; +var gotMostVisited = false; +var gotShownSections = false; + +function mostVisitedPages(data) { + logEvent('received most visited pages'); + + // We append the class name with the "filler" so that we can style fillers + // differently. + var maxItems = 8; + data.length = Math.min(maxItems, data.length); + var len = data.length; + for (var i = len; i < maxItems; i++) { + data[i] = {filler: true}; + } + + mostVisitedData = data; + renderMostVisited(data); + layoutMostVisited(); + + gotMostVisited = true; + onDataLoaded(); +} + +function downloadsList(data) { + logEvent('received downloads'); + data.length = Math.min(data.length, 5); + processData('#download-items', data); +} + +function recentlyClosedTabs(data) { + logEvent('received recently closed tabs'); + data.length = Math.min(data.length, 5); + processData('#tab-items', data); +} + +function onShownSections(m) { + logEvent('received shown sections'); + setShownSections(m); + gotShownSections = true; + onDataLoaded(); +} + +function saveShownSections() { + chrome.send('setShownSections', [String(shownSections)]); +} + +function layoutMostVisited() { + var d0 = Date.now(); + var mostVisitedElement = $('most-visited'); + var thumbnails = mostVisitedElement.querySelectorAll('.thumbnail-container'); + + if (thumbnails.length < 8) { + return; + } + + var small = useSmallGrid(); + + var cols = 4; + var rows = 2; + var marginWidth = 10; + var marginHeight = 7; + var borderWidth = 4; + var thumbWidth = small ? 150 : 212; + var thumbHeight = small ? 93 : 132; + var w = thumbWidth + 2 * borderWidth + 2 * marginWidth; + var h = thumbHeight + 40 + 2 * marginHeight; + var sumWidth = cols * w - 2 * marginWidth; + var sumHeight = rows * h; + var opacity = 1; + + if (shownSections & Section.LIST) { + w = (sumWidth + 2 * marginWidth) / 2; + h = 45; + rows = 4; + cols = 2; + sumHeight = rows * h; + addClass(mostVisitedElement, 'list'); + } else if (shownSections & Section.THUMB) { + removeClass(mostVisitedElement, 'list'); + } else { + sumHeight = 0; + opacity = 0; + } + + mostVisitedElement.style.height = sumHeight + 'px'; + mostVisitedElement.style.opacity = opacity; + // We set overflow to hidden so that the most visited element does not + // "leak" when we hide and show it. + mostVisitedElement.style.overflow = 'hidden'; + + var rtl = document.documentElement.dir == 'rtl'; + + if (shownSections & Section.THUMB || shownSections & Section.LIST) { + for (var i = 0; i < thumbnails.length; i++) { + var t = thumbnails[i]; + + var row, col; + if (shownSections & Section.THUMB) { + row = Math.floor(i / cols); + col = i % cols; + } else { + col = Math.floor(i / rows); + row = i % rows; + } + + if (shownSections & Section.THUMB) { + t.style.left = (rtl ? + sumWidth - col * w - thumbWidth - 2 * borderWidth : + col * w) + 'px'; + } else { + t.style.left = (rtl ? + sumWidth - col * w - w + 2 * marginWidth : + col * w) + 'px'; + } + t.style.top = row * h + 'px'; + + if (shownSections & Section.LIST) { + t.style.width = w - 2 * marginWidth + 'px'; + } else { + t.style.width = ''; + } + } + } + + afterTransition(function() { + mostVisitedElement.style.overflow = ''; + }); + + logEvent('layoutMostVisited: ' + (Date.now() - d0)); +} + +// This global variable is used to skip parts of the DOM tree for the global +// jst processing done by the i18n. +var processing = false; + +function processData(selector, data) { + var output = document.querySelector(selector); + + // Wait until ready + if (typeof JsEvalContext !== 'function' || !output) { + logEvent('JsEvalContext is not yet available, ' + selector); + document.addEventListener('DOMContentLoaded', function() { + processData(selector, data); + }); + } else { + var d0 = Date.now(); + var input = new JsEvalContext(data); + processing = true; + jstProcess(input, output); + processing = false; + logEvent('processData: ' + selector + ', ' + (Date.now() - d0)); + } +} + +var thumbnailTemplate; + +function getThumbnailClassName(data) { + return 'thumbnail-container' + + (data.pinned ? ' pinned' : '') + + (data.filler ? ' filler' : ''); +} + +function renderMostVisited(data) { + var parent = $('most-visited'); + if (!thumbnailTemplate) { + thumbnailTemplate = $('thumbnail-template'); + thumbnailTemplate.parentNode.removeChild(thumbnailTemplate); + } + + var children = parent.children; + for (var i = 0; i < data.length; i++) { + var d = data[i]; + var reuse = !!children[i]; + var t = children[i] || thumbnailTemplate.cloneNode(true); + t.style.display = ''; + t.className = getThumbnailClassName(d); + t.title = d.title; + t.href = d.url; + t.querySelector('.edit-link').textContent = + localStrings.getString('editthumbnail'); + + // There was some concern that a malformed malicious URL could cause an XSS + // attack but setting style.backgroundImage = 'url(javascript:...)' does + // not execute the JavaScript in WebKit. + t.querySelector('.thumbnail-wrapper').style.backgroundImage = + 'url(chrome://thumb/' + d.url + ')'; + var titleDiv = t.querySelector('.title > div'); + titleDiv.textContent = d.title; + titleDiv.style.backgroundImage = 'url(chrome://favicon/' + d.url + ')'; + titleDiv.style.direction = d.direction; + if (!reuse) { + parent.appendChild(t); + } + } +} + +/** + * Calls chrome.send with a callback and restores the original afterwards. + */ +function chromeSend(name, params, callbackName, callback) { + var old = global[callbackName]; + global[callbackName] = function() { + // restore + global[callbackName] = old; + + var args = Array.prototype.slice.call(arguments); + return callback.apply(global, args); + }; + chrome.send(name, params); +} + +function useSmallGrid() { + return document.body.clientWidth <= 940; +} + +function handleWindowResize(e, opt_noUpdate) { + var body = document.body; + if (!body || body.clientWidth < 10) { + // We're probably a background tab, so don't do anything. + return; + } + + var hasSmallClass = hasClass(body, 'small'); + if (hasSmallClass && !useSmallGrid()) { + removeClass(body, 'small'); + if (!opt_noUpdate) { + layoutMostVisited(); + layoutLowerSections(); + } + } else if (!hasSmallClass && useSmallGrid()) { + addClass(body, 'small'); + if (!opt_noUpdate) { + layoutMostVisited(); + layoutLowerSections(); + } + } +} + +/** + * Bitmask for the different UI sections. + * This matches the Section enum in ../dom_ui/shown_sections_handler.h + * @enum {number} + */ +var Section = { + THUMB: 1, + LIST: 2, + RECENT: 4, + RECOMMENDATIONS: 8 +}; + +var shownSections = Section.RECENT | Section.RECOMMENDATIONS; + +function showSection(section) { + if (!(section & shownSections)) { + // THUMBS and LIST are mutually exclusive. + if (section == Section.THUMB) { + hideSection(Section.LIST); + } else if (section == Section.LIST) { + hideSection(Section.THUMB); + } + + shownSections |= section; + notifyLowerSectionForChange(section, false); + + mostVisited.updateDisplayMode(); + layoutMostVisited(); + updateOptionMenu(); + layoutLowerSections(); + } +} + +function hideSection(section) { + if (section & shownSections) { + shownSections &= ~section; + notifyLowerSectionForChange(section, true); + + mostVisited.updateDisplayMode(); + layoutMostVisited(); + updateOptionMenu(); + layoutLowerSections(); + } +} + +function notifyLowerSectionForChange(section, large) { + // Notify recent and recommendations if they need to display more data. + if (section == Section.RECENT || section == Section.RECOMMENDATIONS) { + // we are hiding one of them so if the other one is visible it is now + // {@code large}. + if (shownSections & Section.RECENT) { + recentChangedSize(large); + } else if (shownSections & Section.RECOMMENDATIONS) { + recommendationsChangedSize(large); + } + } +} + +/** + * This is called when we get the shown sections pref from the backend. + */ +function setShownSections(mask) { + if (mask != shownSections) { + shownSections = mask; + mostVisited.updateDisplayMode(); + layoutMostVisited(); + layoutLowerSections(); + updateOptionMenu(); + } +} + +var mostVisited = { + getItem: function(el) { + return findAncestorByClass(el, 'thumbnail-container'); + }, + + getHref: function(el) { + return el.href; + }, + + togglePinned: function(el) { + var index = this.getThumbnailIndex(el); + var data = mostVisitedData[index]; + if (data.pinned) { + removeClass(el, 'pinned'); + chrome.send('removePinnedURL', [data.url]); + } else { + addClass(el, 'pinned'); + chrome.send('addPinnedURL', [data.url, data.title, String(index)]); + } + data.pinned = !data.pinned; + }, + + getThumbnailIndex: function(el) { + var nodes = el.parentNode.querySelectorAll('.thumbnail-container'); + return Array.prototype.indexOf.call(nodes, el); + }, + + swapPosition: function(source, destination) { + var nodes = source.parentNode.querySelectorAll('.thumbnail-container'); + var sourceIndex = this.getThumbnailIndex(source); + var destinationIndex = this.getThumbnailIndex(destination); + swapDomNodes(source, destination); + + var sourceData = mostVisitedData[sourceIndex]; + chrome.send('addPinnedURL', [sourceData.url, sourceData.title, + String(destinationIndex)]); + sourceData.pinned = true; + addClass(source, 'pinned'); + var destinationData = mostVisitedData[destinationIndex]; + // Only update the destination if it was pinned before. + if (destinationData.pinned) { + chrome.send('addPinnedURL', [destinationData.url, destinationData.title, + String(sourceIndex)]); + } + mostVisitedData[destinationIndex] = sourceData; + mostVisitedData[sourceIndex] = destinationData; + }, + + blacklist: function(el) { + var self = this; + var url = this.getHref(el); + chrome.send('blacklistURLFromMostVisited', [url]); + + addClass(el, 'hide'); + + // Find the old item. + var oldUrls = {}; + var oldIndex = -1; + var oldItem; + for (var i = 0; i < mostVisitedData.length; i++) { + if (mostVisitedData[i].url == url) { + oldItem = mostVisitedData[i]; + oldIndex = i; + } + oldUrls[mostVisitedData[i].url] = true; + } + + // Send 'getMostVisitedPages' with a callback since we want to find the new + // page and add that in the place of the removed page. + chromeSend('getMostVisited', [], 'mostVisitedPages', function(data) { + // Find new item. + var newItem; + for (var i = 0; i < data.length; i++) { + if (!(data[i].url in oldUrls)) { + newItem = data[i]; + break; + } + } + + if (!newItem) { + newItem = {filler: true}; + } + + // Replace old item with new item in the mostVisitedData array. + if (oldIndex != -1) { + mostVisitedData.splice(oldIndex, 1, newItem); + mostVisitedPages(mostVisitedData); + addClass(el, 'fade-in'); + } + + var text = localStrings.formatString('thumbnailremovednotification', + oldItem.title); + var actionText = localStrings.getString('undothumbnailremove'); + + // Show notification and add undo callback function. + var wasPinned = oldItem.pinned; + showNotification(text, actionText, function() { + self.removeFromBlackList(url); + if (wasPinned) { + chromeSend('addPinnedURL', [url, oldItem.title, String(oldIndex)]); + } + chrome.send('getMostVisited'); + }); + }); + }, + + removeFromBlackList: function(url) { + chrome.send('removeURLsFromMostVisitedBlacklist', [url]); + }, + + clearAllBlacklisted: function() { + chrome.send('clearMostVisitedURLsBlacklist', []); + }, + + updateDisplayMode: function() { + var thumbCheckbox = $('thumb-checkbox'); + var listCheckbox = $('list-checkbox'); + var mostVisitedElement = $('most-visited'); + + if (shownSections & Section.THUMB) { + thumbCheckbox.checked = true; + listCheckbox.checked = false; + removeClass(mostVisitedElement, 'list'); + } else if (shownSections & Section.LIST) { + thumbCheckbox.checked = false; + listCheckbox.checked = true; + addClass(mostVisitedElement, 'list'); + } else { + thumbCheckbox.checked = false; + listCheckbox.checked = false; + } + } +}; + +function recentChangedSize(large) { + // TODO(arv): Implement +} + +function recommendationsChangedSize(large) { + // TODO(arv): Implement +} + +// Recent activities + +function layoutLowerSections() { + // This lower sections are inline blocks so all we need to do is to set the + // width and opacity. + var lowerSectionsElement = $('lower-sections'); + var recentElement = $('recent-activities'); + var recommendationsElement = $('recommendations'); + var spacer = recentElement.nextElementSibling; + + var totalWidth = useSmallGrid() ? 692 : 940; + var spacing = 20; + var rtl = document.documentElement.dir == 'rtl'; + + var recentShown = shownSections & Section.RECENT; + var recommendationsShown = shownSections & Section.RECOMMENDATIONS; + + if (recentShown || recommendationsShown) { + lowerSectionsElement.style.height = '175px' + lowerSectionsElement.style.opacity = ''; + } else { + lowerSectionsElement.style.height = lowerSectionsElement.style.opacity = 0; + } + + if (recentShown && recommendationsShown) { + var w = (totalWidth - spacing) / 2; + recentElement.style.width = recommendationsElement.style.width = w + 'px' + recentElement.style.opacity = recommendationsElement.style.opacity = ''; + spacer.style.width = spacing + 'px'; + } else if (recentShown) { + recentElement.style.width = totalWidth + 'px'; + recentElement.style.opacity = ''; + recommendationsElement.style.width = + recommendationsElement.style.opacity = 0; + spacer.style.width = 0; + } else if (recommendationsShown) { + recommendationsElement.style.width = totalWidth + 'px'; + recommendationsElement.style.opacity = ''; + recentElement.style.width = recentElement.style.opacity = 0; + spacer.style.width = 0; + } +} + +/** + * 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 localStrings.getString('closedwindowsingle'); + // TODO(arv): Update grd file to use %s so we can use formatString + // http://crbug.com/14878 + // return localStrings.formatString('closedwindowmultiple', numTabs); + return localStrings.getString('closedwindowmultiple').replace(/%/, numTabs); +} + +/** + * We need both most visited and the shown sections to be considered loaded. + * @return {boolean} + */ +function onDataLoaded() { + if (gotMostVisited && gotShownSections) { + // Remove class name in a timeout so that changes done in this JS thread are + // not animated. + window.setTimeout(function() { + removeClass(document.body, 'loading'); + }, 10); + } +} + +// Theme related + +function themeChanged() { + $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); + updateAttribution(); +} + +function updateAttribution() { + // TODO(arv): Implement + //$('attribution-img').src = 'chrome://theme/theme_ntp_attribution?' + + // Date.now(); +} + +function bookmarkBarAttached() { + document.documentElement.setAttribute("bookmarkbarattached", "true"); +} + +function bookmarkBarDetached() { + document.documentElement.setAttribute("bookmarkbarattached", "false"); +} + +function viewLog() { + var lines = []; + var start = log[0][1]; + + for (var i = 0; i < log.length; i++) { + lines.push((log[i][1] - start) + ': ' + log[i][0]); + } + + console.log(lines.join('\n')); +} + +// Updates the visibility of the menu items. +function updateOptionMenu() { + var menuItems = $('option-menu').children; + for (var i = 0; i < menuItems.length; i++) { + var item = menuItems[i]; + var section = Section[item.getAttribute('section')]; + var show = item.getAttribute('show') == 'true'; + // Hide show items if already shown. Hide hide items if already hidden. + var hideMenuItem = show == !!(shownSections & section); + item.style.display = hideMenuItem ? 'none' : ''; + } +} + +// We apply the size class here so that we don't trigger layout animations +// onload. + +handleWindowResize(null, true); + +var localStrings = new LocalStrings(); + +/////////////////////////////////////////////////////////////////////////////// +// Things we know are not needed at startup go below here + +// Notification + +function afterTransition(f) { + // The duration of all transitions are 500ms + window.setTimeout(f, 500); +} + +function showNotification(text, actionText, f) { + var notificationElement = $('notification'); + var actionLink = notificationElement.querySelector('.link'); + notificationElement.firstElementChild.textContent = text; + + actionLink.textContent = actionText; + actionLink.onclick = function() { + f(); + removeClass(notificationElement, 'show'); + // Since we have a :hover rule to not hide the notification banner when the + // mouse is over we need force it to hide. We remove the hide class after + // a short timeout to allow the banner to be shown again. + addClass(notificationElement, 'hide'); + afterTransition(function() { + removeClass(notificationElement, 'hide'); + }) + }; + addClass(notificationElement, 'show'); + window.setTimeout(function() { + removeClass(notificationElement, 'show'); + }, 10000); +} + +// Options menu +// TODO(arv): Keyboard navigation of the menu items. + +function showMenu(button, menu) { + function hide() { + menu.style.display = ''; + menu.removeEventListener('blur', hide); + window.removeEventListener('blur', hide); + }; + menu.addEventListener('blur', hide); + window.addEventListener('blur', hide); + menu.style.display = 'block'; + menu.focus(); +} + +$('option-button').addEventListener('click', function(e) { + showMenu(this, $('option-menu')); +}); + +$('option-menu').addEventListener('click', function(e) { + var section = Section[e.target.getAttribute('section')]; + var show = e.target.getAttribute('show') == 'true'; + if (show) { + showSection(section); + } else { + hideSection(section); + } + + // Hide menu now. + this.style.display = 'none'; + + layoutLowerSections(); + mostVisited.updateDisplayMode(); + layoutMostVisited(); + + saveShownSections(); +}); + +$('most-visited').addEventListener('click', function(e) { + var target = e.target; + if (hasClass(target, 'pin')) { + mostVisited.togglePinned(mostVisited.getItem(target)); + e.preventDefault(); + } else if (hasClass(target, 'remove')) { + mostVisited.blacklist(mostVisited.getItem(target)); + e.preventDefault(); + } else if (hasClass(target, 'edit-link')) { + alert('Not implemented yet') + e.preventDefault(); + } +}); + +$('downloads').addEventListener('click', function(e) { + var el = findAncestor(e.target, function(el) { + return el.fileId !== undefined; + }); + if (el && el.fileId) { + chrome.send('openFile', [String(el.fileId)]); + e.preventDefault(); + } +}); + +$('recent-tabs').addEventListener('click', function(e) { + var el = findAncestor(e.target, function(el) { + return el.sessionId !== undefined; + }); + if (el && el.sessionId !== undefined) { + chrome.send('reopenTab', [String(el.sessionId)]); + e.preventDefault(); + } +}); + +$('thumb-checkbox').addEventListener('change', function(e) { + if (e.target.checked) { + showSection(Section.THUMB); + } else { + hideSection(Section.THUMB); + } + mostVisited.updateDisplayMode(); + layoutMostVisited(); + saveShownSections(); +}); + +$('list-checkbox').addEventListener('change', function(e) { + var newValue = shownSections; + if (e.target.checked) { + showSection(Section.LIST); + } else { + hideSection(Section.LIST); + } + mostVisited.updateDisplayMode(); + layoutMostVisited(); + saveShownSections(); +}); + +window.addEventListener('load', bind(logEvent, global, 'onload fired')); +window.addEventListener('load', onDataLoaded); +window.addEventListener('resize', handleWindowResize); +document.addEventListener('DOMContentLoaded', bind(logEvent, global, + 'domcontentloaded fired')); + +// DnD + +var dnd = { + currentOverItem: null, + dragItem: null, + startX: 0, + startY: 0, + startScreenX: 0, + startScreenY: 0, + dragEndTimer: null, + + handleDragStart: function(e) { + var thumbnail = mostVisited.getItem(e.target); + if (thumbnail) { + e.dataTransfer.setData('text/uri-list', mostVisited.getHref(thumbnail)); + this.dragItem = thumbnail; + addClass(this.dragItem, 'dragging'); + this.dragItem.style.zIndex = 2; + } + }, + + handleDragEnter: function(e) { + this.currentOverItem = mostVisited.getItem(e.target); + if (this.canDropOnElement(this.currentOverItem)) { + e.preventDefault(); + } + }, + + handleDragOver: function(e) { + var item = mostVisited.getItem(e.target); + if (this.canDropOnElement(item)) { + e.preventDefault(); + } + }, + + handleDragLeave: function(e) { + var item = mostVisited.getItem(e.target); + if (item) { + e.preventDefault(); + } + + this.currentOverItem = null; + }, + + handleDrop: function(e) { + var dropTarget = mostVisited.getItem(e.target); + if (this.canDropOnElement(dropTarget)) { + dropTarget.style.zIndex = 1; + mostVisited.swapPosition(this.dragItem, dropTarget); + layoutMostVisited(); + e.preventDefault(); + if (this.dragEndTimer) { + window.clearTimeout(this.dragEndTimer); + this.dragEndTimer = null; + } + afterTransition(function() { + dropTarget.style.zIndex = ''; + }); + } + }, + + handleDragEnd: function(e) { + // WebKit fires dragend before drop. + var dragItem = this.dragItem; + if (dragItem) { + dragItem.style.pointerEvents = ''; + removeClass(dragItem, 'dragging'); + + afterTransition(function() { + // Delay resetting zIndex to let the animation finish. + dragItem.style.zIndex = ''; + // Same for overflow. + dragItem.parentNode.style.overflow = ''; + }); + var self = this; + this.dragEndTimer = window.setTimeout(function() { + // These things needto happen after the drop event. + layoutMostVisited(); + self.dragItem = null; + }, 10); + + } + }, + + handleDrag: function(e) { + var item = mostVisited.getItem(e.target); + var rect = document.querySelector('#most-visited').getBoundingClientRect(); + item.style.pointerEvents = 'none'; + + item.style.left = this.startX + e.screenX - this.startScreenX + 'px'; + item.style.top = this.startY + e.screenY - this.startScreenY + 'px'; + }, + + // We listen to mousedown to get the relative position of the cursor for dnd. + handleMouseDown: function(e) { + var item = mostVisited.getItem(e.target); + if (item) { + this.startX = item.offsetLeft; + this.startY = item.offsetTop; + this.startScreenX = e.screenX; + this.startScreenY = e.screenY; + } + }, + + canDropOnElement: function(el) { + return this.dragItem && el && hasClass(el, 'thumbnail-container') && + !hasClass(el, 'filler'); + }, + + init: function() { + var el = $('most-visited'); + el.addEventListener('dragstart', bind(this.handleDragStart, this)); + el.addEventListener('dragenter', bind(this.handleDragEnter, this)); + el.addEventListener('dragover', bind(this.handleDragOver, this)); + el.addEventListener('dragleave', bind(this.handleDragLeave, this)); + el.addEventListener('drop', bind(this.handleDrop, this)); + el.addEventListener('dragend', bind(this.handleDragEnd, this)); + el.addEventListener('drag', bind(this.handleDrag, this)); + el.addEventListener('mousedown', bind(this.handleMouseDown, this)); + } +}; + +dnd.init(); |