原生javascript 固定表头原理与源码
我在工作中需要固定表头这个功能,我不想去找,没意思。于是就写了一个,我写的是angularjs 自定义指令 起了个 "fix-header" ,有人叫 “freeze-header” ,算了,看心情吧,最近心情不太好就不改了~~~
想了想,我还是改成原生吧,angularjs就是个毛毛~~~。
先讲一下思路:
1.想一想,再想一想,肯定用定位了,具体是绝对定位还是固定定位,看实际情况;
2.clone 一份thead元素,用于再创建一个定位的表头;
3.clone有点坑,不能clone当前元素的 实际 宽高 和 事件, 只能获取原先的加上;
4.加scroll事件;
5.我很开心,成功了~~~~;
先把页面创建了 ,就叫fixHeaderDemo.html,如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <style> 7 *{box-sizing: border-box;} 8 .table{max-width: 100%; width: 100%;border-collapse: collapse;} 9 .table>thead>tr>th{background-color: #059ca1; color:#FFF; padding-top:10px;padding-bottom: 10px;} 10 .table>thead>tr>th,.table>tbody>tr>td{ 11 border:1px solid #CCC; 12 } 13 </style> 14 </head> 15 <body> 16 <div style="width: 80%; margin:40px auto; height: 100px;overflow: auto;position: relative;"> 17 <table class="table"> 18 <thead> 19 <tr> 20 <th>Name1</th> 21 <th>Name2</th> 22 <th>Name3</th> 23 </tr> 24 </thead> 25 <tbody> 26 <tr> 27 <td>亚瑟</td> 28 <td>荆轲</td> 29 <td>程咬金</td> 30 </tr><tr> 31 <td>亚瑟</td> 32 <td>荆轲</td> 33 <td>程咬金</td> 34 </tr><tr> 35 <td>亚瑟</td> 36 <td>荆轲</td> 37 <td>程咬金</td> 38 </tr><tr> 39 <td>亚瑟</td> 40 <td>荆轲</td> 41 <td>程咬金</td> 42 </tr><tr> 43 <td>亚瑟</td> 44 <td>荆轲</td> 45 <td>程咬金</td> 46 </tr><tr> 47 <td>亚瑟</td> 48 <td>荆轲</td> 49 <td>程咬金</td> 50 </tr><tr> 51 <td>亚瑟</td> 52 <td>荆轲</td> 53 <td>程咬金</td> 54 </tr><tr> 55 <td>亚瑟</td> 56 <td>荆轲</td> 57 <td>程咬金</td> 58 </tr><tr> 59 <td>亚瑟</td> 60 <td>荆轲</td> 61 <td>程咬金</td> 62 </tr><tr> 63 <td>亚瑟</td> 64 <td>荆轲</td> 65 <td>程咬金</td> 66 </tr><tr> 67 <td>亚瑟</td> 68 <td>荆轲</td> 69 <td>程咬金</td> 70 </tr><tr> 71 <td>亚瑟</td> 72 <td>荆轲</td> 73 <td>程咬金</td> 74 </tr><tr> 75 <td>亚瑟</td> 76 <td>荆轲</td> 77 <td>程咬金</td> 78 </tr> 79 </tbody> 80 </table> 81 </div> 82 </body> 83 </html>
上面的都太小儿科了,js才是关键。
其实真的很简单,有兴趣还能在优化:
第一步,先来个构造函数,这个构造函数接收一个参数,也就是你要固定的那个表格,代码如下:
1 function FixHeader(tableElement){ 2 this.tableElement=tableElement; 3 }
第二步,写FixHeader构造函数的方法:
1 FixHeader.prototype={ 2 constructor:FixHeader, 3 init:function(){ 4 //这个位置是初始化的位置 5 } 6 };
第三步,其实我们的滚动是表格外面有个div父级元素,设置了他的最大高度,当超过这个最大高度就显示滚动条。那么,我们在初始化函数中肯定先获取div和表头元素等一些初始的事情;
1 init:function(){ 2 //获取表格的父级元素 3 this.tableParent=this.tableElement.parentNode; 4 //获取thead元素 5 this.header=this.tableElement.querySelector('thead'); 6 //克隆thead元素 7 this.cloneHeader=this.header.cloneNode(true); 8 }
第四步,我们要用克隆的数据,往表格中插入一个固定的表头,可能会问为什么要clone一下呢?因为如果直接操作原来的表头数据会直接影响,我们不是要去动原来的东西,那些东西已经完美了;如果你有兴趣可以
尝试一下,你会收获很大。在FixHeader原型中加了一个cloneFixHeader函数;
1 cloneFixHeader:function(){ 2 this.cloneHeader.className='cloneThead'; 3 this.cloneHeader.style.position='absolute'; 4 this.cloneHeader.style.top=0; 5 this.cloneHeader.style.left=0; 6 this.cloneHeader.style.right='-1px'; 7 this.tableElement.appendChild(this.cloneHeader); 8 }
上面的代码大家肯定明白,就是给clone的元素加一些样式和属性,加一个className是为了标识它是克隆的。那先看看效果,在初始化函数中调用cloneFixHeader()函数;
运行的截图如下:
我的天哪,这是怎么回事,怎么这个熊样了。哈哈。。。上面我已经说了clone不能把原来的宽高和事件一起克隆了,有些人是在css中把每个表格的宽度都写死,这是多么不好的做法,每次加或者减一列,都要修改css中的宽度。那我们是怎么解决的呢?太简单了,把每个元素的宽高设置到clone的元素中不就可以了吗!
1 cloneFixHeader:function(){ 2 var cloneThs=this.cloneHeader.children[0].children, 3 ths=this.header.children[0].children, 4 th,cloneTh,i=0,l=cloneThs.length; 5 for(;i<l;i++){ 6 th=ths[i];cloneTh=cloneThs[i]; 7 cloneTh.style.width=th.offsetWidth+'px'; 8 cloneTh.style.height=th.offsetHeight+'px'; 9 } 10 this.cloneHeader.className='cloneThead'; 11 this.cloneHeader.style.position='absolute'; 12 this.cloneHeader.style.top=0; 13 this.cloneHeader.style.left=0; 14 this.cloneHeader.style.right='-1px'; 15 this.tableElement.appendChild(this.cloneHeader); 16 }
运行的结果如下(太完美了~~~):
第五步,应该监听滚动事件了。先在原型中加一个函数叫listenerScroll ,代码如下:
1 listenerScroll:function(ev){ 2 var top=ev.target.scrollTop, 3 //用于判断是否已经添加上了,添加了就不让再次添加 4 cloneThead=ev.target.querySelector('.cloneThead'); 5 if(top>0){ 6 if(cloneThead){ 7 cloneThead.style.display='block'; 8 cloneThead.style.top=top+'px'; 9 return; 10 } 11 this.cloneFixHeader(); 12 }else{ 13 if(cloneThead){ 14 cloneThead.style.display='none'; 15 } 16 } 17 },
把在init中调用的cloneFixHeader() 函数换成监听事件:
1 init:function(){ 2 this.tableParent=this.tableElement.parentNode; 3 this.header=this.tableElement.querySelector('thead'); 4 this.cloneHeader=this.header.cloneNode(true); 5 this.tableParent.addEventListener('scroll',this.listenerScroll.bind(this),false); 6 },
上面看似完美了,但是当你改变浏览器窗口大小时,你会惊讶于我是多么认真与细心,是的,当窗口变化时,一切都不完美了,原因你应该知道的呀!
截图如下:
亲,你想到了方法吗?是的,就是监听窗口大小变化,好了再加一个listenerResize函数:
1 listenerResize:function(){ 2 var that=this; 3 if(that.timer){ 4 clearTimeout(that.timer); 5 } 6 that.timer=setTimeout(function(){ 7 var top=that.tableParent.scrollTop; 8 if(top<=0){ 9 return; 10 } 11 var globalWidth=that.global.innerWidth; 12 if(that.globalWidth&&that.globalWidth==globalWidth){ 13 return; 14 } 15 that.globalWidth=globalWidth; 16 var cloneThead=that.tableElement.querySelector('.cloneThead'), 17 theads=that.tableElement.querySelectorAll('thead'),i,l=theads.length; 18 for(i=0;i<l;i++){ 19 if(theads[i].className!='cloneThead'){ 20 that.header=theads[i]; 21 break; 22 } 23 } 24 if(cloneThead){ 25 var cloneThs=cloneThead.children[0].children, 26 ths=that.header.children[0].children, 27 th,cloneTh; 28 l=cloneThs.length; 29 for(i=0;i<l;i++){ 30 th=ths[i];cloneTh=cloneThs[i]; 31 cloneTh.style.width=th.offsetWidth+'px'; 32 cloneTh.style.height=th.offsetHeight+'px'; 33 } 34 return; 35 } 36 that.cloneFixHeader(); 37 },60); 38 },
最后全部js代码如下:
1 function FixHeader(tableElement, global) { 2 this.tableElement = tableElement; 3 this.global = global; 4 this.timer = null; 5 } 6 FixHeader.prototype = { 7 constructor: FixHeader, 8 init: function () { 9 this.tableParent = this.tableElement.parentNode; 10 this.header = this.tableElement.querySelector('thead'); 11 this.cloneHeader = this.header.cloneNode(true); 12 this.tableParent.addEventListener('scroll', this.listenerScroll.bind(this), false); 13 this.global.addEventListener('resize', this.listenerResize.bind(this), false); 14 }, 15 listenerScroll: function (ev) { 16 var top = ev.target.scrollTop, 17 //用于判断是否已经添加上了,添加了就不让再次添加 18 cloneThead = ev.target.querySelector('.cloneThead'); 19 if (top > 0) { 20 if (cloneThead) { 21 cloneThead.style.display = 'block'; 22 cloneThead.style.top = top + 'px'; 23 return; 24 } 25 this.cloneFixHeader(); 26 } else { 27 if (cloneThead) { 28 cloneThead.style.display = 'none'; 29 } 30 } 31 }, 32 listenerResize: function () { 33 var that = this; 34 if (that.timer) { 35 clearTimeout(that.timer); 36 } 37 that.timer = setTimeout(function () { 38 var top = that.tableParent.scrollTop; 39 if (top <= 0) { 40 return; 41 } 42 var globalWidth = that.global.innerWidth; 43 if (that.globalWidth && that.globalWidth == globalWidth) { 44 return; 45 } 46 that.globalWidth = globalWidth; 47 var cloneThead = that.tableElement.querySelector('.cloneThead'), 48 theads = that.tableElement.querySelectorAll('thead'), i, l = theads.length; 49 for (i = 0; i < l; i++) { 50 if (theads[i].className != 'cloneThead') { 51 that.header = theads[i]; 52 break; 53 } 54 } 55 if (cloneThead) { 56 var cloneThs = cloneThead.children[0].children, 57 ths = that.header.children[0].children, 58 th, cloneTh; 59 l = cloneThs.length; 60 for (i = 0; i < l; i++) { 61 th = ths[i]; 62 cloneTh = cloneThs[i]; 63 cloneTh.style.width = th.offsetWidth + 'px'; 64 cloneTh.style.height = th.offsetHeight + 'px'; 65 } 66 return; 67 } 68 that.cloneFixHeader(); 69 }, 60); 70 }, 71 cloneFixHeader: function () { 72 var cloneThs = this.cloneHeader.children[0].children, 73 ths = this.header.children[0].children, 74 th, cloneTh, i = 0, l = cloneThs.length; 75 for (; i < l; i++) { 76 th = ths[i]; 77 cloneTh = cloneThs[i]; 78 cloneTh.style.width = th.offsetWidth + 'px'; 79 cloneTh.style.height = th.offsetHeight + 'px'; 80 } 81 this.cloneHeader.className = 'cloneThead'; 82 this.cloneHeader.style.position = 'absolute'; 83 this.cloneHeader.style.top = 0; 84 this.cloneHeader.style.left = 0; 85 this.cloneHeader.style.right = '-1px'; 86 this.tableElement.appendChild(this.cloneHeader); 87 } 88 };
调用方式如下:
1 new FixHeader(document.querySelector('.table'), window).init();
总结:
表头固定可以用了,不过由于我知识有限,可能上面有错误的地方,请大家批评指出。