来源于自己的博客小站“虾米成长记”!转载请注明出处,谢谢!
在项目中遇到一个需求:需要一个能显示树形层级关系的下拉控件。之前的做法是使用一个普通的下拉框控件,控件里的选项通过在其头部添加不同数量的空格来体现树形的层次关系。这是一个比较low的做法,既不美观,也无法做到选项节点的收缩展开。因此,需要做一个能“优美”显示树形选项的下拉框控件。由于项目使用了Extjs,而Extjs中并没有一个像这样的现成的控件,所以我扩展了Ext.form.field.Picker,自定义了一个控件,代码如下:
Ext.define('TreeComboBox', {
extend: 'Ext.form.field.Picker',
requires: ['Ext.tree.Panel'],
store: {},
tree: {},
scrollable: false,
config: {
maxPickerWidth: 350,
maxPickerHeight: 350
},
listeners: {
change: function (field, newValue, oldValue) {
if (!newValue || !newValue.trim()) {
var root = getTreeData(unitData);
this.store.setRoot(root);
} else {
this.tree.filterBy(newValue, 'text');
}
},
expand: function (field) {
var root = getTreeData(unitData);
this.store.setRoot(root);
this.tree.selectBy(field.getValue(), 'text');
}
},
initComponent: function () {
var self = this;
Ext.apply(self, {
fieldLabel: self.fieldLabel,
labelWidth: self.labelWidth
});
self.callParent();
this.tree = Ext.create("TreePanel", {
rootVisible: false,
width: self.maxPickerWidth,
height: self.maxPickerHeight,
scrollable: true,
floating: true,
focusOnToFront: false,
shadow: false,
useArrows: false,
store: this.store
});
this.tree.on('itemclick', function (view, record) {
self.setValue(record.get('text'));// 显示值
self.name = record.get("value");// 隐藏值
self.collapse();
});
},
createPicker: function () {
var self = this;
self.picker = this.tree;
return self.picker;
},
alignPicker: function () {
var me = this, picker, isAbove, aboveSfx = '-above';
if (this.isExpanded) {
picker = me.getPicker();
if (me.matchFieldWidth) {
picker.setWidth(680);
}
if (picker.isFloating()) {
picker.alignTo(me.inputEl, "tl-bl?", me.pickerOffset);// ->tl
isAbove = picker.el.getY() < me.inputEl.getY();
me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx);
picker.el[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx);
}
}
}
});
在如上的代码里可以看到“MyApp.plugin.TreePanel”,这是我扩展了“Ext.tree.Panel”的自定义控件。它的代码很简单,结合了另外一个自定义控件“MyApp.plugin.TreeFilter”,以实现TreeComboBox的文本框可以根据输入的字符自动过滤数据的功能,TreePanel的代码如下:
Ext.define('TreePanel', {
extend: 'Ext.tree.Panel',
mixins: ['TreeFilter']
});
TreeFilter的代码如下:
Ext.define("TreeFilter", {
filterBy: function (text, by) {
var view = this.getView(), me = this, nodes = [];
unitTree.cascadeBy(function (tree, view) {
var currNode = this;
if (currNode && currNode.data[by] && currNode.data[by].toString().trim() != 'Root' &&
currNode.data[by].toString().toLowerCase().indexOf(text.toLowerCase()) > -1) {
nodes.push(currNode);
}
}, null, [me, view]);
var root = {
expanded: true,
children: []
}
Ext.each(nodes, function (node) {
var child = {
text: node.get("text"),
value: node.get('value'),
leaf: true,
iconCls: 'no-icon'
}
root.children.push(child);
})
this.store.setRoot(root);
},
selectBy: function (text, by) {
var view = this.getView(), me = this, nodesAndParents = [];
this.getRootNode().cascadeBy(function (tree, view) {
var currNode = this;
if (currNode && currNode.data[by] && currNode.data[by].toString().toLowerCase() === text.toLowerCase()) {
me.expandPath(currNode.getPath(), {
select: true,
focus: true
});
}
}, null, [me, view]);
}
});
这里的TreePanel扩展了“Ext.tree.Panel”,正是想利用它的“tree”的特性以满足需求,只要绑定“Ext.data.TreeStore”对象,就可以显示出树形数据了。这里的TreeStore绑定的数据需要符合如下的数据结构:
{
expanded: true,
children: [{
text: 'zhangsan',
value: '1',
leaf: false,
iconCls: 'no-icon',
expanded: true,
children: [{
text: 'lisi'value: '1',
leaf: true,
children: [],
iconCls: 'no-icon',
expanded: false
}],
{
text: 'wangwu',
value: '3',
leaf: true,
children: [],
expanded: false,
iconCls: 'no-icon'
}
}]
}
需要说明的是在我们的项目里数据表中通过pid和id这两个字段来体现数据的层次关系,pid表示父亲节点的id,id表示子节点的id。后台向前台返回包含pid、id、value、text等字段对象的集合,在前台解析返回结果,组成“Tree”结构的数据,见如下代码:
function getTreeData(records) {
var unitHashMap = new Ext.util.HashMap();
Ext.each(records, function (item) {
if (!unitHashMap.containsKey(item.get("PunitId"))) {//父节点不存在
unitHashMap.add(item.get("PunitId"), [{
value: item.get("UnitId"),
text: item.get("UnitName"),
iconCls: 'no-icon',
leaf: true,
expanded: false,
children: []
}]);
} else {//父节点存在
var unit = unitHashMap.get(item.get("PunitId"));
unit.push({
value: item.get("UnitId"),
text: item.get("UnitName"),
iconCls: 'no-icon',
leaf: true,
expanded: false,
children: []
})
}
});
var removeKeys = [];
unitHashMap.each(function (key, value, length) {
var result = Ext.Array.findBy(records, function (item, index) {
return item.get("UnitId") === key;
});
if (result) {
var unit = unitHashMap.get(result.get("PunitId"));
var child = Ext.Array.findBy(unit, function (item, index) {
return item.value === key;
});
child.leaf = false;
Ext.each(value, function (v) {
child.children.push(v);
});
removeKeys.push(key);
}
})
Ext.each(removeKeys, function (key) {
unitHashMap.removeAtKey(key);
});
var rootData = {
expanded: true,
children: []
};
unitHashMap.each(function (key, value, length) {
Ext.each(value, function (v) {
rootData.children.push(v);
});
});
return rootData;
}
这里大体地阐述一下上述代码的实现思想:首先通过pid将数据分组,可以获得父节点pid对应的子节点id;然后再遍历pid,寻找pid之间的“父子”关系,如果一个节点是另外一个节点的子节点,就将该节点放到父节点的children字段里。在处理节点关系的过程中需要防止重复数据的产生。本文简单的记录一个“TreeComboBox”的实现过程,文字不多,而想要表达的精华其实都藏在代码里了!
Demo