Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(288)

Side by Side Diff: chrome/browser/resources/history/other_devices.js

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

Powered by Google App Engine
This is Rietveld 408576698