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一样,也可以看我下一篇实现自动创建树的代码及效果图