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.
10 * @typedef {{collapsed: boolean,
12 * modifiedTime: string,
15 * windows: Array<WindowData>}}
16 * @see chrome/browser/ui/webui/ntp/foreign_session_handler.cc
21 * @typedef {{sessionId: number,
25 * userVisibleTimestamp: string}}
26 * @see chrome/browser/ui/webui/ntp/foreign_session_handler.cc
30 cr
.define('ntp', function() {
33 /** @const */ var ContextMenuButton
= cr
.ui
.ContextMenuButton
;
34 /** @const */ var Menu
= cr
.ui
.Menu
;
35 /** @const */ var MenuItem
= cr
.ui
.MenuItem
;
36 /** @const */ var MenuButton
= cr
.ui
.MenuButton
;
40 * @extends {cr.ui.MenuButton}
42 var OtherSessionsMenuButton
= cr
.ui
.define('button');
44 // Histogram buckets for UMA tracking of menu usage.
45 /** @const */ var HISTOGRAM_EVENT
= {
49 LINK_RIGHT_CLICKED
: 3,
50 SESSION_NAME_RIGHT_CLICKED
: 4,
56 /** @const */ var HISTOGRAM_EVENT_LIMIT
=
57 HISTOGRAM_EVENT
.OPEN_ALL
+ 1;
60 * Record an event in the UMA histogram.
61 * @param {number} eventId The id of the event to be recorded.
64 function recordUmaEvent_(eventId
) {
65 chrome
.send('metricsHandler:recordInHistogram',
66 ['NewTabPage.OtherSessionsMenu', eventId
, HISTOGRAM_EVENT_LIMIT
]);
69 OtherSessionsMenuButton
.prototype = {
70 __proto__
: MenuButton
.prototype,
72 decorate: function() {
73 MenuButton
.prototype.decorate
.call(this);
75 cr
.ui
.decorate(this.menu
, Menu
);
76 this.menu
.menuItemSelector
= '[role=menuitem]';
77 this.menu
.classList
.add('footer-menu');
78 this.menu
.addEventListener('contextmenu',
79 this.onContextMenu_
.bind(this), true);
80 document
.body
.appendChild(this.menu
);
82 // Create the context menu that appears when the user right clicks
84 this.deviceContextMenu_
= DeviceContextMenuController
.getInstance().menu
;
85 document
.body
.appendChild(this.deviceContextMenu_
);
87 this.promoMessage_
= $('other-sessions-promo-template').cloneNode(true);
88 this.promoMessage_
.removeAttribute('id'); // Prevent a duplicate id.
91 this.anchorType
= cr
.ui
.AnchorType
.ABOVE
;
92 this.invertLeftRight
= true;
94 // Initialize the images for the drop-down buttons that appear beside the
96 MenuButton
.createDropDownArrows();
98 recordUmaEvent_(HISTOGRAM_EVENT
.INITIALIZED
);
102 * Initialize this element.
103 * @param {boolean} signedIn Is the current user signed in?
105 initialize: function(signedIn
) {
106 this.updateSignInState(signedIn
);
110 * Handle a context menu event for an object in the menu's DOM subtree.
112 onContextMenu_: function(e
) {
113 // Only record the action if it occurred in one of the menu items or
114 // on one of the session headings.
115 if (findAncestorByClass(e
.target
, 'footer-menu-item')) {
116 recordUmaEvent_(HISTOGRAM_EVENT
.LINK_RIGHT_CLICKED
);
118 var heading
= findAncestorByClass(e
.target
, 'session-heading');
120 recordUmaEvent_(HISTOGRAM_EVENT
.SESSION_NAME_RIGHT_CLICKED
);
122 // Let the context menu know which session it was invoked on,
123 // since they all share the same instance of the menu.
124 DeviceContextMenuController
.getInstance().setSession(
125 heading
.sessionData_
);
134 hideMenu: function() {
135 // Don't hide if the device context menu is currently showing.
136 if (this.deviceContextMenu_
.hidden
)
137 MenuButton
.prototype.hideMenu
.call(this);
141 * Shows the menu, first rebuilding it if necessary.
142 * TODO(estade): the right of the menu should align with the right of the
146 showMenu: function(shouldSetFocus
) {
147 if (this.sessions_
.length
== 0)
148 chrome
.send('getForeignSessions');
149 recordUmaEvent_(HISTOGRAM_EVENT
.SHOW_MENU
);
150 MenuButton
.prototype.showMenu
.apply(this, arguments
);
152 // Work around https://bugs.webkit.org/show_bug.cgi?id=85884.
153 this.menu
.scrollTop
= 0;
157 * Reset the menu contents to the default state.
160 resetMenuContents_: function() {
161 this.menu
.innerHTML
= '';
162 this.menu
.appendChild(this.promoMessage_
);
166 * Create a custom click handler for a link, so that clicking on a link
167 * restores the session (including back stack) rather than just opening
170 makeClickHandler_: function(sessionTag
, windowId
, tabId
) {
173 recordUmaEvent_(HISTOGRAM_EVENT
.LINK_CLICKED
);
174 chrome
.send('openForeignSession', [sessionTag
, windowId
, tabId
,
175 e
.button
, e
.altKey
, e
.ctrlKey
, e
.metaKey
, e
.shiftKey
]);
181 * Add the UI for a foreign session to the menu.
182 * @param {SessionData} session Object describing the foreign session.
184 addSession_: function(session
) {
185 var doc
= this.ownerDocument
;
187 var section
= doc
.createElement('section');
188 this.menu
.appendChild(section
);
190 var heading
= doc
.createElement('h3');
191 heading
.className
= 'session-heading';
192 heading
.textContent
= session
.name
;
193 heading
.sessionData_
= session
;
194 section
.appendChild(heading
);
196 var dropDownButton
= new ContextMenuButton
;
197 dropDownButton
.classList
.add('drop-down');
198 // Keep track of the drop down that triggered the menu, so we know
199 // which element to apply the command to.
200 function handleDropDownFocus(e
) {
201 DeviceContextMenuController
.getInstance().setSession(session
);
203 dropDownButton
.addEventListener('mousedown', handleDropDownFocus
);
204 dropDownButton
.addEventListener('focus', handleDropDownFocus
);
205 heading
.appendChild(dropDownButton
);
207 var timeSpan
= doc
.createElement('span');
208 timeSpan
.className
= 'details';
209 timeSpan
.textContent
= session
.modifiedTime
;
210 heading
.appendChild(timeSpan
);
212 cr
.ui
.contextMenuHandler
.setContextMenu(heading
,
213 this.deviceContextMenu_
);
215 if (!session
.collapsed
)
216 section
.appendChild(this.createSessionContents_(session
));
220 * Create the DOM tree representing the tabs and windows in a session.
221 * @param {SessionData} session The session model object.
222 * @return {Element} A single div containing the list of tabs & windows.
225 createSessionContents_: function(session
) {
226 var doc
= this.ownerDocument
;
227 var contents
= doc
.createElement('div');
229 for (var i
= 0; i
< session
.windows
.length
; i
++) {
230 var window
= session
.windows
[i
];
232 // Show a separator between multiple windows in the same session.
234 contents
.appendChild(doc
.createElement('hr'));
236 for (var j
= 0; j
< window
.tabs
.length
; j
++) {
237 var tab
= window
.tabs
[j
];
238 var a
= doc
.createElement('a');
239 a
.className
= 'footer-menu-item';
240 a
.textContent
= tab
.title
;
242 a
.style
.backgroundImage
= getFaviconImageSet(tab
.url
);
244 var clickHandler
= this.makeClickHandler_(
245 session
.tag
, String(window
.sessionId
), String(tab
.sessionId
));
246 a
.addEventListener('click', clickHandler
);
247 contents
.appendChild(a
);
248 cr
.ui
.decorate(a
, MenuItem
);
256 * Sets the menu model data. An empty list means that either there are no
257 * foreign sessions, or tab sync is disabled for this profile.
258 * |isTabSyncEnabled| makes it possible to distinguish between the cases.
260 * @param {Array<SessionData>} sessionList Array of objects describing the
261 * sessions from other devices.
262 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
264 setForeignSessions: function(sessionList
, isTabSyncEnabled
) {
265 this.sessions_
= sessionList
;
266 this.resetMenuContents_();
267 if (sessionList
.length
> 0) {
268 // Rebuild the menu with the new data.
269 for (var i
= 0; i
< sessionList
.length
; i
++) {
270 this.addSession_(sessionList
[i
]);
274 // The menu button is shown iff tab sync is enabled.
275 this.hidden
= !isTabSyncEnabled
;
279 * Called when this element is initialized, and from the new tab page when
280 * the user's signed in state changes,
281 * @param {boolean} signedIn Is the user currently signed in?
283 updateSignInState: function(signedIn
) {
285 chrome
.send('getForeignSessions');
292 * Controller for the context menu for device names in the list of sessions.
293 * This class is designed to be used as a singleton.
297 function DeviceContextMenuController() {
298 this.__proto__
= DeviceContextMenuController
.prototype;
301 cr
.addSingletonGetter(DeviceContextMenuController
);
303 DeviceContextMenuController
.prototype = {
305 initialize: function() {
306 var menu
= new cr
.ui
.Menu
;
307 cr
.ui
.decorate(menu
, cr
.ui
.Menu
);
308 menu
.classList
.add('device-context-menu');
309 menu
.classList
.add('footer-menu-context-menu');
311 this.collapseItem_
= this.appendMenuItem_('collapseSessionMenuItemText');
312 this.collapseItem_
.addEventListener('activate',
313 this.onCollapseOrExpand_
.bind(this));
314 this.expandItem_
= this.appendMenuItem_('expandSessionMenuItemText');
315 this.expandItem_
.addEventListener('activate',
316 this.onCollapseOrExpand_
.bind(this));
317 this.openAllItem_
= this.appendMenuItem_('restoreSessionMenuItemText');
318 this.openAllItem_
.addEventListener('activate',
319 this.onOpenAll_
.bind(this));
323 * Appends a menu item to |this.menu|.
324 * @param {string} textId The ID for the localized string that acts as
327 appendMenuItem_: function(textId
) {
328 var button
= cr
.doc
.createElement('button');
329 this.menu
.appendChild(button
);
330 cr
.ui
.decorate(button
, cr
.ui
.MenuItem
);
331 button
.textContent
= loadTimeData
.getString(textId
);
336 * Handler for the 'Collapse' and 'Expand' menu items.
337 * @param {Event} e The activation event.
340 onCollapseOrExpand_: function(e
) {
341 this.session_
.collapsed
= !this.session_
.collapsed
;
342 this.updateMenuItems_();
343 chrome
.send('setForeignSessionCollapsed',
344 [this.session_
.tag
, this.session_
.collapsed
]);
345 chrome
.send('getForeignSessions'); // Refresh the list.
347 var eventId
= this.session_
.collapsed
?
348 HISTOGRAM_EVENT
.COLLAPSE_SESSION
: HISTOGRAM_EVENT
.EXPAND_SESSION
;
349 recordUmaEvent_(eventId
);
353 * Handler for the 'Open all' menu item.
354 * @param {Event} e The activation event.
357 onOpenAll_: function(e
) {
358 chrome
.send('openForeignSession', [this.session_
.tag
]);
359 recordUmaEvent_(HISTOGRAM_EVENT
.OPEN_ALL
);
363 * Set the session data for the session the context menu was invoked on.
364 * This should never be called when the menu is visible.
365 * @param {Object} session The model object for the session.
367 setSession: function(session
) {
368 this.session_
= session
;
369 this.updateMenuItems_();
373 * Set the visibility of the Expand/Collapse menu items based on the state
374 * of the session that this menu is currently associated with.
377 updateMenuItems_: function() {
378 this.collapseItem_
.hidden
= this.session_
.collapsed
;
379 this.expandItem_
.hidden
= !this.session_
.collapsed
;
384 OtherSessionsMenuButton
: OtherSessionsMenuButton
,