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         // Records the action. This will be available as a time-stamped stream
    150         // server-side and can be used to compute time-to-long-dwell.
    151         chrome.send('metricsHandler:recordAction', ['MostVisited_Clicked']);
    152         chrome.send('mostVisitedAction',
    153                     [ntp.NtpFollowAction.CLICKED_TILE]);
    154       }
    155     },
    156 
    157     /**
    158      * Allow blacklisting most visited site using the keyboard.
    159      */
    160     handleKeyDown_: function(e) {
    161       if (!cr.isMac && e.keyCode == 46 || // Del
    162           cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
    163         this.blacklist_();
    164       }
    165     },
    166 
    167     /**
    168      * The mouse has entered a Most Visited tile div. Only log the first
    169      * mouseover event. By doing this we solve the issue with the mouseover
    170      * event listener that bubbles up to the parent, which would cause it to
    171      * fire multiple times even if the mouse stays within one tile.
    172      */
    173     handleMouseOver_: function(e) {
    174       var self = this;
    175       var ancestor = findAncestor(e.relatedTarget, function(node) {
    176         return node == self;
    177       });
    178       // If ancestor is null, mouse is entering the parent element.
    179       if (ancestor == null)
    180         chrome.send('metricsHandler:logMouseover');
    181     },
    182 
    183     /**
    184      * Permanently removes a page from Most Visited.
    185      */
    186     blacklist_: function() {
    187       this.showUndoNotification_();
    188       chrome.send('blacklistURLFromMostVisited', [this.data_.url]);
    189       this.reset();
    190       chrome.send('getMostVisited');
    191       this.classList.add('blacklisted');
    192     },
    193 
    194     showUndoNotification_: function() {
    195       var data = this.data_;
    196       var self = this;
    197       var doUndo = function() {
    198         chrome.send('removeURLsFromMostVisitedBlacklist', [data.url]);
    199         self.updateForData(data);
    200       }
    201 
    202       var undo = {
    203         action: doUndo,
    204         text: loadTimeData.getString('undothumbnailremove'),
    205       };
    206 
    207       var undoAll = {
    208         action: function() {
    209           chrome.send('clearMostVisitedURLsBlacklist');
    210         },
    211         text: loadTimeData.getString('restoreThumbnailsShort'),
    212       };
    213 
    214       ntp.showNotification(
    215           loadTimeData.getString('thumbnailremovednotification'),
    216           [undo, undoAll]);
    217     },
    218 
    219     /**
    220      * Set the size and position of the most visited tile.
    221      * @param {number} size The total size of |this|.
    222      * @param {number} x The x-position.
    223      * @param {number} y The y-position.
    224      *     animate.
    225      */
    226     setBounds: function(size, x, y) {
    227       this.style.width = toCssPx(size);
    228       this.style.height = toCssPx(heightForWidth(size));
    229 
    230       this.style.left = toCssPx(x);
    231       this.style.right = toCssPx(x);
    232       this.style.top = toCssPx(y);
    233     },
    234 
    235     /**
    236      * Returns whether this element can be 'removed' from chrome (i.e. whether
    237      * the user can drag it onto the trash and expect something to happen).
    238      * @return {boolean} True, since most visited pages can always be
    239      *     blacklisted.
    240      */
    241     canBeRemoved: function() {
    242       return true;
    243     },
    244 
    245     /**
    246      * Removes this element from chrome, i.e. blacklists it.
    247      */
    248     removeFromChrome: function() {
    249       this.blacklist_();
    250       this.parentNode.classList.add('finishing-drag');
    251     },
    252 
    253     /**
    254      * Called when a drag of this tile has ended (after all animations have
    255      * finished).
    256      */
    257     finalizeDrag: function() {
    258       this.parentNode.classList.remove('finishing-drag');
    259     },
    260 
    261     /**
    262      * Called when a drag is starting on the tile. Updates dataTransfer with
    263      * data for this tile (for dragging outside of the NTP).
    264      */
    265     setDragData: function(dataTransfer) {
    266       dataTransfer.setData('Text', this.data_.title);
    267       dataTransfer.setData('URL', this.data_.url);
    268     },
    269   };
    270 
    271   var mostVisitedPageGridValues = {
    272     // The fewest tiles we will show in a row.
    273     minColCount: 2,
    274     // The most tiles we will show in a row.
    275     maxColCount: 4,
    276 
    277     // The smallest a tile can be.
    278     minTileWidth: 122,
    279     // The biggest a tile can be. 212 (max thumbnail width) + 2.
    280     maxTileWidth: 214,
    281 
    282     // The padding between tiles, as a fraction of the tile width.
    283     tileSpacingFraction: 1 / 8,
    284   };
    285   TilePage.initGridValues(mostVisitedPageGridValues);
    286 
    287   /**
    288    * Calculates the height for a Most Visited tile for a given width. The size
    289    * is based on the thumbnail, which should have a 212:132 ratio.
    290    * @return {number} The height.
    291    */
    292   function heightForWidth(width) {
    293     // The 2s are for borders, the 31 is for the title.
    294     return (width - 2) * 132 / 212 + 2 + 31;
    295   }
    296 
    297   var THUMBNAIL_COUNT = 8;
    298 
    299   /**
    300    * Creates a new MostVisitedPage object.
    301    * @constructor
    302    * @extends {TilePage}
    303    */
    304   function MostVisitedPage() {
    305     var el = new TilePage(mostVisitedPageGridValues);
    306     el.__proto__ = MostVisitedPage.prototype;
    307     el.initialize();
    308 
    309     return el;
    310   }
    311 
    312   MostVisitedPage.prototype = {
    313     __proto__: TilePage.prototype,
    314 
    315     initialize: function() {
    316       this.classList.add('most-visited-page');
    317       this.data_ = null;
    318       this.mostVisitedTiles_ = this.getElementsByClassName('most-visited real');
    319 
    320       this.addEventListener('carddeselected', this.handleCardDeselected_);
    321       this.addEventListener('cardselected', this.handleCardSelected_);
    322     },
    323 
    324     /**
    325      * Create blank (filler) tiles.
    326      * @private
    327      */
    328     createTiles_: function() {
    329       for (var i = 0; i < THUMBNAIL_COUNT; i++) {
    330         this.appendTile(new MostVisited());
    331       }
    332     },
    333 
    334     /**
    335      * Update the tiles after a change to |data_|.
    336      */
    337     updateTiles_: function() {
    338       for (var i = 0; i < THUMBNAIL_COUNT; i++) {
    339         var page = this.data_[i];
    340         var tile = this.mostVisitedTiles_[i];
    341 
    342         if (i >= this.data_.length)
    343           tile.reset();
    344         else
    345           tile.updateForData(page);
    346       }
    347     },
    348 
    349     /**
    350      * Handles the 'card deselected' event (i.e. the user clicked to another
    351      * pane).
    352      * @param {Event} e The CardChanged event.
    353      */
    354     handleCardDeselected_: function(e) {
    355       if (!document.documentElement.classList.contains('starting-up')) {
    356         chrome.send('mostVisitedAction',
    357                     [ntp.NtpFollowAction.CLICKED_OTHER_NTP_PANE]);
    358       }
    359     },
    360 
    361     /**
    362      * Handles the 'card selected' event (i.e. the user clicked to select the
    363      * Most Visited pane).
    364      * @param {Event} e The CardChanged event.
    365      */
    366     handleCardSelected_: function(e) {
    367       if (!document.documentElement.classList.contains('starting-up'))
    368         chrome.send('mostVisitedSelected');
    369     },
    370 
    371     /**
    372      * Array of most visited data objects.
    373      * @type {Array}
    374      */
    375     get data() {
    376       return this.data_;
    377     },
    378     set data(data) {
    379       var startTime = Date.now();
    380 
    381       // The first time data is set, create the tiles.
    382       if (!this.data_) {
    383         this.createTiles_();
    384         this.data_ = data.slice(0, THUMBNAIL_COUNT);
    385       } else {
    386         this.data_ = refreshData(this.data_, data);
    387       }
    388 
    389       this.updateTiles_();
    390       this.updateFocusableElement();
    391       logEvent('mostVisited.layout: ' + (Date.now() - startTime));
    392     },
    393 
    394     /** @override */
    395     shouldAcceptDrag: function(e) {
    396       return false;
    397     },
    398 
    399     /** @override */
    400     heightForWidth: heightForWidth,
    401   };
    402 
    403   /**
    404    * Executed once the NTP has loaded. Checks if the Most Visited pane is
    405    * shown or not. If it is shown, the 'mostVisitedSelected' message is sent
    406    * to the C++ code, to record the fact that the user has seen this pane.
    407    */
    408   MostVisitedPage.onLoaded = function() {
    409     if (ntp.getCardSlider() &&
    410         ntp.getCardSlider().currentCardValue &&
    411         ntp.getCardSlider().currentCardValue.classList
    412         .contains('most-visited-page')) {
    413       chrome.send('mostVisitedSelected');
    414     }
    415   }
    416 
    417   /**
    418    * We've gotten additional Most Visited data. Update our old data with the
    419    * new data. The ordering of the new data is not important, except when a
    420    * page is pinned. Thus we try to minimize re-ordering.
    421    * @param {Array} oldData The current Most Visited page list.
    422    * @param {Array} newData The new Most Visited page list.
    423    * @return {Array} The merged page list that should replace the current page
    424    *     list.
    425    */
    426   function refreshData(oldData, newData) {
    427     oldData = oldData.slice(0, THUMBNAIL_COUNT);
    428     newData = newData.slice(0, THUMBNAIL_COUNT);
    429 
    430     // Copy over pinned sites directly.
    431     for (var j = 0; j < newData.length; j++) {
    432       if (newData[j].pinned) {
    433         oldData[j] = newData[j];
    434         // Mark the entry as 'updated' so we don't try to update again.
    435         oldData[j].updated = true;
    436         // Mark the newData page as 'used' so we don't try to re-use it.
    437         newData[j].used = true;
    438       }
    439     }
    440 
    441     // Look through old pages; if they exist in the newData list, keep them
    442     // where they are.
    443     for (var i = 0; i < oldData.length; i++) {
    444       if (!oldData[i] || oldData[i].updated)
    445         continue;
    446 
    447       for (var j = 0; j < newData.length; j++) {
    448         if (newData[j].used)
    449           continue;
    450 
    451         if (newData[j].url == oldData[i].url) {
    452           // The background image and other data may have changed.
    453           oldData[i] = newData[j];
    454           oldData[i].updated = true;
    455           newData[j].used = true;
    456           break;
    457         }
    458       }
    459     }
    460 
    461     // Look through old pages that haven't been updated yet; replace them.
    462     for (var i = 0; i < oldData.length; i++) {
    463       if (oldData[i] && oldData[i].updated)
    464         continue;
    465 
    466       for (var j = 0; j < newData.length; j++) {
    467         if (newData[j].used)
    468           continue;
    469 
    470         oldData[i] = newData[j];
    471         oldData[i].updated = true;
    472         newData[j].used = true;
    473         break;
    474       }
    475 
    476       if (oldData[i] && !oldData[i].updated)
    477         oldData[i] = null;
    478     }
    479 
    480     // Clear 'updated' flags so this function will work next time it's called.
    481     for (var i = 0; i < THUMBNAIL_COUNT; i++) {
    482       if (oldData[i])
    483         oldData[i].updated = false;
    484     }
    485 
    486     return oldData;
    487   };
    488 
    489   return {
    490     MostVisitedPage: MostVisitedPage,
    491     refreshData: refreshData,
    492   };
    493 });
    494 
    495 document.addEventListener('ntpLoaded', ntp.MostVisitedPage.onLoaded);
    496