summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'chrome')
-rw-r--r--chrome/browser/dom_ui/new_tab_ui_uitest.cc2
-rw-r--r--chrome/browser/resources/new_new_tab.css333
-rw-r--r--chrome/browser/resources/new_new_tab.html166
-rw-r--r--chrome/browser/resources/new_new_tab.js211
-rw-r--r--chrome/browser/resources/ntp/most_visited.css259
-rw-r--r--chrome/browser/resources/ntp/most_visited.js987
-rw-r--r--chrome/browser/resources/ntp/util.js86
-rw-r--r--chrome/browser/resources/shared/js/class_list.js94
-rw-r--r--chrome/browser/resources/shared/js/class_list_test.html91
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>