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