Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / history / other_devices.js
1 // Copyright (c) 2013 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 section of the history page that shows tabs from sessions
7                  on other devices.
8  */
9
10 ///////////////////////////////////////////////////////////////////////////////
11 // Globals:
12 /** @const */ var MAX_NUM_COLUMNS = 3;
13 /** @const */ var NB_ENTRIES_FIRST_ROW_COLUMN = 6;
14 /** @const */ var NB_ENTRIES_OTHER_ROWS_COLUMN = 0;
15
16 // Histogram buckets for UMA tracking of menu usage.
17 // Using the same values as the Other Devices button in the NTP.
18 /** @const */ var HISTOGRAM_EVENT = {
19   INITIALIZED: 0,
20   SHOW_MENU: 1,
21   LINK_CLICKED: 2,
22   LINK_RIGHT_CLICKED: 3,
23   SESSION_NAME_RIGHT_CLICKED: 4,
24   SHOW_SESSION_MENU: 5,
25   COLLAPSE_SESSION: 6,
26   EXPAND_SESSION: 7,
27   OPEN_ALL: 8,
28   LIMIT: 9  // Should always be the last one.
29 };
30
31 /**
32  * Record an event in the UMA histogram.
33  * @param {number} eventId The id of the event to be recorded.
34  * @private
35  */
36 function recordUmaEvent_(eventId) {
37   chrome.send('metricsHandler:recordInHistogram',
38       ['HistoryPage.OtherDevicesMenu', eventId, HISTOGRAM_EVENT.LIMIT]);
39 }
40
41 ///////////////////////////////////////////////////////////////////////////////
42 // DeviceContextMenuController:
43
44 /**
45  * Controller for the context menu for device names in the list of sessions.
46  * This class is designed to be used as a singleton. Also copied from existing
47  * other devices button in NTP.
48  * TODO(mad): Should we extract/reuse/share with ntp4/other_sessions.js?
49  *
50  * @constructor
51  */
52 function DeviceContextMenuController() {
53   this.__proto__ = DeviceContextMenuController.prototype;
54   this.initialize();
55 }
56 cr.addSingletonGetter(DeviceContextMenuController);
57
58 // DeviceContextMenuController, Public: ---------------------------------------
59
60 /**
61  * Initialize the context menu for device names in the list of sessions.
62  */
63 DeviceContextMenuController.prototype.initialize = function() {
64   var menu = new cr.ui.Menu;
65   cr.ui.decorate(menu, cr.ui.Menu);
66   this.menu = menu;
67   this.collapseItem_ = this.appendMenuItem_('collapseSessionMenuItemText');
68   this.collapseItem_.addEventListener('activate',
69                                       this.onCollapseOrExpand_.bind(this));
70   this.expandItem_ = this.appendMenuItem_('expandSessionMenuItemText');
71   this.expandItem_.addEventListener('activate',
72                                     this.onCollapseOrExpand_.bind(this));
73   this.openAllItem_ = this.appendMenuItem_('restoreSessionMenuItemText');
74   this.openAllItem_.addEventListener('activate',
75                                      this.onOpenAll_.bind(this));
76 };
77
78 /**
79  * Set the session data for the session the context menu was invoked on.
80  * This should never be called when the menu is visible.
81  * @param {Object} session The model object for the session.
82  */
83 DeviceContextMenuController.prototype.setSession = function(session) {
84   this.session_ = session;
85   this.updateMenuItems_();
86 };
87
88 // DeviceContextMenuController, Private: --------------------------------------
89
90 /**
91  * Appends a menu item to |this.menu|.
92  * @param {string} textId The ID for the localized string that acts as
93  *     the item's label.
94  * @return {Element} The button used for a given menu option.
95  * @private
96  */
97 DeviceContextMenuController.prototype.appendMenuItem_ = function(textId) {
98   var button = document.createElement('button');
99   this.menu.appendChild(button);
100   cr.ui.decorate(button, cr.ui.MenuItem);
101   button.textContent = loadTimeData.getString(textId);
102   return button;
103 };
104
105 /**
106  * Handler for the 'Collapse' and 'Expand' menu items.
107  * @param {Event} e The activation event.
108  * @private
109  */
110 DeviceContextMenuController.prototype.onCollapseOrExpand_ = function(e) {
111   this.session_.collapsed = !this.session_.collapsed;
112   this.updateMenuItems_();
113   chrome.send('setForeignSessionCollapsed',
114               [this.session_.tag, this.session_.collapsed]);
115   chrome.send('getForeignSessions');  // Refresh the list.
116
117   var eventId = this.session_.collapsed ?
118       HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION;
119   recordUmaEvent_(eventId);
120 };
121
122 /**
123  * Handler for the 'Open all' menu item.
124  * @param {Event} e The activation event.
125  * @private
126  */
127 DeviceContextMenuController.prototype.onOpenAll_ = function(e) {
128   chrome.send('openForeignSession', [this.session_.tag]);
129   recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL);
130 };
131
132 /**
133  * Set the visibility of the Expand/Collapse menu items based on the state
134  * of the session that this menu is currently associated with.
135  * @private
136  */
137 DeviceContextMenuController.prototype.updateMenuItems_ = function() {
138   this.collapseItem_.hidden = this.session_.collapsed;
139   this.expandItem_.hidden = !this.session_.collapsed;
140 };
141
142
143 ///////////////////////////////////////////////////////////////////////////////
144 // Device:
145
146 /**
147  * Class to hold all the information about a device entry and generate a DOM
148  * node for it.
149  * @param {Object} session An object containing the device's session data.
150  * @param {DevicesView} view The view object this entry belongs to.
151  * @constructor
152  */
153 function Device(session, view) {
154   this.view_ = view;
155   this.session_ = session;
156   this.searchText_ = view.getSearchText();
157 }
158
159 // Device, Public: ------------------------------------------------------------
160
161 /**
162  * Get the DOM node to display this device.
163  * @param {int} maxNumTabs The maximum number of tabs to display.
164  * @param {int} row The row in which this device is displayed.
165  * @return {Object} A DOM node to draw the device.
166  */
167 Device.prototype.getDOMNode = function(maxNumTabs, row) {
168   var deviceDiv = createElementWithClassName('div', 'device');
169   this.row_ = row;
170   if (!this.session_)
171     return deviceDiv;
172
173   // Name heading
174   var heading = document.createElement('h3');
175   heading.textContent = this.session_.name;
176   heading.sessionData_ = this.session_;
177   deviceDiv.appendChild(heading);
178
179   // Keep track of the drop down that triggered the menu, so we know
180   // which element to apply the command to.
181   var session = this.session_;
182   function handleDropDownFocus(e) {
183     DeviceContextMenuController.getInstance().setSession(session);
184   }
185   heading.addEventListener('contextmenu', handleDropDownFocus);
186
187   var dropDownButton = new cr.ui.ContextMenuButton;
188   dropDownButton.classList.add('drop-down');
189   dropDownButton.addEventListener('mousedown', function(event) {
190       handleDropDownFocus(event);
191       // Mousedown handling of cr.ui.MenuButton.handleEvent calls
192       // preventDefault, which prevents blur of the focused element. We need to
193       // do blur manually.
194       document.activeElement.blur();
195   });
196   dropDownButton.addEventListener('focus', handleDropDownFocus);
197   heading.appendChild(dropDownButton);
198
199   var timeSpan = createElementWithClassName('div', 'device-timestamp');
200   timeSpan.textContent = this.session_.modifiedTime;
201   heading.appendChild(timeSpan);
202
203   cr.ui.contextMenuHandler.setContextMenu(
204       heading, DeviceContextMenuController.getInstance().menu);
205   if (!this.session_.collapsed)
206     deviceDiv.appendChild(this.createSessionContents_(maxNumTabs));
207
208   return deviceDiv;
209 };
210
211 /**
212  * Marks tabs as hidden or not in our session based on the given searchText.
213  * @param {string} searchText The search text used to filter the content.
214  */
215 Device.prototype.setSearchText = function(searchText) {
216   this.searchText_ = searchText.toLowerCase();
217   for (var i = 0; i < this.session_.windows.length; i++) {
218     var win = this.session_.windows[i];
219     var foundMatch = false;
220     for (var j = 0; j < win.tabs.length; j++) {
221       var tab = win.tabs[j];
222       if (tab.title.toLowerCase().indexOf(this.searchText_) != -1) {
223         foundMatch = true;
224         tab.hidden = false;
225       } else {
226         tab.hidden = true;
227       }
228     }
229     win.hidden = !foundMatch;
230   }
231 };
232
233 // Device, Private ------------------------------------------------------------
234
235 /**
236  * Create the DOM tree representing the tabs and windows of this device.
237  * @param {int} maxNumTabs The maximum number of tabs to display.
238  * @return {Element} A single div containing the list of tabs & windows.
239  * @private
240  */
241 Device.prototype.createSessionContents_ = function(maxNumTabs) {
242   var contents = createElementWithClassName('div', 'device-contents');
243
244   var sessionTag = this.session_.tag;
245   var numTabsShown = 0;
246   var numTabsHidden = 0;
247   for (var i = 0; i < this.session_.windows.length; i++) {
248     var win = this.session_.windows[i];
249     if (win.hidden)
250       continue;
251
252     // Show a separator between multiple windows in the same session.
253     if (i > 0 && numTabsShown < maxNumTabs)
254       contents.appendChild(document.createElement('hr'));
255
256     for (var j = 0; j < win.tabs.length; j++) {
257       var tab = win.tabs[j];
258       if (tab.hidden)
259         continue;
260
261       if (numTabsShown < maxNumTabs) {
262         numTabsShown++;
263         var a = createElementWithClassName('a', 'device-tab-entry');
264         a.href = tab.url;
265         a.style.backgroundImage = getFaviconImageSet(tab.url);
266         this.addHighlightedText_(a, tab.title);
267         // Add a tooltip, since it might be ellipsized. The ones that are not
268         // necessary will be removed once added to the document, so we can
269         // compute sizes.
270         a.title = tab.title;
271
272         // We need to use this to not lose the ids as we go through other loop
273         // turns.
274         function makeClickHandler(sessionTag, windowId, tabId) {
275           return function(e) {
276             recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED);
277             chrome.send('openForeignSession', [sessionTag, windowId, tabId,
278                 e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
279             e.preventDefault();
280           };
281         };
282         a.addEventListener('click', makeClickHandler(sessionTag,
283                                                      String(win.sessionId),
284                                                      String(tab.sessionId)));
285         contents.appendChild(a);
286       } else {
287         numTabsHidden++;
288       }
289     }
290   }
291
292   if (numTabsHidden > 0) {
293     var moreLinkButton = createElementWithClassName('button',
294         'device-show-more-tabs link-button');
295     moreLinkButton.addEventListener('click', this.view_.increaseRowHeight.bind(
296         this.view_, this.row_, numTabsHidden));
297     var xMore = loadTimeData.getString('xMore');
298     moreLinkButton.appendChild(
299         document.createTextNode(xMore.replace('$1', numTabsHidden)));
300     contents.appendChild(moreLinkButton);
301   }
302
303   return contents;
304 };
305
306 /**
307  * Add child text nodes to a node such that occurrences of this.searchText_ are
308  * highlighted.
309  * @param {Node} node The node under which new text nodes will be made as
310  *     children.
311  * @param {string} content Text to be added beneath |node| as one or more
312  *     text nodes.
313  * @private
314  */
315 Device.prototype.addHighlightedText_ = function(node, content) {
316   var endOfPreviousMatch = 0;
317   if (this.searchText_) {
318     var lowerContent = content.toLowerCase();
319     var searchTextLenght = this.searchText_.length;
320     var newMatch = lowerContent.indexOf(this.searchText_, 0);
321     while (newMatch != -1) {
322       if (newMatch > endOfPreviousMatch) {
323         node.appendChild(document.createTextNode(
324             content.slice(endOfPreviousMatch, newMatch)));
325       }
326       endOfPreviousMatch = newMatch + searchTextLenght;
327       // Mark the highlighted text in bold.
328       var b = document.createElement('b');
329       b.textContent = content.substring(newMatch, endOfPreviousMatch);
330       node.appendChild(b);
331       newMatch = lowerContent.indexOf(this.searchText_, endOfPreviousMatch);
332     }
333   }
334   if (endOfPreviousMatch < content.length) {
335     node.appendChild(document.createTextNode(
336         content.slice(endOfPreviousMatch)));
337   }
338 };
339
340 ///////////////////////////////////////////////////////////////////////////////
341 // DevicesView:
342
343 /**
344  * Functions and state for populating the page with HTML.
345  * @constructor
346  */
347 function DevicesView() {
348   this.devices_ = [];  // List of individual devices.
349   this.resultDiv_ = $('other-devices');
350   this.searchText_ = '';
351   this.rowHeights_ = [NB_ENTRIES_FIRST_ROW_COLUMN];
352   this.updateSignInState(loadTimeData.getBoolean('isUserSignedIn'));
353   recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED);
354 }
355
356 // DevicesView, public: -------------------------------------------------------
357
358 /**
359  * Updates our sign in state by clearing the view is not signed in or sending
360  * a request to get the data to display otherwise.
361  * @param {boolean} signedIn Whether the user is signed in or not.
362  */
363 DevicesView.prototype.updateSignInState = function(signedIn) {
364   if (signedIn)
365     chrome.send('getForeignSessions');
366   else
367     this.clearDOM();
368 };
369
370 /**
371  * Resets the view sessions.
372  * @param {Object} sessionList The sessions to add.
373  */
374 DevicesView.prototype.setSessionList = function(sessionList) {
375   this.devices_ = [];
376   for (var i = 0; i < sessionList.length; i++)
377     this.devices_.push(new Device(sessionList[i], this));
378   this.displayResults_();
379 };
380
381
382 /**
383  * Sets the current search text.
384  * @param {string} searchText The text to search.
385  */
386 DevicesView.prototype.setSearchText = function(searchText) {
387   if (this.searchText_ != searchText) {
388     this.searchText_ = searchText;
389     for (var i = 0; i < this.devices_.length; i++)
390       this.devices_[i].setSearchText(searchText);
391     this.displayResults_();
392   }
393 };
394
395 /**
396  * @return {string} The current search text.
397  */
398 DevicesView.prototype.getSearchText = function() {
399   return this.searchText_;
400 };
401
402 /**
403  * Clears the DOM content of the view.
404  */
405 DevicesView.prototype.clearDOM = function() {
406   while (this.resultDiv_.hasChildNodes()) {
407     this.resultDiv_.removeChild(this.resultDiv_.lastChild);
408   }
409 };
410
411 /**
412  * Increase the height of a row by the given amount.
413  * @param {int} row The row number.
414  * @param {int} height The extra height to add to the givent row.
415  */
416 DevicesView.prototype.increaseRowHeight = function(row, height) {
417   for (var i = this.rowHeights_.length; i <= row; i++)
418     this.rowHeights_.push(NB_ENTRIES_OTHER_ROWS_COLUMN);
419   this.rowHeights_[row] += height;
420   this.displayResults_();
421 };
422
423 // DevicesView, Private -------------------------------------------------------
424
425 /**
426  * Update the page with results.
427  * @private
428  */
429 DevicesView.prototype.displayResults_ = function() {
430   this.clearDOM();
431   var resultsFragment = document.createDocumentFragment();
432   if (this.devices_.length == 0)
433     return;
434
435   // We'll increase to 0 as we create the first row.
436   var rowIndex = -1;
437   // We need to access the last row and device when we get out of the loop.
438   var currentRowElement;
439   // This is only set when changing rows, yet used on all device columns.
440   var maxNumTabs;
441   for (var i = 0; i < this.devices_.length; i++) {
442     var device = this.devices_[i];
443     // Should we start a new row?
444     if (i % MAX_NUM_COLUMNS == 0) {
445       if (currentRowElement)
446         resultsFragment.appendChild(currentRowElement);
447       currentRowElement = createElementWithClassName('div', 'devices-row');
448       rowIndex++;
449       if (rowIndex < this.rowHeights_.length)
450         maxNumTabs = this.rowHeights_[rowIndex];
451       else
452         maxNumTabs = 0;
453     }
454
455     currentRowElement.appendChild(device.getDOMNode(maxNumTabs, rowIndex));
456   }
457   if (currentRowElement)
458     resultsFragment.appendChild(currentRowElement);
459
460   this.resultDiv_.appendChild(resultsFragment);
461   // Remove the tootltip on all lines that don't need it. It's easier to
462   // remove them here, after adding them all above, since we have the data
463   // handy above, but we don't have the width yet. Whereas here, we have the
464   // width, and the nodeValue could contain sub nodes for highlighting, which
465   // makes it harder to extract the text data here.
466   tabs = document.getElementsByClassName('device-tab-entry');
467   for (var i = 0; i < tabs.length; i++) {
468     if (tabs[i].scrollWidth <= tabs[i].clientWidth)
469       tabs[i].title = '';
470   }
471
472   this.resultDiv_.appendChild(
473       createElementWithClassName('div', 'other-devices-bottom'));
474 };
475
476 /**
477  * Sets the menu model data. An empty list means that either there are no
478  * foreign sessions, or tab sync is disabled for this profile.
479  * |isTabSyncEnabled| makes it possible to distinguish between the cases.
480  *
481  * @param {Array} sessionList Array of objects describing the sessions
482  *     from other devices.
483  * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
484  */
485 function setForeignSessions(sessionList, isTabSyncEnabled) {
486   // The other devices is shown iff tab sync is enabled.
487   if (isTabSyncEnabled)
488     devicesView.setSessionList(sessionList);
489   else
490     devicesView.clearDOM();
491 }
492
493 /**
494  * Called when this element is initialized, and from the new tab page when
495  * the user's signed in state changes,
496  * @param {string} header The first line of text (unused here).
497  * @param {string} subHeader The second line of text (unused here).
498  * @param {string} iconURL The url for the login status icon. If this is null
499  then the login status icon is hidden (unused here).
500  * @param {boolean} isUserSignedIn Is the user currently signed in?
501  */
502 function updateLogin(header, subHeader, iconURL, isUserSignedIn) {
503   if (devicesView)
504     devicesView.updateSignInState(isUserSignedIn);
505 }
506
507 ///////////////////////////////////////////////////////////////////////////////
508 // Document Functions:
509 /**
510  * Window onload handler, sets up the other devices view.
511  */
512 function load() {
513   if (!loadTimeData.getBoolean('isInstantExtendedApiEnabled'))
514     return;
515
516   // We must use this namespace to reuse the handler code for foreign session
517   // and login.
518   cr.define('ntp', function() {
519     return {
520       setForeignSessions: setForeignSessions,
521       updateLogin: updateLogin
522     };
523   });
524
525   devicesView = new DevicesView();
526
527   // Create the context menu that appears when the user right clicks
528   // on a device name or hit click on the button besides the device name
529   document.body.appendChild(DeviceContextMenuController.getInstance().menu);
530
531   var doSearch = function(e) {
532     devicesView.setSearchText($('search-field').value);
533   };
534   $('search-field').addEventListener('search', doSearch);
535   $('search-button').addEventListener('click', doSearch);
536 }
537
538 // Add handlers to HTML elements.
539 document.addEventListener('DOMContentLoaded', load);