@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.
- package/CHANGELOG.md +4 -0
- package/README.md +42 -0
- package/__test__/services.test.js +32 -0
- package/auth/index.js +226 -0
- package/auth/keyv.js +23 -0
- package/auth/knex.js +29 -0
- package/auth/redis.js +23 -0
- package/comms/email.js +123 -0
- package/comms/nexmo.js +44 -0
- package/comms/telegram.js +43 -0
- package/comms/telegram2/inbound.js +314 -0
- package/comms/telegram2/outbound.js +574 -0
- package/comms/webpush.js +60 -0
- package/config.js +37 -0
- package/express/controller/auth/oauth.js +39 -0
- package/express/controller/auth/oidc.js +87 -0
- package/express/controller/auth/own.js +100 -0
- package/express/controller/auth/saml.js +74 -0
- package/express/upload.js +48 -0
- package/index.js +1 -0
- package/iso/README.md +4 -0
- package/iso/__tests__/csv-utils.spec.js +128 -0
- package/iso/__tests__/datetime.spec.js +101 -0
- package/iso/__tests__/fetch.spec.js +270 -0
- package/iso/csv-utils.js +206 -0
- package/iso/datetime.js +103 -0
- package/iso/fetch.js +129 -0
- package/iso/fetch2.js +180 -0
- package/iso/log-filter.js +17 -0
- package/iso/sleep.js +6 -0
- package/iso/ws.js +63 -0
- package/node/oss-files/oss-uploader-client-fetch.js +258 -0
- package/node/oss-files/oss-uploader-client-fetch.md +31 -0
- package/node/oss-files/oss-uploader-client.js +219 -0
- package/node/oss-files/oss-uploader-server.js +199 -0
- package/node/oss-files/oss-uploader-usage.js +121 -0
- package/node/oss-files/oss-uploader-usage.md +34 -0
- package/node/oss-files/s3-uploader-client.js +217 -0
- package/node/oss-files/s3-uploader-server.js +123 -0
- package/node/oss-files/s3-uploader-usage.js +77 -0
- package/node/oss-files/s3-uploader-usage.md +34 -0
- package/package.json +53 -0
- package/packageInfo.js +9 -0
- package/services/ali.js +279 -0
- package/services/aws.js +194 -0
- package/services/db/__tests__/keyv.spec.js +31 -0
- package/services/db/keyv.js +14 -0
- package/services/db/knex.js +67 -0
- package/services/db/redis.js +51 -0
- package/services/index.js +57 -0
- package/services/mq/README.md +8 -0
- package/services/websocket.js +139 -0
- package/t4t/README.md +1 -0
- package/traps.js +20 -0
- package/utils/__tests__/aes.spec.js +52 -0
- package/utils/aes.js +23 -0
- package/web/UI.md +71 -0
- package/web/bwc-autocomplete.js +211 -0
- package/web/bwc-combobox.js +343 -0
- package/web/bwc-fileupload.js +87 -0
- package/web/bwc-loading-overlay.js +54 -0
- package/web/bwc-t4t-form.js +511 -0
- package/web/bwc-table.js +756 -0
- package/web/fetch.js +129 -0
- package/web/i18n.js +24 -0
- package/web/idle.js +49 -0
- package/web/parse-jwt.js +15 -0
- package/web/pwa.js +84 -0
- package/web/sign-pad.js +164 -0
- package/web/t4t-fe.js +164 -0
- package/web/util.js +126 -0
- package/web/web-cam.js +182 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// https://www.npmjs.com/package/ws
|
|
2
|
+
// NOTE: if --forcedExit --detectOpenHandles in JEST test, will cause error
|
|
3
|
+
// TODO: automated testing for websockets
|
|
4
|
+
import WebSocket, { WebSocketServer } from 'ws'
|
|
5
|
+
import https from 'https'
|
|
6
|
+
|
|
7
|
+
// NOSONAR
|
|
8
|
+
// function heartbeat() {
|
|
9
|
+
// clearTimeout(this.pingTimeout)
|
|
10
|
+
// // Use `WebSocket#terminate()` and not `WebSocket#close()`. Delay should be
|
|
11
|
+
// // equal to the interval at which your server sends out pings plus a
|
|
12
|
+
// // conservative assumption of the latency.
|
|
13
|
+
// this.pingTimeout = setTimeout(() => {
|
|
14
|
+
// this.terminate()
|
|
15
|
+
// }, 30000 + 1000)
|
|
16
|
+
// const client = new WebSocket('wss://echo.websocket.org/')
|
|
17
|
+
// client.on('open', heartbeat)
|
|
18
|
+
// client.on('ping', heartbeat)
|
|
19
|
+
// client.on('close', function clear() {
|
|
20
|
+
// clearTimeout(this.pingTimeout)
|
|
21
|
+
// })
|
|
22
|
+
// let WSServer = require('ws').Server
|
|
23
|
+
// // Create web socket server on top of a regular http server
|
|
24
|
+
// let wss = new WSServer({ server })
|
|
25
|
+
// server.on('request', app)
|
|
26
|
+
|
|
27
|
+
export default class Wss {
|
|
28
|
+
constructor(options = JSON.parse(process.env.WS_OPTIONS || null) || {}) {
|
|
29
|
+
if (!Wss._instance) {
|
|
30
|
+
Wss._instance = this
|
|
31
|
+
this._port = options.WS_PORT
|
|
32
|
+
this._keepAliveMs = options.WS_KEEEPALIVE_MS
|
|
33
|
+
this._wss = null
|
|
34
|
+
this._onClientConnect = (ws) => { }
|
|
35
|
+
this._onClientClose = (ws) => { }
|
|
36
|
+
this._onClientMessage = async (data, isBinary, ws, wss) => { // client incoming message
|
|
37
|
+
const message = isBinary ? data : data.toString()
|
|
38
|
+
// console.log('message', message)
|
|
39
|
+
try { // try-catch only detect immediate error, cannot detect if write failure
|
|
40
|
+
if (wss) { // send to other clients except self
|
|
41
|
+
wss.clients.forEach((client) => {
|
|
42
|
+
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
|
43
|
+
client.send(message) // send message to others
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
// ws.send('something', function ack(error) { console.log }) // If error !defined, send has been completed, otherwise error object will indicate what failed.
|
|
47
|
+
ws.send(message) // echo back message...
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.log(e.toString())
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Wss._instance
|
|
55
|
+
}
|
|
56
|
+
static getInstance() {
|
|
57
|
+
return this._instance
|
|
58
|
+
}
|
|
59
|
+
setOnClientMessage(onClientMessageFn) {
|
|
60
|
+
this._onClientMessage = onClientMessageFn
|
|
61
|
+
}
|
|
62
|
+
setOnClientConnect(onClientConnectFn) { // what to do when client connects
|
|
63
|
+
this._onClientConnect = onClientConnectFn
|
|
64
|
+
}
|
|
65
|
+
setOnClientCLose(onClientCloseFn) { // what to do when client closes
|
|
66
|
+
this._onClientClose = onClientCloseFn
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get() {
|
|
70
|
+
return this._instance
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
send(data) {
|
|
74
|
+
this._wss.clients.forEach((client) => {
|
|
75
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
76
|
+
client.send(data)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
open(server=null, app=null) {
|
|
82
|
+
const { HTTPS_PRIVATE_KEY, HTTPS_CERTIFICATE } = process.env
|
|
83
|
+
let err
|
|
84
|
+
try {
|
|
85
|
+
if (!this._wss && this._port) {
|
|
86
|
+
if (HTTPS_CERTIFICATE) {
|
|
87
|
+
if (!server) server = https.createServer({
|
|
88
|
+
key: HTTPS_PRIVATE_KEY, cert: HTTPS_CERTIFICATE
|
|
89
|
+
}).listen(this._port) // use same port, create server because of graphql subscriptions
|
|
90
|
+
this._wss = new WebSocketServer({ server })
|
|
91
|
+
} else {
|
|
92
|
+
if (!server) this._wss = new WebSocketServer({ port: this._port }) // use seperate port
|
|
93
|
+
else this._wss = new WebSocketServer({ server })
|
|
94
|
+
}
|
|
95
|
+
if (app) server.on('request', app)
|
|
96
|
+
|
|
97
|
+
console.log('WS API listening on port ' + this._port)
|
|
98
|
+
if (this._wss) {
|
|
99
|
+
this._wss.on('connection', (ws) => {
|
|
100
|
+
// console.log('ws client connected')
|
|
101
|
+
this._onClientConnect(ws) // what else to do when client connects
|
|
102
|
+
ws.isAlive = true
|
|
103
|
+
ws.on('pong', () => { ws.isAlive = true })
|
|
104
|
+
ws.on('close', () => this._onClientClose(ws))
|
|
105
|
+
ws.on('message', (data, isBinary) => this._onClientMessage(data, isBinary, ws, this._wss))
|
|
106
|
+
})
|
|
107
|
+
setInterval(() => { // set keep-alive
|
|
108
|
+
// console.log('WS Clients: ', this._wss.clients.size)
|
|
109
|
+
this._wss && this._wss.clients.forEach((ws) => {
|
|
110
|
+
if (!ws.isAlive) return ws.terminate() // force close
|
|
111
|
+
ws.isAlive = false
|
|
112
|
+
return ws.ping(() => {}) // NOSONAR
|
|
113
|
+
})
|
|
114
|
+
}, this._keepAliveMs)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log('NO WS Service To Open')
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
err = e.toString()
|
|
121
|
+
}
|
|
122
|
+
console.log('WS Open ' + (err ? err : 'Done'))
|
|
123
|
+
return this
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
close () {
|
|
127
|
+
try { // close all connections
|
|
128
|
+
if (this._wss) {
|
|
129
|
+
this._wss.close()
|
|
130
|
+
// this._wss.clients.forEach(client => client.close(0, 'wss close() called')) // close gracefully
|
|
131
|
+
for (const client of this._wss.clients) client.terminate() // https://github.com/websockets/ws/releases/tag/8.0.0
|
|
132
|
+
this._wss = null //delete wss
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(e.toString())
|
|
136
|
+
}
|
|
137
|
+
console.log('WS API CLOSE OK')
|
|
138
|
+
}
|
|
139
|
+
}
|
package/t4t/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
A simple table editor and form backend
|
package/traps.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default (signalFn, exceptionFn) => {
|
|
2
|
+
let defaultExceptionFn = async (e, type) => {
|
|
3
|
+
console.log(type, e.toString())
|
|
4
|
+
// process.emit("SIGTERM") // process.exit(0), process.kill(process.pid, type)
|
|
5
|
+
}
|
|
6
|
+
if (!exceptionFn) exceptionFn = defaultExceptionFn
|
|
7
|
+
const exceptions = ['unhandledRejection', 'uncaughtException']
|
|
8
|
+
exceptions.forEach(type => process.on(type, (e) => exceptionFn(e, type)))
|
|
9
|
+
|
|
10
|
+
let defaultSignalFn = async (type) => console.log(type)
|
|
11
|
+
if (!signalFn) signalFn = defaultSignalFn
|
|
12
|
+
const signals = ['SIGTERM', 'SIGINT', 'SIGUSR2'] // SIGINT now works on windows
|
|
13
|
+
// if (process.platform === 'win32') {
|
|
14
|
+
// import('readline').then(readline => readline.createInterface({ input: process.stdin, output: process.stdout }).on('SIGINT', () => process.emit('SIGINT')))
|
|
15
|
+
// }
|
|
16
|
+
signals.forEach(type => process.once(type, async () => {
|
|
17
|
+
const exitCode = await signalFn(type)
|
|
18
|
+
return Number.isInteger(exitCode) ? process.exit(parseInt(exitCode)) : process.exit(-10001) // should terminate the application
|
|
19
|
+
}))
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { genIv, genKey, encryptText, decryptText } from '../aes.js';
|
|
5
|
+
|
|
6
|
+
describe('utils/aes.js', () => {
|
|
7
|
+
const algorithm = 'aes256';
|
|
8
|
+
const password = 'test-password';
|
|
9
|
+
const plaintext = 'Hello, AES!';
|
|
10
|
+
|
|
11
|
+
it('should generate a valid IV (16 bytes)', () => {
|
|
12
|
+
const iv = genIv();
|
|
13
|
+
assert.ok(Buffer.isBuffer(iv));
|
|
14
|
+
assert.strictEqual(iv.length, 16);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should derive a 256 bit key from password', () => {
|
|
18
|
+
const key = genKey(algorithm, password);
|
|
19
|
+
assert.ok(Buffer.isBuffer(key));
|
|
20
|
+
assert.strictEqual(key.length, 32);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should derive a 256 bit key from password', () => {
|
|
24
|
+
const key = genKey('sha128', password);
|
|
25
|
+
assert.ok(Buffer.isBuffer(key));
|
|
26
|
+
assert.strictEqual(key.length, 32);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should encrypt and decrypt text correctly', () => {
|
|
30
|
+
const iv = genIv();
|
|
31
|
+
const key = genKey(algorithm, password);
|
|
32
|
+
|
|
33
|
+
const encrypted = encryptText(algorithm, key, iv, plaintext);
|
|
34
|
+
assert.ok(typeof encrypted === 'string');
|
|
35
|
+
|
|
36
|
+
const decrypted = decryptText(algorithm, key, iv, encrypted);
|
|
37
|
+
assert.strictEqual(decrypted, plaintext);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
const test_aes = (data = 'test data', password = 'pw', algorithm = 'aes256') => {
|
|
44
|
+
const [iv, key] = [genIv(), genKey(algorithm, password)]
|
|
45
|
+
const encText = encryptText(algorithm, key, iv, data)
|
|
46
|
+
const decText = decryptText(algorithm, key, iv, encText)
|
|
47
|
+
console.log('Password:', password)
|
|
48
|
+
console.log('Key: ', key.toString(DEFAULT_ENCODING))
|
|
49
|
+
console.log('Plaintext: ' + decText)
|
|
50
|
+
console.log('Encrypted (aes256): ' + encText)
|
|
51
|
+
}
|
|
52
|
+
*/
|
package/utils/aes.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
const DEFAULT_ENCODING = 'base64' // 'binary'
|
|
3
|
+
|
|
4
|
+
export const encryptText = (alg, key, iv, text, encoding = DEFAULT_ENCODING) => {
|
|
5
|
+
const cipher = crypto.createCipheriv(alg, key, iv)
|
|
6
|
+
return cipher.update(text, 'utf8', encoding) + cipher.final(encoding)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const decryptText = (alg, key, iv, text, encoding = DEFAULT_ENCODING) => {
|
|
10
|
+
const decipher = crypto.createDecipheriv(alg, key, iv)
|
|
11
|
+
return decipher.update(text, encoding) + decipher.final()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const genIv = () => new Buffer.alloc(16, crypto.pseudoRandomBytes(16))
|
|
15
|
+
|
|
16
|
+
export const genKey = (algorithm, password) => {
|
|
17
|
+
const [size, algo] = algorithm.includes("256") ? [32, 'sha256'] :
|
|
18
|
+
// algorithm.includes("192") ? [32, 'sha192'] : // this is not support
|
|
19
|
+
algorithm.includes("128") ? [32, 'md5'] : []
|
|
20
|
+
const hash = crypto.createHash(algo)
|
|
21
|
+
hash.update(password)
|
|
22
|
+
return new Buffer.alloc(size, hash.digest('hex'),'hex')
|
|
23
|
+
}
|
package/web/UI.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
bulma
|
|
2
|
+
bootstrap
|
|
3
|
+
material UI - muicss
|
|
4
|
+
|
|
5
|
+
DO NOT USE
|
|
6
|
+
- material-components-web, semantic ui
|
|
7
|
+
|
|
8
|
+
input
|
|
9
|
+
- text (with regex)
|
|
10
|
+
- number
|
|
11
|
+
- date
|
|
12
|
+
- time
|
|
13
|
+
- datetime (local)
|
|
14
|
+
- file
|
|
15
|
+
- textarea
|
|
16
|
+
- replace <input> with <textarea>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Extended input
|
|
21
|
+
|
|
22
|
+
- autocomplete / select (can use for selects also..., if multiple..., show tags list, delete from tags list?)
|
|
23
|
+
- single
|
|
24
|
+
- multiple
|
|
25
|
+
- allow new (always false if select)
|
|
26
|
+
bwc-autocomplete (if multiple, use tags? remove tags when not needed) - tag
|
|
27
|
+
bs-autocomplete - badge
|
|
28
|
+
mui-autocomplete - chips
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
- link
|
|
33
|
+
- a[href]
|
|
34
|
+
- button
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Input
|
|
38
|
+
|
|
39
|
+
label: <label> innerText, additional classes
|
|
40
|
+
input: <input> attrs: type, pattern, placeholder, additional classes
|
|
41
|
+
helper: <p>
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
tag:
|
|
46
|
+
attrs: []
|
|
47
|
+
// event:
|
|
48
|
+
children: [
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formEl (node) {
|
|
53
|
+
const { tag, className, attrs, children } = node
|
|
54
|
+
const el = document.createElement(tag)
|
|
55
|
+
if (className) {
|
|
56
|
+
el.className = className
|
|
57
|
+
}
|
|
58
|
+
if (attrs) {
|
|
59
|
+
for (let key in attrs) {
|
|
60
|
+
el.setAttirbutes(key, attrs[key])
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (children) {
|
|
64
|
+
children.forEach(child => {
|
|
65
|
+
childEl = formEl(child)
|
|
66
|
+
el.appendChild(childEl)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
return el
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Autocomplete component using input and datalist.
|
|
3
|
+
Only able to handle single selection due to nature of datalist not able to have click event
|
|
4
|
+
|
|
5
|
+
attributes:
|
|
6
|
+
- value (via v-model)
|
|
7
|
+
- required
|
|
8
|
+
- disabled
|
|
9
|
+
- listid (needed if using more than 2 components on the same page)
|
|
10
|
+
- input-class (style the input)
|
|
11
|
+
|
|
12
|
+
properties:
|
|
13
|
+
- items Array or String or Object
|
|
14
|
+
|
|
15
|
+
methods:
|
|
16
|
+
- setList(items) // should be private, called when items property changes
|
|
17
|
+
|
|
18
|
+
events emitted:
|
|
19
|
+
- @input (via v-model) - e.target.value
|
|
20
|
+
- @search - e.detail String
|
|
21
|
+
- @selected - e.detail String or Object or null
|
|
22
|
+
|
|
23
|
+
if selected data is null (no match found, else match found)
|
|
24
|
+
|
|
25
|
+
Usage with (VueJS):
|
|
26
|
+
|
|
27
|
+
<bwc-autocomplete required :items="ac.items" v-model="ac.value" @search="(e) => autoComplete(e)" @selected=></bwc-autocomplete>
|
|
28
|
+
|
|
29
|
+
const ac = reactive({
|
|
30
|
+
value: 'a',
|
|
31
|
+
items: ['aa9','aa5']
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const autoComplete = (e) => {
|
|
35
|
+
const list = ['aa1', 'aa15', 'aa16', 'aa17', 'aa18', 'aa19', 'aa20', 'aa21', 'aa22', 'aa23']
|
|
36
|
+
const result = []
|
|
37
|
+
for (let i = 0; i < list.length; i++) {
|
|
38
|
+
if (list[i].includes(e.detail)) result.push(list[i])
|
|
39
|
+
}
|
|
40
|
+
ac.items = result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
*/
|
|
44
|
+
const template = document.createElement('template')
|
|
45
|
+
template.innerHTML = /*html*/`
|
|
46
|
+
<input type="text" list="json-datalist" placeholder="search..." autocomplete="off">
|
|
47
|
+
<datalist id="json-datalist"></datalist>
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
class AutoComplete extends HTMLElement {
|
|
51
|
+
// local properties
|
|
52
|
+
#items = [] // private
|
|
53
|
+
selectedItem = null // public
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
super()
|
|
57
|
+
this.inputFn = this.inputFn.bind(this)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
connectedCallback() {
|
|
61
|
+
this.appendChild(template.content.cloneNode(true))
|
|
62
|
+
|
|
63
|
+
// console.log('listid', this.listid)
|
|
64
|
+
this.querySelector('datalist').id = this.listid
|
|
65
|
+
this.querySelector('input').setAttribute('list', this.listid)
|
|
66
|
+
|
|
67
|
+
const el = this.querySelector('input')
|
|
68
|
+
el.addEventListener('input', this.inputFn)
|
|
69
|
+
|
|
70
|
+
el.onblur = (e) => {
|
|
71
|
+
const found = this.items.find(item => {
|
|
72
|
+
return typeof item === 'string' ? item === this.value : item.key === this.value || item.text === this.value
|
|
73
|
+
})
|
|
74
|
+
if (!found) { // not found
|
|
75
|
+
if (this.selectedItem) {
|
|
76
|
+
// console.log('not found but is selected')
|
|
77
|
+
this.selectedItem = null
|
|
78
|
+
this.dispatchEvent(new CustomEvent('selected', { detail: this.selectedItem }))
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (!this.selectedItem) {
|
|
82
|
+
// console.log('found but not selected')
|
|
83
|
+
this.selectedItem = found
|
|
84
|
+
this.dispatchEvent(new CustomEvent('selected', { detail: this.selectedItem }))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// console.log('setup stuff', this.required, this.disabled, this.inputClass)
|
|
89
|
+
el.value = this.value
|
|
90
|
+
el.className = this.inputClass || 'input' // default to bulma?
|
|
91
|
+
|
|
92
|
+
if (this.required) el.setAttribute('required', '')
|
|
93
|
+
else el.removeAttribute('required')
|
|
94
|
+
if (this.disabled) el.setAttribute('disabled', '')
|
|
95
|
+
else el.removeAttribute('disabled')
|
|
96
|
+
|
|
97
|
+
this.setList(this.items)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
disconnectedCallback() {
|
|
101
|
+
const el = this.querySelector('input')
|
|
102
|
+
el.removeEventListener('input', this.inputFn)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
106
|
+
const el = this.querySelector('input')
|
|
107
|
+
switch (name) {
|
|
108
|
+
case 'value': {
|
|
109
|
+
if (el) el.value = newVal
|
|
110
|
+
this.dispatchEvent(new CustomEvent('input', { detail: newVal }))
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
case 'required': {
|
|
114
|
+
if (el) el.setAttribute('required', '')
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
case 'disabled': {
|
|
118
|
+
if (el) el.setAttribute('disabled', '')
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
case 'input-class': {
|
|
122
|
+
if (el) el.className = newVal
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static get observedAttributes() {
|
|
131
|
+
return ['value', 'required', 'listid', 'disabled', 'input-class']
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get value() { return this.getAttribute('value') }
|
|
135
|
+
set value(val) { this.setAttribute('value', val) }
|
|
136
|
+
|
|
137
|
+
get required() { return this.hasAttribute('required') }
|
|
138
|
+
set required(val) {
|
|
139
|
+
if (val) {
|
|
140
|
+
this.setAttribute('required', '')
|
|
141
|
+
} else {
|
|
142
|
+
this.removeAttribute('required')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get listid() { return this.getAttribute('listid') }
|
|
147
|
+
set listid(val) { this.setAttribute('listid', val) }
|
|
148
|
+
|
|
149
|
+
get disabled() { return this.hasAttribute('disabled') }
|
|
150
|
+
set disabled(val) {
|
|
151
|
+
if (val) {
|
|
152
|
+
this.setAttribute('disabled', '')
|
|
153
|
+
} else {
|
|
154
|
+
this.removeAttribute('disabled')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get inputClass() { return this.getAttribute('input-class') }
|
|
159
|
+
set inputClass(val) { this.setAttribute('input-class', val) }
|
|
160
|
+
|
|
161
|
+
// properties
|
|
162
|
+
get items() {
|
|
163
|
+
return this.#items
|
|
164
|
+
}
|
|
165
|
+
set items(val) {
|
|
166
|
+
// console.log('set items', val.length)
|
|
167
|
+
this.#items = val
|
|
168
|
+
this.setList(val)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
inputFn(e) { // whether clicked or typed
|
|
172
|
+
// console.log('inputFn', e.target.value, this.items.length)
|
|
173
|
+
const el = this.querySelector('input')
|
|
174
|
+
const prevItem = this.selectedItem
|
|
175
|
+
this.value = el.value
|
|
176
|
+
|
|
177
|
+
const found = this.items.find(item => {
|
|
178
|
+
return typeof item === 'string' ? item === this.value : item.key === this.value || item.text === this.value
|
|
179
|
+
})
|
|
180
|
+
if (!found) { // not found
|
|
181
|
+
// console.log('emit search')
|
|
182
|
+
this.selectedItem = null
|
|
183
|
+
this.dispatchEvent(new CustomEvent('search', { detail: this.value }))
|
|
184
|
+
} else {
|
|
185
|
+
this.selectedItem = found
|
|
186
|
+
}
|
|
187
|
+
// console.log('emit selected?', prevItem !== this.selectedItem, this.selectedItem)
|
|
188
|
+
if (prevItem !== this.selectedItem) {
|
|
189
|
+
this.dispatchEvent(new CustomEvent('selected', { detail: this.selectedItem }))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setList(_items) {
|
|
194
|
+
// console.log('items', _items, this.value)
|
|
195
|
+
const dd = this.querySelector('datalist')
|
|
196
|
+
if (!dd) return
|
|
197
|
+
while(dd.firstChild) {
|
|
198
|
+
dd.removeChild(dd.lastChild)
|
|
199
|
+
}
|
|
200
|
+
if (typeof _items !== 'object') return
|
|
201
|
+
|
|
202
|
+
_items.forEach((item) => {
|
|
203
|
+
const li = document.createElement('option')
|
|
204
|
+
li.innerHTML = typeof item === 'string' ? item : item.key
|
|
205
|
+
li.value = typeof item === 'string' ? item : item.text
|
|
206
|
+
dd.appendChild(li)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
customElements.define('bwc-autocomplete', AutoComplete)
|