• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
9cr.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