Init Repo
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
<TableHead
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
v-show="header.column.getIsVisible()"
|
||||
:class="[
|
||||
header.column.getCanSort() ? 'cursor-pointer select-none hover:bg-muted/50' : '',
|
||||
header.column.id === 'select' ? 'w-12' : '',
|
||||
header.column.id === 'actions' ? 'w-12' : '',
|
||||
allTaskTypes.includes(header.column.id) ? 'w-[140px]' : '',
|
||||
]"
|
||||
@click="header.column.getCanSort() ? header.column.toggleSorting() : null"
|
||||
>
|
||||
<FlexRender
|
||||
v-if="!header.isPlaceholder"
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-if="table.getRowModel().rows?.length">
|
||||
<TableRow
|
||||
v-for="row in table.getRowModel().rows"
|
||||
:key="row.id"
|
||||
:data-state="row.getIsSelected() ? 'selected' : undefined"
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
:class="{
|
||||
'bg-muted/30': row.getIsSelected(),
|
||||
'table-row-selectable': true,
|
||||
'selecting': isRangeSelecting
|
||||
}"
|
||||
@click="handleRowClick(row.original, $event, row)"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
>
|
||||
<TableCell
|
||||
v-for="cell in row.getAllCells()"
|
||||
:key="cell.id"
|
||||
v-show="cell.column.getIsVisible()"
|
||||
v-memo="[cell.getValue(), cell.column.getIsVisible()]"
|
||||
>
|
||||
<FlexRender
|
||||
:render="cell.column.columnDef.cell"
|
||||
:props="cell.getContext()"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TableRow>
|
||||
<TableCell :colspan="columns.length" class="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
FlexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useVueTable,
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
} from '@tanstack/vue-table'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { type Shot } from '@/services/shot'
|
||||
|
||||
interface Props {
|
||||
columns: ColumnDef<Shot>[]
|
||||
data: Shot[]
|
||||
sorting: SortingState
|
||||
columnVisibility: VisibilityState
|
||||
allTaskTypes: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:sorting': [sorting: SortingState]
|
||||
'update:columnVisibility': [visibility: VisibilityState]
|
||||
'update:rowSelection': [selection: Record<string, boolean>]
|
||||
'row-click': [shot: Shot, event: MouseEvent]
|
||||
'selection-cleared': []
|
||||
}>()
|
||||
|
||||
// Track the last selected row index for range selection
|
||||
const lastSelectedIndex = ref<number | null>(null)
|
||||
const isRangeSelecting = ref(false)
|
||||
const rowSelection = ref<Record<string, boolean>>({})
|
||||
|
||||
const table = useVueTable({
|
||||
get data() {
|
||||
return props.data
|
||||
},
|
||||
get columns() {
|
||||
return props.columns
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getRowId: (row) => String(row.id),
|
||||
onSortingChange: (updaterOrValue) => {
|
||||
const newSorting =
|
||||
typeof updaterOrValue === 'function'
|
||||
? updaterOrValue(props.sorting)
|
||||
: updaterOrValue
|
||||
emit('update:sorting', newSorting)
|
||||
},
|
||||
onColumnVisibilityChange: (updaterOrValue) => {
|
||||
const newVisibility =
|
||||
typeof updaterOrValue === 'function'
|
||||
? updaterOrValue(props.columnVisibility)
|
||||
: updaterOrValue
|
||||
emit('update:columnVisibility', newVisibility)
|
||||
},
|
||||
// Re-add the onRowSelectionChange callback but make it work with our custom logic
|
||||
onRowSelectionChange: (updaterOrValue) => {
|
||||
const newSelection =
|
||||
typeof updaterOrValue === 'function'
|
||||
? updaterOrValue(rowSelection.value)
|
||||
: updaterOrValue
|
||||
rowSelection.value = newSelection
|
||||
},
|
||||
state: {
|
||||
get sorting() {
|
||||
return props.sorting
|
||||
},
|
||||
get columnVisibility() {
|
||||
return props.columnVisibility
|
||||
},
|
||||
get rowSelection() {
|
||||
return rowSelection.value
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const handleRowClick = (shot: Shot, event: MouseEvent, row: any) => {
|
||||
// If double-click handler will handle it, skip selection logic
|
||||
if (event.detail === 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we clicked on an interactive element (simplified check)
|
||||
const target = event.target as HTMLElement
|
||||
if (target) {
|
||||
// Check if we clicked on a button, checkbox, or other interactive element
|
||||
const interactiveElement = target.closest('button, input, select, textarea, a[href], [role="button"], [role="menuitem"]')
|
||||
if (interactiveElement) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle selection based on modifier keys
|
||||
handleRowSelection(row, event)
|
||||
emit('row-click', shot, event)
|
||||
}
|
||||
|
||||
const handleRowSelection = (row: any, event: MouseEvent) => {
|
||||
const currentIndex = row.index
|
||||
const allRows = table.getRowModel().rows
|
||||
const shotId = String(row.id)
|
||||
|
||||
if (event.shiftKey && lastSelectedIndex.value !== null) {
|
||||
// Prevent text selection when shift-clicking
|
||||
event.preventDefault()
|
||||
|
||||
// Range selection
|
||||
const startIndex = Math.min(lastSelectedIndex.value, currentIndex)
|
||||
const endIndex = Math.max(lastSelectedIndex.value, currentIndex)
|
||||
|
||||
// Create new selection object
|
||||
const newSelection: Record<string, boolean> = {}
|
||||
|
||||
// Select all rows in the range
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
if (allRows[i]) {
|
||||
newSelection[allRows[i].id] = true
|
||||
}
|
||||
}
|
||||
|
||||
rowSelection.value = newSelection
|
||||
lastSelectedIndex.value = currentIndex
|
||||
} else if (event.ctrlKey || event.metaKey) {
|
||||
// Ctrl/Cmd + Click: Toggle individual selection (additional selection)
|
||||
const newSelection: Record<string, boolean> = { ...rowSelection.value }
|
||||
|
||||
if (newSelection[shotId]) {
|
||||
// Row is selected, deselect it
|
||||
delete newSelection[shotId]
|
||||
} else {
|
||||
// Row is not selected, select it
|
||||
newSelection[shotId] = true
|
||||
}
|
||||
|
||||
rowSelection.value = newSelection
|
||||
lastSelectedIndex.value = currentIndex
|
||||
} else {
|
||||
// Default: Single selection (clear others and select this one)
|
||||
// This applies even if the row is already selected - it becomes the only selection
|
||||
rowSelection.value = { [shotId]: true }
|
||||
lastSelectedIndex.value = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
// Detect if this is a range selection operation
|
||||
if (event.shiftKey) {
|
||||
isRangeSelecting.value = true
|
||||
// Prevent text selection immediately
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// Reset range selecting state
|
||||
isRangeSelecting.value = false
|
||||
}
|
||||
|
||||
// Watch rowSelection changes and emit selection-change events
|
||||
watch(
|
||||
rowSelection,
|
||||
(newSelection) => {
|
||||
emit('update:rowSelection', newSelection)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Prevent text selection during range selection */
|
||||
.table-row-selectable.selecting {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Prevent text selection on shift key operations */
|
||||
.table-row-selectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Allow text selection for specific elements that should be selectable */
|
||||
.table-row-selectable input,
|
||||
.table-row-selectable textarea,
|
||||
.table-row-selectable [contenteditable] {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user