likes
comments
collection
share

比BackTop更强的快速定位组件

作者站长头像
站长
· 阅读数 59

效果

比BackTop更强的快速定位组件

有一个快捷浏览的滑块组件,拖动它可以滚动整个页面。 不管height多大的页面,都可以通过移动滑块来到达页面的任何位置。

实现拖拽容器组件

export default {
  props: {
    containTrigger: Boolean,
    lockAxis: {
      type: String,
      validator(v) {
        return ['x', 'y'].includes(v);
      },
    },
    restrictedArea: Boolean,
  },
  data() {
    return {
      dragging: false,
      beginPos: { x: 0, y: 0 }, // 鼠标按下位置
      translate: { x: 0, y: 0 },
      position: { left: 0, top: 0 },
    };
  },
  computed: {
    style() {
      const { translate, position } = this;
      const style = {
        position: this.restrictedArea ? 'absolute' : 'fixed',
        left: `${position.left}px`,
        top: `${position.top}px`,
      };
      if (this.dragging) {
        Object.assign(style, {
          // 提升层级,防止被遮挡
          zIndex: '9999',
          // 防止拖拽变文字选择
          userSelector: 'none',
          transform: `translate(${translate.x}px, ${translate.y}px)`,
        });
      }
      return style;
    },
  },
  render(h) {
    return h('div', { style: this.style }, this.$slots.default);
  },
  mounted() {
    this.init();
  },
  methods: {
    setProgress(percent) {
      const { width: w } = this.restrictedEl.getBoundingClientRect();
      const { width: w2 } = this.$el.getBoundingClientRect();
      this.position.left = (w - w2) * percent;
    },
    init() {
      // 拖拽元素 默认使用组件的第一个元素作为拖拽元素
      const triggerEl = this.containTrigger
        ? this.$el.querySelector('[drag-trigger]')
        : this.$slots.default[0].elm;

      // 拖拽限制的祖先元素(拖拽范围)
      let restrictedEl;
      if (this.restrictedArea) {
        restrictedEl = this.$el.parentNode;
        while (
          getComputedStyle(restrictedEl).position === 'static' &&
          restrictedEl !== document.documentElement
        ) {
          restrictedEl = restrictedEl.parentNode;
        }
      } else {
        restrictedEl = document.documentElement;
      }
      this.restrictedEl = restrictedEl;
      triggerEl.addEventListener('mousedown', this.handleMouseDown, true);
    },
    handleMouseDown(e) {
      e.stopPropagation();
      this.dragging = true;
      this.beginPos = { x: e.pageX, y: e.pageY };
      this.restrictedElRect = this.restrictedEl.getBoundingClientRect();
      this.targetRect = e.target.getBoundingClientRect();
      document.addEventListener('mousemove', this.handleMouseMove, true);
      document.addEventListener('mouseup', this.handleMouseUp, true);
    },
    handleMouseMove(e) {
      e.stopPropagation();
      requestAnimationFrame(() => {
        this.translate = this.getChangePosition(e);
      });
    },
    handleMouseUp(e) {
      e.stopPropagation();
      this.dragging = false;
      document.removeEventListener('mousemove', this.handleMouseMove, true);
      document.removeEventListener('mouseup', this.handleMouseUp, true);
      requestAnimationFrame(() => {
        let { left, top } = this.position;
        const { x, y } = this.getChangePosition(e);
        left += x;
        top += y;
        this.position = { left, top };
      });
    },
    getChangePosition(e) {
      let { pageX: x, pageY: y } = e;
      const {
        left: l,
        right: r,
        top: t,
        bottom: b,
        width: w,
      } = this.restrictedElRect;
      const {
        left: l2,
        right: r2,
        top: t2,
        bottom: b2,
        width: w2,
      } = this.targetRect;
      const { x: x2, y: y2 } = this.beginPos;
      x =
        this.lockAxis === 'x'
          ? 0
          : x < l - l2 + x2
          ? l - l2
          : x > r - r2 + x2
          ? r - r2
          : x - x2;
      y =
        this.lockAxis === 'y'
          ? 0
          : y < t - t2 + y2
          ? t - t2
          : y > b  b2 + y2
          ? b - b2
          : y - y2;
      this.$emit('changePosition', (this.position.left + x) / (w - w2));
      return { x, y };
    },
  },
};

props说明

  • containTrigger 布尔值。告诉触发拖拽的部分是否在组件内部。若在组件内部则需要在拖拽元素上添加drag-trigger属性。

  • lockAxis 枚举值。x|y,可以锁定一个轴向不能拖拽。

  • restrictedArea 布尔值。表示是否限制了拖拽区域。默认未限制,拖拽区域为document;若限制,则往上查找第一个positionstatic的祖先元素。

data说明

  • dragging 表示当前是否正在拖拽中。从鼠标按下开始,到鼠标抬起结束。
  • beginPos 鼠标按下的位置。
  • translate 鼠标移动位置与鼠标按下位置的偏移值,使用transform:translate(x,y)实现偏移。
  • position 鼠标抬起时,将偏移量传给定位的lefttop值。

快捷导航组件

// 文章页快捷进度条
const QuickProgressBar = {
  render(h) {
    return h(
      'div',
      {
        staticClass: 'quick-progressbar-container',
      },
      [
        h(
          DragWrapper,
          {
            ref: 'dragger',
            props: { restrictedArea: true, lockAxis: 'y' },
            on: {
              changePosition: persent => {
                requestAnimationFrame(() => {
                  document.documentElement.scrollTop =
                    document.documentElement.scrollHeight * persent;
                });
              },
            },
          },
          [h('span', { staticClass: 'quick-progressbar-slider' }, '快捷浏览')]
        ),
      ]
    );
  },
  mounted() {
    this.$watch(
      '$refs.dragger.dragging',
      v => {
        if (v) {
          document.removeEventListener('scroll', this.handleScroll);
        } else {
          document.addEventListener('scroll', this.handleScroll);
        }
      },
      { immediate: true }
    );
  },
  methods: {
    handleScroll() {
      this.$refs.dragger.setProgress(
        document.documentElement.scrollTop /
          document.documentElement.scrollHeight
      );
    },
  },
};

难点

当快捷浏览组件拖动时,需要滚动页面;页面滚动时,又会移动快捷浏览组件。如何避免相互调用?

  1. 外部组件会监听DragWrapper组件是否处于拖动状态。
  2. 若开始拖拽,则先解绑对页面滚动事件的监听;结束拖拽时,绑定对页面滚动的监听。
  3. DragWrapper组件拖拽发生时,通过$emit('changePosition', percent)来通知页面需要滚动和高度百分比。
  4. 鼠标滚动页面时,DragWrapper组件的拖拽肯定是没有进行的,此时会绑定页面滚动事件的监听,在监听的处理器中调用DragWrappersetProgress(percent)来改变拖动元素的位置。
转载自:https://juejin.cn/post/7283769640803123256
评论
请登录