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