chromium/third_party/google-closure-library/closure/goog/ui/drilldownrow.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Tree-like drilldown components for HTML tables.
 *
 * This component supports expanding and collapsing groups of rows in
 * HTML tables.  The behavior is like typical Tree widgets, but tables
 * need special support to enable the tree behaviors.
 *
 * Any row or rows in an HTML table can be DrilldownRows.  The root
 * DrilldownRow nodes are always visible in the table, but the rest show
 * or hide as input events expand and collapse their ancestors.
 *
 * Programming them:  Top-level DrilldownRows are made by decorating
 * a TR element.  Children are made with addChild or addChildAt, and
 * are entered into the document by the render() method.
 *
 * A DrilldownRow can have any number of children.  If it has no children
 * it can be loaded, not loaded, or with a load in progress.
 * Top-level DrilldownRows are always displayed (though setting
 * style.display on a containing DOM node could make one be not
 * visible to the user).  A DrilldownRow can be expanded, or not.  A
 * DrilldownRow displays if all of its ancestors are expanded.
 *
 * Set up event handlers and style each row for the application in an
 * enterDocument method.
 *
 * Children normally render into the document lazily, at the first
 * moment when all ancestors are expanded.
 *
 * @see ../demos/drilldownrow.html
 */

// TODO(user): Build support for dynamically loading DrilldownRows,
// probably using automplete as an example to follow.

// TODO(user): Make DrilldownRows accessible through the keyboard.

// The render method is redefined in this class because when addChildAt renders
// the new child it assumes that the child's DOM node will be a child
// of the parent component's DOM node, but all DOM nodes of DrilldownRows
// in the same tree of DrilldownRows are siblings to each other.
//
// Arguments (or lack of arguments) to the render methods in Component
// all determine the place of the new DOM node in the DOM tree, but
// the place of a new DrilldownRow in the DOM needs to be determined by
// its position in the tree of DrilldownRows.

goog.provide('goog.ui.DrilldownRow');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');
goog.require('goog.string.Unicode');
goog.require('goog.ui.Component');



/**
 * Builds a DrilldownRow component, which can overlay a tree
 * structure onto sections of an HTML table.
 *
 * @param {!goog.ui.DrilldownRow.DrilldownRowProperties=} opt_properties
 *   Optional properties.
 * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
 * @constructor
 * @extends {goog.ui.Component}
 * @final
 */
goog.ui.DrilldownRow = function(opt_properties, opt_domHelper) {
  'use strict';
  goog.ui.Component.call(this, opt_domHelper);
  var properties = opt_properties || {};

  // Initialize instance variables.

  var html;
  if (properties.html == null) {
    html = goog.html.SafeHtml.EMPTY;
  } else {
    goog.asserts.assert(properties.html instanceof goog.html.SafeHtml);
    html = properties.html;
  }
  /**
   * String of HTML to initialize the DOM structure for the table row.
   * Should have the form '<tr attr="etc">Row contents here</tr>'.
   * @type {!goog.html.SafeHtml}
   * @private
   */
  this.html_ = html;

  /**
   * Controls whether this component's children will show when it shows.
   * @type {boolean}
   * @private
   */
  this.expanded_ =
      typeof properties.expanded != 'undefined' ? properties.expanded : true;

  /**
   * If this component's DOM element is created from a string of
   * HTML, this is the function to call when it is entered into the DOM tree.
   * @type {Function} args are DrilldownRow and goog.events.EventHandler
   *   of the DrilldownRow.
   * @private
   */
  this.decoratorFn_ = properties.decorator || goog.ui.DrilldownRow.decorate;

  /**
   * Is the DrilldownRow to be displayed?  If it is rendered, this mirrors
   * the style.display of the DrilldownRow's row.
   * @type {boolean}
   * @private
   */
  this.displayed_ = true;
};
goog.inherits(goog.ui.DrilldownRow, goog.ui.Component);


/**
 * Used to define properties for a new DrilldownRow. Properties can contain:
 *   loaded: initializes the isLoaded property, defaults to true.
 *   expanded: DrilldownRow expanded or not, default is true.
 *   html: Relevant and required for DrilldownRows to be added as
 *     children.  Ignored when decorating an existing table row.
 *   decorator: Function that accepts one DrilldownRow argument, and
 *     should customize and style the row.  The default is to call
 *     goog.ui.DrilldownRow.decorator.
 * @typedef {{
 *   loaded: (boolean|undefined),
 *   expanded: (boolean|undefined),
 *   html: (!goog.html.SafeHtml|undefined),
 *   decorator: (Function|undefined)
 * }}
 */
goog.ui.DrilldownRow.DrilldownRowProperties;


/**
 * Example object with properties of the form accepted by the class
 * constructor.  These are educational and show the compiler that
 * these properties can be set so it doesn't emit warnings.
 */
goog.ui.DrilldownRow.sampleProperties = {
  html: goog.html.SafeHtml.create(
      goog.dom.TagName.TR, {},
      goog.html.SafeHtml.concat(
          goog.html.SafeHtml.create(goog.dom.TagName.TD, {}, 'Sample'),
          goog.html.SafeHtml.create(goog.dom.TagName.TD, {}, 'Sample'))),
  loaded: true,
  decorator: function(selfObj, handler) {
    'use strict';
    // When the mouse is hovering, add CSS class goog-drilldown-hover.
    goog.ui.DrilldownRow.decorate(selfObj);
    var row = selfObj.getElement();
    handler.listen(row, 'mouseover', function() {
      'use strict';
      goog.dom.classlist.add(row, goog.getCssName('goog-drilldown-hover'));
    });
    handler.listen(row, 'mouseout', function() {
      'use strict';
      goog.dom.classlist.remove(row, goog.getCssName('goog-drilldown-hover'));
    });
  }
};


//
// Implementations of Component methods.
//


/**
 * The base class method calls its superclass method and this
 * drilldown's 'decorator' method as defined in the constructor.
 * @override
 */
goog.ui.DrilldownRow.prototype.enterDocument = function() {
  'use strict';
  goog.ui.DrilldownRow.superClass_.enterDocument.call(this);
  this.decoratorFn_(this, this.getHandler());
};


/** @override */
goog.ui.DrilldownRow.prototype.createDom = function() {
  'use strict';
  this.setElementInternal(
      goog.ui.DrilldownRow.createRowNode_(this.html_, this.getDomHelper()));
};


/**
 * A top-level DrilldownRow decorates a TR element.
 *
 * @param {Element} node The element to test for decorability.
 * @return {boolean} true iff the node is a TR.
 * @override
 */
goog.ui.DrilldownRow.prototype.canDecorate = function(node) {
  'use strict';
  return node.tagName == goog.dom.TagName.TR;
};


/**
 * Child drilldowns are rendered when needed.
 *
 * @param {goog.ui.Component} child New DrilldownRow child to be added.
 * @param {number} index position to be occupied by the child.
 * @param {boolean=} opt_render true to force immediate rendering.
 * @override
 */
goog.ui.DrilldownRow.prototype.addChildAt = function(child, index, opt_render) {
  'use strict';
  goog.asserts.assertInstanceof(child, goog.ui.DrilldownRow);
  goog.ui.DrilldownRow.superClass_.addChildAt.call(this, child, index, false);
  child.setDisplayable_(this.isVisible_() && this.isExpanded());
  if (opt_render && !child.isInDocument()) {
    child.render();
  }
};


/** @override */
goog.ui.DrilldownRow.prototype.removeChild = function(child) {
  'use strict';
  goog.dom.removeNode(child.getElement());
  return goog.ui.DrilldownRow.superClass_.removeChild.call(this, child);
};


/**
 * Rendering of DrilldownRow's is on need, do not call this directly
 * from application code.
 *
 * Rendering a DrilldownRow places it according to its position in its
 * tree of DrilldownRows.  DrilldownRows cannot be placed any other
 * way so this method does not use any arguments.  This does not call
 * the base class method and does not modify any of this
 * DrilldownRow's children.
 * @override
 */
goog.ui.DrilldownRow.prototype.render = function() {
  'use strict';
  if (arguments.length) {
    throw new Error('A DrilldownRow cannot be placed under a specific parent.');
  } else {
    var parent = this.getParent();
    if (!parent.isInDocument()) {
      throw new Error('Cannot render child of un-rendered parent');
    }
    // The new child's TR node needs to go just after the last TR
    // of the part of the parent's subtree that is to the left
    // of this.  The subtree includes the parent.
    goog.asserts.assertInstanceof(parent, goog.ui.DrilldownRow);
    var previous = parent.previousRenderedChild_(this);
    var row;
    if (previous) {
      goog.asserts.assertInstanceof(previous, goog.ui.DrilldownRow);
      row = previous.lastRenderedLeaf_().getElement();
    } else {
      row = parent.getElement();
    }
    row = /** @type {Element} */ (row.nextSibling);
    // Render the child row component into the document.
    if (row) {
      this.renderBefore(row);
    } else {
      // Render at the end of the parent of this DrilldownRow's
      // DOM element.
      var tbody = /** @type {Element} */ (parent.getElement().parentNode);
      goog.ui.DrilldownRow.superClass_.render.call(this, tbody);
    }
  }
};


/**
 * Finds the numeric index of this child within its parent Component.
 * Throws an exception if it has no parent.
 *
 * @return {number} index of this within the children of the parent Component.
 */
goog.ui.DrilldownRow.prototype.findIndex = function() {
  'use strict';
  var parent = this.getParent();
  if (!parent) {
    throw new Error('Component has no parent');
  }
  return parent.indexOfChild(this);
};


//
// Type-specific operations
//


/**
 * Returns the expanded state of the DrilldownRow.
 *
 * @return {boolean} true iff this is expanded.
 */
goog.ui.DrilldownRow.prototype.isExpanded = function() {
  'use strict';
  return this.expanded_;
};


/**
 * Sets the expanded state of this DrilldownRow: makes all children
 * displayable or not displayable corresponding to the expanded state.
 *
 * @param {boolean} expanded whether this should be expanded or not.
 */
goog.ui.DrilldownRow.prototype.setExpanded = function(expanded) {
  'use strict';
  if (expanded != this.expanded_) {
    this.expanded_ = expanded;
    var elem = this.getElement();
    goog.asserts.assert(elem);
    goog.dom.classlist.toggle(elem, goog.getCssName('goog-drilldown-expanded'));
    goog.dom.classlist.toggle(
        elem, goog.getCssName('goog-drilldown-collapsed'));
    if (this.isVisible_()) {
      this.forEachChild(function(child) {
        'use strict';
        child.setDisplayable_(expanded);
      });
    }
  }
};


/**
 * Returns this DrilldownRow's level in the tree.  Top level is 1.
 *
 * @return {number} depth of this DrilldownRow in its tree of drilldowns.
 */
goog.ui.DrilldownRow.prototype.getDepth = function() {
  'use strict';
  for (var component = this, depth = 0;
       component instanceof goog.ui.DrilldownRow;
       component = component.getParent(), depth++) {
  }
  return depth;
};


/**
 * This static function is a default decorator that adds HTML at the
 * beginning of the first cell to display indentation and an expander
 * image; sets up a click handler on the toggler; initializes a class
 * for the row: either goog-drilldown-expanded or
 * goog-drilldown-collapsed, depending on the initial state of the
 * DrilldownRow; and sets up a click event handler on the toggler
 * element.
 *
 * This creates a DIV with class=toggle.  Your application can set up
 * CSS style rules something like this:
 *
 * tr.goog-drilldown-expanded .toggle {
 *   background-image: url('minus.png');
 * }
 *
 * tr.goog-drilldown-collapsed .toggle {
 *   background-image: url('plus.png');
 * }
 *
 * These background images show whether the DrilldownRow is expanded.
 * @param {goog.ui.DrilldownRow} selfObj DrilldownRow to be decorated.
 * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
 */
goog.ui.DrilldownRow.decorate = function(selfObj) {
  'use strict';
  var depth = selfObj.getDepth();
  var row = selfObj.getElement();
  goog.asserts.assert(row);
  if (!row.cells) {
    throw new Error('No cells');
  }
  var cell = row.cells[0];
  var dom = selfObj.getDomHelper();
  var fragment = dom.createDom(
      goog.dom.TagName.DIV, {'style': 'float: left; width: ' + depth + 'em;'},
      dom.createDom(
          goog.dom.TagName.DIV,
          {'class': 'toggle', 'style': 'width: 1em; float: right;'},
          // NOTE: NBSP is probably only needed by IE6. This div can probably be
          // made contentless.
          goog.string.Unicode.NBSP));
  cell.insertBefore(fragment, cell.firstChild);
  goog.dom.classlist.add(
      row, selfObj.isExpanded() ? goog.getCssName('goog-drilldown-expanded') :
                                  goog.getCssName('goog-drilldown-collapsed'));
  // Default mouse event handling:
  var toggler =
      goog.dom.getElementsByTagName(goog.dom.TagName.DIV, fragment)[0];
  selfObj.getHandler().listen(toggler, 'click', function(event) {
    'use strict';
    selfObj.setExpanded(!selfObj.isExpanded());
  });
};


//
// Private methods
//


/**
 * Turn display of a DrilldownRow on or off.  If the DrilldownRow has not
 * yet been rendered, this renders it.  This propagates the effect
 * of the change recursively as needed -- children displaying iff the
 * parent is displayed and expanded.
 *
 * @param {boolean} display state, true iff display is desired.
 * @private
 */
goog.ui.DrilldownRow.prototype.setDisplayable_ = function(display) {
  'use strict';
  if (display && !this.isInDocument()) {
    this.render();
  }
  if (this.displayed_ == display) {
    return;
  }
  this.displayed_ = display;
  if (this.isInDocument()) {
    this.getElement().style.display = display ? '' : 'none';
  }
  var selfObj = this;
  this.forEachChild(function(child) {
    'use strict';
    child.setDisplayable_(display && selfObj.expanded_);
  });
};


/**
 * True iff this and all its DrilldownRow parents are displayable.  The
 * value is an approximation to actual visibility, since it does not
 * look at whether DOM nodes containing the top-level component have
 * display: none, visibility: hidden or are otherwise not displayable.
 * So this visibility is relative to the top-level component.
 *
 * @return {boolean} visibility of this relative to its top-level drilldown.
 * @private
 */
goog.ui.DrilldownRow.prototype.isVisible_ = function() {
  'use strict';
  for (var component = this; component instanceof goog.ui.DrilldownRow;
       component = component.getParent()) {
    if (!component.displayed_) return false;
  }
  return true;
};


/**
 * Create and return a TR element from HTML that looks like
 * "<tr> ... </tr>".
 * @param {!goog.html.SafeHtml} html for one row.
 * @param {!goog.dom.DomHelper} dom DOM to hold the Element.
 * @return {Element} table row node created from the HTML.
 * @private
 * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
 */
goog.ui.DrilldownRow.createRowNode_ = function(html, dom) {
  'use strict';
  // Note: this may be slow.
  var tableHtml = goog.html.SafeHtml.create(goog.dom.TagName.TABLE, {}, html);
  var div = dom.createElement(goog.dom.TagName.DIV);
  goog.dom.safe.setInnerHtml(div, tableHtml);
  return div.firstChild.rows[0];
};


/**
 * Get the recursively rightmost child that is in the document.
 *
 * @return {goog.ui.DrilldownRow} rightmost child currently entered in
 *     the document, potentially this DrilldownRow.  If this is in the
 *     document, result is non-null.
 * @private
 */
goog.ui.DrilldownRow.prototype.lastRenderedLeaf_ = function() {
  'use strict';
  var leaf = null;
  for (var node = this; node && node.isInDocument();
       // Node will become undefined if parent has no children.
       node = node.getChildAt(node.getChildCount() - 1)) {
    leaf = node;
  }
  return /** @type {goog.ui.DrilldownRow} */ (leaf);
};


/**
 * Search this node's direct children for the last one that is in the
 * document and is before the given child.
 * @param {goog.ui.DrilldownRow} child The child to stop the search at.
 * @return {goog.ui.Component?} The last child component before the given child
 *     that is in the document.
 * @private
 */
goog.ui.DrilldownRow.prototype.previousRenderedChild_ = function(child) {
  'use strict';
  for (var i = this.getChildCount() - 1; i >= 0; i--) {
    if (this.getChildAt(i) == child) {
      for (var j = i - 1; j >= 0; j--) {
        var prev = this.getChildAt(j);
        if (prev.isInDocument()) {
          return prev;
        }
      }
    }
  }
  return null;
};