前年的blog,用markdown重新编辑
源码及示例 下载位置
因项目近期特殊需要,自己实现了一个TreeGrid JavaScript组件。其中一些思想是从网络借鉴的,一些来自于开源框架easyui。现将其公布出来,兴许可以帮助一些朋友实现一些奇葩需求。 js组件中注释很详尽,代码结构也很简单。
功能
- 展示树列表,左边是树,右边是表
- 提供勾选框,可以通过接口获取选中数据
- 每个子列表都有自己的分页器
- 支持本地数据展示,和远程数据延迟加载
- 每个值可以有自身的转换器formatter
- 支持行单击事件
- 支持拖拽改变列宽
- 加载时的loading效果
效果图
TreeGrid-1.1.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);
//确保 $context.data('config');能取到config
this.data('config',config);
return this.each(function() {
var $this = $(this);
$this.TreeGrid('createContainer')
.TreeGrid('drawHeader')
.TreeGrid('drawData')
.TreeGrid('bindEvent')
.TreeGrid('bindCheckboxEvent');
});
},
getConfig:function(){
var $context = this;
return $context.data('config');
},
//创建容器
createContainer:function(){
var $context = this;
var config = $context.data('config');
$context.css({width:config.width,height:config.height});
//先清除工作(可能之前有残留)
$context.find('.TreeGrid-inner').remove();
$context.removeClass('TreeGrid');
//正式构造
$context.addClass('TreeGrid');
//创建内部容器,该容器无限宽(css中10000px),因而用户可以无休止拖动列
$context.append('<div class="TreeGrid-inner"></div>');
var $inner = $context.find('.TreeGrid-inner');
var id = config.id || "T"+$.TreeGrid.COUNT++;
$inner.append("<table id='"+id+"' cellspacing=0 cellpadding=0 />");
$inner.find('table').css('width',config.width);
//对每一个符合条件的jquery对象(this即选择的div),对其执行以下函数
return $context.each(function(){
});
},
//画表头
drawHeader:function(){
var $context = this;
var config = $context.data('config');
var $table = $context.find("table");
var headerId = $table.attr('id')+'H';
$table.append("<tr id='"+headerId+"' />");
var $tr = $table.find('#'+headerId);
$tr.addClass('header');
$tr.attr('height',config.headerHeight);
$tr.attr('level',0);
if(config.showCheckbox ){
//第一列要用来显示checkbox
$tr.append("<td width='20px' ><input type='checkbox' trid='"+headerId+"' /></td>");
}
var cols = config.columns;
for(i=0;i<cols.length;i++){
var col = cols[i];
$tr.append("<td />");
var $td = $tr.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',$tr);
}
});
},
//表头拖动改变列宽
resizeHeaderWidth:function($tr){
var $context = this;
var config = $context.data('config');
var resizable = false;//当前位置是可以开始改变宽度的
var resizing = false;//表明正在拖动改变大小
var begin = 0;
var $resizeTarget = null;
//当鼠标滑动到边界,指针发生改变
$tr.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");
}
});
//触发
$tr.mousedown(function(e){
if(resizable){
$context.addClass('noSelect');//拖动过程中不让选中
$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){
//拖动结束可以选中
$context.removeClass('noSelect');
//设置新宽度
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(){
var $context = this;
var config = $context.data('config');
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 headerId = $context.find('table tr.header').attr('id');
return $context.each(function(){
$(this).TreeGrid('drawDataRecursive', headerId,rows,config.displayLevel);
});
},
//递归将rows画在parentId下 displayLevel级别之前的都要显示
drawDataRecursive:function(parentId,rows,displayLevel){
var $context = this;
var config = $context.data('config');
//画行时,会紧接着行prevTrId画新tr
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',id,row,prevTrId,'dataTr',displayLevel );
//递归画子树
if(row.children && row.children.length>0 ){
prevTrId = $context.TreeGrid('drawDataRecursive', id, row.children,displayLevel);
}
}
//parentId节点对应的子节点还没有分页器且子节点数据>1页,才画分页器
var $parentTr = $('#'+parentId);
var paginationExists = $parentTr.data('paginationExists');
if(config.pagination && !paginationExists &&rows.length>config.pageNum ){
var id = parentId + "_" + count;
//分页器上保存着所有子节点数据
prevTrId = $context.TreeGrid('drawTableTr',id,rows,prevTrId,'paginationTr',displayLevel);
$parentTr.data('paginationExists',true);//表明该节点的children有分页器了
}
return prevTrId;
},
//画tr prevTrId:该行的前一行id; displayLevel:该级之前的节点都要显示
drawTableTr:function(id,row,prevTrId,trCls,displayLevel){
var $context = this;
var config = $context.data('config');
$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',$tr);
}else if(trCls=='paginationTr'){
$context.TreeGrid('drawPaginationTd',$tr);
}
return id;
},
//画td
drawDataTd:function($tr){
var $context = this;
var config = $context.data('config');
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
//根据父行来判断子行是否选中
var parentChecked = $context.TreeGrid('isChecked',$tr.attr('pid'));
$tr.append("<td><input type='checkbox' /></td>");
$tr.find("input[type='checkbox']").attr('trid',trid).attr('checked',parentChecked);
}
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.formatter){
displayData = col.formatter.call(this,displayData,row);
}
$td.append(displayData);
}
},
//画分页器
drawPaginationTd:function($tr){
var $context = this;
var config = $context.data('config');
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 value=1 /></span>");
$td.append("/<span class='pageCount'></span>");
$td.append("<span class='next'></span>");
$td.append("<span class='last'></span>");
this.TreeGrid('bindPaginationEvent',$tr);
},
//用rows重画parentId下面的数据
reloadData:function(parentId,rows){
var $context = this;
var config = $context.data('config');
var parentLevel = $context.find('#'+parentId).attr('level');
var displayLevel = parseInt(parentLevel)+1;
//删除数据子项
$context.TreeGrid('deleteDataRecursive',parentId);
//重画数据子项
$context.TreeGrid('drawDataRecursive',parentId,rows,displayLevel);
},
//递归删除parentId的所有子项
deleteDataRecursive:function(parentId){
var $context = this;
var config = $context.data('config');
//注意:parentId节点下的分页器是没有删除的
var dataTrs = $context.find("tr.dataTr[pid="+ parentId +"]");
for(var i=0;i<dataTrs.length;i++){
//将数据行及其子项标记为删除
$context.TreeGrid('markDeleteTr',$(dataTrs[i]).attr('id'));
}
//删除
$context.find('.deleteTr').remove();
},
//将id对应行及其子项(包括分页器)标记为删除
markDeleteTr:function(id){
var $context = this;
var config = $context.data('config');
var $tr = $context.find("#"+id);
$tr.addClass('deleteTr');
var $children = $context.find("tr[pid="+ id +"]");
for(var i=0; i<$children.length;i++){
var $child = $($children[i]);
$child.addClass('deleteTr');
$context.TreeGrid('markDeleteTr',$child.attr('id'));
}
},
//子节点分页事件
bindPaginationEvent:function($tr){
var $context = this;
var config = $context.data('config');
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 = parseInt($page.val());
var allDatas = $tr.data('data');
var pageNum = config.pageNum;
var totalCount = allDatas.length;
var pageCount = Math.ceil( totalCount/pageNum );
$pageCount.text(pageCount);
refreshPagerState(currentPage,pageCount);
$prev.click(function(){
if(currentPage<=1){
return;
}
gotoPage(currentPage-1);
});
$next.click(function(){
if(currentPage>=pageCount){
return;
}
gotoPage(currentPage+1);
});
$first.click(function(){
if(currentPage<=1){
return;
}
gotoPage(1);
});
$last.click(function(){
if(currentPage>=pageCount){
return;
}
gotoPage(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;
}
gotoPage(index);
});
function gotoPage(index){
currentPage = index;
$page.val(index);
$context.TreeGrid('reloadData',pid,getPageContent(index) );
refreshPagerState(index,pageCount);
}
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;
}
function refreshPagerState(currentPage,pageCount){
$first.removeClass('disabled');
$prev.removeClass('disabled');
$next.removeClass('disabled');
$last.removeClass('disabled');
//给分页器加禁止的样式
if(currentPage<=1){
$first.addClass('disabled');
$prev.addClass('disabled');
}
if(currentPage>=pageCount){
$next.addClass('disabled');
$last.addClass('disabled');
}
}
},
bindEvent:function(){
var $context = this;
var config = $context.data('config');
$context.die();//先清除该对象上的事件
//以下事件都是动态绑定的
if(config.showHoverCss){
$context.on('mouseover mouseout','tr.dataTr',function(event){
if(event.type=='mouseover'){
if($(this).hasClass("header")) return;
$(this).addClass("row_hover");
}else if(event.type=='mouseout'){
$(this).removeClass("row_hover");
}
});
}
//bind click to <tr>
$context.on('click','tr.dataTr', function(){
var $this = $(this);
$context.find("tr").removeClass("row_active");
$this.addClass("row_active");
var id = $this.attr('id');
var data = $this.data("data");
//点击行,自动check
if(config.autoChecked){
//全部取消
$context.find("input[type='checkbox'][trid]").attr('checked',false);
$context.TreeGrid('toggleCheckRecursive',id,true);
}
if(config.itemClick){
config.itemClick(id,data);
}
});
//bind click to image
$context.on("click","span.folder", 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",trid);
}
//阻止事件冒泡
return false;
});
return this.each(function(){
});
},
//给checkbox绑定事件
bindCheckboxEvent:function(){
var $context = this;
var config = $context.data('config');
if(!config.showCheckbox){
return;
}
$context.on('click',"input[type='checkbox'][trid]",function(event){
//阻止事件冒泡
event.stopPropagation();
var $ck = $(this);
var checked = this.checked;
var trid = $ck.attr('trid');
$context.TreeGrid('toggleCheckRecursive',trid,checked);
$context.TreeGrid('uncheckParentRecursive',trid,checked);
});
},
//勾选或不勾选父节点,所有子节点跟着变化
toggleCheckRecursive:function(id,status){
var $context = this;
var config = $context.data('config');
//改变当前行的状态
$context.find('#'+id).find("input[type='checkbox'][trid]").attr('checked',status);
//递归处理子行
var $trs = $context.find("tr[pid='"+id+"']");
for( var i=0;i<$trs.length;i++ ){
$context.TreeGrid('toggleCheckRecursive', $($trs[i]).attr('id'),status );
}
},
//取消勾选子节点,父节点取消
uncheckParentRecursive:function(id,status){
var $context = this;
var config = $context.data('config');
var $tr = $context.find('#'+id);
var pid = $tr.attr('pid');
if(!status && pid!=undefined ){
var $ptr = $context.find('#'+pid);
$ptr.find("input[type='checkbox'][trid]").attr('checked',status);
$context.TreeGrid('uncheckParentRecursive', $ptr.attr('id'), status );
}
},
//该行是否选中
isChecked:function(trid){
var $context = this;
var $tr = $context.find('#'+trid);
if($tr == undefined ){
return false;
}
return $tr.find("input[type='checkbox'][trid]").attr('checked');
},
//递归显示数据
showNextLevelRecursive:function(parentId){
var $context = this;
var config = $context.data('config');
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",next.attr('id'));
}
}else if(config.delayLoad && nextTrs.length==0){
//延迟加载且当前没有数据
var rows = [];
if(config.onDelayLoadData){
//显示loading样式
$context.TreeGrid('showLoading');
//调用用户自定义的延迟加载获取数据
rows = config.onDelayLoadData($parentTr.data("data"));
$context.find('.loading').remove();
if(!rows){
console.error('onDelayLoadData get nothing from remote');
return;
}
}
var displayLevel = parseInt($parentTr.attr('level'))+1;
$context.TreeGrid('drawDataRecursive',parentId,rows,displayLevel );
}
}
},
showLoading:function(){
var $context = this;
var config = $context.data('config');
$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;
},
//public 获取勾选上的节点数据
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('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 = {
width:'100%',
headerAlign: 'center',
headerHeight: '25',
dataAlign: 'center',
indentation: '20',
displayLevel:1,//页面默认看到所有的一级节点
treeColumnIndex:0,//默认第0列是树
rownum:0,
showHoverCss:false,
itemClick: function(id,data){},
showCheckbox:false,
autoChecked:true,//自动勾选点击行
//remote props
requestUrl:"",
requestParam:"",
//delay load data
delayLoad:false,
onDelayLoadData:function(data){},
//pager
pagination:true,
pageNum:5,
//resize column width
columnWidthResizable:true,
cursorRange:5 //在td的右侧3像素内能识别到左右滑动鼠标
};
})(jQuery)
源码及示例 下载位置