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.
6 * @fileoverview The menu that shows tabs from sessions on other devices.
9 cr.define('ntp', function() {
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');
18 // Histogram buckets for UMA tracking of menu usage.
19 /** @const */ var HISTOGRAM_EVENT = {
23 LINK_RIGHT_CLICKED: 3,
24 SESSION_NAME_RIGHT_CLICKED: 4,
30 /** @const */ var HISTOGRAM_EVENT_LIMIT =
31 HISTOGRAM_EVENT.OPEN_ALL + 1;
34 * Record an event in the UMA histogram.
35 * @param {number} eventId The id of the event to be recorded.
38 function recordUmaEvent_(eventId) {
39 chrome.send('metricsHandler:recordInHistogram',
40 ['NewTabPage.OtherSessionsMenu', eventId, HISTOGRAM_EVENT_LIMIT]);
43 OtherSessionsMenuButton.prototype = {
44 __proto__: MenuButton.prototype,
46 decorate: function() {
47 MenuButton.prototype.decorate.call(this);
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);
56 // Create the context menu that appears when the user right clicks
58 this.deviceContextMenu_ = DeviceContextMenuController.getInstance().menu;
59 document.body.appendChild(this.deviceContextMenu_);
61 this.promoMessage_ = $('other-sessions-promo-template').cloneNode(true);
62 this.promoMessage_.removeAttribute('id'); // Prevent a duplicate id.
65 this.anchorType = cr.ui.AnchorType.ABOVE;
66 this.invertLeftRight = true;
68 // Initialize the images for the drop-down buttons that appear beside the
70 MenuButton.createDropDownArrows();
72 recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED);
76 * Initialize this element.
77 * @param {boolean} signedIn Is the current user signed in?
79 initialize: function(signedIn) {
80 this.updateSignInState(signedIn);
84 * Handle a context menu event for an object in the menu's DOM subtree.
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);
92 var heading = findAncestorByClass(e.target, 'session-heading');
94 recordUmaEvent_(HISTOGRAM_EVENT.SESSION_NAME_RIGHT_CLICKED);
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_);
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);
115 * Shows the menu, first rebuilding it if necessary.
116 * TODO(estade): the right of the menu should align with the right of the
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);
126 // Work around https://bugs.webkit.org/show_bug.cgi?id=85884.
127 this.menu.scrollTop = 0;
131 * Reset the menu contents to the default state.
134 resetMenuContents_: function() {
135 this.menu.innerHTML = '';
136 this.menu.appendChild(this.promoMessage_);
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
144 makeClickHandler_: function(sessionTag, windowId, tabId) {
147 recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED);
148 chrome.send('openForeignSession', [sessionTag, windowId, tabId,
149 e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
155 * Add the UI for a foreign session to the menu.
156 * @param {Object} session Object describing the foreign session.
158 addSession_: function(session) {
159 var doc = this.ownerDocument;
161 var section = doc.createElement('section');
162 this.menu.appendChild(section);
164 var heading = doc.createElement('h3');
165 heading.className = 'session-heading';
166 heading.textContent = session.name;
167 heading.sessionData_ = session;
168 section.appendChild(heading);
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);
177 dropDownButton.addEventListener('mousedown', handleDropDownFocus);
178 dropDownButton.addEventListener('focus', handleDropDownFocus);
179 heading.appendChild(dropDownButton);
181 var timeSpan = doc.createElement('span');
182 timeSpan.className = 'details';
183 timeSpan.textContent = session.modifiedTime;
184 heading.appendChild(timeSpan);
186 cr.ui.contextMenuHandler.setContextMenu(heading,
187 this.deviceContextMenu_);
189 if (!session.collapsed)
190 section.appendChild(this.createSessionContents_(session));
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.
199 createSessionContents_: function(session) {
200 var doc = this.ownerDocument;
201 var contents = doc.createElement('div');
203 for (var i = 0; i < session.windows.length; i++) {
204 var window = session.windows[i];
206 // Show a separator between multiple windows in the same session.
208 contents.appendChild(doc.createElement('hr'));
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;
216 a.style.backgroundImage = getFaviconImageSet(tab.url);
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);
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.
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?
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]);
248 // The menu button is shown iff tab sync is enabled.
249 this.hidden = !isTabSyncEnabled;
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?
257 updateSignInState: function(signedIn) {
259 chrome.send('getForeignSessions');
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.
271 function DeviceContextMenuController() {
272 this.__proto__ = DeviceContextMenuController.prototype;
275 cr.addSingletonGetter(DeviceContextMenuController);
277 DeviceContextMenuController.prototype = {
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');
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));
297 * Appends a menu item to |this.menu|.
298 * @param {string} textId The ID for the localized string that acts as
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);
310 * Handler for the 'Collapse' and 'Expand' menu items.
311 * @param {Event} e The activation event.
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.
321 var eventId = this.session_.collapsed ?
322 HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION;
323 recordUmaEvent_(eventId);
327 * Handler for the 'Open all' menu item.
328 * @param {Event} e The activation event.
331 onOpenAll_: function(e) {
332 chrome.send('openForeignSession', [this.session_.tag]);
333 recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL);
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.
341 setSession: function(session) {
342 this.session_ = session;
343 this.updateMenuItems_();
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.
351 updateMenuItems_: function() {
352 this.collapseItem_.hidden = this.session_.collapsed;
353 this.expandItem_.hidden = !this.session_.collapsed;
358 OtherSessionsMenuButton: OtherSessionsMenuButton,