因项目需求,编写了一个TreeGrid插件,上线后用户反馈了许多体验不好的地方,做此次修改,且代码结构也做了较大的变动。
改进点:
1. 拖动改变列宽时,不会导致文本被选中,使其感觉更顺畅
2. td内容不折行,宽度不够时用 ... 显示
3. 某节点的子节点一页能显示完,则不显示分页器。
4. 增加配置项autoChecked:true, 当单击某行时,默认勾选中本行。
效果图:
源码和demo下载位置
http://download.csdn.net/detail/williamxww1/9568974
下面是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)