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

Side by Side Diff: ios/chrome/app/resources/history/other_devices.js

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

Powered by Google App Engine
This is Rietveld 408576698