OLD | NEW |
---|---|
(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 /** | |
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 | |
Dan Beam
2013/03/21 02:45:08
1 \s between sentences in comments.
jeremycho
2013/03/21 05:28:42
Done.
| |
79 * the homepage should be updated to support custom or non-custom themes. | |
80 * @type {Array.<string>} | |
81 * @const | |
82 */ | |
83 var WHITE = ['rgba(255,255,255,1)', 'rgba(0,0,0,0)']; | |
Dan Beam
2013/03/21 02:45:08
uh, the second color here is transparent black?
jeremycho
2013/03/21 05:28:42
This gets passed on occasion when a default theme
| |
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} | |
Dan Beam
2013/03/21 02:45:08
@const
jeremycho
2013/03/21 05:28:42
Done.
| |
95 */ | |
96 var MOST_VISITED_HEIGHT = 156; | |
97 | |
98 /** @type {number} @const */ | |
99 var MAX_NUM_TILES_TO_SHOW = 4; | |
100 | |
101 /** @type {number} @const */ | |
102 var MIN_NUM_TILES_TO_SHOW = 2; | |
103 | |
104 /** | |
105 * Minimum total padding to give to the left and right of the most visited | |
106 * section. Used to determine how many tiles to show. | |
107 * @type {number} @const | |
Dan Beam
2013/03/21 02:45:08
@const should be on next line if the comment is mu
jeremycho
2013/03/21 05:28:42
Done.
| |
108 */ | |
109 var MIN_TOTAL_HORIZONTAL_PADDING = 188; | |
110 | |
111 /** @type {string} @const */ | |
112 var TILE_CLASS = 'mv-tile'; | |
113 | |
114 /** | |
115 * Class applied to page tiles. | |
116 * @type {string} @const | |
117 */ | |
118 var PAGE_CLASS = 'mv-page'; | |
119 | |
120 /** | |
121 * Class applied to a page tile's title. | |
122 * @type {string} @const | |
123 */ | |
124 var TITLE_CLASS = 'mv-title'; | |
125 | |
126 /** | |
127 * Class applied to a page tile's thumbnail. | |
128 * @type {string} @const | |
129 */ | |
130 var THUMBNAIL_CLASS = 'mv-thumb'; | |
131 | |
132 /** | |
133 * Class applied to a page tile's domain. | |
134 * @type {string} @const | |
135 */ | |
136 var DOMAIN_CLASS = 'mv-domain'; | |
137 | |
138 /** | |
139 * Class applied to a page tile's blacklist button. | |
140 * @type {string} @const | |
141 */ | |
142 var BLACKLIST_BUTTON_CLASS = 'mv-x'; | |
143 | |
144 /** | |
145 * Class applied to a page tile's favicon. | |
146 * @type {string} @const | |
147 */ | |
148 var FAVICON_CLASS = 'mv-fav'; | |
149 | |
150 /** | |
151 * Class applied to filler tiles. | |
152 * @type {string} @const | |
153 */ | |
154 var FILLER_CLASS = 'mv-filler'; | |
155 | |
156 /** | |
157 * Class applied to tiles to trigger the blacklist animation. | |
158 * @type {string} @const | |
159 */ | |
160 var BLACKLIST_CLASS = 'mv-bl'; | |
161 | |
162 /** | |
163 * Class applied to tiles to hide them on small browser width. | |
164 * @type {string} @const | |
165 */ | |
166 var HIDE_TILE_CLASS = 'mv-tile-hide'; | |
167 | |
168 /** | |
169 * Class applied to hide the blacklist buttons during the blacklist animation. | |
170 * @type {string} @const | |
171 */ | |
172 var HIDE_BLACKLIST_BUTTON_CLASS = 'mv-x-hide'; | |
173 | |
174 /** | |
175 * Class applied to the notification to hide it. | |
176 * @type {string} @const | |
177 */ | |
178 var HIDE_NOTIFICATION_CLASS = 'mv-noti-hide'; | |
Dan Beam
2013/03/21 02:45:08
var CLASSES = {
HIDE_NOTIFICATION: 'mv-noti-hide
jeremycho
2013/03/21 05:28:42
Done.
| |
179 | |
180 /** | |
181 * Time (in milliseconds) to show the notification. | |
182 * @type {number} @const | |
183 */ | |
184 var NOTIFICATION_TIMEOUT = 10000; | |
185 | |
186 /** | |
187 * A Tile is either a rendering of a Most Visited page or "filler" used to | |
188 * pad out the section when not enough pages exist. | |
189 * | |
190 * @param {Element} elem The element for rendering the tile. | |
191 * @param {number=} opt_rid The RID for the corresponding Most Visited page. | |
192 * Should only be left unspecified when creating a filler tile. | |
193 * @constructor | |
194 */ | |
195 Tile = function(elem, opt_rid) { | |
Dan Beam
2013/03/21 02:45:08
s/Tile = function(/function Tile(/
jeremycho
2013/03/21 05:28:42
Done.
| |
196 /** @type {Element} */ | |
197 this.elem = elem; | |
198 | |
199 /** @type {number|undefined} */ | |
200 this.rid = opt_rid; | |
201 }; | |
202 | |
203 /** | |
204 * Updates the NTP based on the current theme. | |
205 * @private | |
206 */ | |
207 function onThemeChange() { | |
208 var info = apiHandle.themeBackgroundInfo; | |
209 if (!info) | |
210 return; | |
211 var background = [info.colorRgba, | |
212 info.imageUrl, | |
213 info.imageTiling, | |
214 info.imageHorizontalAlignment, | |
215 info.imageVerticalAlignment].join(' ').trim(); | |
216 document.body.style.background = background; | |
217 var isCustom = !!background && WHITE.indexOf(background) == -1; | |
218 enable(document.body, 'custom-theme', isCustom); | |
219 } | |
220 | |
221 /** | |
222 * Handles a new set of Most Visited page data. | |
223 */ | |
224 function onMostVisitedChange() { | |
225 var pages = apiHandle.mostVisited; | |
226 | |
227 // If this was called as a result of a blacklist, add a new replacement | |
228 // (possibly filler) tile at the end and trigger the blacklist animation. | |
229 if (isBlacklisting) { | |
230 var replacementTile = createTile(pages[MAX_NUM_TILES_TO_SHOW - 1]); | |
231 | |
232 tiles.push(replacementTile); | |
233 tilesContainer.appendChild(replacementTile.elem); | |
234 | |
235 var lastBlacklistedTileElement = lastBlacklistedTile.elem; | |
236 lastBlacklistedTileElement.addEventListener( | |
237 'webkitTransitionEnd', blacklistAnimationDone); | |
238 lastBlacklistedTileElement.classList.add(BLACKLIST_CLASS); | |
239 // In order to animate the replacement tile sliding into place, it must | |
240 // be made visible. | |
241 updateTileVisibility(numTilesShown + 1); | |
242 | |
243 // If this was called as a result of an undo, re-insert the last blacklisted | |
244 // tile in its old location and trigger the undo animation. | |
245 } else if (isUndoing) { | |
246 tiles.splice( | |
247 lastBlacklistedIndex, 0, lastBlacklistedTile); | |
248 var lastBlacklistedTileElement = lastBlacklistedTile.elem; | |
249 tilesContainer.insertBefore( | |
250 lastBlacklistedTileElement, | |
251 tilesContainer.childNodes[lastBlacklistedIndex]); | |
252 lastBlacklistedTileElement.addEventListener( | |
253 'webkitTransitionEnd', undoAnimationDone); | |
254 // Yield to ensure the tile is added to the DOM before removing the class. | |
255 // Without this, the webkit transition doesn't reliably trigger. | |
256 window.setTimeout(function() { | |
257 lastBlacklistedTileElement.classList.remove(BLACKLIST_CLASS); | |
258 }, 0); | |
259 // Otherwise render the tiles using the new data without animation. | |
260 } else { | |
261 tiles = []; | |
262 for (var i = 0; i < MAX_NUM_TILES_TO_SHOW; ++i) { | |
263 tiles.push(createTile(pages[i])); | |
264 } | |
265 renderTiles(); | |
266 } | |
267 } | |
268 | |
269 /** | |
270 * Renders the current set of tiles without animation. | |
271 */ | |
272 function renderTiles() { | |
273 removeChildren(tilesContainer); | |
274 for (var i = 0, length = tiles.length; i < length; ++i) { | |
275 tilesContainer.appendChild(tiles[i].elem); | |
276 } | |
277 } | |
278 | |
279 /** | |
280 * Creates a Tile with the specified page data. If no data is provided, a | |
281 * filler Tile is created. | |
282 * @param {Object} page The page data. | |
283 * @return {Tile} The new Tile_. | |
284 */ | |
285 function createTile(page) { | |
286 var tileElement = document.createElement('div'); | |
287 tileElement.classList.add(TILE_CLASS); | |
288 | |
289 if (page) { | |
290 var rid = page.rid; | |
291 tileElement.classList.add(PAGE_CLASS); | |
292 | |
293 // The click handler for navigating to the page identified by the RID. | |
294 tileElement.addEventListener('click', function() { | |
295 apiHandle.navigateContentWindow(rid); | |
296 }); | |
297 | |
298 // The shadow DOM which renders the page title. | |
299 var titleElement = page.titleElement; | |
300 if (titleElement) { | |
301 titleElement.classList.add(TITLE_CLASS); | |
302 tileElement.appendChild(titleElement); | |
303 } | |
304 | |
305 // Render the thumbnail if present. Otherwise, fall back to a shadow DOM | |
306 // which renders the domain. | |
307 var thumbnailUrl = page.thumbnailUrl; | |
308 | |
309 var showDomainElement = function() { | |
310 var domainElement = page.domainElement; | |
311 if (domainElement) { | |
312 domainElement.classList.add(DOMAIN_CLASS); | |
313 tileElement.appendChild(domainElement); | |
314 } | |
315 }; | |
316 if (thumbnailUrl) { | |
317 var image = new Image(); | |
318 image.onload = function() { | |
319 var thumbnailElement = createAndAppendElement( | |
320 tileElement, 'div', THUMBNAIL_CLASS); | |
321 thumbnailElement.style['background-image'] = | |
322 'url(' + thumbnailUrl + ')'; | |
323 }; | |
324 | |
325 image.onerror = showDomainElement; | |
326 image.src = thumbnailUrl; | |
327 } else { | |
328 showDomainElement(); | |
329 } | |
330 | |
331 // The button used to blacklist this page. | |
332 var blacklistButton = createAndAppendElement( | |
333 tileElement, 'div', BLACKLIST_BUTTON_CLASS); | |
334 blacklistButton.addEventListener('click', generateBlacklistFunction(rid)); | |
335 // TODO(jeremycho): i18n. | |
336 blacklistButton.title = "Don't show on this page"; | |
337 | |
338 // The page favicon, if any. | |
339 var faviconUrl = page.faviconUrl; | |
340 if (faviconUrl) { | |
341 var favicon = createAndAppendElement( | |
342 tileElement, 'div', FAVICON_CLASS); | |
343 favicon.style['background-image'] = 'url(' + faviconUrl + ')'; | |
344 } | |
345 return new Tile(tileElement, rid); | |
346 } else { | |
347 tileElement.classList.add(FILLER_CLASS); | |
348 return new Tile(tileElement); | |
349 } | |
350 } | |
351 | |
352 /** | |
353 * Generates a function to be called when the page with the corresponding RID | |
354 * is blacklisted. | |
355 * @param {number} rid The RID of the page being blacklisted. | |
356 * @return {function(Event)} A function which handles the blacklisting of the | |
357 * page by displaying the notification, updating state variables, and | |
358 * notifying Chrome. | |
359 */ | |
360 function generateBlacklistFunction(rid) { | |
361 return function(e) { | |
362 // Prevent navigation when the page is being blacklisted. | |
363 e.stopPropagation(); | |
364 | |
365 showNotification(); | |
366 isBlacklisting = true; | |
367 tilesContainer.classList.add(HIDE_BLACKLIST_BUTTON_CLASS); | |
368 lastBlacklistedTile = getTileByRid(rid); | |
369 lastBlacklistedIndex = tiles.indexOf(lastBlacklistedTile); | |
370 apiHandle.deleteMostVisitedItem(rid); | |
371 }; | |
372 } | |
373 | |
374 /** | |
375 * Shows the blacklist notification and refreshes the timer to hide it. | |
376 */ | |
377 function showNotification() { | |
378 notification.classList.remove(HIDE_NOTIFICATION_CLASS); | |
379 if (notificationTimer) | |
380 window.clearTimeout(notificationTimer); | |
381 notificationTimer = window.setTimeout( | |
382 hideNotification, NOTIFICATION_TIMEOUT); | |
383 } | |
384 | |
385 | |
386 /** | |
387 * Hides the blacklist notification. | |
388 */ | |
389 function hideNotification() { | |
390 notification.classList.add(HIDE_NOTIFICATION_CLASS); | |
391 } | |
392 | |
393 /** | |
394 * Handles the end of the blacklist animation by removing the blacklisted tile. | |
395 */ | |
396 function blacklistAnimationDone() { | |
397 tiles.splice(lastBlacklistedIndex, 1); | |
398 removeNode(lastBlacklistedTile.elem); | |
399 updateTileVisibility(numTilesShown); | |
400 isBlacklisting = false; | |
401 tilesContainer.classList.remove(HIDE_BLACKLIST_BUTTON_CLASS); | |
402 lastBlacklistedTile.elem.removeEventListener( | |
403 'webkitTransitionEnd', blacklistAnimationDone); | |
404 } | |
405 | |
406 /** | |
407 * Handles a click on the notification undo link by hiding the notification and | |
408 * informing Chrome. | |
409 */ | |
410 function onUndo() { | |
411 hideNotification(); | |
412 var lastBlacklistedRID = lastBlacklistedTile.rid; | |
413 if (lastBlacklistedRID != null) { | |
414 isUndoing = true; | |
415 apiHandle.undoMostVisitedDeletion(lastBlacklistedRID); | |
416 } | |
417 } | |
418 | |
419 /** | |
420 * Handles the end of the undo animation by removing the extraneous end tile. | |
421 */ | |
422 function undoAnimationDone() { | |
423 isUndoing = false; | |
424 tiles.splice(tiles.length - 1, 1); | |
425 removeNode(tilesContainer.lastElementChild); | |
426 updateTileVisibility(numTilesShown); | |
427 lastBlacklistedTile.elem.removeEventListener( | |
428 'webkitTransitionEnd', undoAnimationDone); | |
429 } | |
430 | |
431 /** | |
432 * Handles a click on the restore all notification link by hiding the | |
433 * notification and informing Chrome. | |
434 */ | |
435 function onRestoreAll() { | |
436 hideNotification(); | |
437 apiHandle.undoAllMostVisitedDeletions(); | |
438 } | |
439 | |
440 /** | |
441 * Handles a resize by vertically centering the most visited section | |
442 * and triggering the tile show/hide animation if necessary. | |
443 */ | |
444 function onResize() { | |
445 var clientHeight = document.documentElement.clientHeight; | |
446 topMarginElement.style['margin-top'] = | |
447 Math.max(0, (clientHeight - MOST_VISITED_HEIGHT) / 2) + 'px'; | |
448 | |
449 var clientWidth = document.documentElement.clientWidth; | |
450 var numTilesToShow = Math.floor( | |
451 (clientWidth - MIN_TOTAL_HORIZONTAL_PADDING) / TILE_WIDTH); | |
452 numTilesToShow = Math.max(MIN_NUM_TILES_TO_SHOW, numTilesToShow); | |
453 if (numTilesToShow != numTilesShown) { | |
454 updateTileVisibility(numTilesToShow); | |
455 numTilesShown = numTilesToShow; | |
456 } | |
457 } | |
458 | |
459 | |
460 /** | |
461 * Triggers an animation to show the first numTilesToShow tiles and hide the | |
462 * remaining. | |
463 * @param {number} numTilesToShow The number of tiles to show. | |
464 */ | |
465 function updateTileVisibility(numTilesToShow) { | |
466 for (var i = 0, length = tiles.length; i < length; ++i) { | |
467 enable(tiles[i].elem, HIDE_TILE_CLASS, i >= numTilesToShow); | |
468 } | |
469 } | |
470 | |
471 /** | |
472 * Returns the tile corresponding to the specified page RID. | |
473 * @param {number} rid The page RID being looked up. | |
474 * @return {Tile} The corresponding tile. | |
475 */ | |
476 function getTileByRid(rid) { | |
477 for (var i = 0, length = tiles.length; i < length; ++i) { | |
478 var tile = tiles[i]; | |
479 if (tile.rid == rid) | |
480 return tile; | |
481 } | |
482 return null; | |
483 } | |
484 | |
485 /** | |
486 * Utility function which creates an element with an optional classname and | |
487 * appends it to the specified parent. | |
488 * @param {Element} parent The parent to append the new element. | |
489 * @param {string} name The name of the new element. | |
490 * @param {string=} opt_class The optional classname of the new element. | |
491 * @return {Element} The new element. | |
492 */ | |
493 function createAndAppendElement(parent, name, opt_class) { | |
494 var child = document.createElement(name); | |
495 if (opt_class) | |
496 child.classList.add(opt_class); | |
497 parent.appendChild(child); | |
498 return child; | |
499 } | |
500 | |
501 /** | |
502 * Removes a node from its parent. | |
503 * @param {Node} node The node to remove. | |
504 */ | |
505 function removeNode(node) { | |
506 node && node.parentNode && node.parentNode.removeChild(node); | |
507 } | |
508 | |
509 /** | |
510 * Removes all the child nodes on a DOM node. | |
511 * @param {Node} node Node to remove children from. | |
512 */ | |
513 function removeChildren(node) { | |
514 var child; | |
515 while ((child = node.firstChild)) { | |
516 node.removeChild(child); | |
517 } | |
518 } | |
519 | |
520 /** | |
521 * Adds or removes a class depending on the enabled argument. | |
522 * @param {Element} element DOM node to add or remove the class on. | |
523 * @param {string} className Class name to add or remove. | |
524 * @param {boolean} enabled Whether to add or remove the class (true adds, | |
525 * false removes). | |
526 */ | |
527 function enable(element, className, enabled) { | |
528 if (enabled) | |
529 element.classList.add(className); | |
530 else | |
531 element.classList.remove(className); | |
532 } | |
533 | |
534 /** | |
535 * @return {Object} the handle to the embeddedSearch API. | |
536 */ | |
537 function getEmbeddedSearchApiHandle() { | |
538 if (window.cideb) | |
539 return window.cideb; | |
540 if (window.navigator && window.navigator.embeddedSearch) | |
541 return window.navigator.embeddedSearch; | |
542 if (window.chrome && window.chrome.embeddedSearch) | |
543 return window.chrome.embeddedSearch; | |
544 return null; | |
545 } | |
546 | |
547 /** | |
548 * Prepares the New Tab Page by adding listeners, rendering the current | |
549 * theme, and the most visited pages section. | |
550 */ | |
551 function init() { | |
552 topMarginElement = document.getElementById('top-margin'); | |
553 tilesContainer = document.getElementById('mv-tiles'); | |
554 notification = document.getElementById('mv-noti'); | |
555 | |
556 // TODO(jeremycho): i18n. | |
557 var notificationMessage = document.getElementById('mv-msg'); | |
558 notificationMessage.innerText = 'Thumbnail removed.'; | |
559 var undoLink = document.getElementById('mv-undo'); | |
560 undoLink.addEventListener('click', onUndo); | |
561 undoLink.innerText = 'Undo'; | |
562 var restoreAllLink = document.getElementById('mv-restore'); | |
563 restoreAllLink.addEventListener('click', onRestoreAll); | |
564 restoreAllLink.innerText = 'Restore all'; | |
565 var notificationCloseButton = document.getElementById('mv-noti-x'); | |
566 notificationCloseButton.addEventListener('click', hideNotification); | |
567 | |
568 window.addEventListener('resize', onResize); | |
569 onResize(); | |
570 | |
571 var topLevelHandle = getEmbeddedSearchApiHandle(); | |
572 // This is to inform Chrome that the NTP is instant-extended capable. | |
573 topLevelHandle.searchBox.onsubmit = function() {}; | |
574 | |
575 apiHandle = topLevelHandle.newTabPage; | |
576 apiHandle.onthemechange = onThemeChange; | |
577 apiHandle.onmostvisitedchange = onMostVisitedChange; | |
578 | |
579 onThemeChange(); | |
580 onMostVisitedChange(); | |
581 } | |
582 | |
583 document.addEventListener('DOMContentLoaded', init); | |
OLD | NEW |