var getClassName = Y.ClassNameManager.getClassName, BOUNDING_BOX = "boundingBox", CONTENT_BOX = "contentBox", TREEVIEW = "treeview", TREENODE = "treenode", CHECKBOXTREEVIEW = "checkboxtreeview", CHECKBOXTREENODE = "checkboxtreenode", classNames = { tree : getClassName(TREENODE), content : getClassName(TREENODE, "content"), label : getClassName(TREENODE, "label"), labelContent : getClassName(TREENODE, "label-content"), toggle : getClassName(TREENODE, "toggle-control"), collapsed : getClassName(TREENODE, "collapsed"), leaf : getClassName(TREENODE, "leaf"), lastnode : getClassName(TREENODE, "last"), checkbox : getClassName(CHECKBOXTREENODE, "checkbox") }, checkStates = { // Check states for checkbox tree unchecked: 10, halfchecked: 20, checked: 30 }, checkStatesClasses = { 10 : getClassName(CHECKBOXTREENODE, "checkbox-unchecked"), 20 : getClassName(CHECKBOXTREENODE, "checkbox-halfchecked"), 30 : getClassName(CHECKBOXTREENODE, "checkbox-checked") }, findChildren; /* * Used in HTML_PARSERs to find children of the current widget */ findChildren = function (srcNode, selector) { var descendants = srcNode.all(selector), children = Array(), child; descendants.each(function(node) { child = { srcNode : node, boundingBox : node, contentBox : node.one("> ul") }; children.push(child); }); return children; }; /** * TreeView widget. Provides a tree style widget, with a hierachical representation of it's components. * It extends WidgetParent and WidgetChild, please refer to it's documentation for more info. * This widget represents the root cotainer for TreeNode objects that build the actual tree structure. * Therefore this widget will not usually have any visual representation. Its also responsible for handling node events. * @class TreeView * @constructor * @uses WidgetParent * @extends Widget * @param {Object} config User configuration object. */ Y.TreeView = Y.Base.create(TREEVIEW, Y.Widget, [Y.WidgetParent], { CONTENT_TEMPLATE : "<ul></ul>", initializer : function (config) { /** * Fires when node is expanded / collapsed * @event nodeToggle * @param {TreeNode} treenode tree node that is expanding / collapsing. * Use this event to listed for nodes being clicked. */ this.publish("nodeToggle", { defaultFn: this._nodeToggleDefaultFn }); /** * Fires when node is collapsed * @event nodeCollapse * @param {TreeNode} treenode tree node that is collapsing */ this.publish("nodeCollapse", { defaultFn: this._nodeCollapseDefaultFn }); /** * Fires when node is expanded * @event nodeExpand * @param {TreeNode} treenode tree node that is expanding */ this.publish("nodeExpand", { defaultFn: this._nodeExpandDefaultFn }); /** * Fires when node is clicked * @event nodeClick * @param {TreeNode} treenode tree node that is being clicked */ this.publish("nodeClick", { defaultFn: this._nodeClickDefaultFn }); }, /** * Default event handler for "nodeclick" event * @method _nodeClickDefaultFn * @protected */ _nodeClickDefaultFn: function(e) { }, /** * Default event handler for "toggleTreeState" event * @method _nodeToggleDefaultFn * @protected */ _nodeToggleDefaultFn: function(e) { if (e.treenode.get("collapsed")) { this.fire("nodeExpand", {treenode: e.treenode}); } else { this.fire("nodeCollapse", {treenode: e.treenode}); } }, /** * Default event handler for "collapse" event * @method _nodeCollapseDefaultFn * @protected */ _nodeCollapseDefaultFn: function(e) { e.treenode.collapse(); }, /** * Default event handler for "expand" event * @method _expandStateDefaultFn * @protected */ _nodeExpandDefaultFn: function(e) { e.treenode.expand(); }, /** * Sets child event handlers * @method _setChildEventHandlers * @protected */ _setChildEventHandlers : function () { var parent; this.after("addChild", function(e) { parent = e.child.get("parent"); if (e.child.get("isLast") && parent.size() > 1) { parent.item(e.child.get("index")-1)._unmarkLast(); } }); this.on("removeChild", function(e) { parent = e.child.get("parent"); if ((parent.size() == 1) || e.child.get("index") === 0) { return; } if (e.child.get("isLast")) { parent.item(e.child.get("index")-1)._markLast(); } }); }, /** * Handles internal tree click events * @method _onClickEvents * @protected */ _onClickEvents : function (event) { var target = event.target, twidget = Y.Widget.getByNode(target), toggle = false; event.preventDefault(); twidget = Y.Widget.getByNode(target); if (!twidget instanceof Y.TreeNode) { return; } if (twidget.get("isLeaf")) { return; } Y.Array.each(target.get("className").split(" "), function(className) { switch (className) { case classNames.toggle: toggle = true; break; case classNames.labelContent: if (this.get("toggleOnLabelClick")) { toggle = true; } break; } }, this); if (toggle) { this.fire("nodeToggle", {treenode: twidget}); } }, /** * Handles internal tree keyboard interaction * @method _onKeyEvents * @protected */ _onKeyEvents : function (event) { var target = event.target, twidget = Y.Widget.getByNode(target), keycode = event.keyCode, collapsed = twidget.get("collapsed"); if (twidget.get("isLeaf")) { return; } if ( ((keycode == 39) && collapsed) || ((keycode == 37) && !collapsed) ) { this.fire("nodeToggle", {treenode: twidget}); } }, bindUI : function() { var boundingBox = this.get(BOUNDING_BOX); boundingBox.on("click", this._onClickEvents, this); boundingBox.on("keypress", this._onKeyEvents, this); boundingBox.delegate("click", Y.bind(function(e) { var twidget = Y.Widget.getByNode(e.target); if (twidget instanceof Y.TreeNode) { this.fire("nodeclick", {treenode: twidget}); } }, this), "."+classNames.label); this._setChildEventHandlers(); boundingBox.plug(Y.Plugin.NodeFocusManager, { descendants: ".yui3-treenode-label", keys: { next: "down:40", // Down arrow previous: "down:38" // Up arrow }, circular: false }); } }, { NAME : TREEVIEW, ATTRS : { /** * @attribute defaultChildType * @type String * @readOnly * @description default child type definition */ defaultChildType : { value: "TreeNode", readOnly: true }, /** * @attribute toggleOnLabelClick * @type Boolean * @description whether to toogle tree state on label clicks with addition to toggle control clicks */ toggleOnLabelClick : { value: true, validator: Y.Lang.isBoolean }, /** * @attribute startCollapsed * @type Boolean * @description Whether to render tree nodes expanded or collapsed by default */ startCollapsed : { value: true, validator: Y.Lang.isBoolean }, /** * @attribute loadOnDemand * @type boolean * * @description Whether children of this node can be loaded on demand * (when this tree node is expanded, for example). * Use with gallery-yui3treeview-ng-datasource. */ loadOnDemand : { value: false, validator: Y.Lang.isBoolean } }, HTML_PARSER : { children : function (srcNode) { return findChildren(srcNode, "> li"); } } }); /** * TreeNode widget. Provides a tree style node widget. * It extends WidgetParent and WidgetChild, please refer to it's documentation for more info. * @class TreeNode * @constructor * @uses WidgetParent, WidgetChild * @extends Widget * @param {Object} config User configuration object. */ Y.TreeNode = Y.Base.create(TREENODE, Y.Widget, [Y.WidgetParent, Y.WidgetChild], { /** * Flag to determine if the tree is being rendered from markup or not * @property _renderFromMarkup * @protected */ _renderFromMarkup : false, CONTENT_TEMPLATE : "<ul></ul>", BOUNDING_TEMPLATE : "<li></li>", TREENODELABEL_TEMPLATE : "<a class={labelClassName} role='treeitem' href='#'></a>", TREENODELABELCONTENT_TEMPLATE : "<span class={labelContentClassName}>{label}</span>", TOGGLECONTROL_TEMPLATE : "<span class={toggleClassName}></span>", bindUI : function() { // Both TreeVew and TreeNode share the same child event handling Y.TreeView.prototype._setChildEventHandlers.apply(this, arguments); }, /** * Renders TreeNode * @method renderUI * @protected */ renderUI : function() { var boundingBox = this.get(BOUNDING_BOX), treeLabel, treeLabelHTML, labelContent, labelContentHTML, toggleControlHTML, label, isLeaf; toggleControlHTML = Y.substitute(this.TOGGLECONTROL_TEMPLATE,{toggleClassName: classNames.toggle}); isLeaf = this.get("isLeaf"); if (this._renderFromMarkup) { treeLabel = boundingBox.one(":first-child"); treeLabel.set("role", "treeitem"); treeLabel.addClass(classNames.label); labelContent = treeLabel.removeChild(treeLabel.one(":first-child")); labelContent.addClass(classNames.labelContent); } else { label = this.get("label"); treeLabelHTML = Y.substitute(this.TREENODELABEL_TEMPLATE, {labelClassName: classNames.label}); labelContentHTML = Y.substitute(this.TREENODELABELCONTENT_TEMPLATE, {labelContentClassName: classNames.labelContent, label: label}); labelContent = labelContentHTML; treeLabel = Y.Node.create(treeLabelHTML); boundingBox.prepend(treeLabel); } if (!isLeaf) { treeLabel.appendChild(toggleControlHTML).appendChild(labelContent); } else { treeLabel.append(labelContent); } boundingBox.set("role","presentation"); if (!isLeaf) { if (this.get("root").get("startCollapsed")) { boundingBox.addClass(classNames.collapsed); } else { if (this.size() === 0) { // Nodes (not leafs) without children should start in collapsed mode boundingBox.addClass(classNames.collapsed); } } } if (isLeaf) { boundingBox.addClass(classNames.leaf); } if (this.get("isLast")) { this._markLast(); } }, /** * Marks this node as the last one in list * @method _markLast * @protected */ _markLast : function() { this.get(BOUNDING_BOX).addClass(classNames.lastnode); }, /** * Unmarks this node as the last one in list * @method _markLast * @protected */ _unmarkLast : function() { this.get(BOUNDING_BOX).removeClass(classNames.lastnode); }, /** * Collapse the tree * @method collapse */ collapse : function () { var boundingBox = this.get(BOUNDING_BOX); if (!boundingBox.hasClass(classNames.collapsed)) { boundingBox.toggleClass(classNames.collapsed); } }, /** * Expands the tree * @method expand */ expand : function () { var boundingBox = this.get(BOUNDING_BOX); if (boundingBox.hasClass(classNames.collapsed)) { boundingBox.toggleClass(classNames.collapsed); } }, /** * Toggle current expaned/collapsed tree state * @method toggleState */ toggleState : function () { this.get(BOUNDING_BOX).toggleClass(classNames.collapsed); }, /** * Returns breadcrumbs path of labels from root of the tree to this node (inclusive) * @method path * @param cfg {Object} An object literal with the following properties: * <dl> * <dt><code>labelAttr</code></dt> * <dd>Attribute name to use for node representation. Can be any attribute of TreeNode</dd> * <dt><code>reverse</code></dt> * <dd>Return breadcrumbs from the node to root instead of root to the node</dd> * </dl> * @return {Array} array of node labels */ path : function(cfg) { var bc = Array(), node = this; if (!cfg) { cfg = {}; } if (!cfg.labelAttr) { cfg.labelAttr = "label"; } while (node && (node instanceof Y.TreeNode) ) { bc.unshift(node.get(cfg.labelAttr)); node = node.get("parent"); } if (cfg.reverse) { bc = bc.reverse(); } return bc; }, /** * Returns toggle control node * @method _getToggleControlNode * @protected */ _getToggleControlNode : function() { return this.get(BOUNDING_BOX).one("." + classNames.toggle); }, /** * Returns label content node * @method _getLabelContentNode * @protected */ _getLabelContentNode : function() { return this.get(BOUNDING_BOX).one("." + classNames.labelContent); } }, { NAME : TREENODE, ATTRS : { /** * @attribute defaultChildType * @type String * @readOnly * @description default child type definition */ defaultChildType : { value: "TreeNode", readOnly: true }, /** * @attribute label * @type String * * @description TreeNode node label */ label : { validator: Y.Lang.isString, value: "" }, /** * @attribute loadOnDemand * @type boolean * * @description Whether children of this node can be loaded on demand * (when this tree node is expanded, for example). * Use with gallery-yui3treeview-ng-datasource. */ loadOnDemand : { value: false, validator: Y.Lang.isBoolean }, /** * @attribute collapsed * @type Boolean * @readOnly * * @description Represents current treenode state - whether its collapsed or extended */ collapsed : { value: null, getter: function() { return this.get(BOUNDING_BOX).hasClass(classNames.collapsed); }, readOnly: true }, /** * @attribute clabel * @type String * * @description Canonical label for the node. * You can set it to anything you like and use later with your external tools. */ clabel : { value: "", validator: Y.Lang.isString }, /** * @attribute nodeId * @type String * * @description Signifies id of this node. * You can set it to anything you like and use later with your external tools. */ nodeId : { value: "", validator: Y.Lang.isString }, /** * @attribute isLeaf * @type Boolean * * @description Signifies whether this node is a leaf node. * Nodes with loadOnDemand set to true are not considered leafs. */ isLeaf : { value: null, getter: function() { return (this.size() > 0 ? false : true) && (!this.get("loadOnDemand")); }, readOnly: true }, /** * @attribute isLast * @type Boolean * * @description Signifies whether this node is the last child of its parent. */ isLast : { value: null, getter: function() { return (this.get("index") + 1 == this.get("parent").size()); }, readOnly: true } }, HTML_PARSER: { children : function (srcNode) { return findChildren(srcNode, "> ul > li"); }, label : function(srcNode) { var labelContentNode = srcNode.one("> a > span"); if (labelContentNode !== null) { this._renderFromMarkup = true; return labelContentNode.getContent(); } } } });