BoxSelect.vue

<template>
  <div
    class="box-select__container"
    @mousedown.left="mouseDown"
    @mousemove.stop="mouseMove"
    :class="uuid"
  >
    <div
      class="box-select__coordinate"
      :style="style"
      ref="selectContainer"
    ></div>
    <slot></slot>
  </div>
</template>

<script>
import { debounce, isNumber } from "lodash"
import { ref, onUnmounted, nextTick, shallowRef } from "vue"

/**
 * @description 判断元素是否在范围内
 * @param {Object} dom dom元素
 */
const isWithinRange = (dom, top, bottom, left, right) => {
  const eleRect = dom.getBoundingClientRect()
  return !(
    eleRect.top > bottom ||
    eleRect.bottom < top ||
    eleRect.right < left ||
    eleRect.left > right
  )
}

export default {
  name: "BoxSelect",
  /**
   * @member props
   * @property  {String} [node] 要框选的元素,可以是元素名,也可以是class名, 也可以是id名
   * @property  {String} [selectedClass] 已选中元素附加的class名
   */
  props: {
    node: {
      required: true,
      type: String
    },
    selectedClass: {
      type: String,
      default: 'box-select__hypocritical'
    }
  },
  // 鼠标按下
  emits: ["mouseUp", "mouseDown"],
  setup(props, { emit }) {
    let top = 0,
      left = 0,
      width = 0,
      height = 0,
      startX = 0,
      startY = 0,
      timer = null,
      // 记录是框选还是点击
      mouseOn = false

    const style = ref({}),
      selectContainer = ref(null),
      // 给当前框容器加一个唯一识别符, 以保证所选择到的元素都是当前容器的. 否则会选择到容器外同名的元素
      uuid = shallowRef("uuid_" + new Date().valueOf())

    const query = (className = '') => {
      let domName = `.${uuid.value} ${props.node}`
      className && (domName += `.${className}`)
      return Array.from(document.querySelectorAll(domName) || [])
    }

    const classOperation = (ele, method = 'add', className = '') => ele.classList[method](className)

    const setStyle = (styles = {}, newStyles = {}) => {
      Object.keys(styles).map((item) => {
        newStyles[item] = styles[item] + (isNumber(styles[item]) ? "px" : '')
      })
      style.value = newStyles
    }

    const getAreaWithinElements = () => {
      const {
        bottom,
        left,
        right,
        top
      } = selectContainer.value.getBoundingClientRect()
      
      // 所有可框选元素
      const elements = query()
      // 已选中元素
      const selectedElements = elements.filter(item => classOperation(item, 'contains', props.selectedClass))
      // 未选中元素
      const unselectedElements = elements.filter(item => !classOperation(item, 'contains', props.selectedClass))
      
      selectedElements.map(item => {
        const withinRange = isWithinRange(item, top, bottom, left, right)
        withinRange && 
        classOperation(item, 'contains', props.selectedClass) && 
        classOperation(item, 'remove', props.selectedClass)
      })

      unselectedElements.map((item) => 
        isWithinRange(item, top, bottom, left, right) && 
          classOperation(item, 'add', props.selectedClass))

      return query(props.selectedClass)
    }

    const mouseDown = debounce((event) => {
      timer = setTimeout(() => {
        mouseOn = true
        startX = event.clientX
        startY = event.clientY
        emit("mouseDown")
      }, 300)
      // 重置本次框选的元素列表
      setStyle({ left, startX, top: startY, width: 0, height: 0, display: "block" })
    })

    const mouseMove = debounce((event) => {
      if (!mouseOn) return false
      const _width = event.clientX - startX
      const _height = event.clientY - startY

      top = _height > 0 ? startY : event.clientY
      left = _width > 0 ? startX : event.clientX
      width = Math.abs(_width)
      height = Math.abs(_height)
      setStyle({ left, top, width, height })
    })

    const mouseUp = debounce((event) => {
      timer && clearTimeout(timer)
      // 判断是否鼠标左键
      if (event.which !== 1) return false
      // 判断是框选还是点击
      if(!mouseOn) return false
      mouseOn = false
      setStyle({ display: "none" })
      // 获得已选中的元素
      const selectedEles = getAreaWithinElements()
      // 响应事件,并传递本次框选的元素列表
      emit("mouseUp", selectedEles)
    })

    nextTick(() => document.addEventListener("mouseup", mouseUp))
    onUnmounted(() => document.removeEventListener("mouseup", mouseUp))

    return {
      mouseUp,
      mouseDown,
      mouseMove,
      timer,
      style,
      selectContainer,
      uuid
    }
  }
}
</script>

<style lang="scss">
.box-select__container {
  .box-select__coordinate {
    position: fixed;
    z-index: 11;
    left: 0;
    top: 0;
    width: 0;
    height: 0;
    background: rgba(0, 0, 0, .5);
    border:1px solid rgba(0, 0, 0, 1);
    opacity: 0.6;
    pointer-events: none;
  }
.box-select__hypocritical {
          background-color: blue;
        }
}
</style>

使用方法

<box-select node=".box">
   <div class="box"></div>
   <div class="box"></div>
   <div class="box"></div>
   <div class="box"></div>
</box-select>

Mrsum
959 声望21 粉丝