@es-labs/jslib 0.0.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +42 -0
  3. package/__test__/services.test.js +32 -0
  4. package/auth/index.js +226 -0
  5. package/auth/keyv.js +23 -0
  6. package/auth/knex.js +29 -0
  7. package/auth/redis.js +23 -0
  8. package/comms/email.js +123 -0
  9. package/comms/nexmo.js +44 -0
  10. package/comms/telegram.js +43 -0
  11. package/comms/telegram2/inbound.js +314 -0
  12. package/comms/telegram2/outbound.js +574 -0
  13. package/comms/webpush.js +60 -0
  14. package/config.js +37 -0
  15. package/express/controller/auth/oauth.js +39 -0
  16. package/express/controller/auth/oidc.js +87 -0
  17. package/express/controller/auth/own.js +100 -0
  18. package/express/controller/auth/saml.js +74 -0
  19. package/express/upload.js +48 -0
  20. package/index.js +1 -0
  21. package/iso/README.md +4 -0
  22. package/iso/__tests__/csv-utils.spec.js +128 -0
  23. package/iso/__tests__/datetime.spec.js +101 -0
  24. package/iso/__tests__/fetch.spec.js +270 -0
  25. package/iso/csv-utils.js +206 -0
  26. package/iso/datetime.js +103 -0
  27. package/iso/fetch.js +129 -0
  28. package/iso/fetch2.js +180 -0
  29. package/iso/log-filter.js +17 -0
  30. package/iso/sleep.js +6 -0
  31. package/iso/ws.js +63 -0
  32. package/node/oss-files/oss-uploader-client-fetch.js +258 -0
  33. package/node/oss-files/oss-uploader-client-fetch.md +31 -0
  34. package/node/oss-files/oss-uploader-client.js +219 -0
  35. package/node/oss-files/oss-uploader-server.js +199 -0
  36. package/node/oss-files/oss-uploader-usage.js +121 -0
  37. package/node/oss-files/oss-uploader-usage.md +34 -0
  38. package/node/oss-files/s3-uploader-client.js +217 -0
  39. package/node/oss-files/s3-uploader-server.js +123 -0
  40. package/node/oss-files/s3-uploader-usage.js +77 -0
  41. package/node/oss-files/s3-uploader-usage.md +34 -0
  42. package/package.json +53 -0
  43. package/packageInfo.js +9 -0
  44. package/services/ali.js +279 -0
  45. package/services/aws.js +194 -0
  46. package/services/db/__tests__/keyv.spec.js +31 -0
  47. package/services/db/keyv.js +14 -0
  48. package/services/db/knex.js +67 -0
  49. package/services/db/redis.js +51 -0
  50. package/services/index.js +57 -0
  51. package/services/mq/README.md +8 -0
  52. package/services/websocket.js +139 -0
  53. package/t4t/README.md +1 -0
  54. package/traps.js +20 -0
  55. package/utils/__tests__/aes.spec.js +52 -0
  56. package/utils/aes.js +23 -0
  57. package/web/UI.md +71 -0
  58. package/web/bwc-autocomplete.js +211 -0
  59. package/web/bwc-combobox.js +343 -0
  60. package/web/bwc-fileupload.js +87 -0
  61. package/web/bwc-loading-overlay.js +54 -0
  62. package/web/bwc-t4t-form.js +511 -0
  63. package/web/bwc-table.js +756 -0
  64. package/web/fetch.js +129 -0
  65. package/web/i18n.js +24 -0
  66. package/web/idle.js +49 -0
  67. package/web/parse-jwt.js +15 -0
  68. package/web/pwa.js +84 -0
  69. package/web/sign-pad.js +164 -0
  70. package/web/t4t-fe.js +164 -0
  71. package/web/util.js +126 -0
  72. package/web/web-cam.js +182 -0
@@ -0,0 +1,756 @@
1
+ // TODO
2
+ // inline edit?
3
+
4
+ // FEATURES
5
+ // handle columns and items
6
+ // row select
7
+ // pagination (optional)
8
+ // filters (optional)
9
+ // sorter single column (optional)
10
+ // checkbox (optional)
11
+ // sticky header (optional)
12
+ // sticky coloumn (optional - currently only for 1st column)
13
+ // checkbox & check all (optional)
14
+ // custom render columns
15
+
16
+ // STYLING...
17
+ // --bwc-table-width: 100%
18
+ // --bwc-table-overflow: auto
19
+ // --bwc-table-height: 100%
20
+ // --bwc-table-navbar-bgcolor: white
21
+ // --bwc-table-filter-bgcolor: white
22
+ // --bwc-table-filter-color: black
23
+ // --bwc-table-filter-top: 56px
24
+ // --bwc-table-th-bgcolor: white
25
+ // --bwc-table-th-color: black
26
+ // --bwc-table-td-bgcolor: transparent
27
+ // --bwc-table-td-color: black
28
+ // --bwc-table-td-select-bgcolor: black
29
+ // --bwc-table-td-select-color: black
30
+ // --bwc-table-sticky-header-top: 56px
31
+
32
+ // PROPERTIES
33
+ // commands="reload,filter"
34
+ // :pagination="true"
35
+ // :sort="true"
36
+ // :page="page"
37
+ // :pageSize="pageSize"
38
+ // :pageSizeList="pageSizeList"
39
+ // :columns="columns"
40
+ // :items="table.items"
41
+ // :total="total"
42
+ // style="--bwc-table-height: calc(100vh - 360px);--bwc-table-width: 200%;"
43
+ // class="sticky-header sticky-column"
44
+
45
+ // TODO change some properties to attributes? handle multiple UI frameworks
46
+
47
+ // EVENTS
48
+ // rowclick { detail: { row, col, data }
49
+ // triggered = sort / page / page-size / reload { detail: { name, sortKey, sortDir, page, pageSize, filters: [ { key, op, val, andOr } ] } }
50
+ // cmd = show/hide filter, reload, add, del, import, export, goback (if parentKey != null)
51
+ // checked = [indexes checked...]
52
+
53
+ // COLUMN PROPERTIES
54
+ // for hidden table columns, please remove before passing it to component
55
+ // label: 'ID',
56
+ // key: 'id',
57
+ // filter: false,
58
+ // sort: false,
59
+ // render: ({val, key, row, idx}) => `<a class='button' onclick='this.dispatchEvent(new CustomEvent("testevent", { detail: ${JSON.stringify({ val, key, row, idx })} }))'>${val}</a>`
60
+ // cell value, column key, row data, row index (0-based)
61
+ // try not to include row property in event detail... can be too much data
62
+
63
+ // NOT NEEDED
64
+ // loading state and loading spinner
65
+
66
+ // NOTES
67
+ // do not use document.querySelector, use this.querySelector
68
+
69
+ const template = document.createElement('template')
70
+ template.innerHTML = /*html*/`
71
+ <style>
72
+ #table-wrapper {
73
+ overflow: var(--bwc-table-overflow, auto);
74
+ height: var(--bwc-table-height, 100%);
75
+ }
76
+ #table-wrapper table {
77
+ table-layout: initial;
78
+ width: var(--bwc-table-width, 100%);
79
+ }
80
+ #table-wrapper > nav {
81
+ position: -webkit-sticky;
82
+ position: sticky;
83
+ top: 0px;
84
+ left: 0px;
85
+ z-index: 2;
86
+ background-color: var(--bwc-table-navbar-bgcolor, lightslategray) !important;
87
+ }
88
+ #table-wrapper #filters {
89
+ position: -webkit-sticky;
90
+ position: sticky;
91
+ top: var(--bwc-table-filter-top, 56px);
92
+ left: 0px;
93
+ z-index: 2;
94
+ background-color: var(--bwc-table-filter-bgcolor, white);
95
+ color: var(--bwc-table-filter-color, black);
96
+ }
97
+ #table-wrapper th {
98
+ background-color: var(--bwc-table-th-bgcolor, white);
99
+ color: var(--bwc-table-th-color, black);
100
+ }
101
+ #table-wrapper tr td {
102
+ background-color: var(--bwc-table-td-bgcolor, transparent);
103
+ color: var(--bwc-table-td-color, black);
104
+ }
105
+ #table-wrapper tr.is-selected td {
106
+ background-color: var(--bwc-table-td-select-bgcolor, lightgrey);
107
+ color: var(--bwc-table-td-select-color, black);
108
+ }
109
+ .sticky-header #table-wrapper th {
110
+ position: -webkit-sticky;
111
+ position: sticky;
112
+ top: var(--bwc-table-sticky-header-top, 56px); /* nav height - TODO filter height*/
113
+ z-index: 2;
114
+ }
115
+ .sticky-column #table-wrapper th[scope=row] {
116
+ position: -webkit-sticky;
117
+ position: sticky;
118
+ left: 0;
119
+ z-index: 3;
120
+ }
121
+ .sticky-column #table-wrapper th:not([scope=row]) {
122
+ }
123
+ .sticky-column #table-wrapper td[scope=row] {
124
+ position: -webkit-sticky;
125
+ position: sticky;
126
+ left: 0;
127
+ z-index: 1;
128
+ }
129
+ input::-webkit-outer-spin-button, /* to remove up and down arrows */
130
+ input::-webkit-inner-spin-button {
131
+ -webkit-appearance: none;
132
+ margin: 0;
133
+ }
134
+ input[type="number"] {
135
+ -moz-appearance: textfield;
136
+ }
137
+ </style>
138
+ <div id="table-wrapper">
139
+ <nav id="table-navbar" class="navbar" role="navigation" aria-label="main navigation">
140
+ <div class="navbar-brand">
141
+ <a id="table-navbar-burger" role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="table-navbar-menu">
142
+ <span aria-hidden="true"></span>
143
+ <span aria-hidden="true"></span>
144
+ <span aria-hidden="true"></span>
145
+ </a>
146
+ </div>
147
+ <div id="table-navbar-menu" class="navbar-menu">
148
+ <div class="navbar-start">
149
+ <div id="commands" class="navbar-item">
150
+ <a id="cmd-goback" class="button">↶</a>
151
+ <a id="cmd-filter" class="button">o</a><!-- need to make this configurable -->
152
+ <a id="cmd-reload" class="button">↻</a>
153
+ <a id="cmd-add" class="button">+</a>
154
+ <a id="cmd-del" class="button">-</a>
155
+ <a id="cmd-import" class="button">↑</a>
156
+ <a id="cmd-export" class="button">↓</a>
157
+ </div>
158
+ </div>
159
+
160
+ <div class="navbar-end pagination">
161
+ <div class="navbar-item">
162
+ <a id="page-dec" class="button">&lt;</a>
163
+ <a><input id="page-input" class="input" type="number" min="1" style="width: auto;"/></a>
164
+ <a class="button is-static">&nbsp;/&nbsp;<span id="pages-span"></span></a>
165
+ <a id="page-inc" class="button">&gt;</a>
166
+ </div>
167
+ <div class="navbar-item">
168
+ <a>
169
+ <span class="select">
170
+ <select id="page-select">
171
+ </select>
172
+ </span>
173
+ </a>
174
+ <a class="button is-static">Rows/Page</a>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </nav>
179
+ <div id="filters"></div>
180
+ </div>
181
+ `
182
+
183
+ class Table extends HTMLElement {
184
+ // basic
185
+ #columns = []
186
+ #items = []
187
+
188
+ // enable pagination
189
+ #pagination = true
190
+ #page = 1 // one based index
191
+ #pageSize = 10
192
+ #pageSizeList = [5, 10, 15]
193
+ #pages = 0 // computed Math.ceil(total / pageSize)
194
+ #total = 0
195
+
196
+ // enable sorting
197
+ #sort = true
198
+ #sortKey = ''
199
+ #sortDir = '' // blank, asc, desc
200
+
201
+ // checkbox
202
+ #checkboxes = true
203
+ #checkedRows = []
204
+
205
+ // selected
206
+ #selectedIndex = -1
207
+ #selectedNode = null
208
+ #selectedItem = null
209
+
210
+ // enable commands menu
211
+ #commands = ''
212
+
213
+ // filters
214
+ #filters = []
215
+ #filterCols = []
216
+ #filterOps = ['=', 'like', '!=', '>=', '>', '<=', '<']
217
+ #filterShow = false
218
+
219
+ // heights
220
+ #navbarHeight = 56 // #table-navbar
221
+ #filterHeight = 0 // #filters
222
+
223
+ constructor() {
224
+ super()
225
+ // this.input = this.input.bind(this)
226
+ }
227
+
228
+ _setHeights () {
229
+ // console.log(this.#navbarHeight, this.#filterHeight)
230
+ const el = this.querySelector('#filters')
231
+ if (!el) return
232
+ el.style.top = `${this.#navbarHeight}px`
233
+ const nodes = this.querySelectorAll('.sticky-header #table-wrapper th')
234
+ for (let i = 0; i<nodes.length; i++) {
235
+ // console.log('nodes', nodes[i])
236
+ nodes[i].style.top = `${this.#navbarHeight + this.#filterHeight}px`
237
+ }
238
+ }
239
+
240
+ _eventPageInputEL(e) {
241
+ const page = Number(e.target.value)
242
+ if (page >= 1 && page <= this.#pages && Number(page) !== Number(this.page)) {
243
+ this.page = page
244
+ this._trigger('page')
245
+ } else {
246
+ this._renderPageInput()
247
+ }
248
+ }
249
+
250
+ connectedCallback() {
251
+ console.log('connected callback')
252
+
253
+ // console.log(this.value, this.required, typeof this.required)
254
+ this.appendChild(template.content.cloneNode(true))
255
+
256
+ // this.querySelector('input').addEventListener('input', this.input)
257
+ // if (this.required !== null) el.setAttribute('required', '')
258
+
259
+ // Check for click events on the navbar burger icon
260
+ this.querySelector('.navbar-burger').onclick = () => {
261
+ // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
262
+ this.querySelector('#table-navbar-burger').classList.toggle('is-active') // navbar-burger
263
+ this.querySelector('#table-navbar-menu').classList.toggle('is-active') // navbar-menu
264
+ }
265
+ this.querySelector('#page-input').onkeypress = (e) => {
266
+ e.code === 'Enter' && this._eventPageInputEL(e)
267
+ }
268
+ this.querySelector('#page-input').onblur = (e) => {
269
+ this._eventPageInputEL(e)
270
+ }
271
+
272
+ this.querySelector('#cmd-filter').onclick = () => {
273
+ this.#filterShow = !this.#filterShow
274
+ this.querySelector('#filters').style.display = this.#filterShow ? 'block': 'none'
275
+ }
276
+
277
+ new ResizeObserver(entries => {
278
+ this.#navbarHeight = entries[0].target.clientHeight
279
+ this._setHeights()
280
+ }).observe(this.querySelector('#table-navbar'))
281
+
282
+ new ResizeObserver(entries => {
283
+ this.#filterHeight = entries[0].target.clientHeight
284
+ this._setHeights()
285
+ }).observe(this.querySelector('#filters')) // start observing a DOM node
286
+
287
+ this.querySelector('#cmd-reload').onclick = () => this._trigger('reload')
288
+ this.querySelector('#cmd-goback').onclick = () => this.dispatchEvent(new CustomEvent('cmd', { detail: { cmd: 'goback' } }))
289
+ this.querySelector('#cmd-add').onclick = () => this.dispatchEvent(new CustomEvent('cmd', { detail: { cmd: 'add' } }))
290
+ this.querySelector('#cmd-del').onclick = () => this.dispatchEvent(new CustomEvent('cmd', { detail: { cmd: 'del', checkedRows: this.#checkedRows } }))
291
+ this.querySelector('#cmd-import').onclick = () => this.dispatchEvent(new CustomEvent('cmd', { detail: { cmd: 'import' } }))
292
+ this.querySelector('#cmd-export').onclick = () => this.dispatchEvent(new CustomEvent('cmd', { detail: { cmd: 'export', checkedRows: this.#checkedRows } }))
293
+ this.querySelector('#page-dec').onclick = (e) => {
294
+ let numPage = Number(this.page)
295
+ if (numPage > 1 && numPage <= this.#pages) {
296
+ numPage -= 1
297
+ this.page = numPage
298
+ this._trigger('page')
299
+ }
300
+ }
301
+ this.querySelector('#page-inc').onclick = (e) => {
302
+ // console.log('inc page', this.page, this.#pages)
303
+ let numPage = Number(this.page)
304
+ if (numPage < this.#pages) {
305
+ numPage += 1
306
+ this.page = numPage
307
+ this._trigger('page')
308
+ }
309
+ }
310
+ this.querySelector('#page-select').onchange = (e) => {
311
+ this.pageSize = e.target.value
312
+ this._trigger('page-size')
313
+ if (this.page > this.#pages){
314
+ this.page = this.#pages
315
+ this._trigger('page-size')
316
+ }
317
+ }
318
+
319
+ // console.log('connectedCallback 0')
320
+
321
+ // initialize non-required properties that are undefined
322
+ if (!this.#sortKey) this.#sortKey = ''
323
+ if (!this.#sortDir) this.#sortDir = ''
324
+
325
+ this.querySelector('#filters').style.display = this.#filterShow ? 'block': 'none'
326
+ if (!this.#pagination) this.querySelector('.pagination').style.display = 'none'
327
+ if (!this.#commands || typeof this.#commands !== 'string') {
328
+ this.querySelector('#commands').style.display = 'none'
329
+ }
330
+ else {
331
+ this.querySelector('#cmd-reload').style.display = this.#commands.includes('reload') ? 'block' : 'none'
332
+ this.querySelector('#cmd-filter').style.display = this.#commands.includes('filter') ? 'block' : 'none'
333
+ this.querySelector('#cmd-add').style.display = this.#commands.includes('add') ? 'block' : 'none'
334
+ this.querySelector('#cmd-del').style.display = this.#commands.includes('del') ? 'block' : 'none'
335
+ this.querySelector('#cmd-import').style.display = this.#commands.includes('import') ? 'block' : 'none'
336
+ this.querySelector('#cmd-export').style.display = this.#commands.includes('export') ? 'block' : 'none'
337
+ this.querySelector('#cmd-goback').style.display = this.#commands.includes('goback') ? 'block' : 'none'
338
+ }
339
+
340
+ this._render()
341
+ this._renderPageSelect()
342
+ this._renderPageInput()
343
+ this._renderPages()
344
+ this._renderFilters()
345
+
346
+ // console.log('connectedCallback 1')
347
+ }
348
+
349
+ disconnectedCallback() {
350
+ // this.querySelector('input').removeEventListener('input', this.input)
351
+ }
352
+
353
+ // attributeChangedCallback(name, oldVal, newVal) {
354
+ // switch (name) {
355
+ // case 'page': { break }
356
+ // }
357
+ // }
358
+ // static get observedAttributes() {
359
+ // return ['page']
360
+ // }
361
+
362
+ get checkboxes () { return this.#checkboxes }
363
+ set checkboxes (val) { this.#checkboxes = val }
364
+ get pagination () { return this.#pagination }
365
+ set pagination (val) { this.#pagination = val }
366
+ get commands () { return this.#commands }
367
+ set commands (val) { this.#commands = val }
368
+ get sort () { return this.#sort }
369
+ set sort (val) { this.#sort = val }
370
+
371
+ get page () { return this.#page }
372
+ set page (val) { this.#page = val } // DONE ELSEWHERE emit event
373
+
374
+ get pageSize () { return this.#pageSize }
375
+ set pageSize (val) {
376
+ console.log('set pageSize', this.total , this.pageSize)
377
+ this.#pageSize = val
378
+ this._renderPages()
379
+ } // DONE ELSEWHERE emit event
380
+
381
+ get pageSizeList () { return this.#pageSizeList }
382
+ set pageSizeList (val) { this.#pageSizeList = val } // TODO emit event
383
+ get items() { return this.#items }
384
+ set items(val) {
385
+ // console.log('set items')
386
+ this.#items = val
387
+ this._render()
388
+ this._renderPageSelect()
389
+ this._renderPageInput()
390
+ this._renderPages()
391
+ } // if columns do something
392
+
393
+ get total () { return this.#total }
394
+ set total (val) {
395
+ this.#total = val
396
+ this._renderPages()
397
+ } // emit event ?
398
+
399
+ get selectedItem () { return this.#selectedItem }
400
+ set selectedItem (val) { this.#selectedItem = val }
401
+ get columns() { return this.#columns }
402
+ set columns(val) {
403
+ this.#columns = val
404
+ this._render()
405
+ }
406
+
407
+ _renderPages () {
408
+ this.#pages = Math.ceil(this.total / this.pageSize)
409
+ const el = this.querySelector('#pages-span')
410
+ if (el) el.textContent = this.#pages
411
+ }
412
+
413
+ _renderPageSelect () {
414
+ const el = this.querySelector('#page-select')
415
+ if (!el) return
416
+ el.textContent = '' // remove all children
417
+ this.pageSizeList.forEach(item => {
418
+ const option = document.createElement('option')
419
+ option.value = item
420
+ option.textContent = item
421
+ if (Number(item) === Number(this.pageSize)) option.selected = true
422
+ el.appendChild(option)
423
+ })
424
+ }
425
+
426
+ _renderPageInput () {
427
+ const el = this.querySelector('#page-input')
428
+ if (!el) return
429
+ el.value = this.page
430
+ }
431
+
432
+ _createSelect (items, filter, prop) {
433
+ const p = document.createElement('p')
434
+ p.classList.add('control', 'm-0')
435
+ const span = document.createElement('span')
436
+ span.classList.add('select')
437
+ const select = document.createElement('select')
438
+ items.forEach(item => {
439
+ const option = document.createElement('option')
440
+ if (item.key) {
441
+ option.textContent = item.label
442
+ option.value = item.key
443
+ } else {
444
+ option.textContent = item
445
+ option.value = item
446
+ }
447
+ select.appendChild(option)
448
+ })
449
+ select.value = filter[prop]
450
+ select.onchange = e => filter[prop] = e.target.value
451
+ span.appendChild(select)
452
+ p.appendChild(span)
453
+ return p
454
+ }
455
+
456
+ _renderFilters () {
457
+ const el = this.querySelector('#filters')
458
+ el.textContent = ''
459
+ if (this.#filters.length) {
460
+ for (let i=0; i < this.#filters.length; i++) {
461
+ const filter = this.#filters[i]
462
+ const div = document.createElement('div')
463
+ div.classList.add('field', 'has-addons', 'm-0', 'p-1')
464
+
465
+ div.appendChild( this._createSelect (this.#filterCols, filter, 'key') ) // TODO set input type and pattern based on column UI change event
466
+ div.appendChild( this._createSelect (this.#filterOps, filter, 'op') )
467
+
468
+ const p = document.createElement('p')
469
+ p.classList.add('control', 'm-0')
470
+ const filterInput = document.createElement('input')
471
+ filterInput.classList.add('input')
472
+ filterInput.value = filter.val
473
+ filterInput.oninput = e => filter.val = e.target.value // so that we can keep the filter value
474
+ p.appendChild(filterInput)
475
+ div.appendChild(p)
476
+
477
+ const pf = document.createElement('p')
478
+ pf.classList.add('control', 'm-0')
479
+ pf.innerHTML = `<span class="select">
480
+ <select id="filter-and-or">
481
+ <option value="and">And</option>
482
+ <option value="or">Or</option>
483
+ </select>
484
+ </span>`
485
+ pf.querySelector('#filter-and-or').value = filter.andOr
486
+ pf.querySelector('#filter-and-or').onchange = e => filter.andOr = e.target.value
487
+ div.appendChild(pf)
488
+
489
+ const p1 = document.createElement('p')
490
+ p1.classList.add('control', 'm-0')
491
+ const delBtn = document.createElement('button')
492
+ delBtn.classList.add('button')
493
+ delBtn.textContent = '-'
494
+ delBtn.onclick = () => this._delFilter(i)
495
+ p1.appendChild(delBtn)
496
+ div.appendChild(p1)
497
+
498
+ const p2 = document.createElement('p')
499
+ p2.classList.add('control', 'm-0')
500
+ const addBtn = document.createElement('button')
501
+ addBtn.classList.add('button')
502
+ addBtn.textContent = '+'
503
+ addBtn.onclick = () => this._addFilter(i + 1)
504
+ p2.appendChild(addBtn)
505
+ div.appendChild(p2)
506
+
507
+ el.appendChild(div)
508
+ }
509
+ } else {
510
+ const div = document.createElement('div')
511
+ div.classList.add('field', 'p-1')
512
+ const p = document.createElement('p')
513
+ p.classList.add('control')
514
+ const btn = document.createElement('button')
515
+ btn.classList.add('button')
516
+ btn.textContent = '+'
517
+ btn.onclick = () => this._addFilter(0)
518
+ p.appendChild(btn)
519
+ div.appendChild(p)
520
+ el.appendChild(div)
521
+ }
522
+ }
523
+
524
+ _trigger (name) {
525
+ const filters = []
526
+ const el = this.querySelector('#filters')
527
+ for (let i=0; i<el.children.length; i++) {
528
+ const div = el.children[i]
529
+ if (div.children.length >= 4) {
530
+ filters.push({
531
+ key: div.children[0].querySelector('select').value,
532
+ op: div.children[1].querySelector('select').value,
533
+ val: div.children[2].querySelector('input').value,
534
+ andOr: div.children[3].querySelector('select').value
535
+ })
536
+ }
537
+ }
538
+ this.dispatchEvent(new CustomEvent('triggered', {
539
+ // get filter information
540
+ detail: {
541
+ name, // page, sort
542
+ sortKey: this.#sortKey,
543
+ sortDir: this.#sortDir,
544
+ page: this.page || 0,
545
+ pageSize: this.pageSize || 0,
546
+ filters
547
+ }
548
+ }))
549
+ }
550
+
551
+ // filters
552
+ _delFilter (index) {
553
+ this.#filters.splice(index, 1) // console.log('remove filter', index)
554
+ this._renderFilters()
555
+ }
556
+ _addFilter (index) {
557
+ this.#filters.splice(index, 0, { key: this.#filterCols[0].key, label: this.#filterCols[0].label, op: this.#filterOps[0], val: '', andOr: 'and' })
558
+ this._renderFilters()
559
+ }
560
+
561
+ _render() {
562
+ // console.log('bwc-table render fired')
563
+ try {
564
+ const el = this.querySelector('#table-wrapper')
565
+ if (!el) return
566
+ //<tfoot><tr><th><abbr title="Position">Pos</abbr></th>
567
+
568
+ let table = el.querySelector('table')
569
+ if (table) {
570
+ // const cNode = table.cloneNode(false)
571
+ // table.parentNode.replaceChild(cNode, table)
572
+ // table.innerHTML = ''
573
+ const parent = el.querySelector('table') // WORKS!
574
+ while (parent.firstChild) {
575
+ parent.firstChild.remove()
576
+ }
577
+ parent.remove()
578
+ }
579
+
580
+ if (this.#columns && typeof this.#columns === 'object') {
581
+ // console.log('render thead')
582
+ table = document.createElement('table')
583
+ table.setAttribute('id', 'table')
584
+ el.appendChild(table)
585
+ const thead = document.createElement('thead')
586
+ thead.onclick = (e) => {
587
+ let target = e.target
588
+ if (this.#checkboxes && !target.cellIndex) { // checkbox clicked - target.type === 'checkbox' // e.stopPropagation()?
589
+ this.#checkedRows = [] // clear first
590
+ const tbody = this.querySelector('table tbody')
591
+ if (tbody && tbody.children) {
592
+ for (let i = 0; i < tbody.children.length; i++) {
593
+ const tr = tbody.children[i]
594
+ const td = tr.firstChild
595
+ if (td) {
596
+ const checkbox = td.firstChild
597
+ if (checkbox.type === 'checkbox') {
598
+ checkbox.checked = target.checked
599
+ if (target.checked) this.#checkedRows.push(i)
600
+ }
601
+ }
602
+ }
603
+ }
604
+ this.dispatchEvent(new CustomEvent('checked', { detail: this.#checkedRows }))
605
+ } else { // sort
606
+ if (!this.sort) return
607
+ const offset = this.#checkboxes ? 1 : 0 // column offset
608
+ const col = target.cellIndex - offset // TD 0-index based column
609
+ if (!this.#columns[col].sort) return
610
+ const key = this.#columns[col].key
611
+
612
+ if (key !== this.#sortKey) {
613
+ this.#sortKey = key
614
+ this.#sortDir = 'asc'
615
+ } else {
616
+ if (this.#sortDir === 'asc') {
617
+ this.#sortDir = 'desc'
618
+ } else if (this.#sortDir === 'desc') {
619
+ this.#sortKey = ''
620
+ this.#sortDir = ''
621
+ }
622
+ }
623
+
624
+ this._trigger('sort') // header is re-rendered, checkboxes are also cleared...
625
+ }
626
+ }
627
+ table.appendChild(thead)
628
+ table.classList.add('table')
629
+ const tr = document.createElement('tr')
630
+ thead.appendChild(tr)
631
+ if (this.#checkboxes) { // check all
632
+ const th = document.createElement('th')
633
+ th.style.width = '50px' // TODO do not hardcode
634
+ const checkbox = document.createElement('input')
635
+ checkbox.type = 'checkbox' // value
636
+ th.setAttribute('scope', 'row')
637
+ th.appendChild(checkbox)
638
+ tr.appendChild(th)
639
+ }
640
+ this.#filterCols = [] // clear this first
641
+ for (const col of this.#columns) {
642
+ const th = document.createElement('th')
643
+ if (col.sort) th.style.cursor = 'pointer'
644
+ let label = col.label
645
+ if (col.sort) {
646
+ if (this.#sortKey === col.key) {
647
+ // &and; (up) & &or; (down)
648
+ label += this.#sortDir === 'asc' ? '↑' : (this.#sortDir === 'desc' ? '↓' : '↕')
649
+ } else {
650
+ label += '↕'
651
+ }
652
+ }
653
+ if (col.width) th.style.width = `${col.width}px`
654
+ if (col.sticky) th.setAttribute('scope', 'row')
655
+
656
+ th.appendChild(document.createTextNode(label))
657
+ tr.appendChild(th)
658
+
659
+ // set filters...
660
+ if (col.filter) this.#filterCols.push({
661
+ key: col.key,
662
+ label: col.label
663
+ }) // process filters (col is key)
664
+ }
665
+
666
+ // populate the data
667
+ if (this.#items && typeof this.#items === 'object' && this.#items.length) {
668
+ // console.log('render tbody')
669
+ const tbody = document.createElement('tbody')
670
+ // TODO function to get checked rows...
671
+ tbody.onclick = (e) => {
672
+ let target = e.target
673
+ if (this.#checkboxes && !target.cellIndex) { // checkbox clicked - target.type === 'checkbox' // e.stopPropagation()?
674
+ if (target.type === 'checkbox') {
675
+ this.#checkedRows = [] // clear first
676
+ for (let i = 0; i < tbody.children.length; i++) {
677
+ const tr = tbody.children[i]
678
+ const td = tr.firstChild
679
+ if (td) {
680
+ const checkbox = td.firstChild
681
+ if (checkbox.type === 'checkbox' && checkbox.checked) {
682
+ this.#checkedRows.push(i)
683
+ }
684
+ }
685
+ }
686
+ this.dispatchEvent(new CustomEvent('checked', { detail: this.#checkedRows }))
687
+ }
688
+ } else {
689
+ const offset = this.#checkboxes ? 1 : 0 // column offset
690
+ const col = target.cellIndex - offset // TD 0-index based column
691
+
692
+ while (target && target.nodeName !== "TR") {
693
+ target = target.parentNode
694
+ }
695
+ const row = target.rowIndex - 1 // TR 1-index based row
696
+ let data = null
697
+ if (target) { // TODO - To handle multiple UI frameworks
698
+ if (this.#selectedNode) { // clear class is-selected
699
+ this.#selectedNode.classList.remove('is-selected')
700
+ }
701
+ if (this.#selectedIndex === row && this.#selectedIndex !== -1) { // unselect
702
+ this.#selectedIndex = -1
703
+ this.selectedItem = null
704
+ } else {
705
+ data = { ...this.#items[row] }
706
+ this.#selectedNode = target // set selected
707
+ this.#selectedIndex = row
708
+ this.selectedItem = { row, col, data }
709
+ target.classList.add('is-selected')
710
+ }
711
+ }
712
+ this.dispatchEvent(new CustomEvent('rowclick', { detail: { row, col, data } }))
713
+ }
714
+ }
715
+
716
+ table.appendChild(tbody)
717
+ for (const [idx, row] of this.#items.entries()) {
718
+ const tr = document.createElement('tr')
719
+ tbody.appendChild(tr)
720
+
721
+ if (this.#checkboxes) { // add checkbox
722
+ const td = document.createElement('td')
723
+ const checkbox = document.createElement('input')
724
+ checkbox.type = 'checkbox' // value
725
+ td.setAttribute('scope', 'row')
726
+ td.appendChild(checkbox)
727
+ tr.appendChild(td)
728
+ }
729
+
730
+ for (const col of this.#columns) {
731
+ const { key, sticky, width, render } = col
732
+ const td = document.createElement('td')
733
+ // if (sticky) td.setAttribute('scope', 'row') // not used yet, need to calculate left property value
734
+ if (width) td.style.width = `${width}px`
735
+ if (render) {
736
+ td.innerHTML = render({
737
+ val: row[key],
738
+ key,
739
+ row,
740
+ idx
741
+ }) // value, key, row - need to sanitize, el (the td element)
742
+ } else {
743
+ td.appendChild(document.createTextNode(row[key]))
744
+ }
745
+ tr.appendChild(td)
746
+ }
747
+ }
748
+ }
749
+ }
750
+ } catch (e) {
751
+ console.log(e)
752
+ }
753
+ }
754
+ }
755
+
756
+ customElements.define('bwc-table', Table)