innoboxrr-vue-datatable 2.2.0 → 3.0.0
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/README.md +130 -168
- package/index.js +13 -2
- package/package.json +6 -3
- package/src/DataTable.vue +249 -423
- package/src/components/DataTableComponent.vue +218 -176
- package/src/components/SelectPaginationComponent.vue +78 -93
- package/src/table.js +251 -0
- package/src/useDataTable.js +370 -0
- package/src/useTheme.js +20 -0
- package/src/components/DisabledLinkComponent.vue +0 -45
- package/src/components/IconLinkComponent.vue +0 -81
- package/src/components/IconRouteComponent.vue +0 -84
- package/src/components/NavDropdownComponent.vue +0 -55
- package/src/components/PaginationComponent.vue +0 -79
|
@@ -1,183 +1,225 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
2
|
+
|
|
3
|
+
<div :class="theme.tableContainer">
|
|
4
|
+
|
|
5
|
+
<table :class="[theme.table, theme.tableSticky]" :aria-busy="loading ? 'true' : 'false'">
|
|
6
|
+
|
|
7
|
+
<thead v-if="showTableHeader">
|
|
8
|
+
<tr>
|
|
9
|
+
<th v-if="selectable" scope="col" :class="theme.tableSelect">
|
|
10
|
+
<input
|
|
11
|
+
type="checkbox"
|
|
12
|
+
:class="theme.checkbox"
|
|
13
|
+
:aria-label="labels.selectAll"
|
|
14
|
+
:checked="allSelected"
|
|
15
|
+
:indeterminate="someSelected"
|
|
16
|
+
:disabled="rows.length === 0"
|
|
17
|
+
@change="table.toggleAllPageRowsSelected($event.target.checked)">
|
|
18
|
+
</th>
|
|
19
|
+
|
|
20
|
+
<th
|
|
21
|
+
v-for="column in head"
|
|
22
|
+
:id="`th_${column.id}`"
|
|
23
|
+
:key="column.id"
|
|
24
|
+
scope="col"
|
|
25
|
+
:class="column.numeric ? theme.tableNumeric : null"
|
|
26
|
+
:aria-sort="ariaSort(column, orderBy, sort)">
|
|
27
|
+
<button
|
|
28
|
+
v-if="column.sortable === true"
|
|
29
|
+
type="button"
|
|
30
|
+
:class="theme.tableSort"
|
|
31
|
+
@click="emit('sortColumn', column)">
|
|
32
|
+
<span>{{ column.value }}</span>
|
|
33
|
+
<IconComponent :name="sortIcon(column, orderBy, sort)" :size="12" />
|
|
34
|
+
</button>
|
|
35
|
+
<template v-else>{{ column.value }}</template>
|
|
36
|
+
</th>
|
|
37
|
+
|
|
38
|
+
<th v-if="actions" scope="col" :class="theme.tableSelect" :aria-label="labels.actions" />
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
|
|
42
|
+
<tbody>
|
|
43
|
+
|
|
44
|
+
<!-- Primera carga: la forma de las filas, mientras llegan. -->
|
|
45
|
+
<template v-if="loading && rows.length === 0">
|
|
46
|
+
<tr v-for="n in SKELETON_ROWS" :key="`skeleton-${n}`" data-skeleton="true">
|
|
47
|
+
<td v-if="selectable" :class="theme.tableSelect" />
|
|
48
|
+
<td v-for="column in head" :key="column.id">
|
|
49
|
+
<SkeletonComponent />
|
|
50
|
+
</td>
|
|
51
|
+
<td v-if="actions" :class="theme.tableSelect" />
|
|
52
|
+
</tr>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<!-- Sin filas se dice por qué: no es lo mismo vacío que prohibido. -->
|
|
56
|
+
<tr v-else-if="rows.length === 0">
|
|
57
|
+
<td :colspan="colspan" :class="theme.tableEmpty">
|
|
58
|
+
<template v-if="error">
|
|
59
|
+
<span role="alert">{{ error.message }}</span>
|
|
60
|
+
<button
|
|
61
|
+
v-if="error.retryable"
|
|
62
|
+
type="button"
|
|
63
|
+
:class="theme.buttonLink"
|
|
64
|
+
@click="emit('retry')">{{ labels.retry }}</button>
|
|
65
|
+
</template>
|
|
66
|
+
<template v-else>{{ labels.empty }}</template>
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
|
|
70
|
+
<template v-else>
|
|
71
|
+
<tr
|
|
72
|
+
v-for="row in table.getRowModel().rows"
|
|
73
|
+
:key="row.id"
|
|
74
|
+
:data-selected="row.getIsSelected() ? 'true' : undefined">
|
|
75
|
+
|
|
76
|
+
<td v-if="selectable" :class="theme.tableSelect">
|
|
77
|
+
<input
|
|
78
|
+
type="checkbox"
|
|
79
|
+
:class="theme.checkbox"
|
|
80
|
+
:aria-label="`${labels.selectRow} ${row.original.id}`"
|
|
81
|
+
:checked="row.getIsSelected()"
|
|
82
|
+
@click="row.getToggleSelectedHandler()($event)">
|
|
83
|
+
</td>
|
|
84
|
+
|
|
85
|
+
<td
|
|
86
|
+
v-for="cell in row.getVisibleCells()"
|
|
87
|
+
:key="cell.id"
|
|
88
|
+
:class="headOf(cell).numeric ? theme.tableNumeric : null">
|
|
89
|
+
<component
|
|
90
|
+
:is="componentFor(cell)"
|
|
91
|
+
v-if="componentFor(cell)"
|
|
92
|
+
v-bind="componentProps(valueFor(cell, row))"
|
|
93
|
+
@callback="onCallback(cell, row, $event)" />
|
|
94
|
+
<span v-else-if="headOf(cell).html" v-html="valueFor(cell, row)" />
|
|
95
|
+
<template v-else>{{ valueFor(cell, row) }}</template>
|
|
96
|
+
</td>
|
|
97
|
+
|
|
98
|
+
<td v-if="actions" :class="theme.tableSelect">
|
|
99
|
+
<MenuComponent
|
|
100
|
+
:items="itemsFor(row.original)"
|
|
101
|
+
:label="`${labels.rowActions} ${row.original.id}`"
|
|
102
|
+
:before-open="() => prepareRow(row.original.id)" />
|
|
103
|
+
</td>
|
|
104
|
+
|
|
105
|
+
</tr>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
</tbody>
|
|
109
|
+
|
|
110
|
+
</table>
|
|
111
|
+
|
|
112
|
+
</div>
|
|
113
|
+
|
|
96
114
|
</template>
|
|
97
115
|
|
|
98
116
|
<script setup>
|
|
99
117
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
118
|
+
/**
|
|
119
|
+
* La tabla propiamente dicha, sobre una instancia de TanStack Table.
|
|
120
|
+
*
|
|
121
|
+
* El menú de cada fila espera a conocer los permisos antes de abrirse.
|
|
122
|
+
* Antes se abría al instante con todo deshabilitado y se habilitaba cuando
|
|
123
|
+
* llegaba la respuesta —si llegaba: un fallo se reintentaba en silencio—,
|
|
124
|
+
* y el usuario veía parpadear lo que no podía hacer.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
import { computed } from 'vue'
|
|
128
|
+
import IconComponent from 'innoboxrr-form-elements/src/IconComponent.vue'
|
|
129
|
+
import MenuComponent from 'innoboxrr-form-elements/src/MenuComponent.vue'
|
|
130
|
+
import SkeletonComponent from 'innoboxrr-form-elements/src/SkeletonComponent.vue'
|
|
131
|
+
|
|
132
|
+
import useTheme from '../useTheme.js'
|
|
133
|
+
import { DEFAULT_LABELS, ariaSort, cellValue, componentProps, sortIcon } from '../table.js'
|
|
134
|
+
|
|
135
|
+
const SKELETON_ROWS = 5
|
|
136
|
+
|
|
137
|
+
const props = defineProps({
|
|
138
|
+
table: {
|
|
139
|
+
type: Object,
|
|
140
|
+
required: true,
|
|
141
|
+
},
|
|
142
|
+
head: {
|
|
143
|
+
type: Array,
|
|
144
|
+
default: () => [],
|
|
145
|
+
},
|
|
146
|
+
rows: {
|
|
147
|
+
type: Array,
|
|
148
|
+
default: () => [],
|
|
149
|
+
},
|
|
150
|
+
clones: {
|
|
151
|
+
type: Array,
|
|
152
|
+
default: () => [],
|
|
153
|
+
},
|
|
154
|
+
loading: {
|
|
155
|
+
type: Boolean,
|
|
156
|
+
default: false,
|
|
157
|
+
},
|
|
158
|
+
error: {
|
|
159
|
+
type: Object,
|
|
160
|
+
default: null,
|
|
161
|
+
},
|
|
162
|
+
actions: {
|
|
163
|
+
type: Boolean,
|
|
164
|
+
default: false,
|
|
165
|
+
},
|
|
166
|
+
selectable: {
|
|
167
|
+
type: Boolean,
|
|
168
|
+
default: false,
|
|
169
|
+
},
|
|
170
|
+
showTableHeader: {
|
|
171
|
+
type: Boolean,
|
|
172
|
+
default: true,
|
|
173
|
+
},
|
|
174
|
+
dataTableComponents: {
|
|
175
|
+
type: Object,
|
|
176
|
+
default: () => ({}),
|
|
177
|
+
},
|
|
178
|
+
labels: {
|
|
179
|
+
type: Object,
|
|
180
|
+
default: () => DEFAULT_LABELS,
|
|
181
|
+
},
|
|
182
|
+
orderBy: {
|
|
183
|
+
type: String,
|
|
184
|
+
default: null,
|
|
185
|
+
},
|
|
186
|
+
sort: {
|
|
187
|
+
type: Object,
|
|
188
|
+
default: () => ({}),
|
|
189
|
+
},
|
|
190
|
+
itemsFor: {
|
|
191
|
+
type: Function,
|
|
192
|
+
default: () => [],
|
|
193
|
+
},
|
|
194
|
+
prepareRow: {
|
|
195
|
+
type: Function,
|
|
196
|
+
default: async () => {},
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const emit = defineEmits(['sortColumn', 'retry'])
|
|
201
|
+
|
|
202
|
+
const theme = useTheme()
|
|
203
|
+
|
|
204
|
+
const colspan = computed(() => props.head.length + (props.selectable ? 1 : 0) + (props.actions ? 1 : 0))
|
|
205
|
+
|
|
206
|
+
const allSelected = computed(() => props.rows.length > 0 && props.table.getIsAllPageRowsSelected())
|
|
207
|
+
const someSelected = computed(() => props.table.getIsSomePageRowsSelected() && ! allSelected.value)
|
|
208
|
+
|
|
209
|
+
const headOf = (cell) => cell.column.columnDef.meta?.head ?? {}
|
|
210
|
+
|
|
211
|
+
const componentFor = (cell) => {
|
|
212
|
+
const name = headOf(cell).component
|
|
213
|
+
|
|
214
|
+
return name ? (props.dataTableComponents[name] ?? null) : null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const valueFor = (cell, row) => cellValue(headOf(cell), props.clones[row.index] ?? row.original)
|
|
218
|
+
|
|
219
|
+
const onCallback = (cell, row, payload) => {
|
|
220
|
+
const callback = headOf(cell).callback
|
|
221
|
+
|
|
222
|
+
return typeof callback === 'function' ? callback(payload, row.original) : null
|
|
223
|
+
}
|
|
175
224
|
|
|
176
225
|
</script>
|
|
177
|
-
|
|
178
|
-
<style>
|
|
179
|
-
.fe-table td {
|
|
180
|
-
padding: 12px 14px;
|
|
181
|
-
vertical-align: middle;
|
|
182
|
-
}
|
|
183
|
-
</style>
|
|
@@ -1,110 +1,95 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div :class="theme.tableFooter">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<span>{{ summaryText }}</span>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<div v-if="last > 1" :class="theme.tablePager">
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
:class="theme.iconButton"
|
|
12
|
+
:aria-label="text.previous"
|
|
13
|
+
:disabled="current <= 1"
|
|
14
|
+
@click="go(current - 1)">
|
|
15
|
+
<IconComponent name="previous" :size="14" />
|
|
16
|
+
</button>
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
<select
|
|
19
|
+
:class="theme.select"
|
|
20
|
+
:aria-label="text.page"
|
|
21
|
+
:value="current"
|
|
22
|
+
@change="go($event.target.value)">
|
|
23
|
+
<option v-for="page in last" :key="page" :value="page">{{ page }}</option>
|
|
24
|
+
</select>
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
<span>{{ text.of }} {{ last }}</span>
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
:class="theme.iconButton"
|
|
31
|
+
:aria-label="text.next"
|
|
32
|
+
:disabled="current >= last"
|
|
33
|
+
@click="go(current + 1)">
|
|
34
|
+
<IconComponent name="next" :size="14" />
|
|
35
|
+
</button>
|
|
16
36
|
|
|
17
|
-
|
|
37
|
+
</div>
|
|
18
38
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</ul>
|
|
22
|
-
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<div class="fe-w-expand">
|
|
26
|
-
|
|
27
|
-
<ul class="fe-pagination fe-justify-end fe-mt-md" fe-mb>
|
|
28
|
-
|
|
29
|
-
<!-- Prev -->
|
|
30
|
-
<li v-if="currentPage > 1">
|
|
31
|
-
|
|
32
|
-
<a href="#" @click="prevPage()">
|
|
33
|
-
|
|
34
|
-
<span fe-page-prev></span>
|
|
35
|
-
|
|
36
|
-
</a>
|
|
37
|
-
|
|
38
|
-
</li>
|
|
39
|
-
|
|
40
|
-
<li>
|
|
41
|
-
<select
|
|
42
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
43
|
-
v-model="currentPage">
|
|
44
|
-
|
|
45
|
-
<option
|
|
46
|
-
v-for="page in meta.last_page"
|
|
47
|
-
:key="'page_' + page"
|
|
48
|
-
:value="page">{{ page }}</option>
|
|
49
|
-
|
|
50
|
-
</select>
|
|
51
|
-
</li>
|
|
52
|
-
|
|
53
|
-
<!-- Next -->
|
|
54
|
-
<li v-if="currentPage < meta.last_page">
|
|
55
|
-
|
|
56
|
-
<a href="#" @click="nextPage()">
|
|
57
|
-
|
|
58
|
-
<span fe-page-next></span>
|
|
59
|
-
|
|
60
|
-
</a>
|
|
61
|
-
|
|
62
|
-
</li>
|
|
63
|
-
|
|
64
|
-
</ul>
|
|
65
|
-
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
</div>
|
|
39
|
+
</div>
|
|
69
40
|
|
|
70
41
|
</template>
|
|
71
42
|
|
|
72
43
|
<script setup>
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
45
|
+
/**
|
|
46
|
+
* El pie de la tabla: cuántos registros se ven y en qué página se está.
|
|
47
|
+
*
|
|
48
|
+
* No guarda la página. Antes tenía su propia copia: al reiniciar los
|
|
49
|
+
* filtros el padre volvía a la 1, el paginador se enteraba después y la
|
|
50
|
+
* devolvía, y eso era una segunda petición idéntica.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { computed } from 'vue'
|
|
54
|
+
import IconComponent from 'innoboxrr-form-elements/src/IconComponent.vue'
|
|
55
|
+
|
|
56
|
+
import useTheme from '../useTheme.js'
|
|
57
|
+
import { DEFAULT_LABELS, summary } from '../table.js'
|
|
58
|
+
|
|
59
|
+
const props = defineProps({
|
|
60
|
+
meta: {
|
|
61
|
+
type: Object,
|
|
62
|
+
default: () => ({}),
|
|
63
|
+
},
|
|
64
|
+
// Se sigue aceptando para no romper a quien lo pasa; la página sale de
|
|
65
|
+
// `meta`.
|
|
66
|
+
links: {
|
|
67
|
+
type: [Array, Object],
|
|
68
|
+
default: () => [],
|
|
69
|
+
},
|
|
70
|
+
labels: {
|
|
71
|
+
type: Object,
|
|
72
|
+
default: () => ({}),
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const emit = defineEmits(['updatePage'])
|
|
77
|
+
|
|
78
|
+
const theme = useTheme()
|
|
79
|
+
|
|
80
|
+
const text = computed(() => ({ ...DEFAULT_LABELS, ...props.labels }))
|
|
81
|
+
|
|
82
|
+
const current = computed(() => Number(props.meta?.current_page ?? 1))
|
|
83
|
+
const last = computed(() => Number(props.meta?.last_page ?? 1))
|
|
84
|
+
|
|
85
|
+
const summaryText = computed(() => summary(props.meta ?? {}, text.value))
|
|
86
|
+
|
|
87
|
+
const go = (value) => {
|
|
88
|
+
const page = Number(value)
|
|
89
|
+
|
|
90
|
+
if (page >= 1 && page <= last.value && page !== current.value) {
|
|
91
|
+
emit('updatePage', page)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
109
94
|
|
|
110
95
|
</script>
|