sprintify-ui 0.6.52 → 0.6.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sprintify-ui.es.js +3091 -2933
- package/dist/style.css +1 -1
- package/dist/tailwindcss/button.js +18 -18
- package/dist/types/src/components/BaseDataTableTemplate.vue.d.ts +230 -0
- package/dist/types/src/components/BaseTable.vue.d.ts +32 -203
- package/dist/types/src/components/BaseTableBody.vue.d.ts +9 -0
- package/dist/types/src/components/BaseTableCell.vue.d.ts +59 -0
- package/dist/types/src/components/BaseTableColumn.vue.d.ts +3 -3
- package/dist/types/src/components/BaseTableHead.vue.d.ts +21 -0
- package/dist/types/src/components/BaseTableHeader.vue.d.ts +22 -0
- package/dist/types/src/components/BaseTableRow.vue.d.ts +49 -0
- package/dist/types/src/components/index.d.ts +7 -2
- package/dist/types/src/services/table/classes.d.ts +2 -0
- package/dist/types/src/services/table/types.d.ts +15 -0
- package/package.json +1 -1
- package/src/components/BaseBadge.stories.js +1 -1
- package/src/components/BaseButton.stories.js +0 -5
- package/src/components/BaseButton.vue +1 -1
- package/src/components/BaseDataIteratorSectionColumns.vue +2 -2
- package/src/components/BaseDataTable.vue +4 -4
- package/src/components/BaseDataTableTemplate.vue +887 -0
- package/src/components/BaseTable.stories.js +108 -0
- package/src/components/BaseTable.vue +25 -874
- package/src/components/BaseTableBody.vue +15 -0
- package/src/components/BaseTableCell.vue +87 -0
- package/src/components/BaseTableHead.vue +23 -0
- package/src/components/BaseTableHeader.vue +56 -0
- package/src/components/BaseTableRow.vue +30 -0
- package/src/components/index.ts +12 -2
- package/src/services/table/classes.ts +26 -0
- package/src/services/table/types.ts +19 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="base-table relative w-full overflow-hidden"
|
|
4
|
+
:class="maxHeight ? 'base-table--has-max-height' : ''"
|
|
5
|
+
>
|
|
6
|
+
<div
|
|
7
|
+
ref="slot"
|
|
8
|
+
style="display: none"
|
|
9
|
+
>
|
|
10
|
+
<slot />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="flex flex-col">
|
|
14
|
+
<div
|
|
15
|
+
ref="scrollable"
|
|
16
|
+
class="overflow-x-auto overflow-y-auto"
|
|
17
|
+
data-scroll-lock-scrollable
|
|
18
|
+
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
|
|
19
|
+
>
|
|
20
|
+
<div class="inline-block min-w-full align-middle">
|
|
21
|
+
<div class="relative min-h-[300px]">
|
|
22
|
+
<table class="min-w-full border-separate border-spacing-0">
|
|
23
|
+
<thead
|
|
24
|
+
v-if="newColumns.length"
|
|
25
|
+
ref="thead"
|
|
26
|
+
>
|
|
27
|
+
<tr>
|
|
28
|
+
<th
|
|
29
|
+
v-if="showDetailRowIcon"
|
|
30
|
+
class="th"
|
|
31
|
+
:style="detailsStyles(true)"
|
|
32
|
+
/>
|
|
33
|
+
<th
|
|
34
|
+
v-if="checkable && checkboxPosition === 'left'"
|
|
35
|
+
class="th group cursor-pointer py-0 pl-3"
|
|
36
|
+
align="left"
|
|
37
|
+
:style="checkStyles(true)"
|
|
38
|
+
@click="checkAll"
|
|
39
|
+
>
|
|
40
|
+
<div class="flex items-center">
|
|
41
|
+
<input
|
|
42
|
+
type="checkbox"
|
|
43
|
+
autocomplete="off"
|
|
44
|
+
:checked="isAllChecked"
|
|
45
|
+
:disabled="isAllUncheckable"
|
|
46
|
+
:class="checkboxStyle"
|
|
47
|
+
>
|
|
48
|
+
</div>
|
|
49
|
+
</th>
|
|
50
|
+
<th
|
|
51
|
+
v-for="(column, index) in visibleColumns"
|
|
52
|
+
:key="column.newKey + ':' + index + 'header'"
|
|
53
|
+
v-bind="column.thAttrs && column.thAttrs(column)"
|
|
54
|
+
:style="[
|
|
55
|
+
column.style,
|
|
56
|
+
index == 0 ? firstColStyles(true) : {},
|
|
57
|
+
]"
|
|
58
|
+
class="th group text-left px-3 py-2"
|
|
59
|
+
:class="{
|
|
60
|
+
'cursor-pointer': column.sortable,
|
|
61
|
+
}"
|
|
62
|
+
@click.stop="sort(column, undefined, $event as any)"
|
|
63
|
+
>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
class="flex w-full items-center bg-transparent text-left text-sm font-medium leading-tight text-slate-900"
|
|
67
|
+
:class="{
|
|
68
|
+
'text-blue-600':
|
|
69
|
+
column.sortable && currentSortColumn === column,
|
|
70
|
+
}"
|
|
71
|
+
>
|
|
72
|
+
<span
|
|
73
|
+
class="whitespace-nowrap text-slate-600"
|
|
74
|
+
:class="{
|
|
75
|
+
'text-[12px]': size == 'sm',
|
|
76
|
+
'text-xs': size == 'md',
|
|
77
|
+
}"
|
|
78
|
+
>
|
|
79
|
+
{{ column.label }}
|
|
80
|
+
</span>
|
|
81
|
+
<div
|
|
82
|
+
v-if="column.sortable"
|
|
83
|
+
class="w-3"
|
|
84
|
+
:class="[
|
|
85
|
+
currentSortColumn === column
|
|
86
|
+
? ''
|
|
87
|
+
: 'opacity-0 duration-200 group-hover:opacity-100',
|
|
88
|
+
]"
|
|
89
|
+
>
|
|
90
|
+
<svg
|
|
91
|
+
viewBox="0 0 20 20"
|
|
92
|
+
class="absolute top-1/2 h-5 w-5 -translate-y-1/2"
|
|
93
|
+
>
|
|
94
|
+
<path
|
|
95
|
+
:opacity="!isAsc ? '0.5' : '1'"
|
|
96
|
+
fill="currentColor"
|
|
97
|
+
d="M9.116 4.823a1.25 1.25 0 0 1 1.768 0l2.646 2.647a.75.75 0 0 1-1.06 1.06l-2.47-2.47-2.47 2.47a.75.75 0 1 1-1.06-1.06l2.646-2.647Z"
|
|
98
|
+
></path>
|
|
99
|
+
<path
|
|
100
|
+
:opacity="isAsc ? '0.5' : '1'"
|
|
101
|
+
fill="currentColor"
|
|
102
|
+
d="M9.116 15.177a1.25 1.25 0 0 0 1.768 0l2.646-2.647a.75.75 0 0 0-1.06-1.06l-2.47 2.47-2.47-2.47a.75.75 0 0 0-1.06 1.06l2.646 2.647Z"
|
|
103
|
+
></path>
|
|
104
|
+
</svg>
|
|
105
|
+
</div>
|
|
106
|
+
</button>
|
|
107
|
+
</th>
|
|
108
|
+
<th
|
|
109
|
+
v-if="checkable && checkboxPosition === 'right'"
|
|
110
|
+
class="th group cursor-pointer pr-3"
|
|
111
|
+
align="right"
|
|
112
|
+
@click="checkAll"
|
|
113
|
+
>
|
|
114
|
+
<input
|
|
115
|
+
autocomplete="off"
|
|
116
|
+
type="checkbox"
|
|
117
|
+
:checked="isAllChecked"
|
|
118
|
+
:disabled="isAllUncheckable"
|
|
119
|
+
:class="checkboxStyle"
|
|
120
|
+
>
|
|
121
|
+
</th>
|
|
122
|
+
</tr>
|
|
123
|
+
</thead>
|
|
124
|
+
|
|
125
|
+
<tbody class="bg-white">
|
|
126
|
+
<template
|
|
127
|
+
v-for="(row, index) in data"
|
|
128
|
+
:key="getRowIndex(row, index)"
|
|
129
|
+
>
|
|
130
|
+
<tr class="item-row">
|
|
131
|
+
<td
|
|
132
|
+
v-if="showDetailRowIcon"
|
|
133
|
+
class="group cursor-pointer bg-white pl-3"
|
|
134
|
+
:class="borderBottomClasses(index, row)"
|
|
135
|
+
:style="detailsStyles(false)"
|
|
136
|
+
@click.stop="toggleDetails(row)"
|
|
137
|
+
>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
class="mr-0 flex h-5 w-5 appearance-none items-center justify-center rounded-full border border-slate-300 bg-white text-slate-400 shadow duration-100 group-hover:text-slate-600 group-hover:shadow-md"
|
|
141
|
+
>
|
|
142
|
+
<BaseIcon
|
|
143
|
+
v-if="hasDetailedVisible(row)"
|
|
144
|
+
icon="mdi:chevron-down"
|
|
145
|
+
class="h-5 w-5 duration-300"
|
|
146
|
+
:class="{
|
|
147
|
+
'rotate-180': isVisibleDetailRow(row),
|
|
148
|
+
}"
|
|
149
|
+
/>
|
|
150
|
+
</button>
|
|
151
|
+
</td>
|
|
152
|
+
|
|
153
|
+
<td
|
|
154
|
+
v-if="checkable && checkboxPosition === 'left'"
|
|
155
|
+
class="group z-[1] cursor-pointer bg-white pl-3"
|
|
156
|
+
:class="borderBottomClasses(index, row)"
|
|
157
|
+
:style="checkStyles(false)"
|
|
158
|
+
@click="checkRow(row, index, $event as MouseEvent)"
|
|
159
|
+
>
|
|
160
|
+
<div class="flex items-center">
|
|
161
|
+
<input
|
|
162
|
+
type="checkbox"
|
|
163
|
+
autocomplete="off"
|
|
164
|
+
:disabled="!isRowCheckable(row)"
|
|
165
|
+
:checked="isRowChecked(row)"
|
|
166
|
+
:class="checkboxStyle"
|
|
167
|
+
>
|
|
168
|
+
</div>
|
|
169
|
+
</td>
|
|
170
|
+
|
|
171
|
+
<SlotComponent
|
|
172
|
+
v-for="(column, colindex) in visibleColumns"
|
|
173
|
+
:key="column.newKey + index + ':' + colindex"
|
|
174
|
+
v-bind="column.tdAttrs && column.tdAttrs(row, column)"
|
|
175
|
+
:component="column"
|
|
176
|
+
scoped
|
|
177
|
+
name="default"
|
|
178
|
+
tag="td"
|
|
179
|
+
class="bg-white text-sm font-light"
|
|
180
|
+
:class="[
|
|
181
|
+
borderBottomClasses(index, row),
|
|
182
|
+
column.clickable ? 'cursor-pointer' : '',
|
|
183
|
+
{
|
|
184
|
+
'py-1 px-3': size == 'sm',
|
|
185
|
+
'py-2 px-3': size == 'md',
|
|
186
|
+
}
|
|
187
|
+
]"
|
|
188
|
+
:style="[
|
|
189
|
+
column.style,
|
|
190
|
+
colindex === 0 ? firstColStyles(false) : {}
|
|
191
|
+
]"
|
|
192
|
+
:data-label="column.label"
|
|
193
|
+
:props="{ row, column, index, colindex, toggleDetails }"
|
|
194
|
+
@click="onColumnClick(row, column, index, colindex, $event)"
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
<td
|
|
198
|
+
v-if="checkable && checkboxPosition === 'right'"
|
|
199
|
+
class="group cursor-pointer"
|
|
200
|
+
:class="[
|
|
201
|
+
borderBottomClasses(index, row),
|
|
202
|
+
{
|
|
203
|
+
'px-3 py-1': size == 'sm',
|
|
204
|
+
'p-3': size == 'md',
|
|
205
|
+
}
|
|
206
|
+
]"
|
|
207
|
+
align="right"
|
|
208
|
+
@click="checkRow(row, index, $event as MouseEvent)"
|
|
209
|
+
>
|
|
210
|
+
<input
|
|
211
|
+
type="checkbox"
|
|
212
|
+
autocomplete="off"
|
|
213
|
+
:disabled="!isRowCheckable(row)"
|
|
214
|
+
:checked="isRowChecked(row)"
|
|
215
|
+
:class="checkboxStyle"
|
|
216
|
+
>
|
|
217
|
+
</td>
|
|
218
|
+
</tr>
|
|
219
|
+
|
|
220
|
+
<transition :name="detailTransition">
|
|
221
|
+
<tr
|
|
222
|
+
v-if="isActiveDetailRow(row)"
|
|
223
|
+
:key="getRowIndex(row, index) + 'detail'"
|
|
224
|
+
>
|
|
225
|
+
<td
|
|
226
|
+
:colspan="columnCount"
|
|
227
|
+
:class="borderBottomDetailClasses(index)"
|
|
228
|
+
>
|
|
229
|
+
<slot
|
|
230
|
+
name="detail"
|
|
231
|
+
:row="row"
|
|
232
|
+
:index="index"
|
|
233
|
+
/>
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
</transition>
|
|
237
|
+
</template>
|
|
238
|
+
|
|
239
|
+
<tr v-if="data.length == 0">
|
|
240
|
+
<td :colspan="columnCount">
|
|
241
|
+
<slot name="empty" />
|
|
242
|
+
</td>
|
|
243
|
+
</tr>
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
|
|
247
|
+
<slot name="loading">
|
|
248
|
+
<Transition
|
|
249
|
+
enter-active-class="transition ease-out duration-200"
|
|
250
|
+
enter-from-class="opacity-0"
|
|
251
|
+
enter-to-class="opacity-100"
|
|
252
|
+
leave-active-class="transition ease-in duration-200"
|
|
253
|
+
leave-from-class="opacity-100"
|
|
254
|
+
leave-to-class="opacity-0"
|
|
255
|
+
>
|
|
256
|
+
<div
|
|
257
|
+
v-if="loading"
|
|
258
|
+
class="absolute inset-0 z-[1] flex h-full w-full items-start justify-center"
|
|
259
|
+
>
|
|
260
|
+
<div class="absolute h-full w-full bg-white bg-opacity-60" />
|
|
261
|
+
|
|
262
|
+
<div class="pt-20">
|
|
263
|
+
<BaseSpinnerLarge class="h-10 w-10 text-blue-500" />
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</Transition>
|
|
267
|
+
</slot>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</template>
|
|
274
|
+
|
|
275
|
+
<script lang="ts">
|
|
276
|
+
export default {
|
|
277
|
+
name: 'BaseDataTableTemplate',
|
|
278
|
+
inheritAttrs: false,
|
|
279
|
+
};
|
|
280
|
+
</script>
|
|
281
|
+
|
|
282
|
+
<script lang="ts" setup>
|
|
283
|
+
import { PropType, ref } from 'vue';
|
|
284
|
+
import { BaseTableColumn, Row } from '@/types';
|
|
285
|
+
import SlotComponent from './SlotComponent';
|
|
286
|
+
import { useResizeObserver, useScroll } from '@vueuse/core';
|
|
287
|
+
import { debounce, isArray } from 'lodash';
|
|
288
|
+
import BaseSpinnerLarge from '../svg/BaseSpinnerLarge.vue';
|
|
289
|
+
import { Size } from '@/utils/sizes';
|
|
290
|
+
|
|
291
|
+
const checkboxStyle =
|
|
292
|
+
'disabled:bg-slate-100 group-hover:shadow-md disabled:border-slate-300 disabled:cursor-not-allowed duration-300 cursor-pointer focus:ring-blue-300 border border-slate-300 shadow h-[18px] w-[18px] rounded';
|
|
293
|
+
const DETAIL_ROW_WIDTH = 36;
|
|
294
|
+
const CHECK_ROW_WIDTH = 36;
|
|
295
|
+
|
|
296
|
+
provide('table', getCurrentInstance());
|
|
297
|
+
|
|
298
|
+
const props = defineProps({
|
|
299
|
+
/** Table data */
|
|
300
|
+
data: {
|
|
301
|
+
type: Array as PropType<Row[]>,
|
|
302
|
+
default: () => [],
|
|
303
|
+
},
|
|
304
|
+
/** Loading state */
|
|
305
|
+
loading: {
|
|
306
|
+
default: false,
|
|
307
|
+
type: Boolean,
|
|
308
|
+
},
|
|
309
|
+
visibleColumns: {
|
|
310
|
+
default: undefined,
|
|
311
|
+
type: Array as PropType<number[]>,
|
|
312
|
+
},
|
|
313
|
+
/** Allow row details */
|
|
314
|
+
detailed: {
|
|
315
|
+
default: false,
|
|
316
|
+
type: Boolean,
|
|
317
|
+
},
|
|
318
|
+
/** Rows can be checked (multiple) */
|
|
319
|
+
checkable: {
|
|
320
|
+
default: false,
|
|
321
|
+
type: Boolean,
|
|
322
|
+
},
|
|
323
|
+
/**
|
|
324
|
+
* Position of the checkbox (if checkable is true)
|
|
325
|
+
* @values left, right
|
|
326
|
+
*/
|
|
327
|
+
checkboxPosition: {
|
|
328
|
+
type: String as PropType<'left' | 'right'>,
|
|
329
|
+
default: 'left',
|
|
330
|
+
},
|
|
331
|
+
/** Custom method to verify if a row is checkable, works when is checkable */
|
|
332
|
+
isRowCheckable: {
|
|
333
|
+
type: Function,
|
|
334
|
+
default: () => true,
|
|
335
|
+
},
|
|
336
|
+
/** Set which rows are checked, use v-model:checkedRows to make it two-way binding */
|
|
337
|
+
checkedRows: {
|
|
338
|
+
default: () => [],
|
|
339
|
+
type: Array as PropType<Row[]>,
|
|
340
|
+
},
|
|
341
|
+
/** Sets the default sort column field */
|
|
342
|
+
sortField: {
|
|
343
|
+
type: String,
|
|
344
|
+
default: '',
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* Sets the default sort column direction
|
|
348
|
+
* @values asc, desc
|
|
349
|
+
*/
|
|
350
|
+
sortDirection: {
|
|
351
|
+
type: String,
|
|
352
|
+
default: 'asc',
|
|
353
|
+
},
|
|
354
|
+
/** Controls the visibility of the trigger that toggles the detailed rows. */
|
|
355
|
+
hasDetailedVisible: {
|
|
356
|
+
type: Function,
|
|
357
|
+
default: () => true,
|
|
358
|
+
},
|
|
359
|
+
/** Use a unique key of your data Object when use detailed or opened detailed. (id recommended) */
|
|
360
|
+
rowKey: {
|
|
361
|
+
type: String,
|
|
362
|
+
default: 'id',
|
|
363
|
+
},
|
|
364
|
+
/* Transition name to use when toggling row details. */
|
|
365
|
+
detailTransition: {
|
|
366
|
+
type: String,
|
|
367
|
+
default: '',
|
|
368
|
+
},
|
|
369
|
+
/* Max height (in px) */
|
|
370
|
+
maxHeight: {
|
|
371
|
+
default: undefined,
|
|
372
|
+
type: Number,
|
|
373
|
+
},
|
|
374
|
+
size: {
|
|
375
|
+
type: String as PropType<Size>,
|
|
376
|
+
default: 'md',
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const emit = defineEmits([
|
|
381
|
+
'check',
|
|
382
|
+
'check-all',
|
|
383
|
+
'update:checkedRows',
|
|
384
|
+
'details-open',
|
|
385
|
+
'details-close',
|
|
386
|
+
'update:openedDetailed',
|
|
387
|
+
'sort',
|
|
388
|
+
'cell-click',
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
const visibleDetailRows = ref<Row[]>([]);
|
|
392
|
+
// eslint-disable-next-line vue/no-setup-props-destructure
|
|
393
|
+
const newCheckedRows = ref<Row[]>([...props.checkedRows]);
|
|
394
|
+
const lastCheckedRowIndex = ref<number | null>(null);
|
|
395
|
+
const currentSortColumn = ref<BaseTableColumn | null>(null);
|
|
396
|
+
const isAsc = ref(true);
|
|
397
|
+
const defaultSlots = ref<BaseTableColumn[]>([]);
|
|
398
|
+
const sequence = ref(1);
|
|
399
|
+
|
|
400
|
+
const slot = ref<HTMLElement | null>(null);
|
|
401
|
+
const thead = ref<HTMLElement | null>(null);
|
|
402
|
+
const theadHeight = ref(0);
|
|
403
|
+
|
|
404
|
+
useResizeObserver(thead, () => setTheadHeightDebounce());
|
|
405
|
+
|
|
406
|
+
const setTheadHeightDebounce = debounce(() => {
|
|
407
|
+
setTheadHeight();
|
|
408
|
+
}, 100);
|
|
409
|
+
|
|
410
|
+
function setTheadHeight() {
|
|
411
|
+
if (thead.value) {
|
|
412
|
+
theadHeight.value = thead.value.clientHeight;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const newColumns = computed(() => {
|
|
417
|
+
return defaultSlots.value;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const visibleColumns = computed(() => {
|
|
421
|
+
if (!newColumns.value) {
|
|
422
|
+
return newColumns.value;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return newColumns.value.filter((column: BaseTableColumn) => {
|
|
426
|
+
if (column.toggle === false) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!isArray(props.visibleColumns)) {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (props.visibleColumns.includes(column.newKey)) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return false;
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Return total column count based if it's checkable or expanded
|
|
444
|
+
*/
|
|
445
|
+
const columnCount = computed(() => {
|
|
446
|
+
let count = visibleColumns.value.length;
|
|
447
|
+
count += props.checkable ? 1 : 0;
|
|
448
|
+
count += props.detailed ? 1 : 0;
|
|
449
|
+
|
|
450
|
+
return count;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Return if detailed row tabled
|
|
455
|
+
* will be with chevron column & icon or not
|
|
456
|
+
*/
|
|
457
|
+
const showDetailRowIcon = computed(() => {
|
|
458
|
+
return props.detailed;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* When checkedRows prop change, update internal value without
|
|
463
|
+
* mutating original data.
|
|
464
|
+
*/
|
|
465
|
+
watch(
|
|
466
|
+
() => props.checkedRows,
|
|
467
|
+
(rows) => {
|
|
468
|
+
newCheckedRows.value = [...rows];
|
|
469
|
+
},
|
|
470
|
+
{ deep: true }
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
watch(
|
|
474
|
+
() => props.sortField,
|
|
475
|
+
() => {
|
|
476
|
+
updateSortState();
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
watch(
|
|
481
|
+
() => props.sortDirection,
|
|
482
|
+
() => {
|
|
483
|
+
updateSortState();
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
onMounted(() => {
|
|
488
|
+
nextTick(() => {
|
|
489
|
+
updateSortState();
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Sort the column.
|
|
495
|
+
* Toggle current direction on column if it's sortable
|
|
496
|
+
* and not just updating the prop.
|
|
497
|
+
*/
|
|
498
|
+
function sort(column: BaseTableColumn, updatingData = false, event = null) {
|
|
499
|
+
if (!column || !column.sortable) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!updatingData) {
|
|
504
|
+
isAsc.value =
|
|
505
|
+
column === currentSortColumn.value
|
|
506
|
+
? !isAsc.value
|
|
507
|
+
: props.sortDirection.toLowerCase() !== 'desc';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* @property {string} field column field
|
|
512
|
+
* @property {boolean} direction 'asc' or 'desc'
|
|
513
|
+
* @property {Event} event native event
|
|
514
|
+
*/
|
|
515
|
+
emit('sort', column.field, isAsc.value ? 'asc' : 'desc', event);
|
|
516
|
+
|
|
517
|
+
currentSortColumn.value = column;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Check if the row is checked (is added to the array).
|
|
522
|
+
*/
|
|
523
|
+
function isRowChecked(row: Row): boolean {
|
|
524
|
+
return (
|
|
525
|
+
newCheckedRows.value.find((r) => r[props.rowKey] == row[props.rowKey]) !==
|
|
526
|
+
undefined
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Check if all rows in the page are checkable.
|
|
532
|
+
*/
|
|
533
|
+
const isAllUncheckable = computed(() => {
|
|
534
|
+
const validData = props.data.filter((row) => props.isRowCheckable(row));
|
|
535
|
+
return validData.length === 0;
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Check if all rows in the page are checked.
|
|
540
|
+
*/
|
|
541
|
+
const isAllChecked = computed(() => {
|
|
542
|
+
const validData = props.data.filter((row) => {
|
|
543
|
+
return props.isRowCheckable(row);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
if (validData.length === 0) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const missingChecked = validData.some((currentRow) => {
|
|
551
|
+
return !isRowChecked(currentRow);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
return !missingChecked;
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
function getCheckedRowIndex(row: Row) {
|
|
558
|
+
return newCheckedRows.value.findIndex(
|
|
559
|
+
(r) => r[props.rowKey] == row[props.rowKey]
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Remove a checked row from the array.
|
|
565
|
+
*/
|
|
566
|
+
function removeCheckedRow(row: Row) {
|
|
567
|
+
const index = getCheckedRowIndex(row);
|
|
568
|
+
if (index >= 0) {
|
|
569
|
+
newCheckedRows.value.splice(index, 1);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Header checkbox click listener.
|
|
575
|
+
* Add or remove all rows in current page.
|
|
576
|
+
*/
|
|
577
|
+
function checkAll() {
|
|
578
|
+
if (isAllChecked.value) {
|
|
579
|
+
newCheckedRows.value = [];
|
|
580
|
+
} else {
|
|
581
|
+
props.data.forEach((currentRow) => {
|
|
582
|
+
if (props.isRowCheckable(currentRow) && !isRowChecked(currentRow)) {
|
|
583
|
+
newCheckedRows.value.push(currentRow);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
sendCheckUpdate();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Remove all rows in current page.
|
|
593
|
+
*/
|
|
594
|
+
function uncheckAll() {
|
|
595
|
+
newCheckedRows.value = [];
|
|
596
|
+
|
|
597
|
+
sendCheckUpdate();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function sendCheckUpdate() {
|
|
601
|
+
emit('check', newCheckedRows.value);
|
|
602
|
+
emit('check-all', newCheckedRows.value);
|
|
603
|
+
emit('update:checkedRows', newCheckedRows.value);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Row checkbox click listener.
|
|
608
|
+
*/
|
|
609
|
+
function checkRow(row: Row, index: number, event: MouseEvent) {
|
|
610
|
+
if (!props.isRowCheckable(row)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const lastIndex = lastCheckedRowIndex.value;
|
|
615
|
+
lastCheckedRowIndex.value = index;
|
|
616
|
+
|
|
617
|
+
if (event.shiftKey && lastIndex !== null && index !== lastIndex) {
|
|
618
|
+
shiftCheckRow(row, index, lastIndex);
|
|
619
|
+
} else if (!isRowChecked(row)) {
|
|
620
|
+
newCheckedRows.value.push(row);
|
|
621
|
+
} else {
|
|
622
|
+
removeCheckedRow(row);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
emit('check', newCheckedRows.value, row);
|
|
626
|
+
|
|
627
|
+
// Emit checked rows to update user variable
|
|
628
|
+
emit('update:checkedRows', newCheckedRows.value);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Check row when shift is pressed.
|
|
633
|
+
*/
|
|
634
|
+
function shiftCheckRow(row: Row, index: number, lastCheckedRowIndex: number) {
|
|
635
|
+
// Get the subset of the list between the two indices
|
|
636
|
+
const subset = props.data.slice(
|
|
637
|
+
Math.min(index, lastCheckedRowIndex),
|
|
638
|
+
Math.max(index, lastCheckedRowIndex) + 1
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// Determine the operation based on the state of the clicked checkbox
|
|
642
|
+
const shouldCheck = !isRowChecked(row);
|
|
643
|
+
|
|
644
|
+
subset.forEach((item) => {
|
|
645
|
+
removeCheckedRow(item);
|
|
646
|
+
if (shouldCheck && props.isRowCheckable(item)) {
|
|
647
|
+
newCheckedRows.value.push(item);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Toggle to show/hide details slot
|
|
654
|
+
*/
|
|
655
|
+
function toggleDetails(row: Row) {
|
|
656
|
+
const found = isVisibleDetailRow(row);
|
|
657
|
+
|
|
658
|
+
if (found) {
|
|
659
|
+
closeDetailRow(row);
|
|
660
|
+
emit('details-close', row);
|
|
661
|
+
} else {
|
|
662
|
+
openDetailRow(row);
|
|
663
|
+
emit('details-open', row);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Syncs the detailed rows with the parent component
|
|
667
|
+
emit('update:openedDetailed', visibleDetailRows.value);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function openDetailRow(row: Row) {
|
|
671
|
+
const index = getDetailedIndex(row);
|
|
672
|
+
visibleDetailRows.value.push(index);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function closeDetailRow(row: Row) {
|
|
676
|
+
const index = getDetailedIndex(row);
|
|
677
|
+
const i = visibleDetailRows.value.indexOf(index);
|
|
678
|
+
if (i >= 0) {
|
|
679
|
+
visibleDetailRows.value.splice(i, 1);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isVisibleDetailRow(row: Row) {
|
|
684
|
+
const index = getDetailedIndex(row);
|
|
685
|
+
return visibleDetailRows.value.indexOf(index) >= 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isActiveDetailRow(row: Row) {
|
|
689
|
+
return props.detailed && isVisibleDetailRow(row);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* When the rowKey is defined we use the object[rowKey] as index.
|
|
694
|
+
* If not, use the object reference by default.
|
|
695
|
+
*/
|
|
696
|
+
function getDetailedIndex(row: Row) {
|
|
697
|
+
const key = props.rowKey;
|
|
698
|
+
return !key.length || !row ? row : row[key];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Update sort state
|
|
703
|
+
*/
|
|
704
|
+
function updateSortState() {
|
|
705
|
+
const sortField = props.sortField;
|
|
706
|
+
|
|
707
|
+
const sortDirection = props.sortDirection;
|
|
708
|
+
|
|
709
|
+
const sortColumn = newColumns.value.filter(
|
|
710
|
+
(column) => column.field === sortField
|
|
711
|
+
)[0];
|
|
712
|
+
|
|
713
|
+
// Set sort state
|
|
714
|
+
|
|
715
|
+
if (sortColumn) {
|
|
716
|
+
currentSortColumn.value = sortColumn;
|
|
717
|
+
isAsc.value = sortDirection.toLowerCase() !== 'desc';
|
|
718
|
+
} else {
|
|
719
|
+
currentSortColumn.value = null;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/*
|
|
725
|
+
|--------------------------------------------------------------------------
|
|
726
|
+
| BaseTableColumns functions
|
|
727
|
+
|--------------------------------------------------------------------------
|
|
728
|
+
*/
|
|
729
|
+
|
|
730
|
+
function addColumn(column: BaseTableColumn) {
|
|
731
|
+
defaultSlots.value.push(column);
|
|
732
|
+
|
|
733
|
+
const slotHTMLElement = slot.value as HTMLElement;
|
|
734
|
+
|
|
735
|
+
if (slotHTMLElement && slotHTMLElement.children) {
|
|
736
|
+
nextTick(() => {
|
|
737
|
+
const ids = defaultSlots.value
|
|
738
|
+
.map((it) => `[data-id="${it.newKey}"]`)
|
|
739
|
+
.join(',');
|
|
740
|
+
|
|
741
|
+
const sortedIds = Array.from(slotHTMLElement.querySelectorAll(ids)).map(
|
|
742
|
+
(el: Element) => el.getAttribute('data-id')
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
defaultSlots.value = defaultSlots.value.sort((a, b) => {
|
|
746
|
+
return (
|
|
747
|
+
sortedIds.indexOf(`${a.newKey}`) - sortedIds.indexOf(`${b.newKey}`)
|
|
748
|
+
);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function removeColumn(column: BaseTableColumn) {
|
|
755
|
+
defaultSlots.value = defaultSlots.value.filter(
|
|
756
|
+
(d) => d.newKey !== column.newKey
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const borderClasses = 'border-b border-slate-200';
|
|
761
|
+
|
|
762
|
+
function borderBottomClasses(index: number, row: Record<string, any>): string {
|
|
763
|
+
if (index < props.data.length - 1) {
|
|
764
|
+
return borderClasses;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (isActiveDetailRow(row)) {
|
|
768
|
+
return borderClasses;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return '';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function borderBottomDetailClasses(index: number): string {
|
|
775
|
+
if (index < props.data.length - 1) {
|
|
776
|
+
return borderClasses;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return '';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function onColumnClick(
|
|
783
|
+
row: Row,
|
|
784
|
+
column: BaseTableColumn,
|
|
785
|
+
index: number,
|
|
786
|
+
colindex: number,
|
|
787
|
+
event: MouseEvent
|
|
788
|
+
) {
|
|
789
|
+
if (column.clickable) {
|
|
790
|
+
emit('cell-click', row, column, index, colindex, event);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function nextSequence() {
|
|
795
|
+
return sequence.value++;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function getRowIndex(row: Row, index: number): string {
|
|
799
|
+
if (row.id) {
|
|
800
|
+
return row.id;
|
|
801
|
+
}
|
|
802
|
+
if (row.key) {
|
|
803
|
+
return row.key;
|
|
804
|
+
}
|
|
805
|
+
if (row.uuid) {
|
|
806
|
+
return row.uuid;
|
|
807
|
+
}
|
|
808
|
+
return index + '';
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Sticky styles
|
|
812
|
+
|
|
813
|
+
const horizontalScrolling = ref(false);
|
|
814
|
+
const scrollable = ref<null | HTMLElement>(null);
|
|
815
|
+
|
|
816
|
+
const { x } = useScroll(scrollable);
|
|
817
|
+
watch(x, (value) => {
|
|
818
|
+
horizontalScrolling.value = value > 0;
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
function zIndex(th: boolean) {
|
|
822
|
+
if (th) {
|
|
823
|
+
return props.maxHeight ? 3 : 2;
|
|
824
|
+
}
|
|
825
|
+
return 1;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function detailsStyles(th: boolean): any {
|
|
829
|
+
if (props.detailed) {
|
|
830
|
+
return {
|
|
831
|
+
zIndex: zIndex(th) + 1,
|
|
832
|
+
position: 'sticky',
|
|
833
|
+
left: 0,
|
|
834
|
+
width: DETAIL_ROW_WIDTH + 'px',
|
|
835
|
+
minWidth: DETAIL_ROW_WIDTH + 'px',
|
|
836
|
+
maxWidth: DETAIL_ROW_WIDTH + 'px',
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
return {};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function checkStyles(th: boolean): any {
|
|
843
|
+
if (props.checkable) {
|
|
844
|
+
return {
|
|
845
|
+
zIndex: zIndex(th) + 1,
|
|
846
|
+
position: 'sticky',
|
|
847
|
+
left: props.detailed ? DETAIL_ROW_WIDTH + 'px' : 0,
|
|
848
|
+
width: CHECK_ROW_WIDTH + 'px',
|
|
849
|
+
minWidth: CHECK_ROW_WIDTH + 'px',
|
|
850
|
+
maxWidth: CHECK_ROW_WIDTH + 'px',
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
return {};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function firstColStyles(th: boolean): any {
|
|
857
|
+
let left = 0;
|
|
858
|
+
if (props.checkable) {
|
|
859
|
+
left += CHECK_ROW_WIDTH;
|
|
860
|
+
}
|
|
861
|
+
if (props.detailed) {
|
|
862
|
+
left += DETAIL_ROW_WIDTH;
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
zIndex: zIndex(th) + 1,
|
|
866
|
+
position: 'sticky',
|
|
867
|
+
left: left + 'px',
|
|
868
|
+
borderRight: horizontalScrolling.value ? '1px solid #e2e8f0' : 'none',
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function scrollTop() {
|
|
873
|
+
if (scrollable.value) {
|
|
874
|
+
scrollable.value.scrollTo({ top: 0, behavior: 'smooth' });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
provide('addColumn', addColumn);
|
|
879
|
+
provide('removeColumn', removeColumn);
|
|
880
|
+
provide('nextSequence', nextSequence);
|
|
881
|
+
|
|
882
|
+
defineExpose({
|
|
883
|
+
newColumns,
|
|
884
|
+
uncheckAll,
|
|
885
|
+
scrollTop,
|
|
886
|
+
});
|
|
887
|
+
</script>
|