Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / host_table_entry.js
blob44a9f5734afa4554f967717bc864a9ec0c51646f
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.
5 /**
6  * @fileoverview
7  * Class representing an entry in the host-list portion of the home screen.
8  */
10 'use strict';
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
15 /**
16  * An entry in the host table.
17  * @param {remoting.Host} host The host, as obtained from Apiary.
18  * @param {number} webappMajorVersion The major version nmber of the web-app,
19  *     used to identify out-of-date hosts.
20  * @param {function(remoting.HostTableEntry):void} onRename Callback for
21  *     rename operations.
22  * @param {function(remoting.HostTableEntry):void=} opt_onDelete Callback for
23  *     delete operations.
24  * @constructor
25  */
26 remoting.HostTableEntry = function(
27     host, webappMajorVersion, onRename, opt_onDelete) {
28   /** @type {remoting.Host} */
29   this.host = host;
30   /** @type {number} */
31   this.webappMajorVersion_ = webappMajorVersion;
32   /** @type {function(remoting.HostTableEntry):void} @private */
33   this.onRename_ = onRename;
34   /** @type {undefined|function(remoting.HostTableEntry):void} @private */
35   this.onDelete_ = opt_onDelete;
37   /** @type {HTMLElement} */
38   this.tableRow = null;
39   /** @type {HTMLElement} @private */
40   this.hostNameCell_ = null;
41   /** @type {HTMLElement} @private */
42   this.warningOverlay_ = null;
43   // References to event handlers so that they can be removed.
44   /** @type {function():void} @private */
45   this.onBlurReference_ = function() {};
46   /** @type {function():void} @private */
47   this.onConfirmDeleteReference_ = function() {};
48   /** @type {function():void} @private */
49   this.onCancelDeleteReference_ = function() {};
50   /** @type {function():void?} @private */
51   this.onConnectReference_ = null;
54 /**
55  * Create the HTML elements for this entry and set up event handlers.
56  * @return {void} Nothing.
57  */
58 remoting.HostTableEntry.prototype.createDom = function() {
59   // Create the top-level <div>
60   var tableRow = /** @type {HTMLElement} */ (document.createElement('div'));
61   tableRow.classList.add('section-row');
62   // Create the host icon cell.
63   var hostIconDiv = /** @type {HTMLElement} */ (document.createElement('div'));
64   hostIconDiv.classList.add('host-list-main-icon');
65   var warningOverlay =
66       /** @type {HTMLElement} */ (document.createElement('span'));
67   hostIconDiv.appendChild(warningOverlay);
68   var hostIcon = /** @type {HTMLElement} */ (document.createElement('img'));
69   hostIcon.src = 'icon_host.webp';
70   hostIconDiv.appendChild(hostIcon);
71   tableRow.appendChild(hostIconDiv);
72   // Create the host name cell.
73   var hostNameCell = /** @type {HTMLElement} */ (document.createElement('div'));
74   hostNameCell.classList.add('box-spacer');
75   hostNameCell.id = 'host_' + this.host.hostId;
76   tableRow.appendChild(hostNameCell);
77   // Create the host rename cell.
78   var editButton = /** @type {HTMLElement} */ (document.createElement('span'));
79   var editButtonImg =
80       /** @type {HTMLElement} */ (document.createElement('img'));
81   editButtonImg.title = chrome.i18n.getMessage(
82       /*i18n-content*/'TOOLTIP_RENAME');
83   editButtonImg.src = 'icon_pencil.webp';
84   editButton.tabIndex = 0;
85   editButton.classList.add('clickable');
86   editButton.classList.add('host-list-edit');
87   editButtonImg.classList.add('host-list-rename-icon');
88   editButton.appendChild(editButtonImg);
89   tableRow.appendChild(editButton);
90   // Create the host delete cell.
91   var deleteButton =
92       /** @type {HTMLElement} */ (document.createElement('span'));
93   var deleteButtonImg =
94       /** @type {HTMLElement} */ (document.createElement('img'));
95   deleteButtonImg.title =
96       chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_DELETE');
97   deleteButtonImg.src = 'icon_cross.webp';
98   deleteButton.tabIndex = 0;
99   deleteButton.classList.add('clickable');
100   deleteButton.classList.add('host-list-edit');
101   deleteButtonImg.classList.add('host-list-remove-icon');
102   deleteButton.appendChild(deleteButtonImg);
103   tableRow.appendChild(deleteButton);
105   this.init(tableRow, warningOverlay, hostNameCell, editButton, deleteButton);
109  * Associate the table row with the specified elements and callbacks, and set
110  * up event handlers.
112  * @param {HTMLElement} tableRow The top-level <div> for the table entry.
113  * @param {HTMLElement} warningOverlay The <span> element to render a warning
114  *     icon on top of the host icon.
115  * @param {HTMLElement} hostNameCell The element containing the host name.
116  * @param {HTMLElement} editButton The <img> containing the pencil icon for
117  *     editing the host name.
118  * @param {HTMLElement=} opt_deleteButton The <img> containing the cross icon
119  *     for deleting the host, if present.
120  * @return {void} Nothing.
121  */
122 remoting.HostTableEntry.prototype.init = function(
123     tableRow, warningOverlay, hostNameCell, editButton, opt_deleteButton) {
124   this.tableRow = tableRow;
125   this.warningOverlay_ = warningOverlay;
126   this.hostNameCell_ = hostNameCell;
127   this.setHostName_();
129   /** @type {remoting.HostTableEntry} */
130   var that = this;
132   /** @param {Event} event The click event. */
133   var beginRename = function(event) {
134     that.beginRename_();
135     event.stopPropagation();
136   };
137   /** @param {Event} event The keyup event. */
138   var beginRenameKeyboard = function(event) {
139     if (event.which == 13 || event.which == 32) {
140       that.beginRename_();
141       event.stopPropagation();
142     }
143   };
144   editButton.addEventListener('click', beginRename, true);
145   editButton.addEventListener('keyup', beginRenameKeyboard, true);
146   this.registerFocusHandlers_(editButton);
148   if (opt_deleteButton) {
149     /** @param {Event} event The click event. */
150     var confirmDelete = function(event) {
151       that.showDeleteConfirmation_();
152       event.stopPropagation();
153     };
154     /** @param {Event} event The keyup event. */
155     var confirmDeleteKeyboard = function(event) {
156       if (event.which == 13 || event.which == 32) {
157         that.showDeleteConfirmation_();
158       }
159     };
160     opt_deleteButton.addEventListener('click', confirmDelete, false);
161     opt_deleteButton.addEventListener('keyup', confirmDeleteKeyboard, false);
162     this.registerFocusHandlers_(opt_deleteButton);
163   }
164   this.updateStatus();
168  * Update the row to reflect the current status of the host (online/offline and
169  * clickable/unclickable).
171  * @param {boolean=} opt_forEdit True if the status is being updated in order
172  *     to allow the host name to be edited.
173  * @return {void} Nothing.
174  */
175 remoting.HostTableEntry.prototype.updateStatus = function(opt_forEdit) {
176   var clickToConnect = this.host.status == 'ONLINE' && !opt_forEdit;
177   if (clickToConnect) {
178     if (!this.onConnectReference_) {
179       /** @type {string} */
180       var encodedHostId = encodeURIComponent(this.host.hostId);
181       this.onConnectReference_ = function() {
182         remoting.connectMe2Me(encodedHostId);
183       };
184       this.tableRow.addEventListener('click', this.onConnectReference_, false);
185     }
186     this.tableRow.classList.add('clickable');
187     this.tableRow.title = chrome.i18n.getMessage(
188         /*i18n-content*/'TOOLTIP_CONNECT', this.host.hostName);
189   } else {
190     if (this.onConnectReference_) {
191       this.tableRow.removeEventListener('click', this.onConnectReference_,
192                                         false);
193       this.onConnectReference_ = null;
194     }
195     this.tableRow.classList.remove('clickable');
196     this.tableRow.title = '';
197   }
198   var showOffline = this.host.status != 'ONLINE';
199   if (showOffline) {
200     this.tableRow.classList.remove('host-online');
201     this.tableRow.classList.add('host-offline');
202   } else {
203     this.tableRow.classList.add('host-online');
204     this.tableRow.classList.remove('host-offline');
205   }
206   var hostReportedError = this.host.hostOfflineReason != '';
207   var hostNeedsUpdate = remoting.Host.needsUpdate(
208       this.host, this.webappMajorVersion_);
209   this.warningOverlay_.hidden = !hostNeedsUpdate && !hostReportedError;
213  * Prepare the host for renaming by replacing its name with an edit box.
214  * @return {void} Nothing.
215  * @private
216  */
217 remoting.HostTableEntry.prototype.beginRename_ = function() {
218   var editBox =
219       /** @type {HTMLInputElement} */ (document.createElement('input'));
220   editBox.type = 'text';
221   editBox.value = this.host.hostName;
222   this.hostNameCell_.innerText = '';
223   this.hostNameCell_.appendChild(editBox);
224   editBox.select();
226   this.onBlurReference_ = this.commitRename_.bind(this);
227   editBox.addEventListener('blur', this.onBlurReference_, false);
229   editBox.addEventListener('keydown', this.onKeydown_.bind(this), false);
230   this.updateStatus(true);
234  * Accept the hostname entered by the user.
235  * @return {void} Nothing.
236  * @private
237  */
238 remoting.HostTableEntry.prototype.commitRename_ = function() {
239   var editBox = this.hostNameCell_.querySelector('input');
240   if (editBox) {
241     if (this.host.hostName != editBox.value) {
242       this.host.hostName = editBox.value;
243       this.onRename_(this);
244     }
245     this.removeEditBox_();
246   }
250  * Prompt the user to confirm or cancel deletion of a host.
251  * @return {void} Nothing.
252  * @private
253  */
254 remoting.HostTableEntry.prototype.showDeleteConfirmation_ = function() {
255   var message = document.getElementById('confirm-host-delete-message');
256   l10n.localizeElement(message, this.host.hostName);
257   var confirm = document.getElementById('confirm-host-delete');
258   var cancel = document.getElementById('cancel-host-delete');
259   this.onConfirmDeleteReference_ = this.confirmDelete_.bind(this);
260   this.onCancelDeleteReference_ = this.cancelDelete_.bind(this);
261   confirm.addEventListener('click', this.onConfirmDeleteReference_, false);
262   cancel.addEventListener('click', this.onCancelDeleteReference_, false);
263   remoting.setMode(remoting.AppMode.CONFIRM_HOST_DELETE);
267  * Confirm deletion of a host.
268  * @return {void} Nothing.
269  * @private
270  */
271 remoting.HostTableEntry.prototype.confirmDelete_ = function() {
272   this.onDelete_(this);
273   this.cleanUpConfirmationEventListeners_();
274   remoting.setMode(remoting.AppMode.HOME);
278  * Cancel deletion of a host.
279  * @return {void} Nothing.
280  * @private
281  */
282 remoting.HostTableEntry.prototype.cancelDelete_ = function() {
283   this.cleanUpConfirmationEventListeners_();
284   remoting.setMode(remoting.AppMode.HOME);
288  * Remove the confirm and cancel event handlers, which refer to this object.
289  * @return {void} Nothing.
290  * @private
291  */
292 remoting.HostTableEntry.prototype.cleanUpConfirmationEventListeners_ =
293     function() {
294   var confirm = document.getElementById('confirm-host-delete');
295   var cancel = document.getElementById('cancel-host-delete');
296   confirm.removeEventListener('click', this.onConfirmDeleteReference_, false);
297   cancel.removeEventListener('click', this.onCancelDeleteReference_, false);
298   this.onCancelDeleteReference_ = function() {};
299   this.onConfirmDeleteReference_ = function() {};
303  * Remove the edit box corresponding to the specified host, and reset its name.
304  * @return {void} Nothing.
305  * @private
306  */
307 remoting.HostTableEntry.prototype.removeEditBox_ = function() {
308   var editBox = this.hostNameCell_.querySelector('input');
309   if (editBox) {
310     // onblur will fire when the edit box is removed, so remove the hook.
311     editBox.removeEventListener('blur', this.onBlurReference_, false);
312   }
313   // Update the tool-top and event handler.
314   this.updateStatus();
315   this.setHostName_();
319  * Formats host's updateTime value relative to current time (i.e. only
320  * displaying hours and minutes if updateTime is less than a day in the past).
321  * @param {string} updateTime RFC 3339 formatted date-time value.
322  * @return {string} Formatted value (i.e. 11/11/2014)
323  */
324 function formatUpdateTime(updateTime) {
325   var lastOnline = new Date(updateTime);
326   var now = new Date();
327   var displayString = '';
328   if (now.getFullYear() == lastOnline.getFullYear() &&
329       now.getMonth() == lastOnline.getMonth() &&
330       now.getDate() == lastOnline.getDate()) {
331     return lastOnline.toLocaleTimeString();
332   } else {
333     return lastOnline.toLocaleDateString();
334   }
338  * Formats host's host-offline-reason value (i.e. 'INVALID_HOST_CONFIGURATION')
339  * to a human-readable description of the error.
340  * @param {string} hostOfflineReason
341  * @return {string}
342  */
343 function formatHostOfflineReason(hostOfflineReason) {
344   var knownReasonTags = [
345     /*i18n-content*/ 'OFFLINE_REASON_INITIALIZATION_FAILED',
346     /*i18n-content*/ 'OFFLINE_REASON_INVALID_HOST_CONFIGURATION',
347     /*i18n-content*/ 'OFFLINE_REASON_INVALID_HOST_DOMAIN',
348     /*i18n-content*/ 'OFFLINE_REASON_INVALID_HOST_ID',
349     /*i18n-content*/ 'OFFLINE_REASON_INVALID_OAUTH_CREDENTIALS',
350     /*i18n-content*/ 'OFFLINE_REASON_LOGIN_SCREEN_NOT_SUPPORTED',
351     /*i18n-content*/ 'OFFLINE_REASON_POLICY_CHANGE_REQUIRES_RESTART',
352     /*i18n-content*/ 'OFFLINE_REASON_POLICY_READ_ERROR',
353     /*i18n-content*/ 'OFFLINE_REASON_SUCCESS_EXIT',
354     /*i18n-content*/ 'OFFLINE_REASON_USERNAME_MISMATCH'
355   ];
356   var offlineReasonTag = 'OFFLINE_REASON_' + hostOfflineReason;
357   if (knownReasonTags.indexOf(offlineReasonTag) != (-1)) {
358     return chrome.i18n.getMessage(offlineReasonTag);
359   } else {
360     return chrome.i18n.getMessage(
361         /*i18n-content*/ 'OFFLINE_REASON_UNKNOWN',
362         hostOfflineReason);
363   }
367  * Create the DOM nodes and event handlers for the hostname cell.
368  * @return {void} Nothing.
369  * @private
370  */
371 remoting.HostTableEntry.prototype.setHostName_ = function() {
372   var hostNameNode = /** @type {HTMLElement} */ (document.createElement('a'));
373   if (this.host.status == 'ONLINE') {
374     if (remoting.Host.needsUpdate(this.host, this.webappMajorVersion_)) {
375       hostNameNode.innerText = chrome.i18n.getMessage(
376           /*i18n-content*/'UPDATE_REQUIRED', this.host.hostName);
377     } else {
378       hostNameNode.innerText = this.host.hostName;
379     }
380     hostNameNode.href = '#';
381     this.registerFocusHandlers_(hostNameNode);
382     /** @type {remoting.HostTableEntry} */
383     var that = this;
384     /** @param {Event} event */
385     var onKeyDown = function(event) {
386       if (that.onConnectReference_ &&
387           (event.which == 13 || event.which == 32)) {
388         that.onConnectReference_();
389       }
390     };
391     hostNameNode.addEventListener('keydown', onKeyDown, false);
392   } else {
393     if (this.host.updatedTime) {
394       var formattedTime = formatUpdateTime(this.host.updatedTime);
395       hostNameNode.innerText = chrome.i18n.getMessage(
396           /*i18n-content*/'LAST_ONLINE', [this.host.hostName, formattedTime]);
397       if (this.host.hostOfflineReason) {
398         var detailsText = formatHostOfflineReason(this.host.hostOfflineReason);
399         // TODO(lukasza): Put detailsText into a hideable div (title/tooltip
400         // is not as discoverable + doesn't work well for touchscreens).
401         hostNameNode.title = detailsText;
402       }
403     } else {
404       hostNameNode.innerText = chrome.i18n.getMessage(
405           /*i18n-content*/'OFFLINE', this.host.hostName);
406     }
407   }
408   hostNameNode.classList.add('host-list-label');
409   this.hostNameCell_.innerText = '';  // Remove previous contents (if any).
410   this.hostNameCell_.appendChild(hostNameNode);
414  * Handle a key event while the user is typing a host name
415  * @param {Event} event The keyboard event.
416  * @return {void} Nothing.
417  * @private
418  */
419 remoting.HostTableEntry.prototype.onKeydown_ = function(event) {
420   if (event.which == 27) {  // Escape
421     this.removeEditBox_();
422   } else if (event.which == 13) {  // Enter
423     this.commitRename_();
424   }
428  * Register focus and blur handlers to cause the parent node to be highlighted
429  * whenever a child link has keyboard focus. Note that this is only necessary
430  * because Chrome does not yet support the draft CSS Selectors 4 specification
431  * (http://www.w3.org/TR/selectors4/#subject), which provides a more elegant
432  * solution to this problem.
434  * @param {HTMLElement} e The element on which to register the event handlers.
435  * @return {void} Nothing.
436  * @private
437  */
438 remoting.HostTableEntry.prototype.registerFocusHandlers_ = function(e) {
439   e.addEventListener('focus', this.onFocusChange_.bind(this), false);
440   e.addEventListener('blur', this.onFocusChange_.bind(this), false);
444  * Handle a focus change event within this table row.
445  * @return {void} Nothing.
446  * @private
447  */
448 remoting.HostTableEntry.prototype.onFocusChange_ = function() {
449   var element = document.activeElement;
450   while (element) {
451     if (element == this.tableRow) {
452       this.tableRow.classList.add('child-focused');
453       return;
454     }
455     element = element.parentNode;
456   }
457   this.tableRow.classList.remove('child-focused');