Home | History | Annotate | Download | only in ntp4
      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