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.
4
5/**
6 * @fileoverview
7 * Class representing an entry in the host-list portion of the home screen.
8 */
9
10'use strict';
11
12/** @suppress {duplicate} */
13var remoting = remoting || {};
14
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 */
26remoting.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;
36
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;
52};
53
54/**
55 * Create the HTML elements for this entry and set up event handlers.
56 * @return {void} Nothing.
57 */
58remoting.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 = /** @type {HTMLElement} */ document.createElement('img');
80  editButtonImg.title = chrome.i18n.getMessage(
81      /*i18n-content*/'TOOLTIP_RENAME');
82  editButtonImg.src = 'icon_pencil.webp';
83  editButton.tabIndex = 0;
84  editButton.classList.add('clickable');
85  editButton.classList.add('host-list-edit');
86  editButtonImg.classList.add('host-list-rename-icon');
87  editButton.appendChild(editButtonImg);
88  tableRow.appendChild(editButton);
89  // Create the host delete cell.
90  var deleteButton = /** @type {HTMLElement} */ document.createElement('span');
91  var deleteButtonImg =
92      /** @type {HTMLElement} */ document.createElement('img');
93  deleteButtonImg.title =
94      chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_DELETE');
95  deleteButtonImg.src = 'icon_cross.webp';
96  deleteButton.tabIndex = 0;
97  deleteButton.classList.add('clickable');
98  deleteButton.classList.add('host-list-edit');
99  deleteButtonImg.classList.add('host-list-remove-icon');
100  deleteButton.appendChild(deleteButtonImg);
101  tableRow.appendChild(deleteButton);
102
103  this.init(tableRow, warningOverlay, hostNameCell, editButton, deleteButton);
104};
105
106/**
107 * Associate the table row with the specified elements and callbacks, and set
108 * up event handlers.
109 *
110 * @param {HTMLElement} tableRow The top-level <div> for the table entry.
111 * @param {HTMLElement} warningOverlay The <span> element to render a warning
112 *     icon on top of the host icon.
113 * @param {HTMLElement} hostNameCell The element containing the host name.
114 * @param {HTMLElement} editButton The <img> containing the pencil icon for
115 *     editing the host name.
116 * @param {HTMLElement=} opt_deleteButton The <img> containing the cross icon
117 *     for deleting the host, if present.
118 * @return {void} Nothing.
119 */
120remoting.HostTableEntry.prototype.init = function(
121    tableRow, warningOverlay, hostNameCell, editButton, opt_deleteButton) {
122  this.tableRow = tableRow;
123  this.warningOverlay_ = warningOverlay;
124  this.hostNameCell_ = hostNameCell;
125  this.setHostName_();
126
127  /** @type {remoting.HostTableEntry} */
128  var that = this;
129
130  /** @param {Event} event The click event. */
131  var beginRename = function(event) {
132    that.beginRename_();
133    event.stopPropagation();
134  };
135  /** @param {Event} event The keyup event. */
136  var beginRenameKeyboard = function(event) {
137    if (event.which == 13 || event.which == 32) {
138      that.beginRename_();
139      event.stopPropagation();
140    }
141  };
142  editButton.addEventListener('click', beginRename, true);
143  editButton.addEventListener('keyup', beginRenameKeyboard, true);
144  this.registerFocusHandlers_(editButton);
145
146  if (opt_deleteButton) {
147    /** @param {Event} event The click event. */
148    var confirmDelete = function(event) {
149      that.showDeleteConfirmation_();
150      event.stopPropagation();
151    };
152    /** @param {Event} event The keyup event. */
153    var confirmDeleteKeyboard = function(event) {
154      if (event.which == 13 || event.which == 32) {
155        that.showDeleteConfirmation_();
156      }
157    };
158    opt_deleteButton.addEventListener('click', confirmDelete, false);
159    opt_deleteButton.addEventListener('keyup', confirmDeleteKeyboard, false);
160    this.registerFocusHandlers_(opt_deleteButton);
161  }
162  this.updateStatus();
163};
164
165/**
166 * Update the row to reflect the current status of the host (online/offline and
167 * clickable/unclickable).
168 *
169 * @param {boolean=} opt_forEdit True if the status is being updated in order
170 *     to allow the host name to be edited.
171 * @return {void} Nothing.
172 */
173remoting.HostTableEntry.prototype.updateStatus = function(opt_forEdit) {
174  var clickToConnect = this.host.status == 'ONLINE' && !opt_forEdit;
175  if (clickToConnect) {
176    if (!this.onConnectReference_) {
177      /** @type {string} */
178      var encodedHostId = encodeURIComponent(this.host.hostId);
179      this.onConnectReference_ = function() {
180        remoting.connectMe2Me(encodedHostId);
181      };
182      this.tableRow.addEventListener('click', this.onConnectReference_, false);
183    }
184    this.tableRow.classList.add('clickable');
185    this.tableRow.title = chrome.i18n.getMessage(
186        /*i18n-content*/'TOOLTIP_CONNECT', this.host.hostName);
187  } else {
188    if (this.onConnectReference_) {
189      this.tableRow.removeEventListener('click', this.onConnectReference_,
190                                        false);
191      this.onConnectReference_ = null;
192    }
193    this.tableRow.classList.remove('clickable');
194    this.tableRow.title = '';
195  }
196  var showOffline = this.host.status != 'ONLINE';
197  if (showOffline) {
198    this.tableRow.classList.remove('host-online');
199    this.tableRow.classList.add('host-offline');
200  } else {
201    this.tableRow.classList.add('host-online');
202    this.tableRow.classList.remove('host-offline');
203  }
204  this.warningOverlay_.hidden = !remoting.Host.needsUpdate(
205      this.host, this.webappMajorVersion_);
206};
207
208/**
209 * Prepare the host for renaming by replacing its name with an edit box.
210 * @return {void} Nothing.
211 * @private
212 */
213remoting.HostTableEntry.prototype.beginRename_ = function() {
214  var editBox = /** @type {HTMLInputElement} */ document.createElement('input');
215  editBox.type = 'text';
216  editBox.value = this.host.hostName;
217  this.hostNameCell_.innerText = '';
218  this.hostNameCell_.appendChild(editBox);
219  editBox.select();
220
221  this.onBlurReference_ = this.commitRename_.bind(this);
222  editBox.addEventListener('blur', this.onBlurReference_, false);
223
224  editBox.addEventListener('keydown', this.onKeydown_.bind(this), false);
225  this.updateStatus(true);
226};
227
228/**
229 * Accept the hostname entered by the user.
230 * @return {void} Nothing.
231 * @private
232 */
233remoting.HostTableEntry.prototype.commitRename_ = function() {
234  var editBox = this.hostNameCell_.querySelector('input');
235  if (editBox) {
236    if (this.host.hostName != editBox.value) {
237      this.host.hostName = editBox.value;
238      this.onRename_(this);
239    }
240    this.removeEditBox_();
241  }
242};
243
244/**
245 * Prompt the user to confirm or cancel deletion of a host.
246 * @return {void} Nothing.
247 * @private
248 */
249remoting.HostTableEntry.prototype.showDeleteConfirmation_ = function() {
250  var message = document.getElementById('confirm-host-delete-message');
251  l10n.localizeElement(message, this.host.hostName);
252  var confirm = document.getElementById('confirm-host-delete');
253  var cancel = document.getElementById('cancel-host-delete');
254  this.onConfirmDeleteReference_ = this.confirmDelete_.bind(this);
255  this.onCancelDeleteReference_ = this.cancelDelete_.bind(this);
256  confirm.addEventListener('click', this.onConfirmDeleteReference_, false);
257  cancel.addEventListener('click', this.onCancelDeleteReference_, false);
258  remoting.setMode(remoting.AppMode.CONFIRM_HOST_DELETE);
259};
260
261/**
262 * Confirm deletion of a host.
263 * @return {void} Nothing.
264 * @private
265 */
266remoting.HostTableEntry.prototype.confirmDelete_ = function() {
267  this.onDelete_(this);
268  this.cleanUpConfirmationEventListeners_();
269  remoting.setMode(remoting.AppMode.HOME);
270};
271
272/**
273 * Cancel deletion of a host.
274 * @return {void} Nothing.
275 * @private
276 */
277remoting.HostTableEntry.prototype.cancelDelete_ = function() {
278  this.cleanUpConfirmationEventListeners_();
279  remoting.setMode(remoting.AppMode.HOME);
280};
281
282/**
283 * Remove the confirm and cancel event handlers, which refer to this object.
284 * @return {void} Nothing.
285 * @private
286 */
287remoting.HostTableEntry.prototype.cleanUpConfirmationEventListeners_ =
288    function() {
289  var confirm = document.getElementById('confirm-host-delete');
290  var cancel = document.getElementById('cancel-host-delete');
291  confirm.removeEventListener('click', this.onConfirmDeleteReference_, false);
292  cancel.removeEventListener('click', this.onCancelDeleteReference_, false);
293  this.onCancelDeleteReference_ = function() {};
294  this.onConfirmDeleteReference_ = function() {};
295};
296
297/**
298 * Remove the edit box corresponding to the specified host, and reset its name.
299 * @return {void} Nothing.
300 * @private
301 */
302remoting.HostTableEntry.prototype.removeEditBox_ = function() {
303  var editBox = this.hostNameCell_.querySelector('input');
304  if (editBox) {
305    // onblur will fire when the edit box is removed, so remove the hook.
306    editBox.removeEventListener('blur', this.onBlurReference_, false);
307  }
308  // Update the tool-top and event handler.
309  this.updateStatus();
310  this.setHostName_();
311};
312
313/**
314 * Create the DOM nodes and event handlers for the hostname cell.
315 * @return {void} Nothing.
316 * @private
317 */
318remoting.HostTableEntry.prototype.setHostName_ = function() {
319  var hostNameNode = /** @type {HTMLElement} */ document.createElement('a');
320  if (this.host.status == 'ONLINE') {
321    if (remoting.Host.needsUpdate(this.host, this.webappMajorVersion_)) {
322      hostNameNode.innerText = chrome.i18n.getMessage(
323          /*i18n-content*/'UPDATE_REQUIRED', this.host.hostName);
324    } else {
325      hostNameNode.innerText = this.host.hostName;
326    }
327    hostNameNode.href = '#';
328    this.registerFocusHandlers_(hostNameNode);
329    /** @type {remoting.HostTableEntry} */
330    var that = this;
331    /** @param {Event} event */
332    var onKeyDown = function(event) {
333      if (that.onConnectReference_ &&
334          (event.which == 13 || event.which == 32)) {
335        that.onConnectReference_();
336      }
337    };
338    hostNameNode.addEventListener('keydown', onKeyDown, false);
339  } else {
340    if (this.host.updatedTime) {
341      var lastOnline = new Date(this.host.updatedTime);
342      var now = new Date();
343      var displayString = '';
344      if (now.getFullYear() == lastOnline.getFullYear() &&
345          now.getMonth() == lastOnline.getMonth() &&
346          now.getDate() == lastOnline.getDate()) {
347        displayString = lastOnline.toLocaleTimeString();
348      } else {
349        displayString = lastOnline.toLocaleDateString();
350      }
351      hostNameNode.innerText = chrome.i18n.getMessage(
352          /*i18n-content*/'LAST_ONLINE', [this.host.hostName, displayString]);
353    } else {
354      hostNameNode.innerText = chrome.i18n.getMessage(
355          /*i18n-content*/'OFFLINE', this.host.hostName);
356    }
357  }
358  hostNameNode.classList.add('host-list-label');
359  this.hostNameCell_.innerText = '';  // Remove previous contents (if any).
360  this.hostNameCell_.appendChild(hostNameNode);
361};
362
363/**
364 * Handle a key event while the user is typing a host name
365 * @param {Event} event The keyboard event.
366 * @return {void} Nothing.
367 * @private
368 */
369remoting.HostTableEntry.prototype.onKeydown_ = function(event) {
370  if (event.which == 27) {  // Escape
371    this.removeEditBox_();
372  } else if (event.which == 13) {  // Enter
373    this.commitRename_();
374  }
375};
376
377/**
378 * Register focus and blur handlers to cause the parent node to be highlighted
379 * whenever a child link has keyboard focus. Note that this is only necessary
380 * because Chrome does not yet support the draft CSS Selectors 4 specification
381 * (http://www.w3.org/TR/selectors4/#subject), which provides a more elegant
382 * solution to this problem.
383 *
384 * @param {HTMLElement} e The element on which to register the event handlers.
385 * @return {void} Nothing.
386 * @private
387 */
388remoting.HostTableEntry.prototype.registerFocusHandlers_ = function(e) {
389  e.addEventListener('focus', this.onFocusChange_.bind(this), false);
390  e.addEventListener('blur', this.onFocusChange_.bind(this), false);
391};
392
393/**
394 * Handle a focus change event within this table row.
395 * @return {void} Nothing.
396 * @private
397 */
398remoting.HostTableEntry.prototype.onFocusChange_ = function() {
399  var element = document.activeElement;
400  while (element) {
401    if (element == this.tableRow) {
402      this.tableRow.classList.add('child-focused');
403      return;
404    }
405    element = element.parentNode;
406  }
407  this.tableRow.classList.remove('child-focused');
408};
409