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 /**
      6  * @fileoverview The menu that shows tabs from sessions on other devices.
      7  */
      8 
      9 cr.define('ntp', function() {
     10   'use strict';
     11 
     12   /** @const */ var ContextMenuButton = cr.ui.ContextMenuButton;
     13   /** @const */ var Menu = cr.ui.Menu;
     14   /** @const */ var MenuItem = cr.ui.MenuItem;
     15   /** @const */ var MenuButton = cr.ui.MenuButton;
     16   /** @const */ var OtherSessionsMenuButton = cr.ui.define('button');
     17 
     18   // Histogram buckets for UMA tracking of menu usage.
     19   /** @const */ var HISTOGRAM_EVENT = {
     20       INITIALIZED: 0,
     21       SHOW_MENU: 1,
     22       LINK_CLICKED: 2,
     23       LINK_RIGHT_CLICKED: 3,
     24       SESSION_NAME_RIGHT_CLICKED: 4,
     25       SHOW_SESSION_MENU: 5,
     26       COLLAPSE_SESSION: 6,
     27       EXPAND_SESSION: 7,
     28       OPEN_ALL: 8
     29   };
     30   /** @const */ var HISTOGRAM_EVENT_LIMIT =
     31       HISTOGRAM_EVENT.OPEN_ALL + 1;
     32 
     33   /**
     34    * Record an event in the UMA histogram.
     35    * @param {number} eventId The id of the event to be recorded.
     36    * @private
     37    */
     38   function recordUmaEvent_(eventId) {
     39     chrome.send('metricsHandler:recordInHistogram',
     40         ['NewTabPage.OtherSessionsMenu', eventId, HISTOGRAM_EVENT_LIMIT]);
     41   }
     42 
     43   OtherSessionsMenuButton.prototype = {
     44     __proto__: MenuButton.prototype,
     45 
     46     decorate: function() {
     47       MenuButton.prototype.decorate.call(this);
     48       this.menu = new Menu;
     49       cr.ui.decorate(this.menu, Menu);
     50       this.menu.menuItemSelector = '[role=menuitem]';
     51       this.menu.classList.add('footer-menu');
     52       this.menu.addEventListener('contextmenu',
     53                                  this.onContextMenu_.bind(this), true);
     54       document.body.appendChild(this.menu);
     55 
     56       // Create the context menu that appears when the user right clicks
     57       // on a device name.
     58       this.deviceContextMenu_ = DeviceContextMenuController.getInstance().menu;
     59       document.body.appendChild(this.deviceContextMenu_);
     60 
     61       this.promoMessage_ = $('other-sessions-promo-template').cloneNode(true);
     62       this.promoMessage_.removeAttribute('id');  // Prevent a duplicate id.
     63 
     64       this.sessions_ = [];
     65       this.anchorType = cr.ui.AnchorType.ABOVE;
     66       this.invertLeftRight = true;
     67 
     68       // Initialize the images for the drop-down buttons that appear beside the
     69       // session names.
     70       MenuButton.createDropDownArrows();
     71 
     72       recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED);
     73     },
     74 
     75     /**
     76      * Initialize this element.
     77      * @param {boolean} signedIn Is the current user signed in?
     78      */
     79     initialize: function(signedIn) {
     80       this.updateSignInState(signedIn);
     81     },
     82 
     83     /**
     84      * Handle a context menu event for an object in the menu's DOM subtree.
     85      */
     86     onContextMenu_: function(e) {
     87       // Only record the action if it occurred in one of the menu items or
     88       // on one of the session headings.
     89       if (findAncestorByClass(e.target, 'footer-menu-item')) {
     90         recordUmaEvent_(HISTOGRAM_EVENT.LINK_RIGHT_CLICKED);
     91       } else {
     92         var heading = findAncestorByClass(e.target, 'session-heading');
     93         if (heading) {
     94           recordUmaEvent_(HISTOGRAM_EVENT.SESSION_NAME_RIGHT_CLICKED);
     95 
     96           // Let the context menu know which session it was invoked on,
     97           // since they all share the same instance of the menu.
     98           DeviceContextMenuController.getInstance().setSession(
     99               heading.sessionData_);
    100         }
    101       }
    102     },
    103 
    104     /**
    105      * Hides the menu.
    106      * @override
    107      */
    108     hideMenu: function() {
    109       // Don't hide if the device context menu is currently showing.
    110       if (this.deviceContextMenu_.hidden)
    111         MenuButton.prototype.hideMenu.call(this);
    112     },
    113 
    114     /**
    115      * Shows the menu, first rebuilding it if necessary.
    116      * TODO(estade): the right of the menu should align with the right of the
    117      * button.
    118      * @override
    119      */
    120     showMenu: function(shouldSetFocus) {
    121       if (this.sessions_.length == 0)
    122         chrome.send('getForeignSessions');
    123       recordUmaEvent_(HISTOGRAM_EVENT.SHOW_MENU);
    124       MenuButton.prototype.showMenu.apply(this, arguments);
    125 
    126       // Work around https://bugs.webkit.org/show_bug.cgi?id=85884.
    127       this.menu.scrollTop = 0;
    128     },
    129 
    130     /**
    131      * Reset the menu contents to the default state.
    132      * @private
    133      */
    134     resetMenuContents_: function() {
    135       this.menu.innerHTML = '';
    136       this.menu.appendChild(this.promoMessage_);
    137     },
    138 
    139     /**
    140      * Create a custom click handler for a link, so that clicking on a link
    141      * restores the session (including back stack) rather than just opening
    142      * the URL.
    143      */
    144     makeClickHandler_: function(sessionTag, windowId, tabId) {
    145       var self = this;
    146       return function(e) {
    147         recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED);
    148         chrome.send('openForeignSession', [sessionTag, windowId, tabId,
    149             e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
    150         e.preventDefault();
    151       };
    152     },
    153 
    154     /**
    155      * Add the UI for a foreign session to the menu.
    156      * @param {Object} session Object describing the foreign session.
    157      */
    158     addSession_: function(session) {
    159       var doc = this.ownerDocument;
    160 
    161       var section = doc.createElement('section');
    162       this.menu.appendChild(section);
    163 
    164       var heading = doc.createElement('h3');
    165       heading.className = 'session-heading';
    166       heading.textContent = session.name;
    167       heading.sessionData_ = session;
    168       section.appendChild(heading);
    169 
    170       var dropDownButton = new ContextMenuButton;
    171       dropDownButton.classList.add('drop-down');
    172       // Keep track of the drop down that triggered the menu, so we know
    173       // which element to apply the command to.
    174       function handleDropDownFocus(e) {
    175         DeviceContextMenuController.getInstance().setSession(session);
    176       }
    177       dropDownButton.addEventListener('mousedown', handleDropDownFocus);
    178       dropDownButton.addEventListener('focus', handleDropDownFocus);
    179       heading.appendChild(dropDownButton);
    180 
    181       var timeSpan = doc.createElement('span');
    182       timeSpan.className = 'details';
    183       timeSpan.textContent = session.modifiedTime;
    184       heading.appendChild(timeSpan);
    185 
    186       cr.ui.contextMenuHandler.setContextMenu(heading,
    187                                               this.deviceContextMenu_);
    188 
    189       if (!session.collapsed)
    190         section.appendChild(this.createSessionContents_(session));
    191     },
    192 
    193     /**
    194      * Create the DOM tree representing the tabs and windows in a session.
    195      * @param {Object} session The session model object.
    196      * @return {Element} A single div containing the list of tabs & windows.
    197      * @private
    198      */
    199     createSessionContents_: function(session) {
    200       var doc = this.ownerDocument;
    201       var contents = doc.createElement('div');
    202 
    203       for (var i = 0; i < session.windows.length; i++) {
    204         var window = session.windows[i];
    205 
    206         // Show a separator between multiple windows in the same session.
    207         if (i > 0)
    208           contents.appendChild(doc.createElement('hr'));
    209 
    210         for (var j = 0; j < window.tabs.length; j++) {
    211           var tab = window.tabs[j];
    212           var a = doc.createElement('a');
    213           a.className = 'footer-menu-item';
    214           a.textContent = tab.title;
    215           a.href = tab.url;
    216           a.style.backgroundImage = getFaviconImageSet(tab.url);
    217 
    218           var clickHandler = this.makeClickHandler_(
    219               session.tag, String(window.sessionId), String(tab.sessionId));
    220           a.addEventListener('click', clickHandler);
    221           contents.appendChild(a);
    222           cr.ui.decorate(a, MenuItem);
    223         }
    224       }
    225 
    226       return contents;
    227     },
    228 
    229     /**
    230      * Sets the menu model data. An empty list means that either there are no
    231      * foreign sessions, or tab sync is disabled for this profile.
    232      * |isTabSyncEnabled| makes it possible to distinguish between the cases.
    233      *
    234      * @param {Array} sessionList Array of objects describing the sessions
    235      *     from other devices.
    236      * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
    237      */
    238     setForeignSessions: function(sessionList, isTabSyncEnabled) {
    239       this.sessions_ = sessionList;
    240       this.resetMenuContents_();
    241       if (sessionList.length > 0) {
    242         // Rebuild the menu with the new data.
    243         for (var i = 0; i < sessionList.length; i++) {
    244           this.addSession_(sessionList[i]);
    245         }
    246       }
    247 
    248       // The menu button is shown iff tab sync is enabled.
    249       this.hidden = !isTabSyncEnabled;
    250     },
    251 
    252     /**
    253      * Called when this element is initialized, and from the new tab page when
    254      * the user's signed in state changes,
    255      * @param {boolean} signedIn Is the user currently signed in?
    256      */
    257     updateSignInState: function(signedIn) {
    258       if (signedIn)
    259         chrome.send('getForeignSessions');
    260       else
    261         this.hidden = true;
    262     },
    263   };
    264 
    265   /**
    266    * Controller for the context menu for device names in the list of sessions.
    267    * This class is designed to be used as a singleton.
    268    *
    269    * @constructor
    270    */
    271   function DeviceContextMenuController() {
    272     this.__proto__ = DeviceContextMenuController.prototype;
    273     this.initialize();
    274   }
    275   cr.addSingletonGetter(DeviceContextMenuController);
    276 
    277   DeviceContextMenuController.prototype = {
    278 
    279     initialize: function() {
    280       var menu = new cr.ui.Menu;
    281       cr.ui.decorate(menu, cr.ui.Menu);
    282       menu.classList.add('device-context-menu');
    283       menu.classList.add('footer-menu-context-menu');
    284       this.menu = menu;
    285       this.collapseItem_ = this.appendMenuItem_('collapseSessionMenuItemText');
    286       this.collapseItem_.addEventListener('activate',
    287                                           this.onCollapseOrExpand_.bind(this));
    288       this.expandItem_ = this.appendMenuItem_('expandSessionMenuItemText');
    289       this.expandItem_.addEventListener('activate',
    290                                         this.onCollapseOrExpand_.bind(this));
    291       this.openAllItem_ = this.appendMenuItem_('restoreSessionMenuItemText');
    292       this.openAllItem_.addEventListener('activate',
    293                                          this.onOpenAll_.bind(this));
    294     },
    295 
    296     /**
    297      * Appends a menu item to |this.menu|.
    298      * @param {string} textId The ID for the localized string that acts as
    299      *     the item's label.
    300      */
    301     appendMenuItem_: function(textId) {
    302       var button = cr.doc.createElement('button');
    303       this.menu.appendChild(button);
    304       cr.ui.decorate(button, cr.ui.MenuItem);
    305       button.textContent = loadTimeData.getString(textId);
    306       return button;
    307     },
    308 
    309     /**
    310      * Handler for the 'Collapse' and 'Expand' menu items.
    311      * @param {Event} e The activation event.
    312      * @private
    313      */
    314     onCollapseOrExpand_: function(e) {
    315       this.session_.collapsed = !this.session_.collapsed;
    316       this.updateMenuItems_();
    317       chrome.send('setForeignSessionCollapsed',
    318                   [this.session_.tag, this.session_.collapsed]);
    319       chrome.send('getForeignSessions');  // Refresh the list.
    320 
    321       var eventId = this.session_.collapsed ?
    322           HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION;
    323       recordUmaEvent_(eventId);
    324     },
    325 
    326     /**
    327      * Handler for the 'Open all' menu item.
    328      * @param {Event} e The activation event.
    329      * @private
    330      */
    331     onOpenAll_: function(e) {
    332       chrome.send('openForeignSession', [this.session_.tag]);
    333       recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL);
    334     },
    335 
    336     /**
    337      * Set the session data for the session the context menu was invoked on.
    338      * This should never be called when the menu is visible.
    339      * @param {Object} session The model object for the session.
    340      */
    341     setSession: function(session) {
    342       this.session_ = session;
    343       this.updateMenuItems_();
    344     },
    345 
    346     /**
    347      * Set the visibility of the Expand/Collapse menu items based on the state
    348      * of the session that this menu is currently associated with.
    349      * @private
    350      */
    351     updateMenuItems_: function() {
    352       this.collapseItem_.hidden = this.session_.collapsed;
    353       this.expandItem_.hidden = !this.session_.collapsed;
    354     }
    355   };
    356 
    357   return {
    358     OtherSessionsMenuButton: OtherSessionsMenuButton,
    359   };
    360 });
    361