tablevu 0.2.22 → 0.3.0-beta.1

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