diff --git a/frontend/src/composables/useSwipeSelect.ts b/frontend/src/composables/useSwipeSelect.ts index 9d7ff12747..090d37d399 100644 --- a/frontend/src/composables/useSwipeSelect.ts +++ b/frontend/src/composables/useSwipeSelect.ts @@ -5,7 +5,7 @@ import { ref, onMounted, onUnmounted, type Ref } from 'vue' * with a semi-transparent marquee overlay showing the selection area. * * Features: - * - Start dragging from anywhere on the page (not just inside the table) + * - Start dragging only inside the table container * - Mouse wheel scrolling continues selecting new rows * - Auto-scroll when dragging near viewport edges * - 5px drag threshold to avoid accidental selection on click @@ -202,10 +202,12 @@ export function useSwipeSelect( if (e.button !== 0) return if (!containerRef.value) return + const target = e.target as HTMLElement + if (!containerRef.value.contains(target)) return + // Skip clicks on any scrollbar (inner containers + document) if (isOnScrollbar(e)) return - const target = e.target as HTMLElement if (target.closest('button, a, input, select, textarea, [role="button"], [role="menuitem"], [role="combobox"], [role="dialog"]')) return if (shouldPreferNativeTextSelection(target)) return @@ -367,7 +369,6 @@ export function useSwipeSelect( // --- Lifecycle --- onMounted(() => { - // Listen on document so drag can start from anywhere on the page document.addEventListener('mousedown', onMouseDown) window.addEventListener('blur', onWindowBlur) }) diff --git a/frontend/src/composables/useTableSelection.ts b/frontend/src/composables/useTableSelection.ts new file mode 100644 index 0000000000..a65144a91e --- /dev/null +++ b/frontend/src/composables/useTableSelection.ts @@ -0,0 +1,98 @@ +import { computed, ref, type Ref } from 'vue' + +interface UseTableSelectionOptions { + rows: Ref + getId: (row: T) => number +} + +export function useTableSelection({ rows, getId }: UseTableSelectionOptions) { + const selectedSet = ref>(new Set()) + + const selectedIds = computed(() => Array.from(selectedSet.value)) + const selectedCount = computed(() => selectedSet.value.size) + + const isSelected = (id: number) => selectedSet.value.has(id) + + const replaceSelectedSet = (next: Set) => { + selectedSet.value = next + } + + const setSelectedIds = (ids: number[]) => { + selectedSet.value = new Set(ids) + } + + const select = (id: number) => { + if (selectedSet.value.has(id)) return + const next = new Set(selectedSet.value) + next.add(id) + replaceSelectedSet(next) + } + + const deselect = (id: number) => { + if (!selectedSet.value.has(id)) return + const next = new Set(selectedSet.value) + next.delete(id) + replaceSelectedSet(next) + } + + const toggle = (id: number) => { + if (selectedSet.value.has(id)) { + deselect(id) + return + } + select(id) + } + + const clear = () => { + if (selectedSet.value.size === 0) return + replaceSelectedSet(new Set()) + } + + const removeMany = (ids: number[]) => { + if (ids.length === 0 || selectedSet.value.size === 0) return + const next = new Set(selectedSet.value) + let changed = false + ids.forEach((id) => { + if (next.delete(id)) changed = true + }) + if (changed) replaceSelectedSet(next) + } + + const allVisibleSelected = computed(() => { + if (rows.value.length === 0) return false + return rows.value.every((row) => selectedSet.value.has(getId(row))) + }) + + const toggleVisible = (checked: boolean) => { + const next = new Set(selectedSet.value) + rows.value.forEach((row) => { + const id = getId(row) + if (checked) { + next.add(id) + } else { + next.delete(id) + } + }) + replaceSelectedSet(next) + } + + const selectVisible = () => { + toggleVisible(true) + } + + return { + selectedSet, + selectedIds, + selectedCount, + allVisibleSelected, + isSelected, + setSelectedIds, + select, + deselect, + toggle, + clear, + removeMany, + toggleVisible, + selectVisible + } +} diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index e9f35752fc..f3194112ae 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -131,7 +131,7 @@