import { ref } from 'vue' import type { SampleGroup } from '../types' import { useReorderAnnouncer } from '../composables/useListReorder' import { getSampleGroupMajorPrefix } from '../utils/sampleGroupPath' import { detectSampleGroupSeparator, getSampleGroupParentPath, moveSampleToGroup, reorderSampleGroup, reorderSampleMajorGroup, type GroupDragKind, type GroupReorderPosition, } from './SampleSelector.groups' interface MutableSampleGroups { value: SampleGroup[] } export function useSampleSelectorDrag(groups: MutableSampleGroups) { const draggingSample = ref(null) const dragSourceGroup = ref(null) const dragOverGroup = ref(null) const draggingGroup = ref(null) const draggingGroupKind = ref(null) const reorderTarget = ref(null) const reorderPosition = ref(null) const { announcement, announce, announcerStyle } = useReorderAnnouncer() function handleDragStart(sample: string, sourceGroup: string | null, event: DragEvent) { draggingSample.value = sample dragSourceGroup.value = sourceGroup if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('text/plain', sample) } } function resetDragState() { draggingSample.value = null dragSourceGroup.value = null dragOverGroup.value = null } function handleDragEnd() { resetDragState() } function handleDragOver(groupName: string, event: DragEvent) { // Only accept this drop target when a sample is being dragged; group drags are handled separately. if (!draggingSample.value) return event.preventDefault() if (event.dataTransfer) { event.dataTransfer.dropEffect = 'move' } dragOverGroup.value = groupName } function handleDragLeave() { dragOverGroup.value = null } function handleDrop(targetGroupName: string, event: DragEvent) { if (!draggingSample.value) return event.preventDefault() const sample = draggingSample.value const sourceGroup = dragSourceGroup.value if (sourceGroup === targetGroupName) { resetDragState() return } groups.value = moveSampleToGroup(groups.value, sample, sourceGroup, targetGroupName) resetDragState() } function handleGroupDragStart(name: string, kind: GroupDragKind, event: DragEvent) { draggingGroup.value = name draggingGroupKind.value = kind if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('text/plain', `mint-group:${name}`) } } function handleGroupDragEnd() { draggingGroup.value = null draggingGroupKind.value = null reorderTarget.value = null reorderPosition.value = null } function handleGroupDragOver(name: string, kind: GroupDragKind, event: DragEvent) { if (!draggingGroup.value || draggingGroupKind.value !== kind) return if (draggingGroup.value === name) return // Leaf reorder is restricted to siblings under the same immediate parent; // crossing any hierarchy boundary would silently rename the group. if (kind === 'sub') { const separator = detectSampleGroupSeparator(groups.value) if ( getSampleGroupParentPath(draggingGroup.value, separator) !== getSampleGroupParentPath(name, separator) ) return } event.preventDefault() if (event.dataTransfer) event.dataTransfer.dropEffect = 'move' const rect = (event.currentTarget as HTMLElement).getBoundingClientRect() reorderTarget.value = name reorderPosition.value = event.clientY < rect.top + rect.height / 2 ? 'before' : 'after' } function handleGroupDragLeave(event: DragEvent) { const cur = event.currentTarget as HTMLElement const rel = event.relatedTarget as Node | null if (rel && cur.contains(rel)) return reorderTarget.value = null reorderPosition.value = null } function handleGroupDrop(name: string, kind: GroupDragKind, event: DragEvent) { if (!draggingGroup.value || draggingGroupKind.value !== kind) { handleGroupDragEnd() return } const position = reorderPosition.value if (!position || draggingGroup.value === name) { handleGroupDragEnd() return } event.preventDefault() if (kind === 'major') { const nextGroups = reorderSampleMajorGroup(groups.value, draggingGroup.value, name, position) if (nextGroups !== groups.value) groups.value = nextGroups } else { const nextGroups = reorderSampleGroup(groups.value, draggingGroup.value, name, position) if (nextGroups !== groups.value) groups.value = nextGroups } handleGroupDragEnd() } /** * Keyboard alternative to the group reorder drag. The hierarchy constrains a * move to the dragged node's siblings (a leaf may not cross a parent), so this * is not a flat list reorder and does not use `useListReorder`: Alt+ArrowUp / * Alt+ArrowDown swap the node with its previous / next sibling instead. */ function handleGroupReorderKeydown(name: string, kind: GroupDragKind, event: KeyboardEvent) { if (!event.altKey) return const direction = event.key === 'ArrowUp' ? -1 : event.key === 'ArrowDown' ? 1 : 0 if (direction === 0) return event.preventDefault() const separator = detectSampleGroupSeparator(groups.value) const siblings = kind === 'major' ? uniqueMajorPrefixes(groups.value, separator) : groups.value .filter(group => getSampleGroupParentPath(group.name, separator) === getSampleGroupParentPath(name, separator)) .map(group => group.name) const currentIndex = siblings.indexOf(name) const target = siblings[currentIndex + direction] if (currentIndex === -1 || target === undefined) return const position: GroupReorderPosition = direction === -1 ? 'before' : 'after' const nextGroups = kind === 'major' ? reorderSampleMajorGroup(groups.value, name, target, position) : reorderSampleGroup(groups.value, name, target, position) if (nextGroups === groups.value) return groups.value = nextGroups announce(`${name} moved ${direction === -1 ? 'up' : 'down'} to position ${currentIndex + direction + 1} of ${siblings.length}.`) } return { announcement, announcerStyle, handleGroupReorderKeydown, draggingSample, dragOverGroup, draggingGroup, draggingGroupKind, reorderTarget, reorderPosition, handleDragStart, handleDragEnd, handleDragOver, handleDragLeave, handleDrop, handleGroupDragStart, handleGroupDragEnd, handleGroupDragOver, handleGroupDragLeave, handleGroupDrop, } } /** Major-group prefixes in tree order, de-duplicated. */ function uniqueMajorPrefixes(groups: SampleGroup[], separator: string): string[] { const seen: string[] = [] for (const group of groups) { const prefix = getSampleGroupMajorPrefix(group.name, separator) if (!seen.includes(prefix)) seen.push(prefix) } return seen }