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