el-table 实现列拖拽【干货满满】

前段时间在el-table 源码加了个列显隐功能,后面老大说用户自定义列表也是一个特点,我就想了下,之前看过别人写的都是动态去渲染el-table-column标签,但是在我们项目中功能页面都是饱和状态,去做动态渲染,改动成本太大,而且标签中大多都是使用formatter,不是理想,就想着在源码上去动手脚。
  1. 使用 sortablejs
  2. 支持本地存储列和列宽
  3. 对 el-table 源码进行改动
改动文件目录
  • el-table
    • table.vue
    • table-header.js
    • store
      • index.js

具体效果如下,屏蔽了一些拖拽的列【selection、expand、index,操作,二级菜单】

以上列出都不支持拖拽也不支持拖拽到这里,保证表格基本布局信息。
在这里插入图片描述

改动这个和上次一样,需要将el-table 源码拉出来,不使用extends方式,这是因为改不到子文件上去。前面步骤: 给el-table实现列显隐

覆盖注册组件

import ElementUI from "element-ui"
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
import ElTable from '@/components/element-ui/ElTable';
import ElTableColumn from "@/components/element-ui/el-table-column";
Vue.component(ElTable.name, ElTable); // 这个步骤同等于对象重新赋值的操作
Vue.component(ElTableColumn.name, ElTableColumn);

项目中将抛出3个关联钩子

钩子备注描述
@header-cell-dragend=“headerCellDragend”列被拖拽的钩子column被拖拽
@table-ready=“tableReady”初始化完成可以对ElTable进行操作
@header-dragend=“headerDragend”ElTable原生事件钩子由于是扩展,原参数基础加了一个参数

项目中使用方式

<template>
  <el-row class="mt-4">
    <el-col :span="12" :push="2">
      <el-table size="mini" :data="table.data" border stripe :height="300" :header-cell-style="{ background: '#e9d5ff' }" @header-cell-dragend="headerCellDragend" @table-ready="tableReady" @header-dragend="headerDragend">
        <el-table-column type="selection" />
        <el-table-column type="index" label="#" />
        <el-table-column align="center" prop="ip" label="IP" />
        <el-table-column align="center" prop="type" label="操作类型" />

        <el-table-column align="center" label="动作">
          <el-table-column align="center" prop="count" label="总访问次数" />
          <el-table-column align="center" prop="visitCount" label="访问次数" />
        </el-table-column>

        <el-table-column align="center" label="操作">
          <template slot-scope="scope">
            <el-button type="text" size="mini">查看</el-button>
            <el-button type="text" size="mini">删除</el-button>
          </template>
        </el-table-column>
        
      </el-table>
    </el-col>
  </el-row>
</template>

<script>
function localstorageSet(key, obj = {}) {
  if (!key) return;
  localStorage.setItem(key, JSON.stringify(obj));
}

function localstorageGet(key) {
  if (!key) return;
  return JSON.parse(localStorage.getItem(key) || "{}");
}

export default {
  name: "Demo",
  data() {
    return {
      table: {
        data: Array.from({ length: 10 }, (v, k) => ({
          ip: "192.178.2.12",
          count: Math.floor(Math.random() * 100),
          type: "登录",
          visitCount: "访问次数",
        })),
      },
    };
  },
  methods: {
    headerDragend(newWidth, _oldWidth, column, _event, columns) {
      const name = this.$options.name;
      const tableMap = localstorageGet("ElTable");
      const BBQ = `${name}`;
      tableMap[BBQ] = columns;
      tableMap[BBQ].forEach((item) => {
        if (item.label === column.label) item.width = newWidth;
      });
      localstorageSet("ElTable", tableMap);
    },
    tableReady(table) {
      const tableMap = localstorageGet("ElTable");
      const name = this.$options.name;
      const BBQ = `${name}`;
      table.changeExistingSort(tableMap[BBQ]);
    },
    headerCellDragend(columns) {
      const name = this.$options.name;
      const tableMap = localstorageGet("ElTable");
      const BBQ = `${name}`; // 本来想和tableId串起来,但是做法不太行
      tableMap[BBQ] = columns;
      localstorageSet("ElTable", tableMap);
      /** 组件名称作为 Key
       * ElTable: {
       *  Demo: [
       *    {"width": 48},
       *    {"label": "#","width": 48},
       *    {"label": "IP","width": 129},
       *    {"label": "总访问次数"},
       *    {"width": 48},
       *    {"label": "操作"}
       *  ]
       * }
      */
    },
  },
};
</script>

table 组件中 (./el-table/table.vue)

<script>
  export default {
    name: "ElTable",
    mounted() {
      ...
      ,
      this.$ready = true;
      this.$emit("table-ready", this);
    },
    methods: {
      ...
      ,
      changeExistingSort(columns) {
        const { _columns } = this.store.states;
        if (!this.originColumns) this.originColumns = _columns.map(x => ({ label: x.label, width: x.width }));
        if (!columns || !columns.length) columns = this.originColumns;
        this.store.commit('updateColumnsCell', columns);
      },
    }
  }

</script>

store/index.js (./el-table/store/index.js)

Watcher.prototype.mutations = {
  // ...其他代码
  // 根据传入的列,更新列和列宽
  updateColumnsCell(states, cache) {
    if (!Array.isArray(cache)) return;

    const { _columns } = states;
    _columns.sort((item1, item2) => {
      const aa = cache.findIndex((elem) => elem.label === item1.label);
      if (aa === -1) return 1;
      const bb = cache.findIndex((elem) => elem.label === item2.label);
      return aa - bb;
    });

    _columns.forEach((item) => {
      const corresponding = cache.find((x) => x.label === item.label);
      if (corresponding && corresponding.width && corresponding.width !== item.width) {
        item.width = corresponding.width;
      }
    });

    this.scheduleLayout();
    this.updateColumns();
  },
};

Watcher.prototype.commit = function (name, ...args) {...
};

Watcher.prototype.updateTableScrollY = function () {...
};

export default Watcher;

table-header (./el-table/table-header.js)

改动比较大,建议直接复制文件,我会分为两部分,一部分是仅添加代码,一部分是完整代码

table-header (./el-table/table-header.js) 【改动代码】

// ...其他代码
import Sortable from "sortablejs";

export default {
  name: "ElTableHeader",

  render(h) {
    // ...其他代码
    // 1. tr 上 加入 了 sortableRef_rowIndex 的 ref 属性
    // 2. th 上 加入 了 on-mouseover={($event) => this.handleMouseOver($event)} 事件
    return (
      <table class="el-table__header" cellspacing="0" cellpadding="0" border="0">
        <colgroup>
          {this.columns.map((column) => (
            <col name={column.id} key={column.id} />
          ))}
          {this.hasGutter ? <col name="gutter" /> : ""}
        </colgroup>

        <thead class={[{ "is-group": isGroup, "has-gutter": this.hasGutter }]}>
          {this._l(columnRows, (columns, rowIndex) => (
            <tr ref={"sortableRef_" + rowIndex} style={this.getHeaderRowStyle(rowIndex)} class={this.getHeaderRowClass(rowIndex)}>
              {columns.map((column, cellIndex) => (
                <th colspan={column.colSpan} rowspan={column.rowSpan} on-mouseover={($event) => this.handleMouseOver($event)} on-mousedown={($event) => this.handleMouseDown($event, column)} on-mousemove={($event) => this.handleMouseMove($event, column)} on-mouseout={this.handleMouseOut} on-click={($event) => this.handleHeaderClick($event, column)} on-contextmenu={($event) => this.handleHeaderContextMenu($event, column)} style={this.getHeaderCellStyle(rowIndex, cellIndex, columns, column)} class={this.getHeaderCellClass(rowIndex, cellIndex, columns, column)} key={column.id}>
                  <div class={["cell", column.filteredValue && column.filteredValue.length > 0 ? "highlight" : "", column.labelClassName]}>
                    {column.renderHeader ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context }) : column.label}
                    {column.sortable ? (
                      <span class="caret-wrapper" on-click={($event) => this.handleSortClick($event, column)}>
                        <i class="sort-caret ascending" on-click={($event) => this.handleSortClick($event, column, "ascending")}></i>
                        <i class="sort-caret descending" on-click={($event) => this.handleSortClick($event, column, "descending")}></i>
                      </span>
                    ) : (
                      ""
                    )}
                    {column.filterable ? (
                      <span class="el-table__column-filter-trigger" on-click={($event) => this.handleFilterClick($event, column)}>
                        <i class={["el-icon-arrow-down", column.filterOpened ? "el-icon-arrow-up" : ""]}></i>
                      </span>
                    ) : (
                      ""
                    )}
                  </div>
                </th>
              ))}
              {this.hasGutter ? <th class="el-table__cell gutter"></th> : ""}
            </tr>
          ))}
        </thead>
      </table>
    );
  },



  mounted() {
    this.$nextTick(() => {
      // ...其他代码
      const _this = this;
      // 只针对第一行表头进行拖拽
      new Sortable(this.$refs.sortableRef_0, {
        animation: 150,
        draggable: "th",
        direction: "horizontal",
        swapThreshold: 0.5,
        clone: false,
        supportPointer: false,
        filter: function (e, target) {
          const { _columns } = _this.store.states;
          const findItem = _columns.find((item) => target.classList.contains(item.id));
          if (["selection", "index", "expand"].includes(findItem?.type) || ["#", "操作"].includes(findItem?.label)) {
            return true;
          }

          const rect = target.getBoundingClientRect();
          const { clientX } = e;
          const left = clientX - rect.left;
          const right = rect.right - clientX;
          const padding = 30;

          if (left < padding || right < padding) {
            return true;
          }

          return false;
        },
        onMove: this.onSortableMove,
        onEnd: this.onSortableEnd,
      });
    });
  },

  methods: {
    // ...其他代码
    
    // 禁止移动到固定列
    onSortableMove(evt) {
      const { _columns } = this.store.states;
      const related = evt.related;
      const findItem = _columns.find((item) => related.classList.contains(item.id));
      if (["selection", "index", "expand"].includes(findItem?.type) || ["#", "操作"].includes(findItem?.label)) {
        return false;
      }

      return true;
    },
    // 更新列顺序并抛出自定义事件
    onSortableEnd(e) {
      const { _columns } = this.store.states;
      const oldIndex = _columns.findIndex((item) => e.item.classList.contains(item.id));
      const target = _columns[oldIndex];

      if (oldIndex === -1 || target.level !== 1 || target.fixed) {
        e.preventDefault();
        return;
      }

      const newIndex = e.newIndex;
      const item = _columns.splice(oldIndex, 1)[0];
      _columns.splice(newIndex, 0, item);
      this.store.scheduleLayout();
      this.store.updateColumns();
      this.$parent.$emit(
        "header-cell-dragend",
        _columns.map((x) => ({ label: x.label, width: x.width }))
      );
      e.preventDefault();
    },
    // 给列设置padding边界,防止拖拽和列宽调整冲突
    handleMouseOver(event) {
      event.stopPropagation();
      event.preventDefault();
      const target = event.target;
      const cell = target.tagName === "TH" ? target : target.parentNode;

      function mouseMove(event) {
        const { clientX } = event;
        const rect = target.getBoundingClientRect();
        const left = clientX - rect.left;
        const right = rect.right - clientX;
        const padding = 30;
        if (left < padding || right < padding) return;
        document.body.style.cursor = "move";
      }

      function mouseOut() {
        document.removeEventListener("mousemove", mouseMove);
        cell.removeEventListener("mouseout", mouseOut);
      }

      document.addEventListener("mousemove", mouseMove);
      cell.addEventListener("mouseout", mouseOut);
    },
    // 在鼠标弹起发起emit事件参数中追加了一个columns参数,用于拖拽时记录当前表格的所有列名称和宽
    handleMouseDown(event, column) {
      if (this.$isServer) return;
      if (column.children && column.children.length > 0) return;
      /* istanbul ignore if */
      if (this.draggingColumn && this.border) {
        this.dragging = true;

        this.$parent.resizeProxyVisible = true;

        const table = this.$parent;
        const tableEl = table.$el;
        const tableLeft = tableEl.getBoundingClientRect().left;
        const columnEl = this.$el.querySelector(`th.${column.id}`);
        const columnRect = columnEl.getBoundingClientRect();
        const minLeft = columnRect.left - tableLeft + 30;

        addClass(columnEl, "noclick");
        this.dragState = {
          startMouseLeft: event.clientX,
          startLeft: columnRect.right - tableLeft,
          startColumnLeft: columnRect.left - tableLeft,
          tableLeft,
        };

        const resizeProxy = table.$refs.resizeProxy;
        resizeProxy.style.left = this.dragState.startLeft + "px";

        document.onselectstart = function () {
          return false;
        };
        document.ondragstart = function () {
          return false;
        };

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

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

        const handleMouseUp = () => {
          if (this.dragging) {
            const { startColumnLeft, startLeft } = this.dragState;
            const finalLeft = parseInt(resizeProxy.style.left, 10);
            const columnWidth = finalLeft - startColumnLeft;
            column.width = column.realWidth = columnWidth;

            const { _columns } = this.store.states;
            table.$emit(
              "header-dragend",
              column.width,
              startLeft - startColumnLeft,
              column,
              event,
              _columns.map((x) => ({ label: x.label, width: x.width }))
            );

            this.store.scheduleLayout();

            document.body.style.cursor = "";
            this.dragging = false;
            this.draggingColumn = null;
            this.dragState = {};
            table.resizeProxyVisible = false;
          }

          document.removeEventListener("mousemove", handleMouseMove);
          document.removeEventListener("mouseup", handleMouseUp);
          document.onselectstart = null;
          document.ondragstart = null;

          setTimeout(function () {
            removeClass(columnEl, "noclick");
          }, 0);
        };

        document.addEventListener("mousemove", handleMouseMove);
        document.addEventListener("mouseup", handleMouseUp);
      }
    },

  },

};

table-header (./el-table/table-header.js) 【完整代码】
import Vue from "vue";
import { hasClass, addClass, removeClass } from "element-ui/src/utils/dom";
import FilterPanel from "./filter-panel.vue";
import LayoutObserver from "./layout-observer";
import { mapStates } from "./store/helper";
import Sortable from "sortablejs";

const getAllColumns = (columns) => {
  const result = [];
  columns.forEach((column) => {
    if (column.children) {
      result.push(column);
      result.push.apply(result, getAllColumns(column.children));
    } else {
      result.push(column);
    }
  });
  return result;
};

const convertToRows = (originColumns) => {
  let maxLevel = 1;
  const traverse = (column, parent) => {
    if (parent) {
      column.level = parent.level + 1;
      if (maxLevel < column.level) {
        maxLevel = column.level;
      }
    }
    if (column.children) {
      let colSpan = 0;
      column.children.forEach((subColumn) => {
        traverse(subColumn, column);
        colSpan += subColumn.colSpan;
      });
      column.colSpan = colSpan;
    } else {
      column.colSpan = 1;
    }
  };

  originColumns.forEach((column) => {
    column.level = 1;
    traverse(column);
  });

  const rows = [];
  for (let i = 0; i < maxLevel; i++) {
    rows.push([]);
  }

  const allColumns = getAllColumns(originColumns);

  allColumns.forEach((column) => {
    if (!column.children) {
      column.rowSpan = maxLevel - column.level + 1;
    } else {
      column.rowSpan = 1;
    }
    rows[column.level - 1].push(column);
  });

  return rows;
};

export default {
  name: "ElTableHeader",

  mixins: [LayoutObserver],

  render(h) {
    const originColumns = this.store.states.originColumns;
    const columnRows = convertToRows(originColumns, this.columns);
    console.log(columnRows);
    // 是否拥有多级表头
    const isGroup = columnRows.length > 1;
    if (isGroup) this.$parent.isGroup = true;
    return (
      <table class="el-table__header" cellspacing="0" cellpadding="0" border="0">
        <colgroup>
          {this.columns.map((column) => (
            <col name={column.id} key={column.id} />
          ))}
          {this.hasGutter ? <col name="gutter" /> : ""}
        </colgroup>

        <thead class={[{ "is-group": isGroup, "has-gutter": this.hasGutter }]}>
          {this._l(columnRows, (columns, rowIndex) => (
            <tr ref={"sortableRef_" + rowIndex} style={this.getHeaderRowStyle(rowIndex)} class={this.getHeaderRowClass(rowIndex)}>
              {columns.map((column, cellIndex) => (
                <th colspan={column.colSpan} rowspan={column.rowSpan} on-mouseover={($event) => this.handleMouseOver($event)} on-mousedown={($event) => this.handleMouseDown($event, column)} on-mousemove={($event) => this.handleMouseMove($event, column)} on-mouseout={this.handleMouseOut} on-click={($event) => this.handleHeaderClick($event, column)} on-contextmenu={($event) => this.handleHeaderContextMenu($event, column)} style={this.getHeaderCellStyle(rowIndex, cellIndex, columns, column)} class={this.getHeaderCellClass(rowIndex, cellIndex, columns, column)} key={column.id}>
                  <div class={["cell", column.filteredValue && column.filteredValue.length > 0 ? "highlight" : "", column.labelClassName]}>
                    {column.renderHeader ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context }) : column.label}
                    {column.sortable ? (
                      <span class="caret-wrapper" on-click={($event) => this.handleSortClick($event, column)}>
                        <i class="sort-caret ascending" on-click={($event) => this.handleSortClick($event, column, "ascending")}></i>
                        <i class="sort-caret descending" on-click={($event) => this.handleSortClick($event, column, "descending")}></i>
                      </span>
                    ) : (
                      ""
                    )}
                    {column.filterable ? (
                      <span class="el-table__column-filter-trigger" on-click={($event) => this.handleFilterClick($event, column)}>
                        <i class={["el-icon-arrow-down", column.filterOpened ? "el-icon-arrow-up" : ""]}></i>
                      </span>
                    ) : (
                      ""
                    )}
                  </div>
                </th>
              ))}
              {this.hasGutter ? <th class="el-table__cell gutter"></th> : ""}
            </tr>
          ))}
        </thead>
      </table>
    );
  },

  data() {
    return {
      draggingColumn: null,
      dragging: false,
      dragState: {},
    };
  },

  props: {
    fixed: String,
    store: {
      required: true,
    },
    border: Boolean,
    defaultSort: {
      type: Object,
      default() {
        return {
          prop: "",
          order: "",
        };
      },
    },
  },

  created() {
    this.filterPanels = {};
  },

  mounted() {
    // nextTick 是有必要的 https://github.com/ElemeFE/element/pull/11311
    this.$nextTick(() => {
      const { prop, order } = this.defaultSort;
      const init = true;
      this.store.commit("sort", { prop, order, init });
      const _this = this;
      new Sortable(this.$refs.sortableRef_0, {
        animation: 150,
        draggable: "th",
        direction: "horizontal",
        swapThreshold: 0.5,
        clone: false,
        supportPointer: false,
        filter: function (e, target) {
          const { _columns } = _this.store.states;
          const findItem = _columns.find((item) => target.classList.contains(item.id));
          if (["selection", "index", "expand"].includes(findItem?.type) || ["#", "操作"].includes(findItem?.label)) {
            return true;
          }

          const rect = target.getBoundingClientRect();
          const { clientX } = e;
          const left = clientX - rect.left;
          const right = rect.right - clientX;
          const padding = 30;

          if (left < padding || right < padding) {
            return true;
          }

          return false;
        },
        onMove: this.onSortableMove,
        onEnd: this.onSortableEnd,
      });
    });
  },

  methods: {
    onSortableMove(evt) {
      const { _columns } = this.store.states;
      const related = evt.related;
      const findItem = _columns.find((item) => related.classList.contains(item.id));
      if (["selection", "index", "expand"].includes(findItem?.type) || ["#", "操作"].includes(findItem?.label)) {
        return false;
      }

      return true;
    },

    onSortableEnd(e) {
      const { _columns } = this.store.states;
      const oldIndex = _columns.findIndex((item) => e.item.classList.contains(item.id));
      const target = _columns[oldIndex];

      if (oldIndex === -1 || target.level !== 1 || target.fixed) {
        e.preventDefault();
        return;
      }

      const newIndex = e.newIndex;
      const item = _columns.splice(oldIndex, 1)[0];
      _columns.splice(newIndex, 0, item);
      this.store.scheduleLayout();
      this.store.updateColumns();
      this.$parent.$emit(
        "header-cell-dragend",
        _columns.map((x) => ({ label: x.label, width: x.width }))
      );
      e.preventDefault();
    },

    handleMouseOver(event) {
      event.stopPropagation();
      event.preventDefault();
      const target = event.target;
      const cell = target.tagName === "TH" ? target : target.parentNode;

      function mouseMove(event) {
        const { clientX } = event;
        const rect = target.getBoundingClientRect();
        const left = clientX - rect.left;
        const right = rect.right - clientX;
        const padding = 30;
        if (left < padding || right < padding) return;
        document.body.style.cursor = "move";
      }

      function mouseOut() {
        document.removeEventListener("mousemove", mouseMove);
        cell.removeEventListener("mouseout", mouseOut);
      }

      document.addEventListener("mousemove", mouseMove);
      cell.addEventListener("mouseout", mouseOut);
    },

    isCellHidden(index, columns) {
      let start = 0;
      for (let i = 0; i < index; i++) {
        start += columns[i].colSpan;
      }
      const after = start + columns[index].colSpan - 1;
      if (this.fixed === true || this.fixed === "left") {
        return after >= this.leftFixedLeafCount;
      } else if (this.fixed === "right") {
        return start < this.columnsCount - this.rightFixedLeafCount;
      } else {
        return after < this.leftFixedLeafCount || start >= this.columnsCount - this.rightFixedLeafCount;
      }
    },

    getHeaderRowStyle(rowIndex) {
      const headerRowStyle = this.table.headerRowStyle;
      if (typeof headerRowStyle === "function") {
        return headerRowStyle.call(null, { rowIndex });
      }
      return headerRowStyle;
    },

    getHeaderRowClass(rowIndex) {
      const classes = [];

      const headerRowClassName = this.table.headerRowClassName;
      if (typeof headerRowClassName === "string") {
        classes.push(headerRowClassName);
      } else if (typeof headerRowClassName === "function") {
        classes.push(headerRowClassName.call(null, { rowIndex }));
      }

      return classes.join(" ");
    },

    getHeaderCellStyle(rowIndex, columnIndex, row, column) {
      const headerCellStyle = this.table.headerCellStyle;
      if (typeof headerCellStyle === "function") {
        return headerCellStyle.call(null, {
          rowIndex,
          columnIndex,
          row,
          column,
        });
      }
      return headerCellStyle;
    },

    getHeaderCellClass(rowIndex, columnIndex, row, column) {
      const classes = [column.id, column.order, column.headerAlign, column.className, column.labelClassName];

      if (rowIndex === 0 && this.isCellHidden(columnIndex, row)) {
        classes.push("is-hidden");
      }

      if (!column.children) {
        classes.push("is-leaf");
      }

      if (column.sortable) {
        classes.push("is-sortable");
      }

      const headerCellClassName = this.table.headerCellClassName;
      if (typeof headerCellClassName === "string") {
        classes.push(headerCellClassName);
      } else if (typeof headerCellClassName === "function") {
        classes.push(
          headerCellClassName.call(null, {
            rowIndex,
            columnIndex,
            row,
            column,
          })
        );
      }

      classes.push("el-table__cell");

      return classes.join(" ");
    },

    toggleAllSelection() {
      this.store.commit("toggleAllSelection");
    },

    handleFilterClick(event, column) {
      event.stopPropagation();
      const target = event.target;
      let cell = target.tagName === "TH" ? target : target.parentNode;
      if (hasClass(cell, "noclick")) return;
      cell = cell.querySelector(".el-table__column-filter-trigger") || cell;
      const table = this.$parent;

      let filterPanel = this.filterPanels[column.id];

      if (filterPanel && column.filterOpened) {
        filterPanel.showPopper = false;
        return;
      }

      if (!filterPanel) {
        filterPanel = new Vue(FilterPanel);
        this.filterPanels[column.id] = filterPanel;
        if (column.filterPlacement) {
          filterPanel.placement = column.filterPlacement;
        }
        filterPanel.table = table;
        filterPanel.cell = cell;
        filterPanel.column = column;
        !this.$isServer && filterPanel.$mount(document.createElement("div"));
      }

      setTimeout(() => {
        filterPanel.showPopper = true;
      }, 16);
    },

    handleHeaderClick(event, column) {
      if (!column.filters && column.sortable) {
        this.handleSortClick(event, column);
      } else if (column.filterable && !column.sortable) {
        this.handleFilterClick(event, column);
      }

      this.$parent.$emit("header-click", column, event);
    },

    handleHeaderContextMenu(event, column) {
      this.$parent.$emit("header-contextmenu", column, event);
    },

    handleMouseDown(event, column) {
      if (this.$isServer) return;
      if (column.children && column.children.length > 0) return;
      /* istanbul ignore if */
      if (this.draggingColumn && this.border) {
        this.dragging = true;

        this.$parent.resizeProxyVisible = true;

        const table = this.$parent;
        const tableEl = table.$el;
        const tableLeft = tableEl.getBoundingClientRect().left;
        const columnEl = this.$el.querySelector(`th.${column.id}`);
        const columnRect = columnEl.getBoundingClientRect();
        const minLeft = columnRect.left - tableLeft + 30;

        addClass(columnEl, "noclick");
        this.dragState = {
          startMouseLeft: event.clientX,
          startLeft: columnRect.right - tableLeft,
          startColumnLeft: columnRect.left - tableLeft,
          tableLeft,
        };

        const resizeProxy = table.$refs.resizeProxy;
        resizeProxy.style.left = this.dragState.startLeft + "px";

        document.onselectstart = function () {
          return false;
        };
        document.ondragstart = function () {
          return false;
        };

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

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

        const handleMouseUp = () => {
          if (this.dragging) {
            const { startColumnLeft, startLeft } = this.dragState;
            const finalLeft = parseInt(resizeProxy.style.left, 10);
            const columnWidth = finalLeft - startColumnLeft;
            column.width = column.realWidth = columnWidth;

            const { _columns } = this.store.states;
            table.$emit(
              "header-dragend",
              column.width,
              startLeft - startColumnLeft,
              column,
              event,
              _columns.map((x) => ({ label: x.label, width: x.width }))
            );

            this.store.scheduleLayout();

            document.body.style.cursor = "";
            this.dragging = false;
            this.draggingColumn = null;
            this.dragState = {};
            table.resizeProxyVisible = false;
          }

          document.removeEventListener("mousemove", handleMouseMove);
          document.removeEventListener("mouseup", handleMouseUp);
          document.onselectstart = null;
          document.ondragstart = null;

          setTimeout(function () {
            removeClass(columnEl, "noclick");
          }, 0);
        };

        document.addEventListener("mousemove", handleMouseMove);
        document.addEventListener("mouseup", handleMouseUp);
      }
    },

    handleMouseMove(event, column) {
      if (column.children && column.children.length > 0) return;
      let target = event.target;
      while (target && target.tagName !== "TH") {
        target = target.parentNode;
      }

      if (!column || !column.resizable) return;

      if (!this.dragging && this.border) {
        const rect = target.getBoundingClientRect();

        const bodyStyle = document.body.style;
        if (rect.width > 12 && rect.right - event.pageX < 8) {
          bodyStyle.cursor = "col-resize";
          if (hasClass(target, "is-sortable")) {
            target.style.cursor = "col-resize";
          }
          this.draggingColumn = column;
        } else if (!this.dragging) {
          bodyStyle.cursor = "";
          if (hasClass(target, "is-sortable")) {
            target.style.cursor = "pointer";
          }
          this.draggingColumn = null;
        }
      }
    },

    handleMouseOut() {
      if (this.$isServer) return;
      document.body.style.cursor = "";
    },

    toggleOrder({ order, sortOrders }) {
      if (order === "") return sortOrders[0];
      const index = sortOrders.indexOf(order || null);
      return sortOrders[index > sortOrders.length - 2 ? 0 : index + 1];
    },

    handleSortClick(event, column, givenOrder) {
      event.stopPropagation();
      const order = column.order === givenOrder ? null : givenOrder || this.toggleOrder(column);

      let target = event.target;
      while (target && target.tagName !== "TH") {
        target = target.parentNode;
      }

      if (target && target.tagName === "TH") {
        if (hasClass(target, "noclick")) {
          removeClass(target, "noclick");
          return;
        }
      }

      if (!column.sortable) return;

      const states = this.store.states;
      let sortProp = states.sortProp;
      let sortOrder;
      const sortingColumn = states.sortingColumn;

      if (sortingColumn !== column || (sortingColumn === column && sortingColumn.order === null)) {
        if (sortingColumn) {
          sortingColumn.order = null;
        }
        states.sortingColumn = column;
        sortProp = column.property;
      }

      if (!order) {
        sortOrder = column.order = null;
      } else {
        sortOrder = column.order = order;
      }

      states.sortProp = sortProp;
      states.sortOrder = sortOrder;

      this.store.commit("changeSortCondition");
    },
  },

  computed: {
    table() {
      return this.$parent;
    },

    hasGutter() {
      return !this.fixed && this.tableLayout.gutterWidth;
    },

    ...mapStates({
      columns: "columns",
      isAllSelected: "isAllSelected",
      leftFixedLeafCount: "fixedLeafColumnsLength",
      rightFixedLeafCount: "rightFixedLeafColumnsLength",
      columnsCount: (states) => states.columns.length,
      leftFixedCount: (states) => states.fixedColumns.length,
      rightFixedCount: (states) => states.rightFixedColumns.length,
    }),
  },

  beforeDestroy() {
    const panels = this.filterPanels;
    for (const prop in panels) {
      // eslint-disable-next-line
      if (panels.hasOwnProperty(prop) && panels[prop]) {
        panels[prop].$destroy(true);
      }
    }
  },
};

解决 ElTable 鼠标浮在header/footer 滑动滚轮出现X轴偏移。

这是 ElTable 的 bug,当 table 出现 X 轴滚动条时候,鼠标放在表头滑动滚轮就会移动,源代码有实现。不过源代码中参数写反了,它不产生效果,也不会有负面影响。如果后续有改动(当前版本2.15.13),不过停止更新多年了,应该不会被改。

   // 该函数是table.vue中methods 中的一个事件,本来说写博客中,忘记了。评论区提一下
    handleHeaderFooterMousewheel(event, data) {
      const { pixelX, pixelY } = data;
      if (Math.abs(pixelX) <= Math.abs(pixelY)) {
        this.bodyWrapper.scrollLeft += data.pixelY / 5;
      }
    },
  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值