import { ref, type Ref } from 'vue' export interface DragSelectBox { x1: number y1: number x2: number y2: number } export interface DragSelectOptions { getSelected: () => string[] setSelected: (keys: string[]) => void keyOf?: (el: HTMLElement) => string onBox?: (box: DragSelectBox | null) => void } const DRAG_THRESHOLD = 4 function overlaps(ax1: number, ay1: number, ax2: number, ay2: number, bx1: number, by1: number, bx2: number, by2: number) { return ax1 < bx2 && ax2 > bx1 && ay1 < by2 && ay2 > by1 } export function useDragSelect(containerRef: Ref, opts: DragSelectOptions) { const box = ref(null) function setBox(b: DragSelectBox | null) { box.value = b opts.onBox?.(b) } let startX = 0 let startY = 0 let curX = 0 let curY = 0 let moved = false let lastWasDrag = false function onMousedown(e: MouseEvent) { if (e.button !== 0) return if (e.ctrlKey || e.metaKey || e.shiftKey) return const target = e.target as HTMLElement if (target.closest('button, a, input, textarea, select, .ms-ppl-card-checkbox, .ms-fm-play-btn')) return startX = curX = e.clientX startY = curY = e.clientY moved = false lastWasDrag = false setBox({ x1: startX, y1: startY, x2: startX, y2: startY }) window.addEventListener('mousemove', onMousemove) window.addEventListener('mouseup', onMouseup) } function onMousemove(e: MouseEvent) { curX = e.clientX curY = e.clientY if (!moved && Math.abs(curX - startX) < DRAG_THRESHOLD && Math.abs(curY - startY) < DRAG_THRESHOLD) { setBox({ x1: startX, y1: startY, x2: startX, y2: startY }) return } if (!moved) { moved = true document.body.style.userSelect = 'none' } setBox({ x1: Math.min(startX, curX), y1: Math.min(startY, curY), x2: Math.max(startX, curX), y2: Math.max(startY, curY), }) } function onMouseup(e: MouseEvent) { window.removeEventListener('mousemove', onMousemove) window.removeEventListener('mouseup', onMouseup) if (!moved) { setBox(null) return } lastWasDrag = true const rect = box.value setBox(null) if (document.body.style.userSelect === 'none') { document.body.style.userSelect = '' } const container = containerRef.value if (!container || !rect) { return } const bx1 = rect.x1 const by1 = rect.y1 const bx2 = rect.x2 const by2 = rect.y2 const keyOf = opts.keyOf ?? ((el: HTMLElement) => el.dataset.dragKey || '') const intersecting = new Set() const cards = container.querySelectorAll('[data-drag-key]') cards.forEach((card) => { const r = card.getBoundingClientRect() if (overlaps(r.left, r.top, r.right, r.bottom, bx1, by1, bx2, by2)) { const key = keyOf(card) if (key) intersecting.add(key) } }) const prev = opts.getSelected() if (e.shiftKey) { const next = new Set(prev) intersecting.forEach((k) => next.add(k)) opts.setSelected(Array.from(next)) } else if (e.ctrlKey || e.metaKey) { const next = new Set(prev) intersecting.forEach((k) => { if (next.has(k)) next.delete(k) else next.add(k) }) opts.setSelected(Array.from(next)) } else { opts.setSelected(Array.from(intersecting)) } } function onClickCapture(e: MouseEvent) { if (lastWasDrag) { e.preventDefault() e.stopPropagation() lastWasDrag = false } } return { box, onMousedown, onClickCapture, } } export function dragSelectRectStyle(b: DragSelectBox) { return { left: b.x1 + 'px', top: b.y1 + 'px', width: Math.max(0, b.x2 - b.x1) + 'px', height: Math.max(0, b.y2 - b.y1) + 'px', } }