Skip to content

Commit 29f8461

Browse files
committed
feature: add column width and fixed columns
1 parent f271b5e commit 29f8461

File tree

7 files changed

+235
-33
lines changed

7 files changed

+235
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"author": "HC200ok",
44
"description": "A customizable and easy-to-use data table component made with Vue.js 3.x.",
55
"private": false,
6-
"version": "1.2.9",
6+
"version": "1.2.11",
77
"types": "./types/main.d.ts",
88
"license": "MIT",
99
"files": [

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<!-- <ServerSideMode /> -->
2+
<ServerSideMode />
33
<br><br>
44
<ClientMode />
55
</template>

src/components/DataTable.vue

Lines changed: 189 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,38 @@
44
class="vue3-easy-data-table"
55
>
66
<div
7+
ref="tableBody"
78
class="data-table__body"
89
:class="{
910
'fixed-header': fixedHeader,
1011
'fixed-height': tableHeight,
12+
'show-shadow': showShadow,
1113
}"
1214
>
1315
<table>
16+
<colgroup>
17+
<col
18+
v-for="(header, index) in headersForRender"
19+
:key="index"
20+
:style="getColStyle(header)"
21+
/>
22+
</colgroup>
1423
<thead v-if="headersForRender.length">
1524
<tr>
1625
<th
1726
v-for="(header, index) in headersForRender"
1827
:key="index"
28+
colspan="1"
29+
rowspan="1"
1930
:class="{
2031
sortable: header.sortable,
2132
'none': header.sortable && header.sortType === 'none',
2233
'desc': header.sortable && header.sortType === 'desc',
2334
'asc': header.sortable && header.sortType === 'asc',
35+
'fixed': header.fixed,
36+
'hasShadow': header.value === lastFixedColumn,
2437
}"
38+
:style="getFixedDistance(header.value)"
2539
@click="(header.sortable && header.sortType) ? updateSortField(header.value, header.sortType) : null"
2640
>
2741
<MutipleSelectCheckBox
@@ -43,7 +57,6 @@
4357
class="sortType-icon"
4458
:class="{'desc': header.sortType === 'desc'}"
4559
></i>
46-
4760
</span>
4861
</th>
4962
</tr>
@@ -65,6 +78,10 @@
6578
<td
6679
v-for="(column, i) in headerColumns"
6780
:key="i"
81+
colspan="1"
82+
rowspan="1"
83+
:style="getFixedDistance(column, 'td')"
84+
:class="{'hasShadow': column === lastFixedColumn}"
6885
>
6986
<slot
7087
v-if="slots[column]"
@@ -114,11 +131,14 @@
114131
v-if="showFooter"
115132
class="data-table__footer"
116133
>
117-
<div class="footer__rows-per-page">
134+
<div
135+
v-if="!hideRowsPerPage"
136+
class="footer__rows-per-page"
137+
>
118138
rows per page:
119139
<RowsSelector
120140
v-model="rowsPerPageReactive"
121-
:rows-items="rowsItems"
141+
:rows-items="rowsItemsComputed"
122142
/>
123143
</div>
124144
<div class="footer__items-index">
@@ -162,7 +182,7 @@
162182

163183
<script setup lang="ts">
164184
import {
165-
useSlots, computed, toRefs, PropType, ref, watch, provide,
185+
useSlots, computed, toRefs, PropType, ref, watch, provide, onMounted,
166186
} from 'vue';
167187
import MutipleSelectCheckBox from './MutipleSelectCheckBox.vue';
168188
import SingleSelectCheckBox from './SingleSelectCheckBox.vue';
@@ -185,6 +205,8 @@ type HeaderForRender = {
185205
value: string,
186206
sortable?: boolean,
187207
sortType?: SortType | 'none',
208+
fixed?: Boolean,
209+
width?: number,
188210
}
189211
190212
type ServerOptionsComputed = {
@@ -339,6 +361,26 @@ const props = defineProps({
339361
type: Array as PropType<FilterOption[]>,
340362
default: null,
341363
},
364+
fixedCheckbox: {
365+
type: Boolean,
366+
default: false,
367+
},
368+
fixedIndex: {
369+
type: Boolean,
370+
default: false,
371+
},
372+
indexColumnWidth: {
373+
type: Number,
374+
default: 60,
375+
},
376+
checkboxColumnWidth: {
377+
type: Number,
378+
default: null,
379+
},
380+
hideRowsPerPage: {
381+
type: Boolean,
382+
default: false,
383+
},
342384
});
343385
344386
const {
@@ -359,6 +401,7 @@ const {
359401
const fontSizePx = computed(() => `${props.tableFontSize}px`);
360402
const rowHeight = computed(() => props.tableFontSize * (props.dense ? 2 : 3));
361403
const rowHeightPx = computed(() => `${rowHeight.value}px`);
404+
const shadowRightPx = computed(() => `-${rowHeight.value}px`);
362405
const tableHeightPx = computed(() => (props.tableHeight ? `${props.tableHeight}px` : null));
363406
const minHeightPx = computed(() => `${rowHeight.value * 5}px`);
364407
const sortTypeIconSize = computed(() => Math.round(props.tableFontSize / 2.5));
@@ -369,7 +412,7 @@ const sortTypeDescIconMarginTopPx = computed(() => `${sortTypeIconMargin.value}p
369412
const loadingEntitySizePx = computed(() => `${props.tableFontSize * 5}px`);
370413
const loadingWrapperSizePx = computed(() => (props.tableHeight ? `${props.tableHeight - rowHeight.value}px`
371414
: `${props.tableFontSize * 5 * 2}px`));
372-
415+
const checkboxColumnWidthComputed = computed(() => props.checkboxColumnWidth ?? 1.3 * props.tableFontSize + 20);
373416
// global style related variable
374417
provide('themeColor', props.themeColor);
375418
provide('loadingEntitySizePx', loadingEntitySizePx.value);
@@ -386,8 +429,17 @@ const ifHasLoadingSlot = computed(() => slots.loading);
386429
387430
// global dataTable $ref
388431
const dataTable = ref();
432+
const tableBody = ref();
389433
provide('dataTable', dataTable);
390434
435+
// fixed shadow
436+
const showShadow = ref(false);
437+
onMounted(() => {
438+
tableBody.value.addEventListener('scroll', () => {
439+
showShadow.value = tableBody.value.scrollLeft > 0;
440+
});
441+
});
442+
391443
// define emits
392444
const emits = defineEmits([
393445
'update:itemsSelected',
@@ -419,6 +471,13 @@ const isMutipleSelectable = computed((): boolean => props.itemsSelected !== null
419471
420472
const isServerSideMode = computed((): boolean => serverOptionsComputed.value !== null);
421473
474+
const rowsItemsComputed = computed((): number[] => {
475+
if (!isServerSideMode.value && props.rowsItems.findIndex((item) => item === props.rowsPerPage) === -1) {
476+
return [props.rowsPerPage, ...props.rowsItems];
477+
}
478+
return props.rowsItems;
479+
});
480+
422481
const initClientSortOptions = (): ClientSortOptions | null => {
423482
if (props.sortBy !== '') {
424483
return {
@@ -431,9 +490,33 @@ const initClientSortOptions = (): ClientSortOptions | null => {
431490
432491
const clientSortOptions = ref<ClientSortOptions | null>(initClientSortOptions());
433492
493+
type FixedColumnsInfo = {
494+
value: string,
495+
fixed: Boolean,
496+
distance: number,
497+
width: number,
498+
};
499+
500+
const getColStyle = (header: HeaderForRender): string | undefined => {
501+
const width = header.width ?? (header.fixed ? 100 : null);
502+
if (width) return `width: ${width}px; min-width: ${width}px;`;
503+
return undefined;
504+
};
505+
506+
const hasFixedColumnsFromUser = computed(() => props.headers.findIndex((header) => header.fixed) !== -1);
507+
const fixedHeadersFromUser = computed(() => {
508+
if (hasFixedColumnsFromUser.value) return props.headers.filter((header) => header.fixed);
509+
return [];
510+
});
511+
const unFixedHeaders = computed(() => props.headers.filter((header) => !header.fixed));
512+
434513
// headers for render (integrating sortType, checkbox...)
435514
const headersForRender = computed((): HeaderForRender[] => {
436-
const headersSorting = props.headers.map((header: HeaderForRender) => {
515+
// fixed order
516+
const fixedHeaders = [...fixedHeadersFromUser.value,
517+
...unFixedHeaders.value] as HeaderForRender[];
518+
// sorting
519+
const headersSorting: HeaderForRender[] = fixedHeaders.map((header: HeaderForRender) => {
437520
const headerSorting = header;
438521
if (header.sortable) headerSorting.sortType = 'none';
439522
if (serverOptionsComputed.value
@@ -445,12 +528,59 @@ const headersForRender = computed((): HeaderForRender[] => {
445528
}
446529
return headerSorting;
447530
});
448-
const headersWithIndex = props.showIndex ? [{ text: '#', value: 'index' }, ...headersSorting] : headersSorting;
449-
const headersWithCheckbox = isMutipleSelectable.value
450-
? [{ text: 'checkbox', value: 'checkbox' }, ...headersWithIndex] : headersWithIndex;
531+
// show index
532+
let headersWithIndex: HeaderForRender[] = [];
533+
if (!props.showIndex) {
534+
headersWithIndex = headersSorting;
535+
} else {
536+
const headerIndex: HeaderForRender = (props.fixedIndex || hasFixedColumnsFromUser.value) ? {
537+
text: '#', value: 'index', fixed: true, width: props.indexColumnWidth,
538+
} : { text: '#', value: 'index' };
539+
headersWithIndex = [headerIndex, ...headersSorting];
540+
}
541+
// checkbox
542+
let headersWithCheckbox: HeaderForRender[] = [];
543+
if (!isMutipleSelectable.value) {
544+
headersWithCheckbox = headersWithIndex;
545+
} else {
546+
const headerCheckbox: HeaderForRender = (props.fixedCheckbox || hasFixedColumnsFromUser.value) ? {
547+
text: 'checkbox', value: 'checkbox', fixed: true, width: checkboxColumnWidthComputed.value,
548+
} : { text: 'checkbox', value: 'checkbox' };
549+
headersWithCheckbox = [headerCheckbox, ...headersWithIndex];
550+
}
451551
return headersWithCheckbox;
452552
});
453553
554+
const fixedHeaders = computed((): HeaderForRender[] => headersForRender.value.filter((header) => header.fixed));
555+
const lastFixedColumn = computed((): string => {
556+
if (!fixedHeaders.value.length) return '';
557+
return fixedHeaders.value[fixedHeaders.value.length - 1].value;
558+
});
559+
560+
const fixedColumnsInfos = computed((): FixedColumnsInfo[] => {
561+
if (!fixedHeaders.value.length) return [];
562+
const fixedHeadersWidthArr = fixedHeaders.value.map((header) => header.width ?? 100);
563+
return fixedHeaders.value.map((header: HeaderForRender, index: number): FixedColumnsInfo => ({
564+
value: header.value,
565+
fixed: header.fixed ?? true,
566+
width: header.width ?? 100,
567+
distance: index === 0 ? 0 : fixedHeadersWidthArr.reduce((previous: number, current: number, i: number): number => {
568+
let distance = previous;
569+
if (i < index) distance += current;
570+
return distance;
571+
}),
572+
}));
573+
});
574+
575+
const getFixedDistance = (column: string, type: 'td' | 'th' = 'th') => {
576+
if (!fixedHeaders.value.length) return undefined;
577+
const columInfo = fixedColumnsInfos.value.find((info) => info.value === column);
578+
if (columInfo) {
579+
return `left: ${columInfo.distance}px;z-index: ${type === 'th' ? 3 : 1}; position: sticky`;
580+
}
581+
return undefined;
582+
};
583+
454584
const headerColumns = computed((): string[] => headersForRender.value.map((header) => header.value));
455585
456586
const getItemValue = (column: string, item: Item) => {
@@ -784,11 +914,38 @@ defineExpose({
784914
overflow: auto;
785915
min-height: v-bind(minHeightPx);
786916
917+
&::-webkit-scrollbar-track
918+
{
919+
border-radius: 10px;
920+
background-color: #fff;
921+
}
922+
923+
&::-webkit-scrollbar
924+
{
925+
width: 7px;
926+
height: 7px;
927+
background-color: #fff;
928+
}
929+
930+
&::-webkit-scrollbar-thumb
931+
{
932+
border-radius: 10px;
933+
background-color: #c1c1c1;
934+
}
935+
936+
&.show-shadow {
937+
th.hasShadow, td.hasShadow {
938+
&::after {
939+
box-shadow: inset 6px 0 5px -3px rgb(0 0 0 / 20%)
940+
}
941+
}
942+
}
943+
787944
&.fixed-header {
788-
thead {
945+
thead tr th{
789946
position: sticky;
790947
top: 0;
791-
z-index: 1;
948+
z-index: 2;
792949
}
793950
}
794951
&.fixed-height {
@@ -856,6 +1013,19 @@ defineExpose({
8561013
text-align: left;
8571014
padding: 0px 10px;
8581015
}
1016+
th.hasShadow, td.hasShadow {
1017+
&::after {
1018+
pointer-events: none;
1019+
content: "";
1020+
width: v-bind(rowHeightPx);
1021+
display: inline-block;
1022+
height: 100%;
1023+
position: absolute;
1024+
top: 0px;
1025+
right: v-bind(shadowRightPx);
1026+
box-shadow: none;
1027+
}
1028+
}
8591029
thead, tbody {
8601030
position: relative;
8611031
}
@@ -877,6 +1047,11 @@ defineExpose({
8771047
height: v-bind(fontSizePx);
8781048
}
8791049
1050+
&.fixed {
1051+
position: sticky;
1052+
z-index: 3;
1053+
}
1054+
8801055
&.sortable {
8811056
cursor: pointer;
8821057
@@ -948,8 +1123,10 @@ defineExpose({
9481123
}
9491124
}
9501125
td {
1126+
background-color: v-bind(rowBackgroundColor);
9511127
border: none;
9521128
border-bottom: 1px solid v-bind(rowBorderColor);
1129+
position: relative;
9531130
}
9541131
&.row-alternation {
9551132
&.hover-to-change-color {
@@ -958,7 +1135,7 @@ defineExpose({
9581135
color: v-bind(rowHoverFontColor);
9591136
}
9601137
}
961-
tr:nth-child(2n) {
1138+
tr:nth-child(2n) td{
9621139
color: v-bind(evenRowFontColor);
9631140
background-color: v-bind(evenRowBackgroundColor);
9641141
}

0 commit comments

Comments
 (0)