1 // Copyright (c) 2012 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 cr.define('ntp', function() { 6 'use strict'; 7 8 var TilePage = ntp.TilePage; 9 10 /** 11 * A counter for generating unique tile IDs. 12 */ 13 var tileID = 0; 14 15 /** 16 * Creates a new Most Visited object for tiling. 17 * @constructor 18 * @extends {HTMLAnchorElement} 19 */ 20 function MostVisited() { 21 var el = cr.doc.createElement('a'); 22 el.__proto__ = MostVisited.prototype; 23 el.initialize(); 24 25 return el; 26 } 27 28 MostVisited.prototype = { 29 __proto__: HTMLAnchorElement.prototype, 30 31 initialize: function() { 32 this.reset(); 33 34 this.addEventListener('click', this.handleClick_); 35 this.addEventListener('keydown', this.handleKeyDown_); 36 this.addEventListener('mouseover', this.handleMouseOver_); 37 }, 38 39 get index() { 40 assert(this.tile); 41 return this.tile.index; 42 }, 43 44 get data() { 45 return this.data_; 46 }, 47 48 /** 49 * Clears the DOM hierarchy for this node, setting it back to the default 50 * for a blank thumbnail. 51 */ 52 reset: function() { 53 this.className = 'most-visited filler real'; 54 this.innerHTML = 55 '<span class="thumbnail-wrapper fills-parent">' + 56 '<div class="close-button"></div>' + 57 '<span class="thumbnail fills-parent">' + 58 // thumbnail-shield provides a gradient fade effect. 59 '<div class="thumbnail-shield fills-parent"></div>' + 60 '</span>' + 61 '<span class="favicon"></span>' + 62 '</span>' + 63 '<div class="color-stripe"></div>' + 64 '<span class="title"></span>'; 65 66 this.querySelector('.close-button').title = 67 loadTimeData.getString('removethumbnailtooltip'); 68 69 this.tabIndex = -1; 70 this.data_ = null; 71 this.removeAttribute('id'); 72 this.title = ''; 73 }, 74 75 /** 76 * Update the appearance of this tile according to |data|. 77 * @param {Object} data A dictionary of relevant data for the page. 78 */ 79 updateForData: function(data) { 80 if (this.classList.contains('blacklisted') && data) { 81 // Animate appearance of new tile. 82 this.classList.add('new-tile-contents'); 83 } 84 this.classList.remove('blacklisted'); 85 86 if (!data || data.filler) { 87 if (this.data_) 88 this.reset(); 89 return; 90 } 91 92 var id = tileID++; 93 this.id = 'most-visited-tile-' + id; 94 this.data_ = data; 95 this.classList.add('focusable'); 96 97 var faviconDiv = this.querySelector('.favicon'); 98 faviconDiv.style.backgroundImage = getFaviconImageSet(data.url); 99 100 // The favicon should have the same dominant color regardless of the 101 // device pixel ratio the favicon is requested for. 102 chrome.send('getFaviconDominantColor', 103 [getFaviconUrlForCurrentDevicePixelRatio(data.url), this.id]); 104 105 var title = this.querySelector('.title'); 106 title.textContent = data.title; 107 title.dir = data.direction; 108 109 // Sets the tooltip. 110 this.title = data.title; 111 112 var thumbnailUrl = 'chrome://thumb/' + data.url; 113 this.querySelector('.thumbnail').style.backgroundImage = 114 url(thumbnailUrl); 115 116 this.href = data.url; 117 118 this.classList.remove('filler'); 119 }, 120 121 /** 122 * Sets the color of the favicon dominant color bar. 123 * @param {string} color The css-parsable value for the color. 124 */ 125 set stripeColor(color) { 126 this.querySelector('.color-stripe').style.backgroundColor = color; 127 }, 128 129 /** 130 * Handles a click on the tile. 131 * @param {Event} e The click event. 132 */ 133 handleClick_: function(e) { 134 if (e.target.classList.contains('close-button')) { 135 this.blacklist_(); 136 e.preventDefault(); 137 } else { 138 ntp.logTimeToClick('MostVisited'); 139 // Records an app launch from the most visited page (Chrome will decide 140 // whether the url is an app). TODO(estade): this only works for clicks; 141 // other actions like "open in new tab" from the context menu won't be 142 // recorded. Can this be fixed? 143 chrome.send('recordAppLaunchByURL', 144 [encodeURIComponent(this.href), 145 ntp.APP_LAUNCH.NTP_MOST_VISITED]); 146 // Records the index of this tile. 147 chrome.send('metricsHandler:recordInHistogram', 148 ['NewTabPage.MostVisited', this.index, 8]); 149 chrome.send('mostVisitedAction', 150 [ntp.NtpFollowAction.CLICKED_TILE]); 151 } 152 }, 153 154 /** 155 * Allow blacklisting most visited site using the keyboard. 156 */ 157 handleKeyDown_: function(e) { 158 if (!cr.isMac && e.keyCode == 46 || // Del 159 cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace 160 this.blacklist_(); 161 } 162 }, 163 164 /** 165 * The mouse has entered a Most Visited tile div. Only log the first 166 * mouseover event. By doing this we solve the issue with the mouseover 167 * event listener that bubbles up to the parent, which would cause it to 168 * fire multiple times even if the mouse stays within one tile. 169 */ 170 handleMouseOver_: function(e) { 171 var self = this; 172 var ancestor = findAncestor(e.relatedTarget, function(node) { 173 return node == self; 174 }); 175 // If ancestor is null, mouse is entering the parent element. 176 if (ancestor == null) 177 chrome.send('metricsHandler:logMouseover'); 178 }, 179 180 /** 181 * Permanently removes a page from Most Visited. 182 */ 183 blacklist_: function() { 184 this.showUndoNotification_(); 185 chrome.send('blacklistURLFromMostVisited', [this.data_.url]); 186 this.reset(); 187 chrome.send('getMostVisited'); 188 this.classList.add('blacklisted'); 189 }, 190 191 showUndoNotification_: function() { 192 var data = this.data_; 193 var self = this; 194 var doUndo = function() { 195 chrome.send('removeURLsFromMostVisitedBlacklist', [data.url]); 196 self.updateForData(data); 197 } 198 199 var undo = { 200 action: doUndo, 201 text: loadTimeData.getString('undothumbnailremove'), 202 }; 203 204 var undoAll = { 205 action: function() { 206 chrome.send('clearMostVisitedURLsBlacklist'); 207 }, 208 text: loadTimeData.getString('restoreThumbnailsShort'), 209 }; 210 211 ntp.showNotification( 212 loadTimeData.getString('thumbnailremovednotification'), 213 [undo, undoAll]); 214 }, 215 216 /** 217 * Set the size and position of the most visited tile. 218 * @param {number} size The total size of |this|. 219 * @param {number} x The x-position. 220 * @param {number} y The y-position. 221 * animate. 222 */ 223 setBounds: function(size, x, y) { 224 this.style.width = toCssPx(size); 225 this.style.height = toCssPx(heightForWidth(size)); 226 227 this.style.left = toCssPx(x); 228 this.style.right = toCssPx(x); 229 this.style.top = toCssPx(y); 230 }, 231 232 /** 233 * Returns whether this element can be 'removed' from chrome (i.e. whether 234 * the user can drag it onto the trash and expect something to happen). 235 * @return {boolean} True, since most visited pages can always be 236 * blacklisted. 237 */ 238 canBeRemoved: function() { 239 return true; 240 }, 241 242 /** 243 * Removes this element from chrome, i.e. blacklists it. 244 */ 245 removeFromChrome: function() { 246 this.blacklist_(); 247 this.parentNode.classList.add('finishing-drag'); 248 }, 249 250 /** 251 * Called when a drag of this tile has ended (after all animations have 252 * finished). 253 */ 254 finalizeDrag: function() { 255 this.parentNode.classList.remove('finishing-drag'); 256 }, 257 258 /** 259 * Called when a drag is starting on the tile. Updates dataTransfer with 260 * data for this tile (for dragging outside of the NTP). 261 */ 262 setDragData: function(dataTransfer) { 263 dataTransfer.setData('Text', this.data_.title); 264 dataTransfer.setData('URL', this.data_.url); 265 }, 266 }; 267 268 var mostVisitedPageGridValues = { 269 // The fewest tiles we will show in a row. 270 minColCount: 2, 271 // The most tiles we will show in a row. 272 maxColCount: 4, 273 274 // The smallest a tile can be. 275 minTileWidth: 122, 276 // The biggest a tile can be. 212 (max thumbnail width) + 2. 277 maxTileWidth: 214, 278 279 // The padding between tiles, as a fraction of the tile width. 280 tileSpacingFraction: 1 / 8, 281 }; 282 TilePage.initGridValues(mostVisitedPageGridValues); 283 284 /** 285 * Calculates the height for a Most Visited tile for a given width. The size 286 * is based on the thumbnail, which should have a 212:132 ratio. 287 * @return {number} The height. 288 */ 289 function heightForWidth(width) { 290 // The 2s are for borders, the 31 is for the title. 291 return (width - 2) * 132 / 212 + 2 + 31; 292 } 293 294 var THUMBNAIL_COUNT = 8; 295 296 /** 297 * Creates a new MostVisitedPage object. 298 * @constructor 299 * @extends {TilePage} 300 */ 301 function MostVisitedPage() { 302 var el = new TilePage(mostVisitedPageGridValues); 303 el.__proto__ = MostVisitedPage.prototype; 304 el.initialize(); 305 306 return el; 307 } 308 309 MostVisitedPage.prototype = { 310 __proto__: TilePage.prototype, 311 312 initialize: function() { 313 this.classList.add('most-visited-page'); 314 this.data_ = null; 315 this.mostVisitedTiles_ = this.getElementsByClassName('most-visited real'); 316 317 this.addEventListener('carddeselected', this.handleCardDeselected_); 318 this.addEventListener('cardselected', this.handleCardSelected_); 319 }, 320 321 /** 322 * Create blank (filler) tiles. 323 * @private 324 */ 325 createTiles_: function() { 326 for (var i = 0; i < THUMBNAIL_COUNT; i++) { 327 this.appendTile(new MostVisited()); 328 } 329 }, 330 331 /** 332 * Update the tiles after a change to |data_|. 333 */ 334 updateTiles_: function() { 335 for (var i = 0; i < THUMBNAIL_COUNT; i++) { 336 var page = this.data_[i]; 337 var tile = this.mostVisitedTiles_[i]; 338 339 if (i >= this.data_.length) 340 tile.reset(); 341 else 342 tile.updateForData(page); 343 } 344 }, 345 346 /** 347 * Handles the 'card deselected' event (i.e. the user clicked to another 348 * pane). 349 * @param {Event} e The CardChanged event. 350 */ 351 handleCardDeselected_: function(e) { 352 if (!document.documentElement.classList.contains('starting-up')) { 353 chrome.send('mostVisitedAction', 354 [ntp.NtpFollowAction.CLICKED_OTHER_NTP_PANE]); 355 } 356 }, 357 358 /** 359 * Handles the 'card selected' event (i.e. the user clicked to select the 360 * Most Visited pane). 361 * @param {Event} e The CardChanged event. 362 */ 363 handleCardSelected_: function(e) { 364 if (!document.documentElement.classList.contains('starting-up')) 365 chrome.send('mostVisitedSelected'); 366 }, 367 368 /** 369 * Array of most visited data objects. 370 * @type {Array} 371 */ 372 get data() { 373 return this.data_; 374 }, 375 set data(data) { 376 var startTime = Date.now(); 377 378 // The first time data is set, create the tiles. 379 if (!this.data_) { 380 this.createTiles_(); 381 this.data_ = data.slice(0, THUMBNAIL_COUNT); 382 } else { 383 this.data_ = refreshData(this.data_, data); 384 } 385 386 this.updateTiles_(); 387 this.updateFocusableElement(); 388 logEvent('mostVisited.layout: ' + (Date.now() - startTime)); 389 }, 390 391 /** @override */ 392 shouldAcceptDrag: function(e) { 393 return false; 394 }, 395 396 /** @override */ 397 heightForWidth: heightForWidth, 398 }; 399 400 /** 401 * Executed once the NTP has loaded. Checks if the Most Visited pane is 402 * shown or not. If it is shown, the 'mostVisitedSelected' message is sent 403 * to the C++ code, to record the fact that the user has seen this pane. 404 */ 405 MostVisitedPage.onLoaded = function() { 406 if (ntp.getCardSlider() && 407 ntp.getCardSlider().currentCardValue && 408 ntp.getCardSlider().currentCardValue.classList 409 .contains('most-visited-page')) { 410 chrome.send('mostVisitedSelected'); 411 } 412 } 413 414 /** 415 * We've gotten additional Most Visited data. Update our old data with the 416 * new data. The ordering of the new data is not important, except when a 417 * page is pinned. Thus we try to minimize re-ordering. 418 * @param {Array} oldData The current Most Visited page list. 419 * @param {Array} newData The new Most Visited page list. 420 * @return {Array} The merged page list that should replace the current page 421 * list. 422 */ 423 function refreshData(oldData, newData) { 424 oldData = oldData.slice(0, THUMBNAIL_COUNT); 425 newData = newData.slice(0, THUMBNAIL_COUNT); 426 427 // Copy over pinned sites directly. 428 for (var j = 0; j < newData.length; j++) { 429 if (newData[j].pinned) { 430 oldData[j] = newData[j]; 431 // Mark the entry as 'updated' so we don't try to update again. 432 oldData[j].updated = true; 433 // Mark the newData page as 'used' so we don't try to re-use it. 434 newData[j].used = true; 435 } 436 } 437 438 // Look through old pages; if they exist in the newData list, keep them 439 // where they are. 440 for (var i = 0; i < oldData.length; i++) { 441 if (!oldData[i] || oldData[i].updated) 442 continue; 443 444 for (var j = 0; j < newData.length; j++) { 445 if (newData[j].used) 446 continue; 447 448 if (newData[j].url == oldData[i].url) { 449 // The background image and other data may have changed. 450 oldData[i] = newData[j]; 451 oldData[i].updated = true; 452 newData[j].used = true; 453 break; 454 } 455 } 456 } 457 458 // Look through old pages that haven't been updated yet; replace them. 459 for (var i = 0; i < oldData.length; i++) { 460 if (oldData[i] && oldData[i].updated) 461 continue; 462 463 for (var j = 0; j < newData.length; j++) { 464 if (newData[j].used) 465 continue; 466 467 oldData[i] = newData[j]; 468 oldData[i].updated = true; 469 newData[j].used = true; 470 break; 471 } 472 473 if (oldData[i] && !oldData[i].updated) 474 oldData[i] = null; 475 } 476 477 // Clear 'updated' flags so this function will work next time it's called. 478 for (var i = 0; i < THUMBNAIL_COUNT; i++) { 479 if (oldData[i]) 480 oldData[i].updated = false; 481 } 482 483 return oldData; 484 }; 485 486 return { 487 MostVisitedPage: MostVisitedPage, 488 refreshData: refreshData, 489 }; 490 }); 491 492 document.addEventListener('ntpLoaded', ntp.MostVisitedPage.onLoaded); 493