- add sources.
[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 cr.define('ntp', function() {
10   'use strict';
11
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');
17
18   // Histogram buckets for UMA tracking of menu usage.
19   /** @const */ var HISTOGRAM_EVENT = {
20       INITIALIZED: 0,
21       SHOW_MENU: 1,
22       LINK_CLICKED: 2,
23       LINK_RIGHT_CLICKED: 3,
24       SESSION_NAME_RIGHT_CLICKED: 4,
25       SHOW_SESSION_MENU: 5,
26       COLLAPSE_SESSION: 6,
27       EXPAND_SESSION: 7,
28       OPEN_ALL: 8
29   };
30   /** @const */ var HISTOGRAM_EVENT_LIMIT =
31       HISTOGRAM_EVENT.OPEN_ALL + 1;
32
33   /**
34    * Record an event in the UMA histogram.
35    * @param {number} eventId The id of the event to be recorded.
36    * @private
37    */
38   function recordUmaEvent_(eventId) {
39     chrome.send('metricsHandler:recordInHistogram',
40         ['NewTabPage.OtherSessionsMenu', eventId, HISTOGRAM_EVENT_LIMIT]);
41   }
42
43   OtherSessionsMenuButton.prototype = {
44     __proto__: MenuButton.prototype,
45
46     decorate: function() {
47       MenuButton.prototype.decorate.call(this);
48       this.menu = new Menu;
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);
55
56       // Create the context menu that appears when the user right clicks
57       // on a device name.
58       this.deviceContextMenu_ = DeviceContextMenuController.getInstance().menu;
59       document.body.appendChild(this.deviceContextMenu_);
60
61       this.promoMessage_ = $('other-sessions-promo-template').cloneNode(true);
62       this.promoMessage_.removeAttribute('id');  // Prevent a duplicate id.
63
64       this.sessions_ = [];
65       this.anchorType = cr.ui.AnchorType.ABOVE;
66       this.invertLeftRight = true;
67
68       // Initialize the images for the drop-down buttons that appear beside the
69       // session names.
70       MenuButton.createDropDownArrows();
71
72       recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED);
73     },
74
75     /**
76      * Initialize this element.
77      * @param {boolean} signedIn Is the current user signed in?
78      */
79     initialize: function(signedIn) {
80       this.updateSignInState(signedIn);
81     },
82
83     /**
84      * Handle a context menu event for an object in the menu's DOM subtree.
85      */
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);
91       } else {
92         var heading = findAncestorByClass(e.target, 'session-heading');
93         if (heading) {
94           recordUmaEvent_(HISTOGRAM_EVENT.SESSION_NAME_RIGHT_CLICKED);
95
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_);
100         }
101       }
102     },
103
104     /**
105      * Hides the menu.
106      * @override
107      */
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);
112     },
113
114     /**
115      * Shows the menu, first rebuilding it if necessary.
116      * TODO(estade): the right of the menu should align with the right of the
117      * button.
118      * @override
119      */
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);
125
126       // Work around https://bugs.webkit.org/show_bug.cgi?id=85884.
127       this.menu.scrollTop = 0;
128     },
129
130     /**
131      * Reset the menu contents to the default state.
132      * @private
133      */
134     resetMenuContents_: function() {
135       this.menu.innerHTML = '';
136       this.menu.appendChild(this.promoMessage_);
137     },
138
139     /**
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
142      * the URL.
143      */
144     makeClickHandler_: function(sessionTag, windowId, tabId) {
145       var self = this;
146       return function(e) {
147         recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED);
148         chrome.send('openForeignSession', [sessionTag, windowId, tabId,
149             e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
150         e.preventDefault();
151       };
152     },
153
154     /**
155      * Add the UI for a foreign session to the menu.
156      * @param {Object} session Object describing the foreign session.
157      */
158     addSession_: function(session) {
159       var doc = this.ownerDocument;
160
161       var section = doc.createElement('section');
162       this.menu.appendChild(section);
163
164       var heading = doc.createElement('h3');
165       heading.className = 'session-heading';
166       heading.textContent = session.name;
167       heading.sessionData_ = session;
168       section.appendChild(heading);
169
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);
176       }
177       dropDownButton.addEventListener('mousedown', handleDropDownFocus);
178       dropDownButton.addEventListener('focus', handleDropDownFocus);
179       heading.appendChild(dropDownButton);
180
181       var timeSpan = doc.createElement('span');
182       timeSpan.className = 'details';
183       timeSpan.textContent = session.modifiedTime;
184       heading.appendChild(timeSpan);
185
186       cr.ui.contextMenuHandler.setContextMenu(heading,
187                                               this.deviceContextMenu_);
188
189       if (!session.collapsed)
190         section.appendChild(this.createSessionContents_(session));
191     },
192
193     /**
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.
197      * @private
198      */
199     createSessionContents_: function(session) {
200       var doc = this.ownerDocument;
201       var contents = doc.createElement('div');
202
203       for (var i = 0; i < session.windows.length; i++) {
204         var window = session.windows[i];
205
206         // Show a separator between multiple windows in the same session.
207         if (i > 0)
208           contents.appendChild(doc.createElement('hr'));
209
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;
215           a.href = tab.url;
216           a.style.backgroundImage = getFaviconImageSet(tab.url);
217
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);
223         }
224       }
225
226       return contents;
227     },
228
229     /**
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.
233      *
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?
237      */
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]);
245         }
246       }
247
248       // The menu button is shown iff tab sync is enabled.
249       this.hidden = !isTabSyncEnabled;
250     },
251
252     /**
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?
256      */
257     updateSignInState: function(signedIn) {
258       if (signedIn)
259         chrome.send('getForeignSessions');
260       else
261         this.hidden = true;
262     },
263   };
264
265   /**
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.
268    *
269    * @constructor
270    */
271   function DeviceContextMenuController() {
272     this.__proto__ = DeviceContextMenuController.prototype;
273     this.initialize();
274   }
275   cr.addSingletonGetter(DeviceContextMenuController);
276
277   DeviceContextMenuController.prototype = {
278
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');
284       this.menu = 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));
294     },
295
296     /**
297      * Appends a menu item to |this.menu|.
298      * @param {string} textId The ID for the localized string that acts as
299      *     the item's label.
300      */
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);
306       return button;
307     },
308
309     /**
310      * Handler for the 'Collapse' and 'Expand' menu items.
311      * @param {Event} e The activation event.
312      * @private
313      */
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.
320
321       var eventId = this.session_.collapsed ?
322           HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION;
323       recordUmaEvent_(eventId);
324     },
325
326     /**
327      * Handler for the 'Open all' menu item.
328      * @param {Event} e The activation event.
329      * @private
330      */
331     onOpenAll_: function(e) {
332       chrome.send('openForeignSession', [this.session_.tag]);
333       recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL);
334     },
335
336     /**
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.
340      */
341     setSession: function(session) {
342       this.session_ = session;
343       this.updateMenuItems_();
344     },
345
346     /**
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.
349      * @private
350      */
351     updateMenuItems_: function() {
352       this.collapseItem_.hidden = this.session_.collapsed;
353       this.expandItem_.hidden = !this.session_.collapsed;
354     }
355   };
356
357   return {
358     OtherSessionsMenuButton: OtherSessionsMenuButton,
359   };
360 });