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 (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 })(); | |
OLD | NEW |