import { computed, ref, toValue, watch, type MaybeRefOrGetter } from 'vue' import type { DataTableHeaders, TableColumnHeader, TableFilterInputConfig } from './types' import { sortHeaders } from './organizeColumns/sortHeaders' /** * Composable for merging, normalizing and enhancing table headers. * * @param headersProp - Headers provided by the table props. * @param storedHeaders - Optional headers restored from table state persistence. When both * sources are defined, persisted headers provide the base order and prop values override * matching headers. This value is not reactive because persistence is initialized once. * @param filterInputConfig - Optional filter-input configuration indexed by column key. * @returns Reactive header collections and utilities for retrieving or enhancing headers. */ export function useTableHeaders({ headersProp, storedHeaders, filterInputConfig = {}, componentName = 'SyTable', }: { headersProp: MaybeRefOrGetter storedHeaders?: DataTableHeaders[] filterInputConfig?: MaybeRefOrGetter | undefined> componentName?: string }) { function normalizeHeader(header: DataTableHeaders): DataTableHeaders { const mapped = { ...header, title: header.title ?? header.text, } as TableColumnHeader // Si maxWidth est défini sans width → applique-le à width if (mapped.maxWidth != null && mapped.width == null) { mapped.width = mapped.maxWidth } // Si maxWidth est défini → l'appliquer aussi aux via cellProps if (mapped.maxWidth != null) { const existingCellProps = (mapped.cellProps ?? {}) as Record const existingStyle = (existingCellProps.style ?? {}) as Record const multilineStyle = { ...existingStyle, maxWidth: mapped.maxWidth, whiteSpace: 'normal', overflowWrap: 'anywhere', wordBreak: 'break-word', } const baseProps = { ...existingCellProps, style: multilineStyle } mapped.cellProps = () => ({ ...baseProps, }) } return mapped as unknown as DataTableHeaders } const mergedHeaders = computed(() => { const incoming = toValue(headersProp) const stored = Array.isArray(storedHeaders) ? storedHeaders : undefined // Si aucun header, rien à faire if (!Array.isArray(incoming) && !Array.isArray(stored)) { return undefined } // Fusion logique entre storedHeaders et incoming let headers: DataTableHeaders[] = [] if (stored && incoming) { // Fusionne les headers existants avec les nouveaux const merged = stored.map((storedHeader) => { const match = incoming.find( h => (h.key && h.key === storedHeader.key) || (h.value && h.value === storedHeader.value), ) return { ...storedHeader, ...(match || {}), // les nouveaux props écrasent les anciens } }) // Ajoute les headers qui n'existent pas encore dans le stockage const extras = incoming.filter( h => !merged.some( x => (x.key && x.key === h.key) || (x.value && x.value === h.value), ), ) headers = [...merged, ...extras] } else { headers = (stored ?? incoming) as DataTableHeaders[] } return headers }) // Process headers to ensure they have title property and regenerate cellProps after reordering const normalizedHeaders = computed(() => mergedHeaders.value?.map(normalizeHeader)) /** * Enhance header with filter type and configuration */ function getEnhancedHeader(column: TableColumnHeader): TableColumnHeader { // If column is not filterable, return as is if (!column.filterable) return column // Default filter type is 'text' if not specified const filterType = column.filterType || 'text' // Get column-specific filter config or empty object const columnIdentifier = typeof column.key === 'string' && column.key ? column.key : typeof column.value === 'string' && column.value ? column.value : undefined const filterConfig = toValue(filterInputConfig) ?? {} const columnFilterConfig = columnIdentifier && filterConfig[columnIdentifier] ? filterConfig[columnIdentifier] : {} // Return enhanced header with filter properties return { ...(column as object), filterType, filterConfig: columnFilterConfig as TableFilterInputConfig, } as TableColumnHeader } // Mutable internal headers to manage state inside the component const internalHeaders = ref([]) watch(normalizedHeaders, (newHeaders) => { // Update internal headers when props.headers changes internalHeaders.value = newHeaders }, { immediate: true, deep: true }) const normalizedInternalHeaders = computed(() => internalHeaders.value?.map(normalizeHeader)) // Get filterable headers const filterableHeaders = computed(() => { if (!normalizedInternalHeaders.value) return [] return normalizedInternalHeaders.value.filter(header => header.filterable) }) // `filterInputConfig` doit être indexé par clé de colonne : avertit une seule fois si l'ancien format (options à la racine) est détecté let hasWarnedInvalidFilterInputConfig = false watch(filterableHeaders, (headers) => { if (hasWarnedInvalidFilterInputConfig) return const configKeys = Object.keys(toValue(filterInputConfig) ?? {}) if (configKeys.length === 0 || headers.length === 0) return const columnIdentifiers = new Set( headers .map(header => (typeof header.key === 'string' && header.key) || (typeof header.value === 'string' && header.value)) .filter((identifier): identifier is string => !!identifier), ) const hasUnknownKey = configKeys.some(key => !columnIdentifiers.has(key)) if (hasUnknownKey) { hasWarnedInvalidFilterInputConfig = true // eslint-disable-next-line no-console console.warn( `[${componentName}] La prop \`filterInputConfig\` doit être indexée par la clé (ou value) de chaque colonne filtrable, ex. { [columnKey]: { maxlength: 10 } }. ` + 'L\'ancien format (options placées directement à la racine) n\'est plus supporté.', ) } }, { immediate: true }) /** * Get header by key */ function getHeaderByKey(key: string): TableColumnHeader | undefined { return normalizedInternalHeaders.value?.find(header => header.key === key) } /** * Get original header from a rendered column (match by key or value) */ function getHeaderForColumn(column: TableColumnHeader): TableColumnHeader | undefined { if (!normalizedInternalHeaders.value) return undefined const key = column.key ?? undefined if (key) { const byKey = normalizedInternalHeaders.value.find(h => h.key === key) if (byKey) return byKey } // Fallback: try matching by value when key is not present or didn't match const val = typeof column.value === 'string' ? column.value : undefined if (val) { const byValue = normalizedInternalHeaders.value.find(h => h.value === val) if (byValue) return byValue } return undefined } /** * The headers filtered by visibility and sorted by order */ const displayHeaders = computed(() => { if (!normalizedInternalHeaders.value) return undefined const filteredHeaders = normalizedInternalHeaders.value.filter(header => header.hidden !== true) return sortHeaders(filteredHeaders) }) return { headers: internalHeaders, displayHeaders, filterableHeaders, getEnhancedHeader, getHeaderByKey, getHeaderForColumn, } }