diff --git a/src/Playground.vue b/src/Playground.vue index d00a069217ea00c96dda7f00e6acd77e384f0671..88da18788043fdcd5754428403808f50065c6932 100644 --- a/src/Playground.vue +++ b/src/Playground.vue @@ -91,8 +91,9 @@ const tableColumns: ITableColumn[] = [ }, { name: 'Age', - type: 'text', + type: 'number', filterable: true, + sortable: true, }, { name: 'Hobbies', @@ -126,7 +127,7 @@ const tableData = [ value: 'John', }, { - value: '25', + value: '7', }, { value: 'Football', @@ -182,6 +183,15 @@ const tableData = [ <template> <h2 class="title gradient-text">Playground</h2> + <Table + show-all-lines + :columns="tableColumns" + darknessTextColor="500" + :data="tableData" + fontSize="36px" + theme="black" + stripedRows + ></Table> <Table show-all-lines :columns="tableColumns" diff --git a/src/common/interfaces/componentsProp.ts b/src/common/interfaces/componentsProp.ts index 269998ab438077d4179cf6f5dd8ee98009bc379a..5cf8fb55269a88c7ea24f486427764299ae3a293 100644 --- a/src/common/interfaces/componentsProp.ts +++ b/src/common/interfaces/componentsProp.ts @@ -20,7 +20,6 @@ export type TTableColumnType = | 'number' | 'text' | 'date' - | 'tag' | 'select' | 'rating' | 'progressBar' diff --git a/src/stories/components/Popup/Popup.vue b/src/stories/components/Popup/Popup.vue index 839e36245ea5ce9ed2e9392b4c3ae2cb461b05d7..dc2b20f8a159f86650aa9df2068a2553fa2d3755 100644 --- a/src/stories/components/Popup/Popup.vue +++ b/src/stories/components/Popup/Popup.vue @@ -22,7 +22,7 @@ const left = ref(); const isOnContainerClick = ref(); const parent = computed(() => props.parentSelector); -const container = ref(document.querySelector(props.parentSelector)); +const container: Ref = ref(document.querySelector(props.parentSelector)); if (!container.value) { setTimeout(() => { container.value = document.querySelector(props.parentSelector); @@ -37,7 +37,6 @@ watch( const clientRect = container.value?.getBoundingClientRect(); top.value = props.top ?? clientRect.top; left.value = props.left ?? clientRect.left; - console.log('left: ', left.value); } container.value.addEventListener('pointerdown', (event: Event) => { @@ -72,7 +71,7 @@ watch( @pointerdown.stop="" :style="`top: ${top}px; left: ${left}px; opacity: ${active ? 1 : 0}; pointer-events: ${active ? 'auto' : 'none'}; padding: ${padding}`" > - <div :style="`max-width: ${maxWidth}; max-height: ${maxHeight}; overflow: auto; padding-right: 5px`"> + <div :style="`max-width: ${maxWidth}; max-height: ${maxHeight}; overflow: auto`"> <slot /> <p v-if="!$slots.default" style="background-color: black; color: white; padding: 10px">Popup</p> </div> diff --git a/src/stories/components/Table/Table.stories.ts b/src/stories/components/Table/Table.stories.ts index 88f41701bb88b773b45827a42b64c46d3d4cae7c..7d417174a86572a2a1ff3d799847347b471d6cec 100644 --- a/src/stories/components/Table/Table.stories.ts +++ b/src/stories/components/Table/Table.stories.ts @@ -84,7 +84,7 @@ export const Simple: Story = { }, { name: 'Age', - type: 'text', + type: 'number', }, { name: 'Hobbies', @@ -128,7 +128,8 @@ export const Full: Story = { }, { name: 'Age', - type: 'text', + type: 'number', + sortable: true, filterable: true, }, { @@ -163,7 +164,7 @@ export const Full: Story = { value: 'John', }, { - value: '25', + value: '7', }, { value: 'Football', diff --git a/src/stories/components/Table/Table.vue b/src/stories/components/Table/Table.vue index 46697703025345245e9ed7dfd17f4c348cea70eb..9fd0ddb72879a40531f789e466c7b9874d44137f 100644 --- a/src/stories/components/Table/Table.vue +++ b/src/stories/components/Table/Table.vue @@ -3,15 +3,8 @@ 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'; +import { calcGap, calcRows } from '@stories/components/Table/helpers'; +import TableHeader from '@stories/components/Table/TableHeader.vue'; const props = withDefaults(defineProps<ITableProps>(), { theme: 'white', @@ -22,28 +15,14 @@ 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 isFilterPopup = ref<boolean>(false); const filterValue = ref<string>(''); +const isRegisterSensitive = ref<boolean>(false); 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 initGap = computed(() => calcGap(props.gap, props.fontSize)); const themeColor = computed(() => convertThemeToColor(props.theme, props.darknessTheme)); const color = computed(() => props.textColor @@ -52,7 +31,6 @@ const color = computed(() => ); 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 = []; @@ -61,9 +39,15 @@ const sortState = computed<string[]>(() => { } return result; }); - const rows = computed<ITableItem[][]>(() => - calcRows(data.value!, sortStateActive.value, props.multipleSort, columnToFilter.value, filterValue.value), + calcRows( + data.value!, + sortStateActive.value, + props.multipleSort, + props.columns[sortStateActive.value[0] ?? 0].type, + filterValue.value, + isRegisterSensitive.value, + ), ); const changeColumnSortMode = (index: number) => { @@ -83,15 +67,10 @@ const setFilter = (column: number) => { isFilterPopup.value = !isFilterPopup.value; } if (columnToFilter.value !== column) { + filterValue.value = ''; 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; @@ -108,69 +87,25 @@ const cancelFilter = () => { 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> + <TableHeader + v-model:filterValue="filterValue" + v-model:isFilterPopup="isFilterPopup" + v-model:isRegisterSensitive="isRegisterSensitive" + :columns="columns" + :sortState="sortState" + :columnToFilter="columnToFilter" + :initGap="initGap" + :theme="theme" + :themeColor="themeColor" + :secondaryColor="secondaryColor" + :color="color" + :showAllLines="!!showAllLines" + :center="!!center" + :fontSize="fontSize" + @changeColumnSortMode="changeColumnSortMode" + @setFilter="setFilter" + @cancelFilter="cancelFilter" + /> </thead> <tbody> <tr v-for="(row, index) of rows" :key="index"> @@ -210,15 +145,6 @@ tr::after { 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); @@ -229,15 +155,4 @@ tr::after { .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> diff --git a/src/stories/components/Table/TableHeader.vue b/src/stories/components/Table/TableHeader.vue new file mode 100644 index 0000000000000000000000000000000000000000..074eed4faee066d8ac73e80878d625bc9bc1a4ab --- /dev/null +++ b/src/stories/components/Table/TableHeader.vue @@ -0,0 +1,150 @@ +<script setup lang="ts"> +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 } 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'; +import type { TThemeColor } from '@interfaces/common'; +import type { ITableColumn } from '@interfaces/componentsProp'; +import { computed } from 'vue'; + +interface Props { + columns: ITableColumn[]; + sortState: string[]; + columnToFilter: number; + initGap: string; + theme: TThemeColor; + themeColor: string; + secondaryColor: string; + color: string; + showAllLines: boolean; + center: boolean; + fontSize: string; +} +const props = defineProps<Props>(); +const emit = defineEmits(['changeColumnSortMode', 'setFilter', 'cancelFilter']); +const filterValue = defineModel<string>('filterValue'); +const isFilterPopup = defineModel<boolean, string, boolean, boolean>('isFilterPopup'); +const isRegisterSensitive = defineModel<boolean, string, boolean, boolean>('isRegisterSensitive'); + +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 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 isColumnTypeText = computed(() => props.columns[props.columnToFilter].type === 'text'); +</script> + +<template> + <tr> + <th + :class="{ + leftBorder: showAllLines, + }" + v-for="(column, index) of columns" + :key="column.name" + :style="`padding: calc(${initGap} / 2) ${initGap}`" + > + <div + :style="`justify-content: ${center ? 'center' : 'start'}; gap: ${center ? '0' : initGap}; padding: ${calcColumnPadding(column, center, initGap)}`" + class="columnFlex" + > + <div class="columnHeader-container"> + <h3> + {{ column.name }} + </h3> + <button + v-if="column.sortable" + @click.prevent="emit('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="emit('setFilter', index)" + :id="`filter${index}`" + 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}`)" + maxHeight="200px" + > + <article style="padding: 2px"> + <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 + v-show="isColumnTypeText" + iconOnly + size="small" + theme="sky" + @click.prevent="isRegisterSensitive = !isRegisterSensitive" + > + <div style="width: 50px; font-size: 20px">{{ isRegisterSensitive ? 'A\u{2260}a' : 'A = a' }}</div> + </Button> + <Button iconOnly size="small" theme="red" @click.prevent="emit('cancelFilter')"> + <CrossIcon color="white" size="20" /> + </Button> + </section> + </article> + </Popup> + </tr> +</template> + +<style scoped> +.columnFlex { + display: flex; + font-weight: bold; +} +.columnHeader-container { + display: flex; + align-items: center; + gap: 10px; +} +.filterInput { + width: 150px; + padding: 5px; + margin-bottom: 5px; + border: 2px solid #64748b; + border-radius: 5px; +} +.filterButtons { + display: flex; + justify-content: space-between; +} +.leftBorder { + border-left: 1px solid v-bind(secondaryColor); +} +</style> diff --git a/src/stories/components/Table/helpers.ts b/src/stories/components/Table/helpers.ts index 4e5e9c10a9711ee8dfeccf6643a8425a17ad7b3f..4d7b2dee741fc3b20a3cf6ef300e702be5001d0c 100644 --- a/src/stories/components/Table/helpers.ts +++ b/src/stories/components/Table/helpers.ts @@ -1,17 +1,22 @@ -import type { ITableColumn, ITableItem } from '@interfaces/componentsProp'; +import type { ITableColumn, ITableItem, TTableColumnType } from '@interfaces/componentsProp'; export const calcRows = ( initRows: ITableItem[][], sortStateActive: [number, string] | [], multipleSort: boolean, - columnToFilter: number, + columnToFilterType: TTableColumnType, filterValue: string, + isRegisterSensitive: boolean, ) => { // ['up', 'down', ...] let rows = [...initRows]; + const sortIndex = sortStateActive[0]; - if (filterValue) { - rows = rows.filter((row) => row[columnToFilter].value.startsWith(filterValue)); + if (filterValue && sortIndex) { + rows = rows.filter((row) => { + const item = isRegisterSensitive ? row[sortIndex].value : row[sortIndex].value.toLowerCase(); + return item.startsWith(isRegisterSensitive ? filterValue : filterValue.toLowerCase()); + }); } if (!sortStateActive.length) return rows; @@ -36,11 +41,23 @@ export const calcRows = ( } else { const index = sortStateActive[0]; const value = sortStateActive[1]; + if (columnToFilterType === 'number') + return rows.sort((a, b) => + value === 'down' ? +a[index].value - +b[index].value : +b[index].value - +a[index].value, + ); return rows.sort((a, b) => value === 'down' ? a[index].value.localeCompare(b[index].value) : b[index].value.localeCompare(a[index].value), ); } }; +export const calcGap = (gap: string, fontSize: string) => + gap ?? + (!fontSize || isNaN(+fontSize.slice(0, -3)) || parseInt(fontSize) < 20 + ? '5px' + : parseInt(fontSize) < 36 + ? '10px' + : '15px'); + export const calcColumnPadding = (column: ITableColumn, center: boolean, gap: string) => center ? `0px calc(${gap} / 2 + ${column.padding ?? '0px'} / 2)` : `0 ${column.padding ?? '0px'} 0 0`;