Last active
September 19, 2023 19:39
-
-
Save sean-brydon/ddd428ee1012360936556da5d0016c49 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div :class="wrapperClass" v-bind="attrs"> | |
<table :class="['relative overflow-x-auto', 'divide-y divide-gray-300 dark:divide-gray-700']"> | |
<thead class=""> | |
<tr class=""> | |
<th v-if="modelValue" scope="col" :class="['ps-4']"> | |
<input type="checkbox" :checked="indeterminate || selected.length === rows.length" @change="onChange" /> | |
</th> | |
<th v-for="(column, index) in columns" :key="index" scope="col" class="" | |
:class="['text-left rtl:text-right px-3 py-3.5 text-gray-900 dark:text-white font-semibold text-sm', column.class]"> | |
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort"> | |
<button @click="onSort(column)"> | |
{{ (!sort.column || sort.column !== column.key) ? "" : | |
sort.direction === 'asc' ? "▲" : "▼" }} | |
<span>{{ column[columnAttribute] }}</span> | |
</button> | |
</slot> | |
</th> | |
</tr> | |
</thead> | |
<tbody class="divide-y divide-gray-200 dark:divide-gray-800"> | |
<tr v-if="loading"> | |
<td :colspan="columns.length + (modelValue ? 1 : 0)"> | |
<slot name="loading-state"> | |
<div class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14"> | |
<p class="text-sm text-center text-gray-900 dark:text-white"> | |
DUDE WE"RE LOADING ALL THE PEOPLE | |
</p> | |
</div> | |
</slot> | |
</td> | |
</tr> | |
<tr v-if="!rows.length"> | |
<td :colspan="columns.length + (modelValue ? 1 : 0)"> | |
<slot name="empty-state"> | |
<div class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14"> | |
<p class="text-sm text-center text-gray-900 dark:text-white"> | |
UH OH YOURE FUCKING EMPTY | |
</p> | |
</div> | |
</slot> | |
</td> | |
</tr> | |
<template v-if="!loading && rows.length > 0"> | |
<tr v-for="(row, index) in rows " :key="index" | |
:class="['', isSelected(row) && 'bg-gray-50 dark:bg-gray-800/50', $attrs.onSelect && 'hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer']" | |
@click="() => onSelect(row)"> | |
<td v-if="modelValue" class="ps-4"> | |
<input type="checkbox" :checked="isSelected(row)" @click.stop /> | |
</td> | |
<td v-for="(column, subIndex) in columns " :key="subIndex" | |
:class="['', 'px-3 py-4', 'text-gray-500 dark:text-gray-400', 'text-sm']"> | |
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" | |
:get-row-data="(defaultValue: any) => getRowData(row, column.key, defaultValue)"> | |
{{ getRowData(row, column.key) }} | |
</slot> | |
</td> | |
</tr> | |
</template> | |
</tbody> | |
</table> | |
</div> | |
</template> | |
<script lang="ts"> | |
import { ref, computed, defineComponent, toRaw } from 'vue' | |
import type { PropType } from 'vue' | |
import { twMerge } from 'tailwind-merge' | |
import { defu } from 'defu' | |
function omit<T extends object, K extends keyof T>(object: T, keys: K[]): Omit<T, K> { | |
const result = {} as Omit<T, K>; | |
for (const key in object) { | |
if (!keys.includes(key as unknown as K)) { | |
// @ts-ignore | |
result[key] = object[key]; | |
} | |
} | |
return result; | |
} | |
function get<T, K extends keyof T>(object: T, path: string, defaultValue?: any): any { | |
const keys = path.split('.'); | |
let result: any = object; | |
for (const key of keys) { | |
if (result && key in result) { | |
result = result[key]; | |
} else { | |
return defaultValue; | |
} | |
} | |
return result; | |
} | |
function upperFirst(str: string): string { | |
if (str.length === 0) { | |
return str; | |
} | |
return str.charAt(0).toUpperCase() + str.slice(1); | |
} | |
// const appConfig = useAppConfig() | |
function defaultComparator<T>(a: T, z: T): boolean { | |
return a === z | |
} | |
export default defineComponent({ | |
inheritAttrs: false, | |
props: { | |
modelValue: { | |
type: Array, | |
default: null | |
}, | |
by: { | |
type: [String, Function], | |
default: () => defaultComparator | |
}, | |
rows: { | |
type: Array as PropType<{ [key: string]: any, click?: Function }[]>, | |
default: () => [] | |
}, | |
columns: { | |
type: Array as PropType<{ key: string, sortable?: boolean, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>, | |
default: null | |
}, | |
columnAttribute: { | |
type: String, | |
default: 'label' | |
}, | |
sort: { | |
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>, | |
default: () => ({}) | |
}, | |
loading: { | |
type: Boolean, | |
default: false | |
}, | |
// emptyState: { | |
// type: Object as PropType<{ icon: string, label: string }>, | |
// default: () => appConfig.ui.table.default.emptyState | |
// }, | |
}, | |
emits: ['update:modelValue'], | |
setup(props, { emit, attrs }) { | |
const wrapperClass = computed(() => twMerge("", attrs.class as string)) | |
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: upperFirst(key), sortable: false }))) | |
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' })) | |
const rows = computed(() => { | |
if (!sort.value?.column) { | |
return props.rows | |
} | |
const { column, direction } = sort.value | |
return props.rows.slice().sort((a, b) => { | |
const aValue = a[column] | |
const bValue = b[column] | |
if (aValue === bValue) { | |
return 0 | |
} | |
if (direction === 'asc') { | |
return aValue < bValue ? -1 : 1 | |
} else { | |
return aValue > bValue ? -1 : 1 | |
} | |
}) | |
}) | |
const selected = computed({ | |
get() { | |
return props.modelValue | |
}, | |
set(value) { | |
emit('update:modelValue', value) | |
} | |
}) | |
const indeterminate = computed(() => selected.value && selected.value.length > 0 && selected.value.length < props.rows.length) | |
function compare(a: any, z: any) { | |
if (typeof props.by === 'string') { | |
const property = props.by as unknown as any | |
return a?.[property] === z?.[property] | |
} | |
return props.by(a, z) | |
} | |
function isSelected(row: any) { | |
if (!props.modelValue) { | |
return false | |
} | |
return selected.value.some((item) => compare(toRaw(item), toRaw(row))) | |
} | |
function onSort(column: { key: string, direction?: 'asc' | 'desc' }) { | |
if (sort.value.column === column.key) { | |
const direction = !column.direction || column.direction === 'asc' ? 'desc' : 'asc' | |
if (sort.value.direction === direction) { | |
sort.value = defu({}, props.sort, { column: null, direction: 'asc' }) | |
} else { | |
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc' | |
} | |
} else { | |
sort.value = { column: column.key, direction: column.direction || 'asc' } | |
} | |
} | |
function onSelect(row: any) { | |
if (!attrs.onSelect) { | |
return | |
} | |
// @ts-ignore | |
attrs.onSelect(row) | |
} | |
function selectAllRows() { | |
props.rows.forEach((row) => { | |
// If the row is already selected, don't select it again | |
if (selected.value.some((item) => compare(toRaw(item), toRaw(row)))) { | |
return | |
} | |
// @ts-ignore | |
attrs.onSelect ? attrs.onSelect(row) : selected.value.push(row) | |
}) | |
} | |
function onChange(event: any) { | |
if (event.target.checked) { | |
selectAllRows() | |
} else { | |
selected.value = [] | |
} | |
} | |
function getRowData(row: Object, rowKey: string | string[], defaultValue: any = 'Failed to get cell value') { | |
return get(row, Array.isArray(rowKey) ? rowKey[0] : rowKey, defaultValue) | |
} | |
return { | |
attrs: computed(() => omit(attrs, ['class'])), | |
// eslint-disable-next-line vue/no-dupe-keys | |
wrapperClass, | |
// eslint-disable-next-line vue/no-dupe-keys | |
sort, | |
// eslint-disable-next-line vue/no-dupe-keys | |
columns, | |
// eslint-disable-next-line vue/no-dupe-keys | |
rows, | |
selected, | |
indeterminate, | |
isSelected, | |
onSort, | |
onSelect, | |
onChange, | |
getRowData | |
} | |
} | |
}) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment