资讯专栏INFORMATION COLUMN

可拖动table表头的实现

paulli3 / 3021人阅读

摘要:前言自己做的项目碰到这样一个需求,就是对所有的表格添加表头可以拖动的效果。需要说明的是,表头固定的那种是需要用两个去实现,做过的人应该也都明白。拜拜后续补充更改了宽度改变的方式,应该是只改变拖动列后面的列的宽度。

前言

自己做的项目碰到这样一个需求,就是对所有的表格添加表头可以拖动的效果。我一想,这不简单,分分钟钟给你做出来。拿起我的电脑,啪啪啪就敲起来了。

一定是哪里不对,以我的聪明才智,结果应该不是这样的,然后净下心来,好好理了下思路后,总算是做出来了。
至于结果嘛,我肯定是做出来的,像下面这种:

准备

首先要说明的是,我们的项目使用的表格大概只分为两类,一类是表头不固定,就是普通的表格,另一类是表头固定,tbody部分是可以滚动的。需要说明的是,表头固定的那种是需要用两个table去实现,做过的人应该也都明白。前者看起来比较简单,因为宽度是受thead里的th影响的,后者看起来就不好处理,因为你用两个table就会出现下面的情况:

emmm,这和我们想象的应该不一样,这可咋整,感觉处理起来很麻烦啊。想起看过element-ui中的表格,似乎有拖动表头的实现,先打开控制台看下结构吧:

呃,话说长这么大我都没用过这两个标签,但仔细观察上面有个width,看到这大概也知道是怎么回事了,打开MDN看下相关属性的描述,和想的一样,width能控制当前列的宽度。
宽度的控制我们是解决了,还有一个问题,就是拖动后,其他列的宽度改怎么改变,如下:

a b c d

如果我拖动a列,改变的宽度应该怎样分配到b,c,d上,我这里是这样处理的,b、c、d有个属性去表示该列是否已经被拖动过了,如果b、c、d都没拖动过,那么把a改变的宽度平分到b、c、d三列的宽度上,如果b、c、d都改变了话,那么只改变最后一列d的宽度。好了,思路已经有了,我们可以去实现了。
事实证明,如果按照上面的设计就太蠢了,已经改成只改变拖动列后面的列且这些列没有改变过宽度。

实现

首先html结构大概是这样的:

a b
1 2

js方面

  constructor (id, options) {
    this._el = document.querySelector(`#${id}`);
    // 实际使用中需要对dom结构进行判断,这里就不做了
    this._tables = Array.from(this._el.querySelectorAll("table"));
    setTimeout(() => this._resolveDom());

    this.store = {
      dragging: false,                 //是否拖动
      draggingColumn: null,            //拖动的对象
      miniWidth: 30,                   //拖动的最小宽度
      startMouseLeft: undefined,       //鼠标点击时的clientX
      startLeft: undefined,            //th右离table的距离
      startColumnLeft: undefined,      //th左离table的距离
      tableLeft: undefined,            //table离页面左边的距离,
      HColumns: [],
      BColumns: [],
    };
  };

添加dom:

const [ THeader ] = this._tables;
let TBody;
const Tr = THeader.tHead.rows[0];
const columns = Array.from(Tr.cells);
const Bcolgroup = document.createElement("colgroup");
const cols = columns.map((item, index) => {
  const col = document.createElement("col");
  item.dataset.index = index;
  col.width = +item.offsetWidth;
  return col;
});
cols.reduce((newDom, item) => {
  newDom.appendChild(item);
  return newDom;
}, Bcolgroup);
const HColgroup = Bcolgroup.cloneNode(true);
THeader.appendChild(HColgroup);

//不管是一个table还是两个,都把header和body提出来
if (this._tables.length === 1) {
  const [ , tbody ] = Array.from(THeader.children);
  tbody.remove();
  TBody = THeader.cloneNode();
  TBody.appendChild(Bcolgroup);
  TBody.appendChild(tbody);
  this._el.appendChild(TBody);
} else {
  [ , TBody ] = this._tables;
  TBody.appendChild(Bcolgroup);
}

//拖动时的占位线
const hold = document.createElement("div");
hold.classList.add("resizable-hold");
this._el.appendChild(hold);

上面这块就是添加节点的,对dom进行处理,为了复用,这里我们不管你是表头固定还是表头不固定,我们都拆分为两个table,这样处理起来也方便的多。
然后就是处理手指移到列右侧cursor的值设为col-resize:

handleMouseMove(evt) {
  //...
  if (!this.store.dragging) {
    const rect = target.getBoundingClientRect();
    const bodyStyle = document.body.style;
    if (rect.width > 12 && rect.right - event.pageX < 8) {
      bodyStyle.cursor = "col-resize";
      target.style.cursor = "col-resize";
      this.store.draggingColumn = target;
    } else {
      bodyStyle.cursor = "";
      target.style.cursor = "pointer";
      this.store.draggingColumn = null;
    }
  }
};

需要注意的是,getBoundingClientRect()获取的rigth是元素右侧距离页面左边缘的距离,不是离页面右边缘的距离。这里就是给theadtr添加mousemove事件,当鼠标指针距离右边缘小于8的时候,改变指针形状,然后改变store里的状态,表示此时点击是可以拖动的了。
然后就是mousedown+mousemove+mouseup来处理拖动了:

const handleMouseDown = (evt) => {
  if (this.store.draggingColumn) {
    this.store.dragging = true;

    let { target } = evt;
    if (!target) return;

    const tableEle = THeader;
    const tableLeft = tableEle.getBoundingClientRect().left;
    const columnRect = target.getBoundingClientRect();
    const minLeft = columnRect.left - tableLeft + 30;
    target.classList.add("noclick");

    this.store.startMouseLeft = evt.clientX;
    this.store.startLeft = columnRect.right - tableLeft;
    this.store.startColumnLeft = columnRect.left - tableLeft;
    this.store.tableLeft = tableLeft;

    document.onselectstart = () => false;
    document.ondragstart = () => false;

    hold.style.display = "block";
    hold.style.left = this.store.startLeft + "px";

    const handleOnMouseMove = (event) => {
      const deltaLeft = event.clientX - this.store.startMouseLeft;
      const proxyLeft = this.store.startLeft + deltaLeft;

      hold.style.left = Math.max(minLeft, proxyLeft) + "px";
    };

    // 宽度是这样分配的,举个?,如果a,b,c,d,他们每个都有个changed状态,默认false,拖过a,a.changed改为true,改变的宽度就由剩下的b,c,d平摊,如果都改变了,就让最后一个元素d背锅
    const handleOnMouseUp = (event) => {
      if (this.store.dragging) {
        const { startColumnLeft } = this.store;
        const finalLeft = parseInt(hold.style.left, 10);
        const columnWidth = finalLeft - startColumnLeft;
        const index = +target.dataset.index;
        HColgroup.children[index].width = columnWidth;

        if (index !== this.store.HColumns.length - 1) {
          this.store.HColumns[index].isChange = true;
        }
        const deltaLeft = event.clientX - this.store.startMouseLeft;
        const changeColumns = this.store.HColumns.filter(v => !v.isChange && +v.el.width > 30);
        changeColumns.forEach(item => {
          item.el.width = +item.el.width - deltaLeft / changeColumns.length;
        });

        this.store.BColumns.forEach((item, i) => {
          item.el.width = this.store.HColumns[i].el.width;
        });

        //...init store
      }

      document.removeEventListener("mousemove", handleOnMouseMove);
      document.removeEventListener("mouseup", handleOnMouseUp);
      document.onselectstart = null;
      document.ondragstart = null;

      // noclick主要是用来判断是点击还是拖动,防止拖动触发排序
      setTimeout(() => {
        target.classList.remove("noclick");
      }, 0);
    };

    document.addEventListener("mouseup", handleOnMouseUp);
    document.addEventListener("mousemove", handleOnMouseMove);
  }
};
Tr.addEventListener("mousedown", handleMouseDown);

预览效果 (chrome + Safari + Firefox)

总结

觉得很有意思也很有用的东西,也让自己涨了很多姿势,源码,已经做成类的形式,使用起来还算简单,因为是突然提出的需求,还未做过多测试,可能存在不知道的bug。

祝福

写在最后,马上就要过年了,心情还是非常happy的。那么,我就在这里提前祝大家新年大吉、、吧,皮一下才开心,哎嘿嘿。拜拜~

后续补充

更改了宽度改变的方式,应该是只改变拖动列后面的列的宽度。有BUG,colgroup放在了thead下面,导致在safari下面有BUG,已经修复了,看的不仔细,但上面的代码还没有改,看代码的化还是去看源码,我没发现这个问题,别人帮我找出来的。
emmmmm,又发现了一个问题,就是拖动最后一列时。。。我想想,先睡了==

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107383.html

相关文章

  • 拖动table表头实现

    摘要:前言自己做的项目碰到这样一个需求,就是对所有的表格添加表头可以拖动的效果。需要说明的是,表头固定的那种是需要用两个去实现,做过的人应该也都明白。拜拜后续补充更改了宽度改变的方式,应该是只改变拖动列后面的列的宽度。 前言 自己做的项目碰到这样一个需求,就是对所有的表格添加表头可以拖动的效果。我一想,这不简单,分分钟钟给你做出来。拿起我的电脑,啪啪啪就敲起来了。showImg(https:...

    canopus4u 评论0 收藏0
  • 列表——表头自定义显示字段

    摘要:今天我就来讲讲插件的使用,它是如何实现列表表头自定义显示字段的,我把我的经验分享出来,满足一下不懂英语的人,给你们搭个快车。需求分析实现列表表头自定义显示字段,自定义表头排序。 序言 Yii2框架的扩展性能真的很不错,很多效果都可以通过插件去实现,你想不到的老外都帮你想好了,于是,人群中就流传了这么一句话:效果不会写不要紧,会用插件也不错。GitHub是一个庞大而且开放的资源库,平时有...

    Yangyang 评论0 收藏0
  • vueTable大数据展示优化

    摘要:懒加载方式常见的有淘宝一屏用元素占据一定的高度,然后再去拉图片数据。但这种方式还是需要元素占位,淘宝一页的数据量其实不算大,因为它结合了分页。 背景 showImg(https://segmentfault.com/img/bVbhSVh?w=1606&h=440);大数据项目根据用户输入代码查询数据,用户的代码不可控(比如select from db limit 5000),有可能...

    JasinYip 评论0 收藏0
  • 一种局部固定表头实现方案(横向跟随内容一同滚动,纵向锁定表头

    摘要:将设置为将和的放入一个的中,设置横向轴可以滚动,轴不可滚动。表头和表内容的横向方向滚动能力其实是父级样式赋予的。 1.为了固定表头我们需要先把表格的head和tbody切分到两个table里: 和 里。 2.将table-body放在一个table-body-box的div里设置y轴方向可以滚动,x轴方向不可滚动,这样表内容就能在自己的区域实现垂直方向的滚动了。3.将table-hea...

    zhkai 评论0 收藏0
  • 一种局部固定表头实现方案(横向跟随内容一同滚动,纵向锁定表头

    摘要:将设置为将和的放入一个的中,设置横向轴可以滚动,轴不可滚动。表头和表内容的横向方向滚动能力其实是父级样式赋予的。 1.为了固定表头我们需要先把表格的head和tbody切分到两个table里: 和 里。 2.将table-body放在一个table-body-box的div里设置y轴方向可以滚动,x轴方向不可滚动,这样表内容就能在自己的区域实现垂直方向的滚动了。3.将table-hea...

    developerworks 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<