text: // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS-IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileOverview Definition of the goog.ui.tree.BaseNode class.
   *
   *
   * This is a based on the webfx tree control. It since been updated to add
   * typeahead support, as well as accessibility support using ARIA framework.
   * See file comment in treecontrol.js.
   */
  
  goog.provide('goog.ui.tree.BaseNode');
  goog.provide('goog.ui.tree.BaseNode.EventType');
  
  goog.forwardDeclare('goog.ui.tree.TreeControl');
  goog.require('goog.Timer');
  goog.require('goog.a11y.aria');
  goog.require('goog.a11y.aria.State');
  goog.require('goog.asserts');
  goog.require('goog.dom.safe');
  goog.require('goog.events.Event');
  goog.require('goog.events.KeyCodes');
  goog.require('goog.html.SafeHtml');
  goog.require('goog.html.SafeStyle');
  goog.require('goog.string');
  goog.require('goog.string.StringBuffer');
  goog.require('goog.style');
  goog.require('goog.ui.Component');  // circular
  
  
  
  /**
   * An abstract base class for a node in the tree.
   *
   * @param {string|!goog.html.SafeHtml} content The content of the node label.
   *     Strings are treated as plain-text and will be HTML escaped.
   * @param {Object=} opt_config The configuration for the tree. See
   *    {@link goog.ui.tree.BaseNode.defaultConfig}. If not specified the
   *    default config will be used.
   * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
   * @constructor
   * @extends {goog.ui.Component}
   */
  goog.ui.tree.BaseNode = function(content, opt_config, opt_domHelper) {
    goog.ui.Component.call(this, opt_domHelper);
  
    /**
     * The configuration for the tree.
     * @type {Object}
     * @private
     */
    this.config_ = opt_config || goog.ui.tree.BaseNode.defaultConfig;
  
    /**
     * HTML content of the node label.
     * @type {!goog.html.SafeHtml}
     * @private
     */
    this.html_ = goog.html.SafeHtml.htmlEscapePreservingNewlines(content);
  
    /** @private {string} */
    this.iconClass_;
  
    /** @private {string} */
    this.expandedIconClass_;
  
    /** @protected {goog.ui.tree.TreeControl} */
    this.tree;
  
    /** @private {goog.ui.tree.BaseNode} */
    this.previousSibling_;
  
    /** @private {goog.ui.tree.BaseNode} */
    this.nextSibling_;
  
    /** @private {goog.ui.tree.BaseNode} */
    this.firstChild_;
  
    /** @private {goog.ui.tree.BaseNode} */
    this.lastChild_;
  
    /**
     * Whether the tree item is selected.
     * @private {boolean}
     */
    this.selected_ = false;
  
    /**
     * Whether the tree node is expanded.
     * @private {boolean}
     */
    this.expanded_ = false;
  
    /**
     * Tooltip for the tree item
     * @private {?string}
     */
    this.toolTip_ = null;
  
    /**
     * HTML that can appear after the label (so not inside the anchor).
     * @private {!goog.html.SafeHtml}
     */
    this.afterLabelHtml_ = goog.html.SafeHtml.EMPTY;
  
    /**
     * Whether to allow user to collapse this node.
     * @private {boolean}
     */
    this.isUserCollapsible_ = true;
  
    /**
     * Nesting depth of this node; cached result of computeDepth_.
     * -1 if value has not been cached.
     * @private {number}
     */
    this.depth_ = -1;
  };
  goog.inherits(goog.ui.tree.BaseNode, goog.ui.Component);
  
  
  /**
   * The event types dispatched by this class.
   * @enum {string}
   */
  goog.ui.tree.BaseNode.EventType = {
    BEFORE_EXPAND: 'beforeexpand',
    EXPAND: 'expand',
    BEFORE_COLLAPSE: 'beforecollapse',
    COLLAPSE: 'collapse'
  };
  
  
  /**
   * Map of nodes in existence. Needed to route events to the appropriate nodes.
   * Nodes are added to the map at {@link #enterDocument} time and removed at
   * {@link #exitDocument} time.
   * @type {Object}
   * @protected
   */
  goog.ui.tree.BaseNode.allNodes = {};
  
  
  /** @override */
  goog.ui.tree.BaseNode.prototype.disposeInternal = function() {
    goog.ui.tree.BaseNode.superClass_.disposeInternal.call(this);
    if (this.tree) {
      this.tree.removeNode(this);
      this.tree = null;
    }
    this.setElementInternal(null);
  };
  
  
  /**
   * Adds roles and states.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.initAccessibility = function() {
    var el = this.getElement();
    if (el) {
      // Set an id for the label
      var label = this.getLabelElement();
      if (label && !label.id) {
        label.id = this.getId() + '.label';
      }
  
      goog.a11y.aria.setRole(el, 'treeitem');
      goog.a11y.aria.setState(el, 'selected', false);
      goog.a11y.aria.setState(el, 'level', this.getDepth());
      if (label) {
        goog.a11y.aria.setState(el, 'labelledby', label.id);
      }
  
      var img = this.getIconElement();
      if (img) {
        goog.a11y.aria.setRole(img, 'presentation');
      }
      var ei = this.getExpandIconElement();
      if (ei) {
        goog.a11y.aria.setRole(ei, 'presentation');
      }
  
      var ce = this.getChildrenElement();
      if (ce) {
        goog.a11y.aria.setRole(ce, 'group');
  
        // In case the children will be created lazily.
        if (ce.hasChildNodes()) {
          // Only set aria-expanded if the node has children (can be expanded).
          goog.a11y.aria.setState(el, goog.a11y.aria.State.EXPANDED, false);
  
          // do setsize for each child
          var count = this.getChildCount();
          for (var i = 1; i <= count; i++) {
            var child = this.getChildAt(i - 1).getElement();
            goog.asserts.assert(child, 'The child element cannot be null');
            goog.a11y.aria.setState(child, 'setsize', count);
            goog.a11y.aria.setState(child, 'posinset', i);
          }
        }
      }
    }
  };
  
  
  /** @override */
  goog.ui.tree.BaseNode.prototype.createDom = function() {
    var element = this.getDomHelper().safeHtmlToNode(this.toSafeHtml());
    this.setElementInternal(/** @type {!Element} */ (element));
  };
  
  
  /** @override */
  goog.ui.tree.BaseNode.prototype.enterDocument = function() {
    goog.ui.tree.BaseNode.superClass_.enterDocument.call(this);
    goog.ui.tree.BaseNode.allNodes[this.getId()] = this;
    this.initAccessibility();
  };
  
  
  /** @override */
  goog.ui.tree.BaseNode.prototype.exitDocument = function() {
    goog.ui.tree.BaseNode.superClass_.exitDocument.call(this);
    delete goog.ui.tree.BaseNode.allNodes[this.getId()];
  };
  
  
  /**
   * The method assumes that the child doesn't have parent node yet.
   * The `opt_render` argument is not used. If the parent node is expanded,
   * the child node's state will be the same as the parent's. Otherwise the
   * child's DOM tree won't be created.
   * @override
   */
  goog.ui.tree.BaseNode.prototype.addChildAt = function(
      child, index, opt_render) {
    goog.asserts.assert(!child.getParent());
    goog.asserts.assertInstanceof(child, goog.ui.tree.BaseNode);
    var prevNode = this.getChildAt(index - 1);
    var nextNode = this.getChildAt(index);
  
    goog.ui.tree.BaseNode.superClass_.addChildAt.call(this, child, index);
  
    child.previousSibling_ = prevNode;
    child.nextSibling_ = nextNode;
  
    if (prevNode) {
      prevNode.nextSibling_ = child;
    } else {
      this.firstChild_ = child;
    }
    if (nextNode) {
      nextNode.previousSibling_ = child;
    } else {
      this.lastChild_ = child;
    }
  
    var tree = this.getTree();
    if (tree) {
      child.setTreeInternal(tree);
    }
  
    child.setDepth_(this.getDepth() + 1);
  
    var el = this.getElement();
    if (el) {
      this.updateExpandIcon();
      goog.a11y.aria.setState(
          el, goog.a11y.aria.State.EXPANDED, this.getExpanded());
      if (this.getExpanded()) {
        var childrenEl = this.getChildrenElement();
        if (!child.getElement()) {
          child.createDom();
        }
        var childElement = child.getElement();
        var nextElement = nextNode && nextNode.getElement();
        childrenEl.insertBefore(/** @type {!Node} */ (childElement), nextElement);
  
        if (this.isInDocument()) {
          child.enterDocument();
        }
  
        if (!nextNode) {
          if (prevNode) {
            prevNode.updateExpandIcon();
          } else {
            goog.style.setElementShown(childrenEl, true);
            this.setExpanded(this.getExpanded());
          }
        }
      }
    }
  };
  
  
  /**
   * Adds a node as a child to the current node.
   * @param {goog.ui.tree.BaseNode} child The child to add.
   * @param {goog.ui.tree.BaseNode=} opt_before If specified, the new child is
   *    added as a child before this one. If not specified, it's appended to the
   *    end.
   * @return {!goog.ui.tree.BaseNode} The added child.
   */
  goog.ui.tree.BaseNode.prototype.add = function(child, opt_before) {
    goog.asserts.assert(
        !opt_before || opt_before.getParent() == this,
        'Can only add nodes before siblings');
    if (child.getParent()) {
      child.getParent().removeChild(child);
    }
    this.addChildAt(
        child, opt_before ? this.indexOfChild(opt_before) : this.getChildCount());
    return child;
  };
  
  
  /**
   * Removes a child. The caller is responsible for disposing the node.
   * @param {goog.ui.Component|string} childNode The child to remove. Must be a
   *     {@link goog.ui.tree.BaseNode}.
   * @param {boolean=} opt_unrender Unused. The child will always be unrendered.
   * @return {!goog.ui.tree.BaseNode} The child that was removed.
   * @override
   */
  goog.ui.tree.BaseNode.prototype.removeChild = function(
      childNode, opt_unrender) {
    // In reality, this only accepts BaseNodes.
    var child = /** @type {goog.ui.tree.BaseNode} */ (childNode);
  
    // if we remove selected or tree with the selected we should select this
    var tree = this.getTree();
    var selectedNode = tree ? tree.getSelectedItem() : null;
    if (selectedNode == child || child.contains(selectedNode)) {
      if (tree.hasFocus()) {
        this.select();
        goog.Timer.callOnce(this.onTimeoutSelect_, 10, this);
      } else {
        this.select();
      }
    }
  
    goog.ui.tree.BaseNode.superClass_.removeChild.call(this, child);
  
    if (this.lastChild_ == child) {
      this.lastChild_ = child.previousSibling_;
    }
    if (this.firstChild_ == child) {
      this.firstChild_ = child.nextSibling_;
    }
    if (child.previousSibling_) {
      child.previousSibling_.nextSibling_ = child.nextSibling_;
    }
    if (child.nextSibling_) {
      child.nextSibling_.previousSibling_ = child.previousSibling_;
    }
  
    var wasLast = child.isLastSibling();
  
    child.tree = null;
    child.depth_ = -1;
  
    if (tree) {
      // Tell the tree control that the child node is now removed.
      tree.removeNode(child);
  
      if (this.isInDocument()) {
        var childrenEl = this.getChildrenElement();
  
        if (child.isInDocument()) {
          var childEl = child.getElement();
          childrenEl.removeChild(/** @type {!Node} */ (childEl));
  
          child.exitDocument();
        }
  
        if (wasLast) {
          var newLast = this.getLastChild();
          if (newLast) {
            newLast.updateExpandIcon();
          }
        }
        if (!this.hasChildren()) {
          childrenEl.style.display = 'none';
          this.updateExpandIcon();
          this.updateIcon_();
  
          var el = this.getElement();
          if (el) {
            goog.a11y.aria.removeState(el, goog.a11y.aria.State.EXPANDED);
          }
        }
      }
    }
  
    return child;
  };
  
  
  /**
   * @deprecated Use {@link #removeChild}.
   */
  goog.ui.tree.BaseNode.prototype.remove =
      goog.ui.tree.BaseNode.prototype.removeChild;
  
  
  /**
   * Handler for setting focus asynchronously.
   * @private
   */
  goog.ui.tree.BaseNode.prototype.onTimeoutSelect_ = function() {
    this.select();
  };
  
  
  /**
   * Returns the tree.
   * @return {?goog.ui.tree.TreeControl}
   */
  goog.ui.tree.BaseNode.prototype.getTree = goog.abstractMethod;
  
  
  /**
   * Returns the depth of the node in the tree. Should not be overridden.
   * @return {number} The non-negative depth of this node (the root is zero).
   */
  goog.ui.tree.BaseNode.prototype.getDepth = function() {
    var depth = this.depth_;
    if (depth < 0) {
      depth = this.computeDepth_();
      this.setDepth_(depth);
    }
    return depth;
  };
  
  
  /**
   * Computes the depth of the node in the tree.
   * Called only by getDepth, when the depth hasn't already been cached.
   * @return {number} The non-negative depth of this node (the root is zero).
   * @private
   */
  goog.ui.tree.BaseNode.prototype.computeDepth_ = function() {
    var parent = this.getParent();
    if (parent) {
      return parent.getDepth() + 1;
    } else {
      return 0;
    }
  };
  
  
  /**
   * Changes the depth of a node (and all its descendants).
   * @param {number} depth The new nesting depth; must be non-negative.
   * @private
   */
  goog.ui.tree.BaseNode.prototype.setDepth_ = function(depth) {
    if (depth != this.depth_) {
      this.depth_ = depth;
      var row = this.getRowElement();
      if (row) {
        var indent = this.getPixelIndent_() + 'px';
        if (this.isRightToLeft()) {
          row.style.paddingRight = indent;
        } else {
          row.style.paddingLeft = indent;
        }
      }
      this.forEachChild(function(child) { child.setDepth_(depth + 1); });
    }
  };
  
  
  /**
   * Returns true if the node is a descendant of this node
   * @param {goog.ui.tree.BaseNode} node The node to check.
   * @return {boolean} True if the node is a descendant of this node, false
   *    otherwise.
   */
  goog.ui.tree.BaseNode.prototype.contains = function(node) {
    var current = node;
    while (current) {
      if (current == this) {
        return true;
      }
      current = current.getParent();
    }
    return false;
  };
  
  
  /**
   * An array of empty children to return for nodes that have no children.
   * @type {!Array<!goog.ui.tree.BaseNode>}
   * @private
   */
  goog.ui.tree.BaseNode.EMPTY_CHILDREN_ = [];
  
  
  /**
   * @param {number} index 0-based index.
   * @return {goog.ui.tree.BaseNode} The child at the given index; null if none.
   */
  goog.ui.tree.BaseNode.prototype.getChildAt;
  
  
  /**
   * Returns the children of this node.
   * @return {!Array<!goog.ui.tree.BaseNode>} The children.
   */
  goog.ui.tree.BaseNode.prototype.getChildren = function() {
    var children = [];
    this.forEachChild(function(child) { children.push(child); });
    return children;
  };
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The first child of this node.
   */
  goog.ui.tree.BaseNode.prototype.getFirstChild = function() {
    return this.getChildAt(0);
  };
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The last child of this node.
   */
  goog.ui.tree.BaseNode.prototype.getLastChild = function() {
    return this.getChildAt(this.getChildCount() - 1);
  };
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The previous sibling of this node.
   */
  goog.ui.tree.BaseNode.prototype.getPreviousSibling = function() {
    return this.previousSibling_;
  };
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The next sibling of this node.
   */
  goog.ui.tree.BaseNode.prototype.getNextSibling = function() {
    return this.nextSibling_;
  };
  
  
  /**
   * @return {boolean} Whether the node is the last sibling.
   */
  goog.ui.tree.BaseNode.prototype.isLastSibling = function() {
    return !this.nextSibling_;
  };
  
  
  /**
   * @return {boolean} Whether the node is selected.
   */
  goog.ui.tree.BaseNode.prototype.isSelected = function() {
    return this.selected_;
  };
  
  
  /**
   * Selects the node.
   */
  goog.ui.tree.BaseNode.prototype.select = function() {
    var tree = this.getTree();
    if (tree) {
      tree.setSelectedItem(this);
    }
  };
  
  
  /**
   * Originally it was intended to deselect the node but never worked.
   * @deprecated Use `tree.setSelectedItem(null)`.
   */
  goog.ui.tree.BaseNode.prototype.deselect = goog.nullFunction;
  
  
  /**
   * Called from the tree to instruct the node change its selection state.
   * @param {boolean} selected The new selection state.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.setSelectedInternal = function(selected) {
    if (this.selected_ == selected) {
      return;
    }
    this.selected_ = selected;
  
    this.updateRow();
  
    var el = this.getElement();
    if (el) {
      goog.a11y.aria.setState(el, 'selected', selected);
      if (selected) {
        var treeElement = this.getTree().getElement();
        goog.asserts.assert(
            treeElement, 'The DOM element for the tree cannot be null');
        goog.a11y.aria.setState(treeElement, 'activedescendant', this.getId());
      }
    }
  };
  
  
  /**
   * @return {boolean} Whether the node is expanded.
   */
  goog.ui.tree.BaseNode.prototype.getExpanded = function() {
    return this.expanded_;
  };
  
  
  /**
   * Sets the node to be expanded internally, without state change events.
   * @param {boolean} expanded Whether to expand or close the node.
   */
  goog.ui.tree.BaseNode.prototype.setExpandedInternal = function(expanded) {
    this.expanded_ = expanded;
  };
  
  
  /**
   * Sets the node to be expanded.
   * @param {boolean} expanded Whether to expand or close the node.
   */
  goog.ui.tree.BaseNode.prototype.setExpanded = function(expanded) {
    var isStateChange = expanded != this.expanded_;
    if (isStateChange) {
      // Only fire events if the expanded state has actually changed.
      var prevented = !this.dispatchEvent(
          expanded ? goog.ui.tree.BaseNode.EventType.BEFORE_EXPAND :
                     goog.ui.tree.BaseNode.EventType.BEFORE_COLLAPSE);
      if (prevented) return;
    }
    var ce;
    this.expanded_ = expanded;
    var tree = this.getTree();
    var el = this.getElement();
  
    if (this.hasChildren()) {
      if (!expanded && tree && this.contains(tree.getSelectedItem())) {
        this.select();
      }
  
      if (el) {
        ce = this.getChildrenElement();
        if (ce) {
          goog.style.setElementShown(ce, expanded);
          goog.a11y.aria.setState(el, goog.a11y.aria.State.EXPANDED, expanded);
  
          // Make sure we have the HTML for the children here.
          if (expanded && this.isInDocument() && !ce.hasChildNodes()) {
            var children = [];
            this.forEachChild(function(child) {
              children.push(child.toSafeHtml());
            });
            goog.dom.safe.setInnerHtml(ce, goog.html.SafeHtml.concat(children));
            this.forEachChild(function(child) { child.enterDocument(); });
          }
        }
        this.updateExpandIcon();
      }
    } else {
      ce = this.getChildrenElement();
      if (ce) {
        goog.style.setElementShown(ce, false);
      }
    }
    if (el) {
      this.updateIcon_();
    }
  
    if (isStateChange) {
      this.dispatchEvent(
          expanded ? goog.ui.tree.BaseNode.EventType.EXPAND :
                     goog.ui.tree.BaseNode.EventType.COLLAPSE);
    }
  };
  
  
  /**
   * Toggles the expanded state of the node.
   */
  goog.ui.tree.BaseNode.prototype.toggle = function() {
    this.setExpanded(!this.getExpanded());
  };
  
  
  /**
   * Expands the node.
   */
  goog.ui.tree.BaseNode.prototype.expand = function() {
    this.setExpanded(true);
  };
  
  
  /**
   * Collapses the node.
   */
  goog.ui.tree.BaseNode.prototype.collapse = function() {
    this.setExpanded(false);
  };
  
  
  /**
   * Collapses the children of the node.
   */
  goog.ui.tree.BaseNode.prototype.collapseChildren = function() {
    this.forEachChild(function(child) { child.collapseAll(); });
  };
  
  
  /**
   * Collapses the children and the node.
   */
  goog.ui.tree.BaseNode.prototype.collapseAll = function() {
    this.collapseChildren();
    this.collapse();
  };
  
  
  /**
   * Expands the children of the node.
   */
  goog.ui.tree.BaseNode.prototype.expandChildren = function() {
    this.forEachChild(function(child) { child.expandAll(); });
  };
  
  
  /**
   * Expands the children and the node.
   */
  goog.ui.tree.BaseNode.prototype.expandAll = function() {
    this.expandChildren();
    this.expand();
  };
  
  
  /**
   * Expands the parent chain of this node so that it is visible.
   */
  goog.ui.tree.BaseNode.prototype.reveal = function() {
    var parent = this.getParent();
    if (parent) {
      parent.setExpanded(true);
      parent.reveal();
    }
  };
  
  
  /**
   * Sets whether the node will allow the user to collapse it.
   * @param {boolean} isCollapsible Whether to allow node collapse.
   */
  goog.ui.tree.BaseNode.prototype.setIsUserCollapsible = function(isCollapsible) {
    this.isUserCollapsible_ = isCollapsible;
    if (!this.isUserCollapsible_) {
      this.expand();
    }
    if (this.getElement()) {
      this.updateExpandIcon();
    }
  };
  
  
  /**
   * @return {boolean} Whether the node is collapsible by user actions.
   */
  goog.ui.tree.BaseNode.prototype.isUserCollapsible = function() {
    return this.isUserCollapsible_;
  };
  
  
  /**
   * Creates HTML for the node.
   * @return {!goog.html.SafeHtml}
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.toSafeHtml = function() {
    var tree = this.getTree();
    var hideLines = !tree.getShowLines() ||
        tree == this.getParent() && !tree.getShowRootLines();
  
    var childClass =
        hideLines ? this.config_.cssChildrenNoLines : this.config_.cssChildren;
  
    var nonEmptyAndExpanded = this.getExpanded() && this.hasChildren();
  
    var attributes = {'class': childClass, 'style': this.getLineStyle()};
  
    var content = [];
    if (nonEmptyAndExpanded) {
      // children
      this.forEachChild(function(child) { content.push(child.toSafeHtml()); });
    }
  
    var children = goog.html.SafeHtml.create('div', attributes, content);
  
    return goog.html.SafeHtml.create(
        'div', {'class': this.config_.cssItem, 'id': this.getId()},
        [this.getRowSafeHtml(), children]);
  };
  
  
  /**
   * @return {number} The pixel indent of the row.
   * @private
   */
  goog.ui.tree.BaseNode.prototype.getPixelIndent_ = function() {
    return Math.max(0, (this.getDepth() - 1) * this.config_.indentWidth);
  };
  
  
  /**
   * @return {!goog.html.SafeHtml} The html for the row.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getRowSafeHtml = function() {
    var style = {};
    style['padding-' + (this.isRightToLeft() ? 'right' : 'left')] =
        this.getPixelIndent_() + 'px';
    var attributes = {'class': this.getRowClassName(), 'style': style};
    var content = [
      this.getExpandIconSafeHtml(), this.getIconSafeHtml(),
      this.getLabelSafeHtml()
    ];
    return goog.html.SafeHtml.create('div', attributes, content);
  };
  
  
  /**
   * @return {string} The class name for the row.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getRowClassName = function() {
    var selectedClass;
    if (this.isSelected()) {
      selectedClass = ' ' + this.config_.cssSelectedRow;
    } else {
      selectedClass = '';
    }
    return this.config_.cssTreeRow + selectedClass;
  };
  
  
  /**
   * @return {!goog.html.SafeHtml} The html for the label.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getLabelSafeHtml = function() {
    var html = goog.html.SafeHtml.create(
        'span',
        {'class': this.config_.cssItemLabel, 'title': this.getToolTip() || null},
        this.getSafeHtml());
    return goog.html.SafeHtml.concat(
        html,
        goog.html.SafeHtml.create('span', {}, this.getAfterLabelSafeHtml()));
  };
  
  
  /**
   * Returns the html that appears after the label. This is useful if you want to
   * put extra UI on the row of the label but not inside the anchor tag.
   * @return {string} The html.
   * @final
   */
  goog.ui.tree.BaseNode.prototype.getAfterLabelHtml = function() {
    return goog.html.SafeHtml.unwrap(this.getAfterLabelSafeHtml());
  };
  
  
  /**
   * Returns the html that appears after the label. This is useful if you want to
   * put extra UI on the row of the label but not inside the anchor tag.
   * @return {!goog.html.SafeHtml} The html.
   */
  goog.ui.tree.BaseNode.prototype.getAfterLabelSafeHtml = function() {
    return this.afterLabelHtml_;
  };
  
  
  /**
   * Sets the html that appears after the label. This is useful if you want to
   * put extra UI on the row of the label but not inside the anchor tag.
   * @param {!goog.html.SafeHtml} html The html.
   */
  goog.ui.tree.BaseNode.prototype.setAfterLabelSafeHtml = function(html) {
    this.afterLabelHtml_ = html;
    var el = this.getAfterLabelElement();
    if (el) {
      goog.dom.safe.setInnerHtml(el, html);
    }
  };
  
  
  /**
   * @return {!goog.html.SafeHtml} The html for the icon.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getIconSafeHtml = function() {
    return goog.html.SafeHtml.create('span', {
      'style': {'display': 'inline-block'},
      'class': this.getCalculatedIconClass()
    });
  };
  
  
  /**
   * Gets the calculated icon class.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getCalculatedIconClass = goog.abstractMethod;
  
  
  /**
   * @return {!goog.html.SafeHtml} The source for the icon.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getExpandIconSafeHtml = function() {
    return goog.html.SafeHtml.create('span', {
      'type': 'expand',
      'style': {'display': 'inline-block'},
      'class': this.getExpandIconClass()
    });
  };
  
  
  /**
   * @return {string} The class names of the icon used for expanding the node.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getExpandIconClass = function() {
    var tree = this.getTree();
    var hideLines = !tree.getShowLines() ||
        tree == this.getParent() && !tree.getShowRootLines();
  
    var config = this.config_;
    var sb = new goog.string.StringBuffer();
    sb.append(config.cssTreeIcon, ' ', config.cssExpandTreeIcon, ' ');
  
    if (this.hasChildren()) {
      var bits = 0;
      /*
        Bitmap used to determine which icon to use
        1  Plus
        2  Minus
        4  T Line
        8  L Line
      */
  
      if (tree.getShowExpandIcons() && this.isUserCollapsible_) {
        if (this.getExpanded()) {
          bits = 2;
        } else {
          bits = 1;
        }
      }
  
      if (!hideLines) {
        if (this.isLastSibling()) {
          bits += 4;
        } else {
          bits += 8;
        }
      }
  
      switch (bits) {
        case 1:
          sb.append(config.cssExpandTreeIconPlus);
          break;
        case 2:
          sb.append(config.cssExpandTreeIconMinus);
          break;
        case 4:
          sb.append(config.cssExpandTreeIconL);
          break;
        case 5:
          sb.append(config.cssExpandTreeIconLPlus);
          break;
        case 6:
          sb.append(config.cssExpandTreeIconLMinus);
          break;
        case 8:
          sb.append(config.cssExpandTreeIconT);
          break;
        case 9:
          sb.append(config.cssExpandTreeIconTPlus);
          break;
        case 10:
          sb.append(config.cssExpandTreeIconTMinus);
          break;
        default:  // 0
          sb.append(config.cssExpandTreeIconBlank);
      }
    } else {
      if (hideLines) {
        sb.append(config.cssExpandTreeIconBlank);
      } else if (this.isLastSibling()) {
        sb.append(config.cssExpandTreeIconL);
      } else {
        sb.append(config.cssExpandTreeIconT);
      }
    }
    return sb.toString();
  };
  
  
  /**
   * @return {!goog.html.SafeStyle} The line style.
   */
  goog.ui.tree.BaseNode.prototype.getLineStyle = function() {
    var nonEmptyAndExpanded = this.getExpanded() && this.hasChildren();
    return goog.html.SafeStyle.create({
      'background-position': this.getBackgroundPosition(),
      'display': nonEmptyAndExpanded ? null : 'none'
    });
  };
  
  
  /**
   * @return {string} The background position style value.
   */
  goog.ui.tree.BaseNode.prototype.getBackgroundPosition = function() {
    return (this.isLastSibling() ? '-100' : (this.getDepth() - 1) *
                    this.config_.indentWidth) +
        'px 0';
  };
  
  
  /**
   * @return {Element} The element for the tree node.
   * @override
   */
  goog.ui.tree.BaseNode.prototype.getElement = function() {
    var el = goog.ui.tree.BaseNode.superClass_.getElement.call(this);
    if (!el) {
      el = this.getDomHelper().getElement(this.getId());
      this.setElementInternal(el);
    }
    return el;
  };
  
  
  /**
   * @return {Element} The row is the div that is used to draw the node without
   *     the children.
   */
  goog.ui.tree.BaseNode.prototype.getRowElement = function() {
    var el = this.getElement();
    return el ? /** @type {Element} */ (el.firstChild) : null;
  };
  
  
  /**
   * @return {Element} The expanded icon element.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getExpandIconElement = function() {
    var el = this.getRowElement();
    return el ? /** @type {Element} */ (el.firstChild) : null;
  };
  
  
  /**
   * @return {Element} The icon element.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getIconElement = function() {
    var el = this.getRowElement();
    return el ? /** @type {Element} */ (el.childNodes[1]) : null;
  };
  
  
  /**
   * @return {Element} The label element.
   */
  goog.ui.tree.BaseNode.prototype.getLabelElement = function() {
    var el = this.getRowElement();
    // TODO: find/fix race condition that requires us to add
    // the lastChild check
    return el && el.lastChild ?
        /** @type {Element} */ (el.lastChild.previousSibling) :
                               null;
  };
  
  
  /**
   * @return {Element} The element after the label.
   */
  goog.ui.tree.BaseNode.prototype.getAfterLabelElement = function() {
    var el = this.getRowElement();
    return el ? /** @type {Element} */ (el.lastChild) : null;
  };
  
  
  /**
   * @return {Element} The div containing the children.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.getChildrenElement = function() {
    var el = this.getElement();
    return el ? /** @type {Element} */ (el.lastChild) : null;
  };
  
  
  /**
   * Sets the icon class for the node.
   * @param {string} s The icon class.
   */
  goog.ui.tree.BaseNode.prototype.setIconClass = function(s) {
    this.iconClass_ = s;
    if (this.isInDocument()) {
      this.updateIcon_();
    }
  };
  
  
  /**
   * Gets the icon class for the node.
   * @return {string} s The icon source.
   */
  goog.ui.tree.BaseNode.prototype.getIconClass = function() {
    return this.iconClass_;
  };
  
  
  /**
   * Sets the icon class for when the node is expanded.
   * @param {string} s The expanded icon class.
   */
  goog.ui.tree.BaseNode.prototype.setExpandedIconClass = function(s) {
    this.expandedIconClass_ = s;
    if (this.isInDocument()) {
      this.updateIcon_();
    }
  };
  
  
  /**
   * Gets the icon class for when the node is expanded.
   * @return {string} The class.
   */
  goog.ui.tree.BaseNode.prototype.getExpandedIconClass = function() {
    return this.expandedIconClass_;
  };
  
  
  /**
   * Sets the text of the label.
   * @param {string} s The plain text of the label.
   */
  goog.ui.tree.BaseNode.prototype.setText = function(s) {
    this.setSafeHtml(goog.html.SafeHtml.htmlEscape(s));
  };
  
  
  /**
   * Returns the text of the label. If the text was originally set as HTML, the
   * return value is unspecified.
   * @return {string} The plain text of the label.
   */
  goog.ui.tree.BaseNode.prototype.getText = function() {
    return goog.string.unescapeEntities(goog.html.SafeHtml.unwrap(this.html_));
  };
  
  
  /**
   * Sets the HTML of the label.
   * @param {!goog.html.SafeHtml} html The HTML object for the label.
   */
  goog.ui.tree.BaseNode.prototype.setSafeHtml = function(html) {
    this.html_ = html;
    var el = this.getLabelElement();
    if (el) {
      goog.dom.safe.setInnerHtml(el, html);
    }
    var tree = this.getTree();
    if (tree) {
      // Tell the tree control about the updated label text.
      tree.setNode(this);
    }
  };
  
  
  /**
   * Returns the html of the label.
   * @return {string} The html string of the label.
   * @final
   */
  goog.ui.tree.BaseNode.prototype.getHtml = function() {
    return goog.html.SafeHtml.unwrap(this.getSafeHtml());
  };
  
  
  /**
   * Returns the html of the label.
   * @return {!goog.html.SafeHtml} The html string of the label.
   */
  goog.ui.tree.BaseNode.prototype.getSafeHtml = function() {
    return this.html_;
  };
  
  
  /**
   * Sets the text of the tooltip.
   * @param {string} s The tooltip text to set.
   */
  goog.ui.tree.BaseNode.prototype.setToolTip = function(s) {
    this.toolTip_ = s;
    var el = this.getLabelElement();
    if (el) {
      el.title = s;
    }
  };
  
  
  /**
   * Returns the text of the tooltip.
   * @return {?string} The tooltip text.
   */
  goog.ui.tree.BaseNode.prototype.getToolTip = function() {
    return this.toolTip_;
  };
  
  
  /**
   * Updates the row styles.
   */
  goog.ui.tree.BaseNode.prototype.updateRow = function() {
    var rowEl = this.getRowElement();
    if (rowEl) {
      rowEl.className = this.getRowClassName();
    }
  };
  
  
  /**
   * Updates the expand icon of the node.
   */
  goog.ui.tree.BaseNode.prototype.updateExpandIcon = function() {
    var img = this.getExpandIconElement();
    if (img) {
      img.className = this.getExpandIconClass();
    }
    var cel = this.getChildrenElement();
    if (cel) {
      cel.style.backgroundPosition = this.getBackgroundPosition();
    }
  };
  
  
  /**
   * Updates the icon of the node. Assumes that this.getElement() is created.
   * @private
   */
  goog.ui.tree.BaseNode.prototype.updateIcon_ = function() {
    this.getIconElement().className = this.getCalculatedIconClass();
  };
  
  
  /**
   * Handles mouse down event.
   * @param {!goog.events.BrowserEvent} e The browser event.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.onMouseDown = function(e) {
    var el = e.target;
    // expand icon
    var type = el.getAttribute('type');
    if (type == 'expand' && this.hasChildren()) {
      if (this.isUserCollapsible_) {
        this.toggle();
      }
      return;
    }
  
    this.select();
    this.updateRow();
  };
  
  
  /**
   * Handles a click event.
   * @param {!goog.events.BrowserEvent} e The browser event.
   * @protected
   * @suppress {underscore|visibility}
   */
  goog.ui.tree.BaseNode.prototype.onClick_ = goog.events.Event.preventDefault;
  
  
  /**
   * Handles a double click event.
   * @param {!goog.events.BrowserEvent} e The browser event.
   * @protected
   * @suppress {underscore|visibility}
   */
  goog.ui.tree.BaseNode.prototype.onDoubleClick_ = function(e) {
    var el = e.target;
    // expand icon
    var type = el.getAttribute('type');
    if (type == 'expand' && this.hasChildren()) {
      return;
    }
  
    if (this.isUserCollapsible_) {
      this.toggle();
    }
  };
  
  
  /**
   * Handles a key down event.
   * @param {!goog.events.BrowserEvent} e The browser event.
   * @return {boolean} The handled value.
   * @protected
   */
  goog.ui.tree.BaseNode.prototype.onKeyDown = function(e) {
    var handled = true;
    switch (e.keyCode) {
      case goog.events.KeyCodes.RIGHT:
        if (e.altKey) {
          break;
        }
        if (this.hasChildren()) {
          if (!this.getExpanded()) {
            this.setExpanded(true);
          } else {
            this.getFirstChild().select();
          }
        }
        break;
  
      case goog.events.KeyCodes.LEFT:
        if (e.altKey) {
          break;
        }
        if (this.hasChildren() && this.getExpanded() && this.isUserCollapsible_) {
          this.setExpanded(false);
        } else {
          var parent = this.getParent();
          var tree = this.getTree();
          // don't go to root if hidden
          if (parent && (tree.getShowRootNode() || parent != tree)) {
            parent.select();
          }
        }
        break;
  
      case goog.events.KeyCodes.DOWN:
        var nextNode = this.getNextShownNode();
        if (nextNode) {
          nextNode.select();
        }
        break;
  
      case goog.events.KeyCodes.UP:
        var previousNode = this.getPreviousShownNode();
        if (previousNode) {
          previousNode.select();
        }
        break;
  
      default:
        handled = false;
    }
  
    if (handled) {
      e.preventDefault();
      var tree = this.getTree();
      if (tree) {
        // clear type ahead buffer as user navigates with arrow keys
        tree.clearTypeAhead();
      }
    }
  
    return handled;
  };
  
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The last shown descendant.
   */
  goog.ui.tree.BaseNode.prototype.getLastShownDescendant = function() {
    if (!this.getExpanded() || !this.hasChildren()) {
      return this;
    }
    // we know there is at least 1 child
    return this.getLastChild().getLastShownDescendant();
  };
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The next node to show or null if there isn't
   *     a next node to show.
   */
  goog.ui.tree.BaseNode.prototype.getNextShownNode = function() {
    if (this.hasChildren() && this.getExpanded()) {
      return this.getFirstChild();
    } else {
      var parent = this;
      var next;
      while (parent != this.getTree()) {
        next = parent.getNextSibling();
        if (next != null) {
          return next;
        }
        parent = parent.getParent();
      }
      return null;
    }
  };
  
  
  /**
   * @return {goog.ui.tree.BaseNode} The previous node to show.
   */
  goog.ui.tree.BaseNode.prototype.getPreviousShownNode = function() {
    var ps = this.getPreviousSibling();
    if (ps != null) {
      return ps.getLastShownDescendant();
    }
    var parent = this.getParent();
    var tree = this.getTree();
    if (!tree.getShowRootNode() && parent == tree) {
      return null;
    }
    // The root is the first node.
    if (this == tree) {
      return null;
    }
    return /** @type {goog.ui.tree.BaseNode} */ (parent);
  };
  
  
  /**
   * @return {*} Data set by the client.
   * @deprecated Use {@link #getModel} instead.
   */
  goog.ui.tree.BaseNode.prototype.getClientData =
      goog.ui.tree.BaseNode.prototype.getModel;
  
  
  /**
   * Sets client data to associate with the node.
   * @param {*} data The client data to associate with the node.
   * @deprecated Use {@link #setModel} instead.
   */
  goog.ui.tree.BaseNode.prototype.setClientData =
      goog.ui.tree.BaseNode.prototype.setModel;
  
  
  /**
   * @return {Object} The configuration for the tree.
   */
  goog.ui.tree.BaseNode.prototype.getConfig = function() {
    return this.config_;
  };
  
  
  /**
   * Internal method that is used to set the tree control on the node.
   * @param {goog.ui.tree.TreeControl} tree The tree control.
   */
  goog.ui.tree.BaseNode.prototype.setTreeInternal = function(tree) {
    if (this.tree != tree) {
      this.tree = tree;
      // Add new node to the type ahead node map.
      tree.setNode(this);
      this.forEachChild(function(child) { child.setTreeInternal(tree); });
    }
  };
  
  
  /**
   * A default configuration for the tree.
   */
  goog.ui.tree.BaseNode.defaultConfig = {
    indentWidth: 19,
    cssRoot: goog.getCssName('goog-tree-root') + ' ' +
        goog.getCssName('goog-tree-item'),
    cssHideRoot: goog.getCssName('goog-tree-hide-root'),
    cssItem: goog.getCssName('goog-tree-item'),
    cssChildren: goog.getCssName('goog-tree-children'),
    cssChildrenNoLines: goog.getCssName('goog-tree-children-nolines'),
    cssTreeRow: goog.getCssName('goog-tree-row'),
    cssItemLabel: goog.getCssName('goog-tree-item-label'),
    cssTreeIcon: goog.getCssName('goog-tree-icon'),
    cssExpandTreeIcon: goog.getCssName('goog-tree-expand-icon'),
    cssExpandTreeIconPlus: goog.getCssName('goog-tree-expand-icon-plus'),
    cssExpandTreeIconMinus: goog.getCssName('goog-tree-expand-icon-minus'),
    cssExpandTreeIconTPlus: goog.getCssName('goog-tree-expand-icon-tplus'),
    cssExpandTreeIconTMinus: goog.getCssName('goog-tree-expand-icon-tminus'),
    cssExpandTreeIconLPlus: goog.getCssName('goog-tree-expand-icon-lplus'),
    cssExpandTreeIconLMinus: goog.getCssName('goog-tree-expand-icon-lminus'),
    cssExpandTreeIconT: goog.getCssName('goog-tree-expand-icon-t'),
    cssExpandTreeIconL: goog.getCssName('goog-tree-expand-icon-l'),
    cssExpandTreeIconBlank: goog.getCssName('goog-tree-expand-icon-blank'),
    cssExpandedFolderIcon: goog.getCssName('goog-tree-expanded-folder-icon'),
    cssCollapsedFolderIcon: goog.getCssName('goog-tree-collapsed-folder-icon'),
    cssFileIcon: goog.getCssName('goog-tree-file-icon'),
    cssExpandedRootIcon: goog.getCssName('goog-tree-expanded-folder-icon'),
    cssCollapsedRootIcon: goog.getCssName('goog-tree-collapsed-folder-icon'),
    cssSelectedRow: goog.getCssName('selected')
  };
  