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

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

Powered by Google App Engine
This is Rietveld 408576698