Javascript  |  409行  |  14.21 KB

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * Class representing an entry in the host-list portion of the home screen.
 */

'use strict';

/** @suppress {duplicate} */
var remoting = remoting || {};

/**
 * An entry in the host table.
 * @param {remoting.Host} host The host, as obtained from Apiary.
 * @param {number} webappMajorVersion The major version nmber of the web-app,
 *     used to identify out-of-date hosts.
 * @param {function(remoting.HostTableEntry):void} onRename Callback for
 *     rename operations.
 * @param {function(remoting.HostTableEntry):void=} opt_onDelete Callback for
 *     delete operations.
 * @constructor
 */
remoting.HostTableEntry = function(
    host, webappMajorVersion, onRename, opt_onDelete) {
  /** @type {remoting.Host} */
  this.host = host;
  /** @type {number} */
  this.webappMajorVersion_ = webappMajorVersion;
  /** @type {function(remoting.HostTableEntry):void} @private */
  this.onRename_ = onRename;
  /** @type {undefined|function(remoting.HostTableEntry):void} @private */
  this.onDelete_ = opt_onDelete;

  /** @type {HTMLElement} */
  this.tableRow = null;
  /** @type {HTMLElement} @private */
  this.hostNameCell_ = null;
  /** @type {HTMLElement} @private */
  this.warningOverlay_ = null;
  // References to event handlers so that they can be removed.
  /** @type {function():void} @private */
  this.onBlurReference_ = function() {};
  /** @type {function():void} @private */
  this.onConfirmDeleteReference_ = function() {};
  /** @type {function():void} @private */
  this.onCancelDeleteReference_ = function() {};
  /** @type {function():void?} @private */
  this.onConnectReference_ = null;
};

/**
 * Create the HTML elements for this entry and set up event handlers.
 * @return {void} Nothing.
 */
remoting.HostTableEntry.prototype.createDom = function() {
  // Create the top-level <div>
  var tableRow = /** @type {HTMLElement} */ document.createElement('div');
  tableRow.classList.add('section-row');
  // Create the host icon cell.
  var hostIconDiv = /** @type {HTMLElement} */ document.createElement('div');
  hostIconDiv.classList.add('host-list-main-icon');
  var warningOverlay =
      /** @type {HTMLElement} */ document.createElement('span');
  hostIconDiv.appendChild(warningOverlay);
  var hostIcon = /** @type {HTMLElement} */ document.createElement('img');
  hostIcon.src = 'icon_host.webp';
  hostIconDiv.appendChild(hostIcon);
  tableRow.appendChild(hostIconDiv);
  // Create the host name cell.
  var hostNameCell = /** @type {HTMLElement} */ document.createElement('div');
  hostNameCell.classList.add('box-spacer');
  hostNameCell.id = 'host_' + this.host.hostId;
  tableRow.appendChild(hostNameCell);
  // Create the host rename cell.
  var editButton = /** @type {HTMLElement} */ document.createElement('span');
  var editButtonImg = /** @type {HTMLElement} */ document.createElement('img');
  editButtonImg.title = chrome.i18n.getMessage(
      /*i18n-content*/'TOOLTIP_RENAME');
  editButtonImg.src = 'icon_pencil.webp';
  editButton.tabIndex = 0;
  editButton.classList.add('clickable');
  editButton.classList.add('host-list-edit');
  editButtonImg.classList.add('host-list-rename-icon');
  editButton.appendChild(editButtonImg);
  tableRow.appendChild(editButton);
  // Create the host delete cell.
  var deleteButton = /** @type {HTMLElement} */ document.createElement('span');
  var deleteButtonImg =
      /** @type {HTMLElement} */ document.createElement('img');
  deleteButtonImg.title =
      chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_DELETE');
  deleteButtonImg.src = 'icon_cross.webp';
  deleteButton.tabIndex = 0;
  deleteButton.classList.add('clickable');
  deleteButton.classList.add('host-list-edit');
  deleteButtonImg.classList.add('host-list-remove-icon');
  deleteButton.appendChild(deleteButtonImg);
  tableRow.appendChild(deleteButton);

  this.init(tableRow, warningOverlay, hostNameCell, editButton, deleteButton);
};

/**
 * Associate the table row with the specified elements and callbacks, and set
 * up event handlers.
 *
 * @param {HTMLElement} tableRow The top-level <div> for the table entry.
 * @param {HTMLElement} warningOverlay The <span> element to render a warning
 *     icon on top of the host icon.
 * @param {HTMLElement} hostNameCell The element containing the host name.
 * @param {HTMLElement} editButton The <img> containing the pencil icon for
 *     editing the host name.
 * @param {HTMLElement=} opt_deleteButton The <img> containing the cross icon
 *     for deleting the host, if present.
 * @return {void} Nothing.
 */
remoting.HostTableEntry.prototype.init = function(
    tableRow, warningOverlay, hostNameCell, editButton, opt_deleteButton) {
  this.tableRow = tableRow;
  this.warningOverlay_ = warningOverlay;
  this.hostNameCell_ = hostNameCell;
  this.setHostName_();

  /** @type {remoting.HostTableEntry} */
  var that = this;

  /** @param {Event} event The click event. */
  var beginRename = function(event) {
    that.beginRename_();
    event.stopPropagation();
  };
  /** @param {Event} event The keyup event. */
  var beginRenameKeyboard = function(event) {
    if (event.which == 13 || event.which == 32) {
      that.beginRename_();
      event.stopPropagation();
    }
  };
  editButton.addEventListener('click', beginRename, true);
  editButton.addEventListener('keyup', beginRenameKeyboard, true);
  this.registerFocusHandlers_(editButton);

  if (opt_deleteButton) {
    /** @param {Event} event The click event. */
    var confirmDelete = function(event) {
      that.showDeleteConfirmation_();
      event.stopPropagation();
    };
    /** @param {Event} event The keyup event. */
    var confirmDeleteKeyboard = function(event) {
      if (event.which == 13 || event.which == 32) {
        that.showDeleteConfirmation_();
      }
    };
    opt_deleteButton.addEventListener('click', confirmDelete, false);
    opt_deleteButton.addEventListener('keyup', confirmDeleteKeyboard, false);
    this.registerFocusHandlers_(opt_deleteButton);
  }
  this.updateStatus();
};

/**
 * Update the row to reflect the current status of the host (online/offline and
 * clickable/unclickable).
 *
 * @param {boolean=} opt_forEdit True if the status is being updated in order
 *     to allow the host name to be edited.
 * @return {void} Nothing.
 */
remoting.HostTableEntry.prototype.updateStatus = function(opt_forEdit) {
  var clickToConnect = this.host.status == 'ONLINE' && !opt_forEdit;
  if (clickToConnect) {
    if (!this.onConnectReference_) {
      /** @type {string} */
      var encodedHostId = encodeURIComponent(this.host.hostId)
      this.onConnectReference_ = function() {
        remoting.connectMe2Me(encodedHostId);
      };
      this.tableRow.addEventListener('click', this.onConnectReference_, false);
    }
    this.tableRow.classList.add('clickable');
    this.tableRow.title = chrome.i18n.getMessage(
        /*i18n-content*/'TOOLTIP_CONNECT', this.host.hostName);
  } else {
    if (this.onConnectReference_) {
      this.tableRow.removeEventListener('click', this.onConnectReference_,
                                        false);
      this.onConnectReference_ = null;
    }
    this.tableRow.classList.remove('clickable');
    this.tableRow.title = '';
  }
  var showOffline = this.host.status != 'ONLINE';
  if (showOffline) {
    this.tableRow.classList.remove('host-online');
    this.tableRow.classList.add('host-offline');
  } else {
    this.tableRow.classList.add('host-online');
    this.tableRow.classList.remove('host-offline');
  }
  this.warningOverlay_.hidden = !remoting.Host.needsUpdate(
      this.host, this.webappMajorVersion_);
};

/**
 * Prepare the host for renaming by replacing its name with an edit box.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.beginRename_ = function() {
  var editBox = /** @type {HTMLInputElement} */ document.createElement('input');
  editBox.type = 'text';
  editBox.value = this.host.hostName;
  this.hostNameCell_.innerText = '';
  this.hostNameCell_.appendChild(editBox);
  editBox.select();

  this.onBlurReference_ = this.commitRename_.bind(this);
  editBox.addEventListener('blur', this.onBlurReference_, false);

  editBox.addEventListener('keydown', this.onKeydown_.bind(this), false);
  this.updateStatus(true);
};

/**
 * Accept the hostname entered by the user.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.commitRename_ = function() {
  var editBox = this.hostNameCell_.querySelector('input');
  if (editBox) {
    if (this.host.hostName != editBox.value) {
      this.host.hostName = editBox.value;
      this.onRename_(this);
    }
    this.removeEditBox_();
  }
};

/**
 * Prompt the user to confirm or cancel deletion of a host.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.showDeleteConfirmation_ = function() {
  var message = document.getElementById('confirm-host-delete-message');
  l10n.localizeElement(message, this.host.hostName);
  var confirm = document.getElementById('confirm-host-delete');
  var cancel = document.getElementById('cancel-host-delete');
  this.onConfirmDeleteReference_ = this.confirmDelete_.bind(this);
  this.onCancelDeleteReference_ = this.cancelDelete_.bind(this);
  confirm.addEventListener('click', this.onConfirmDeleteReference_, false);
  cancel.addEventListener('click', this.onCancelDeleteReference_, false);
  remoting.setMode(remoting.AppMode.CONFIRM_HOST_DELETE);
};

/**
 * Confirm deletion of a host.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.confirmDelete_ = function() {
  this.onDelete_(this);
  this.cleanUpConfirmationEventListeners_();
  remoting.setMode(remoting.AppMode.HOME);
};

/**
 * Cancel deletion of a host.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.cancelDelete_ = function() {
  this.cleanUpConfirmationEventListeners_();
  remoting.setMode(remoting.AppMode.HOME);
};

/**
 * Remove the confirm and cancel event handlers, which refer to this object.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.cleanUpConfirmationEventListeners_ =
    function() {
  var confirm = document.getElementById('confirm-host-delete');
  var cancel = document.getElementById('cancel-host-delete');
  confirm.removeEventListener('click', this.onConfirmDeleteReference_, false);
  cancel.removeEventListener('click', this.onCancelDeleteReference_, false);
  this.onCancelDeleteReference_ = function() {};
  this.onConfirmDeleteReference_ = function() {};
};

/**
 * Remove the edit box corresponding to the specified host, and reset its name.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.removeEditBox_ = function() {
  var editBox = this.hostNameCell_.querySelector('input');
  if (editBox) {
    // onblur will fire when the edit box is removed, so remove the hook.
    editBox.removeEventListener('blur', this.onBlurReference_, false);
  }
  // Update the tool-top and event handler.
  this.updateStatus();
  this.setHostName_();
};

/**
 * Create the DOM nodes and event handlers for the hostname cell.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.setHostName_ = function() {
  var hostNameNode = /** @type {HTMLElement} */ document.createElement('a');
  if (this.host.status == 'ONLINE') {
    if (remoting.Host.needsUpdate(this.host, this.webappMajorVersion_)) {
      hostNameNode.innerText = chrome.i18n.getMessage(
          /*i18n-content*/'UPDATE_REQUIRED', this.host.hostName);
    } else {
      hostNameNode.innerText = this.host.hostName;
    }
    hostNameNode.href = '#';
    this.registerFocusHandlers_(hostNameNode);
    /** @type {remoting.HostTableEntry} */
    var that = this;
    /** @param {Event} event */
    var onKeyDown = function(event) {
      if (that.onConnectReference_ &&
          (event.which == 13 || event.which == 32)) {
        that.onConnectReference_();
      }
    };
    hostNameNode.addEventListener('keydown', onKeyDown, false);
  } else {
    if (this.host.updatedTime) {
      var lastOnline = new Date(this.host.updatedTime);
      var now = new Date();
      var displayString = '';
      if (now.getFullYear() == lastOnline.getFullYear() &&
          now.getMonth() == lastOnline.getMonth() &&
          now.getDate() == lastOnline.getDate()) {
        displayString = lastOnline.toLocaleTimeString();
      } else {
        displayString = lastOnline.toLocaleDateString();
      }
      hostNameNode.innerText = chrome.i18n.getMessage(
          /*i18n-content*/'LAST_ONLINE', [this.host.hostName, displayString]);
    } else {
      hostNameNode.innerText = chrome.i18n.getMessage(
          /*i18n-content*/'OFFLINE', this.host.hostName);
    }
  }
  hostNameNode.classList.add('host-list-label');
  this.hostNameCell_.innerText = '';  // Remove previous contents (if any).
  this.hostNameCell_.appendChild(hostNameNode);
};

/**
 * Handle a key event while the user is typing a host name
 * @param {Event} event The keyboard event.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.onKeydown_ = function(event) {
  if (event.which == 27) {  // Escape
    this.removeEditBox_();
  } else if (event.which == 13) {  // Enter
    this.commitRename_();
  }
};

/**
 * Register focus and blur handlers to cause the parent node to be highlighted
 * whenever a child link has keyboard focus. Note that this is only necessary
 * because Chrome does not yet support the draft CSS Selectors 4 specification
 * (http://www.w3.org/TR/selectors4/#subject), which provides a more elegant
 * solution to this problem.
 *
 * @param {HTMLElement} e The element on which to register the event handlers.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.registerFocusHandlers_ = function(e) {
  e.addEventListener('focus', this.onFocusChange_.bind(this), false);
  e.addEventListener('blur', this.onFocusChange_.bind(this), false);
};

/**
 * Handle a focus change event within this table row.
 * @return {void} Nothing.
 * @private
 */
remoting.HostTableEntry.prototype.onFocusChange_ = function() {
  var element = document.activeElement;
  while (element) {
    if (element == this.tableRow) {
      this.tableRow.classList.add('child-focused');
      return;
    }
    element = element.parentNode;
  }
  this.tableRow.classList.remove('child-focused');
};