Roll src/third_party/WebKit 3529d49:06e8485 (svn 202554:202555)
[chromium-blink-merge.git] / remoting / webapp / crd / js / host_table_entry.js
blob6c06c5d8f45b63821ae00667dc4c615204de2fed
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 /** @suppress {duplicate} */
11 var remoting = remoting || {};
13 (function() {
15 'use strict';
17 /**
18  * An entry in the host table.
19  *
20  * @param {number} webappMajorVersion The major version nmber of the web-app,
21  *     used to identify out-of-date hosts.
22  * @param {function(string):void} onConnect Callback for
23  *     connect operations.
24  * @param {function(remoting.HostTableEntry):void} onRename Callback for
25  *     rename operations.
26  * @param {function(remoting.HostTableEntry):void=} opt_onDelete Callback for
27  *     delete operations.
28  *
29  * @constructor
30  * @implements {base.Disposable}
31  */
32 remoting.HostTableEntry = function(
33     webappMajorVersion, onConnect, onRename, opt_onDelete) {
34   /** @type {remoting.Host} */
35   this.host = null;
36   /** @private {number} */
37   this.webappMajorVersion_ = webappMajorVersion;
38   /** @private {function(remoting.HostTableEntry):void} */
39   this.onRename_ = onRename;
40   /** @private {undefined|function(remoting.HostTableEntry):void} */
41   this.onDelete_ = opt_onDelete;
42   /** @private {function(string):void} */
43   this.onConnect_ = onConnect;
45   /** @private {HTMLElement} */
46   this.rootElement_ = null;
47   /** @private {HTMLElement} */
48   this.hostNameLabel_ = null;
49   /** @private {HTMLInputElement} */
50   this.renameInputField_ = null;
51   /** @private {HTMLElement} */
52   this.warningOverlay_ = null;
54   /** @private {base.Disposables} */
55   this.renameInputEventHooks_ = null;
56   /** @private {base.Disposables} */
57   this.disposables_ = new base.Disposables();
59   // References to event handlers so that they can be removed.
60   /** @private {function():void} */
61   this.onConfirmDeleteReference_ = function() {};
62   /** @private {function():void} */
63   this.onCancelDeleteReference_ = function() {};
65   this.createDom_();
68 /** @param {remoting.Host} host */
69 remoting.HostTableEntry.prototype.setHost = function(host) {
70   this.host = host;
71   this.updateUI_();
74 /** @return {HTMLElement} */
75 remoting.HostTableEntry.prototype.element = function() {
76   return this.rootElement_;
79 remoting.HostTableEntry.prototype.dispose = function() {
80   base.dispose(this.disposables_);
81   this.disposables_ = null;
82   base.dispose(this.renameInputEventHooks_);
83   this.renameInputEventHooks_ = null;
86 /** @return {string} */
87 remoting.HostTableEntry.prototype.getHTML_ = function() {
88   var html =
89     '<div class="host-list-main-icon">' +
90       '<span class="warning-overlay"></span>' +
91       '<img src="icon_host.webp">' +
92     '</div>' +
93     '<div class="box-spacer">' +
94       '<a class="host-name-label" href="#""></a>' +
95       '<input class="host-rename-input" type="text" hidden/>' +
96     '</div>' +
97     '<span tabindex="0" class="clickable host-list-edit rename-button">' +
98       '<img src="icon_pencil.webp" class="host-list-rename-icon">' +
99     '</span>';
100   if (this.onDelete_) {
101     html +=
102     '<span tabindex="0" class="clickable host-list-edit delete-button">' +
103        '<img src="icon_cross.webp" class="host-list-remove-icon">' +
104     '</span>';
105   }
106   return '<div class="section-row host-offline">' + html + '</div>';
110  * Create the HTML elements for this entry and set up event handlers.
111  * @return {void} Nothing.
112  */
113 remoting.HostTableEntry.prototype.createDom_ = function() {
114   var container = /** @type {HTMLElement} */ (document.createElement('div'));
115   container.innerHTML = this.getHTML_();
117   // Setup DOM references.
118   this.rootElement_ = /** @type {HTMLElement} */ (container.firstElementChild);
119   this.warningOverlay_ = container.querySelector('.warning-overlay');
120   this.hostNameLabel_ = container.querySelector('.host-name-label');
121   this.renameInputField_ = /** @type {HTMLInputElement} */ (
122       container.querySelector('.host-rename-input'));
124   // Register event handlers and set tooltips.
125   var editButton = container.querySelector('.rename-button');
126   var deleteButton = container.querySelector('.delete-button');
127   editButton.title = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_RENAME');
128   this.registerButton_(editButton, this.beginRename_.bind(this));
129   this.registerButton_(this.rootElement_, this.onConnectButton_.bind(this));
130   if (deleteButton) {
131     this.registerButton_(deleteButton, this.showDeleteConfirmation_.bind(this));
132     deleteButton.title =
133         chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_DELETE');
134   }
137 /** @return {base.Disposable} @private */
138 remoting.HostTableEntry.prototype.registerButton_ = function(
139     /** HTMLElement */ button, /** Function */ callback) {
140   var onKeyDown = function(/** Event */ e) {
141     if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACEBAR) {
142       callback();
143       e.stopPropagation();
144     }
145   };
146   var onClick = function(/** Event */ e) {
147     callback();
148     e.stopPropagation();
149   };
150   var onFocusChanged = this.onFocusChange_.bind(this);
151   this.disposables_.add(
152       new base.DomEventHook(button, 'click', onClick, false),
153       new base.DomEventHook(button, 'keydown', onKeyDown, false),
154       // Register focus and blur handlers to cause the parent node to be
155       // highlighted whenever a child link has keyboard focus. Note that this is
156       // only necessary because Chrome does not yet support the draft CSS
157       // Selectors 4 specification (http://www.w3.org/TR/selectors4/#subject),
158       // which provides a more elegant solution to this problem.
159       new base.DomEventHook(button, 'focus', onFocusChanged, false),
160       new base.DomEventHook(button, 'blur', onFocusChanged, false));
164 /** @return {void} @private */
165 remoting.HostTableEntry.prototype.onConnectButton_ = function() {
166   // Don't connect during renaming as this event fires if the user hits Enter
167   // after typing a new name.
168   if (!this.isRenaming_() && this.isOnline_()) {
169     var encodedHostId = encodeURIComponent(this.host.hostId);
170     this.onConnect_(encodedHostId);
171   }
174 /** @return {boolean} @private */
175 remoting.HostTableEntry.prototype.isOnline_ = function() {
176   return Boolean(this.host) && this.host.status === 'ONLINE';
179 /** @return {string} @private */
180 remoting.HostTableEntry.prototype.getHostDisplayName_ = function() {
181   if (this.isOnline_()) {
182     if (remoting.Host.needsUpdate(this.host, this.webappMajorVersion_)) {
183       return  chrome.i18n.getMessage(
184           /*i18n-content*/'UPDATE_REQUIRED', this.host.hostName);
185     }
186     return this.host.hostName;
187   } else {
188     if (this.host.updatedTime) {
189       var formattedTime = formatUpdateTime(this.host.updatedTime);
190       return chrome.i18n.getMessage(/*i18n-content*/ 'LAST_ONLINE',
191                                     [this.host.hostName, formattedTime]);
192     }
193     return chrome.i18n.getMessage(/*i18n-content*/ 'OFFLINE',
194                                   this.host.hostName);
195   }
199  * Update the UI to reflect the current status of the host
201  * @return {void} Nothing.
202  * @private
203  */
204 remoting.HostTableEntry.prototype.updateUI_ = function() {
205   if (!this.host) {
206     return;
207   }
208   var clickToConnect = this.isOnline_() && !this.isRenaming_();
209   var showOffline = !this.isOnline_();
210   this.rootElement_.querySelector('.box-spacer').id =
211       'host_' + this.host.hostId;
212   var connectLabel = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
213                                             this.host.hostName);
214   this.rootElement_.classList.toggle('clickable', clickToConnect);
215   this.rootElement_.title = (clickToConnect) ? connectLabel : '';
216   this.rootElement_.classList.toggle('host-online', !showOffline);
217   this.rootElement_.classList.toggle('host-offline', showOffline);
219   var hostReportedError = this.host.hostOfflineReason !== '';
220   var hostNeedsUpdate = remoting.Host.needsUpdate(
221       this.host, this.webappMajorVersion_);
222   this.warningOverlay_.hidden = !hostNeedsUpdate && !hostReportedError;
223   this.hostNameLabel_.innerText = this.getHostDisplayName_();
224   this.hostNameLabel_.title =
225       formatHostOfflineReason(this.host.hostOfflineReason);
226   this.renameInputField_.hidden = !this.isRenaming_();
227   this.hostNameLabel_.hidden = this.isRenaming_();
231  * Prepares the host for renaming by showing an edit box.
232  * @return {void} Nothing.
233  * @private
234  */
235 remoting.HostTableEntry.prototype.beginRename_ = function() {
236   this.renameInputField_.value = this.host.hostName;
237   console.assert(this.renameInputEventHooks_ === null,
238                  '|renameInputEventHooks_| already exists.');
239   base.dispose(this.renameInputEventHooks_);
240   this.renameInputEventHooks_ = new base.Disposables(
241       new base.DomEventHook(this.renameInputField_, 'blur',
242                             this.commitRename_.bind(this), false),
243       new base.DomEventHook(this.renameInputField_, 'keydown',
244                             this.onKeydown_.bind(this), false));
245   this.updateUI_();
246   this.renameInputField_.focus();
249 /** @return {boolean} @private */
250 remoting.HostTableEntry.prototype.isRenaming_ = function() {
251   return Boolean(this.renameInputEventHooks_);
254 /** @return {void} @private */
255 remoting.HostTableEntry.prototype.commitRename_ = function() {
256   if (this.host.hostName != this.renameInputField_.value) {
257     this.host.hostName = this.renameInputField_.value;
258     this.onRename_(this);
259   }
260   this.hideEditBox_();
263 /** @return {void} @private */
264 remoting.HostTableEntry.prototype.hideEditBox_ = function() {
265   // onblur will fire when the edit box is removed, so remove the hook.
266   base.dispose(this.renameInputEventHooks_);
267   this.renameInputEventHooks_ = null;
268   // Update the tool-tip and event handler.
269   this.updateUI_();
273  * Handle a key event while the user is typing a host name
274  * @param {Event} event The keyboard event.
275  * @return {void} Nothing.
276  * @private
277  */
278 remoting.HostTableEntry.prototype.onKeydown_ = function(event) {
279   if (event.which == KeyCodes.ESCAPE) {
280     this.hideEditBox_();
281   } else if (event.which == KeyCodes.ENTER) {
282     this.commitRename_();
283     event.stopPropagation();
284   }
288  * Prompt the user to confirm or cancel deletion of a host.
289  * @return {void} Nothing.
290  * @private
291  */
292 remoting.HostTableEntry.prototype.showDeleteConfirmation_ = function() {
293   var message = document.getElementById('confirm-host-delete-message');
294   l10n.localizeElement(message, this.host.hostName);
295   var confirm = document.getElementById('confirm-host-delete');
296   var cancel = document.getElementById('cancel-host-delete');
297   this.onConfirmDeleteReference_ = this.confirmDelete_.bind(this);
298   this.onCancelDeleteReference_ = this.cancelDelete_.bind(this);
299   confirm.addEventListener('click', this.onConfirmDeleteReference_, false);
300   cancel.addEventListener('click', this.onCancelDeleteReference_, false);
301   remoting.setMode(remoting.AppMode.CONFIRM_HOST_DELETE);
305  * Confirm deletion of a host.
306  * @return {void} Nothing.
307  * @private
308  */
309 remoting.HostTableEntry.prototype.confirmDelete_ = function() {
310   this.onDelete_(this);
311   this.cleanUpConfirmationEventListeners_();
312   remoting.setMode(remoting.AppMode.HOME);
316  * Cancel deletion of a host.
317  * @return {void} Nothing.
318  * @private
319  */
320 remoting.HostTableEntry.prototype.cancelDelete_ = function() {
321   this.cleanUpConfirmationEventListeners_();
322   remoting.setMode(remoting.AppMode.HOME);
326  * Remove the confirm and cancel event handlers, which refer to this object.
327  * @return {void} Nothing.
328  * @private
329  */
330 remoting.HostTableEntry.prototype.cleanUpConfirmationEventListeners_ =
331     function() {
332   var confirm = document.getElementById('confirm-host-delete');
333   var cancel = document.getElementById('cancel-host-delete');
334   confirm.removeEventListener('click', this.onConfirmDeleteReference_, false);
335   cancel.removeEventListener('click', this.onCancelDeleteReference_, false);
336   this.onCancelDeleteReference_ = function() {};
337   this.onConfirmDeleteReference_ = function() {};
341  * Handle a focus change event within this table row.
342  * @return {void} Nothing.
343  * @private
344  */
345 remoting.HostTableEntry.prototype.onFocusChange_ = function() {
346   var element = document.activeElement;
347   while (element) {
348     if (element == this.rootElement_) {
349       this.rootElement_.classList.add('child-focused');
350       return;
351     }
352     element = element.parentNode;
353   }
354   this.rootElement_.classList.remove('child-focused');
358  * Formats host's updateTime value relative to current time (i.e. only
359  * displaying hours and minutes if updateTime is less than a day in the past).
360  * @param {string} updateTime RFC 3339 formatted date-time value.
361  * @return {string} Formatted value (i.e. 11/11/2014)
362  */
363 function formatUpdateTime(updateTime) {
364   var lastOnline = new Date(updateTime);
365   var now = new Date();
366   var displayString = '';
367   if (now.getFullYear() == lastOnline.getFullYear() &&
368       now.getMonth() == lastOnline.getMonth() &&
369       now.getDate() == lastOnline.getDate()) {
370     return lastOnline.toLocaleTimeString();
371   } else {
372     return lastOnline.toLocaleDateString();
373   }
377  * Formats host's host-offline-reason value (i.e. 'INVALID_HOST_CONFIGURATION')
378  * to a human-readable description of the error.
379  * @param {string} hostOfflineReason
380  * @return {string}
381  */
382 function formatHostOfflineReason(hostOfflineReason) {
383   if (!hostOfflineReason) {
384     return '';
385   }
386   var knownReasonTags = [
387     /*i18n-content*/'OFFLINE_REASON_INITIALIZATION_FAILED',
388     /*i18n-content*/'OFFLINE_REASON_INVALID_HOST_CONFIGURATION',
389     /*i18n-content*/'OFFLINE_REASON_INVALID_HOST_DOMAIN',
390     /*i18n-content*/'OFFLINE_REASON_INVALID_HOST_ID',
391     /*i18n-content*/'OFFLINE_REASON_INVALID_OAUTH_CREDENTIALS',
392     /*i18n-content*/'OFFLINE_REASON_LOGIN_SCREEN_NOT_SUPPORTED',
393     /*i18n-content*/'OFFLINE_REASON_POLICY_CHANGE_REQUIRES_RESTART',
394     /*i18n-content*/'OFFLINE_REASON_POLICY_READ_ERROR',
395     /*i18n-content*/'OFFLINE_REASON_SUCCESS_EXIT',
396     /*i18n-content*/'OFFLINE_REASON_USERNAME_MISMATCH'
397   ];
398   var offlineReasonTag = 'OFFLINE_REASON_' + hostOfflineReason;
399   if (knownReasonTags.indexOf(offlineReasonTag) != (-1)) {
400     return chrome.i18n.getMessage(offlineReasonTag);
401   } else {
402     return chrome.i18n.getMessage(
403         /*i18n-content*/'OFFLINE_REASON_UNKNOWN', hostOfflineReason);
404   }
407 /** @enum {number} */
408 var KeyCodes = {
409   ENTER: 13,
410   ESCAPE: 27,
411   SPACEBAR: 32
414 })();