| // 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. |
| * |
| * @author arv@google.com (Erik Arvidsson) |
| * @author eae@google.com (Emil A Eklund) |
| * |
| * 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.require('goog.Timer'); |
| goog.require('goog.a11y.aria'); |
| 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.html.legacyconversions'); |
| goog.require('goog.string'); |
| goog.require('goog.string.StringBuffer'); |
| goog.require('goog.style'); |
| goog.require('goog.ui.Component'); |
| |
| |
| |
| /** |
| * An abstract base class for a node in the tree. |
| * |
| * @param {string|!goog.html.SafeHtml} html The html content of the node label. |
| * @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(html, 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_ = (html instanceof goog.html.SafeHtml ? html : |
| goog.html.legacyconversions.safeHtmlFromString(html)); |
| |
| /** @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_; |
| }; |
| 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 = {}; |
| |
| |
| /** |
| * Whether the tree item is selected. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.tree.BaseNode.prototype.selected_ = false; |
| |
| |
| /** |
| * Whether the tree node is expanded. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.tree.BaseNode.prototype.expanded_ = false; |
| |
| |
| /** |
| * Tooltip for the tree item |
| * @type {?string} |
| * @private |
| */ |
| goog.ui.tree.BaseNode.prototype.toolTip_ = null; |
| |
| |
| /** |
| * HTML that can appear after the label (so not inside the anchor). |
| * @type {!goog.html.SafeHtml} |
| * @private |
| */ |
| goog.ui.tree.BaseNode.prototype.afterLabelHtml_ = goog.html.SafeHtml.EMPTY; |
| |
| |
| /** |
| * Whether to allow user to collapse this node. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.tree.BaseNode.prototype.isUserCollapsible_ = true; |
| |
| |
| /** |
| * Nesting depth of this node; cached result of computeDepth_. |
| * -1 if value has not been cached. |
| * @type {number} |
| * @private |
| */ |
| goog.ui.tree.BaseNode.prototype.depth_ = -1; |
| |
| |
| /** @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, 'expanded', 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()) { |
| // 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 {@code 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); |
| |
| if (this.getElement()) { |
| this.updateExpandIcon(); |
| if (this.getExpanded()) { |
| var el = this.getChildrenElement(); |
| if (!child.getElement()) { |
| child.createDom(); |
| } |
| var childElement = child.getElement(); |
| var nextElement = nextNode && nextNode.getElement(); |
| el.insertBefore(childElement, nextElement); |
| |
| if (this.isInDocument()) { |
| child.enterDocument(); |
| } |
| |
| if (!nextNode) { |
| if (prevNode) { |
| prevNode.updateExpandIcon(); |
| } else { |
| goog.style.setElementShown(el, 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 this node is now removed. |
| tree.removeNode(this); |
| |
| if (this.isInDocument()) { |
| var el = this.getChildrenElement(); |
| |
| if (child.isInDocument()) { |
| var childEl = child.getElement(); |
| el.removeChild(childEl); |
| |
| child.exitDocument(); |
| } |
| |
| if (wasLast) { |
| var newLast = this.getLastChild(); |
| if (newLast) { |
| newLast.updateExpandIcon(); |
| } |
| } |
| if (!this.hasChildren()) { |
| el.style.display = 'none'; |
| this.updateExpandIcon(); |
| this.updateIcon_(); |
| } |
| } |
| } |
| |
| 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. |
| */ |
| 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 {@code 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); |
| |
| // 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_(); |
| goog.a11y.aria.setState(el, 'expanded', expanded); |
| } |
| |
| 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_; |
| }; |
| |
| |
| // TODO(jakubvrana): Deprecate in favor of setSafeHtml, once developer docs on |
| // using goog.html.SafeHtml are in place. |
| /** |
| * 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 {string} html The html. |
| */ |
| goog.ui.tree.BaseNode.prototype.setAfterLabelHtml = function(html) { |
| this.setAfterLabelSafeHtml(goog.html.legacyconversions.safeHtmlFromString( |
| html)); |
| }; |
| |
| |
| /** |
| * 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_)); |
| }; |
| |
| |
| // TODO(jakubvrana): Deprecate in favor of setSafeHtml, once developer docs on |
| // using goog.html.SafeHtml are in place. |
| /** |
| * Sets the html of the label. |
| * @param {string} s The html string for the label. |
| */ |
| goog.ui.tree.BaseNode.prototype.setHtml = function(s) { |
| this.setSafeHtml(goog.html.legacyconversions.safeHtmlFromString(s)); |
| }; |
| |
| |
| /** |
| * 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') |
| }; |