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