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.
@@ -1,1102 +0,0 @@
1
- import { h, reactive, watch, onMounted, ref } from 'vue';
2
-
3
- function adaptArray(props, data) {
4
- const d = Array.isArray(data) ? [...data] : [];
5
- return new Promise((resolve, reject) => {
6
- try {
7
- // Filter & Search //
8
- if (props.filter || props.search) {
9
- const searchableCols = props.cols
10
- ?.map((m, i) => ({ index: i, searchable: m.searchable !== false }))
11
- .filter(q => q.searchable)
12
- .map(m => m.index) || [];
13
- let filtered = d;
14
- if (props.filter) {
15
- filtered = filtered.filter(row => {
16
- return Object.entries(props.filter).every(([key, value]) => {
17
- return row[key] === value;
18
- });
19
- });
20
- }
21
- if (props.search && searchableCols.length > 0) {
22
- const searchLower = props.search.toLowerCase();
23
- filtered = filtered.filter(row => {
24
- return searchableCols.some(colIndex => {
25
- const col = props.cols?.[colIndex];
26
- if (!col)
27
- return false;
28
- const val = row[col.name];
29
- return val && String(val).toLowerCase().includes(searchLower);
30
- });
31
- });
32
- }
33
- // Update d with filtered results
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 start = (props.currPage - 1) * props.pageSize;
68
- const end = props.currPage * props.pageSize;
69
- result = d.slice(start, end);
70
- }
71
- resolve({ rows: result, count: d.length });
72
- }
73
- catch (err) {
74
- reject(err);
75
- }
76
- });
77
- }
78
-
79
- const TablevuStatus = {
80
- Error: -1,
81
- Ready: 0,
82
- Loading: 1
83
- };
84
-
85
- const tablevu = {
86
- props: {
87
- settings: { type: Object, default: null },
88
- data: { type: [Array, Function] },
89
- group: { type: [String, Object] },
90
- search: { type: String },
91
- filter: { type: [Array, Object] },
92
- rowid: { type: String },
93
- onRowClick: { type: Function },
94
- onRowDblClick: { type: Function },
95
- class: { type: String, default: 'table' },
96
- style: { type: String, default: null }
97
- },
98
- emits: ['onRowClick', 'onRowDblClick', 'selectionChange', 'columnVisibilityChange'],
99
- setup(props, { attrs, slots, emit, expose }) {
100
- let rows = [];
101
- let rowg = [];
102
- let grps = [];
103
- let adapter = null;
104
- let timer = null;
105
- const spec = reactive({
106
- caption: null,
107
- group: {
108
- value: (props.settings && props.settings.group) ? props.settings.group.value : props.group ? props.group : undefined,
109
- content: (props.settings && props.settings.group) ? props.settings.group.content : undefined,
110
- expandAll: true,
111
- class: 'table-light'
112
- },
113
- page: {
114
- count: 0,
115
- size: (props.settings && props.settings.page) ? (props.settings.page.size || 0) : 0,
116
- current: 0,
117
- type: undefined
118
- },
119
- sort: (props.settings && props.settings.sort) ? props.settings.sort : [],
120
- columns: (props.settings && props.settings.columns) ? props.settings.columns : [],
121
- footer: {
122
- button: {
123
- class: (props.settings && props.settings.footer && props.settings.footer.button && props.settings.footer.button.class) || 'btn'
124
- }
125
- },
126
- status: TablevuStatus.Ready,
127
- time: null,
128
- search: null,
129
- filter: null,
130
- onData: (props.settings && props.settings.onData) ? props.settings.onData : undefined,
131
- selectedRows: [],
132
- columnWidths: {},
133
- columnOrder: [],
134
- columnVisibility: {}
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
- spec.search = props.search;
146
- watch(() => props.search, (n, o) => {
147
- const x = parseInt(String(props.settings?.searchMinChar)) || 3;
148
- if (timer) {
149
- clearTimeout(timer);
150
- timer = null;
151
- }
152
- if (n && n.length > x) {
153
- spec.search = n;
154
- timer = setTimeout(() => { refresh(); timer = null; }, 444);
155
- }
156
- else if (o && o.length > x && (n == undefined || n == null || n.length <= x)) {
157
- spec.search = null;
158
- timer = setTimeout(() => { refresh(); timer = null; }, 444);
159
- }
160
- });
161
- if (props.filter)
162
- spec.filter = props.filter;
163
- watch(() => props.filter, () => {
164
- if (props.filter)
165
- spec.filter = props.filter;
166
- else
167
- spec.filter = null;
168
- refresh();
169
- }, { deep: true });
170
- function prepareGroup(g, r) {
171
- grps = [];
172
- rowg = [];
173
- for (let i = 0; i < r.length; i++) {
174
- let v = null;
175
- let c = null;
176
- const o = r[i];
177
- if (g.value) {
178
- if (typeof g.value == 'function')
179
- v = g.value(o);
180
- else
181
- v = o[g.value];
182
- }
183
- else if (typeof g == 'function') {
184
- v = g(o);
185
- }
186
- else {
187
- if (typeof g == 'bigint' || typeof g == 'number') {
188
- const l = parseFloat(String(g));
189
- if (l)
190
- v = o[l];
191
- }
192
- else if (typeof g == 'string')
193
- v = o[g];
194
- }
195
- if (g.content) {
196
- if (typeof g.content == 'function')
197
- c = g.content(o);
198
- else
199
- c = o[g.content];
200
- }
201
- let n = grps.findIndex(f => f.group == v);
202
- if (n == -1) {
203
- n = grps.push({ group: v, content: c, expand: spec.group.expandAll }) - 1;
204
- }
205
- rowg.push(n);
206
- }
207
- }
208
- function first() { loadPage(1); }
209
- function prior() {
210
- if (spec.page.current > 1) {
211
- loadPage(spec.page.current - 1);
212
- }
213
- else {
214
- loadPage(spec.page.current);
215
- }
216
- }
217
- function next() { loadPage(spec.page.current + 1); }
218
- function last() { loadPage(spec.page.count); }
219
- // Navigate to specific page number
220
- function page(currentPage) {
221
- if (currentPage < 1)
222
- currentPage = 1;
223
- if (currentPage > spec.page.count && spec.page.count > 0)
224
- currentPage = spec.page.count;
225
- loadPage(currentPage);
226
- }
227
- function refresh() { loadPage(spec.page.current); }
228
- function orderby(ind, dir) {
229
- if (spec.sort && spec.sort.length > 0) {
230
- let l = spec.sort.findIndex(f => f.index == ind);
231
- if (l < 0) {
232
- let n = spec.columns[ind]?.name;
233
- l = spec.sort.findIndex(f => f.name == n);
234
- }
235
- if (l == 0) {
236
- spec.sort[l].dir = dir ? dir : (spec.sort[0].dir ? null : 'desc');
237
- }
238
- else if (l > 0) {
239
- spec.sort.splice(l, 1);
240
- }
241
- if (l != 0) {
242
- spec.sort.unshift({ index: ind, dir: dir ? dir : null });
243
- }
244
- if (spec.sort.length > 2) {
245
- spec.sort.pop();
246
- }
247
- }
248
- else {
249
- spec.sort.push({ index: ind, dir: dir ? dir : null });
250
- }
251
- loadPage(spec.page.current);
252
- }
253
- // Internal load function (alias for adapter call)
254
- function loadPage(currentPage) {
255
- let dat = null;
256
- if (props.data) {
257
- if (Array.isArray(props.data))
258
- dat = props.data;
259
- }
260
- else
261
- return;
262
- spec.status = TablevuStatus.Loading;
263
- if (adapter) {
264
- const pageSize = spec.page.size || 20;
265
- adapter({
266
- pageSize,
267
- currPage: currentPage,
268
- sort: spec.sort,
269
- cols: spec.columns,
270
- filter: spec.filter,
271
- search: spec.search
272
- }, dat).then(ada => {
273
- if (spec.onData)
274
- rows = spec.onData(ada.rows);
275
- else
276
- rows = ada.rows;
277
- const groupValue = spec.group.value;
278
- if (groupValue && (typeof groupValue == 'string' || typeof groupValue == 'number' || typeof groupValue == 'bigint' || (typeof groupValue == 'object' && groupValue.value)))
279
- prepareGroup(spec.group, rows);
280
- spec.page.count = (ada.count > 0 && pageSize > 0) ? Math.ceil(ada.count / pageSize) : 0;
281
- spec.page.current = currentPage;
282
- spec.status = TablevuStatus.Ready;
283
- spec.time = Date.now();
284
- }).catch(err => {
285
- console.error(err);
286
- spec.status = TablevuStatus.Error;
287
- });
288
- }
289
- }
290
- function renderValue(col, val) {
291
- if (val) {
292
- let v;
293
- if (col.type == 'datetime' || col.type == 'date' || col.type == 'time') {
294
- const t = new Date(val);
295
- if (col.type == 'date')
296
- v = t.toLocaleDateString();
297
- else if (col.type == 'time')
298
- v = t.toLocaleTimeString();
299
- else
300
- v = t.toLocaleString();
301
- }
302
- else {
303
- if (col.type == 'numeric') {
304
- v = parseFloat(val);
305
- }
306
- else {
307
- if (col.type == 'integer') {
308
- v = parseInt(val);
309
- }
310
- else
311
- v = val;
312
- }
313
- }
314
- return v;
315
- }
316
- else
317
- return '';
318
- }
319
- function renderCell(col, val) {
320
- const o = { style: {} };
321
- if (col.colspan && col.colspan > 1)
322
- o.colspan = col.colspan;
323
- if (col.style)
324
- o.style = { ...o.style, ...col.style };
325
- if (col.align)
326
- o.style['text-align'] = col.align;
327
- if (col.width)
328
- o.style['max-width'] = col.width;
329
- const v = renderValue(col, val);
330
- return h('td', o, v);
331
- }
332
- function renderCustomCell(col, row) {
333
- const o = { style: {} };
334
- if (col.colspan && col.colspan > 1)
335
- o.colspan = col.colspan;
336
- if (col.style)
337
- o.style = { ...o.style, ...col.style };
338
- if (col.align)
339
- o.style['text-align'] = col.align;
340
- if (col.width)
341
- o.style['max-width'] = col.width;
342
- const f = col.render?.(row) ?? '';
343
- return h('td', { ...o, innerHTML: f });
344
- }
345
- function renderGroup(grp) {
346
- const b = h('button', {
347
- title: String(grp.expand),
348
- onClick: (event) => {
349
- grp.expand = !grp.expand;
350
- event.currentTarget.value = grp.expand ? '▼' : '►';
351
- spec.time = Date.now();
352
- event.preventDefault();
353
- },
354
- style: { padding: 0, margin: '0 6px 0 0', width: '12px', height: '100%', border: 'none', outline: '0', backgroundColor: 'transparent' }
355
- }, grp.expand ? '▼' : '►');
356
- const v = grp.content || grp.group;
357
- return h('tr', {}, h('td', { colspan: spec.columns.length, class: spec.group.class }, [b, v]));
358
- }
359
- function renderRow(index, row) {
360
- const o = { key: index, index: index };
361
- if (props.rowid)
362
- o.rowid = row[props.rowid];
363
- const r = [];
364
- for (let i = 0; i < spec.columns.length; i++) {
365
- const c = spec.columns[i];
366
- if (c.render) {
367
- r.push(renderCustomCell(c, row));
368
- }
369
- else {
370
- let v;
371
- if (c.depth) {
372
- v = row;
373
- for (let j = 0; j < c.depth.length; j++) {
374
- if (v)
375
- v = v[c.depth[j]];
376
- }
377
- }
378
- else {
379
- v = row[c.name];
380
- }
381
- r.push(renderCell(c, v));
382
- }
383
- }
384
- o.ondblclick = (event) => {
385
- const target = event.currentTarget;
386
- const idx = target.getAttribute('index');
387
- if (idx !== null) {
388
- const rowData = rows[parseInt(idx)];
389
- emit('onRowDblClick', event, rowData);
390
- }
391
- };
392
- if (props.onRowClick) {
393
- o.onClick = (event) => {
394
- const target = event.currentTarget;
395
- const idx = target.getAttribute('index');
396
- if (idx !== null) {
397
- const rowData = rows[parseInt(idx)];
398
- props.onRowClick(event, rowData);
399
- }
400
- };
401
- }
402
- // Selection
403
- if (spec.selectedRows.some(s => s[props.rowid || 'id'] === row[props.rowid || 'id'])) {
404
- o.class = (o.class || '') + ' selected';
405
- }
406
- return h('tr', o, r);
407
- }
408
- function renderHeader() {
409
- const ar = [];
410
- for (let i = 0; i < spec.columns.length; i++) {
411
- const cl = [];
412
- if (spec.columns[i].label)
413
- cl.push(h('span', spec.columns[i].label));
414
- else
415
- cl.push(h('span', spec.columns[i].name));
416
- if (spec.sort && spec.sort.length > 0 && spec.sort[0].index == i) {
417
- if (spec.sort[0].dir == 'desc')
418
- cl.push(h('span', { style: { float: 'right' } }, '▼'));
419
- else
420
- cl.push(h('span', { style: { float: 'right' } }, '▲'));
421
- }
422
- const ch = { position: 'relative' };
423
- if (spec.columns[i].width) {
424
- ch.width = spec.columns[i].width;
425
- ch.minWidth = spec.columns[i].width;
426
- ch.maxWidth = spec.columns[i].width;
427
- }
428
- const cy = { display: 'block', 'font-weight': 500, textDecoration: 'none', textAlign: 'center' };
429
- let a;
430
- let f;
431
- if (spec.columns[i].popup) {
432
- const r = ref(null);
433
- const c = [
434
- h('a', { href: '#', onClick: (event) => { orderby(i); event.preventDefault(); }, style: { display: 'block', textDecoration: 'none', color: 'black', fontWeight: 'initial', padding: '.25rem' } }, 'Order by Ascending'),
435
- h('a', { href: '#', onClick: (event) => { orderby(i, 'desc'); event.preventDefault(); }, style: { display: 'block', textDecoration: 'none', color: 'black', fontWeight: 'initial', padding: '.25rem' } }, 'Order by Descending'),
436
- ];
437
- a = h('a', {
438
- href: '#',
439
- onClick: (event) => {
440
- if (r.value && r.value.style && r.value.style.display && r.value.style.display == 'block')
441
- r.value.style.display = 'none';
442
- else if (r.value && r.value.style)
443
- r.value.style.display = 'block';
444
- event.preventDefault();
445
- },
446
- style: cy
447
- }, cl);
448
- f = h('div', {
449
- ref: r,
450
- style: { position: 'absolute', display: 'none', width: spec.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' }
451
- }, c);
452
- }
453
- else
454
- a = h('a', { href: '#', onClick: (event) => { orderby(i); event.preventDefault(); }, style: cy }, cl);
455
- if (spec.columns[i].filter) ;
456
- ar.push(h('th', {
457
- colspan: null,
458
- style: ch
459
- }, [a, f]));
460
- }
461
- const tr = h('tr', {}, ar);
462
- return [tr];
463
- }
464
- function renderBody() {
465
- const dr = [];
466
- if (rows && rows.length > 0) {
467
- if (grps && grps.length > 0) {
468
- const r = [];
469
- for (let g = 0; g < grps.length; g++)
470
- r.push({ index: g, childs: [] });
471
- for (let i = 0; i < rows.length; i++) {
472
- const x = rowg[i];
473
- r[x].childs.push(renderRow(i, rows[i]));
474
- }
475
- for (let g = 0; g < grps.length; g++) {
476
- dr.push(renderGroup(grps[g]));
477
- if (grps[g].expand)
478
- dr.push(r[g].childs);
479
- }
480
- }
481
- else {
482
- for (let i = 0; i < rows.length; i++) {
483
- dr.push(renderRow(i, rows[i]));
484
- }
485
- }
486
- }
487
- else if (spec.settings?.emptyMessage) {
488
- dr.push(h('tr', {}, h('td', { colspan: spec.columns.length, style: { textAlign: 'center', padding: '2rem', color: '#666' } }, spec.settings.emptyMessage)));
489
- }
490
- return dr;
491
- }
492
- // Prompt user for page number and navigate
493
- function goToPage() {
494
- const input = prompt('Page number:', String(spec.page.current));
495
- if (input) {
496
- const pageNum = parseInt(input, 10);
497
- if (!isNaN(pageNum) && pageNum > 0 && pageNum !== spec.page.current) {
498
- page(pageNum);
499
- }
500
- }
501
- }
502
- function makeHandler(fn) {
503
- return (e) => {
504
- e.preventDefault();
505
- e.stopPropagation();
506
- fn();
507
- };
508
- }
509
- function renderFooter() {
510
- const fr = h('button', { class: spec.footer.button.class, style: 'font: initial; margin-right: .5rem', onClick: makeHandler(first), disabled: spec.page.current == 1 }, '◀◀');
511
- const ls = h('button', { class: spec.footer.button.class, style: 'font: initial; margin-right: .5rem', onClick: makeHandler(last), disabled: spec.page.count == 0 || spec.page.current == spec.page.count }, '▶▶');
512
- const pg = h('button', { class: spec.footer.button.class, style: 'font: initial; margin-right: .5rem', onClick: makeHandler(goToPage) }, spec.page.current + (spec.page.count > 0 ? (' / ' + spec.page.count) : ''));
513
- const pr = h('button', { class: spec.footer.button.class, style: 'font: initial; margin-right: .5rem', onClick: makeHandler(prior), disabled: spec.page.current == 1 }, '◀');
514
- const nx = h('button', { class: spec.footer.button.class, style: 'font: initial; margin-right: .5rem', onClick: makeHandler(next), disabled: spec.page.count > 0 && spec.page.current == spec.page.count }, '▶');
515
- const td = h('td', { colspan: spec.columns.length, align: 'center' }, [fr, pr, pg, nx, ls]);
516
- const tr = h('tr', {}, [td]);
517
- return [tr];
518
- }
519
- function renderCaption() {
520
- return null;
521
- }
522
- // Initialize column metadata
523
- if (spec.columns.length == 0) {
524
- console.error('Columns not defined !');
525
- }
526
- for (let i = 0; i < spec.columns.length; i++) {
527
- if (spec.columns[i].name && spec.columns[i].name.indexOf('.') > -1) {
528
- spec.columns[i].depth = spec.columns[i].name.split('.');
529
- }
530
- // Initialize visibility
531
- if (spec.columns[i].visible === undefined) {
532
- spec.columns[i].visible = true;
533
- spec.columnVisibility[spec.columns[i].name] = true;
534
- }
535
- }
536
- loadPage(1);
537
- // Expose methods for external access via ref
538
- expose({
539
- first,
540
- prior,
541
- next,
542
- last,
543
- page,
544
- refresh,
545
- orderby,
546
- getSelectedRows: () => spec.selectedRows,
547
- setSelectedRows: (rows) => { spec.selectedRows = rows; }
548
- });
549
- onMounted(() => {
550
- // mounted hook
551
- });
552
- return {
553
- spec, renderHeader, renderBody, renderFooter, renderCaption,
554
- first, prior, next, last, page, refresh, orderby,
555
- getSelectedRows: () => spec.selectedRows,
556
- setSelectedRows: (rows) => { spec.selectedRows = rows; }
557
- };
558
- },
559
- render(props) {
560
- let prps = { class: 'tablevu' };
561
- if (props.class)
562
- prps.class = 'tablevu ' + props.class;
563
- if (props.style)
564
- prps.style = props.style;
565
- const cntn = [];
566
- cntn.push(h('thead', {}, this.renderHeader()));
567
- if (this.spec.status == TablevuStatus.Ready)
568
- cntn.push(h('tbody', { 'data-time': this.spec.time }, this.renderBody()));
569
- if (this.spec.caption)
570
- cntn.push(h('caption', {}, this.renderCaption()));
571
- if (this.spec.status == TablevuStatus.Ready)
572
- if (this.spec.page.size > 0)
573
- cntn.push(h('tfoot', {}, this.renderFooter()));
574
- return h('table', prps, cntn);
575
- }
576
- };
577
-
578
- /* ==========================================================================
579
- * OData DataTable Query Builder
580
- * ========================================================================== */
581
- /* ==========================================================================
582
- * Value
583
- * ========================================================================== */
584
- function formatValue(value) {
585
- if (value === null)
586
- return 'null';
587
- if (typeof value === 'string')
588
- return `'${escapeODataString(value)}'`;
589
- if (typeof value === 'number') {
590
- if (!Number.isFinite(value))
591
- throw new Error('Invalid numeric value.');
592
- return String(value);
593
- }
594
- if (typeof value === 'boolean')
595
- return value ? 'true' : 'false';
596
- if (value instanceof Date)
597
- return value.toISOString();
598
- /*
599
- * Backward compatibility with:
600
- *
601
- * {
602
- * func: 'now',
603
- * params: []
604
- * }
605
- */
606
- if (typeof value === 'object' &&
607
- value !== null &&
608
- 'func' in value &&
609
- 'params' in value) {
610
- const funcValue = value;
611
- return (funcValue.func +
612
- '(' +
613
- funcValue.params
614
- .map(m => formatValue(m))
615
- .join(',') +
616
- ')');
617
- }
618
- throw new Error(`Unsupported OData value type: ${typeof value}`);
619
- }
620
- function escapeODataString(value) {
621
- /*
622
- * OData escapes a single quote by doubling it.
623
- *
624
- * O'Reilly
625
- * ↓
626
- * 'O''Reilly'
627
- */
628
- return value.replace(/'/g, "''");
629
- }
630
- /* ==========================================================================
631
- * Filter
632
- * ========================================================================== */
633
- function toODataFilter(filter) {
634
- return buildFilter(filter);
635
- }
636
- function buildFilter(filter) {
637
- if (!filter)
638
- throw new Error('Invalid filter.');
639
- /*
640
- * Backward compatibility:
641
- *
642
- * {
643
- * func: 'contains',
644
- * params: [...]
645
- * }
646
- */
647
- if (filter.func) {
648
- return buildFunctionObject(filter);
649
- }
650
- switch (filter.operator) {
651
- /* ------------------------------------------------------------------ */
652
- /* Logical */
653
- /* ------------------------------------------------------------------ */
654
- case 'and':
655
- return buildLogical(filter, 'and');
656
- case 'or':
657
- return buildLogical(filter, 'or');
658
- case 'not':
659
- return buildNot(filter);
660
- /* ------------------------------------------------------------------ */
661
- /* Comparison */
662
- /* ------------------------------------------------------------------ */
663
- case 'eq':
664
- case 'ne':
665
- case 'gt':
666
- case 'ge':
667
- case 'lt':
668
- case 'le':
669
- return buildComparison(filter);
670
- /* ------------------------------------------------------------------ */
671
- /* String */
672
- /* ------------------------------------------------------------------ */
673
- case 'contains':
674
- return buildFunction(filter, 'contains');
675
- case 'startsWith':
676
- return buildFunction(filter, 'startswith');
677
- case 'endsWith':
678
- return buildFunction(filter, 'endswith');
679
- /* ------------------------------------------------------------------ */
680
- /* Value */
681
- /* ------------------------------------------------------------------ */
682
- case 'in':
683
- return buildIn(filter);
684
- case 'between':
685
- return buildBetween(filter);
686
- case 'isNull':
687
- return `${getColumn(filter)} eq null`;
688
- case 'isNotNull':
689
- return `${getColumn(filter)} ne null`;
690
- /* ------------------------------------------------------------------ */
691
- /* Collection */
692
- /* ------------------------------------------------------------------ */
693
- case 'any':
694
- return buildAny(filter);
695
- case 'all':
696
- return buildAll(filter);
697
- /* ------------------------------------------------------------------ */
698
- /* Collection count */
699
- /* ------------------------------------------------------------------ */
700
- case 'countGt':
701
- case 'countGe':
702
- case 'countLt':
703
- case 'countLe':
704
- return buildCount(filter);
705
- default:
706
- throw new Error(`Unsupported OData operator: ${filter.operator}`);
707
- }
708
- }
709
- /* ==========================================================================
710
- * Logical
711
- * ========================================================================== */
712
- function buildLogical(filter, operator) {
713
- const filters = filter.filters || [];
714
- if (filters.length === 0)
715
- return '';
716
- return filters
717
- .map(buildFilter)
718
- .map(expression => `(${expression})`)
719
- .join(` ${operator} `);
720
- }
721
- function buildNot(filter) {
722
- const filters = filter.filters || [];
723
- if (filters.length !== 1)
724
- return '';
725
- return `not (${buildFilter(filters[0])})`;
726
- }
727
- /* ==========================================================================
728
- * Comparison
729
- * ========================================================================== */
730
- function buildComparison(filter) {
731
- const column = getColumn(filter);
732
- return (`${column} ` +
733
- `${filter.operator} ` +
734
- `${formatValue(filter.value)}`);
735
- }
736
- /* ==========================================================================
737
- * String functions
738
- * ========================================================================== */
739
- function buildFunction(filter, functionName) {
740
- const column = getColumn(filter);
741
- return (`${functionName}(` +
742
- `${column},` +
743
- `${formatValue(filter.value)}` +
744
- `)`);
745
- }
746
- /* ==========================================================================
747
- * Function object
748
- *
749
- * Backward compatibility with the old:
750
- *
751
- * {
752
- * func: 'contains',
753
- * params: [...]
754
- * }
755
- * ========================================================================== */
756
- function buildFunctionObject(filter) {
757
- if (!filter.func)
758
- throw new Error('Function is required.');
759
- return (filter.func +
760
- '(' +
761
- (filter.params ?? [])
762
- .map(m => formatValue(m))
763
- .join(',') +
764
- ')');
765
- }
766
- /* ==========================================================================
767
- * IN
768
- * ========================================================================== */
769
- function buildIn(filter) {
770
- const column = getColumn(filter);
771
- if (!Array.isArray(filter.value))
772
- throw new Error("Operator 'in' requires an array value.");
773
- if (filter.value.length === 0)
774
- throw new Error("Operator 'in' requires at least one value.");
775
- return (`${column} in (` +
776
- filter.value
777
- .map(formatValue)
778
- .join(',') +
779
- `)`);
780
- }
781
- /* ==========================================================================
782
- * BETWEEN
783
- * ========================================================================== */
784
- function buildBetween(filter) {
785
- const column = getColumn(filter);
786
- if (!Array.isArray(filter.value) ||
787
- filter.value.length !== 2) {
788
- throw new Error("Operator 'between' requires exactly two values.");
789
- }
790
- const [min, max] = filter.value;
791
- return (`(${column} ge ${formatValue(min)} ` +
792
- `and ${column} le ${formatValue(max)})`);
793
- }
794
- /* ==========================================================================
795
- * ANY
796
- * ========================================================================== */
797
- function buildAny(filter) {
798
- const column = getColumn(filter);
799
- /*
800
- * orders/any()
801
- */
802
- if (!filter.filters?.length)
803
- return `${column}/any()`;
804
- if (filter.filters.length !== 1)
805
- throw new Error("Operator 'any' requires exactly one filter.");
806
- return (`${column}/any(x: ` +
807
- `${buildLambdaFilter(filter.filters[0], 'x')}` +
808
- `)`);
809
- }
810
- /* ==========================================================================
811
- * ALL
812
- * ========================================================================== */
813
- function buildAll(filter) {
814
- const column = getColumn(filter);
815
- const filters = filter.filters || [];
816
- if (filters.length !== 1)
817
- return '';
818
- return (`${column}/all(x: ` +
819
- `${buildLambdaFilter(filters[0], 'x')}` +
820
- `)`);
821
- }
822
- /* ==========================================================================
823
- * Lambda filter
824
- * ========================================================================== */
825
- function buildLambdaFilter(filter, variable) {
826
- /*
827
- * Logical
828
- */
829
- if (filter.operator === 'and')
830
- return buildLambdaLogical(filter, variable, 'and');
831
- if (filter.operator === 'or')
832
- return buildLambdaLogical(filter, variable, 'or');
833
- if (filter.operator === 'not') {
834
- const filters = requireFilters(filter);
835
- if (filters.length !== 1)
836
- throw new Error("Operator 'not' requires exactly one filter.");
837
- return (`not (` +
838
- `${buildLambdaFilter(filters[0], variable)}` +
839
- `)`);
840
- }
841
- /*
842
- * Comparison
843
- */
844
- switch (filter.operator) {
845
- case 'eq':
846
- case 'ne':
847
- case 'gt':
848
- case 'ge':
849
- case 'lt':
850
- case 'le':
851
- return (`${variable}/${getColumn(filter)} ` +
852
- `${filter.operator} ` +
853
- `${formatValue(filter.value)}`);
854
- /*
855
- * String
856
- */
857
- case 'contains':
858
- return (`contains(` +
859
- `${getLambdaColumn(filter, variable)},` +
860
- `${formatValue(filter.value)}` +
861
- `)`);
862
- case 'startsWith':
863
- return (`startswith(` +
864
- `${getLambdaColumn(filter, variable)},` +
865
- `${formatValue(filter.value)}` +
866
- `)`);
867
- case 'endsWith':
868
- return (`endswith(` +
869
- `${getLambdaColumn(filter, variable)},` +
870
- `${formatValue(filter.value)}` +
871
- `)`);
872
- /*
873
- * Null
874
- */
875
- case 'isNull':
876
- return (`${getLambdaColumn(filter, variable)} eq null`);
877
- case 'isNotNull':
878
- return (`${getLambdaColumn(filter, variable)} ne null`);
879
- default:
880
- throw new Error(`Operator '${filter.operator}' ` +
881
- `is not supported inside any/all.`);
882
- }
883
- }
884
- function buildLambdaLogical(filter, variable, operator) {
885
- const filters = requireFilters(filter);
886
- if (filters.length === 0)
887
- throw new Error(`Operator '${filter.operator}' requires at least one filter.`);
888
- return filters
889
- .map(item => buildLambdaFilter(item, variable))
890
- .map(expression => `(${expression})`)
891
- .join(` ${operator} `);
892
- }
893
- /* ==========================================================================
894
- * Collection count
895
- * ========================================================================== */
896
- function buildCount(filter) {
897
- const column = getColumn(filter);
898
- const operators = {
899
- countGt: 'gt',
900
- countGe: 'ge',
901
- countLt: 'lt',
902
- countLe: 'le'
903
- };
904
- return (`${column}/$count ` +
905
- `${operators[filter.operator]} ` +
906
- `${formatValue(filter.value)}`);
907
- }
908
- /* ==========================================================================
909
- * Helpers
910
- * ========================================================================== */
911
- function getColumn(filter) {
912
- if (!filter.column ||
913
- filter.column.trim().length === 0) {
914
- throw new Error(`Operator '${filter.operator}' requires a column.`);
915
- }
916
- /*
917
- * Allow:
918
- *
919
- * customer.name
920
- * customer.address.city
921
- *
922
- * to become:
923
- *
924
- * customer/name
925
- * customer/address/city
926
- */
927
- return filter.column.replace(/\./g, '/');
928
- }
929
- function getLambdaColumn(filter, variable) {
930
- return (`${variable}/` +
931
- `${getColumn(filter)}`);
932
- }
933
- function requireFilters(filter) {
934
- return filter.filters || [];
935
- }
936
- /* ==========================================================================
937
- * OData Request
938
- * ========================================================================== */
939
- function odata(options, props = {}) {
940
- const url = new URL(options.url);
941
- /* ---------------------------------------------------------------------- */
942
- /* Paging */
943
- /* ---------------------------------------------------------------------- */
944
- if (props.pageSize &&
945
- props.pageSize > 0) {
946
- const currPage = props.currPage && props.currPage > 0
947
- ? props.currPage
948
- : 1;
949
- const skip = props.pageSize * (currPage - 1);
950
- url.searchParams.set('$skip', String(skip));
951
- url.searchParams.set('$top', String(props.pageSize));
952
- }
953
- /* ---------------------------------------------------------------------- */
954
- /* Count */
955
- /* ---------------------------------------------------------------------- */
956
- if (options.count === true) {
957
- url.searchParams.set('$count', 'true');
958
- }
959
- /* ---------------------------------------------------------------------- */
960
- /* Select */
961
- /* ---------------------------------------------------------------------- */
962
- if (options.select &&
963
- options.select.length > 0) {
964
- url.searchParams.set('$select', options.select
965
- .map(m => m.replace(/\./g, '/'))
966
- .join(','));
967
- }
968
- /* ---------------------------------------------------------------------- */
969
- /* Expand */
970
- /* ---------------------------------------------------------------------- */
971
- if (options.expand &&
972
- options.expand.length > 0) {
973
- const expand = options.expand
974
- .map(expand => {
975
- const name = expand.name.replace(/\./g, '/');
976
- if (expand.select &&
977
- expand.select.length > 0) {
978
- return (`${name}` +
979
- `($select=` +
980
- `${expand.select
981
- .map(m => m.replace(/\./g, '/'))
982
- .join(',')}` +
983
- `)`);
984
- }
985
- return name;
986
- })
987
- .join(',');
988
- url.searchParams.set('$expand', expand);
989
- }
990
- /* ---------------------------------------------------------------------- */
991
- /* Filter */
992
- /* ---------------------------------------------------------------------- */
993
- const filters = [];
994
- /*
995
- * Static filter from options
996
- */
997
- if (options.filter &&
998
- ((options.filter.filters && options.filter.filters.length > 0) ||
999
- options.filter.column ||
1000
- options.filter.func)) {
1001
- const expression = toODataFilter(options.filter);
1002
- if (expression)
1003
- filters.push(`(${expression})`);
1004
- }
1005
- /*
1006
- * Runtime filter from DataTable
1007
- */
1008
- if (props.filter &&
1009
- ((props.filter.filters && props.filter.filters.length > 0) ||
1010
- props.filter.column ||
1011
- props.filter.func)) {
1012
- const expression = toODataFilter(props.filter);
1013
- if (expression)
1014
- filters.push(`(${expression})`);
1015
- }
1016
- /* ---------------------------------------------------------------------- */
1017
- /* Search */
1018
- /* ---------------------------------------------------------------------- */
1019
- if (props.search &&
1020
- props.search.trim() &&
1021
- props.cols &&
1022
- props.cols.length > 0) {
1023
- const searchValue = escapeODataString(props.search.trim());
1024
- const searchExpressions = props.cols
1025
- .filter(column => column.searchable === undefined ||
1026
- column.searchable === true)
1027
- .map(column => {
1028
- const name = column.name.replace(/\./g, '/');
1029
- return (`contains(` +
1030
- `${name},` +
1031
- `'${searchValue}'` +
1032
- `)`);
1033
- });
1034
- if (searchExpressions.length > 0) {
1035
- filters.push(`(${searchExpressions.join(' or ')})`);
1036
- }
1037
- }
1038
- /*
1039
- * Combine all filters with AND.
1040
- */
1041
- if (filters.length > 0) {
1042
- url.searchParams.set('$filter', filters.join(' and '));
1043
- }
1044
- /* ---------------------------------------------------------------------- */
1045
- /* Order By */
1046
- /* ---------------------------------------------------------------------- */
1047
- if (props.sort &&
1048
- props.sort.length > 0) {
1049
- const orderBy = props.sort
1050
- .map(sort => {
1051
- let column;
1052
- if (sort.index !== undefined &&
1053
- sort.index >= 0) {
1054
- if (!props.cols ||
1055
- !props.cols[sort.index]) {
1056
- throw new Error(`Invalid sort column index: ${sort.index}`);
1057
- }
1058
- column =
1059
- props.cols[sort.index].name;
1060
- }
1061
- else if (sort.name) {
1062
- column = sort.name;
1063
- }
1064
- else {
1065
- throw new Error('Sort requires either index or name.');
1066
- }
1067
- column =
1068
- column.replace(/\./g, '/');
1069
- return (column +
1070
- (sort.dir
1071
- ? ` ${sort.dir}`
1072
- : ''));
1073
- })
1074
- .join(',');
1075
- url.searchParams.set('$orderby', orderBy);
1076
- }
1077
- /* ---------------------------------------------------------------------- */
1078
- /* Request */
1079
- /* ---------------------------------------------------------------------- */
1080
- const requestInit = options.headers ? { headers: options.headers } : undefined;
1081
- return new Request(url, requestInit);
1082
- }
1083
-
1084
- const param = (uri, header, props) => {
1085
- const url = new URL(uri);
1086
- // Paging //
1087
- if (props.pageSize && props.currentPage) {
1088
- url.searchParams.append('skip', String(props.pageSize * (props.currentPage - 1)));
1089
- url.searchParams.append('top', String(props.pageSize));
1090
- }
1091
- // Filter //
1092
- if (props.search)
1093
- url.searchParams.append('search', props.search);
1094
- // Order //
1095
- if (props.order && props.order.length > 0 && props.cols) {
1096
- url.searchParams.append('orderby', props.order.map(m => props.cols?.[m.index]?.name || '').join(','));
1097
- url.searchParams.append('orderdir', props.order.map(m => m.type ? m.type : 'asc').join(','));
1098
- }
1099
- return new Request(url, header);
1100
- };
1101
-
1102
- export { tablevu as TableVu, adaptArray, toODataFilter as buildFilter, odata, param as webapi };