<script setup lang="ts"> import type { ITableProps } from '@interfaces/componentsProps'; import { computed, ref, watch } from 'vue'; import { convertThemeToColor, convertThemeToSecondaryColor, convertThemeToTextColor } from '@helpers/common'; import type { ITableItem } from '@interfaces/componentsProp'; import FilterIcon from '@stories/icons/Mono/FilterIcon.vue'; import SortDownIcon from '@stories/icons/Mono/SortDownIcon.vue'; import SortUpIcon from '@stories/icons/Mono/SortUpIcon.vue'; import SortVerticalIcon from '@stories/icons/Mono/SortVerticalIcon.vue'; import { calcColumnPadding, calcRows } from '@stories/components/Table/helpers'; import Popup from '@stories/components/Popup/Popup.vue'; import Button from '@stories/components/Button/Button.vue'; import CheckMarkIcon from '@stories/icons/Mono/CheckMarkIcon.vue'; import CrossIcon from '@stories/icons/Mono/CrossIcon.vue'; const props = withDefaults(defineProps<ITableProps>(), { theme: 'white', darknessTheme: '500', fontSize: '16px', }); const data = defineModel<ITableItem[][]>('data'); const columns = ref(props.columns); const sortStateActive = ref<[number, string] | []>([]); const isFilterPopup = ref<boolean>(false); const columnToFilter = ref<number>(0); const filterValue = ref<string>(''); watch(props.columns, () => (columns.value = props.columns)); const initGap = computed( () => props.gap ?? (!props.fontSize || isNaN(+props.fontSize.slice(0, -3)) ? '5px' : parseInt(props.fontSize) < 20 ? '5px' : parseInt(props.fontSize) < 36 ? '10px' : '15px'), ); const iconSize = computed(() => { const twoLetters = props.fontSize.slice(0, -2); const threeLetters = props.fontSize.slice(0, -3); return !twoLetters || isNaN(+twoLetters) ? (!threeLetters || isNaN(+threeLetters) ? '16' : threeLetters) : twoLetters; }); const themeColor = computed(() => convertThemeToColor(props.theme, props.darknessTheme)); const color = computed(() => props.textColor ? convertThemeToColor(props.textColor, props.darknessTextColor) : convertThemeToTextColor(props.theme, props.darknessTheme), ); const secondaryColor = computed(() => convertThemeToSecondaryColor(props.theme, props.darknessTheme)); const darkCellColor = computed(() => convertThemeToSecondaryColor(props.theme, String(+props.darknessTheme + 300))); // ['', 'up', 'none', '', 'none', ...] const sortState = computed<string[]>(() => { const result = []; for (const column of columns.value) { result.push(column.sortable ? (column.initSort ?? 'none') : ''); } return result; }); const rows = computed<ITableItem[][]>(() => calcRows(data.value!, sortStateActive.value, props.multipleSort, columnToFilter.value, filterValue.value), ); const changeColumnSortMode = (index: number) => { const cur = sortState.value[index]; const newValue = cur === 'none' ? 'down' : cur === 'down' ? 'up' : 'none'; if (cur === 'up') { sortStateActive.value = []; } else { sortStateActive.value[0] = index; sortStateActive.value[1] = newValue; } if (!props.multipleSort) columns.value.forEach((column) => (column.initSort = 'none')); columns.value[index].initSort = newValue; }; const setFilter = (column: number) => { if (columnToFilter.value === column || !isFilterPopup.value) { isFilterPopup.value = !isFilterPopup.value; } if (columnToFilter.value !== column) { columnToFilter.value = column; } }; const calcLeft = (selector: string) => { const el = document.querySelector(selector); const table = document.querySelector('#table')!; if (!el) return 0; return el.getBoundingClientRect().left - table.getBoundingClientRect().left + +iconSize.value; }; const cancelFilter = () => { filterValue.value = ''; isFilterPopup.value = false; }; </script> <template> <div> <table :class="{ tableLines: showAllLines, }" :style="`background-color: ${themeColor}; color: ${color}`" id="table" > <thead> <tr> <th :class="{ leftBorder: showAllLines, }" v-for="(column, index) of columns" :key="column.name" class="columnHeader" :style="`padding: calc(${initGap} / 2) ${initGap}`" > <div :style="`justify-content: ${center ? 'center' : 'start'}; gap: ${center ? '0' : initGap}; padding: ${calcColumnPadding(column, props.center, initGap)}`" class="columnFlex" > <div class="columnHeader-container"> <h3> {{ column.name }} </h3> <button v-if="column.sortable" @click.prevent="changeColumnSortMode(index)" style="min-width: 20px; min-height: 20px" > <SortVerticalIcon v-show="sortState[index] === 'none'" :color="color" :size="iconSize" /> <SortDownIcon v-show="sortState[index] === 'down'" :color="color" :size="iconSize" /> <SortUpIcon v-show="sortState[index] === 'up'" :color="color" :size="iconSize" /> </button> <button v-if="column.filterable" @pointerdown="setFilter(index)" :id="`filter${column.name}`" style="position: relative" > <FilterIcon :color="color" :size="iconSize" /> </button> </div> <div v-if="!center"></div> </div> </th> <Popup v-model:active="isFilterPopup" :parentSelector="`#filter${columnToFilter}`" buttonMenu :theme="theme" :top="+iconSize + 10" :left="calcLeft(`#filter${columnToFilter}`)" > <input v-model="filterValue" type="text" class="filterInput" :style="`background-color: ${themeColor}; color: ${color}`" /> <section class="filterButtons"> <Button iconOnly size="small" theme="green" @click.prevent="isFilterPopup = false"> <CheckMarkIcon color="white" size="20" /> </Button> <Button iconOnly size="small" theme="red" @click.prevent="cancelFilter"> <CrossIcon color="white" size="20" /> </Button> </section> </Popup> </tr> </thead> <tbody> <tr v-for="(row, index) of rows" :key="index"> <td :class="{ leftBorder: showAllLines, darkRow: stripedRows && index % 2, }" v-for="item of row" :key="item.value" :style="`padding: calc(${initGap} / 2) ${initGap}; text-align: ${center ? 'center' : 'start'}`" > {{ item.value }} </td> </tr> </tbody> </table> </div> </template> <style scoped> table { border-collapse: collapse; } table * { font-size: v-bind(fontSize); } tr { position: relative; } tr::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 1px; background-color: v-bind(secondaryColor); } .columnFlex { display: flex; font-weight: bold; } .columnHeader-container { display: flex; align-items: center; gap: 10px; } .tableLines { border-top: 1px solid v-bind(secondaryColor); border-right: 1px solid v-bind(secondaryColor); } .leftBorder { border-left: 1px solid v-bind(secondaryColor); } .darkRow { background-color: v-bind(darkCellColor); } .filterInput { width: 150px; padding: 5px; margin-bottom: 5px; border: 2px solid #64748b; border-radius: 5px; } .filterButtons { display: flex; justify-content: space-between; } </style>