From 6973fdc1b6e5ec73ec1adce8da849d06beb20e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BB=D1=8E=D0=B3=D0=B8=D0=BD?= <d.malygin@iqdev.digital> Date: Wed, 29 Jan 2025 15:54:12 +0500 Subject: [PATCH] feat: finish adding data types to 'Table' and paginator --- src/Playground.vue | 15 +- src/common/interfaces/componentsProp.ts | 1 + src/components/Table/Table.stories.ts | 26 +-- src/components/Table/Table.vue | 197 +++++++++++------- src/components/Table/components/TableCell.vue | 113 ++++++---- .../Table/components/TableHeader.vue | 10 +- src/components/Table/helpers.ts | 20 +- 7 files changed, 226 insertions(+), 156 deletions(-) diff --git a/src/Playground.vue b/src/Playground.vue index 572f7bb..a101e80 100644 --- a/src/Playground.vue +++ b/src/Playground.vue @@ -116,10 +116,14 @@ const tableColumns: ITableColumn[] = [ { name: 'Is gay?', type: 'checkbox', + filterable: true, + sortable: true, }, { name: 'Status', type: 'select', + filterable: true, + sortable: true, options: { options: [{ value: 'Married' }, { value: 'Oh no...(s)he is dead' }], theme: 'black', @@ -128,6 +132,8 @@ const tableColumns: ITableColumn[] = [ { name: 'Children', type: 'rating', + filterable: true, + sortable: true, options: { theme: 'yellow', }, @@ -135,6 +141,8 @@ const tableColumns: ITableColumn[] = [ { name: 'Job progress', type: 'progressBar', + filterable: true, + sortable: true, options: { theme: 'red', size: 'small', @@ -143,7 +151,8 @@ const tableColumns: ITableColumn[] = [ { name: 'Strength', type: 'knob', - options: {}, + filterable: true, + sortable: true, }, ]; const tableData = ref([ @@ -164,6 +173,7 @@ const selectOptions = [ ]; const knob = ref(0); const pbValue = ref(0); +const openDrawer = () => (visibleDrawer.value = true); </script> <template> @@ -200,7 +210,6 @@ const pbValue = ref(0); v-model="tableData" theme="black" stripedRows - editable paginator :no-editing-settings="{ cells: [[0, 0]], @@ -208,7 +217,7 @@ const pbValue = ref(0); :handlers="[ { cell: [0, 0], - handler: () => (visibleDrawer = true), + handler: () => openDrawer(), }, ]" ></Table> diff --git a/src/common/interfaces/componentsProp.ts b/src/common/interfaces/componentsProp.ts index c01e4bb..bcda003 100644 --- a/src/common/interfaces/componentsProp.ts +++ b/src/common/interfaces/componentsProp.ts @@ -11,6 +11,7 @@ export interface ITableColumn { name: string; options?: ITableColumnOptions; type?: TTableColumnType; + width?: number; editable?: boolean; filterable?: boolean; sortable?: boolean; diff --git a/src/components/Table/Table.stories.ts b/src/components/Table/Table.stories.ts index d4cc696..f87a930 100644 --- a/src/components/Table/Table.stories.ts +++ b/src/components/Table/Table.stories.ts @@ -95,28 +95,8 @@ export const Simple: Story = { }, ], data: [ - [ - { - value: 'Pete', - }, - { - value: '30', - }, - { - value: 'Chess', - }, - ], - [ - { - value: 'John', - }, - { - value: '25', - }, - { - value: 'Football', - }, - ], + ['Pete', '30', 'Chess'], + ['John', '25', 'Football'], ], }, }; @@ -133,6 +113,7 @@ export const Full: Story = { type: 'number', filterable: true, sortable: true, + width: '50px', }, { name: 'Hobbies', @@ -154,7 +135,6 @@ export const Full: Story = { type: 'select', options: { options: [{ value: 'Married' }, { value: 'Oh no...(s)he is dead' }], - theme: 'sky', }, }, { diff --git a/src/components/Table/Table.vue b/src/components/Table/Table.vue index 922e657..98be4be 100644 --- a/src/components/Table/Table.vue +++ b/src/components/Table/Table.vue @@ -6,6 +6,7 @@ import { calcAdditionalHeight, calcGap, calcRows } from '@components/Table/helpe import TableHeader from '@components/Table/components/TableHeader.vue'; import TableCell from '@components/Table/components/TableCell.vue'; import Paginator from '@components/Paginator/Paginator.vue'; +import ToggleSwitch from '@components/ToggleSwitch/ToggleSwitch.vue'; const props = withDefaults(defineProps<ITableProps>(), { size: 'normal', @@ -18,7 +19,10 @@ const data = defineModel() as Ref<unknown[][]>; const emit = defineEmits(['updateData']); const table = ref(); -const currentPage = ref(1); +const currentPage = ref<number>(1); +const itemsPerPage = ref<number>(10); +const isEditMode = ref<boolean>(props.editable); + const columns = ref(props.columns); const sortStateActive = ref<[number, string] | []>([]); const indexColumnToFilter = ref<number>(0); @@ -49,6 +53,8 @@ const sortState = computed<string[]>(() => { const rows = computed<unknown[][]>(() => calcRows( data.value, + currentPage.value, + itemsPerPage.value, sortStateActive.value, props.multipleSort, indexColumnToFilter.value, @@ -58,7 +64,7 @@ const rows = computed<unknown[][]>(() => ), ); const types = computed(() => props.columns.map((column) => column.type)); - +const paginatorContainerHeight = computed(() => (props.paginator || props.editable ? '50px' : '0')); const themeColor = computed(() => convertThemeToColor(props.theme, props.darknessTheme)); const color = computed(() => props.textColor @@ -107,84 +113,97 @@ const updateData = (newValue: Ref<unknown>, rowIndex: number, columnIndex: numbe </script> <template> - <div :style="`background-color: ${themeColor}; color: ${color}`"> - <table - :class="{ - tableLines: showAllLines, - }" - :style="`background-color: ${themeColor}; color: ${color}`" - class="table" - ref="table" - > - <thead> - <TableHeader - v-model:filterValue="filterValue" - v-model:isFilterPopup="isFilterPopup" - v-model:isRegisterSensitive="isRegisterSensitive" - :table="table" - :columns="columns" - :sortState="sortState" - :indexColumnToFilter="indexColumnToFilter" - :types="types" - :initGap="initGap" - :additionalHeightFromSize="additionalHeightFromSize" - :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, rowIndex) of rows" - :key="rowIndex" + <table + :class="{ + tableLines: showAllLines, + }" + :style="`background-color: ${themeColor}; color: ${color}`" + class="table" + ref="table" + > + <thead> + <TableHeader + v-model:filterValue="filterValue" + v-model:isFilterPopup="isFilterPopup" + v-model:isRegisterSensitive="isRegisterSensitive" + :table="table" + :columns="columns" + :sortState="sortState" + :indexColumnToFilter="indexColumnToFilter" + :types="types" + :initGap="initGap" + :additionalHeightFromSize="additionalHeightFromSize" + :theme="theme" + :themeColor="themeColor" + :secondaryColor="secondaryColor" + :color="color" + :showAllLines="!!showAllLines" + :center="!!center" + :fontSize="fontSize" + :isEditMode="isEditMode" + @changeColumnSortMode="changeColumnSortMode" + @setFilter="setFilter" + @cancelFilter="cancelFilter" + /> + </thead> + <tbody> + <tr + v-for="(row, rowIndex) of rows" + :key="rowIndex" + :class="{ + noEdit: + !isEditMode || + (noEditingSettings?.rows && noEditingSettings?.rows.find((i) => data?.[i]?.join('') === row.join(''))), + }" + > + <td + v-for="(item, columnIndex) of row" + :key="columnIndex" + @click=" + handlers ? handlers?.find((i) => i.cell?.[0] === rowIndex && i.cell?.[1] === columnIndex)?.handler() : null + " :class="{ - noEdit: - !editable || - (noEditingSettings?.rows && noEditingSettings?.rows.find((i) => data?.[i]?.join('') === row.join(''))), + leftBorder: showAllLines, + darkRow: stripedRows && !(rowIndex % 2), + noEdit: !isEditMode || (noEditingSettings?.columns && ~noEditingSettings.columns?.indexOf(columnIndex)), + pointer: handlers && handlers?.find((i) => i.cell?.[0] === rowIndex && i.cell?.[1] === columnIndex), }" + :style="`padding: calc(${initGap} / 2 + ${additionalHeightFromSize}) ${initGap}`" > - <td - v-for="(item, columnIndex) of row" - :key="columnIndex" - @click="handlers?.find((i) => i.cell?.[0] === rowIndex && i.cell?.[1] === columnIndex)?.handler" - :class="{ - leftBorder: showAllLines, - darkRow: stripedRows && rowIndex % 2, - noEdit: !editable || (noEditingSettings?.columns && ~noEditingSettings.columns?.indexOf(columnIndex)), - pointer: handlers?.find((i) => i.cell?.[0] === rowIndex && i.cell?.[1] === columnIndex), - }" - :style="`padding: calc(${initGap} / 2 + ${additionalHeightFromSize}) ${initGap}`" - > - <TableCell - :item="item" - :types="types" - :columns="columns" - :rowIndex="rowIndex" - :columnIndex="columnIndex" - :center="center" - :editable="editable" - :noEditingSettings="noEditingSettings?.cells" - :fontSize="fontSize" - :knobWidth="knobWidth" - :noEdit="!!handlers?.find((i) => i.cell?.[0] === rowIndex && i.cell?.[1] === columnIndex)" - @updateData="updateData" - /> - </td> - </tr> - </tbody> - </table> + <TableCell + :item="item" + :types="types" + :column="columns[columnIndex]" + :rowIndex="rowIndex" + :columnIndex="columnIndex" + :center="center" + :isEditMode="isEditMode" + :noEditingSettings="noEditingSettings?.cells" + :fontSize="fontSize" + :initGap="initGap" + :knobWidth="knobWidth" + :noEdit=" + handlers ? !!handlers?.find((i) => i.cell?.[0] === rowIndex && i.cell?.[1] === columnIndex) : false + " + :theme="theme" + @updateData="updateData" + /> + </td> + </tr> + </tbody> + </table> + <div class="paginatorContainer"> + <section v-if="editable" class="editMenu"> + <p class="editText">Edit mode:</p> + <ToggleSwitch v-model="isEditMode" negativeTheme="red" /> + </section> <Paginator v-show="paginator" - v-model="currentPage" + v-model:current="currentPage" + v-model:itemsPerPage="itemsPerPage" :theme="theme" :total="data.length" + :itemsPerPageOptions="[2, 5]" v-bind="paginatorOptions" class="paginator" /> @@ -194,9 +213,34 @@ const updateData = (newValue: Ref<unknown>, rowIndex: number, columnIndex: numbe <style scoped> .table { border-collapse: collapse; + position: relative; + * { + font-size: v-bind(fontSize); + } +} +.table::after { + content: ''; + position: absolute; + top: 100%; + z-index: -1; + width: 100%; + height: v-bind(paginatorContainerHeight); + background-color: v-bind(themeColor); } -table * { - font-size: v-bind(fontSize); +.editMenu { + display: flex; + align-items: center; + gap: 10px; + width: max-content; + padding: 10px; +} +.editText { + color: v-bind(color); +} +.paginatorContainer { + height: 50px; + display: flex; + align-items: center; } tr { position: relative; @@ -216,15 +260,14 @@ tr::after { } .cell { display: flex; - width: 100%; height: 100%; + margin: 0 auto; } .cellCenter { justify-content: center; align-items: center; } .paginator { - display: block; margin: 0 auto; } .leftBorder { diff --git a/src/components/Table/components/TableCell.vue b/src/components/Table/components/TableCell.vue index 7f5ddca..ef07be1 100644 --- a/src/components/Table/components/TableCell.vue +++ b/src/components/Table/components/TableCell.vue @@ -6,19 +6,22 @@ import Select from '@components/Select/Select.vue'; import Rating from '@components/Rating/Rating.vue'; import ProgressBar from '@components/ProgressBar/ProgressBar.vue'; import Knob from '@components/Knob/Knob.vue'; +import type { TThemeColor } from '@interfaces/common'; interface IProps { item: unknown; types: (TTableColumnType | undefined)[]; - columns: ITableColumn[]; + column: ITableColumn; rowIndex: number; columnIndex: number; center: boolean | undefined; - editable: boolean; + isEditMode: boolean; fontSize: string; + initGap: string; knobWidth: string; noEditingSettings: [number, number][] | undefined; noEdit: boolean; + theme: TThemeColor; } defineProps<IProps>(); defineEmits(['updateData']); @@ -26,14 +29,16 @@ defineEmits(['updateData']); <template> <div + :style="`width: calc(${column.width ?? 'auto'} - 2 * ${initGap})`" :class="[ 'cell', { cellCenter: center, noEdit: noEdit || - !editable || - noEditingSettings?.find((i: [number, number]) => i[0] === rowIndex && i[1] === columnIndex), + !isEditMode || + (noEditingSettings && + noEditingSettings?.find((i: [number, number]) => i[0] === rowIndex && i[1] === columnIndex)), }, ]" > @@ -43,43 +48,71 @@ defineEmits(['updateData']); @input="(event) => $emit('updateData', event.target, rowIndex, columnIndex)" :id="`${rowIndex}-${columnIndex}`" :type="types[columnIndex]" - :style="`display: inline; width: 100%; text-align: ${center ? 'center' : 'auto'}`" - /> - <Checkbox - v-else-if="types[columnIndex] === 'checkbox'" - v-bind="filterCheckboxProps(columns[columnIndex].options)" - :active="item as boolean" - @update="$emit('updateData', $event, rowIndex, columnIndex)" - /> - <Select - v-else-if="types[columnIndex] === 'select'" - noBorder - noSelectedBackground - v-bind="filterSelectProps(columns[columnIndex].options)" - width="150px" - :selected="item as string" - @update="$emit('updateData', $event, rowIndex, columnIndex)" - /> - <Rating - v-else-if="types[columnIndex] === 'rating'" - v-bind="columns[columnIndex].options" - :value="item as number" - @update="$emit('updateData', $event, rowIndex, columnIndex)" - /> - <ProgressBar - v-else-if="types[columnIndex] === 'progressBar'" - v-bind="columns[columnIndex].options" - :value="item as number" - @update="$emit('updateData', $event, rowIndex, columnIndex)" - /> - <Knob - v-else-if="types[columnIndex] === 'knob'" - v-bind="columns[columnIndex].options" - :value="item as number" - :width="knobWidth" - :fontSize="fontSize" - @update="$emit('updateData', $event, rowIndex, columnIndex)" + :style="`width: 100%; text-align: ${center ? 'center' : 'auto'}`" /> + <div v-else-if="isEditMode"> + <Checkbox + v-if="types[columnIndex] === 'checkbox'" + v-bind="filterCheckboxProps(column.options)" + :active="item as boolean" + @update="$emit('updateData', $event, rowIndex, columnIndex)" + /> + <Select + v-else-if="types[columnIndex] === 'select'" + noBorder + noSelectedBackground + v-bind="filterSelectProps(column.options)" + width="150px" + :theme="theme" + :selected="item as string" + @update="$emit('updateData', $event, rowIndex, columnIndex)" + /> + <Rating + v-else-if="types[columnIndex] === 'rating'" + v-bind="column.options" + :value="item as number" + @update="$emit('updateData', $event, rowIndex, columnIndex)" + /> + <ProgressBar + v-else-if="types[columnIndex] === 'progressBar'" + v-bind="column.options" + :value="item as number" + @update="$emit('updateData', $event, rowIndex, columnIndex)" + /> + <Knob + v-else-if="types[columnIndex] === 'knob'" + v-bind="column.options" + :value="item as number" + :width="knobWidth" + :fontSize="fontSize" + @update="$emit('updateData', $event, rowIndex, columnIndex)" + /> + </div> + <div v-else> + <Checkbox + v-if="types[columnIndex] === 'checkbox'" + v-bind="filterCheckboxProps(column.options)" + :active="item as boolean" + /> + <Select + v-else-if="types[columnIndex] === 'select'" + noBorder + noSelectedBackground + v-bind="filterSelectProps(column.options)" + width="150px" + :theme="theme" + :selected="item as string" + /> + <Rating v-else-if="types[columnIndex] === 'rating'" v-bind="column.options" :value="item as number" /> + <ProgressBar v-else-if="types[columnIndex] === 'progressBar'" v-bind="column.options" :value="item as number" /> + <Knob + v-else-if="types[columnIndex] === 'knob'" + v-bind="column.options" + :value="item as number" + :width="knobWidth" + :fontSize="fontSize" + /> + </div> </div> </template> diff --git a/src/components/Table/components/TableHeader.vue b/src/components/Table/components/TableHeader.vue index 7873848..0c965f5 100644 --- a/src/components/Table/components/TableHeader.vue +++ b/src/components/Table/components/TableHeader.vue @@ -27,6 +27,7 @@ interface Props { showAllLines: boolean; center: boolean; fontSize: string; + isEditMode: boolean; } const props = defineProps<Props>(); const emit = defineEmits(['changeColumnSortMode', 'setFilter', 'cancelFilter']); @@ -58,7 +59,6 @@ const isColumnTypeText = computed(() => props.columns[props.indexColumnToFilter] `column`, { leftBorder: showAllLines, - textMinWidth: types[index] === 'text', }, ]" > @@ -70,8 +70,9 @@ const isColumnTypeText = computed(() => props.columns[props.indexColumnToFilter] <h3> {{ column.name }} </h3> + <div v-show="column.sortable && isEditMode" :style="`width: ${fontSize}; height: ${fontSize}`"></div> <button - v-if="column.sortable" + v-show="column.sortable && !isEditMode" @click.prevent="emit('changeColumnSortMode', index)" :style="`min-width: ${fontSize}; min-height: ${fontSize}; max-height: ${fontSize}`" > @@ -80,7 +81,7 @@ const isColumnTypeText = computed(() => props.columns[props.indexColumnToFilter] <SortUpIcon v-show="sortState[index] === 'up'" :color="color" :size="iconSize" /> </button> <button - v-if="column.filterable" + v-if="column.filterable && ~['text', 'number'].indexOf(column.type as string)" @pointerdown="emit('setFilter', index)" :id="`filter${index}`" :style="`position: relative; width: ${fontSize}; max-height: ${fontSize}`" @@ -158,7 +159,4 @@ const isColumnTypeText = computed(() => props.columns[props.indexColumnToFilter] .leftBorder { border-left: 1px solid v-bind(secondaryColor); } -.textMinWidth { - min-width: 230px; -} </style> diff --git a/src/components/Table/helpers.ts b/src/components/Table/helpers.ts index 10c9e93..efaf318 100644 --- a/src/components/Table/helpers.ts +++ b/src/components/Table/helpers.ts @@ -4,6 +4,8 @@ import type { ICheckboxProps, ISelectProps } from '@interfaces/componentsProps'; export const calcRows = ( initRows: unknown[][] | undefined, + currentPage: number, + itemsPerPage: number, sortStateActive: [number, string] | [], multipleSort: boolean, indexColumnToFilter: number, @@ -21,7 +23,7 @@ export const calcRows = ( }); } - if (!sortStateActive.length) return rows; + if (!sortStateActive.length) return rows.splice(itemsPerPage * (currentPage - 1), itemsPerPage); if (multipleSort) { // TODO: multiple sort logic @@ -44,13 +46,17 @@ export const calcRows = ( const index = sortStateActive[0]; const value = sortStateActive[1]; if (~['text', 'select'].indexOf(columnToSortType)) - return rows.sort((a, b) => { - if (typeof a[index] === 'string' && typeof b[index] === 'string') - return value === 'down' ? a[index].localeCompare(b[index]) : b[index].localeCompare(a[index]); - return 0; - }); + return rows + .sort((a, b) => { + if (typeof a[index] === 'string' && typeof b[index] === 'string') + return value === 'down' ? a[index].localeCompare(b[index]) : b[index].localeCompare(a[index]); + return 0; + }) + .splice(itemsPerPage * (currentPage - 1), itemsPerPage); // 'number', 'checkbox', 'rating', 'progressBar', 'knob' - return rows.sort((a, b) => (value === 'down' ? +a[index] - +b[index] : +b[index] - +a[index])); + return (rows as number[][]) + .sort((a, b) => (value === 'down' ? +a[index] - +b[index] : +b[index] - +a[index])) + .splice(itemsPerPage * (currentPage - 1), itemsPerPage); } }; -- GitLab