tablevu 0.3.0-beta.3 → 0.3.0-beta.5

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