OLD | NEW |
| (Empty) |
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 /** | |
10 * @typedef {{collapsed: boolean, | |
11 * deviceType: string, | |
12 * modifiedTime: string, | |
13 * name: string, | |
14 * tag: string, | |
15 * windows: Array<WindowData>}} | |
16 * @see chrome/browser/ui/webui/ntp/foreign_session_handler.cc | |
17 */ | |
18 var SessionData; | |
19 | |
20 /** | |
21 * @typedef {{sessionId: number, | |
22 * tabs: Array, | |
23 * timestamp: number, | |
24 * type: string, | |
25 * userVisibleTimestamp: string}} | |
26 * @see chrome/browser/ui/webui/ntp/foreign_session_handler.cc | |
27 */ | |
28 var WindowData; | |
29 | |
30 cr.define('ntp', function() { | |
31 'use strict'; | |
32 | |
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; | |
37 | |
38 /** | |
39 * @constructor | |
40 * @extends {cr.ui.MenuButton} | |
41 */ | |
42 var OtherSessionsMenuButton = cr.ui.define('button'); | |
43 | |
44 // Histogram buckets for UMA tracking of menu usage. | |
45 /** @const */ var HISTOGRAM_EVENT = { | |
46 INITIALIZED: 0, | |
47 SHOW_MENU: 1, | |
48 LINK_CLICKED: 2, | |
49 LINK_RIGHT_CLICKED: 3, | |
50 SESSION_NAME_RIGHT_CLICKED: 4, | |
51 SHOW_SESSION_MENU: 5, | |
52 COLLAPSE_SESSION: 6, | |
53 EXPAND_SESSION: 7, | |
54 OPEN_ALL: 8 | |
55 }; | |
56 /** @const */ var HISTOGRAM_EVENT_LIMIT = | |
57 HISTOGRAM_EVENT.OPEN_ALL + 1; | |
58 | |
59 /** | |
60 * Record an event in the UMA histogram. | |
61 * @param {number} eventId The id of the event to be recorded. | |
62 * @private | |
63 */ | |
64 function recordUmaEvent_(eventId) { | |
65 chrome.send('metricsHandler:recordInHistogram', | |
66 ['NewTabPage.OtherSessionsMenu', eventId, HISTOGRAM_EVENT_LIMIT]); | |
67 } | |
68 | |
69 OtherSessionsMenuButton.prototype = { | |
70 __proto__: MenuButton.prototype, | |
71 | |
72 decorate: function() { | |
73 MenuButton.prototype.decorate.call(this); | |
74 this.menu = new Menu; | |
75 this.menu.menuItemSelector = '[role=menuitem]'; // before decoration | |
76 cr.ui.decorate(this.menu, Menu); | |
77 this.menu.classList.add('footer-menu'); | |
78 this.menu.addEventListener('contextmenu', | |
79 this.onContextMenu_.bind(this), true); | |
80 document.body.appendChild(this.menu); | |
81 | |
82 // Create the context menu that appears when the user right clicks | |
83 // on a device name. | |
84 this.deviceContextMenu_ = DeviceContextMenuController.getInstance().menu; | |
85 document.body.appendChild(this.deviceContextMenu_); | |
86 | |
87 this.promoMessage_ = $('other-sessions-promo-template').cloneNode(true); | |
88 this.promoMessage_.removeAttribute('id'); // Prevent a duplicate id. | |
89 | |
90 this.sessions_ = []; | |
91 this.anchorType = cr.ui.AnchorType.ABOVE; | |
92 this.invertLeftRight = true; | |
93 | |
94 // Initialize the images for the drop-down buttons that appear beside the | |
95 // session names. | |
96 MenuButton.createDropDownArrows(); | |
97 | |
98 recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED); | |
99 }, | |
100 | |
101 /** | |
102 * Initialize this element. | |
103 * @param {boolean} signedIn Is the current user signed in? | |
104 */ | |
105 initialize: function(signedIn) { | |
106 this.updateSignInState(signedIn); | |
107 }, | |
108 | |
109 /** | |
110 * Handle a context menu event for an object in the menu's DOM subtree. | |
111 */ | |
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); | |
117 } else { | |
118 var heading = findAncestorByClass(e.target, 'session-heading'); | |
119 if (heading) { | |
120 recordUmaEvent_(HISTOGRAM_EVENT.SESSION_NAME_RIGHT_CLICKED); | |
121 | |
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_); | |
126 } | |
127 } | |
128 }, | |
129 | |
130 /** | |
131 * Hides the menu. | |
132 * @override | |
133 */ | |
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); | |
138 }, | |
139 | |
140 /** | |
141 * Shows the menu, first rebuilding it if necessary. | |
142 * TODO(estade): the right of the menu should align with the right of the | |
143 * button. | |
144 * @override | |
145 */ | |
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); | |
151 | |
152 // Work around https://bugs.webkit.org/show_bug.cgi?id=85884. | |
153 this.menu.scrollTop = 0; | |
154 }, | |
155 | |
156 /** | |
157 * Reset the menu contents to the default state. | |
158 * @private | |
159 */ | |
160 resetMenuContents_: function() { | |
161 this.menu.innerHTML = ''; | |
162 this.menu.appendChild(this.promoMessage_); | |
163 }, | |
164 | |
165 /** | |
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 | |
168 * the URL. | |
169 */ | |
170 makeClickHandler_: function(sessionTag, windowId, tabId) { | |
171 var self = this; | |
172 return function(e) { | |
173 recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED); | |
174 chrome.send('openForeignSession', [sessionTag, windowId, tabId, | |
175 e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]); | |
176 e.preventDefault(); | |
177 }; | |
178 }, | |
179 | |
180 /** | |
181 * Add the UI for a foreign session to the menu. | |
182 * @param {SessionData} session Object describing the foreign session. | |
183 */ | |
184 addSession_: function(session) { | |
185 var doc = this.ownerDocument; | |
186 | |
187 var section = doc.createElement('section'); | |
188 this.menu.appendChild(section); | |
189 | |
190 var heading = doc.createElement('h3'); | |
191 heading.className = 'session-heading'; | |
192 heading.textContent = session.name; | |
193 heading.sessionData_ = session; | |
194 section.appendChild(heading); | |
195 | |
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); | |
202 } | |
203 dropDownButton.addEventListener('mousedown', handleDropDownFocus); | |
204 dropDownButton.addEventListener('focus', handleDropDownFocus); | |
205 heading.appendChild(dropDownButton); | |
206 | |
207 var timeSpan = doc.createElement('span'); | |
208 timeSpan.className = 'details'; | |
209 timeSpan.textContent = session.modifiedTime; | |
210 heading.appendChild(timeSpan); | |
211 | |
212 cr.ui.contextMenuHandler.setContextMenu(heading, | |
213 this.deviceContextMenu_); | |
214 | |
215 if (!session.collapsed) | |
216 section.appendChild(this.createSessionContents_(session)); | |
217 }, | |
218 | |
219 /** | |
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. | |
223 * @private | |
224 */ | |
225 createSessionContents_: function(session) { | |
226 var doc = this.ownerDocument; | |
227 var contents = doc.createElement('div'); | |
228 | |
229 for (var i = 0; i < session.windows.length; i++) { | |
230 var window = session.windows[i]; | |
231 | |
232 // Show a separator between multiple windows in the same session. | |
233 if (i > 0) | |
234 contents.appendChild(doc.createElement('hr')); | |
235 | |
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; | |
241 a.href = tab.url; | |
242 a.style.backgroundImage = getFaviconImageSet(tab.url); | |
243 | |
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); | |
249 } | |
250 } | |
251 | |
252 return contents; | |
253 }, | |
254 | |
255 /** | |
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. | |
259 * | |
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? | |
263 */ | |
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]); | |
271 } | |
272 } | |
273 | |
274 // The menu button is shown iff tab sync is enabled. | |
275 this.hidden = !isTabSyncEnabled; | |
276 }, | |
277 | |
278 /** | |
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? | |
282 */ | |
283 updateSignInState: function(signedIn) { | |
284 if (signedIn) | |
285 chrome.send('getForeignSessions'); | |
286 else | |
287 this.hidden = true; | |
288 }, | |
289 }; | |
290 | |
291 /** | |
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. | |
294 * | |
295 * @constructor | |
296 */ | |
297 function DeviceContextMenuController() { | |
298 this.__proto__ = DeviceContextMenuController.prototype; | |
299 this.initialize(); | |
300 } | |
301 cr.addSingletonGetter(DeviceContextMenuController); | |
302 | |
303 DeviceContextMenuController.prototype = { | |
304 | |
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'); | |
310 this.menu = 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)); | |
320 }, | |
321 | |
322 /** | |
323 * Appends a menu item to |this.menu|. | |
324 * @param {string} textId The ID for the localized string that acts as | |
325 * the item's label. | |
326 */ | |
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); | |
332 return button; | |
333 }, | |
334 | |
335 /** | |
336 * Handler for the 'Collapse' and 'Expand' menu items. | |
337 * @param {Event} e The activation event. | |
338 * @private | |
339 */ | |
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. | |
346 | |
347 var eventId = this.session_.collapsed ? | |
348 HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION; | |
349 recordUmaEvent_(eventId); | |
350 }, | |
351 | |
352 /** | |
353 * Handler for the 'Open all' menu item. | |
354 * @param {Event} e The activation event. | |
355 * @private | |
356 */ | |
357 onOpenAll_: function(e) { | |
358 chrome.send('openForeignSession', [this.session_.tag]); | |
359 recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL); | |
360 }, | |
361 | |
362 /** | |
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. | |
366 */ | |
367 setSession: function(session) { | |
368 this.session_ = session; | |
369 this.updateMenuItems_(); | |
370 }, | |
371 | |
372 /** | |
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. | |
375 * @private | |
376 */ | |
377 updateMenuItems_: function() { | |
378 this.collapseItem_.hidden = this.session_.collapsed; | |
379 this.expandItem_.hidden = !this.session_.collapsed; | |
380 } | |
381 }; | |
382 | |
383 return { | |
384 OtherSessionsMenuButton: OtherSessionsMenuButton, | |
385 }; | |
386 }); | |
OLD | NEW |