Skip to content
Snippets Groups Projects
Commit ce8f2f96 authored by Дмитрий Малюгин's avatar Дмитрий Малюгин :clock4:
Browse files

feat: filter for Table and component 'TableHeader'

parent 209112d0
No related branches found
No related tags found
1 merge request!3Table (partially), Checkbox, Tag, Select and Knob
...@@ -91,8 +91,9 @@ const tableColumns: ITableColumn[] = [ ...@@ -91,8 +91,9 @@ const tableColumns: ITableColumn[] = [
}, },
{ {
name: 'Age', name: 'Age',
type: 'text', type: 'number',
filterable: true, filterable: true,
sortable: true,
}, },
{ {
name: 'Hobbies', name: 'Hobbies',
...@@ -126,7 +127,7 @@ const tableData = [ ...@@ -126,7 +127,7 @@ const tableData = [
value: 'John', value: 'John',
}, },
{ {
value: '25', value: '7',
}, },
{ {
value: 'Football', value: 'Football',
...@@ -182,6 +183,15 @@ const tableData = [ ...@@ -182,6 +183,15 @@ const tableData = [
<template> <template>
<h2 class="title gradient-text">Playground</h2> <h2 class="title gradient-text">Playground</h2>
<Table
show-all-lines
:columns="tableColumns"
darknessTextColor="500"
:data="tableData"
fontSize="36px"
theme="black"
stripedRows
></Table>
<Table <Table
show-all-lines show-all-lines
:columns="tableColumns" :columns="tableColumns"
......
...@@ -20,7 +20,6 @@ export type TTableColumnType = ...@@ -20,7 +20,6 @@ export type TTableColumnType =
| 'number' | 'number'
| 'text' | 'text'
| 'date' | 'date'
| 'tag'
| 'select' | 'select'
| 'rating' | 'rating'
| 'progressBar' | 'progressBar'
......
...@@ -22,7 +22,7 @@ const left = ref(); ...@@ -22,7 +22,7 @@ const left = ref();
const isOnContainerClick = ref(); const isOnContainerClick = ref();
const parent = computed(() => props.parentSelector); const parent = computed(() => props.parentSelector);
const container = ref(document.querySelector(props.parentSelector)); const container: Ref = ref(document.querySelector(props.parentSelector));
if (!container.value) { if (!container.value) {
setTimeout(() => { setTimeout(() => {
container.value = document.querySelector(props.parentSelector); container.value = document.querySelector(props.parentSelector);
...@@ -37,7 +37,6 @@ watch( ...@@ -37,7 +37,6 @@ watch(
const clientRect = container.value?.getBoundingClientRect(); const clientRect = container.value?.getBoundingClientRect();
top.value = props.top ?? clientRect.top; top.value = props.top ?? clientRect.top;
left.value = props.left ?? clientRect.left; left.value = props.left ?? clientRect.left;
console.log('left: ', left.value);
} }
container.value.addEventListener('pointerdown', (event: Event) => { container.value.addEventListener('pointerdown', (event: Event) => {
...@@ -72,7 +71,7 @@ watch( ...@@ -72,7 +71,7 @@ watch(
@pointerdown.stop="" @pointerdown.stop=""
:style="`top: ${top}px; left: ${left}px; opacity: ${active ? 1 : 0}; pointer-events: ${active ? 'auto' : 'none'}; padding: ${padding}`" :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 /> <slot />
<p v-if="!$slots.default" style="background-color: black; color: white; padding: 10px">Popup</p> <p v-if="!$slots.default" style="background-color: black; color: white; padding: 10px">Popup</p>
</div> </div>
......
...@@ -84,7 +84,7 @@ export const Simple: Story = { ...@@ -84,7 +84,7 @@ export const Simple: Story = {
}, },
{ {
name: 'Age', name: 'Age',
type: 'text', type: 'number',
}, },
{ {
name: 'Hobbies', name: 'Hobbies',
...@@ -128,7 +128,8 @@ export const Full: Story = { ...@@ -128,7 +128,8 @@ export const Full: Story = {
}, },
{ {
name: 'Age', name: 'Age',
type: 'text', type: 'number',
sortable: true,
filterable: true, filterable: true,
}, },
{ {
...@@ -163,7 +164,7 @@ export const Full: Story = { ...@@ -163,7 +164,7 @@ export const Full: Story = {
value: 'John', value: 'John',
}, },
{ {
value: '25', value: '7',
}, },
{ {
value: 'Football', value: 'Football',
......
...@@ -3,15 +3,8 @@ import type { ITableProps } from '@interfaces/componentsProps'; ...@@ -3,15 +3,8 @@ import type { ITableProps } from '@interfaces/componentsProps';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { convertThemeToColor, convertThemeToSecondaryColor, convertThemeToTextColor } from '@helpers/common'; import { convertThemeToColor, convertThemeToSecondaryColor, convertThemeToTextColor } from '@helpers/common';
import type { ITableItem } from '@interfaces/componentsProp'; import type { ITableItem } from '@interfaces/componentsProp';
import FilterIcon from '@stories/icons/Mono/FilterIcon.vue'; import { calcGap, calcRows } from '@stories/components/Table/helpers';
import SortDownIcon from '@stories/icons/Mono/SortDownIcon.vue'; import TableHeader from '@stories/components/Table/TableHeader.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>(), { const props = withDefaults(defineProps<ITableProps>(), {
theme: 'white', theme: 'white',
...@@ -22,28 +15,14 @@ const data = defineModel<ITableItem[][]>('data'); ...@@ -22,28 +15,14 @@ const data = defineModel<ITableItem[][]>('data');
const columns = ref(props.columns); const columns = ref(props.columns);
const sortStateActive = ref<[number, string] | []>([]); const sortStateActive = ref<[number, string] | []>([]);
const isFilterPopup = ref<boolean>(false);
const columnToFilter = ref<number>(0); const columnToFilter = ref<number>(0);
const isFilterPopup = ref<boolean>(false);
const filterValue = ref<string>(''); const filterValue = ref<string>('');
const isRegisterSensitive = ref<boolean>(false);
watch(props.columns, () => (columns.value = props.columns)); watch(props.columns, () => (columns.value = props.columns));
const initGap = computed( const initGap = computed(() => calcGap(props.gap, props.fontSize));
() =>
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 themeColor = computed(() => convertThemeToColor(props.theme, props.darknessTheme));
const color = computed(() => const color = computed(() =>
props.textColor props.textColor
...@@ -52,7 +31,6 @@ const color = computed(() => ...@@ -52,7 +31,6 @@ const color = computed(() =>
); );
const secondaryColor = computed(() => convertThemeToSecondaryColor(props.theme, props.darknessTheme)); const secondaryColor = computed(() => convertThemeToSecondaryColor(props.theme, props.darknessTheme));
const darkCellColor = computed(() => convertThemeToSecondaryColor(props.theme, String(+props.darknessTheme + 300))); const darkCellColor = computed(() => convertThemeToSecondaryColor(props.theme, String(+props.darknessTheme + 300)));
// ['', 'up', 'none', '', 'none', ...] // ['', 'up', 'none', '', 'none', ...]
const sortState = computed<string[]>(() => { const sortState = computed<string[]>(() => {
const result = []; const result = [];
...@@ -61,9 +39,15 @@ const sortState = computed<string[]>(() => { ...@@ -61,9 +39,15 @@ const sortState = computed<string[]>(() => {
} }
return result; return result;
}); });
const rows = computed<ITableItem[][]>(() => 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) => { const changeColumnSortMode = (index: number) => {
...@@ -83,15 +67,10 @@ const setFilter = (column: number) => { ...@@ -83,15 +67,10 @@ const setFilter = (column: number) => {
isFilterPopup.value = !isFilterPopup.value; isFilterPopup.value = !isFilterPopup.value;
} }
if (columnToFilter.value !== column) { if (columnToFilter.value !== column) {
filterValue.value = '';
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 = () => { const cancelFilter = () => {
filterValue.value = ''; filterValue.value = '';
isFilterPopup.value = false; isFilterPopup.value = false;
...@@ -108,69 +87,25 @@ const cancelFilter = () => { ...@@ -108,69 +87,25 @@ const cancelFilter = () => {
id="table" id="table"
> >
<thead> <thead>
<tr> <TableHeader
<th v-model:filterValue="filterValue"
:class="{ v-model:isFilterPopup="isFilterPopup"
leftBorder: showAllLines, v-model:isRegisterSensitive="isRegisterSensitive"
}" :columns="columns"
v-for="(column, index) of columns" :sortState="sortState"
:key="column.name" :columnToFilter="columnToFilter"
class="columnHeader" :initGap="initGap"
:style="`padding: calc(${initGap} / 2) ${initGap}`" :theme="theme"
> :themeColor="themeColor"
<div :secondaryColor="secondaryColor"
:style="`justify-content: ${center ? 'center' : 'start'}; gap: ${center ? '0' : initGap}; padding: ${calcColumnPadding(column, props.center, initGap)}`" :color="color"
class="columnFlex" :showAllLines="!!showAllLines"
> :center="!!center"
<div class="columnHeader-container"> :fontSize="fontSize"
<h3> @changeColumnSortMode="changeColumnSortMode"
{{ column.name }} @setFilter="setFilter"
</h3> @cancelFilter="cancelFilter"
<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> </thead>
<tbody> <tbody>
<tr v-for="(row, index) of rows" :key="index"> <tr v-for="(row, index) of rows" :key="index">
...@@ -210,15 +145,6 @@ tr::after { ...@@ -210,15 +145,6 @@ tr::after {
height: 1px; height: 1px;
background-color: v-bind(secondaryColor); background-color: v-bind(secondaryColor);
} }
.columnFlex {
display: flex;
font-weight: bold;
}
.columnHeader-container {
display: flex;
align-items: center;
gap: 10px;
}
.tableLines { .tableLines {
border-top: 1px solid v-bind(secondaryColor); border-top: 1px solid v-bind(secondaryColor);
border-right: 1px solid v-bind(secondaryColor); border-right: 1px solid v-bind(secondaryColor);
...@@ -229,15 +155,4 @@ tr::after { ...@@ -229,15 +155,4 @@ tr::after {
.darkRow { .darkRow {
background-color: v-bind(darkCellColor); 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> </style>
<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>
import type { ITableColumn, ITableItem } from '@interfaces/componentsProp'; import type { ITableColumn, ITableItem, TTableColumnType } from '@interfaces/componentsProp';
export const calcRows = ( export const calcRows = (
initRows: ITableItem[][], initRows: ITableItem[][],
sortStateActive: [number, string] | [], sortStateActive: [number, string] | [],
multipleSort: boolean, multipleSort: boolean,
columnToFilter: number, columnToFilterType: TTableColumnType,
filterValue: string, filterValue: string,
isRegisterSensitive: boolean,
) => { ) => {
// ['up', 'down', ...] // ['up', 'down', ...]
let rows = [...initRows]; let rows = [...initRows];
const sortIndex = sortStateActive[0];
if (filterValue) { if (filterValue && sortIndex) {
rows = rows.filter((row) => row[columnToFilter].value.startsWith(filterValue)); 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; if (!sortStateActive.length) return rows;
...@@ -36,11 +41,23 @@ export const calcRows = ( ...@@ -36,11 +41,23 @@ export const calcRows = (
} else { } else {
const index = sortStateActive[0]; const index = sortStateActive[0];
const value = sortStateActive[1]; 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) => return rows.sort((a, b) =>
value === 'down' ? a[index].value.localeCompare(b[index].value) : b[index].value.localeCompare(a[index].value), 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) => export const calcColumnPadding = (column: ITableColumn, center: boolean, gap: string) =>
center ? `0px calc(${gap} / 2 + ${column.padding ?? '0px'} / 2)` : `0 ${column.padding ?? '0px'} 0 0`; center ? `0px calc(${gap} / 2 + ${column.padding ?? '0px'} / 2)` : `0 ${column.padding ?? '0px'} 0 0`;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment