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.
7 * Class representing an entry in the host-list portion of the home screen.
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
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
22 * @param {function(remoting.HostTableEntry):void=} opt_onDelete Callback for
26 remoting.HostTableEntry = function(
27 host, webappMajorVersion, onRename, opt_onDelete) {
28 /** @type {remoting.Host} */
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} */
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;
55 * Create the HTML elements for this entry and set up event handlers.
56 * @return {void} Nothing.
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');
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'));
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.
92 /** @type {HTMLElement} */ (document.createElement('span'));
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
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.
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;
129 /** @type {remoting.HostTableEntry} */
132 /** @param {Event} event The click event. */
133 var beginRename = function(event) {
135 event.stopPropagation();
137 /** @param {Event} event The keyup event. */
138 var beginRenameKeyboard = function(event) {
139 if (event.which == 13 || event.which == 32) {
141 event.stopPropagation();
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();
154 /** @param {Event} event The keyup event. */
155 var confirmDeleteKeyboard = function(event) {
156 if (event.which == 13 || event.which == 32) {
157 that.showDeleteConfirmation_();
160 opt_deleteButton.addEventListener('click', confirmDelete, false);
161 opt_deleteButton.addEventListener('keyup', confirmDeleteKeyboard, false);
162 this.registerFocusHandlers_(opt_deleteButton);
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.
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);
184 this.tableRow.addEventListener('click', this.onConnectReference_, false);
186 this.tableRow.classList.add('clickable');
187 this.tableRow.title = chrome.i18n.getMessage(
188 /*i18n-content*/'TOOLTIP_CONNECT', this.host.hostName);
190 if (this.onConnectReference_) {
191 this.tableRow.removeEventListener('click', this.onConnectReference_,
193 this.onConnectReference_ = null;
195 this.tableRow.classList.remove('clickable');
196 this.tableRow.title = '';
198 var showOffline = this.host.status != 'ONLINE';
200 this.tableRow.classList.remove('host-online');
201 this.tableRow.classList.add('host-offline');
203 this.tableRow.classList.add('host-online');
204 this.tableRow.classList.remove('host-offline');
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.
217 remoting.HostTableEntry.prototype.beginRename_ = function() {
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);
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.
238 remoting.HostTableEntry.prototype.commitRename_ = function() {
239 var editBox = this.hostNameCell_.querySelector('input');
241 if (this.host.hostName != editBox.value) {
242 this.host.hostName = editBox.value;
243 this.onRename_(this);
245 this.removeEditBox_();
250 * Prompt the user to confirm or cancel deletion of a host.
251 * @return {void} Nothing.
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.
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.
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.
292 remoting.HostTableEntry.prototype.cleanUpConfirmationEventListeners_ =
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.
307 remoting.HostTableEntry.prototype.removeEditBox_ = function() {
308 var editBox = this.hostNameCell_.querySelector('input');
310 // onblur will fire when the edit box is removed, so remove the hook.
311 editBox.removeEventListener('blur', this.onBlurReference_, false);
313 // Update the tool-top and event handler.
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)
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();
333 return lastOnline.toLocaleDateString();
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
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'
356 var offlineReasonTag = 'OFFLINE_REASON_' + hostOfflineReason;
357 if (knownReasonTags.indexOf(offlineReasonTag) != (-1)) {
358 return chrome.i18n.getMessage(offlineReasonTag);
360 return chrome.i18n.getMessage(
361 /*i18n-content*/ 'OFFLINE_REASON_UNKNOWN',
367 * Create the DOM nodes and event handlers for the hostname cell.
368 * @return {void} Nothing.
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);
378 hostNameNode.innerText = this.host.hostName;
380 hostNameNode.href = '#';
381 this.registerFocusHandlers_(hostNameNode);
382 /** @type {remoting.HostTableEntry} */
384 /** @param {Event} event */
385 var onKeyDown = function(event) {
386 if (that.onConnectReference_ &&
387 (event.which == 13 || event.which == 32)) {
388 that.onConnectReference_();
391 hostNameNode.addEventListener('keydown', onKeyDown, false);
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;
404 hostNameNode.innerText = chrome.i18n.getMessage(
405 /*i18n-content*/'OFFLINE', this.host.hostName);
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.
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_();
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.
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.
448 remoting.HostTableEntry.prototype.onFocusChange_ = function() {
449 var element = document.activeElement;
451 if (element == this.tableRow) {
452 this.tableRow.classList.add('child-focused');
455 element = element.parentNode;
457 this.tableRow.classList.remove('child-focused');