Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(569)

Side by Side Diff: chrome/browser/resources/local_ntp/local_ntp.js

Issue 12840003: Implement local NTP for fallback. (Closed) Base URL: https://git.chromium.org/chromium/src.git@master
Patch Set: Respond to comments Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
Dan Beam 2013/03/22 19:32:50 can you add a namespace here? this shouldn't all
jeremycho 2013/03/22 21:33:30 Done.
4
5 /**
6 * The element used to vertically position the most visited section on
7 * window resize.
8 * @type {Element}
9 */
10 var topMarginElement;
11
12 /**
13 * The container for the tile elements.
14 * @type {Element}
15 */
16 var tilesContainer;
17
18 /**
19 * The notification displayed when a page is blacklisted.
20 * @type {Element}
21 */
22 var notification;
23
24 /**
25 * The handle for the timer used to hide the notification.
26 * @type {?number}
27 */
28 var notificationTimer = null;
29
30 /**
31 * The array of rendered tiles, ordered by appearance.
32 * @type {Array.<Tile>}
33 */
34 var tiles = [];
35
36 /**
37 * The last blacklisted tile if any, which by definition should not be filler.
38 * @type {?Tile}
39 */
40 var lastBlacklistedTile = null;
41
42 /**
43 * The index of the last blacklisted tile, if any. Used to determine where to
44 * re-insert a tile on undo.
45 * @type {number}
46 */
47 var lastBlacklistedIndex = -1;
48
49 /**
50 * True if a page has been blacklisted and we're waiting on the
51 * onmostvisitedchange callback. See onMostVisitedChange() for how this
52 * is used.
53 * @type {boolean}
54 */
55 var isBlacklisting = false;
56
57 /**
58 * True if a blacklist has been undone and we're waiting on the
59 * onmostvisitedchange callback. See onMostVisitedChange() for how this
60 * is used.
61 * @type {boolean}
62 */
63 var isUndoing = false;
64
65 /**
66 * Current number of tiles shown based on the window width, including filler.
67 * @type {number}
68 */
69 var numTilesShown = 0;
70
71 /**
72 * The browser embeddedSearch.newTabPage object.
73 * @type {Object}
74 */
75 var apiHandle;
76
77 /**
78 * Possible background-colors of a non-custom theme. Used to determine whether
79 * the homepage should be updated to support custom or non-custom themes.
80 * @type {Array.<string>}
Dan Beam 2013/03/22 19:32:50 nit: !Array
jeremycho 2013/03/22 21:33:30 Done.
81 * @const
82 */
83 var WHITE = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)'];
84
85 /**
86 * Should be equal to mv-tile's -webkit-margin-start + width.
87 * @type {number}
88 * @const
89 */
90 var TILE_WIDTH = 160;
91
92 /**
93 * The height of the most visited section.
94 * @type {number}
95 * @const
96 */
97 var MOST_VISITED_HEIGHT = 156;
98
99 /** @type {number} @const */
100 var MAX_NUM_TILES_TO_SHOW = 4;
101
102 /** @type {number} @const */
103 var MIN_NUM_TILES_TO_SHOW = 2;
104
105 /**
106 * Minimum total padding to give to the left and right of the most visited
107 * section. Used to determine how many tiles to show.
108 * @type {number}
109 * @const
110 */
111 var MIN_TOTAL_HORIZONTAL_PADDING = 188;
112
113 /**
114 * Enum for classnames.
115 * @enum {string}
116 * @const
117 */
118 var CLASSES = {
119 TOP_MARGIN: 'mv-top-margin',
120 TILES: 'mv-tiles',
121 TILE: 'mv-tile',
122 PAGE: 'mv-page', // page tiles
123 TITLE: 'mv-title',
124 THUMBNAIL: 'mv-thumb',
125 DOMAIN: 'mv-domain',
126 BLACKLIST_BUTTON: 'mv-x',
127 FAVICON: 'mv-favicon',
128 FILLER: 'mv-filler', // filler tiles
129 NOTIFICATION: 'mv-notice',
130 BLACKLIST: 'mv-blacklist', // triggers tile blacklist animation
131 HIDE_TILE: 'mv-tile-hide', // hides tiles on small browser width
132 HIDE_BLACKLIST_BUTTON: 'mv-x-hide', // hides blacklist button during animation
133 HIDE_NOTIFICATION: 'mv-notice-hide'
Dan Beam 2013/03/22 19:32:50 nit: alpha
jeremycho 2013/03/22 21:33:30 Sorry, not sure what you mean.
134 };
135
136 /**
137 * Time (in milliseconds) to show the notification.
138 * @type {number}
139 * @const
140 */
141 var NOTIFICATION_TIMEOUT = 10000;
142
143 /**
144 * A Tile is either a rendering of a Most Visited page or "filler" used to
145 * pad out the section when not enough pages exist.
146 *
147 * @param {Element} elem The element for rendering the tile.
148 * @param {number=} opt_rid The RID for the corresponding Most Visited page.
149 * Should only be left unspecified when creating a filler tile.
150 * @constructor
151 */
152 function Tile(elem, opt_rid) {
153 /** @type {Element} */
154 this.elem = elem;
155
156 /** @type {number|undefined} */
157 this.rid = opt_rid;
158 }
159
160 /**
161 * Updates the NTP based on the current theme.
162 * @private
163 */
164 function onThemeChange() {
165 var info = apiHandle.themeBackgroundInfo;
166 if (!info)
167 return;
168 var background = [info.colorRgba,
169 info.imageUrl,
170 info.imageTiling,
171 info.imageHorizontalAlignment,
172 info.imageVerticalAlignment].join(' ').trim();
173 document.body.style.background = background;
174 var isCustom = !!background && WHITE.indexOf(background) == -1;
175 enable(document.body, 'custom-theme', isCustom);
176 }
177
178 /**
179 * Handles a new set of Most Visited page data.
180 */
181 function onMostVisitedChange() {
182 var pages = apiHandle.mostVisited;
183
184 // If this was called as a result of a blacklist, add a new replacement
185 // (possibly filler) tile at the end and trigger the blacklist animation.
186 if (isBlacklisting) {
187 var replacementTile = createTile(pages[MAX_NUM_TILES_TO_SHOW - 1]);
188
189 tiles.push(replacementTile);
190 tilesContainer.appendChild(replacementTile.elem);
191
192 var lastBlacklistedTileElement = lastBlacklistedTile.elem;
193 lastBlacklistedTileElement.addEventListener(
194 'webkitTransitionEnd', blacklistAnimationDone);
195 lastBlacklistedTileElement.classList.add(CLASSES.BLACKLIST);
196 // In order to animate the replacement tile sliding into place, it must
197 // be made visible.
198 updateTileVisibility(numTilesShown + 1);
199
200 // If this was called as a result of an undo, re-insert the last blacklisted
201 // tile in its old location and trigger the undo animation.
Dan Beam 2013/03/22 19:32:50 ^ indent off
jeremycho 2013/03/22 21:33:30 Done.
jeremycho 2013/03/22 21:33:30 Done.
202 } else if (isUndoing) {
203 tiles.splice(
204 lastBlacklistedIndex, 0, lastBlacklistedTile);
205 var lastBlacklistedTileElement = lastBlacklistedTile.elem;
206 tilesContainer.insertBefore(
207 lastBlacklistedTileElement,
208 tilesContainer.childNodes[lastBlacklistedIndex]);
209 lastBlacklistedTileElement.addEventListener(
210 'webkitTransitionEnd', undoAnimationDone);
211 // Yield to ensure the tile is added to the DOM before removing the class.
212 // Without this, the webkit transition doesn't reliably trigger.
213 window.setTimeout(function() {
214 lastBlacklistedTileElement.classList.remove(CLASSES.BLACKLIST);
Dan Beam 2013/03/22 19:32:50 just putting lastBlacklistedTileElement.scrollT
jeremycho 2013/03/22 21:33:30 Neat! Done.
215 }, 0);
216 // Otherwise render the tiles using the new data without animation.
217 } else {
218 tiles = [];
219 for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) {
220 tiles.push(createTile(pages[i]));
221 }
222 renderTiles();
223 }
224 }
225
226 /**
227 * Renders the current set of tiles without animation.
228 */
229 function renderTiles() {
230 removeChildren(tilesContainer);
231 for (var i = 0, length = tiles.length; i < length; ++i) {
232 tilesContainer.appendChild(tiles[i].elem);
233 }
234 }
235
236 /**
237 * Creates a Tile with the specified page data. If no data is provided, a
238 * filler Tile is created.
239 * @param {Object} page The page data.
240 * @return {Tile} The new Tile_.
Dan Beam 2013/03/22 19:32:50 why Tile_?
jeremycho 2013/03/22 21:33:30 Done.
241 */
242 function createTile(page) {
243 var tileElement = document.createElement('div');
244 tileElement.classList.add(CLASSES.TILE);
245
246 if (page) {
247 var rid = page.rid;
248 tileElement.classList.add(CLASSES.PAGE);
249
250 // The click handler for navigating to the page identified by the RID.
251 tileElement.addEventListener('click', function() {
252 apiHandle.navigateContentWindow(rid);
253 });
254
255 // The shadow DOM which renders the page title.
256 var titleElement = page.titleElement;
257 if (titleElement) {
258 titleElement.classList.add(CLASSES.TITLE);
259 tileElement.appendChild(titleElement);
260 }
261
262 // Render the thumbnail if present. Otherwise, fall back to a shadow DOM
263 // which renders the domain.
264 var thumbnailUrl = page.thumbnailUrl;
265
266 var showDomainElement = function() {
267 var domainElement = page.domainElement;
268 if (domainElement) {
269 domainElement.classList.add(CLASSES.DOMAIN);
270 tileElement.appendChild(domainElement);
271 }
272 };
273 if (thumbnailUrl) {
274 var image = new Image();
275 image.onload = function() {
276 var thumbnailElement = createAndAppendElement(
277 tileElement, 'div', CLASSES.THUMBNAIL);
278 thumbnailElement.style['background-image'] =
Dan Beam 2013/03/22 19:32:50 thumbnailElement.style.backgroundImage =
jeremycho 2013/03/22 21:33:30 Done.
279 'url(' + thumbnailUrl + ')';
280 };
281
282 image.onerror = showDomainElement;
283 image.src = thumbnailUrl;
284 } else {
285 showDomainElement();
286 }
287
288 // The button used to blacklist this page.
289 var blacklistButton = createAndAppendElement(
290 tileElement, 'div', CLASSES.BLACKLIST_BUTTON);
291 blacklistButton.addEventListener('click', generateBlacklistFunction(rid));
292 // TODO(jeremycho): i18n.
293 blacklistButton.title = "Don't show on this page";
Dan Beam 2013/03/22 19:32:50 ^ when will this happen?
jeremycho 2013/03/22 21:33:30 Added the tracking bug.
294
295 // The page favicon, if any.
296 var faviconUrl = page.faviconUrl;
297 if (faviconUrl) {
298 var favicon = createAndAppendElement(
299 tileElement, 'div', CLASSES.FAVICON);
300 favicon.style['background-image'] = 'url(' + faviconUrl + ')';
301 }
302 return new Tile(tileElement, rid);
303 } else {
304 tileElement.classList.add(CLASSES.FILLER);
305 return new Tile(tileElement);
306 }
307 }
308
309 /**
310 * Generates a function to be called when the page with the corresponding RID
311 * is blacklisted.
312 * @param {number} rid The RID of the page being blacklisted.
313 * @return {function(Event)} A function which handles the blacklisting of the
314 * page by displaying the notification, updating state variables, and
315 * notifying Chrome.
316 */
317 function generateBlacklistFunction(rid) {
318 return function(e) {
319 // Prevent navigation when the page is being blacklisted.
320 e.stopPropagation();
321
322 showNotification();
323 isBlacklisting = true;
324 tilesContainer.classList.add(CLASSES.HIDE_BLACKLIST_BUTTON);
325 lastBlacklistedTile = getTileByRid(rid);
326 lastBlacklistedIndex = tiles.indexOf(lastBlacklistedTile);
327 apiHandle.deleteMostVisitedItem(rid);
328 };
329 }
330
331 /**
332 * Shows the blacklist notification and refreshes the timer to hide it.
333 */
334 function showNotification() {
335 notification.classList.remove(CLASSES.HIDE_NOTIFICATION);
336 if (notificationTimer)
337 window.clearTimeout(notificationTimer);
338 notificationTimer = window.setTimeout(
339 hideNotification, NOTIFICATION_TIMEOUT);
340 }
341
Dan Beam 2013/03/22 19:32:50 \n\n or \n between methods -- be consistent (closu
jeremycho 2013/03/22 21:33:30 Done.
342
343 /**
344 * Hides the blacklist notification.
345 */
346 function hideNotification() {
347 notification.classList.add(CLASSES.HIDE_NOTIFICATION);
348 }
349
350 /**
351 * Handles the end of the blacklist animation by removing the blacklisted tile.
352 */
353 function blacklistAnimationDone() {
354 tiles.splice(lastBlacklistedIndex, 1);
355 removeNode(lastBlacklistedTile.elem);
356 updateTileVisibility(numTilesShown);
357 isBlacklisting = false;
358 tilesContainer.classList.remove(CLASSES.HIDE_BLACKLIST_BUTTON);
359 lastBlacklistedTile.elem.removeEventListener(
360 'webkitTransitionEnd', blacklistAnimationDone);
361 }
362
363 /**
364 * Handles a click on the notification undo link by hiding the notification and
365 * informing Chrome.
366 */
367 function onUndo() {
368 hideNotification();
369 var lastBlacklistedRID = lastBlacklistedTile.rid;
370 if (lastBlacklistedRID != null) {
Dan Beam 2013/03/22 19:32:50 can rid ever be 0? if not, then just if (lastB
jeremycho 2013/03/22 21:33:30 Using undefined just to be safe. On 2013/03/22 19
371 isUndoing = true;
372 apiHandle.undoMostVisitedDeletion(lastBlacklistedRID);
373 }
374 }
375
376 /**
377 * Handles the end of the undo animation by removing the extraneous end tile.
378 */
379 function undoAnimationDone() {
380 isUndoing = false;
381 tiles.splice(tiles.length - 1, 1);
382 removeNode(tilesContainer.lastElementChild);
383 updateTileVisibility(numTilesShown);
384 lastBlacklistedTile.elem.removeEventListener(
385 'webkitTransitionEnd', undoAnimationDone);
386 }
387
388 /**
389 * Handles a click on the restore all notification link by hiding the
390 * notification and informing Chrome.
391 */
392 function onRestoreAll() {
393 hideNotification();
394 apiHandle.undoAllMostVisitedDeletions();
395 }
396
397 /**
398 * Handles a resize by vertically centering the most visited section
399 * and triggering the tile show/hide animation if necessary.
400 */
401 function onResize() {
402 var clientHeight = document.documentElement.clientHeight;
403 topMarginElement.style['margin-top'] =
Dan Beam 2013/03/22 19:32:50 topMarginElement.style.marginTop
jeremycho 2013/03/22 21:33:30 Done.
404 Math.max(0, (clientHeight - MOST_VISITED_HEIGHT) / 2) + 'px';
405
406 var clientWidth = document.documentElement.clientWidth;
407 var numTilesToShow = Math.floor(
408 (clientWidth - MIN_TOTAL_HORIZONTAL_PADDING) / TILE_WIDTH);
409 numTilesToShow = Math.max(MIN_NUM_TILES_TO_SHOW, numTilesToShow);
410 if (numTilesToShow != numTilesShown) {
411 updateTileVisibility(numTilesToShow);
412 numTilesShown = numTilesToShow;
413 }
414 }
415
Dan Beam 2013/03/22 19:32:50 \n\n or \n
jeremycho 2013/03/22 21:33:30 Done.
416
417 /**
418 * Triggers an animation to show the first numTilesToShow tiles and hide the
419 * remaining.
420 * @param {number} numTilesToShow The number of tiles to show.
421 */
422 function updateTileVisibility(numTilesToShow) {
423 for (var i = 0, length = tiles.length; i < length; ++i) {
424 enable(tiles[i].elem, CLASSES.HIDE_TILE, i >= numTilesToShow);
425 }
426 }
427
428 /**
429 * Returns the tile corresponding to the specified page RID.
430 * @param {number} rid The page RID being looked up.
431 * @return {Tile} The corresponding tile.
432 */
433 function getTileByRid(rid) {
434 for (var i = 0, length = tiles.length; i < length; ++i) {
435 var tile = tiles[i];
436 if (tile.rid == rid)
437 return tile;
438 }
439 return null;
440 }
441
442 /**
443 * Utility function which creates an element with an optional classname and
444 * appends it to the specified parent.
445 * @param {Element} parent The parent to append the new element.
446 * @param {string} name The name of the new element.
447 * @param {string=} opt_class The optional classname of the new element.
448 * @return {Element} The new element.
449 */
450 function createAndAppendElement(parent, name, opt_class) {
451 var child = document.createElement(name);
452 if (opt_class)
453 child.classList.add(opt_class);
454 parent.appendChild(child);
455 return child;
456 }
457
458 /**
459 * Removes a node from its parent.
460 * @param {Node} node The node to remove.
461 */
462 function removeNode(node) {
463 node && node.parentNode && node.parentNode.removeChild(node);
464 }
465
466 /**
467 * Removes all the child nodes on a DOM node.
468 * @param {Node} node Node to remove children from.
469 */
470 function removeChildren(node) {
Dan Beam 2013/03/22 19:32:50 node.innerHTML = '';
jeremycho 2013/03/22 21:33:30 Done.
471 var child;
472 while ((child = node.firstChild)) {
473 node.removeChild(child);
474 }
475 }
476
477 /**
478 * Adds or removes a class depending on the enabled argument.
479 * @param {Element} element DOM node to add or remove the class on.
480 * @param {string} className Class name to add or remove.
481 * @param {boolean} enabled Whether to add or remove the class (true adds,
482 * false removes).
483 */
484 function enable(element, className, enabled) {
Dan Beam 2013/03/22 19:32:50 element.classList.toggle(className, enabled);
jeremycho 2013/03/22 21:33:30 Done.
485 if (enabled)
486 element.classList.add(className);
487 else
488 element.classList.remove(className);
489 }
490
491 /**
492 * @return {Object} the handle to the embeddedSearch API.
493 */
494 function getEmbeddedSearchApiHandle() {
495 if (window.cideb)
496 return window.cideb;
497 if (window.navigator && window.navigator.embeddedSearch)
Dan Beam 2013/03/22 19:32:50 when will navigator not be there?
jeremycho 2013/03/22 21:33:30 Isn't currently supported, but hopefully someday.
498 return window.navigator.embeddedSearch;
499 if (window.chrome && window.chrome.embeddedSearch)
Dan Beam 2013/03/22 19:32:50 when will chrome not be there?
jeremycho 2013/03/22 21:33:30 This shouldn't happen. If it does, other things w
500 return window.chrome.embeddedSearch;
501 return null;
502 }
503
504 /**
505 * Prepares the New Tab Page by adding listeners, rendering the current
506 * theme, and the most visited pages section.
507 */
508 function init() {
509 topMarginElement = document.getElementById(CLASSES.TOP_MARGIN);
Dan Beam 2013/03/22 19:32:50 ^ uh, what? getElementById('class')? also, why are
jeremycho 2013/03/22 21:33:30 AFAIK, because we're in chrome-search://, we can't
510 tilesContainer = document.getElementById(CLASSES.TILES);
511 notification = document.getElementById(CLASSES.NOTIFICATION);
512
513 // TODO(jeremycho): i18n.
514 var notificationMessage = document.getElementById('mv-msg');
515 notificationMessage.innerText = 'Thumbnail removed.';
516 var undoLink = document.getElementById('mv-undo');
517 undoLink.addEventListener('click', onUndo);
518 undoLink.innerText = 'Undo';
519 var restoreAllLink = document.getElementById('mv-restore');
520 restoreAllLink.addEventListener('click', onRestoreAll);
521 restoreAllLink.innerText = 'Restore all';
522 var notificationCloseButton = document.getElementById('mv-notice-x');
523 notificationCloseButton.addEventListener('click', hideNotification);
524
525 window.addEventListener('resize', onResize);
526 onResize();
527
528 var topLevelHandle = getEmbeddedSearchApiHandle();
529 // This is to inform Chrome that the NTP is instant-extended capable.
530 topLevelHandle.searchBox.onsubmit = function() {};
Dan Beam 2013/03/22 19:32:50 ^ er, what is this doing?
jeremycho 2013/03/22 21:33:30 See comment. Without this, Chrome doesn't fire ev
531
532 apiHandle = topLevelHandle.newTabPage;
533 apiHandle.onthemechange = onThemeChange;
534 apiHandle.onmostvisitedchange = onMostVisitedChange;
535
536 onThemeChange();
537 onMostVisitedChange();
538 }
539
540 document.addEventListener('DOMContentLoaded', init);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698