近段时间做一个编辑器,就需要一个鼠标拖拽选区的功能,方便批量选中元素,进行相应操作,所有就有了这篇文章。
效果展示
建立选区组件
1、要想选中元素,肯定要先建立选区
- 根据两个坐标点确定选区位置,并绘制出选区
- 根据两个坐标删除选区的宽与高
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const size = computed(() => { const width = props.endPoint.x === 0 ? 0 : Math.abs(props.startPoint.x - props.endPoint.x); const height = props.endPoint.y === 0 ? 0 : Math.abs(props.startPoint.y - props.endPoint.y); return { width, height, }; });
|
- 确定起始坐标点
- 不管从何处点击都需要找到两个坐标点所绘制的矩形的左上角的坐标点。
- 左上角的坐标点,很明显是所有坐标的最小值,也就是 X,Y 取最小的值的点(如下)
- 还需要考虑终点还没有产生时的情况,也就是排除终点为初始值也就是 0 的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const Point = computed(() => { const x = props.endPoint.x === 0 ? props.startPoint.x : Math.min(props.startPoint.x, props.endPoint.x); const y = props.endPoint.y === 0 ? props.startPoint.y : Math.min(props.startPoint.y, props.endPoint.y); return { x, y, }; });
|
2、根据选区,筛选出选中的元素
- 筛选选区的元素主要是根据 nodeType 选中出编辑区所有可选择的元素节点。
- 怎么才算可选元素,这就得看自己需要,我在元素节点上标注了 canChecked,通过这个属性排除非可选元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
function getChildrens(parentElement: HTMLElement, keyCode: string) { const ary = []; const childs = parentElement.childNodes; for (let i = 0; i < childs.length; i++) { if (childs[i].nodeType === 1) { if ((childs[i] as HTMLElement).getAttribute(keyCode) !== null) { ary.push(childs[i]); } } } return ary as Array<HTMLElement>; }
|
3、判断节点是否在选区内
- 这个主要根据 getBoundingClientRect 方法返回的信息
- 当选区的 top 与 left 小于判断元素,bottom 与大于判断元素,即认为该元素在选区内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
function judgeContainElement( selectBoxElement: HTMLElement, canCheckedElements: Array<HTMLElement> ) { const ContainElement: Array<HTMLElement> = []; const { left, right, bottom, top } = selectBoxElement.getBoundingClientRect(); canCheckedElements.forEach((item) => { const child = item.getBoundingClientRect(); if ( child.left > left && child.top > top && child.bottom < bottom && child.right < right ) { ContainElement.push(item); } }); return ContainElement; }
|
4、至此我们已经可以获得到选区内选中元素了,然后就可对选中元素做需要的操作了,也就是如效果图所示/
完整代码附上
1、组件基础结构文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| <template> <div id="select-area" class="select-area" :style="[ { width: size.width + 'px' }, { height: size.height + 'px' }, { top: Point.y + 'px' }, { left: Point.x + 'px' }, ]" /> </template>
<script lang="ts"> import { computed, defineComponent } from 'vue'; import type { PropType } from 'vue'; interface Point { x: number; y: number; } export default defineComponent({ name: 'SelectArea', props: { startPoint: { type: Object as PropType<Point>, required: true, }, endPoint: { type: Object as PropType<Point>, required: true, }, }, setup(props) { const Point = computed(() => { const x = props.endPoint.x === 0 ? props.startPoint.x : Math.min(props.startPoint.x, props.endPoint.x); const y = props.endPoint.y === 0 ? props.startPoint.y : Math.min(props.startPoint.y, props.endPoint.y); return { x, y, }; }); const size = computed(() => { const width = props.endPoint.x === 0 ? 0 : Math.abs(props.startPoint.x - props.endPoint.x); const height = props.endPoint.y === 0 ? 0 : Math.abs(props.startPoint.y - props.endPoint.y); return { width, height, }; }); return { Point, size, }; }, }); </script> <style lang="less" scoped> .select-area { position: fixed; background-color: rgba(255, 192, 203, 0.1); border: 1px solid red; z-index: 9; } </style>
|
2、导出组件文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { createVNode, render } from 'vue'; import SelectAreaConstructor from './SelectArea.vue'; let instence: HTMLElement | undefined; let instenceIsExit = false; const SelectArea = function(options: any) { if (instenceIsExit) { document.body.removeChild(instence as HTMLElement); instenceIsExit = false; } const vm = createVNode(SelectAreaConstructor, options); const container = document.createElement('div'); render(vm, container); instence = container.firstElementChild as HTMLElement; document.body.appendChild(instence); instenceIsExit = true; return instence; };
const close = () => { if (instenceIsExit) { instenceIsExit = false; document.body.removeChild(instence as HTMLElement); instence = undefined; } }; export { SelectArea, close };
|
3、应用文件 setup 部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| setup() { let selectProps = reactive({ startPoint: { x: 0, y: 0, }, endPoint: { x: 0, y: 0, }, }); let mouseKey = ref(false); let mouseComplete = ref(false); const headleMouseDown = (e: MouseEvent) => { close(); selectProps.startPoint.x = e.clientX; selectProps.startPoint.y = e.clientY; SelectArea(selectProps); mouseKey.value = true; mouseComplete.value = false; }; const headleMouseMove = (e: MouseEvent) => { if (mouseKey.value && !mouseComplete.value) { selectProps.endPoint.x = e.clientX; selectProps.endPoint.y = e.clientY; const div = document.querySelector('#select-area'); const parent = document.querySelector('.edit-area'); const containDiv = selectElement( parent as HTMLElement, div as HTMLElement, 'canChecked' ); containDiv.canCheckedElements.forEach((item) => { item.style.border = 'none'; }); containDiv.containElements.forEach((item) => { item.style.border = '1px solid red'; item.style.cursor = 'move'; }); } }; const headleDrag = (e: MouseEvent) => { e.preventDefault(); }; const headleMouseUp = () => { mouseKey.value = false; mouseComplete.value = true; selectProps.startPoint.x = 0; selectProps.startPoint.y = 0; selectProps.endPoint.x = 0; selectProps.endPoint.y = 0; close(); }; window.addEventListener('mousedown', headleMouseDown); window.addEventListener('mousemove', headleMouseMove); window.addEventListener('mouseup', headleMouseUp); onUnmounted(() => { window.removeEventListener('mousedown', headleMouseDown); window.removeEventListener('mousemove', headleMouseMove); window.removeEventListener('mouseup', headleMouseUp); }); const saveWeekly = () => { console.log('click'); }; return { headleMouseDown, headleMouseMove, headleMouseUp, headleDrag, saveWeekly, }; },
|
4、辅助工具函数文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
function selectElement( parentElement: HTMLElement, selectBoxElement: HTMLElement, keyCode: string ) { const canCheckedElements = getChildrens(parentElement, keyCode); const containElements = judgeContainElement( selectBoxElement, canCheckedElements ); return { containElements, canCheckedElements, }; } export { selectElement };
function getChildrens(parentElement: HTMLElement, keyCode: string) { const ary = []; const childs = parentElement.childNodes; for (let i = 0; i < childs.length; i++) { if (childs[i].nodeType === 1) { if ((childs[i] as HTMLElement).getAttribute(keyCode) !== null) { ary.push(childs[i]); } } } return ary as Array<HTMLElement>; } function judgeContainElement( selectBoxElement: HTMLElement, canCheckedElements: Array<HTMLElement> ) { const ContainElement: Array<HTMLElement> = []; const { left, right, bottom, top } = selectBoxElement.getBoundingClientRect(); canCheckedElements.forEach((item) => { const child = item.getBoundingClientRect(); if ( child.left > left && child.top > top && child.bottom < bottom && child.right < right ) { ContainElement.push(item); } }); return ContainElement; }
|