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
4357 class =" sortType-icon"
4458 :class =" {'desc': header.sortType === 'desc'}"
4559 ></i >
46-
4760 </span >
4861 </th >
4962 </tr >
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]"
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" >
162182
163183<script setup lang="ts">
164184import {
165- useSlots , computed , toRefs , PropType , ref , watch , provide ,
185+ useSlots , computed , toRefs , PropType , ref , watch , provide , onMounted ,
166186} from ' vue' ;
167187import MutipleSelectCheckBox from ' ./MutipleSelectCheckBox.vue' ;
168188import 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
190212type 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
344386const {
@@ -359,6 +401,7 @@ const {
359401const fontSizePx = computed (() => ` ${props .tableFontSize }px ` );
360402const rowHeight = computed (() => props .tableFontSize * (props .dense ? 2 : 3 ));
361403const rowHeightPx = computed (() => ` ${rowHeight .value }px ` );
404+ const shadowRightPx = computed (() => ` -${rowHeight .value }px ` );
362405const tableHeightPx = computed (() => (props .tableHeight ? ` ${props .tableHeight }px ` : null ));
363406const minHeightPx = computed (() => ` ${rowHeight .value * 5 }px ` );
364407const sortTypeIconSize = computed (() => Math .round (props .tableFontSize / 2.5 ));
@@ -369,7 +412,7 @@ const sortTypeDescIconMarginTopPx = computed(() => `${sortTypeIconMargin.value}p
369412const loadingEntitySizePx = computed (() => ` ${props .tableFontSize * 5 }px ` );
370413const 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
374417provide (' themeColor' , props .themeColor );
375418provide (' loadingEntitySizePx' , loadingEntitySizePx .value );
@@ -386,8 +429,17 @@ const ifHasLoadingSlot = computed(() => slots.loading);
386429
387430// global dataTable $ref
388431const dataTable = ref ();
432+ const tableBody = ref ();
389433provide (' 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
392444const emits = defineEmits ([
393445 ' update:itemsSelected' ,
@@ -419,6 +471,13 @@ const isMutipleSelectable = computed((): boolean => props.itemsSelected !== null
419471
420472const 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+
422481const initClientSortOptions = (): ClientSortOptions | null => {
423482 if (props .sortBy !== ' ' ) {
424483 return {
@@ -431,9 +490,33 @@ const initClientSortOptions = (): ClientSortOptions | null => {
431490
432491const 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...)
435514const 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+
454584const headerColumns = computed ((): string [] => headersForRender .value .map ((header ) => header .value ));
455585
456586const 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 (2 n ) {
1138+ tr :nth-child (2 n ) td {
9621139 color : v-bind (evenRowFontColor );
9631140 background-color : v-bind (evenRowBackgroundColor );
9641141 }
0 commit comments