记录项目中关于ztree插件的使用。
实现的功能有:排序、迁移、搜索、删除、右键菜单、重命名、新增。
html代码
html代码非常简单:具体的样式找美工调整
搜索框
<input type="text" placeholder="搜索 ..." id="nav-search-input">
树
<ul id="categoryTree" ></ul>
菜单
<div id="rMenu" >
<ul>
<li id="m_add" onclick="addTreeNode();">添加节点</li>
<li id="m_resetname" onclick="renameTreeNode();">重命名</li>
<li id="m_delete" onclick="removeTreeNode();">删除节点</li>
<li id="m_allcate_administrator" onclick="allcateAdmin();">分配管理员</li>
<li id="m_allcate_download" onclick="allcateDownload();">下载权限配置</li>
</ul>
</div>
参数设置及数据加载
var setting = {
async: {
enable: true,//采用异步加载
url : "加载树数据的url",
autoParam: ["id"],//自动提交的参数,每次异步加载时都会附带该参数
dataType : "json"//返回json格式的数据
},
data : {
key : {},
simpleData : {
enable : true,//采用简单数据格式
idKey : "id",//每个节点数据的id
//这里必须是pId,而不能写成pid,Pid等形式,否则数据无法显示
idPKey : "pId",
rootPid : "0"//根节点的id
}
},
view : {
expandSpeed : "",//关闭动画,否则搜索时频繁开闭节点会使节点状态错乱
fontCss : getFontCss,//设置字体:主要是搜索后对目标节点着色处理
showIcon : true,
dblClickExpand : false,
selectedMulti : true,
addHoverDom: addHoverDom,
removeHoverDom: removeHoverDom
},
callback : {
onRightClick : OnRightClick,
onClick : zTreeOnClick,
//beforeRename : beforeRename,
//onRename : zTreeOnRename,
beforeDrag : beforeDrag,
beforeDrop : zTreeBeforeDrop
},
check : {
enable : false,
},
edit : {
drag : {
isCopy : false,//
isMove : true,//
prev : true,//允许移动到目标节点前面
next : true,//允许移动到目标节点后面
inner : true,//允许移动到目标节点内
},
enable : true,
showRemoveBtn : false,
showRenameBtn : false
}
};
var categoryTree = $.fn.zTree.init($("#categoryTree"), setting);
var rMenu = $("#rMenu");
右键的显示与隐藏
这里是由于项目需要不同人员的权限不同,右键菜单选项不同;如果你也有这种需求,我觉得这里有两种实现方式,一是像这里写的,点击右键时去后代判断,根据判断结果再去显示;二是在获取树的信息的时候,把判断所需要的必要参数直接放在json里一块返回来,这样前台只需要直接判断就行了。
function OnRightClick(event, treeId, treeNode) {
categoryTree.selectNode(treeNode);
//后台请求右键选项有哪些
$.ajax({
cache : true,
type : "POST",
url : "${root.contextPath!}/klcategory/rightClickCategory",
data : {
id : treeNode.id
},
async : false,
error : function(request) {},
success : function(data) {
//data是map类型,放了一些判断右键菜单选项的参数
data = eval("(" + data + ")");
showRMenu(data, treeNode, event.clientX, event.clientY);
},
});
}
//弹出右键菜单
function showRMenu(data, treeNode, x, y) {
$("#rMenu").show();
var count = 0;
if(!data.canRename && !data.canAddCategoty
&& !data.canDelete && !data.canAllocateDownload
&& !data.canAllocateAdmin) {
$("#rMenu").hide();
}else{
$("#rMenu").show();
$("#rMenu ul").show();
if(data.canRename){
$("#m_resetname").show();
count +=1
}else{
$("#m_resetname").hide();
}
if(data.canAddCategoty){
$("#m_add").show();
count +=1
}else{
$("#m_add").hide();
}
if(data.canDelete){
$("#m_delete").show();
count +=1
}else{
$("#m_delete").hide();
}
if(data.canAllocateDownload){
$("#m_allcate_download").show();
count +=1
}else{
$("#m_allcate_download").hide();
}
if(data.canAllocateAdmin){
$("#m_allcate_administrator").show();
count +=1
}else{
$("#m_allcate_administrator").hide();
}
}
//判断菜单会不会溢出窗口,如果溢出就改成上浮
//这是由于树的信息很多,有时右键菜单显示的时候,可能会溢出浏览器。
//所以做了一下处理。这里的33是每个菜单选项的高度值(单位px)
//需要根据你实际项目中的值再设定。
if($(window).height() - y < 33*count){
y = y - 33*count;
}
rMenu.css({
"top" : y + "px",
"left" : x + "px",
"visibility" : "visible"
});
$("body").bind("mousedown", onBodyMouseDown);
}
//隐藏右键菜单
function hideRMenu() {
if (rMenu)
rMenu.css({
"visibility" : "hidden"
});
$("body").unbind("mousedown", onBodyMouseDown);
}
//鼠标在非右键菜单位置点击时,隐藏菜单
function onBodyMouseDown(event) {
if (!(event.target.id == "rMenu"
|| $(event.target).parents("#rMenu").length > 0)) {
rMenu.css({
"visibility" : "hidden"
});
}
}
//点击树后的回调:选中当前节点
function zTreeOnClick(event, treeId, treeNode) {
categoryTree.selectNode(treeNode);
//这里可以根据需要,添加其他事件。
};
重命名 / 删除 / 新增
function renameTreeNode(){
//这里有两种实现方式:
(1)直接调用ztree的editName(),使节点进入可编辑状态。
命名之后再实现ztree的回调函数onRename,去后台更新数据;
(2)弹出对话框,在其中进行重命名,后台更新数据完成之后。
再设置该节点的name属性,调用树的updateNode方法进行更新树上的节点数据。
}
//---------------------------------删除节点------------------------------
function removeTreeNode() {
var nodes = categoryTree.getSelectedNodes();
//这里要进行后台的删除,后代删除成功后调用下面的方法删除
//categoryTree.removeNode(nodes[0]);
}
//----------------------------------新增节点--------------------------------------------------
function addTreeNode(){
//与重命名类似,实现方式有两种
(1)调用数的addNodes方法,再去后台执行逻辑
(2)弹出对话框先去后台执行逻辑,后台增加成功后,去刷新新增目录的直接父节点。
(调用reAsyncChildNodes,但要注意的是,如果父节点的isParent属性是false的话,
该方法是不会执行的,记得在调用reAsyncChildNodes之前先用ztree的updateNode
方法更新父节点的isParent属性为true);或者addNodes直接在树上添加节点
}
节点的检索
实现的效果是:输入框内只要有输入变化,就会执行一次搜索方法
火狐有一个bug,就是假如树中含有一个名字为“haha”的节点和名字为“哈哈”的节点,这时你用中文输入法去打“哈”这个字,发现只能输入英文。。扯远了,当然这个可以不做。重点是搜索方法。
其实获取到含有搜索关键字的所有节点很简单,ztree已经封装好了,就是getNodesByParamFuzzy()。
但是,想要的效果是,执行下一次搜索时,要把上一次搜索展开的节点全都关闭,再去展开这种的符合条件的节点。
按道理讲,也很简单,调用expandAll(false)就可以将节点都关闭,然后再去展开符合条件的节点就行了。
但是这样写了以后,发现上次搜索展开的节点并不会全部关闭,而是有的关闭了,有的还是展开的。
在网上搜了很久,在ztree的github项目的issue中找到了一点线索,说展开和关闭都是有动画的。
我想可能是前面的关闭所有节点还没有执行完,与后面的展开节点冲突了。
于是就去api里找,发现有expandSpeed这个参数设置,将动画关闭,这个bug竟然好了。
具体的关于dom的内部原理我不了解。
//输入框内获取焦点时按回车执行搜索,并屏蔽掉后续事件(提交表单等)
$("#nav-search-input").focus(function() {
//输入框内容变化时执行搜索
var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
if (userAgent.indexOf("Firefox") > -1) {//单独对Firefox浏览器
var node = document.querySelector('#nav-search-input');
var cpLock = false;
node.addEventListener('compositionstart', function(){
cpLock = true;
});
node.addEventListener('compositionend', function(){
cpLock = false;
});
node.addEventListener('input', function(){
if(!cpLock){
categorySearch();
$("#nav-search-input").focus();
}
});
}else{
$("#nav-search-input").bind('input propertychange', function() {
categorySearch();
$("#nav-search-input").focus();
});
}
$("#nav-search-input").keypress(function(e) {
var key = window.event ? e.keyCode : e.which;
if (key.toString() == "13") {
categorySearch();
return false;
}
});
});
//关于节点检索,只看这个方法就行了。
function categorySearch() {
//收起所有节点
categoryTree.expandAll(false);
//获取所有节点
var allNodes = getAllNodes();
//将所有节点不着色
$(allNodes).each(function() {
this.highlight = false;
categoryTree.updateNode(this);
});
//清除选中状态
$(allNodes).each(function() {
categoryTree.cancelSelectedNode(this);
});
//获取输入的内容
var searchContent = $("#nav-search-input").val();
//如果节点中含有搜索内容,就展开其父节点,并选中当前节点
if (searchContent.length > 0) {
//获取包含搜索内容的节点
var nodes = categoryTree.getNodesByParamFuzzy("name",
searchContent, null);
//对包含搜索内容的节点着色和展开
$(nodes).each(
function() {
categoryTree.selectNode(this, true, true);
this.highlight = true;
categoryTree.updateNode(this);
categoryTree.expandNode(this.getParentNode(), true,
false, true);
});
}
}
//获取所有节点数据
function getAllNodes() {
//这个方法仅仅获取所有根节点
var rootNodes = categoryTree.getNodes();
//这样才可以获取到所有节点
return categoryTree.transformToArray(rootNodes);
}
//设置字体颜色
function getFontCss(treeId, treeNode) {
return (!!treeNode.highlight) ? {
color : "#A60000",
"font-weight" : "bold"
} : {
color : "#333",
"font-weight" : "normal"
};
}
节点的迁移(包含了排序)
//迁移节点前执行的操作:主要是对权限的处理
//项目中,有些节点不能迁移。如果是随意移动的话,可以不看这个方法。
var moveType;//节点移动类型:prev/next/inner
function beforeDrag(treeId, treeNodes) {
//获取拖拽节点的根节点
dragNodeRoot = treeNodes[0];
for (var i = 0; i < treeNodes.length; i++) {
if (treeNodes[i].id.length < dragNodeRoot.id.length) {
dragNodeRoot = treeNodes[i];
}
}
//记录原来迁移节点的最上层节点的父节点/相邻节点,便于回撤迁移操作
dragNodeRootParent = dragNodeRoot.getParentNode();
dragNodeRootPrev = dragNodeRoot.getPreNode();
dragNodeRootNext = dragNodeRoot.getNextNode();
//如果是一级节点,不能迁移
if (如果是一级节点) {
return false;
}
//如果不是该节点的管理员,不能迁移
if (不是该节点的管理员) {
return false;
}
curDragNodes = treeNodes;
return true;
}
//迁移节点时,释放节点时的操作:同样进行了权限的处理
function zTreeBeforeDrop(treeId, treeNodes, targetNode, moveType, isCopy) {
//不允许迁移成一级节点
if(moveType != "inner" && targetNode是一级节点 ){
return false;
}
return true;
后代执行迁移的操作:
后台成功:
//刷新树上节点的id和pid信息
if(moveType == "inner"){
categoryTree.reAsyncChildNodes(targetNode, "refresh");
}else{
categoryTree.reAsyncChildNodes(targetNode.getParentNode(), "refresh");
}
后台失败:
moveNodeBack();
}
function moveNodeBack() {
if(dragNodeRootPrev != null){
categoryTree.moveNode(dragNodeRootPrev, dragNodeRoot, "next");
}else if(dragNodeRootNext != null){
categoryTree.moveNode(dragNodeRootNext, dragNodeRoot, "prev");
}else {
categoryTree.moveNode(dragNodeRootParent, dragNodeRoot, "inner");
}
}
右键其他菜单选项
function allcateAdmin() {
//.............这里是右键其他选项的功能
}
function allcateDownload() {
//.............这里是右键其他选项的功能
}
鼠标移入移出事件
这里实现的效果是:鼠标移动到某个节点上时,节点后面会出现一个图标,点击图标,实现something
//鼠标移入事件
var IDMark_A = "_a";// 拼装获取树节点a链接id
function addHoverDom(treeId, treeNode) {
if (treeNode.parentNode && treeNode.parentNode.id!=1) return;
var aObj = $("#" + treeNode.tId + IDMark_A);
var ico = document.getElementById(treeNode.tId+"_ico").style.backgroundImage;
if ($("#addBtn_"+treeNode.id).length>0) {
return;
}else{
var flag = (ico.indexOf("project")==-1);
if(flag){
var editStr = "<img width='12' height='12' style='margin-left:7px;' title='增加' src='${root.contextPath!}/resource/images/kd_add.png' id='addBtn_" +treeNode.id+ "' title='"+treeNode.name+"' onfocus='this.blur();'>";
aObj.append(editStr);
var addbtn = $("#addBtn_"+treeNode.id);
if (addbtn) {
addbtn.bind("click", function(event){
dosomething(event, treeNode);
});
}
}
}
}
//鼠标移出节点事件——开始
function removeHoverDom(treeId, treeNode) {
if (treeNode.parentNode && treeNode.parentNode.id!=1) return;
$("#addBtn_"+treeNode.id).unbind().remove();
}
function dosomething(event, treeNode) {
}
```
---
### 关于排序
排序其实就是节点的两种拖拽类型:拖到节点之后,拖到节点之前。主要是后台在处理的时候。
项目中的实现思路是:
(1)对每个节点数据,设置一个排序号字段;
(2)同一级的节点按排序号进行排序;
(3)拖拽节点之后,更新相关节点的排序号,以及对应的父子关系。
还有一点,在读取树信息的时候,将节点按节点排序号排序之后,再放在json里面。对同一级的节点来说,树的节点的显示顺序是按照json里的节点存放顺序排列的。
举个例子:下面两种json对象排列顺序,显示出来排列结果是一样的
```
[{ id:1, pId:0, name:"植物"},
{ id:2, pId:0, name:"动物"},
{ id:3, pId:2, name:"大象"},
{ id:4, pId:2, name:"鲨鱼"},
{ id:5, pId:1, name:"大白菜"},
{ id:6, pId:1, name:"西红柿"}];
[{ id:1, pId:0, name:"植物"},
{ id:3, pId:2, name:"大象"},
{ id:5, pId:1, name:"大白菜"},
{ id:4, pId:2, name:"鲨鱼"},
{ id:2, pId:0, name:"动物"},
{ id:6, pId:1, name:"西红柿"}];
两种写法的显示效果都是下面这样:
植物
大白菜
西红寺
动物
大象
鲨鱼
```
---
### 后台获取树信息的java代码
```
项目中采用的是SpringMVC框架,@ResponseBody注解是必须的,
将获取信息直接写到response中
ztree的配置如下:
url : "加载树数据的url",
autoParam: ["id"],//第一次提交会这个id是空,之后点击节点,如果该节点是父节点,并且还没有加载直接子级的信息,那么就会提交该节点的id。
@RequestMapping(value = "/categoryTreeInfo", method = RequestMethod.POST)
@ResponseBody
public void getCategoryTreeInfo(Integer id, HttpServletRequest request,HttpServletResponse response) {
try {
JSONArray jsonArr = new JSONArray();
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
// 获取目录信息
if (null == id) {//可以理解为根节点的id为1
id = 1;
}
//获取节点的直接子级节点的信息,这里要注意,对同一级目录而言,它的显示顺序跟放在json里的前后顺序是一致的,所以在这个方法getDirectChildCategoryById(id)的实现里,要按照自己想要的顺序,写sql的时候,order by 一下。
List<Category> categoryList = categoryServiceImpl.getDirectChildCategoryById(id);
// 将信息组成树需要的JSON对象
User user = super.getLoginUser();
//递归获取所有节点信息
getCategoryTreeJson(user, jsonArr, categoryList, id);
response.getWriter().print(jsonArr.toString());
// 也可以在获取到所有的json数据后,针对某一字段进行排序
//JSONArray jsonArray = sortJsonArray(jsonArr);
//logger.info("获取到的目录树信息共" + jsonArr.size() + "个:" + jsonArr.toString());
//return jsonArray.toString();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 功能说明:封装目录树
*
* @param rootURL
* @param jsonArr
* @param categoryList
* @param id
*
*/
private void getCategoryTreeJson(User user, JSONArray jsonArr, List<Category> categoryList, Integer id) {
for (int i = 0; i < categoryList.size(); i++) {
JSONObject json = new JSONObject();
json.put("id", categoryList.get(i).getId());
json.put("name", categoryList.get(i).getName());
//这里key关键字必须是pId,不能是pid或者其他,否则会出现open属性失效等问题
json.put("pId", categoryList.get(i).getParent_id());
json.put("isFirst", categoryList.get(i).getParent_id()==1 ? 1 :0);
json.put("order", categoryList.get(i).getSort_number());
//获取当前用户是否是该目录的管理员:目录迁移的时候用到这个参数
boolean isAdministrator = categoryServiceImpl.
isCategoryAdmin(categoryList.get(i).getId(), super.getLoginUser().getId());
if(isAdministrator){
json.put("administrator", 1);
}else {
json.put("administrator", 0);
}
// 判断是否为父级
List<Category> childOrgList = categoryServiceImpl.getDirectChildCategoryById(categoryList.get(i).getId());
if (childOrgList != null && childOrgList.size() > 0) {
json.put("isParent", true);
//json.put("open", true);
//递归,获取所有目录信息
getCategoryTreeJson(user, jsonArr, childOrgList, categoryList.get(i).getId());
}
jsonArr.add(json);
}
}
/**
* 功能说明:对JSONArray排序
* 对于ztree来说,只要同级节点的顺序排成你想要的效果就可以了,不同
*级的节点顺序不用处理
* @param jsonArr
* @return
*
*/
private JSONArray sortJsonArray(JSONArray jsonArr) {
JSONArray sortedJsonArray = new JSONArray();
List<JSONObject> jsonValues = new ArrayList<JSONObject>();
for (int i = 0; i < jsonArr.size(); i++) {
jsonValues.add(jsonArr.getJSONObject(i));
}
Collections.sort(jsonValues, new Comparator<JSONObject>() {
//这里,order是我json对象里的一个属性,我要按这个排序。也可以按照id、name等你想要的属性进行排序
private static final String KEY_NAME = "order";
@Override
public int compare(JSONObject a, JSONObject b) {
long valA = 0;
long valB = 0;
try {
valA = (Integer) a.get(KEY_NAME);
valB = (Integer) b.get(KEY_NAME);
} catch (Exception e) {
e.printStackTrace();
}
if (valB > valA) {
return -1;
} else {
return 1;
}
}
});
for (int i = 0; i < jsonArr.size(); i++) {
sortedJsonArray.add(jsonValues.get(i));
}
return sortedJsonArray;
}
```
### 还有一点
搜索的时候,对还没有加载的节点,是搜索不到的。
所以,如果要做搜索节点功能,就要在第一次加载树信息的时候,将所有节点全部获取。
但异步加载还是要配置成enable,因为在对树进行排序、删除、新增以及一些其他操作后,节点的信息很可能需要从后台获取才能完整的更新。
这时就要调用reAsyncChildNodes方法,而该方法只有在setting.async.enable = true 时有效。
---
### 最后一点
几乎所有对树节点的操作,都可以在ztree的api中找到相应的方法,
也可以在相应的demo中找到例子。