Skip to content
On this page

使用vue 封装一个拖拽组件

TIP

封装一个支持拖拽的组件,效果图如下:

示意图

功能支持

  • 1.支持方向(LB、LT、RB、RT)
  • 2.仅支持在屏幕内拖动(边缘检测)
  • 3.支持设置边缘margin

支持拖拽

TIP

在仅支持拖拽默认在左上角还是比较好实现的,具体如下:

vue
<template>
  <div
    ref="draggableRef"
    class="draggable"
    :style="style"
  >
    <slot />
  </div>
</template>

<script>
export default {
  name: 'DraggableComponent',

  props: {
    initialValue: {
      type: Object,
      required: false,
      default: () => ({ x: 0, y: 0 }),
    },
  },

  data() {
    return {
      currentValue: { x: 0, y: 0 },
      isDragging: false,
      startX: 0,
      startY: 0,
      diffX: 0,
      diffY: 0,
    };
  },

  computed: {
    style() {
      return `left: ${this.currentValue.x + this.diffX}px; top: ${this.currentValue.y + this.diffY}px`;
    },
  },

  watch: {
    initialValue: {
      handler(val) {
        this.currentValue = val;
      },
      immediate: true,
    },
  },

  mounted() {
    this.$nextTick(() => {
      const { draggableRef } = this.$refs;
      if (draggableRef) {
        draggableRef.addEventListener('mousedown', this.startDrag);
        document.addEventListener('mousemove', this.moveDrag);
        document.addEventListener('mouseup', this.endDrag);
      }
    });
  },

  beforeDestroy() {
    const { draggableRef } = this.$refs;
    draggableRef.removeEventListener('mousedown', this.startDrag);
    document.removeEventListener('mousemove', this.moveDrag);
    document.removeEventListener('mouseup', this.endDrag);
  },

  methods: {
    startDrag({ clientX: x1, clientY: y1 }) {
      this.isDragging = true;
      document.onselectstart = () => false;
      this.startX = x1;
      this.startY = y1;
    },

    moveDrag({ clientX: x2, clientY: y2 }) {
      if (this.isDragging) {
        this.diffX = x2 - this.startX;
        this.diffY = y2 - this.startY;
      }
    },

    endDrag() {
      this.isDragging = false;
      document.onselectstart = () => true;
      this.currentValue.x += this.diffX;
      this.currentValue.y += this.diffY;
      this.diffX = 0;
      this.diffY = 0;
    },
  },
};
</script>

<style>
.draggable {
  position: fixed;
  z-index: 9;
}
</style>

使用方式:

vue
<template>
<div class="wrap">
  <DraggableComponent>
    <div style="width: 300px; height: 200px;background: red;">
      123
    </div>
  </DraggableComponent>
</template>

<script>
import DraggableComponent from '@/components/draggable/index.vue';
export default {
  components: { DraggableComponent },
};
</script>

TIP

代码解释就是:在鼠标摁下的时候开始isDragging字段,然后还是计算move的距离,最后赋值给拖动的元素。

支持4个方向以及增加边缘检测

TIP

向着目标实现 完整代码如下:

vue
<template>
  <div
    ref="draggableRef"
    class="draggable"
    :style="style"
  >
    <slot />
  </div>
</template>

<script>

const getScrollBarWidth = () => {
  const outer = document.createElement('div');
  outer.style.overflow = 'scroll';
  outer.style.height = '200px';
  outer.style.width = '100px';
  document.body.appendChild(outer);
  const widthNoScroll = outer.offsetWidth;

  const inner = document.createElement('div');
  inner.style.width = '100%';
  outer.appendChild(inner);
  const widthWithScroll = inner.offsetWidth;
  const scrollBarWidth = widthNoScroll - widthWithScroll;

  return scrollBarWidth;
};

const START_DIRECTION = {
  RT: 'RT', // right -> top
  RB: 'RB', // right -> bottom
  LT: 'LT', // left -> top
  LB: 'LB', // left -> bottom
};

export default {
  name: 'DraggableComponent',

  props: {
    initialValue: {
      type: Object,
      required: false,
      default: () => ({ x: 24, y: 24 }),
    },

    direction: {
      type: String,
      required: false,
      default: START_DIRECTION.LT, // LT | LB | RB | RT
    },

    windowOnly: {
      type: Boolean,
      required: false,
      default: false,
    },

    margin: {
      type: Number,
      required: false,
      default: 0,
    },
  },

  data() {
    return {
      currentValue: { x: 0, y: 0 },
      isDragging: false,
      startX: 0,
      startY: 0,
      diffX: 0,
      diffY: 0,
      maxValue: {
        width: 0,
        height: 0,
      },
    };
  },

  computed: {
    style() {
      const bottomValue = this.currentValue.y - this.diffY;
      const topValue = this.currentValue.y + this.diffY;
      const rightValue = this.currentValue.x - this.diffX;
      const leftValue = this.currentValue.x + this.diffX;

      if (this.direction === START_DIRECTION.RB) {
        if (this.windowOnly) {
          const [currentRightValue, currentBottomValue] = this.getWindowOnlyValue(START_DIRECTION.RB, rightValue, bottomValue);
          return `right: ${currentRightValue}px; bottom: ${currentBottomValue}px`;
        }
        return `right: ${rightValue}px; bottom: ${bottomValue}px`;
      }
      if (this.direction === START_DIRECTION.RT) {
        if (this.windowOnly) {
          const [currentRightValue, currentTopValue] = this.getWindowOnlyValue(START_DIRECTION.RT, rightValue, topValue);
          return `right: ${currentRightValue}px; top: ${currentTopValue}px`;
        }
        return `right: ${rightValue}px; top: ${topValue}px`;
      }
      if (this.direction === START_DIRECTION.LT) {
        if (this.windowOnly) {
          const [currentLeftValue, currentTopValue] = this.getWindowOnlyValue(START_DIRECTION.LT, leftValue, topValue);
          return `left: ${currentLeftValue}px; top: ${currentTopValue}px`;
        }
        return `left: ${leftValue}px; top: ${topValue}px`;
      }
      if (this.direction === START_DIRECTION.LB) {
        if (this.windowOnly) {
          const [currentLeftValue, currentBottomValue] = this.getWindowOnlyValue(START_DIRECTION.LB, leftValue, bottomValue);
          return `left: ${currentLeftValue}px; bottom: ${currentBottomValue}px`;
        }
        return `left: ${leftValue}px; bottom: ${bottomValue}px`;
      }

      return `left: ${leftValue}px; top: ${bottomValue}px`;
    },
  },

  watch: {
    initialValue: {
      handler(val) {
        this.currentValue = val;
      },
      immediate: true,
    },
  },

  mounted() {
    this.$nextTick(() => {
      const { draggableRef } = this.$refs;
      if (draggableRef) {
        this.isWindowOnlyCheck();
        draggableRef.addEventListener('mousedown', this.startDrag);
        document.addEventListener('mousemove', this.moveDrag);
        document.addEventListener('mouseup', this.endDrag);
      }
    });

    window.addEventListener('resize', this.isWindowOnlyCheck);
  },

  beforeDestroy() {
    const { draggableRef } = this.$refs;
    draggableRef.removeEventListener('mousedown', this.startDrag);
    document.removeEventListener('mousemove', this.moveDrag);
    document.removeEventListener('mouseup', this.endDrag);
    window.removeEventListener('resize', this.isWindowOnlyCheck);
  },

  methods: {
    startDrag({ clientX: x1, clientY: y1 }) {
      this.isDragging = true;
      document.onselectstart = () => false;
      this.startX = x1;
      this.startY = y1;
    },

    moveDrag({ clientX: x2, clientY: y2 }) {
      if (this.isDragging) {
        this.diffX = x2 - this.startX;
        this.diffY = y2 - this.startY;
      }
    },

    getWindowOnlyValue(direction = START_DIRECTION.LT, value1 = 0, value2 = 0) {
      const { height: maxHeight, width: maxWidth } = this.maxValue;

      const minRight = this.margin;
      const maxRight = maxWidth;

      const minBottom = this.margin;
      const maxBottom = maxHeight;

      const minLeft = this.margin;
      const maxLeft = maxWidth;

      const minTop = this.margin;
      const maxTop = maxHeight;

      let currentRight = 0;
      let currentLeft = 0;
      if (value1 < 0) {
        currentRight = value1 < minRight ? minRight : value1;
        currentLeft = value1 < maxLeft ? maxLeft : value1;
      } else {
        currentRight = value1 > maxRight ? maxRight : value1;
        currentLeft = value1 > maxLeft ? maxLeft : value1;
      }

      let currentBottom = 0;
      let currentTop = 0;
      if (value2 < 0) {
        currentBottom = value2 < minBottom ? minBottom : value2;
        currentTop = value2 < minTop ? minTop : value2;
      } else {
        currentBottom = value2 > maxBottom ? maxBottom : value2;
        currentTop = value2 > maxTop ? maxTop : value2;
      }

      if (direction === START_DIRECTION.RB) {
        return [currentRight, currentBottom];
      }
      if (direction === START_DIRECTION.RT) {
        return [currentRight, currentTop];
      }
      if (direction === START_DIRECTION.LB) {
        return [currentLeft, currentBottom];
      }
      if (direction === START_DIRECTION.LT) {
        return [currentLeft, currentTop];
      }

      return [minLeft, maxLeft, minTop, maxTop];
    },

    endDrag() {
      document.onselectstart = () => true;

      const bottomValue = this.currentValue.y - this.diffY;
      const topValue = this.currentValue.y + this.diffY;
      const rightValue = this.currentValue.x - this.diffX;
      const leftValue = this.currentValue.x + this.diffX;

      if (this.direction === START_DIRECTION.RB) {
        if (this.windowOnly) {
          [this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.RB, rightValue, bottomValue);
        } else {
          this.currentValue.x = rightValue;
          this.currentValue.y = bottomValue;
        }
      }

      if (this.direction === START_DIRECTION.RT) {
        if (this.windowOnly) {
          [this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.RT, rightValue, topValue);
        } else {
          this.currentValue.x = rightValue;
          this.currentValue.y = topValue;
        }
      }

      if (this.direction === START_DIRECTION.LT) {
        if (this.windowOnly) {
          [this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.LT, leftValue, topValue);
        } else {
          this.currentValue.x = leftValue;
          this.currentValue.y = topValue;
        }
      }

      if (this.direction === START_DIRECTION.LB) {
        if (this.windowOnly) {
          [this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.LB, leftValue, bottomValue);
        } else {
          this.currentValue.x = leftValue;
          this.currentValue.y = bottomValue;
        }
      }

      this.isDragging = false;
      this.diffX = 0;
      this.diffY = 0;
    },

    isWindowOnlyCheck() {
      if (this.windowOnly) {
        const { draggableRef } = this.$refs;
        const { width: conatinerWidth, height: containerHeight } = draggableRef?.getBoundingClientRect() || { width: 0, height: 0 };
        const maxWidth = window.innerWidth;
        const maxHeight = window.innerHeight;
        const isScroll = document.body.scrollHeight > window.innerHeight;

        this.maxValue = {
          width: maxWidth - conatinerWidth - this.margin - (isScroll ? getScrollBarWidth() : 0),
          height: maxHeight - containerHeight - this.margin,
        };
      }
    },
  },
};
</script>

<style>
.draggable {
  position: fixed;
  z-index: 9;
}
</style>

使用方式:

vue
<template>
<div class="wrap">
  <DraggableComponent
    start-direction="RB"
    window-only
    :margin="10"
  >
    <div style="width: 300px; height: 200px;background: red;">
      123
    </div>
  </DraggableComponent>
</template>

<script>
import DraggableComponent from '@/components/draggable/index.vue';
export default {
  components: { DraggableComponent },
};
</script>

结束语

TIP

在增加边缘检测的时候主要是要把滚动条的宽度需要参与计算,不然就会不准确。

Released under the MIT License.