JavaScript Table行定位效果

本文介绍了如何使用JavaScript实现表格(Table)在滚动时表头始终保持可见的行定位效果。作者探讨了尝试过的不同方法,最终采用创建新table并克隆原始tr来实现定位。文章详细阐述了程序原理,包括克隆table、tr、设置样式、处理边框和背景色,以及处理元素定位和覆盖select等问题。此外,还提到了Chrome的一个bug。
摘要由CSDN通过智能技术生成

近来有客户要求用table显示一大串数据,由于滚动后就看不到表头,很不方便,所以想到这个效果。
上次做table排序对table有了一些了解,这次更是深入了解了一番,发现table原来是这么不简单。
还不清楚这个效果叫什么,有点像表头固定的效果,就叫行定位吧,本来想把列定位也做出来,但暂时还没这个需求,等以后有时间再弄吧。
淘宝的商品搜索页也看到类似的效果,但淘宝的不是table,而是li,而我这个是用在table上的。
要说明一下的是,我这个效果是用在一些普通的产品列表,当数据比较多时提高用户体验,而不是单单做数据显示,跟excel那样的方式是不同的。


效果预览

为方便预览,建议缩小浏览器。

<script> var isIE = (document.all) ? true : false; var isIE6 = isIE && (navigator.userAgent.indexOf('MSIE 6.0') != -1); var isIE7 = isIE && (navigator.userAgent.indexOf('MSIE 7.0') != -1); var isIE6or7 = isIE6 || isIE7; var isChrome = navigator.userAgent.indexOf('Chrome') != -1; var $$ = function (id) { return "string" == typeof id ? document.getElementById(id) : id; }; var Extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; } var CurrentStyle = function(element){ return element.currentStyle || document.defaultView.getComputedStyle(element, null); } var forEach = function(array, callback, thisObject){ if(array.forEach){ array.forEach(callback, thisObject); }else{ for (var i = 0, len = array.length; i < len; i++) { callback.call(thisObject, array[i], i, array); } } } var Filter = function(array, callback, thisObject){ if(array.filter){ return array.filter(callback, thisObject); }else{ var res = []; for (var i = 0, len = array.length; i < len; i++) { callback.call(thisObject, array[i], i, array) && res.push(array[i]); } return res; } } var Bind = function(object, fun) { var args = Array.prototype.slice.call(arguments).slice(2); return function() { return fun.apply(object, args.concat(Array.prototype.slice.call(arguments))); } } function addEventHandler(oTarget, sEventType, fnHandler) { if (oTarget.addEventListener) { oTarget.addEventListener(sEventType, fnHandler, false); } else if (oTarget.attachEvent) { oTarget.attachEvent("on" + sEventType, fnHandler); } else { oTarget["on" + sEventType] = fnHandler; } }; var TableFixed = function(table, options){ this._oTable = $$(table);//原table this._nTable = this._oTable.cloneNode(false);//新table this._nTable.id = "";//避免id冲突 this._oTableLeft = this._oTableTop = this._oTableBottom = 0;//记录原table坐标参数 this._oRowTop = this._oRowBottom = 0;//记录原tr坐标参数 this._viewHeight = this._oTableHeight = this._nTableHeight = 0;//记录高度 this._nTableViewTop = 0;//记录新table视框top this._selects = [];//select集合,用于ie6覆盖select this._style = this._nTable.style;//用于简化代码 //chrome的scroll用document.body this._doc = isChrome ? document.body : document.documentElement; //chrome透明用rgba(0, 0, 0, 0) this._transparent = isChrome ? "rgba(0, 0, 0, 0)" : "transparent"; this.SetOptions(options); this._index = this.options.Index; this._pos = this.options.Pos; this.Auto = !!this.options.Auto; this.Hide = !!this.options.Hide; addEventHandler(window, "resize", Bind(this, this.SetPos)); addEventHandler(window, "scroll", Bind(this, this.Run)); this._oTable.parentNode.insertBefore(this._nTable, this._oTable); this.Clone(); }; TableFixed.prototype = { //设置默认属性 SetOptions: function(options) { this.options = {//默认值 Index: 0,//tr索引 Auto: true,//是否自动定位 Pos: 0,//自定义定位位置百分比(0到1) Hide: false//是否隐藏(不显示) }; Extend(this.options, options || {}); }, //克隆表格 Clone: function(index) { //设置table样式 this._style.width = this._oTable.offsetWidth + "px"; this._style.position = isIE6 ? "absolute" : "fixed"; this._style.zIndex = 100; //设置index this._index = Math.max(0, Math.min(this._oTable.rows.length - 1, isNaN(index) ? this._index : index)); //克隆新行 this._oRow = this._oTable.rows[this._index]; var oT = this._oRow, nT = oT.cloneNode(true); if(oT.parentNode != this._oTable){ nT = oT.parentNode.cloneNode(false).appendChild(nT).parentNode; } //插入新行 if(this._nTable.firstChild){ this._nTable.replaceChild(nT, this._nTable.firstChild); }else{ this._nTable.appendChild(nT); } //去掉table上面和下面的边框 if(this._oTable.border > 0){ switch (this._oTable.frame) { case "above" : case "below" : case "hsides" : this._nTable.frame = "void"; break; case "" : case "border" : case "box" : this._nTable.frame = "vsides"; break; } } this._style.borderTopWidth = this._style.borderBottomWidth = 0; //设置td样式 var nTds = this._nTable.rows[0].cells; forEach(this._oRow.cells, Bind(this, function(o, i){ var css = CurrentStyle(o), style = nTds[i].style; //设置td背景 style.backgroundColor = this.GetBgColor(o, css.backgroundColor); //设置td的width,没考虑ie8/chrome设scroll的情况 style.width = (document.defaultView ? parseFloat(css.width) : (o.clientWidth - parseInt(css.paddingLeft) - parseInt(css.paddingRight))) + "px"; })); //获取table高度 this._oTableHeight = this._oTable.offsetHeight; this._nTableHeight = this._nTable.offsetHeight; this.SetRect(); this.SetPos(); }, //获取背景色 GetBgColor: function(node, bgc) { //不要透明背景(没考虑图片背景) while (bgc == this._transparent && (node = node.parentNode) != document) { bgc = CurrentStyle(node).backgroundColor; } return bgc == this._transparent ? "#fff" : bgc; }, //设置坐标属性 SetRect: function() { if(this._oTable.getBoundingClientRect){ //用getBoundingClientRect获取原table位置 var top = this._doc.scrollTop, rect = this._oTable.getBoundingClientRect(); this._oTableLeft = rect.left + this._doc.scrollLeft; this._oTableTop = rect.top + top; this._oTableBottom = rect.bottom + top; //获取原tr位置 rect = this._oRow.getBoundingClientRect(); this._oRowTop = rect.top + top; this._oRowBottom = rect.bottom + top; }else{//chrome不支持getBoundingClientRect //获取原table位置 var o = this._oTable, iLeft = o.offsetLeft, iTop = o.offsetTop; while (o.offsetParent) { o = o.offsetParent; iLeft += o.offsetLeft; iTop += o.offsetTop; } this._oTableLeft = iLeft; this._oTableTop = iTop; this._oTableBottom = iTop + this._oTableHeight; //获取原tr位置 o = this._oRow; iTop = o.offsetTop; while (o.offsetParent) { o = o.offsetParent; iTop += o.offsetTop; } this._oRowTop = iTop; this._oRowBottom = iTop + this._oRow.offsetHeight; } }, //设置新table位置属性 SetPos: function(pos) { //设置pos this._pos = Math.max(0, Math.min(1, isNaN(pos) ? this._pos : pos)); //获取位置 this._viewHeight = document.documentElement.clientHeight; this._nTableViewTop = (this._viewHeight - this._nTableHeight) * this._pos; this.Run(); }, //运行 Run: function() { if(!this.Hide){ var top = this._doc.scrollTop, left = this._doc.scrollLeft //原tr是否超过顶部和底部 ,outViewTop = this._oRowTop < top, outViewBottom = this._oRowBottom > top + this._viewHeight; //原tr超过视窗范围 if(outViewTop || outViewBottom){ var viewTop = !this.Auto ? this._nTableViewTop : (outViewTop ? 0 : (this._viewHeight - this._nTableHeight))//视窗top ,posTop = viewTop + top;//位置top //在原table范围内 if(posTop > this._oTableTop && posTop + this._nTableHeight < this._oTableBottom){ //定位 if(isIE6){ this._style.top = posTop + "px"; this._style.left = this._oTableLeft + "px"; setTimeout(Bind(this, this.SetSelect), 0);//iebug }else{ this._style.top = viewTop + "px"; this._style.left = this._oTableLeft - left + "px"; } return; } } } //隐藏 this._style.top = "-99999px"; isIE6 && this.ResetSelect(); }, //设置select集合 SetSelect: function() { this.ResetSelect(); var rect = this._nTable.getBoundingClientRect(); //把需要隐藏的放到_selects集合 this._selects = Filter(this._oTable.getElementsByTagName("select"), Bind(this, function(o){ var r = o.getBoundingClientRect(); if(r.top <= rect.bottom && r.bottom >= rect.top){ o._count ? o._count++ : (o._count = 1);//防止多个实例冲突 //设置隐藏 var visi = o.style.visibility; if(visi != "hidden"){ o._css = visi; o.style.visibility = "hidden"; } return true; } })) }, //恢复select样式 ResetSelect: function() { forEach(this._selects, function(o){ !--o._count && (o.style.visibility = o._css); }); this._selects = []; } }; </script>
表头
图片滑动切换效果
图片变换效果(ie only)
图片切割效果
仿LightBox内容显示效果
图片滑动展示效果
仿163网盘无刷新文件上传系统
拖放效果
图片切割系统
自定义多级联动浮动菜单
滑动条效果
拖拉缩放效果
渐变效果
Table排序
Tween算法及缓动效果
颜色梯度和渐变效果
表尾

点击行选择克隆行:当前克隆第 1

<script> var tf = new TableFixed("idTableFixed"); forEach($$("idTableFixed").rows, function(o, i){ var n = i + 1; o.cells[0].innerHTML = n; o.cells[2].innerHTML = n % 4 ? " " : " "; o.onclick = function(){ $$("idIndex").innerHTML = n; tf.Auto = true; tf.Clone(i); } }); tf.Clone();//表格结构修改后应重新Clone一次 $$("idPox").onclick = function(){ tf.Auto = false; tf.SetPos(.5); } $$("idHide").onclick = function(){ if(tf.Hide){ tf.Hide = false; this.value = "取消定位"; }else{ tf.Hide = true; this.value = "显示定位"; } tf.Run(); } </script>

注意,使用ie8的兼容性视图会有偏移。


程序原理

一开始的需求只是表头部分在滚动时能一直固定在头部,那关键要实现的就是让tr能定位。
首先想到的方法是给tr设置relative,用ie6/7测试以下代码:

Code


给tr设置relative后就能相对table定位了,看来很简单啊,但问题是这个方法ie8和ff都无效,而且存在很多问题,所以很快就被抛弃了。
ps:该效果用来做tr的拖动会很方便。

接着想到的是给table插入一个新tr,克隆原来的tr,并设置这个tr为fixed(ie6为absolute),例如:

Code


第一个问题是fixed的tr在ie7中不能进行定位,而且td在定位后并不能保持在表格中的布局,这样在原表格插tr就没意义了。
ps:fixed的相关应用可参考仿LightBox效果

最后我用的方法是新建一个table,并把源tr克隆到新table中,然后通过对新table定位来实现效果。
用这个方法关键有两点,首先要做一个仿真度尽可能高的tr,还有是要准确的定位,这些请看后面的程序说明。


程序说明

【克隆table】

克隆一个元素用cloneNode就可以了,它有一个bool参数,表示克隆是否包含子节点。
程序第一步就是克隆原table:

this ._oTable  =  $(table); // 源table
this ._nTable  =   this ._oTable.cloneNode( false ); // 新table
this ._nTable.id  =   "" ; // 避免id冲突


要注意虽然ie的cloneNode参数是可选的(默认是false),但在ff是必须的,建议使用时都写上参数。
还要注意的是id属性也会被克隆,也就是克隆后会有两个相同id的元素(如果克隆对象有设置的话),这很容易会导致其他问题,程序会把克隆table的id属性设空。
ps:table请用class来绑定样式,用id的话新table就获取不了样式了。

克隆之后再设置样式:

this ._style.width  =   this ._oTable.offsetWidth  +   " px " ;
this ._style.position  =  isIE6  ?   " absolute "  :  " fixed " ;
this ._style.zIndex  =   100 ;


一般来说offsetWidth是width+padding+border的结果,但table比较特别,测试下面的代码:

Code


只要给table设置width(style或本身的width属性),不管设置padding和border是多少,offsetWidth都等于width的值。
经测量offsetWidth是没错的,那就是说是table的width设置的问题。
w3c的table部分中说width属性是the desired width of the entire table,我估计entire就是包含了padding和border,找不到什么其他说明,先这么理解吧。
定位方面,除了不支持fixed的ie6用absolute,其他都使用fixed定位。


【克隆tr】

table有一个rows集合,包括了table的所有tr(包括thead和tfoot里面的)。
程序的Clone方法会根据其参数克隆对应索引的tr:

this ._index  =  Math.max( 0 , Math.min( this ._oTable.rows.length  -   1 , isNaN(index)  ?   this ._index : index));
this ._oRow  =   this ._oTable.rows[ this ._index];
var  oT  =   this ._oRow, nT  =  oT.cloneNode( true );


由于tr可能是包含在thead这些中,所以还要判断一下:

if (oT.parentNode  !=   this ._oTable){
    nT 
=  oT.parentNode.cloneNode( false ).appendChild(nT).parentNode;
}


然后再插入到新table中:

if ( this ._nTable.firstChild){
    
this ._nTable.replaceChild(nT,  this ._nTable.firstChild);
}
else {
    
this ._nTable.appendChild(nT);
}


因为程序允许修改克隆的tr,所以会判断有没有插入过,没有就直接appendChild,否则用replaceChild替换原来的tr。

 
【table的border和frame属性】

table的border属性用来指定边框宽度,table特有的frame属性是用来设置或获取表格周围的边框显示的方式。
w3c的tabel的frame部分说明frame可以是以下值:
void: No sides. This is the default value.
above: The top side only.
below: The bottom side only.
hsides: The top and bottom sides only.
vsides: The right and left sides only.
lhs: The left-hand side only.
rhs: The right-hand side only.
box: All four sides.
border: All four sides.
这些值指明了要显示的边框。要留意的是虽然说void是默认值,但不设置的话其实是一个空值,这时四条边框都会显示。
还有frame对style设置的border没有效果,测试下面代码:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值