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>