YUI3的树实现
在ExtFrame里,我实现了一颗可以自动加载所有节点的树(编程人员无需再为树编写一大堆代码),这颗树是通过继承Ext.TreePanel实现的
但是YUI3的标准版本里,并没有树的相关实现,想做到同样功能有点难了
经过查找,YUI3的Gallery里到是有treeview模块实现(版本3.7),花了几天测试,不过后来发现,原来YUI3 Gallery里有两个treeview实现,一个是Treeview(T索引),另一个是YUI Treeview(Y索引),不过我研究的是前一个,后一个咋看起来好像更好看些
这个treeview的效果网页上有Demo,代码调用方式是直接使用Gallery模块,但是这种方式要求种子使用官方地址,对内部使用的系统不合适
代码是这样的:
var treeview = new Y.CheckBoxTreeView({ startCollapsed: true, toggleOnLabelClick: false, children: [ { label: "Root", checked: "halfchecked", children: [ { label : "sub 1", checked: "checked", children : [ { label: "sub 1-1"}, { label: "sub 1-2"}, ] }, { label : "sub 2", children : [ { label: "sub 2-1"}, { label: "sub 2-2", children: [ { label: "sub 2-2-1" }, { label: "sub 2-2-2" } ] }, ] } ] }] }); treeview.render("#cattree1");
效果么看起来马马虎虎,反正给人一种远不如ExtJS的感觉
地址是http://yuilibrary.com/gallery/show/yui3treeview-ng
这个Demo的问题是要使用必须引用的种子是官方地址,然后模块是gallery-yui3treeview-ng,对内网系统来说有点不太方便(至少种子引用应该是本地服务器吧)
还是下载到本地加入本地模块
下载下来后有些代码要修改,再加上拷贝的代码里还有些错误,研究后做了如下修正:
1、修正了有张背景图片不透明的问题(在灰色背景下有的节点显示正常,有的显示白底)
2、为节点添加了图片,通过icon属性设定CSS可以显示图标,这样效果基本就看起来很象ExtJS的树了
3、修正了些小错误,例如nodeclick事件
4、考虑到框架应用,原本展开、收缩的框是通过isLeaf属性判断的(即节点是否叶子),但实际应用中,有可能需要动态展开,所以改成添加expandable属性,根据该属性判断是否可展开
修改后代码是这样的
YUI.add('treeview', function(Y) { 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("expandable")) { // 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 }); } else { this.fire("nodeClick", { 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}><a class={iconClassName}>{label}</a></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, iconClassName: this.get('icon') == '' ? '' : ' ' + this.get('icon'), label: label }); labelContent = labelContentHTML; treeLabel = Y.Node.create(treeLabelHTML); boundingBox.prepend(treeLabel); } if (this.get('expandable')) { treeLabel.appendChild(toggleControlHTML).appendChild(labelContent); } else { treeLabel.append(labelContent); } boundingBox.set("role", "presentation"); if (this.get('expandable')) { 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 (!this.get('expandable')) { 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 }, icon: { value: '' }, expandable: { value: false } }, 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(); } } } }); }, '3.6.0', { requires: ["substitute", "widget", "widget-parent", "widget-child", "node-focusmanager", "array-extras"] });
创建树及操作的代码可以和demo一样,也可以看我下一篇实现自动创建树的代码及效果图