Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / ntp4 / other_sessions.js
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       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);
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 });