diff options
Diffstat (limited to 'chrome')
-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> |