/**
* @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;
};