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
,