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