tablevu 0.3.0-beta.3 → 0.3.0-beta.4
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 +25 -65
- package/dist/adapters/array.d.ts +3 -0
- package/dist/adapters/odata.d.ts +4 -0
- package/dist/index.d.ts +4 -5
- package/dist/table.d.ts +168 -46
- package/dist/tablevu.cjs +765 -0
- package/dist/tablevu.esm.js +759 -0
- package/dist/types/adapter.d.ts +1 -1
- package/dist/types/index.d.ts +12 -11
- package/dist/types/odata.d.ts +27 -0
- package/dist/types/tablevu-column.d.ts +2 -2
- package/dist/types/tablevu-emits.d.ts +2 -6
- package/dist/types/{tablevu-footer-settings.d.ts → tablevu-footer.d.ts} +5 -0
- package/dist/types/tablevu-page.d.ts +10 -0
- package/dist/types/tablevu-props.d.ts +10 -4
- package/dist/types/tablevu-settings.d.ts +2 -13
- package/dist/types/tablevu-spec.d.ts +6 -11
- package/package.json +10 -29
- package/dist/array.d.ts +0 -14
- package/dist/odata.d.ts +0 -40
- package/dist/tablevu-index.esm.js +0 -1102
- package/dist/tablevu-index.umd.cjs.js +0 -1112
- package/dist/tablevu-odata.esm.js +0 -507
- package/dist/tablevu-odata.umd.cjs.js +0 -518
- package/dist/tablevu-table.esm.js +0 -578
- package/dist/tablevu-table.umd.cjs.js +0 -586
- package/dist/tablevu-webapi.esm.js +0 -19
- package/dist/tablevu-webapi.umd.cjs.js +0 -29
- package/dist/types/tablevu-page-settings.d.ts +0 -4
- package/dist/webapi.d.ts +0 -15
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { defineComponent, h, reactive, watch, onBeforeUnmount, ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
const getPath = (row, path) => path.split('.').reduce((value, key) => value?.[key], row);
|
|
4
|
+
const adaptArray = async (props, data) => {
|
|
5
|
+
const d = Array.isArray(data) ? [...data] : [];
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
try {
|
|
8
|
+
// Filter & Search //
|
|
9
|
+
if (props.filter || props.search) {
|
|
10
|
+
const searchableCols = props.cols
|
|
11
|
+
?.map((m, i) => ({ index: i, searchable: m.searchable !== false }))
|
|
12
|
+
.filter(q => q.searchable)
|
|
13
|
+
.map(m => m.index) || [];
|
|
14
|
+
let filtered = d;
|
|
15
|
+
if (props.filter) {
|
|
16
|
+
filtered = filtered.filter(row => {
|
|
17
|
+
return Object.entries(props.filter).every(([key, value]) => {
|
|
18
|
+
return getPath(row, key) === value;
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (props.search && searchableCols.length > 0) {
|
|
23
|
+
const searchLower = props.search.toLowerCase();
|
|
24
|
+
filtered = filtered.filter(row => {
|
|
25
|
+
return searchableCols.some(colIndex => {
|
|
26
|
+
const col = props.cols?.[colIndex];
|
|
27
|
+
if (!col)
|
|
28
|
+
return false;
|
|
29
|
+
const val = getPath(row, col.name);
|
|
30
|
+
return val != null && String(val).toLowerCase().includes(searchLower);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
d.length = 0;
|
|
35
|
+
d.push(...filtered);
|
|
36
|
+
}
|
|
37
|
+
// Sort //
|
|
38
|
+
if (props.sort && props.sort.length > 0) {
|
|
39
|
+
for (let i = props.sort.length - 1; i >= 0; i--) {
|
|
40
|
+
const s = props.sort[i];
|
|
41
|
+
let field;
|
|
42
|
+
if (s.index !== undefined && s.index >= 0 && props.cols?.[s.index]) {
|
|
43
|
+
field = props.cols[s.index].name;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
field = s.name || '';
|
|
47
|
+
}
|
|
48
|
+
if (!field)
|
|
49
|
+
continue;
|
|
50
|
+
const dir = s.dir === 'desc' ? -1 : 1;
|
|
51
|
+
d.sort((a, b) => {
|
|
52
|
+
const av = a[field];
|
|
53
|
+
const bv = b[field];
|
|
54
|
+
if (av === bv)
|
|
55
|
+
return 0;
|
|
56
|
+
if (av === null || av === undefined)
|
|
57
|
+
return 1;
|
|
58
|
+
if (bv === null || bv === undefined)
|
|
59
|
+
return -1;
|
|
60
|
+
return (av > bv ? 1 : -1) * dir;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Paging //
|
|
65
|
+
let result = d;
|
|
66
|
+
if (props.pageSize && props.pageSize > 0) {
|
|
67
|
+
const currentPage = Math.max(1, props.currPage ?? 1);
|
|
68
|
+
const start = (currentPage - 1) * props.pageSize;
|
|
69
|
+
const end = currentPage * props.pageSize;
|
|
70
|
+
result = d.slice(start, end);
|
|
71
|
+
}
|
|
72
|
+
resolve({ rows: result, count: d.length });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
reject(err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const TablevuStatus = {
|
|
81
|
+
Error: -1,
|
|
82
|
+
Ready: 0,
|
|
83
|
+
Loading: 1
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const tablevu = defineComponent({
|
|
87
|
+
name: 'TableVu',
|
|
88
|
+
props: {
|
|
89
|
+
settings: { type: Object, default: null },
|
|
90
|
+
data: { type: [Array, Function] },
|
|
91
|
+
group: { type: [String, Object] },
|
|
92
|
+
search: { type: String },
|
|
93
|
+
filter: { type: [Array, Object] },
|
|
94
|
+
command: { type: Array, default: () => [] },
|
|
95
|
+
rowid: { type: String },
|
|
96
|
+
onRowClick: { type: Function },
|
|
97
|
+
onRowDblClick: { type: Function },
|
|
98
|
+
class: { type: String, default: 'table' },
|
|
99
|
+
style: { type: [String, Object], default: null }
|
|
100
|
+
},
|
|
101
|
+
emits: ['row-click', 'row-dbl-click'],
|
|
102
|
+
setup(props, { emit, expose }) {
|
|
103
|
+
let rows = [];
|
|
104
|
+
let rowGroups = [];
|
|
105
|
+
let groups = [];
|
|
106
|
+
let adapter = null;
|
|
107
|
+
let searchTimer = null;
|
|
108
|
+
const state = reactive({
|
|
109
|
+
caption: null,
|
|
110
|
+
group: {
|
|
111
|
+
value: props.settings?.group?.value ?? (typeof props.group === 'string' ? props.group : undefined),
|
|
112
|
+
content: props.settings?.group?.content ?? (typeof props.group === 'object' ? props.group?.content : undefined),
|
|
113
|
+
expandAll: props.settings?.group?.expandAll ?? (typeof props.group === 'object' ? props.group?.expandAll : undefined) ?? true,
|
|
114
|
+
class: props.settings?.group?.class ?? (typeof props.group === 'object' ? props.group?.class : undefined) ?? 'table-light'
|
|
115
|
+
},
|
|
116
|
+
page: {
|
|
117
|
+
count: 0,
|
|
118
|
+
size: props.settings?.page?.size ?? 0,
|
|
119
|
+
current: 0,
|
|
120
|
+
type: null
|
|
121
|
+
},
|
|
122
|
+
sort: (props.settings && props.settings.sort) ? props.settings.sort : [],
|
|
123
|
+
columns: (props.settings && props.settings.columns) ? props.settings.columns : [],
|
|
124
|
+
footer: {
|
|
125
|
+
button: {
|
|
126
|
+
class: (props.settings && props.settings.footer && props.settings.footer.button && props.settings.footer.button.class) || 'btn'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
status: TablevuStatus.Ready,
|
|
130
|
+
time: null,
|
|
131
|
+
search: null,
|
|
132
|
+
filter: null,
|
|
133
|
+
onData: props.settings?.onData,
|
|
134
|
+
settings: props.settings ?? undefined
|
|
135
|
+
});
|
|
136
|
+
if (props.data) {
|
|
137
|
+
if (Array.isArray(props.data)) {
|
|
138
|
+
adapter = adaptArray;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
adapter = props.data;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (props.search)
|
|
145
|
+
state.search = props.search;
|
|
146
|
+
watch(() => props.search, (newVal, oldVal) => {
|
|
147
|
+
const minChar = parseInt(String(props.settings?.searchMinChar)) || 3;
|
|
148
|
+
if (searchTimer) {
|
|
149
|
+
clearTimeout(searchTimer);
|
|
150
|
+
searchTimer = null;
|
|
151
|
+
}
|
|
152
|
+
if (newVal && newVal.length > minChar) {
|
|
153
|
+
state.search = newVal;
|
|
154
|
+
searchTimer = setTimeout(() => { refresh(); searchTimer = null; }, 444);
|
|
155
|
+
}
|
|
156
|
+
else if (oldVal && oldVal.length > minChar && (newVal == undefined || newVal == null || newVal.length <= minChar)) {
|
|
157
|
+
state.search = null;
|
|
158
|
+
searchTimer = setTimeout(() => { refresh(); searchTimer = null; }, 444);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
onBeforeUnmount(() => {
|
|
162
|
+
if (searchTimer)
|
|
163
|
+
clearTimeout(searchTimer);
|
|
164
|
+
});
|
|
165
|
+
if (props.filter)
|
|
166
|
+
state.filter = props.filter;
|
|
167
|
+
watch(() => props.filter, () => {
|
|
168
|
+
state.filter = props.filter || null;
|
|
169
|
+
refresh();
|
|
170
|
+
}, { deep: true });
|
|
171
|
+
watch(() => props.command?.slice(), (newVal) => {
|
|
172
|
+
if (newVal && newVal.length > 0) {
|
|
173
|
+
const command = newVal[newVal.length - 1];
|
|
174
|
+
if (command && command.name) {
|
|
175
|
+
switch (command.name.toLowerCase()) {
|
|
176
|
+
case 'refresh':
|
|
177
|
+
refresh();
|
|
178
|
+
break;
|
|
179
|
+
case 'first':
|
|
180
|
+
first();
|
|
181
|
+
break;
|
|
182
|
+
case 'prior':
|
|
183
|
+
prior();
|
|
184
|
+
break;
|
|
185
|
+
case 'next':
|
|
186
|
+
next();
|
|
187
|
+
break;
|
|
188
|
+
case 'last':
|
|
189
|
+
last();
|
|
190
|
+
break;
|
|
191
|
+
case 'go':
|
|
192
|
+
if (typeof command.page === 'number')
|
|
193
|
+
go(command.page);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}, { deep: true });
|
|
199
|
+
function prepareGroup(groupDef, dataRows) {
|
|
200
|
+
groups = [];
|
|
201
|
+
rowGroups = [];
|
|
202
|
+
for (let i = 0; i < dataRows.length; i++) {
|
|
203
|
+
let groupValue = null;
|
|
204
|
+
let groupContent = null;
|
|
205
|
+
const row = dataRows[i];
|
|
206
|
+
if (groupDef.value) {
|
|
207
|
+
if (typeof groupDef.value == 'function')
|
|
208
|
+
groupValue = groupDef.value(row);
|
|
209
|
+
else
|
|
210
|
+
groupValue = row[groupDef.value];
|
|
211
|
+
}
|
|
212
|
+
else if (typeof groupDef == 'function') {
|
|
213
|
+
groupValue = groupDef(row);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
if (typeof groupDef == 'bigint' || typeof groupDef == 'number') {
|
|
217
|
+
const index = parseFloat(String(groupDef));
|
|
218
|
+
if (index)
|
|
219
|
+
groupValue = row[index];
|
|
220
|
+
}
|
|
221
|
+
else if (typeof groupDef == 'string')
|
|
222
|
+
groupValue = row[groupDef];
|
|
223
|
+
}
|
|
224
|
+
if (groupDef.content) {
|
|
225
|
+
if (typeof groupDef.content == 'function')
|
|
226
|
+
groupContent = groupDef.content(row);
|
|
227
|
+
else
|
|
228
|
+
groupContent = row[groupDef.content];
|
|
229
|
+
}
|
|
230
|
+
let groupIndex = groups.findIndex(g => g.group === groupValue);
|
|
231
|
+
if (groupIndex === -1) {
|
|
232
|
+
groupIndex = groups.push({ group: groupValue, content: groupContent, expand: state.group.expandAll }) - 1;
|
|
233
|
+
}
|
|
234
|
+
rowGroups.push(groupIndex);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function first() { return loadPage(1); }
|
|
238
|
+
function prior() {
|
|
239
|
+
return go(Math.max(1, state.page.current - 1));
|
|
240
|
+
}
|
|
241
|
+
function next() { return go(state.page.current + 1); }
|
|
242
|
+
function last() { return state.page.count > 0 ? go(state.page.count) : undefined; }
|
|
243
|
+
function go(pageNumber) {
|
|
244
|
+
if (!Number.isInteger(pageNumber) || pageNumber < 1)
|
|
245
|
+
return;
|
|
246
|
+
const target = state.page.count > 0 ? Math.min(pageNumber, state.page.count) : pageNumber;
|
|
247
|
+
if (target !== state.page.current)
|
|
248
|
+
return loadPage(target);
|
|
249
|
+
}
|
|
250
|
+
function choosePage() {
|
|
251
|
+
const input = prompt('Page number:', String(state.page.current));
|
|
252
|
+
if (input === null || input.trim() === '')
|
|
253
|
+
return;
|
|
254
|
+
const pageNumber = Number(input);
|
|
255
|
+
if (Number.isInteger(pageNumber) && pageNumber > 0)
|
|
256
|
+
go(pageNumber);
|
|
257
|
+
}
|
|
258
|
+
function refresh() { return loadPage(Math.max(1, state.page.current)); }
|
|
259
|
+
function sortBy(columnIndex, direction) {
|
|
260
|
+
if (state.sort && state.sort.length > 0) {
|
|
261
|
+
let sortIndex = state.sort.findIndex(s => s.index === columnIndex);
|
|
262
|
+
if (sortIndex < 0) {
|
|
263
|
+
const columnName = state.columns[columnIndex]?.name;
|
|
264
|
+
sortIndex = state.sort.findIndex(s => s.name === columnName);
|
|
265
|
+
}
|
|
266
|
+
if (sortIndex === 0) {
|
|
267
|
+
state.sort[sortIndex].dir = direction ? direction : (state.sort[0].dir ? null : 'desc');
|
|
268
|
+
}
|
|
269
|
+
else if (sortIndex > 0) {
|
|
270
|
+
state.sort.splice(sortIndex, 1);
|
|
271
|
+
}
|
|
272
|
+
if (sortIndex !== 0) {
|
|
273
|
+
state.sort.unshift({ index: columnIndex, dir: direction ? direction : null });
|
|
274
|
+
}
|
|
275
|
+
if (state.sort.length > 2) {
|
|
276
|
+
state.sort.pop();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
state.sort.push({ index: columnIndex, dir: direction ? direction : null });
|
|
281
|
+
}
|
|
282
|
+
return loadPage(Math.max(1, state.page.current));
|
|
283
|
+
}
|
|
284
|
+
let requestId = 0;
|
|
285
|
+
async function loadPage(pageNumber) {
|
|
286
|
+
let dataArray = null;
|
|
287
|
+
if (props.data) {
|
|
288
|
+
if (Array.isArray(props.data))
|
|
289
|
+
dataArray = props.data;
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
return;
|
|
293
|
+
const currentRequest = ++requestId;
|
|
294
|
+
state.status = TablevuStatus.Loading;
|
|
295
|
+
if (adapter) {
|
|
296
|
+
try {
|
|
297
|
+
const result = await adapter({
|
|
298
|
+
pageSize: state.page.size,
|
|
299
|
+
currPage: pageNumber,
|
|
300
|
+
sort: state.sort,
|
|
301
|
+
cols: state.columns,
|
|
302
|
+
filter: state.filter,
|
|
303
|
+
search: state.search
|
|
304
|
+
}, dataArray);
|
|
305
|
+
if (currentRequest !== requestId)
|
|
306
|
+
return;
|
|
307
|
+
if (state.onData)
|
|
308
|
+
rows = state.onData(result.rows);
|
|
309
|
+
else
|
|
310
|
+
rows = result.rows;
|
|
311
|
+
const groupValue = state.group.value;
|
|
312
|
+
if (groupValue && (typeof groupValue === 'string' || typeof groupValue === 'number' || typeof groupValue === 'bigint' || (typeof groupValue === 'object' && groupValue.value)))
|
|
313
|
+
prepareGroup(state.group, rows);
|
|
314
|
+
state.page.count = (result.count > 0 && state.page.size > 0) ? Math.ceil(result.count / state.page.size) : 0;
|
|
315
|
+
state.page.current = pageNumber;
|
|
316
|
+
state.status = TablevuStatus.Ready;
|
|
317
|
+
state.time = Date.now();
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (currentRequest !== requestId)
|
|
321
|
+
return;
|
|
322
|
+
console.error(error);
|
|
323
|
+
state.status = TablevuStatus.Error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function renderValue(column, value) {
|
|
328
|
+
if (value !== null && value !== undefined) {
|
|
329
|
+
let formatted;
|
|
330
|
+
if (column.type === 'datetime' || column.type === 'date' || column.type === 'time') {
|
|
331
|
+
const date = new Date(value);
|
|
332
|
+
if (column.type === 'date')
|
|
333
|
+
formatted = date.toLocaleDateString();
|
|
334
|
+
else if (column.type === 'time')
|
|
335
|
+
formatted = date.toLocaleTimeString();
|
|
336
|
+
else
|
|
337
|
+
formatted = date.toLocaleString();
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
if (column.type === 'numeric') {
|
|
341
|
+
formatted = parseFloat(value);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
if (column.type === 'integer') {
|
|
345
|
+
formatted = parseInt(value, 10);
|
|
346
|
+
}
|
|
347
|
+
else
|
|
348
|
+
formatted = value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return formatted;
|
|
352
|
+
}
|
|
353
|
+
else
|
|
354
|
+
return '';
|
|
355
|
+
}
|
|
356
|
+
function renderCell(column, value) {
|
|
357
|
+
const options = { style: {} };
|
|
358
|
+
if (column.colspan && column.colspan > 1)
|
|
359
|
+
options.colspan = column.colspan;
|
|
360
|
+
if (column.style)
|
|
361
|
+
options.style = { ...options.style, ...column.style };
|
|
362
|
+
if (column.align)
|
|
363
|
+
options.style['text-align'] = column.align;
|
|
364
|
+
if (column.width)
|
|
365
|
+
options.style['width'] = column.width;
|
|
366
|
+
const formatted = renderValue(column, value);
|
|
367
|
+
return h('td', options, formatted);
|
|
368
|
+
}
|
|
369
|
+
function renderCustomCell(column, row) {
|
|
370
|
+
const options = { style: {} };
|
|
371
|
+
if (column.colspan && column.colspan > 1)
|
|
372
|
+
options.colspan = column.colspan;
|
|
373
|
+
if (column.style)
|
|
374
|
+
options.style = { ...options.style, ...column.style };
|
|
375
|
+
if (column.align)
|
|
376
|
+
options.style['text-align'] = column.align;
|
|
377
|
+
if (column.width)
|
|
378
|
+
options.style['width'] = column.width;
|
|
379
|
+
const content = column.render?.(row);
|
|
380
|
+
return h('td', options, content);
|
|
381
|
+
}
|
|
382
|
+
function renderGroup(group) {
|
|
383
|
+
const expandButton = h('button', {
|
|
384
|
+
title: String(group.expand),
|
|
385
|
+
onClick: (event) => {
|
|
386
|
+
group.expand = !group.expand;
|
|
387
|
+
event.currentTarget.value = group.expand ? '▼' : '►';
|
|
388
|
+
state.time = Date.now();
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
},
|
|
391
|
+
style: { padding: 0, margin: '0 6px 0 0', width: '12px', height: '100%', border: 'none', outline: '0', backgroundColor: 'transparent' }
|
|
392
|
+
}, group.expand ? '▼' : '►');
|
|
393
|
+
const content = group.content || group.group;
|
|
394
|
+
return h('tr', {}, h('td', { colspan: state.columns.length, class: state.group.class }, [expandButton, content]));
|
|
395
|
+
}
|
|
396
|
+
function renderRow(rowIndex, row) {
|
|
397
|
+
const trProps = { key: rowIndex, index: rowIndex };
|
|
398
|
+
if (props.rowid)
|
|
399
|
+
trProps.rowid = row[props.rowid];
|
|
400
|
+
const cells = [];
|
|
401
|
+
for (let i = 0; i < state.columns.length; i++) {
|
|
402
|
+
const column = state.columns[i];
|
|
403
|
+
if (column.render) {
|
|
404
|
+
cells.push(renderCustomCell(column, row));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
let value;
|
|
408
|
+
if (column.depth) {
|
|
409
|
+
value = row;
|
|
410
|
+
for (let j = 0; j < column.depth.length; j++) {
|
|
411
|
+
if (value)
|
|
412
|
+
value = value[column.depth[j]];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
value = row[column.name];
|
|
417
|
+
}
|
|
418
|
+
cells.push(renderCell(column, value));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
trProps.ondblclick = (event) => {
|
|
422
|
+
const target = event.currentTarget;
|
|
423
|
+
const index = target.getAttribute('index');
|
|
424
|
+
if (index !== null) {
|
|
425
|
+
const rowData = rows[parseInt(index, 10)];
|
|
426
|
+
props.onRowDblClick?.(event, rowData);
|
|
427
|
+
emit('row-dbl-click', event, rowData);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
if (props.onRowClick) {
|
|
431
|
+
trProps.onClick = (event) => {
|
|
432
|
+
const target = event.currentTarget;
|
|
433
|
+
const index = target.getAttribute('index');
|
|
434
|
+
if (index !== null) {
|
|
435
|
+
const rowData = rows[parseInt(index, 10)];
|
|
436
|
+
props.onRowClick?.(event, rowData);
|
|
437
|
+
emit('row-click', event, rowData);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return h('tr', trProps, cells);
|
|
442
|
+
}
|
|
443
|
+
function renderHeader() {
|
|
444
|
+
const headers = [];
|
|
445
|
+
for (let i = 0; i < state.columns.length; i++) {
|
|
446
|
+
const labelContent = [];
|
|
447
|
+
if (state.columns[i].label)
|
|
448
|
+
labelContent.push(h('span', state.columns[i].label));
|
|
449
|
+
else
|
|
450
|
+
labelContent.push(h('span', state.columns[i].name));
|
|
451
|
+
if (state.sort && state.sort.length > 0 && state.sort[0].index === i) {
|
|
452
|
+
if (state.sort[0].dir === 'desc')
|
|
453
|
+
labelContent.push(h('span', { style: { float: 'right' } }, '▼'));
|
|
454
|
+
else
|
|
455
|
+
labelContent.push(h('span', { style: { float: 'right' } }, '▲'));
|
|
456
|
+
}
|
|
457
|
+
const headerStyle = { position: 'relative' };
|
|
458
|
+
if (state.columns[i].width) {
|
|
459
|
+
headerStyle.width = state.columns[i].width;
|
|
460
|
+
headerStyle.minWidth = state.columns[i].width;
|
|
461
|
+
headerStyle.maxWidth = state.columns[i].width;
|
|
462
|
+
}
|
|
463
|
+
let headerLink;
|
|
464
|
+
let popupContent;
|
|
465
|
+
if (state.columns[i].popup) {
|
|
466
|
+
const popupRef = ref(null);
|
|
467
|
+
const popupItems = [
|
|
468
|
+
h('a', { href: '#', onClick: (event) => { sortBy(i); event.preventDefault(); }, style: { display: 'block', textDecoration: 'none', color: 'black', fontWeight: 'initial', padding: '.25rem' } }, 'Order by Ascending'),
|
|
469
|
+
h('a', { href: '#', onClick: (event) => { sortBy(i, 'desc'); event.preventDefault(); }, style: { display: 'block', textDecoration: 'none', color: 'black', fontWeight: 'initial', padding: '.25rem' } }, 'Order by Descending'),
|
|
470
|
+
];
|
|
471
|
+
headerLink = h('a', {
|
|
472
|
+
href: '#',
|
|
473
|
+
onClick: (event) => {
|
|
474
|
+
if (popupRef.value && popupRef.value.style && popupRef.value.style.display && popupRef.value.style.display === 'block')
|
|
475
|
+
popupRef.value.style.display = 'none';
|
|
476
|
+
else if (popupRef.value && popupRef.value.style)
|
|
477
|
+
popupRef.value.style.display = 'block';
|
|
478
|
+
event.preventDefault();
|
|
479
|
+
},
|
|
480
|
+
style: { display: 'block', 'font-weight': 500, textDecoration: 'none', textAlign: 'center' }
|
|
481
|
+
}, labelContent);
|
|
482
|
+
popupContent = h('div', {
|
|
483
|
+
ref: popupRef,
|
|
484
|
+
style: { position: 'absolute', display: 'none', width: state.columns[i].width || '12rem', height: 'fit-content', border: '1px solid lightgray', backgroundColor: 'whitesmoke', zIndex: 1, left: 0, top: '2rem', bottom: 0, right: 0, padding: '.25rem' }
|
|
485
|
+
}, popupItems);
|
|
486
|
+
}
|
|
487
|
+
else
|
|
488
|
+
headerLink = h('a', { href: '#', onClick: (event) => { sortBy(i); event.preventDefault(); }, style: { display: 'block', 'font-weight': 500, textDecoration: 'none', textAlign: 'center' } }, labelContent);
|
|
489
|
+
if (state.columns[i].filter) ;
|
|
490
|
+
headers.push(h('th', { colspan: null, style: headerStyle }, [headerLink, popupContent]));
|
|
491
|
+
}
|
|
492
|
+
return [h('tr', {}, headers)];
|
|
493
|
+
}
|
|
494
|
+
function renderBody() {
|
|
495
|
+
const rowsHtml = [];
|
|
496
|
+
if (rows && rows.length > 0) {
|
|
497
|
+
if (groups && groups.length > 0) {
|
|
498
|
+
const groupRows = [];
|
|
499
|
+
for (let g = 0; g < groups.length; g++)
|
|
500
|
+
groupRows.push({ index: g, childs: [] });
|
|
501
|
+
for (let i = 0; i < rows.length; i++) {
|
|
502
|
+
const groupIdx = rowGroups[i];
|
|
503
|
+
groupRows[groupIdx].childs.push(renderRow(i, rows[i]));
|
|
504
|
+
}
|
|
505
|
+
for (let g = 0; g < groups.length; g++) {
|
|
506
|
+
rowsHtml.push(renderGroup(groups[g]));
|
|
507
|
+
if (groups[g].expand)
|
|
508
|
+
rowsHtml.push(groupRows[g].childs);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
for (let i = 0; i < rows.length; i++) {
|
|
513
|
+
rowsHtml.push(renderRow(i, rows[i]));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else if (state.settings?.emptyMessage) {
|
|
518
|
+
rowsHtml.push(h('tr', {}, h('td', { colspan: state.columns.length, style: { textAlign: 'center', padding: '2rem', color: '#666' } }, state.settings.emptyMessage)));
|
|
519
|
+
}
|
|
520
|
+
return rowsHtml;
|
|
521
|
+
}
|
|
522
|
+
function renderFooter() {
|
|
523
|
+
const icons = {
|
|
524
|
+
first: `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" style="transform:scale(1.15)"><path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6zM6 6h2v12H6z"/></svg>`,
|
|
525
|
+
prior: `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" style="transform:scale(1.15)"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>`,
|
|
526
|
+
next: `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" style="transform:scale(1.15)"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>`,
|
|
527
|
+
last: `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" style="transform:scale(1.15)"><path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6zM16 6h2v12h-2z"/></svg>`
|
|
528
|
+
};
|
|
529
|
+
const buttonProps = { class: state.footer.button?.class, style: 'font: initial; margin-right: .5rem' };
|
|
530
|
+
const firstBtn = h('button', { ...buttonProps, type: 'button', onClick: (e) => { e.preventDefault(); first(); }, disabled: state.page.current === 1, innerHTML: icons.first });
|
|
531
|
+
const lastBtn = h('button', { ...buttonProps, type: 'button', onClick: (e) => { e.preventDefault(); last(); }, disabled: state.page.count === 0 || state.page.current === state.page.count, innerHTML: icons.last });
|
|
532
|
+
const pageBtn = h('button', { ...buttonProps, type: 'button', onClick: (e) => { e.preventDefault(); choosePage(); } }, state.page.current + (state.page.count > 0 ? (' / ' + state.page.count) : ''));
|
|
533
|
+
const priorBtn = h('button', { ...buttonProps, type: 'button', onClick: (e) => { e.preventDefault(); prior(); }, disabled: state.page.current === 1, innerHTML: icons.prior });
|
|
534
|
+
const nextBtn = h('button', { ...buttonProps, type: 'button', onClick: (e) => { e.preventDefault(); next(); }, disabled: state.page.count > 0 && state.page.current === state.page.count, innerHTML: icons.next });
|
|
535
|
+
const td = h('td', { colspan: state.columns.length, align: 'center' }, [firstBtn, priorBtn, pageBtn, nextBtn, lastBtn]);
|
|
536
|
+
const tr = h('tr', {}, [td]);
|
|
537
|
+
return [tr];
|
|
538
|
+
}
|
|
539
|
+
function renderCaption() {
|
|
540
|
+
return '';
|
|
541
|
+
}
|
|
542
|
+
if (state.columns.length === 0) {
|
|
543
|
+
console.error('Columns not defined !');
|
|
544
|
+
}
|
|
545
|
+
for (let i = 0; i < state.columns.length; i++)
|
|
546
|
+
if (state.columns[i].name && state.columns[i].name.includes('.')) {
|
|
547
|
+
state.columns[i].depth = state.columns[i].name.split('.');
|
|
548
|
+
}
|
|
549
|
+
loadPage(1);
|
|
550
|
+
const methods = {
|
|
551
|
+
first,
|
|
552
|
+
prior,
|
|
553
|
+
next,
|
|
554
|
+
last,
|
|
555
|
+
go,
|
|
556
|
+
page: loadPage,
|
|
557
|
+
refresh,
|
|
558
|
+
sortBy,
|
|
559
|
+
getRows: () => rows,
|
|
560
|
+
setRows: (newRows) => { rows = [...newRows]; state.time = Date.now(); }
|
|
561
|
+
};
|
|
562
|
+
expose(methods);
|
|
563
|
+
return {
|
|
564
|
+
state,
|
|
565
|
+
...methods,
|
|
566
|
+
renderHeader,
|
|
567
|
+
renderBody,
|
|
568
|
+
renderFooter,
|
|
569
|
+
renderCaption
|
|
570
|
+
};
|
|
571
|
+
},
|
|
572
|
+
render(props) {
|
|
573
|
+
const tableProps = {
|
|
574
|
+
class: 'tablevu',
|
|
575
|
+
style: [{ tableLayout: 'fixed', width: '100%' }, props.style]
|
|
576
|
+
};
|
|
577
|
+
if (props.class)
|
|
578
|
+
tableProps.class = 'tablevu ' + props.class;
|
|
579
|
+
const content = [];
|
|
580
|
+
content.push(h('thead', {}, this.renderHeader()));
|
|
581
|
+
if (this.state.status === TablevuStatus.Ready)
|
|
582
|
+
content.push(h('tbody', { 'data-time': this.state.time }, this.renderBody()));
|
|
583
|
+
if (this.state.caption)
|
|
584
|
+
content.push(h('caption', {}, this.renderCaption()));
|
|
585
|
+
if (this.state.status === TablevuStatus.Ready)
|
|
586
|
+
if (this.state.page.size > 0)
|
|
587
|
+
content.push(h('tfoot', {}, this.renderFooter()));
|
|
588
|
+
return h('table', tableProps, content);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const fieldPath = (filter, lambdaVariable) => {
|
|
593
|
+
const field = filter.field ?? filter.column;
|
|
594
|
+
if (!field)
|
|
595
|
+
throw new TypeError(`OData operator "${filter.operator}" requires a field.`);
|
|
596
|
+
const path = field.replace(/\./g, '/');
|
|
597
|
+
return lambdaVariable && path !== lambdaVariable && !path.startsWith(`${lambdaVariable}/`)
|
|
598
|
+
? `${lambdaVariable}/${path}` : path;
|
|
599
|
+
};
|
|
600
|
+
function formatValue(value) {
|
|
601
|
+
if (value === null)
|
|
602
|
+
return 'null';
|
|
603
|
+
if (typeof value === 'string')
|
|
604
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
605
|
+
if (typeof value === 'number') {
|
|
606
|
+
if (!Number.isFinite(value))
|
|
607
|
+
throw new TypeError('OData numeric values must be finite.');
|
|
608
|
+
return String(value);
|
|
609
|
+
}
|
|
610
|
+
if (typeof value === 'boolean')
|
|
611
|
+
return String(value);
|
|
612
|
+
if (value instanceof Date) {
|
|
613
|
+
if (Number.isNaN(value.getTime()))
|
|
614
|
+
throw new TypeError('Invalid Date cannot be used in an OData filter.');
|
|
615
|
+
return value.toISOString();
|
|
616
|
+
}
|
|
617
|
+
throw new TypeError(`Unsupported OData filter value: ${Object.prototype.toString.call(value)}.`);
|
|
618
|
+
}
|
|
619
|
+
function requireString(value, operator) {
|
|
620
|
+
if (typeof value !== 'string')
|
|
621
|
+
throw new TypeError(`OData operator "${operator}" requires a string value.`);
|
|
622
|
+
return value.replace(/'/g, "''");
|
|
623
|
+
}
|
|
624
|
+
function buildExpression(expression, lambdaVariable) {
|
|
625
|
+
const { operator } = expression;
|
|
626
|
+
if (operator === 'and' || operator === 'or' || operator === 'not') {
|
|
627
|
+
const children = expression.filters ?? (expression.filter ? [expression.filter] : []);
|
|
628
|
+
if (operator === 'not') {
|
|
629
|
+
if (children.length !== 1)
|
|
630
|
+
throw new TypeError('OData "not" requires exactly one child filter.');
|
|
631
|
+
return `not (${buildExpression(children[0], lambdaVariable)})`;
|
|
632
|
+
}
|
|
633
|
+
if (children.length < 2)
|
|
634
|
+
throw new TypeError(`OData "${operator}" requires at least two child filters.`);
|
|
635
|
+
return `(${children.map(child => buildExpression(child, lambdaVariable)).join(` ${operator} `)})`;
|
|
636
|
+
}
|
|
637
|
+
if (operator === 'any' || operator === 'all') {
|
|
638
|
+
const collection = fieldPath(expression, lambdaVariable);
|
|
639
|
+
const child = expression.filter;
|
|
640
|
+
if (!child)
|
|
641
|
+
throw new TypeError(`OData "${operator}" requires a filter expression.`);
|
|
642
|
+
const variable = expression.variable ?? 'item';
|
|
643
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable))
|
|
644
|
+
throw new TypeError('Invalid OData lambda variable.');
|
|
645
|
+
return `${collection}/${operator}(${variable}: ${buildExpression(child, variable)})`;
|
|
646
|
+
}
|
|
647
|
+
if (operator.startsWith('count')) {
|
|
648
|
+
const op = { countGt: 'gt', countGe: 'ge', countLt: 'lt', countLe: 'le' }[operator];
|
|
649
|
+
if (typeof expression.value !== 'number' || !Number.isInteger(expression.value) || expression.value < 0)
|
|
650
|
+
throw new TypeError(`OData operator "${operator}" requires a non-negative integer value.`);
|
|
651
|
+
return `${fieldPath(expression, lambdaVariable)}/$count ${op} ${expression.value}`;
|
|
652
|
+
}
|
|
653
|
+
const field = fieldPath(expression, lambdaVariable);
|
|
654
|
+
const value = expression.value;
|
|
655
|
+
switch (operator) {
|
|
656
|
+
case 'eq':
|
|
657
|
+
case 'ne':
|
|
658
|
+
case 'gt':
|
|
659
|
+
case 'ge':
|
|
660
|
+
case 'lt':
|
|
661
|
+
case 'le':
|
|
662
|
+
return `${field} ${operator} ${formatValue(value)}`;
|
|
663
|
+
case 'contains':
|
|
664
|
+
case 'startsWith':
|
|
665
|
+
case 'endsWith': {
|
|
666
|
+
const functionName = operator === 'startsWith' ? 'startswith' : operator === 'endsWith' ? 'endswith' : 'contains';
|
|
667
|
+
return `${functionName}(${field},'${requireString(value, operator)}')`;
|
|
668
|
+
}
|
|
669
|
+
case 'in': {
|
|
670
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
671
|
+
throw new TypeError('OData "in" requires a non-empty array value.');
|
|
672
|
+
return `${field} in (${value.map(formatValue).join(',')})`;
|
|
673
|
+
}
|
|
674
|
+
case 'between': {
|
|
675
|
+
if (!Array.isArray(value) || value.length !== 2)
|
|
676
|
+
throw new TypeError('OData "between" requires exactly two values.');
|
|
677
|
+
return `(${field} ge ${formatValue(value[0])} and ${field} le ${formatValue(value[1])})`;
|
|
678
|
+
}
|
|
679
|
+
case 'isNull': return `${field} eq null`;
|
|
680
|
+
case 'isNotNull': return `${field} ne null`;
|
|
681
|
+
default: throw new TypeError(`Unsupported OData filter operator: ${operator}.`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/** Converts the typed filter tree into an OData `$filter` expression. */
|
|
685
|
+
function buildFilter(filter) {
|
|
686
|
+
if (filter == null || filter === '')
|
|
687
|
+
return '';
|
|
688
|
+
if (typeof filter === 'string')
|
|
689
|
+
return filter;
|
|
690
|
+
return buildExpression(filter);
|
|
691
|
+
}
|
|
692
|
+
function createODataAdapter(options) {
|
|
693
|
+
return async (params = {}) => {
|
|
694
|
+
const url = new URL(options.url);
|
|
695
|
+
if (params.pageSize && params.pageSize > 0) {
|
|
696
|
+
const page = Math.max(1, params.currPage ?? 1);
|
|
697
|
+
url.searchParams.set('$skip', String(params.pageSize * (page - 1)));
|
|
698
|
+
url.searchParams.set('$top', String(params.pageSize));
|
|
699
|
+
}
|
|
700
|
+
if (options.count)
|
|
701
|
+
url.searchParams.set('$count', 'true');
|
|
702
|
+
if (options.select?.length)
|
|
703
|
+
url.searchParams.set('$select', options.select.join(','));
|
|
704
|
+
if (options.expand?.length) {
|
|
705
|
+
url.searchParams.set('$expand', options.expand.map(expand => {
|
|
706
|
+
const options = [];
|
|
707
|
+
if (expand.select?.length)
|
|
708
|
+
options.push(`$select=${expand.select.join(',')}`);
|
|
709
|
+
if (expand.filter)
|
|
710
|
+
options.push(`$filter=${buildFilter(expand.filter)}`);
|
|
711
|
+
if (expand.orderby)
|
|
712
|
+
options.push(`$orderby=${expand.orderby}`);
|
|
713
|
+
return options.length ? `${expand.name}(${options.join(';')})` : expand.name;
|
|
714
|
+
}).join(','));
|
|
715
|
+
}
|
|
716
|
+
const filters = [];
|
|
717
|
+
const staticFilter = buildFilter(options.filter);
|
|
718
|
+
const dynamicFilter = buildFilter(params.filter);
|
|
719
|
+
if (staticFilter)
|
|
720
|
+
filters.push(`(${staticFilter})`);
|
|
721
|
+
if (dynamicFilter)
|
|
722
|
+
filters.push(`(${dynamicFilter})`);
|
|
723
|
+
if (params.search?.trim()) {
|
|
724
|
+
const term = params.search.replace(/'/g, "''");
|
|
725
|
+
const searchable = (params.cols ?? []).filter(column => column.searchable !== false);
|
|
726
|
+
const searchFilter = searchable.map(column => `contains(${column.name.replace(/\./g, '/')},'${term}')`).join(' or ');
|
|
727
|
+
if (searchFilter)
|
|
728
|
+
filters.push(`(${searchFilter})`);
|
|
729
|
+
}
|
|
730
|
+
if (filters.length)
|
|
731
|
+
url.searchParams.set('$filter', filters.join(' and '));
|
|
732
|
+
if (params.sort?.length) {
|
|
733
|
+
const order = params.sort.map(sort => {
|
|
734
|
+
const name = sort.index !== undefined && sort.index >= 0
|
|
735
|
+
? params.cols?.[sort.index]?.name
|
|
736
|
+
: sort.name;
|
|
737
|
+
if (!name)
|
|
738
|
+
return '';
|
|
739
|
+
const direction = sort.dir?.toLowerCase();
|
|
740
|
+
if (direction && direction !== 'asc' && direction !== 'desc')
|
|
741
|
+
throw new TypeError(`Invalid sort direction: ${sort.dir}.`);
|
|
742
|
+
return `${name.replace(/\./g, '/')}${direction ? ` ${direction}` : ''}`;
|
|
743
|
+
}).filter(Boolean);
|
|
744
|
+
if (order.length)
|
|
745
|
+
url.searchParams.set('$orderby', order.join(','));
|
|
746
|
+
}
|
|
747
|
+
let request = new Request(url, { headers: options.headers, credentials: options.credentials });
|
|
748
|
+
if (options.onfetch)
|
|
749
|
+
request = await options.onfetch(request);
|
|
750
|
+
const response = await (options.fetch ?? fetch)(request);
|
|
751
|
+
if (!response.ok)
|
|
752
|
+
throw new Error(`OData request failed: ${response.status} ${response.statusText}`);
|
|
753
|
+
const payload = await response.json();
|
|
754
|
+
const rows = Array.isArray(payload.value) ? payload.value : [];
|
|
755
|
+
return { rows, count: typeof payload['@odata.count'] === 'number' ? payload['@odata.count'] : rows.length };
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export { tablevu as TableVu, TablevuStatus, adaptArray, buildFilter, createODataAdapter as odata };
|