| 1 |
import { computed, ref, watch, type ComputedRef, type Ref } from 'vue' |
| 2 |
import { useResizeObserver } from '@vueuse/core' |
| 3 |
|
| 4 |
export interface WidthColumn { |
| 5 |
visible?: boolean |
| 6 |
showInModal?: boolean |
| 7 |
width?: string | number |
| 8 |
minWidth?: string | number |
| 9 |
} |
| 10 |
|
| 11 |
export interface DynamicWidthContext { |
| 12 |
dynamicWidth: number | null |
| 13 |
remainingWidth: number |
| 14 |
totalTableWidth: number |
| 15 |
} |
| 16 |
|
| 17 |
export interface UseTableWidthOptions<T extends WidthColumn> { |
| 18 |
columns: Ref<T[]> | ComputedRef<T[]> |
| 19 |
isDynamic?: (col: T) => boolean |
| 20 |
widthFor?: (col: T, ctx: DynamicWidthContext) => number | null |
| 21 |
minWidthFilter?: (col: T) => boolean |
| 22 |
} |
| 23 |
|
| 24 |
type TableRefLike = HTMLElement | { $el?: HTMLElement | null } | null | undefined |
| 25 |
|
| 26 |
function getNumericColumnWidth(width: WidthColumn['width']): number { |
| 27 |
if (typeof width === 'number') return width |
| 28 |
if (typeof width === 'string') { |
| 29 |
const parsed = parseFloat(width) |
| 30 |
return Number.isFinite(parsed) ? parsed : 0 |
| 31 |
} |
| 32 |
return 0 |
| 33 |
} |
| 34 |
|
| 35 |
export function useTableWidth<T extends WidthColumn>( |
| 36 |
tableRef: Ref<TableRefLike>, |
| 37 |
opts: UseTableWidthOptions<T>, |
| 38 |
) { |
| 39 |
const tableWidth = ref(0) |
| 40 |
|
| 41 |
const isDynamic = opts.isDynamic ?? ((col: T) => col.showInModal === true) |
| 42 |
const minWidthFilter = opts.minWidthFilter ?? (() => true) |
| 43 |
|
| 44 |
const containerEl = computed<HTMLElement | null>(() => { |
| 45 |
const value = tableRef.value |
| 46 |
if (!value) return null |
| 47 |
if (value instanceof HTMLElement) return value.parentElement |
| 48 |
const el = (value as { $el?: HTMLElement | null }).$el |
| 49 |
return el?.parentElement ?? null |
| 50 |
}) |
| 51 |
|
| 52 |
const measureContainerWidth = (el: HTMLElement | null) => { |
| 53 |
if (el) { |
| 54 |
tableWidth.value = el.clientWidth |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
watch(containerEl, measureContainerWidth, { flush: 'post', immediate: true }) |
| 59 |
|
| 60 |
useResizeObserver(containerEl, (entries) => { |
| 61 |
for (const entry of entries) { |
| 62 |
tableWidth.value = entry.contentRect.width |
| 63 |
} |
| 64 |
}) |
| 65 |
|
| 66 |
const dynamicWidthColumns: ComputedRef<T[]> = computed(() => { |
| 67 |
const visibleColumns = opts.columns.value.filter((col) => col.visible ?? true) |
| 68 |
const modalColumns = visibleColumns.filter((col) => isDynamic(col)) |
| 69 |
const totalTableWidth = tableWidth.value || 0 |
| 70 |
const totalFixedWidth = visibleColumns |
| 71 |
.filter((col) => !isDynamic(col)) |
| 72 |
.reduce((sum, col) => sum + getNumericColumnWidth(col.width), 0) |
| 73 |
|
| 74 |
const remainingWidth = totalTableWidth - totalFixedWidth |
| 75 |
const dynamicWidth = |
| 76 |
remainingWidth > 0 && modalColumns.length > 0 ? remainingWidth / modalColumns.length : null |
| 77 |
|
| 78 |
const ctx: DynamicWidthContext = { dynamicWidth, remainingWidth, totalTableWidth } |
| 79 |
|
| 80 |
return visibleColumns.map((col) => { |
| 81 |
if (opts.widthFor) { |
| 82 |
const override = opts.widthFor(col, ctx) |
| 83 |
if (override !== null && override !== undefined) { |
| 84 |
return { ...col, width: override } |
| 85 |
} |
| 86 |
} |
| 87 |
if (isDynamic(col) && dynamicWidth !== null) { |
| 88 |
return { ...col, width: Math.floor(dynamicWidth) } |
| 89 |
} |
| 90 |
return col |
| 91 |
}) |
| 92 |
}) |
| 93 |
|
| 94 |
const minTableWidth: ComputedRef<number> = computed(() => { |
| 95 |
return opts.columns.value.reduce((sum, col) => { |
| 96 |
if (!minWidthFilter(col)) return sum |
| 97 |
const value = |
| 98 |
typeof col.minWidth === 'number' ? col.minWidth : parseFloat(String(col.minWidth)) || 0 |
| 99 |
return sum + value |
| 100 |
}, 0) |
| 101 |
}) |
| 102 |
|
| 103 |
return { tableWidth, dynamicWidthColumns, minTableWidth } |
| 104 |
} |
| 105 |
|