因项目近期特殊需要,自己实现了一个TreeGrid javascript组件。其中一些思想是从网络借鉴的,一些来自于开源框架easyui。现将其公布出来,兴许可以帮助一些朋友实现一些奇葩需求。 js组件中注释很详尽,代码结构也很简单。
该组件实现的功能:
1.展示树列表,左边是树,右边是表
2.提供勾选框,可以通过接口获取选中数据
3.每个子列表都有自己的分页器
4.支持本地数据展示,和远程数据延迟加载
5.每个值可以有自身的转换器formatter
6.支持行单击事件
7.支持拖拽改变列宽
8.加载时的loading效果
效果图:
js代码如下:
(function($){
//工具方法
function request(url,restricts){
if(restricts==""){
restricts = {};
}
if(typeof(restricts)!="object"){
console.error("requestParam should be Ojbect");
}
var jsonData = null;
$.ajax({
type:'POST',
data:restricts,
async:false,
url:url,
success:function(data,success){
if(success){
jsonData = eval('('+data+')');
}else{
console.error('fail to call '+url);
}
}
});
return jsonData;
}
function getParentId(id){
return id.substring(0,id.lastIndexOf('_'));
}
function getCurrentLevel(id){
var pid = getParentId(id);
var levelStr = $('#'+pid).attr('level');
if( levelStr==undefined ){
levelStr = 0;
}
return parseInt(levelStr)+1;
}
var methods = {
init:function(options){
var config = $.extend({}, this.TreeGrid.defaults, options);
return this.each(function() {
var $this = $(this);
$this.TreeGrid('createContainer', config)
.TreeGrid('drawHeader', config)
.TreeGrid('drawData', config)
.TreeGrid('bindEvent', config)
.TreeGrid('bindCheckboxEvent', config);
});
},
//创建容器
createContainer:function(config){
var $context = this;
$context.css({width:config.width,height:config.height});
//先清除工作(可能之前有残留)
$context.find('.TreeGrid-inner').remove();
$context.removeClass('TreeGrid');
//正式构造
$context.addClass('TreeGrid');
//创建遮罩容器
$context.append('<div class="TreeGrid-inner"></div>');
var $inner = $context.find('.TreeGrid-inner');
var id = config.id || "TreeGrid"+$.TreeGrid.COUNT++;
$inner.append("<table id='"+id+"' />");
var $table = $("#"+id);
$table.attr('cellspacing',0);
$table.attr('cellpadding',0);
//对每一个符合条件的jquery对象(this即选择的div),对其执行以下函数
return $context.each(function(){
});
},
//画表头
drawHeader:function(config){
var id = config.id+'H';
$(this).find("table").append("<tr id='"+id+"' />");
var $th = $('#'+id);
$th.addClass('header');
$th.attr('height',config.headerHeight);
$th.attr('level',0);
if(config.showCheckbox ){
//第一列要用来显示checkbox
$th.append("<td><input type='checkbox' trid='"+id+"' /></td>");
}
var cols = config.columns;
for(i=0;i<cols.length;i++){
var col = cols[i];
$th.append("<td />");
var $td = $th.find('td:last');
$td.attr('align',(col.headerAlign || config.headerAlign) );
$td.css('width',(col.width || "") );
$td.append(col.headerText || "");
}
return this.each(function(){
if(config.columnWidthResizable){
$(this).TreeGrid('resizeHeaderWidth',config,$th);
}
});
},
//改变列宽
resizeHeaderWidth:function(config,$th){
var $context = this;
var resizable = false;//当前位置是可以开始改变宽度的
var resizing = false;//表明正在拖动改变大小
var begin = 0;
var $resizeTarget = null;
//当鼠标滑动到边界,指针发生改变
$th.mousemove(function(e){
var $target = $(e.target);
var x = e.pageX;//鼠标位置的左边距
var offset = $target.offset();
var left = offset.left;//元素整体偏移量
var allWidth = left + $target.outerWidth();//td右侧边线的左边距
if(x>allWidth-config.cursorRange){
resizable = true;
$target.css('cursor','e-resize');
}else{
resizable = false;
$target.css("cursor", "default");
}
});
//触发
$th.mousedown(function(e){
if(resizable){
$resizeTarget = $(e.target);
var $inner = $context.find('.TreeGrid-inner');
$inner.append("<div class='table-resize-proxy'></div>");
var $proxy = $context.find('.table-resize-proxy');
$proxy.css({left : e.pageX-$context.offset().left+2,display : 'block'});
begin = e.pageX;
resizing = true;
}
});
//辅助线移动
$context.mousemove(function(e){
if(resizing){
var $proxy = $context.find('.table-resize-proxy');
$proxy.css({left : e.pageX-$context.offset().left,display : 'block'});
}
});
$context.mouseup(function(e){
if(resizing){
//设置新宽度
var changedWith = e.pageX-begin;
var newWidth = $resizeTarget.width()+changedWith;
$resizeTarget.width(newWidth);
//清理工作
$context.find('.table-resize-proxy').remove();
resizing = false;
$resizeTarget = null;
begin = 0;
}
});
},
//画数据
drawData : function(config){
var $context = this;
var rows = [];
//本地有数据用本地的,没有则远程获取
if(config.data){
rows = config.data;
}else if(config.requestUrl!=''){
rows = request(config.requestUrl,config.requestParam);
}else{
console.error('pls config the data source');
}
//表头即为整体的根
var rootId = config.id+'H';
//表头中存放了所有数据,为了保持规律:在所有的父节点中都能找到子节点的数据
$context.find('#'+rootId).data('data',{children:rows});
return $context.each(function(){
$(this).TreeGrid('drawDataRecursive', config,rootId,rows,config.displayLevel);
});
},
//递归将rows画在parentId下 displayLevel级别之前的都要显示
drawDataRecursive:function(config,parentId,rows,displayLevel){
var $context = this;
var prevTrId = parentId;
var count = rows.length;
if(config.pagination){
//只画前 pageNum 行
count = count<config.pageNum ? count:config.pageNum;
}
for(var i=0; i<count; i++){
var id = parentId + "_" + i;
var row = rows[i];
prevTrId = $context.TreeGrid('drawTableTr',config,id,row,prevTrId,'dataTr',displayLevel );
//递归画子树
if(row.children && row.children.length>0 ){
prevTrId = $context.TreeGrid('drawDataRecursive', config,id, row.children,displayLevel);
}
}
//分页器
if(config.pagination){
var id = parentId + "_" + count;
prevTrId = $context.TreeGrid('drawTableTr',config,id,row,prevTrId,'paginationTr',displayLevel );
}
return prevTrId;
},
//画tr prevTrId:该行的前一行id; displayLevel:该级之前的节点都要显示
drawTableTr:function(config,id,row,prevTrId,trCls,displayLevel){
var $context = this;
$context.find('#'+prevTrId).after("<tr id="+id+" />");
var $tr = $('#'+id);
var pid = getParentId(id);
var currentLevel = getCurrentLevel(id);
$tr.attr('level',currentLevel);
$tr.attr('pid',pid );
$tr.attr('rowIndex',config.rownum++);
$tr.data('data',row);
$tr.addClass(trCls);
var openStatus = "N";
var display = "none";
if(currentLevel<displayLevel) openStatus = "Y";//级别<displayLevel的节点,都为展开状态
if(currentLevel<=displayLevel) display = ""; //级别<=displayLevel的节点,都要显示
$tr.attr('openStatus',openStatus);
$tr.css('display',display);
if(trCls=='dataTr'){
$context.TreeGrid('drawDataTd',config,$tr);
}else if(trCls=='paginationTr'){
$context.TreeGrid('drawPaginationTd',config,$tr);
}
return id;
},
//画td
drawDataTd:function(config,$tr){
var treeColumnIndex = config.treeColumnIndex;
var columns = config.columns;
var row = $tr.data('data');
var trid = $tr.attr('id');
var currentLevel = $tr.attr('level');
var openStatus = $tr.attr('openStatus');
if(config.showCheckbox ){
//第一列要用来显示checkbox
$tr.append("<td><input type='checkbox' /></td>");
$tr.find("input[type='checkbox']").attr('trid',trid);
}
for(var j=0;j<columns.length;j++){
var col = columns[j];
$tr.append("<td />");
var $td = $tr.find('td:last');
$td.attr('align',(col.dataAlign || config.dataAlign) );
//层次缩进
if(j==treeColumnIndex){
$td.css('text-indent',parseInt(config.indentation)*(currentLevel-1) );
$td.append('<span />');
var $img = $td.find('span');
$img.attr('trid',trid);
//如果是延迟加载则只要求有children属性即可,否则要求children.length>0
if((config.delayLoad&&row.children) || (row.children&&row.children.length>0) ){
$img.addClass('folder');
var nodeClass = (openStatus=="Y")? "nodeOpen" : "nodeClose";
$img.addClass(nodeClass);
}else{
$img.addClass('image_nohand');
$img.addClass('nodeLeaf');
}
}
var displayData = row[col.dataField] || "";
//该字段有转换器
if(col.converter){
displayData = col.converter.call(this,displayData);
}
$td.append(displayData);
}
},
//画分页器
drawPaginationTd:function(config,$tr){
var level = parseInt( $tr.attr('level') );
$tr.append("<td />");
var $td = $tr.find('td:last');
$td.attr('align','right');
$td.css('padding-right',(level-1)*20);
//$td.css('border','none');
$td.addClass('pagination');
var colspan = config.columns.length;
if(config.showCheckbox){
colspan += 1;
}
$td.attr('colspan',colspan);
$td.append("<span class='first'></span>");
$td.append("<span class='prev'></span>");
$td.append("<span class='page'><input /></span>");
$td.append("/<span class='pageCount'></span>");
$td.append("<span class='next'></span>");
$td.append("<span class='last'></span>");
this.TreeGrid('bindPaginationEvent',config,$tr);
},
//用rows重画parentId下面的数据
reloadData:function(config,parentId,rows){
var $context = this;
var parentLevel = $context.find('#'+parentId).attr('level');
var currentLevel = parseInt(parentLevel)+1;
//删除所有子项
$context.TreeGrid('deleteDataRecursive',config,parentId);
//重画所有子项
$context.TreeGrid('drawDataRecursive', config,parentId,rows,currentLevel);
},
//递归删除parentId的所有子项
deleteDataRecursive:function(config,parentId){
var $context = this;
$context.TreeGrid('markDeleteTr',config,parentId);
$context.find('.deleteTr').remove();
},
//将所有需要删除的行标记出
markDeleteTr:function(config,parentId){
var $context = this;
var $trs = $context.find("tr[pid="+ parentId +"]");
for(var i=0; i<$trs.length;i++){
var $tr = $($trs[i]);
$tr.addClass('deleteTr');
$context.TreeGrid('markDeleteTr',config,$tr.attr('id'));
}
},
//子节点分页事件
bindPaginationEvent:function(config,$tr){
var $context = this;
var pid = $tr.attr('pid');
var $parent = $context.find('#'+pid)
var $first = $tr.find('span.first');
var $prev = $tr.find('span.prev');
var $page = $tr.find('span.page input');
var $pageCount = $tr.find('span.pageCount');
var $next = $tr.find('span.next');
var $last = $tr.find('span.last');
//从父节点上获取子节点的当前页
var currentPage = $parent.data('currentPage');
if(currentPage == undefined ){
currentPage = 1;
}
//从父节点上获取所有子节点
var allDatas = $parent.data('data').children;
var pageNum = config.pageNum;
var totalCount = allDatas.length;
var pageCount = Math.ceil( totalCount/pageNum );
$page.val(currentPage);
$pageCount.text(pageCount);
//给分页器加禁止的样式
if(currentPage<=1){
$first.addClass('disabled');
$prev.addClass('disabled');
}
if(currentPage>=pageCount){
$next.addClass('disabled');
$last.addClass('disabled');
}
$prev.click(function(){
if(currentPage<=1){
return;
}
//将子页面 页码存放于父节点
$parent.data('currentPage',currentPage-1);
$context.TreeGrid('reloadData',config,pid,getPageContent(currentPage-1) );
});
$next.click(function(){
if(currentPage>=pageCount){
return;
}
$parent.data('currentPage',currentPage+1);
$context.TreeGrid('reloadData',config,pid,getPageContent(currentPage+1) );
});
$first.click(function(){
if(currentPage<=1){
return;
}
$parent.data('currentPage',1);
$context.TreeGrid('reloadData',config,pid,getPageContent(1) );
});
$last.click(function(){
if(currentPage>=pageCount){
return;
}
$parent.data('currentPage',pageCount);
$context.TreeGrid('reloadData',config,pid,getPageContent(pageCount) );
});
$page.change(function(){
var index = $page.val();
var reg = new RegExp("^[0-9]*$");
if(!reg.test(index)){
$page.val(currentPage);
return;
}
index = parseInt(index);
if(index>pageCount){
index = pageCount;
}else if(index<1){
index = 1;
}
$parent.data('currentPage',index);
$context.TreeGrid('reloadData',config,pid,getPageContent(index) );
});
function getPageContent(index){
var rows = [];
var begin = (index-1<0?0:index-1)*pageNum;
var end = begin+pageNum<totalCount-1 ? begin+pageNum : totalCount-1;
for(var i=begin;i<=end;i++){
rows.push(allDatas[i]);
}
return rows;
}
},
bindEvent:function(config){
var $context = this;
//对数据行增加悬浮样式,因为数据行有可能是动态增加的,因而采用如下写法
if(config.showHoverCss){
$context.find("tr.dataTr").die().live({
mouseenter:function(){
if($(this).hasClass("header")) return;
$(this).addClass("row_hover");
},
mouseleave:function(){
$(this).removeClass("row_hover");
}
});
}
var timer = null;//区分单双击
//bind click to <tr>
$context.find("tr.dataTr").die().live("click", function(){
var $this = $(this);
$context.find("tr").removeClass("row_active");
$this.addClass("row_active");
var id = $this.attr('id');
var data = $this.data("data");
if(config.itemClick){
config.itemClick(id,data);
}
});
//bind click to image
$context.find("span.folder").die().live("click", function(){
var trid = $(this).attr("trid");
var $tr = $context.find("#" + trid);
var isOpen = $tr.attr("openStatus");
var statusAfterClick = (isOpen == "Y") ? "N" : "Y";//当前为打开状态则关闭
$tr.attr("openStatus", statusAfterClick);
if(statusAfterClick == "N"){ //隐藏子节点
$tr.find("span.folder").removeClass("nodeOpen").addClass("nodeClose");
$context.find("tr[id^=" + trid + "_]").css("display", "none");
}else{ //显示子节点
$tr.find("span.folder").removeClass("nodeClose").addClass("nodeOpen");
$context.TreeGrid("showNextLevelRecursive",config,trid);
}
//阻止事件冒泡
return false;
});
return this.each(function(){
});
},
//给checkbox绑定事件
bindCheckboxEvent:function(config){
if(!config.showCheckbox){
return;
}
var $context = this;
$context.on('click',"input[type='checkbox'][trid]",function(event){
//阻止事件冒泡
event.stopPropagation();
var $ck = $(this);
var checked = this.checked;
var trid = $ck.attr('trid');
checkRecursive(trid);
uncheckParentRecursive(trid);
//勾选或不勾选父节点,所有子节点跟着变化
function checkRecursive(pid){
var $trs = $context.find("tr[pid='"+pid+"']");
for( var i=0;i<$trs.length;i++ ){
$($trs[i]).find("input[type='checkbox'][trid]").attr('checked',checked);
checkRecursive($($trs[i]).attr('id'));
}
}
//取消勾选子节点父节点取消
function uncheckParentRecursive(id){
var $tr = $context.find('#'+id);
var pid = $tr.attr('pid');
if(!checked && pid!=undefined ){
var $ptr = $context.find('#'+pid);
$ptr.find("input[type='checkbox'][trid]").attr('checked',checked);
uncheckParentRecursive($ptr.attr('id'));
}
}
});
},
//递归显示数据
showNextLevelRecursive:function(config,parentId){
var $context = this;
var $parentTr = $context.find("#" + parentId);
var isOpen = $parentTr.attr("openStatus");
//只有当前行处于打开状态才可以显示下一行
if(isOpen == "Y"){
//找出所有trid的子行
var nextTrs = $context.find("tr[pid=" + parentId + "]");
if(nextTrs.length>0){
for(var i=0;i<nextTrs.length;i++){
var next = $(nextTrs[i]);
next.css("display", "");
this.TreeGrid("showNextLevelRecursive",config,next.attr('id'));
}
}else if(config.delayLoad && nextTrs.length==0){
//延迟加载且当前没有数据
var rows = [];
if(config.onDelayLoadData){
//显示loading样式
$context.TreeGrid('showLoading',config);
//调用用户自定义的延迟加载获取数据
rows = config.onDelayLoadData($parentTr.data("data"));
$context.find('.loading').remove();
if(!rows){
console.error('onDelayLoadData get nothing from remote');
return;
}
$parentTr.data('data').children = rows;
}
var currentLevel = parseInt($parentTr.attr('level'))+1;
$context.TreeGrid('drawDataRecursive', config,parentId,rows,currentLevel );
}
}
},
showLoading:function(config){
var $context = this;
$context.append('<div class="loading">loading...</div>');
var $loading = $context.find('.loading');
var containerWidth = $context.outerWidth();
var containerHeight = $context.outerHeight();
$loading.css('left',containerWidth/2-10);
$loading.css('top',containerHeight/2-5);
return this;
},
//获取勾选上的节点数据
getCheckedRows:function(){
var $context = this;
//注意排除表头
var $cks = $context.find("tr.dataTr input[type='checkbox'][trid]:checked");
var result = [];
for(var i=0;i<$cks.length;i++){
var trid = $($cks[i]).attr('trid');
result.push($('#'+trid).data());
}
return result;
}
};
$.fn.TreeGrid = function(method) {
if (methods[method]) {
return methods[ method ].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method with name ' + method + ' does not exists for jQuery.TreeGrid');
}
};
$.TreeGrid = {};
$.TreeGrid.COUNT = 1;
$.fn.TreeGrid.defaults = {
id:'T',
width:'100%',
headerAlign: 'center',
headerHeight: '25',
dataAlign: 'center',
indentation: '20',
displayLevel:1,//页面默认看到所有的一级节点
treeColumnIndex:0,//默认第0列是树
rownum:0,
showHoverCss:true,
itemClick: function(id,data){},
showCheckbox:false,
//remote props
requestUrl:"",
requestParam:"",
//delay load data
delayLoad:false,
onDelayLoadData:function(data){},
//pager
pagination:true,
pageNum:5,
//resize column width
columnWidthResizable:true,
cursorRange:3 //在td的右侧3像素内能识别到左右滑动鼠标
};
})(jQuery)
附件中带了有demo
文件位置
http://download.csdn.net/detail/williamxww1/9559555