| 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 |