diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-30 00:41:37 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-30 00:41:37 +0000 |
commit | a02afd36072896ba584f32664cc8e4bc1889bae0 (patch) | |
tree | d202bf35af23f91f629df2680d3e2af3218f9b8d /chrome/browser | |
parent | e030e767b4f0fef8ef1de21ebe977c2a4dec840b (diff) | |
download | chromium_src-a02afd36072896ba584f32664cc8e4bc1889bae0.zip chromium_src-a02afd36072896ba584f32664cc8e4bc1889bae0.tar.gz chromium_src-a02afd36072896ba584f32664cc8e4bc1889bae0.tar.bz2 |
NTP - Refactor the most visited code to uncouple it from the rest of the NTP.
The goal of this refactoring is to allow splitting the different parts of the NTP into different reusable components.
BUG=None
TEST=Manually
Review URL: http://codereview.chromium.org/1695022
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@46021 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/dom_ui/new_tab_ui_uitest.cc | 2 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.css | 333 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.html | 166 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.js | 211 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/most_visited.css | 259 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/most_visited.js | 987 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/util.js | 86 | ||||
-rw-r--r-- | chrome/browser/resources/shared/js/class_list.js | 94 | ||||
-rw-r--r-- | chrome/browser/resources/shared/js/class_list_test.html | 91 |
9 files changed, 1181 insertions, 1048 deletions
diff --git a/chrome/browser/dom_ui/new_tab_ui_uitest.cc b/chrome/browser/dom_ui/new_tab_ui_uitest.cc index d130b4e..2014942 100644 --- a/chrome/browser/dom_ui/new_tab_ui_uitest.cc +++ b/chrome/browser/dom_ui/new_tab_ui_uitest.cc @@ -147,7 +147,7 @@ TEST_F(NewTabUITest, HomePageLink) { L"window.domAutomationController.send(" L"(function() {" L" var el = document.querySelector('#notification');" - L" return hasClass(el, 'show');" + L" return el.classList.contains('show');" L"})()" L")", &has_class)); diff --git a/chrome/browser/resources/new_new_tab.css b/chrome/browser/resources/new_new_tab.css index 16d4839..56da1fd 100644 --- a/chrome/browser/resources/new_new_tab.css +++ b/chrome/browser/resources/new_new_tab.css @@ -44,159 +44,6 @@ html[anim=false] *, -webkit-animation: none !important; } -/* Most Visited */ - -#most-visited { - position: relative; - padding: 0; - height: 366px; - margin-top: -10px; - -webkit-user-select: none; - -webkit-transition: height .15s, opacity .15s; -} - -.thumbnail-container { - position: absolute; - color: black; - text-decoration: none; - -webkit-transition: left .15s, right .15s, top .15s; -} - -.list > .thumbnail-container { - overflow: hidden; -} - -/* hide outline in thumbnail view */ -:not(.list) > .thumbnail-container:focus { - outline: none; -} - -.thumbnail, -.thumbnail-container > .title { - width: 207px; /* natural size is 196 */ - height: 129px; /* 136 */ - -webkit-transition: width .15s, height .15s; -} - -.thumbnail-wrapper { - display: block; - background-size: 212px 132px; - background: no-repeat 4px 4px; - background-color: white; - border-radius: 5px; - -webkit-transition: background-size .15s; - position: relative; -} - -.filler * { - visibility: hidden; -} - -.filler { - pointer-events: none; -} - -.filler .thumbnail-wrapper { - visibility: visible; - border: 3px solid hsl(213, 60%, 92%); -} - -.list > .filler * { - visibility: hidden !important; -} - -.filler .thumbnail { - visibility: inherit; - border: 1px solid white; - padding: 0; - background-color: hsl(213, 60%, 92%); -} - -.edit-bar { - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: stretch; - padding: 3px; - padding-bottom: 0; - height: 17px; /* 23 - 2 * 3 */ - cursor: move; - font-size: 100%; - line-height: 17px; - background: hsl(213, 54%, 95%); - border-top-left-radius: 4px; - border-top-right-radius: 4px; - position: relative; - margin-top: 21px; - margin-bottom: -21px; - -webkit-transition: margin .15s, background .15s; -} - -.edit-bar > * { - display: block; - position: relative; -} - -.thumbnail-container:focus .edit-bar, -.thumbnail-container:hover .edit-bar { - margin-top: 0; - margin-bottom: 0; - -webkit-transition-delay: .5s, 0s; - - /* We need background-color as well to get the fade out animation correct */ - background-color: hsl(213, 66%, 57%); - background-image: -webkit-gradient(linear, left top, left bottom, - from(hsl(213, 87%, 67%)), - to(hsl(213, 66%, 57%))); -} - -.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 { @@ -213,117 +60,12 @@ html[anim=false] *, text-decoration: none; } -.thumbnail-container { - color: hsl(213, 90%, 24%); - text-decoration: none; -} - -.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 .15s, width .15s; - color: black; -} - -.thumbnail-container > .title > div { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - background: no-repeat 0 50%; - 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 hsl(213, 63%, 93%); - padding: 1px; - border-radius: 5px; - display: block; - -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, 0); - -webkit-transition: width .15s, height .15s, border-color .15s, - border-radius .15s, -webkit-box-shadow .15s; -} - -.edit-mode-border { - border-radius: 4px; - - /* when dragged over we move this */ - position: relative; - -webkit-transition: top .15s, left .15s; -} - -.thumbnail-container:focus .thumbnail, -.thumbnail-container:hover .thumbnail { - border-color: hsl(213, 66%, 57%); - -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)) - ); - - /* delay border radius transition as much as the edit bar slide delay */ - -webkit-transition-delay: 0, 0, 0, .5s, 0; -} - -.thumbnail-container:focus > .edit-mode-border, -.thumbnail-container:hover > .edit-mode-border { - background-color: hsl(213, 66%, 57%); - -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, .5); -} - -.dragging, -.dragging * { - -webkit-transition: none !important; -} - -.dragging > .title { - opacity: 0; -} - -.list > .dragging > .title { - opacity: 1; -} - .hide { opacity: 0 !important; visibility: hidden !important; pointer-events: none; } -@-webkit-keyframes 'fade-in' { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -.fade-in { - -webkit-animation: 'fade-in' .15s; -} - /* Notification */ #notification { @@ -384,35 +126,6 @@ html[dir=rtl] .thumbnail-container > .title > div { white-space: nowrap; } -/* List mode */ - -.list .thumbnail, -.list .edit-bar { - display: none; -} - -.list > .thumbnail-container { - -webkit-box-sizing: border-box; -} - -.list > .thumbnail-container > .title { - font-size: 120%; - line-height: 34px; - height: 34px; - color: hsl(213, 27%, 68%); - width: 100%; -} - -.list > .thumbnail-container { - color: hsl(213, 27%, 68%); - text-decoration: underline; -} - -.list > .thumbnail-container > .title > div { - text-decoration: none; - color: rgb(6, 45, 117); -} - .item { background: no-repeat 0% 50%; padding: 2px; @@ -651,14 +364,6 @@ html[dir=rtl] #option-menu > [command=hide]:before { background-image: url(chrome://theme/newtab_checkbox_white); } -#most-visited.list { - height: 294px; -} - -.list > .thumbnail-container { - max-width: 920px; -} - #attribution { margin: 10px 0; } @@ -758,11 +463,11 @@ html[dir=rtl] #option-menu > [command=hide]:before { } #apps-launch-control { - margin-top: 0.5em; + margin-top: 10px; } #apps-launch-control input { - position:relative; + position: relative; top: 1px; margin-right: 0.2em; margin-left: 1em; @@ -790,12 +495,16 @@ html[dir=rtl] #option-menu > [command=hide]:before { width: 105px; /* 920 / 8 - margin * 2 */ } -#debug h2 { +#debug > h2 { color: red; } +#debug > div { + margin: 0; +} + .section.disabled { - display: none!important; + display: none !important; } .section + :not(.hidden) { @@ -860,10 +569,6 @@ html[dir=rtl] #option-menu > [command=hide]:before { font-size: 13px; } -#recently-closed { - border-bottom: 0; -} - /* small */ @media (max-width: 920px) { @@ -872,28 +577,6 @@ html[dir=rtl] #option-menu > [command=hide]:before { width: 692px; } - #most-visited { - height: 294px; - } - - .thumbnail, - .thumbnail-container > .title { - width: 150px; - height: 93px; - } - - .thumbnail-container > .title { - height: auto; - } - - .thumbnail-wrapper { - background-size: 150px 93px; - } - - .list > .thumbnail-container { - max-width: 692px; - } - #notification > * { max-width: 300px; } diff --git a/chrome/browser/resources/new_new_tab.html b/chrome/browser/resources/new_new_tab.html index cfebba8..e7e4d3e 100644 --- a/chrome/browser/resources/new_new_tab.html +++ b/chrome/browser/resources/new_new_tab.html @@ -47,7 +47,6 @@ chrome.send('getRecentlyClosedTabs'); chrome.send('getTips'); chrome.send('getApps'); -registerCallback('onShownSections'); registerCallback('mostVisitedPages'); registerCallback('recentlyClosedTabs'); registerCallback('syncMessageChanged'); @@ -58,6 +57,7 @@ registerCallback('getAppsCallback'); </script> <!-- template data placeholder --> <link rel="stylesheet" href="new_new_tab.css"> +<link rel="stylesheet" href="ntp/most_visited.css"> <script> /** @@ -76,14 +76,10 @@ var Section = { var shownSections = templateData['shown_sections']; -function $(id) { - return document.getElementById(id); -} - // Until themes can clear the cache, force-reload the theme stylesheet. document.write('<link id="themecss" rel="stylesheet" ' + 'href="chrome://theme/css/newtab.css?' + - (new Date()).getTime() + '">'); + Date.now() + '">'); function useSmallGrid() { return window.innerWidth <= 920; @@ -93,83 +89,11 @@ function isRtl() { return templateData['textdirection'] == 'rtl'; } -// Keep track of the last small state so we can ensure that we are using the -// right mode. This is a workaround for http://crbug.com/25329 -var wasSmallGrid; - -function getMostVisitedLayoutRects() { - var small = useSmallGrid(); - wasSmallGrid = small; - - var cols = 4; - var rows = 2; - var marginWidth = 10; - var marginHeight = 7; - var borderWidth = 4; - var thumbWidth = small ? 150 : 207; - var thumbHeight = small ? 93 : 129; - var w = thumbWidth + 2 * borderWidth + 2 * marginWidth; - var h = thumbHeight + 40 + 2 * marginHeight; - var sumWidth = cols * w - 2 * marginWidth; - - var rtl = isRtl(); - var rects = []; - - if (shownSections & Section.THUMB) { - for (var i = 0; i < rows * cols; i++) { - var row = Math.floor(i / cols); - var col = i % cols; - var left = rtl ? sumWidth - col * w - thumbWidth - 2 * borderWidth : - col * w; - - var top = row * h; - - rects[i] = {left: left, top: top}; - } - } - return rects; -} - -function applyMostVisitedRects() { - if (shownSections & Section.THUMB) { - var rects = getMostVisitedLayoutRects(); - var children = $('most-visited').children; - for (var i = 0; i < 8; i++) { - var t = children[i]; - t.style.left = rects[i].left + 'px'; - t.style.top = rects[i].top + 'px'; - t.style.right = ''; - var innerStyle = t.firstElementChild.style; - innerStyle.left = innerStyle.top = ''; - } - } -} - -// 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) { - var names = el.className.split(/\s+/); - if (names.indexOf(name) == -1) { - el.className += ' ' + name; - } -} - -function removeClass(el, name) { - var names = el.className.split(/\s+/); - el.className = names.filter(function(n) { - return name != n; - }).join(' '); -} - +// This will get overridden in new_new_tab.js function updateSimpleSection(id, section) { - if (shownSections & section) - removeClass($(id), 'hidden'); - else - addClass($(id), 'hidden'); + // All sections start off shown. + if (!(shownSections & section)) + document.getElementById(id).className += ' hidden'; } </script> @@ -201,52 +125,37 @@ function updateSimpleSection(id, section) { <span class="link"><span class="link-color"></span></span> </div> - <div class="section disabled" id="apps-section"></div> - - <div id="most-visited-section" class="section" section="THUMB"> - <h2 i18n-content="mostvisited"></h2> + <div class="sections"> + <div class="section disabled" id="apps-section"></div> - <div id="most-visited"> - <a class="thumbnail-container filler" tabindex="1" id="t0"> - <div class="edit-mode-border"> - <div class="edit-bar"> - <div class="pin"></div> - <div class="spacer"></div> - <div class="remove"></div> - </div> - <span class="thumbnail-wrapper"> - <span class="thumbnail"></span> - </span> - </div> - <div class="title"> - <div></div> - </div> - </a> + <div id="most-visited-section" class="section" section="THUMB"> + <h2 i18n-content="mostvisited"></h2> + <div id="most-visited"></div> </div> - </div> - <div id="recently-closed" class="section" section="RECENT"> - <h2 i18n-content="recentlyclosed"></h2> - <span> - <span class="nav"> - <a href="chrome://history/" class="item" - i18n-content="viewfullhistory"></a> + <div id="recently-closed" class="section" section="RECENT"> + <h2 i18n-content="recentlyclosed"></h2> + <span> + <span class="nav"> + <a href="chrome://history/" class="item" + i18n-content="viewfullhistory"></a> + </span> </span> - </span> - </div> + </div> - <div id="debug" class="section disabled" section="DEBUG"> - <h2>Debug</h2> - <div id="apps-launch-control"> - Open apps in:<label - ><input type="radio" name="launch-container-type" value="" - checked="true">Default</label - ><label><input type="radio" name="launch-container-type" value="tab" - >Tab</label - ><label><input type="radio" name="launch-container-type" value="window" - >Window</label - ><label><input type="radio" name="launch-container-type" value="panel" - >Panel</label> + <div id="debug" class="section disabled" section="DEBUG"> + <h2>Debug</h2> + <div id="apps-launch-control"> + Open apps in:<label + ><input type="radio" name="launch-container-type" value="" + checked="true">Default</label + ><label><input type="radio" name="launch-container-type" value="tab" + >Tab</label + ><label><input type="radio" name="launch-container-type" value="window" + >Window</label + ><label><input type="radio" name="launch-container-type" value="panel" + >Panel</label> + </div> </div> </div> @@ -255,12 +164,6 @@ function updateSimpleSection(id, section) { updateSimpleSection('recently-closed', Section.RECENT); updateSimpleSection('most-visited-section', Section.THUMB); updateSimpleSection('debug', Section.DEBUG); - var el = $('most-visited'); - for (var i = 1; i < 8; i++) { - el.appendChild(el.firstElementChild.cloneNode(true)).id = 't' + i; - } - - applyMostVisitedRects(); })(); </script> @@ -294,9 +197,12 @@ function updateSimpleSection(id, section) { <div class="window-menu" id="window-tooltip"></div> </body> + <script src="shared/js/i18n_template.js"></script> <script src="shared/js/local_strings.js"></script> +<script src="shared/js/class_list.js"></script> <script src="shared/js/parse_html_subset.js"></script> -<script src="new_new_tab.js"></script> +<script src="ntp/util.js"></script> <script src="ntp/most_visited.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 index 64c7e1a..3af8a33 100644 --- a/chrome/browser/resources/new_new_tab.js +++ b/chrome/browser/resources/new_new_tab.js @@ -2,56 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Helpers - -function findAncestorByClass(el, className) { - return findAncestor(el, function(el) { - return hasClass(el, className); - }); -} - -/** - * Return the first ancestor for which the {@code predicate} returns true. - * @param {Node} node The node to check. - * @param {function(Node) : boolean} predicate The function that tests the - * nodes. - * @return {Node} The found ancestor or null if not found. - */ -function findAncestor(node, predicate) { - var last = false; - while (node != null && !(last = predicate(node))) { - node = node.parentNode; - } - return last ? node : null; -} - -// 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); -} +var loading = true; -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); - } +function updateSimpleSection(id, section) { + if (shownSections & section) + $(id).classList.remove('hidden'); + else + $(id).classList.add('hidden'); } -const IS_MAC = /$Mac/.test(navigator.platform); - -var loading = true; - function getAppsCallback(data) { + logEvent('recieved apps'); var appsSection = $('apps-section'); var debugSection = $('debug'); appsSection.innerHTML = ''; @@ -63,11 +24,11 @@ function getAppsCallback(data) { // TODO(aa): Figure out what to do with the debug mode when we turn apps on // for everyone. if (data.length) { - removeClass(appsSection, 'disabled'); - removeClass(debugSection, 'disabled'); + appsSection.classList.remove('disabled'); + debugSection.classList.remove('disabled'); } else { - addClass(appsSection, 'disabled'); - addClass(debugSection, 'disabled'); + appsSection.classList.add('disabled'); + debugSection.classList.add('disabled'); } } @@ -196,55 +157,10 @@ function createRecentItem(data) { return wrapperEl; } -function onShownSections(mask) { - logEvent('received shown sections'); - if (mask != shownSections) { - var oldShownSections = shownSections; - shownSections = mask; - - // Only invalidate most visited if needed. - if ((mask & Section.THUMB) != (oldShownSections & Section.THUMB)) { - mostVisited.invalidate(); - } - - mostVisited.updateDisplayMode(); - renderRecentlyClosed(); - } -} - function saveShownSections() { chrome.send('setShownSections', [String(shownSections)]); } -function url(s) { - // http://www.w3.org/TR/css3-values/#uris - // Parentheses, commas, whitespace characters, single quotes (') and double - // quotes (") appearing in a URI must be escaped with a backslash - var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); - // WebKit has a bug when it comes to URLs that end with \ - // https://bugs.webkit.org/show_bug.cgi?id=28885 - if (/\\\\$/.test(s2)) { - // Add a space to work around the WebKit bug. - s2 += ' '; - } - return 'url("' + s2 + '")'; -} - -/** - * 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); -} - var LayoutMode = { SMALL: 1, NORMAL: 2 @@ -259,35 +175,45 @@ function handleWindowResize() { } var oldLayoutMode = layoutMode; - layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL + var b = useSmallGrid(); + layoutMode = b ? LayoutMode.SMALL : LayoutMode.NORMAL if (layoutMode != oldLayoutMode){ - mostVisited.invalidate(); + mostVisited.useSmallGrid = b; mostVisited.layout(); renderRecentlyClosed(); } } +window.addEventListener('resize', handleWindowResize); + +var sectionToElementMap; +function getSectionElement(section) { + if (!sectionToElementMap) { + sectionToElementMap = {}; + for (var key in Section) { + sectionToElementMap[Section[key]] = + document.querySelector('.section[section=' + key + ']'); + } + } + return sectionToElementMap[section]; +} + function showSection(section) { if (!(section & shownSections)) { shownSections |= section; switch (section) { case Section.THUMB: - mostVisited.invalidate(); - mostVisited.updateDisplayMode(); + mostVisited.visible = true; mostVisited.layout(); break; case Section.RECENT: renderRecentlyClosed(); break; - case Section.TIPS: - removeClass($('tip-line'), 'hidden'); - break; - case Section.DEBUG: - removeClass($('debug'), 'hidden'); - break; } + + getSectionElement(section).classList.remove('hidden'); } } @@ -297,20 +223,15 @@ function hideSection(section) { switch (section) { case Section.THUMB: - mostVisited.invalidate(); - mostVisited.updateDisplayMode(); + mostVisited.visible = false; mostVisited.layout(); break; case Section.RECENT: renderRecentlyClosed(); break; - case Section.TIPS: - addClass($('tip-line'), 'hidden'); - break; - case Section.DEBUG: - addClass($('debug'), 'hidden'); - break; } + + getSectionElement(section).classList.add('hidden'); } } @@ -318,7 +239,6 @@ function hideSection(section) { function layoutRecentlyClosed() { var recentShown = shownSections & Section.RECENT; - updateSimpleSection('recently-closed', Section.RECENT); if (recentShown) { var recentElement = $('recently-closed'); @@ -441,23 +361,6 @@ function formatTabsText(numTabs) { return localStrings.getStringF('closedwindowmultiple', numTabs); } -/** - * We need both most visited and the shown sections to be considered loaded. - * @return {boolean} - */ -function onDataLoaded() { - if (gotMostVisited) { - mostVisited.layout(); - loading = false; - // Remove class name in a timeout so that changes done in this JS thread are - // not animated. - window.setTimeout(function() { - ensureSmallGridCorrect(); - removeClass(document.body, 'loading'); - }, 1); - } -} - // Theme related function themeChanged() { @@ -535,8 +438,8 @@ function showNotification(text, actionText, opt_f, opt_delay) { function show() { window.clearTimeout(notificationTimeout); - addClass(notificationElement, 'show'); - addClass(document.body, 'notification-shown'); + notificationElement.classList.add('show'); + document.body.classList.add('notification-shown'); } function delayedHide() { @@ -549,7 +452,7 @@ function showNotification(text, actionText, opt_f, opt_delay) { } // Remove any possible first-run trails. - removeClass(notification, 'first-run'); + notification.classList.remove('first-run'); var actionLink = notificationElement.querySelector('.link-color'); notificationElement.firstElementChild.textContent = text; @@ -573,8 +476,8 @@ function showNotification(text, actionText, opt_f, opt_delay) { */ function hideNotification() { var notificationElement = $('notification'); - removeClass(notificationElement, 'show'); - removeClass(document.body, 'notification-shown'); + notificationElement.classList.remove('show'); + document.body.classList.remove('notification-shown'); var actionLink = notificationElement.querySelector('.link-color'); // Prevent tabbing to the hidden link. actionLink.tabIndex = -1; @@ -590,7 +493,7 @@ function showFirstRunNotification() { localStrings.getString('closefirstrunnotification'), null, 30000); var notificationElement = $('notification'); - addClass(notification, 'first-run'); + notification.classList.add('first-run'); } /** @@ -616,7 +519,7 @@ OptionMenu.prototype = { updateOptionMenu(); this.positionMenu_(); this.menu.style.display = 'block'; - addClass(this.button, 'open'); + this.button.classList.add('open'); this.button.focus(); // Listen to document and window events so that we hide the menu when the @@ -631,7 +534,7 @@ OptionMenu.prototype = { hide: function() { this.menu.style.display = 'none'; - removeClass(this.button, 'open'); + this.button.classList.remove('open'); this.setSelectedIndex(-1); document.removeEventListener('focus', this.boundMaybeHide_, true); @@ -967,7 +870,6 @@ var windowTooltip = new WindowTooltip($('window-tooltip')); window.addEventListener('load', bind(logEvent, global, 'Tab.NewTabOnload', true)); -window.addEventListener('load', onDataLoaded); window.addEventListener('resize', handleWindowResize); document.addEventListener('DOMContentLoaded', @@ -1021,7 +923,7 @@ window.addEventListener('keydown', function(e) { document.addEventListener('mouseover', function(e) { // We don't want to do this while we are dragging because it makes things very // janky - if (dnd.dragItem) { + if (mostVisited.isDragging()) { return; } @@ -1063,7 +965,7 @@ updateAttribution(); // Closes the promo line when close button is clicked. $('promo-close').onclick = function (e) { - addClass($('promo-line'), 'hidden'); + $('promo-line').classList.add('hidden'); chrome.send('stopPromoLineMessage'); e.preventDefault(); }; @@ -1076,3 +978,28 @@ function setUpPromoMessage() { syncButton.onclick = syncSectionLinkClicked; fixLinkUnderlines($('promo-message')); } + +var mostVisited = new MostVisited($('most-visited'), + useSmallGrid(), + shownSections & Section.THUMB); + +function mostVisitedPages(data, firstRun) { + logEvent('received most visited pages'); + + mostVisited.data = data; + mostVisited.layout(); + + loading = false; + + // Remove class name in a timeout so that changes done in this JS thread are + // not animated. + window.setTimeout(function() { + mostVisited.ensureSmallGridCorrect(); + document.body.classList.remove('loading'); + }, 1); + + // Only show the first run notification if first run. + if (firstRun) { + showFirstRunNotification(); + } +} diff --git a/chrome/browser/resources/ntp/most_visited.css b/chrome/browser/resources/ntp/most_visited.css new file mode 100644 index 0000000..3df8f68 --- /dev/null +++ b/chrome/browser/resources/ntp/most_visited.css @@ -0,0 +1,259 @@ +/* Most Visited */ + +#most-visited { + position: relative; + padding: 0; + height: 366px; + margin-top: -10px; + -webkit-user-select: none; +} + +.thumbnail-container { + position: absolute; + color: hsl(213, 90%, 24%); + text-decoration: none; + -webkit-transition: left .15s, right .15s, top .15s; + text-decoration: none; +} + +.thumbnail-container:focus { + outline: none; +} + +.thumbnail, +.thumbnail-container > .title { + width: 207px; /* natural size is 196 */ + height: 129px; /* 136 */ + -webkit-transition: width .15s, height .15s; +} + +.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 .15s, width .15s; + color: black; +} + +.thumbnail-wrapper { + display: block; + background-size: 212px 132px; + background: no-repeat 4px 4px; + background-color: white; + border-radius: 5px; + -webkit-transition: background-size .15s; + position: relative; +} + +.filler * { + visibility: hidden; +} + +.filler { + pointer-events: none; +} + +.filler .thumbnail-wrapper { + visibility: visible; + border: 3px solid hsl(213, 60%, 92%); +} + +.filler .thumbnail { + visibility: inherit; + border: 1px solid white; + padding: 0; + background-color: hsl(213, 60%, 92%); +} + +.edit-bar { + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: stretch; + padding: 3px; + padding-bottom: 0; + height: 17px; /* 23 - 2 * 3 */ + cursor: move; + font-size: 100%; + line-height: 17px; + background: hsl(213, 54%, 95%); + border-top-left-radius: 4px; + border-top-right-radius: 4px; + position: relative; + margin-top: 21px; + margin-bottom: -21px; + -webkit-transition: margin .15s, background .15s; +} + +.edit-bar > * { + display: block; + position: relative; +} + +.thumbnail-container:focus .edit-bar, +.thumbnail-container:hover .edit-bar { + margin-top: 0; + margin-bottom: 0; + -webkit-transition-delay: .5s, 0s; + + /* We need background-color as well to get the fade out animation correct */ + background-color: hsl(213, 66%, 57%); + background-image: -webkit-gradient(linear, left top, left bottom, + from(hsl(213, 87%, 67%)), + to(hsl(213, 66%, 57%))); +} + +.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); +} + +.thumbnail-container > .title > div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background: no-repeat 0 50%; + 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 hsl(213, 63%, 93%); + padding: 1px; + border-radius: 5px; + display: block; + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, 0); + -webkit-transition: width .15s, height .15s, border-color .15s, + border-radius .15s, -webkit-box-shadow .15s; +} + +.edit-mode-border { + border-radius: 4px; + + /* when dragged over we move this */ + position: relative; + -webkit-transition: top .15s, left .15s; +} + +.thumbnail-container:focus .thumbnail, +.thumbnail-container:hover .thumbnail { + border-color: hsl(213, 66%, 57%); + -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)) + ); + + /* delay border radius transition as much as the edit bar slide delay */ + -webkit-transition-delay: 0, 0, 0, .5s, 0; +} + +.thumbnail-container:focus > .edit-mode-border, +.thumbnail-container:hover > .edit-mode-border { + background-color: hsl(213, 66%, 57%); + -webkit-box-shadow: 0px 2px 2px hsla(0, 0%, 0%, .5); +} + +.dragging, +.dragging * { + -webkit-transition: none !important; +} + +.dragging > .title { + opacity: 0; +} + +@-webkit-keyframes 'fade-in' { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.fade-in { + -webkit-animation: 'fade-in' .15s; +} + +@media (max-width: 920px) { + #most-visited { + height: 294px; + } + + .thumbnail, + .thumbnail-container > .title { + width: 150px; + height: 93px; + } + + .thumbnail-container > .title { + height: auto; + } + + .thumbnail-wrapper { + background-size: 150px 93px; + } +} diff --git a/chrome/browser/resources/ntp/most_visited.js b/chrome/browser/resources/ntp/most_visited.js index 9a19c0d..fbdffb4 100644 --- a/chrome/browser/resources/ntp/most_visited.js +++ b/chrome/browser/resources/ntp/most_visited.js @@ -2,491 +2,578 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -var mostVisitedData = []; -var gotMostVisited = false; - -function mostVisitedPages(data, firstRun) { - 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}; +// Dependencies that we should remove/formalize: +// ../shared/js/class_list.js +// util.js +// +// afterTransition +// chrome.send +// hideNotification +// isRtl +// localStrings +// logEvent +// showNotification + + +var MostVisited = (function() { + + function addPinnedUrl(item, index) { + chrome.send('addPinnedURL', [item.url, item.title, item.faviconUrl || '', + item.thumbnailUrl || '', String(index)]); } - mostVisitedData = data; - renderMostVisited(data); - - gotMostVisited = true; - onDataLoaded(); - - // Only show the first run notification if first run. - if (firstRun) { - showFirstRunNotification(); - } -} -function getThumbnailClassName(data) { - return 'thumbnail-container' + - (data.pinned ? ' pinned' : '') + - (data.filler ? ' filler' : ''); -} - -function renderMostVisited(data) { - var parent = $('most-visited'); - var children = parent.children; - for (var i = 0; i < data.length; i++) { - var d = data[i]; - var t = children[i]; - - // If we have a filler continue - var oldClassName = t.className; - var newClassName = getThumbnailClassName(d); - if (oldClassName != newClassName) { - t.className = newClassName; - } - - // No need to continue if this is a filler. - if (newClassName == 'thumbnail-container filler') { - // Make sure the user cannot tab to the filler. - t.tabIndex = -1; - continue; - } - // Allow focus. - t.tabIndex = 1; - - t.href = d.url; - t.querySelector('.pin').title = localStrings.getString(d.pinned ? - 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); - t.querySelector('.remove').title = - localStrings.getString('removethumbnailtooltip'); - - // 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. - - var thumbnailUrl = d.thumbnailUrl || 'chrome://thumb/' + d.url; - t.querySelector('.thumbnail-wrapper').style.backgroundImage = - url(thumbnailUrl); - var titleDiv = t.querySelector('.title > div'); - titleDiv.xtitle = titleDiv.textContent = d.title; - var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; - titleDiv.style.backgroundImage = url(faviconUrl); - titleDiv.dir = d.direction; - } -} - -var mostVisited = { - addPinnedUrl_: function(data, index) { - chrome.send('addPinnedURL', [data.url, data.title, data.faviconUrl || '', - data.thumbnailUrl || '', String(index)]); - }, - getItem: function(el) { + function getItem(el) { return findAncestorByClass(el, 'thumbnail-container'); - }, - - getHref: function(el) { - return el.href; - }, - - togglePinned: function(el) { - var index = this.getThumbnailIndex(el); - var data = mostVisitedData[index]; - data.pinned = !data.pinned; - if (data.pinned) { - this.addPinnedUrl_(data, index); - } else { - chrome.send('removePinnedURL', [data.url]); - } - this.updatePinnedDom_(el, data.pinned); - }, + } - updatePinnedDom_: function(el, pinned) { + function updatePinnedDom(el, pinned) { el.querySelector('.pin').title = localStrings.getString(pinned ? 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); if (pinned) { - addClass(el, 'pinned'); + el.classList.add('pinned'); } else { - removeClass(el, 'pinned'); + el.classList.remove('pinned'); } - }, + } - getThumbnailIndex: function(el) { + function getThumbnailIndex(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]; - this.addPinnedUrl_(sourceData, destinationIndex); - sourceData.pinned = true; - this.updatePinnedDom_(source, true); - - var destinationData = mostVisitedData[destinationIndex]; - // Only update the destination if it was pinned before. - if (destinationData.pinned) { - this.addPinnedUrl_(destinationData, 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; + function MostVisited(el, useSmallGrid, visible) { + this.element = el; + this.useSmallGrid_ = useSmallGrid; + this.visible_ = visible; + + this.createThumbnails_(); + this.applyMostVisitedRects_(); + + el.addEventListener('click', bind(this.handleClick_, this)); + el.addEventListener('keydown', bind(this.handleKeyDown_, this)); + + document.addEventListener('DOMContentLoaded', + bind(this.ensureSmallGridCorrect, this)); + + // DND + 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)); + } + + MostVisited.prototype = { + togglePinned_: function(el) { + var index = getThumbnailIndex(el); + var item = this.data[index]; + item.pinned = !item.pinned; + if (item.pinned) { + addPinnedUrl(item, index); + } else { + chrome.send('removePinnedURL', [item.url]); + } + updatePinnedDom(el, item.pinned); + }, + + swapPosition_: function(source, destination) { + var nodes = source.parentNode.querySelectorAll('.thumbnail-container'); + var sourceIndex = getThumbnailIndex(source); + var destinationIndex = getThumbnailIndex(destination); + swapDomNodes(source, destination); + + var sourceData = this.data[sourceIndex]; + addPinnedUrl(sourceData, destinationIndex); + sourceData.pinned = true; + updatePinnedDom(source, true); + + var destinationData = this.data[destinationIndex]; + // Only update the destination if it was pinned before. + if (destinationData.pinned) { + addPinnedUrl(destinationData, sourceIndex); + } + this.data[destinationIndex] = sourceData; + this.data[sourceIndex] = destinationData; + }, + + blacklist: function(el) { + var self = this; + var url = el.href; + chrome.send('blacklistURLFromMostVisited', [url]); + + el.classList.add('hide'); + + // Find the old item. + var oldUrls = {}; + var oldIndex = -1; + var oldItem; + var data = this.data; for (var i = 0; i < data.length; i++) { - if (!(data[i].url in oldUrls)) { - newItem = data[i]; - break; + if (data[i].url == url) { + oldItem = data[i]; + oldIndex = i; } + oldUrls[data[i].url] = true; } - if (!newItem) { - // If no other page is available to replace the blacklisted item, - // we need to reorder items s.t. all filler items are in the rightmost - // indices. - mostVisitedPages(data); - - // Replace old item with new item in the mostVisitedData array. - } else if (oldIndex != -1) { - mostVisitedData.splice(oldIndex, 1, newItem); - mostVisitedPages(mostVisitedData); - addClass(el, 'fade-in'); - } - - // We wrap the title in a <span class=blacklisted-title>. We pass an empty - // string to the notifier function and use DOM to insert the real string. - var actionText = localStrings.getString('undothumbnailremove'); - - // Show notification and add undo callback function. - var wasPinned = oldItem.pinned; - showNotification('', actionText, function() { - self.removeFromBlackList(url); - if (wasPinned) { - self.addPinnedUrl_(oldItem, oldIndex); + // 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; + } } - chrome.send('getMostVisited'); - }); - - // Now change the DOM. - var removeText = localStrings.getString('thumbnailremovednotification'); - var notifySpan = document.querySelector('#notification > span'); - notifySpan.textContent = removeText; - - // Focus the undo link. - var undoLink = document.querySelector( - '#notification > .link > [tabindex]'); - undoLink.focus(); - }); - }, - - removeFromBlackList: function(url) { - chrome.send('removeURLsFromMostVisitedBlacklist', [url]); - }, - - clearAllBlacklisted: function() { - chrome.send('clearMostVisitedURLsBlacklist', []); - hideNotification(); - }, - - updateDisplayMode: function() { - if (!this.dirty_) { - return; - } - updateSimpleSection('most-visited-section', Section.THUMB); - }, - - dirty_: false, - - invalidate: function() { - this.dirty_ = true; - }, - - layout: function() { - if (!this.dirty_) { - return; - } - var d0 = Date.now(); - - var mostVisitedElement = $('most-visited'); - var thumbnails = mostVisitedElement.children; - var hidden = !(shownSections & Section.THUMB); - - // We set overflow to hidden so that the most visited element does not - // "leak" when we hide and show it. - if (hidden) { - mostVisitedElement.style.overflow = 'hidden'; - } - - applyMostVisitedRects(); + if (!newItem) { + // If no other page is available to replace the blacklisted item, + // we need to reorder items s.t. all filler items are in the rightmost + // indices. + self.data = data; + + // Replace old item with new item in the most visited data array. + } else if (oldIndex != -1) { + var oldData = self.data.concat(); + oldData.splice(oldIndex, 1, newItem); + self.data = oldData; + el.classList.add('fade-in'); + } - // Only set overflow to visible if the element is shown. - if (!hidden) { - afterTransition(function() { - mostVisitedElement.style.overflow = ''; + // We wrap the title in a <span class=blacklisted-title>. We pass an + // empty string to the notifier function and use DOM to insert the real + // string. + var actionText = localStrings.getString('undothumbnailremove'); + + // Show notification and add undo callback function. + var wasPinned = oldItem.pinned; + showNotification('', actionText, function() { + self.removeFromBlackList(url); + if (wasPinned) { + addPinnedUrl(oldItem, oldIndex); + } + chrome.send('getMostVisited'); + }); + + // Now change the DOM. + var removeText = localStrings.getString('thumbnailremovednotification'); + var notifySpan = document.querySelector('#notification > span'); + notifySpan.textContent = removeText; + + // Focus the undo link. + var undoLink = document.querySelector( + '#notification > .link > [tabindex]'); + undoLink.focus(); }); - } - - this.dirty_ = false; - - logEvent('mostVisited.layout: ' + (Date.now() - d0)); - }, - - getRectByIndex: function(index) { - return getMostVisitedLayoutRects()[index]; - } -}; - -$('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(); - } -}); - -// Allow blacklisting most visited site using the keyboard. -$('most-visited').addEventListener('keydown', function(e) { - if (!IS_MAC && e.keyCode == 46 || // Del - IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace - mostVisited.blacklist(e.target); - } -}); - -window.addEventListener('load', onDataLoaded); + }, + + removeFromBlackList: function(url) { + chrome.send('removeURLsFromMostVisitedBlacklist', [url]); + }, + + clearAllBlacklisted: function() { + chrome.send('clearMostVisitedURLsBlacklist', []); + hideNotification(); + }, + + dirty_: false, + invalidate_: function() { + this.dirty_ = true; + }, + + visible_: true, + get visible() { + return this.visible_; + }, + set visible(visible) { + if (this.visible_ != visible) { + this.visible_ = visible; + this.invalidate_(); + } + }, + + useSmallGrid_: false, + get useSmallGrid() { + return this.useSmallGrid_; + }, + set useSmallGrid(b) { + if (this.useSmallGrid_ != b) { + this.useSmallGrid_ = b; + this.invalidate_(); + } + }, + + layout: function() { + if (!this.dirty_) + return; + var d0 = Date.now(); + this.applyMostVisitedRects_(); + this.dirty_ = false; + logEvent('mostVisited.layout: ' + (Date.now() - d0)); + }, + + createThumbnails_: function() { + var singleHtml = + '<a class="thumbnail-container filler" tabindex="1">' + + '<div class="edit-mode-border">' + + '<div class="edit-bar">' + + '<div class="pin"></div>' + + '<div class="spacer"></div>' + + '<div class="remove"></div>' + + '</div>' + + '<span class="thumbnail-wrapper">' + + '<span class="thumbnail"></span>' + + '</span>' + + '</div>' + + '<div class="title">' + + '<div></div>' + + '</div>' + + '</a>'; + this.element.innerHTML = Array(8 + 1).join(singleHtml); + var children = this.element.children; + for (var i = 0; i < 8; i++) { + children[i].id = 't' + i; + } + }, + + getMostVisitedLayoutRects_: function() { + var small = this.useSmallGrid; + + var cols = 4; + var rows = 2; + var marginWidth = 10; + var marginHeight = 7; + var borderWidth = 4; + var thumbWidth = small ? 150 : 207; + var thumbHeight = small ? 93 : 129; + var w = thumbWidth + 2 * borderWidth + 2 * marginWidth; + var h = thumbHeight + 40 + 2 * marginHeight; + var sumWidth = cols * w - 2 * marginWidth; + + var rtl = isRtl(); + var rects = []; + + if (this.visible) { + for (var i = 0; i < rows * cols; i++) { + var row = Math.floor(i / cols); + var col = i % cols; + var left = rtl ? sumWidth - col * w - thumbWidth - 2 * borderWidth : + col * w; + + var top = row * h; + + rects[i] = {left: left, top: top}; + } + } + return rects; + }, + + applyMostVisitedRects_: function() { + if (this.visible) { + var rects = this.getMostVisitedLayoutRects_(); + var children = this.element.children; + for (var i = 0; i < 8; i++) { + var t = children[i]; + t.style.left = rects[i].left + 'px'; + t.style.top = rects[i].top + 'px'; + t.style.right = ''; + var innerStyle = t.firstElementChild.style; + innerStyle.left = innerStyle.top = ''; + } + } + }, + + // Work around for http://crbug.com/25329 + ensureSmallGridCorrect: function(expected) { + if (expected != this.useSmallGrid) + this.applyMostVisitedRects_(); + }, + + getRectByIndex_: function(index) { + return this.getMostVisitedLayoutRects_()[index]; + }, + + // DND + + currentOverItem_: null, + get currentOverItem() { + return this.currentOverItem_; + }, + set currentOverItem(item) { + var style; + if (item != this.currentOverItem_) { + if (this.currentOverItem_) { + style = this.currentOverItem_.firstElementChild.style; + style.left = style.top = ''; + } + this.currentOverItem_ = item; + + if (item) { + // Make the drag over item move 15px towards the source. The movement + // is done by only moving the edit-mode-border (as in the mocks) and + // it is done with relative positioning so that the movement does not + // change the drop target. + var dragIndex = getThumbnailIndex(this.dragItem_); + var overIndex = getThumbnailIndex(item); + if (dragIndex == -1 || overIndex == -1) { + return; + } + + var dragRect = this.getRectByIndex_(dragIndex); + var overRect = this.getRectByIndex_(overIndex); + + var x = dragRect.left - overRect.left; + var y = dragRect.top - overRect.top; + var z = Math.sqrt(x * x + y * y); + var z2 = 15; + var x2 = x * z2 / z; + var y2 = y * z2 / z; + + style = this.currentOverItem_.firstElementChild.style; + style.left = x2 + 'px'; + style.top = y2 + 'px'; + } + } + }, + dragItem_: null, + startX_: 0, + startY_: 0, + startScreenX_: 0, + startScreenY_: 0, + dragEndTimer_: null, + + isDragging: function() { + return !!this.dragItem_; + }, + + handleDragStart_: function(e) { + var thumbnail = getItem(e.target); + if (thumbnail) { + // Don't set data since HTML5 does not allow setting the name for + // url-list. Instead, we just rely on the dragging of link behavior. + this.dragItem_ = thumbnail; + this.dragItem_.classList.add('dragging'); + this.dragItem_.style.zIndex = 2; + e.dataTransfer.effectAllowed = 'copyLinkMove'; + } + }, -window.addEventListener('resize', handleWindowResize); + handleDragEnter_: function(e) { + if (this.canDropOnElement_(this.currentOverItem)) { + e.preventDefault(); + } + }, -// Work around for http://crbug.com/25329 -function ensureSmallGridCorrect() { - if (wasSmallGrid != useSmallGrid()) { - applyMostVisitedRects(); - } -} -document.addEventListener('DOMContentLoaded', ensureSmallGridCorrect); - -// DnD - -var dnd = { - currentOverItem_: null, - get currentOverItem() { - return this.currentOverItem_; - }, - set currentOverItem(item) { - var style; - if (item != this.currentOverItem_) { - if (this.currentOverItem_) { - style = this.currentOverItem_.firstElementChild.style; - style.left = style.top = ''; + handleDragOver_: function(e) { + var item = getItem(e.target); + this.currentOverItem = item; + if (this.canDropOnElement_(item)) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; } - this.currentOverItem_ = item; + }, + handleDragLeave_: function(e) { + var item = getItem(e.target); if (item) { - // Make the drag over item move 15px towards the source. The movement is - // done by only moving the edit-mode-border (as in the mocks) and it is - // done with relative positioning so that the movement does not change - // the drop target. - var dragIndex = mostVisited.getThumbnailIndex(this.dragItem); - var overIndex = mostVisited.getThumbnailIndex(item); - if (dragIndex == -1 || overIndex == -1) { - return; + e.preventDefault(); + } + + this.currentOverItem = null; + }, + + handleDrop_: function(e) { + var dropTarget = getItem(e.target); + if (this.canDropOnElement_(dropTarget)) { + dropTarget.style.zIndex = 1; + this.swapPosition_(this.dragItem_, dropTarget); + // The timeout below is to allow WebKit to see that we turned off + // pointer-event before moving the thumbnails so that we can get out of + // hover mode. + window.setTimeout(bind(function() { + this.invalidate_(); + this.layout(); + }, this), 10); + e.preventDefault(); + if (this.dragEndTimer_) { + window.clearTimeout(this.dragEndTimer_); + this.dragEndTimer_ = null; } + afterTransition(function() { + dropTarget.style.zIndex = ''; + }); + } + }, + + handleDragEnd_: function(e) { + var dragItem = this.dragItem_; + if (dragItem) { + dragItem.style.pointerEvents = ''; + dragItem.classList.remove('dragging'); + + afterTransition(function() { + // Delay resetting zIndex to let the animation finish. + dragItem.style.zIndex = ''; + // Same for overflow. + dragItem.parentNode.style.overflow = ''; + }); + + this.invalidate_(); + this.layout(); + this.dragItem_ = null; + } + }, + + handleDrag_: function(e) { + // Moves the drag item making sure that it is not displayed outside the + // browser viewport. + var item = getItem(e.target); + var rect = this.element.getBoundingClientRect(); + item.style.pointerEvents = 'none'; + + var x = this.startX_ + e.screenX - this.startScreenX_; + var y = this.startY_ + e.screenY - this.startScreenY_; + + // The position of the item is relative to #most-visited so we need to + // subtract that when calculating the allowed position. + x = Math.max(x, -rect.left); + x = Math.min(x, document.body.clientWidth - rect.left - item.offsetWidth - + 2); + // The shadow is 2px + y = Math.max(-rect.top, y); + y = Math.min(y, document.body.clientHeight - rect.top - + item.offsetHeight - 2); + + // Override right in case of RTL. + item.style.right = 'auto'; + item.style.left = x + 'px'; + item.style.top = y + 'px'; + item.style.zIndex = 2; + }, + + // We listen to mousedown to get the relative position of the cursor for dnd. + handleMouseDown_: function(e) { + var item = getItem(e.target); + if (item) { + this.startX_ = item.offsetLeft; + this.startY_ = item.offsetTop; + this.startScreenX_ = e.screenX; + this.startScreenY_ = e.screenY; + + // We don't want to focus the item on mousedown. However, to prevent + // focus one has to call preventDefault but this also prevents the drag + // and drop (sigh) so we only prevent it when the user is not doing a + // left mouse button drag. + if (e.button != 0) // LEFT + e.preventDefault(); + } + }, + + canDropOnElement_: function(el) { + return this.dragItem_ && el && + el.classList.contains('thumbnail-container') && + !el.classList.contains('filler'); + }, + + + /// data + + data_: null, + get data() { + return this.data_; + }, + set data(data) { + // 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}; + } - var dragRect = mostVisited.getRectByIndex(dragIndex); - var overRect = mostVisited.getRectByIndex(overIndex); + // On setting we need to update the items + this.data_ = data; + this.updateMostVisited_(); + }, - var x = dragRect.left - overRect.left; - var y = dragRect.top - overRect.top; - var z = Math.sqrt(x * x + y * y); - var z2 = 15; - var x2 = x * z2 / z; - var y2 = y * z2 / z; + updateMostVisited_: function() { - style = this.currentOverItem_.firstElementChild.style; - style.left = x2 + 'px'; - style.top = y2 + 'px'; + function getThumbnailClassName(item) { + return 'thumbnail-container' + + (item.pinned ? ' pinned' : '') + + (item.filler ? ' filler' : ''); } - } - }, - dragItem: null, - startX: 0, - startY: 0, - startScreenX: 0, - startScreenY: 0, - dragEndTimer: null, - - handleDragStart: function(e) { - var thumbnail = mostVisited.getItem(e.target); - if (thumbnail) { - // Don't set data since HTML5 does not allow setting the name for - // url-list. Instead, we just rely on the dragging of link behavior. - this.dragItem = thumbnail; - addClass(this.dragItem, 'dragging'); - this.dragItem.style.zIndex = 2; - e.dataTransfer.effectAllowed = 'copyLinkMove'; - } - }, - handleDragEnter: function(e) { - if (this.canDropOnElement(this.currentOverItem)) { - e.preventDefault(); - } - }, - - handleDragOver: function(e) { - var item = mostVisited.getItem(e.target); - this.currentOverItem = item; - if (this.canDropOnElement(item)) { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - } - }, - - handleDragLeave: function(e) { - var item = mostVisited.getItem(e.target); - if (item) { - e.preventDefault(); - } + var data = this.data; + var children = this.element.children; + for (var i = 0; i < data.length; i++) { + var d = data[i]; + var t = children[i]; + + // If we have a filler continue + var oldClassName = t.className; + var newClassName = getThumbnailClassName(d); + if (oldClassName != newClassName) { + t.className = newClassName; + } - 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); - // The timeout below is to allow WebKit to see that we turned off - // pointer-event before moving the thumbnails so that we can get out of - // hover mode. - window.setTimeout(function() { - mostVisited.invalidate(); - mostVisited.layout(); - }, 10); - e.preventDefault(); - if (this.dragEndTimer) { - window.clearTimeout(this.dragEndTimer); - this.dragEndTimer = null; + // No need to continue if this is a filler. + if (newClassName == 'thumbnail-container filler') { + // Make sure the user cannot tab to the filler. + t.tabIndex = -1; + continue; + } + // Allow focus. + t.tabIndex = 1; + + t.href = d.url; + t.querySelector('.pin').title = localStrings.getString(d.pinned ? + 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); + t.querySelector('.remove').title = + localStrings.getString('removethumbnailtooltip'); + + // 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. + + var thumbnailUrl = d.thumbnailUrl || 'chrome://thumb/' + d.url; + t.querySelector('.thumbnail-wrapper').style.backgroundImage = + url(thumbnailUrl); + var titleDiv = t.querySelector('.title > div'); + titleDiv.xtitle = titleDiv.textContent = d.title; + var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; + titleDiv.style.backgroundImage = url(faviconUrl); + titleDiv.dir = d.direction; } - afterTransition(function() { - dropTarget.style.zIndex = ''; - }); - } - }, - - handleDragEnd: function(e) { - 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 = ''; - }); + }, - mostVisited.invalidate(); - mostVisited.layout(); - this.dragItem = null; - } - }, - - handleDrag: function(e) { - // Moves the drag item making sure that it is not displayed outside the - // browser viewport. - var item = mostVisited.getItem(e.target); - var rect = document.querySelector('#most-visited').getBoundingClientRect(); - item.style.pointerEvents = 'none'; - - var x = this.startX + e.screenX - this.startScreenX; - var y = this.startY + e.screenY - this.startScreenY; - - // The position of the item is relative to #most-visited so we need to - // subtract that when calculating the allowed position. - x = Math.max(x, -rect.left); - x = Math.min(x, document.body.clientWidth - rect.left - item.offsetWidth - - 2); - // The shadow is 2px - y = Math.max(-rect.top, y); - y = Math.min(y, document.body.clientHeight - rect.top - item.offsetHeight - - 2); - - // Override right in case of RTL. - item.style.right = 'auto'; - item.style.left = x + 'px'; - item.style.top = y + 'px'; - item.style.zIndex = 2; - }, - - // 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; - - // We don't want to focus the item on mousedown. However, to prevent focus - // one has to call preventDefault but this also prevents the drag and drop - // (sigh) so we only prevent it when the user is not doing a left mouse - // button drag. - if (e.button != 0) // LEFT + handleClick_: function(e) { + var target = e.target; + if (target.classList.contains('pin')) { + this.togglePinned_(getItem(target)); e.preventDefault(); + } else if (target.classList.contains('remove')) { + this.blacklist(getItem(target)); + e.preventDefault(); + } + }, + + /** + * Allow blacklisting most visited site using the keyboard. + */ + handleKeyDown_: function(e) { + if (!IS_MAC && e.keyCode == 46 || // Del + IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace + this.blacklist(e.target); + } } - }, - - 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(); + }; + return MostVisited; +})(); diff --git a/chrome/browser/resources/ntp/util.js b/chrome/browser/resources/ntp/util.js new file mode 100644 index 0000000..ab99ec2 --- /dev/null +++ b/chrome/browser/resources/ntp/util.js @@ -0,0 +1,86 @@ +// Copyright (c) 2010 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(arv): Move to shared/js once namespaced and tested. + +var global = this; +const IS_MAC = /$Mac/.test(navigator.platform); + +function $(id) { + return document.getElementById(id); +} + +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); + } +} + +function url(s) { + // http://www.w3.org/TR/css3-values/#uris + // Parentheses, commas, whitespace characters, single quotes (') and double + // quotes (") appearing in a URI must be escaped with a backslash + var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); + // WebKit has a bug when it comes to URLs that end with \ + // https://bugs.webkit.org/show_bug.cgi?id=28885 + if (/\\\\$/.test(s2)) { + // Add a space to work around the WebKit bug. + s2 += ' '; + } + return 'url("' + s2 + '")'; +} + +function findAncestorByClass(el, className) { + return findAncestor(el, function(el) { + if (el.classList) + return el.classList.contains(className); + return null; + }); +} + +/** + * Return the first ancestor for which the {@code predicate} returns true. + * @param {Node} node The node to check. + * @param {function(Node) : boolean} predicate The function that tests the + * nodes. + * @return {Node} The found ancestor or null if not found. + */ +function findAncestor(node, predicate) { + var last = false; + while (node != null && !(last = predicate(node))) { + node = node.parentNode; + } + return last ? node : null; +} + +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); +} + +/** + * 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); +} + + diff --git a/chrome/browser/resources/shared/js/class_list.js b/chrome/browser/resources/shared/js/class_list.js new file mode 100644 index 0000000..edfd98f --- /dev/null +++ b/chrome/browser/resources/shared/js/class_list.js @@ -0,0 +1,94 @@ +// Copyright (c) 2010 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 This implements the HTML5 HTMLElement classList property. + */ + +// TODO(arv): Remove this when classList is available in WebKit. +// https://bugs.webkit.org/show_bug.cgi?id=20709 + +if (typeof document.createElement('div').classList == 'undefined') { + +function DOMTokenList(el) { + this.el_ = el; +} + +(function() { + + var re = /\s+/; + + function split(el) { + var cn = el.className.replace(/^\s+|\s$/g, ''); + if (cn == '') + return []; + return cn.split(re); + } + + function DOMException_(code) { + this.code = code; + } + DOMException_.prototype = DOMException.prototype; + + function assertValidToken(s) { + if (s == '') + throw new DOMException_(DOMException.SYNTAX_ERR); + if (re.test(s)) + throw new DOMException_(DOMException.INVALID_CHARACTER_ERR); + } + + DOMTokenList.prototype = { + contains: function(token) { + assertValidToken(token); + return split(this.el_).indexOf(token) >= 0; + }, + add: function(token) { + assertValidToken(token); + if (this.contains(token)) + return; + var cn = this.el_.className; + this.el_.className += (cn == '' || re.test(cn.slice(-1)) ? '' : ' ') + + token; + }, + remove: function(token) { + assertValidToken(token); + var names = split(this.el_); + var s = []; + for (var i = 0; i < names.length; i++) { + if (names[i] != token) + s.push(names[i]) + } + this.el_.className = s.join(' '); + }, + toggle: function(token) { + assertValidToken(token); + if (this.contains(token)) { + this.remove(token); + return false; + } else { + this.add(token); + return true; + } + }, + get length() { + return split(this.el_).length; + }, + item: function(index) { + return split(this.el_)[index]; + } + }; + +})(); + +HTMLElement.prototype.__defineGetter__('classList', function() { + var tl = new DOMTokenList(this); + // Override so that we reuse the same DOMTokenList and so that + // el.classList == el.classList + this.__defineGetter__('classList', function() { + return tl; + }); + return tl; +}); + +} // if diff --git a/chrome/browser/resources/shared/js/class_list_test.html b/chrome/browser/resources/shared/js/class_list_test.html new file mode 100644 index 0000000..c84b8ce --- /dev/null +++ b/chrome/browser/resources/shared/js/class_list_test.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> +<head> +<title></title> +<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> +<script src="class_list.js"></script> +<script> + +goog.require('goog.testing.jsunit'); + +</script> +</head> +<body> + +<script> + +var el = document.body; + +function testLength() { + el.className = 'a b'; + assertEquals(2, el.classList.length); + el.setAttribute('class', 'a b c'); + assertEquals(3, el.classList.length); +} + +function testItem() { + el.className = 'a b'; + assertEquals('a', el.classList.item(0)); + assertEquals('b', el.classList.item(1)); + el.setAttribute('class', 'a b c'); + assertEquals('a', el.classList.item(0)); + assertEquals('b', el.classList.item(1)); + assertEquals('c', el.classList.item(2)); +} + +function testContains() { + el.className = 'a b'; + assertTrue(el.classList.contains('a')); + assertTrue(el.classList.contains('b')); + assertFalse(el.classList.contains('c')); + + assertEquals('b', el.classList.item(1)); + el.setAttribute('class', 'a b c'); + assertTrue(el.classList.contains('a')); + assertTrue(el.classList.contains('b')); + assertTrue(el.classList.contains('c')); +} + +function testToken() { + assertThrows(function() { + el.classList.add(''); + }); + assertThrows(function() { + el.classList.add(' '); + }); + assertThrows(function() { + el.classList.add('\t'); + }); + assertThrows(function() { + el.classList.add('\n'); + }); +} + +function testAdd() { + el.className = 'a b'; + el.classList.add('a'); + assertEquals('a b', el.className); + el.classList.add('c'); + assertEquals('a b c', el.className); +} + +function testRemove() { + el.className = 'a b'; + el.classList.remove('b'); + assertEquals('a', el.className); + el.classList.remove('a'); + assertEquals('', el.className); +} + +function testToggle() { + el.className = 'a b'; + assertFalse(el.classList.toggle('a')); + assertEquals('b', el.className); + assertTrue(el.classList.toggle('a')); + assertEquals('b a', el.className); +} + +</script> + +</body> +</html> |