table 横向滚动条定位在底部

774 阅读3分钟

1. 先创建一个tableScrollbarFixed.js

// 表格横向滚动条固定页面底部指令
/**
 * 创建一个scroller的dom
 */

import { throttle } from 'lodash';
import Vue from 'vue';

class Scroller {
  /**
   * 给tableBody创建一个scroller
   * @param {Element} targetTableWrapperEl
   * @param {string} mode
   */
  constructor(targetTableWrapperEl, mode = 'hover', scrollWrapper) {
    if (!targetTableWrapperEl) {
      throw new Error('need have table element');
    }
    this.targetTableWrapperEl = targetTableWrapperEl;
    this.fullwidth = false;
    this.mode = mode;
    this.scrollWrapper = scrollWrapper;

    /**
     * 创建相关dom
     */
    const scroller = document.createElement('div');
    scroller.classList.add('zk-scrollbar');
    scroller.style.height = '12px';
    scroller.style.position = 'sticky';
    scroller.style.bottom = 0;
    scroller.style.left = 0;
    scroller.style.zIndex = 3;
    scroller.style.display = 'none';

    this.dom = scroller;

    const bar = document.createElement('div');
    bar.classList.add('zk-scrollbar__bar', 'is-horizontal');
    bar.style.cssText = 'position: absolute;right: 2px;bottom: 2px;z-index: 1;border-radius: 4px;opacity: 0;transition: opacity 120ms ease-out; height: 6px;left: 2px;';
    this.bar = bar;
    scroller.appendChild(bar);

    const thumb = document.createElement('div');
    thumb.classList.add('zk-scrollbar__thumb');
    thumb.style.cssText = `position: relative;
    display: block;
    width: 0;
    height: 100%;
    cursor: pointer;
    border-radius: inherit;
    background-color: #99999999;
    transition: .3s background-color;`;
    bar.appendChild(thumb);
    this.thumb = thumb;

    /**
     * 初始化配置
     */
    // eslint-disable-next-line consistent-this
    const instance = this;
    this.checkIsScrollBottom = throttle(() => {
      if (!this.targetTableWrapperEl.offsetParent) return;
      const viewHeight = this.scrollWrapper.getBoundingClientRect().bottom;
      const { bottom } = targetTableWrapperEl.getBoundingClientRect();
      const scrollBarBottom = Number(this.dom.style.bottom.split('px')[0]) - 4;
      if (bottom + scrollBarBottom <= viewHeight) {
        instance.hideScroller();
      } else {
        // 需要重新设置一次当前宽度
        instance.resetBar(false);
        // 显示当前的bar
        instance.showScroller();
      }
    }, 1000 / 60);
    this.initBar = throttle(() => {
      this.dom.style.display = 'none';
      // bar宽度自动重制
      setTimeout(() => {
        this.resetBar();
        // this.resetScroller();
        this.resetThumbPosition();
      }, 1000);
    }, 2000);
    this.scrollWrapper.addEventListener('scroll', this.checkIsScrollBottom); // 全局判断是否需要显示scroller
    this.resizeObserver = new ResizeObserver(this.initBar);
    this.resizeObserver.observe(this.targetTableWrapperEl);

    // 自动同步,table => scroller
    targetTableWrapperEl.addEventListener(
      'scroll',
      throttle(() => {
        instance.resetThumbPosition();
      }, 1000 / 60),
    );

    // 自动同步 scroller => table
    this.syncDestoryHandler = this.initScrollSyncHandler();

    // 监听table的dom变化,自动重新设置
    this.tableElObserver = new MutationObserver(this.initBar);
    this.tableElObserver.observe(targetTableWrapperEl, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['style'],
    });
  }

  /**
   * 自动设置Bar
   * @param {boolean} changeScrollerVisible 是否开启自动设置滚动条显示与否
   */
  resetBar(changeScrollerVisible = true) {
    const { targetTableWrapperEl } = this;
    const widthPercentage = (targetTableWrapperEl.clientWidth * 100) / targetTableWrapperEl.scrollWidth;
    const thumbWidth = Math.min(widthPercentage, 100);
    this.thumb.style.width = `${thumbWidth}%`;

    this.fullwidth = thumbWidth >= 100;

    if (changeScrollerVisible) {
      if (this.fullwidth) {
        this.hideScroller();
      } else {
        this.checkIsScrollBottom();
      }
    }
  }

  resetThumbPosition() {
    this.thumb.style.transform = `translateX(${this.moveX}%)`;
  }

  resetScroller() {
    const { targetTableWrapperEl, dom, scrollWrapper } = this;
    const boundingClientRect = targetTableWrapperEl.getBoundingClientRect();
    // console.log(scrollWrapper);
    dom.style.width = `${boundingClientRect.width}px`;
    const scrollWrapperPaddingBottom = Number(getComputedStyle(scrollWrapper).paddingBottom.split('px')[0]);
    if (dom.style.display === 'none') return;
    dom.style.bottom = `${-scrollWrapperPaddingBottom}px`;
    setTimeout(() => {
      // 获取dom元素所处位置的点击元素,如果点击元素不是dom元素 证明被遮挡,需要设置dom元素bottom位置
      const domClientReact = dom.getBoundingClientRect();
      const clickElement = document.elementFromPoint(domClientReact.x, domClientReact.y);
      if (clickElement && clickElement.className !== 'zk-scrollbar') {
        dom.style.bottom = `${clickElement.offsetHeight - scrollWrapperPaddingBottom}px`;
      }
    }, 300);
  }

  get moveX() {
    const { targetTableWrapperEl } = this;
    return (targetTableWrapperEl.scrollLeft * 100) / targetTableWrapperEl.clientWidth;
  }

  /**
   * 让scroller的拖动行为和table的同步
   * 处理类似element-ui的拖拽处理
   */
  initScrollSyncHandler() {
    let cursorDown = false;
    let tempClientX = 0;
    let rate = 1;

    const { thumb, targetTableWrapperEl, bar } = this;

    function getRate() {
      // 计算一下变换比例,拖拽走的是具体数字,但是这个实际上应该是按照比例变的
      return bar.offsetWidth / thumb.offsetWidth;
    }

    const mouseMoveDocumentHandler = throttle(
      /** @param {MouseEvent} e */
      (e) => {
        if (cursorDown === false) {
          return;
        }
        const { clientX } = e;
        const offset = clientX - tempClientX;
        const originTempClientX = tempClientX;
        tempClientX = clientX;

        const tempScrollleft = targetTableWrapperEl.scrollLeft;
        targetTableWrapperEl.scrollLeft += offset * rate;
        if (tempScrollleft === targetTableWrapperEl.scrollLeft) {
          tempClientX = originTempClientX;
        }
      },
      1000 / 60,
    );
    /** @param {MouseEvent} e */
    function mouseUpDocumentHandler() {
      cursorDown = false;
      document.removeEventListener('mousemove', mouseMoveDocumentHandler);
      document.removeEventListener('mouseup', mouseUpDocumentHandler);
      document.onselectstart = null;
    }

    /**
     * 拖拽处理
     * @param {MouseEvent} e
     */
    function startDrag(e) {
      e.stopImmediatePropagation();
      cursorDown = true;
      document.addEventListener('mousemove', mouseMoveDocumentHandler);
      document.addEventListener('mouseup', mouseUpDocumentHandler);
      document.onselectstart = () => false;
    }

    thumb.onmousedown = function (e) {
      // prevent click event of right button
      if (e.ctrlKey || e.button === 2) {
        return;
      }

      const { clientX } = e;
      tempClientX = clientX;
      rate = getRate();
      startDrag(e);
    };

    /**
     * 点击槽快速移动
     * @param {PointerEvent} e
     */
    bar.onclick = function (e) {
      const { target } = e;
      if (target !== bar) {
        return;
      }
      rate = getRate();
      const { clientX } = e;
      let offset = 0;
      const thumbPosition = thumb.getBoundingClientRect();
      if (thumbPosition.left >= clientX) {
        offset = clientX - thumbPosition.left;
      } else {
        offset = clientX - thumbPosition.left - thumbPosition.width;
      }

      const targetScrollLeft = targetTableWrapperEl.scrollLeft + offset * rate;
      targetTableWrapperEl.scrollTo({
        left: targetScrollLeft,
        behavior: 'smooth',
      });
    };

    return function () {
      document.removeEventListener('mouseup', mouseUpDocumentHandler);
    };
  }

  /**
   * 显示整体
   */
  showScroller() {
    if (!this.fullwidth) {
      if (this.dom.style.display === 'none') {
        this.resetScroller();
      }
      this.dom.style.display = 'block';
    }
    if (this.mode === 'force') {
      this.bar.style.opacity = 1;
    }
  }

  /**
   * 隐藏整体
   */
  hideScroller() {
    this.dom.style.display = 'none';
  }

  /**
   * 显示滚动条
   */
  showBar() {
    this.bar.style.opacity = 1;
    this.checkIsScrollBottom();
    this.resetScroller();
  }

  /**
   * 隐藏滚动条
   */
  hideBar() {
    if (this.mode === 'force') {
      this.bar.style.opacity = 1;
    } else {
      this.bar.style.opacity = 0;
    }
  }

  destory() {
    this.tableElObserver.disconnect();
    this.scrollWrapper.removeEventListener('scroll', this.checkIsScrollBottom);
    this.syncDestoryHandler();
  }
}

/** @type {Vue.DirectiveOptions} */
export const directiveVue2 = {
  inserted(el, binding) {
    if (binding.value === false) {
      return;
    }
    const { value = {} } = binding;
    const { mode = 'hover', noNeed = false } = value;
    if (noNeed) return;
    const tableBodyWrapper = el.querySelector('.zk-table__body');
    const scrollWrapper = getScrollParent(tableBodyWrapper);
    console.log(scrollWrapper);

    const scroller = new Scroller(tableBodyWrapper, mode, scrollWrapper);

    el.appendChild(scroller.dom);
    el.horizontalScroll = scroller;

    if (mode === 'hover') {
      el.addEventListener('mouseenter', scroller.showBar.bind(scroller));
      el.addEventListener('mouseleave', scroller.hideBar.bind(scroller));
    } else {
      // scroller.showBar();
    }
  },
  unbind(el, binding) {
    if (binding.value === false) {
      return;
    }
    const { value = {} } = binding;
    const { noNeed = false } = value;
    if (noNeed) return;
    el.horizontalScroll.destory();
  },
};
// 获取目标元素的滚动父级
function getScrollParent(element, includeHidden) {
  let style = getComputedStyle(element);
  const excludeStaticParent = style.position === 'absolute';
  const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;

  if (style.position === 'fixed') return document.body;
  // eslint-disable-next-line no-cond-assign
  for (let parent = element; (parent = parent.parentElement); ) {
    style = getComputedStyle(parent);
    if (excludeStaticParent && style.position === 'static') {
      continue;
    }
    if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
  }

  return document.body;
}

export default Vue.directive('scrollBarFixed', directiveVue2);

2. 然后在main.js 中引令这个指令

import tableScrollbarFixed from '@/tableScrollbarFixed.js';
Vue.use(tableScrollbarFixed);

3. 最后在二次封装好的table 组件中 引入指令 v-scroll-bar-fixed

image.png

4. 效果如下

image.png