@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
package/web/fetch.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// TODO add retry - https://dev.to/ycmjason/javascript-fetch-retry-upon-failure-3p6g
|
|
2
|
+
class Fetch {
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {*} options
|
|
6
|
+
* @param {*} tokens
|
|
7
|
+
*/
|
|
8
|
+
constructor(options = {}, tokens = {}) {
|
|
9
|
+
this.options = {
|
|
10
|
+
baseUrl: '',
|
|
11
|
+
credentials: 'same-origin',
|
|
12
|
+
forceLogoutFn: () => {}, // function to call when forcing a logout
|
|
13
|
+
refreshUrl: '',
|
|
14
|
+
timeoutMs: 0,
|
|
15
|
+
maxRetry: 0
|
|
16
|
+
}
|
|
17
|
+
Object.assign(this.options, options)
|
|
18
|
+
this.tokens = { access: '', refresh: '' }
|
|
19
|
+
Object.assign(this.tokens, tokens)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {string} url
|
|
25
|
+
* @param {string} baseUrl
|
|
26
|
+
* @returns {object} { urlOrigin, urlPath, urlFull, urlSearch }
|
|
27
|
+
* @throws {Error} if URL is invalid
|
|
28
|
+
*/
|
|
29
|
+
static parseUrl (url, baseUrl = '') {
|
|
30
|
+
let urlPath = url
|
|
31
|
+
let urlOrigin = baseUrl
|
|
32
|
+
let urlFull = baseUrl + urlPath
|
|
33
|
+
let urlSearch = ''
|
|
34
|
+
try {
|
|
35
|
+
urlSearch = (url.lastIndexOf('?') !== -1) ? url.split('?').pop() : '' // handle /abc/def?aa=1&bb=2
|
|
36
|
+
if (urlSearch) urlSearch = '?' + urlSearch // prepend ?
|
|
37
|
+
const { origin = '', pathname = '', search = '' } = new URL(url) // http://example.com:3001/abc/ees?aa=1&bb=2
|
|
38
|
+
urlOrigin = origin
|
|
39
|
+
urlPath = pathname
|
|
40
|
+
urlFull = origin + pathname
|
|
41
|
+
urlSearch = search
|
|
42
|
+
} catch (e) {
|
|
43
|
+
}
|
|
44
|
+
return { urlOrigin, urlPath, urlFull, urlSearch }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setOptions (options) { Object.assign(this.options, options) }
|
|
48
|
+
getOptions () { return this.options }
|
|
49
|
+
|
|
50
|
+
setTokens (tokens) { Object.assign(this.tokens, tokens) }
|
|
51
|
+
getTokens () { return this.tokens }
|
|
52
|
+
|
|
53
|
+
async http (method, url, body = null, query = null, headers = null) {
|
|
54
|
+
const { urlOrigin, urlPath, urlFull, urlSearch } = Fetch.parseUrl(url, this.options.baseUrl)
|
|
55
|
+
try {
|
|
56
|
+
const controller = new AbortController()
|
|
57
|
+
const signal = controller.signal
|
|
58
|
+
if (this.options.timeoutMs > 0) setTimeout(() => controller.abort(), this.options.timeoutMs) // err.name === 'AbortError'
|
|
59
|
+
|
|
60
|
+
let qs = (query && typeof query === 'object') // null is also an object
|
|
61
|
+
? '?' +
|
|
62
|
+
Object.keys(query).map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(query[key])).join('&')
|
|
63
|
+
: (query || '')
|
|
64
|
+
qs = qs ? qs + urlSearch.substring(1) // remove the question mark
|
|
65
|
+
: urlSearch
|
|
66
|
+
|
|
67
|
+
if (!headers) {
|
|
68
|
+
headers = {
|
|
69
|
+
Accept: 'application/json'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const options = { method, headers }
|
|
73
|
+
if (this.options.timeoutMs > 0) options.signal = signal
|
|
74
|
+
if (this.options.credentials !== 'include') { // include === HTTPONLY_TOKEN
|
|
75
|
+
if (this.tokens.access) options.headers.Authorization = `Bearer ${this.tokens.access}`
|
|
76
|
+
}
|
|
77
|
+
options.credentials = this.options.credentials
|
|
78
|
+
|
|
79
|
+
if (['POST', 'PATCH', 'PUT'].includes(method)) { // check if HTTP method has req body (DELETE is maybe)
|
|
80
|
+
if (body && body instanceof FormData) {
|
|
81
|
+
options.body = body // options.headers['Content-Type'] = 'multipart/form-data' // NOT NEEDED!!!
|
|
82
|
+
} else if (options.headers['Content-Type'] && options.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
|
|
83
|
+
options.body = new URLSearchParams(body) // body should be JSON
|
|
84
|
+
} else if (options.headers['Content-Type'] && options.headers['Content-Type'] === 'application/octet-stream') {
|
|
85
|
+
options.body = body // handling stream...
|
|
86
|
+
} else {
|
|
87
|
+
options.headers['Content-Type'] = 'application/json' // NEEDED!!!
|
|
88
|
+
options.body = JSON.stringify(body)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rv0 = await fetch(urlFull + qs, options)
|
|
93
|
+
const txt0 = await rv0.text() // handle empty body as xxx.json() cannot
|
|
94
|
+
rv0.data = txt0.length ? JSON.parse(txt0) : {}
|
|
95
|
+
if (rv0.status >= 200 && rv0.status < 400) return rv0
|
|
96
|
+
else if (rv0.status === 401) { // no longer needed urlPath !== '/api/auth/refresh'
|
|
97
|
+
if (rv0.data.message === 'Token Expired Error' && this.options.refreshUrl) {
|
|
98
|
+
try {
|
|
99
|
+
const rv1 = await this.http('POST', urlOrigin + this.options.refreshUrl, { refresh_token: this.tokens.refresh }) // rv1 JSON already processed
|
|
100
|
+
// status code should be < 400 here
|
|
101
|
+
this.tokens.access = rv1.data.access_token
|
|
102
|
+
this.tokens.refresh = rv1.data.refresh_token
|
|
103
|
+
if (options.credentials !== 'include') { // include === HTTPONLY_TOKEN
|
|
104
|
+
if (this.tokens.access) options.headers.Authorization = `Bearer ${this.tokens.access}`
|
|
105
|
+
}
|
|
106
|
+
const rv2 = await fetch(urlFull + qs, options)
|
|
107
|
+
const txt2 = await rv2.text()
|
|
108
|
+
rv2.data = txt2.length ? JSON.parse(txt2) : {}
|
|
109
|
+
return rv2
|
|
110
|
+
} catch (e) {
|
|
111
|
+
throw e
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw rv0 // error
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (e?.data?.message !== 'Token Expired Error' && (e.status === 401 || e.status === 403)) this.options.forceLogoutFn()
|
|
118
|
+
throw e // some other error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async post (url, body = null, query = null, headers = null) { return this.http('POST', url, body, query, headers) }
|
|
123
|
+
async put (url, body = null, query = null, headers = null) { return this.http('PUT', url, body, query, headers) }
|
|
124
|
+
async patch (url, body = null, query = null, headers = null) { return this.http('PATCH', url, body, query, headers) }
|
|
125
|
+
async del (url, query = null, headers = null) { return this.http('DELETE', url, null, query, headers) }
|
|
126
|
+
async get (url, query = null, headers = null) { return this.http('GET', url, null, query, headers) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default Fetch
|
package/web/i18n.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// https://phrase.com/blog/posts/detecting-a-users-locale/
|
|
2
|
+
// https://gist.github.com/ashour/5f169a6dd9b6293691629ee0d06cae6f
|
|
3
|
+
// get languages / locales from browser
|
|
4
|
+
// sample output: ['en-US', 'en-GB']
|
|
5
|
+
|
|
6
|
+
function getBrowserLocales(options = {}) {
|
|
7
|
+
const defaultOptions = {
|
|
8
|
+
languageCodeOnly: false
|
|
9
|
+
}
|
|
10
|
+
const opt = {
|
|
11
|
+
...defaultOptions,
|
|
12
|
+
...options
|
|
13
|
+
}
|
|
14
|
+
const browserLocales = navigator.languages === undefined ? [navigator.language] : navigator.languages
|
|
15
|
+
if (!browserLocales) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
return browserLocales.map((locale) => {
|
|
19
|
+
const trimmedLocale = locale.trim()
|
|
20
|
+
return opt.languageCodeOnly ? trimmedLocale.split(/-|_/)[0] : trimmedLocale
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default getBrowserLocales
|
package/web/idle.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const idleTimer = { // logout user if idle
|
|
2
|
+
timeouts: [],
|
|
3
|
+
|
|
4
|
+
_idleSecondsTimer: null,
|
|
5
|
+
_idleSecondsCounter: 0,
|
|
6
|
+
_elapsedTsMs: 0, // elapsed timestamp in milliseconds
|
|
7
|
+
_reset: null,
|
|
8
|
+
_check: null,
|
|
9
|
+
|
|
10
|
+
reset () { this._idleSecondsCounter = 0 },
|
|
11
|
+
|
|
12
|
+
clearElapsedTimeStart () { this._elapsedTsMs = 0 },
|
|
13
|
+
setElapsedTimeStart () { this._elapsedTsMs = Date.now() },
|
|
14
|
+
getElapsedTimeSeconds () { return parseInt((Date.now() - this._elapsedTsMs) / 1000.0) },
|
|
15
|
+
|
|
16
|
+
getIdleTimeSeconds () { return this._idleSecondsCounter },
|
|
17
|
+
|
|
18
|
+
check() {
|
|
19
|
+
this._idleSecondsCounter++
|
|
20
|
+
for (let timeout of this.timeouts) {
|
|
21
|
+
// console.log('check2', this._idleSecondsCounter, timeout.time)
|
|
22
|
+
if (this._idleSecondsCounter >= timeout.time) {
|
|
23
|
+
if (timeout.stop) this.stop()
|
|
24
|
+
timeout.fn()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
start () {
|
|
30
|
+
if (!this._reset) this._reset = this.reset.bind(this)
|
|
31
|
+
if (!this._check) this._check = this.check.bind(this)
|
|
32
|
+
|
|
33
|
+
this.setElapsedTimeStart()
|
|
34
|
+
document.addEventListener('click', this._reset)
|
|
35
|
+
document.addEventListener('mousemove', this._reset)
|
|
36
|
+
document.addEventListener('keypress', this._reset)
|
|
37
|
+
this._idleSecondsTimer = window.setInterval(this._check, 1000)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
stop () {
|
|
41
|
+
this.clearElapsedTimeStart()
|
|
42
|
+
document.removeEventListener('click', this._reset)
|
|
43
|
+
document.removeEventListener('mousemove', this._reset)
|
|
44
|
+
document.removeEventListener('keypress', this._reset)
|
|
45
|
+
window.clearInterval(this._idleSecondsTimer)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default idleTimer
|
package/web/parse-jwt.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// parses and returns JWT payload
|
|
2
|
+
// exceptions to be handled by caller
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {string} token
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
export default function (token) {
|
|
9
|
+
const base64Url = token.split('.')[1]
|
|
10
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
11
|
+
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
|
|
12
|
+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
13
|
+
}).join(''))
|
|
14
|
+
return JSON.parse(jsonPayload)
|
|
15
|
+
}
|
package/web/pwa.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// // First get a public key from our Express server
|
|
2
|
+
// We use this function to subscribe to our push notifications
|
|
3
|
+
// As soon as you run this code once, it shouldn't run again if the initial subscription went well
|
|
4
|
+
// Except if you clear your storage
|
|
5
|
+
export const webpushSubscribe = async (publicKey) => {
|
|
6
|
+
const registration = await navigator.serviceWorker.ready
|
|
7
|
+
// NOSONAR
|
|
8
|
+
// alternate = navigator.serviceWorker.getRegistration()
|
|
9
|
+
// let subscription = await registration.pushManager.getSubscription()
|
|
10
|
+
// if (subscription) return subscription
|
|
11
|
+
|
|
12
|
+
// this is an annoying part of the process we have to turn our public key into a Uint8Array
|
|
13
|
+
const Uint8ArrayPublicKey = urlBase64ToUint8Array(publicKey)
|
|
14
|
+
|
|
15
|
+
// registering a new subscription to our service worker's Push manager
|
|
16
|
+
let subscription = await registration.pushManager.subscribe({
|
|
17
|
+
userVisibleOnly: true, // chrome only supports user visible for now
|
|
18
|
+
applicationServerKey: Uint8ArrayPublicKey
|
|
19
|
+
})
|
|
20
|
+
subscription = JSON.stringify(subscription)
|
|
21
|
+
|
|
22
|
+
console.log('subscription', subscription)
|
|
23
|
+
return subscription
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Let's create an unsubscribe function as well
|
|
27
|
+
export const webpushUnsubscribe = async () => {
|
|
28
|
+
const registration = await navigator.serviceWorker.ready
|
|
29
|
+
const subscription = await registration.pushManager.getSubscription()
|
|
30
|
+
if (subscription) await subscription.unsubscribe() // This tells our browser that we want to unsubscribe
|
|
31
|
+
}
|
|
32
|
+
// This tells our Express server that we want to unsubscribe
|
|
33
|
+
// await fetch(VITE_API_URL + '/api/webpush/unsub?subId=test', {
|
|
34
|
+
// method: 'POST',
|
|
35
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
// body: JSON.stringify(subscription.toJSON())
|
|
37
|
+
// })
|
|
38
|
+
|
|
39
|
+
// I have found this code (or variations of) from; multiple sources
|
|
40
|
+
// but I could not find the original author
|
|
41
|
+
// here's one such source:
|
|
42
|
+
// https://stackoverflow.com/questions/42362235/web-pushnotification-unauthorizedregistration-or-gone-or-unauthorized-sub
|
|
43
|
+
const urlBase64ToUint8Array = (base64String) => {
|
|
44
|
+
// const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
|
45
|
+
// const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/')
|
|
46
|
+
// https://stackoverflow.com/questions/52379865/eslint-replace-cant-read-method
|
|
47
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
|
48
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
|
49
|
+
|
|
50
|
+
const rawData = window.atob(base64)
|
|
51
|
+
const outputArray = new Uint8Array(rawData.length)
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
54
|
+
outputArray[i] = rawData.charCodeAt(i)
|
|
55
|
+
}
|
|
56
|
+
return outputArray
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleSwMessage = async (e) => {
|
|
60
|
+
console.log('handleSwMessage', e)
|
|
61
|
+
//NOSONAR if (e && e.data && e.data.msg === 'pushsubscriptionchange') { }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const addSwMessageEvent = (handler = handleSwMessage) => {
|
|
65
|
+
navigator.serviceWorker.addEventListener('message', handleSwMessage)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const removeSwMessageEvent = (handler = handleSwMessage) => {
|
|
69
|
+
navigator.serviceWorker.removeEventListener('message', handleSwMessage)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// https://felixgerschau.com/how-to-communicate-with-service-workers/
|
|
73
|
+
// app to sw
|
|
74
|
+
// // app.js - Somewhere in your web app
|
|
75
|
+
// navigator.serviceWorker.controller.postMessage({
|
|
76
|
+
// type: 'MESSAGE_IDENTIFIER',
|
|
77
|
+
// })
|
|
78
|
+
// service-worker.js
|
|
79
|
+
// On the Service Worker side we have to listen to the message event
|
|
80
|
+
// self.addEventListener('message', (e) => {
|
|
81
|
+
// if (e.data && e.data.type === 'MESSAGE_IDENTIFIER') {
|
|
82
|
+
// // do something
|
|
83
|
+
// }
|
|
84
|
+
// })
|
package/web/sign-pad.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// FRONTEND ONLY
|
|
2
|
+
const template = document.createElement('template')
|
|
3
|
+
template.innerHTML = `
|
|
4
|
+
<style>
|
|
5
|
+
canvas {
|
|
6
|
+
background-color: var(--vcxwc-sign-pad-background-color, #ddd);
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
<canvas id="canvas"></canvas>
|
|
10
|
+
`
|
|
11
|
+
class SignPad extends HTMLElement {
|
|
12
|
+
constructor() {
|
|
13
|
+
super()
|
|
14
|
+
const shadowRoot = this.attachShadow({ mode: 'open' })
|
|
15
|
+
shadowRoot.appendChild(template.content.cloneNode(true))
|
|
16
|
+
console.log('SignPad()')
|
|
17
|
+
this.mouse = {
|
|
18
|
+
current: { x: 0, y: 0 },
|
|
19
|
+
previous: { x: 0, y: 0 },
|
|
20
|
+
down: false,
|
|
21
|
+
move: false
|
|
22
|
+
}
|
|
23
|
+
this.handleMouseDown = this.handleMouseDown.bind(this) // bind callback function
|
|
24
|
+
this.handleMouseUp = this.handleMouseUp.bind(this) // bind callback function
|
|
25
|
+
this.handleMouseMove = this.handleMouseMove.bind(this) // bind callback function
|
|
26
|
+
this.currentMouse = this.currentMouse.bind(this) // bind callback function
|
|
27
|
+
this.draw = this.draw.bind(this) // bind callback function
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static get observedAttributes() {
|
|
31
|
+
// default width = 300 (in px)
|
|
32
|
+
// default height = 150 (in px)
|
|
33
|
+
// default value should be ''
|
|
34
|
+
return ['value', 'width', 'height']
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// getter and setter for property - show
|
|
38
|
+
get value() {
|
|
39
|
+
return this.getAttribute('value')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
set value(val) {
|
|
43
|
+
// console.log('setting value', val)
|
|
44
|
+
this.setAttribute('value', val || '')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
48
|
+
const el = this.shadowRoot.querySelector('#canvas')
|
|
49
|
+
switch (name) {
|
|
50
|
+
case 'width': // set canvas width
|
|
51
|
+
el.width = newVal
|
|
52
|
+
break
|
|
53
|
+
case 'height': // set canvas height
|
|
54
|
+
el.height = newVal
|
|
55
|
+
break
|
|
56
|
+
// value no need to handle
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
connectedCallback() {
|
|
61
|
+
// const styleSheetList = this.shadowRoot.styleSheets // do this here, not in constructor
|
|
62
|
+
// const bg_rule = styleSheetList[0].cssRules[0]
|
|
63
|
+
// const yyy = context_rule.style.getPropertyValue('--vcxwc-sign-pad-background-color')
|
|
64
|
+
// console.log('bg', bg_rule.style.backgroundColor, yyy)
|
|
65
|
+
|
|
66
|
+
// console.log('connect sign')
|
|
67
|
+
this.mouse = {
|
|
68
|
+
current: { x: 0, y: 0 },
|
|
69
|
+
previous: { x: 0, y: 0 },
|
|
70
|
+
down: false,
|
|
71
|
+
move: false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// console.log('has value?', this.hasAttribute('value'), this.getAttribute('value'))
|
|
75
|
+
if (!this.hasAttribute('value')) this.setAttribute('value', '')
|
|
76
|
+
|
|
77
|
+
const el = this.shadowRoot.querySelector('#canvas')
|
|
78
|
+
// console.log('canvas w h', el.width, el.height)
|
|
79
|
+
const ctx = el.getContext('2d')
|
|
80
|
+
ctx.translate(0.5, 0.5)
|
|
81
|
+
ctx.imageSmoothingEnabled = false
|
|
82
|
+
|
|
83
|
+
const ctx2d = JSON.parse(this.getAttribute('context2d'))
|
|
84
|
+
// console.log('ctx2d', ctx2d)
|
|
85
|
+
for (const k in ctx2d) {
|
|
86
|
+
// strokeStyle, lineWidth
|
|
87
|
+
ctx[k] = ctx2d[k]
|
|
88
|
+
}
|
|
89
|
+
el.addEventListener('mousedown', this.handleMouseDown)
|
|
90
|
+
el.addEventListener('mouseup', this.handleMouseUp)
|
|
91
|
+
el.addEventListener('mousemove', this.handleMouseMove)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
adoptedCallback() {}
|
|
95
|
+
|
|
96
|
+
disconnectedCallback() {
|
|
97
|
+
// console.log('disconnect sign')
|
|
98
|
+
const el = this.shadowRoot.querySelector('#canvas')
|
|
99
|
+
el.removeEventListener('mousedown', this.handleMouseDown)
|
|
100
|
+
el.removeEventListener('mouseup', this.handleMouseUp)
|
|
101
|
+
el.removeEventListener('mousemove', this.handleMouseMove)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handleMouseDown(event) {
|
|
105
|
+
this.mouse.down = true
|
|
106
|
+
this.mouse.move = false
|
|
107
|
+
this.mouse.current = {
|
|
108
|
+
x: event.offsetX,
|
|
109
|
+
y: event.offsetY
|
|
110
|
+
}
|
|
111
|
+
const el = this.shadowRoot.querySelector('#canvas')
|
|
112
|
+
const ctx = el.getContext('2d')
|
|
113
|
+
ctx.clearRect(0, 0, el.width, el.height) // clear
|
|
114
|
+
ctx.beginPath() // clear lines
|
|
115
|
+
this.value = ''
|
|
116
|
+
|
|
117
|
+
const { x, y } = this.currentMouse()
|
|
118
|
+
ctx.moveTo(x, y)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
handleMouseUp() {
|
|
122
|
+
const el = this.shadowRoot.querySelector('#canvas')
|
|
123
|
+
if (this.mouse.move) this.value = el.toDataURL('image/png')
|
|
124
|
+
else this.value = ''
|
|
125
|
+
|
|
126
|
+
const event = new CustomEvent('input', { detail: this.value })
|
|
127
|
+
this.dispatchEvent(event)
|
|
128
|
+
|
|
129
|
+
this.mouse.down = false
|
|
130
|
+
this.mouse.move = false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleMouseMove(event) {
|
|
134
|
+
this.mouse.move = true
|
|
135
|
+
this.mouse.current = {
|
|
136
|
+
x: event.offsetX,
|
|
137
|
+
y: event.offsetY
|
|
138
|
+
}
|
|
139
|
+
this.draw(event)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
draw(event) {
|
|
143
|
+
// requestAnimationFrame(this.draw)
|
|
144
|
+
if (this.mouse.down) {
|
|
145
|
+
const el = this.shadowRoot.querySelector('#canvas')
|
|
146
|
+
// console.log('draw', event, el.width, el.height, c)
|
|
147
|
+
// const c = document.getElementById('canvas')
|
|
148
|
+
const ctx = el.getContext('2d')
|
|
149
|
+
ctx.clearRect(0, 0, el.width, el.height)
|
|
150
|
+
const { x, y } = this.currentMouse()
|
|
151
|
+
ctx.lineTo(x, y)
|
|
152
|
+
ctx.stroke()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
currentMouse() {
|
|
157
|
+
return {
|
|
158
|
+
x: this.mouse.current.x,
|
|
159
|
+
y: this.mouse.current.y
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
customElements.define('vcxwc-sign-pad', SignPad)
|
package/web/t4t-fe.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import Fetch from './fetch.js'
|
|
2
|
+
let tableName = ''
|
|
3
|
+
let parentFilter = null
|
|
4
|
+
let config = null
|
|
5
|
+
let urlPrefix = '/api'
|
|
6
|
+
let http = new Fetch()
|
|
7
|
+
// Do Not Catch Errors - let it be caught by caller
|
|
8
|
+
|
|
9
|
+
function setTableName(name) { // set table name
|
|
10
|
+
tableName = name
|
|
11
|
+
parentFilter = null // TBD: find a more sustainable way using prototype or let caller handle this part
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// this might change
|
|
15
|
+
const setFetch = (_fetch) => http = _fetch // set the fetch function
|
|
16
|
+
const setParentFilter = (_filter) => parentFilter = _filter
|
|
17
|
+
const setUrlPrefix = (_urlPrefix) => urlPrefix = _urlPrefix
|
|
18
|
+
|
|
19
|
+
async function getConfig() {
|
|
20
|
+
const { data } = await http.get(urlPrefix + '/t4t/config/' + tableName)
|
|
21
|
+
if (data) config = data
|
|
22
|
+
return data
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function find(filters, sorter, page, limit) {
|
|
26
|
+
let rv = {
|
|
27
|
+
results: [],
|
|
28
|
+
total: 0
|
|
29
|
+
}
|
|
30
|
+
if (parentFilter) {
|
|
31
|
+
filters.push({col: parentFilter.col, op: "=", val: parentFilter.id, andOr: "and"})
|
|
32
|
+
}
|
|
33
|
+
filters = filters ? JSON.stringify(filters) : '' // [{col, op, val, andOr}, ...]
|
|
34
|
+
sorter = sorter ? JSON.stringify(sorter) : '' // [{ column: '<col_name>', order: 'asc|desc' }, ...]
|
|
35
|
+
const { data } = await http.get(urlPrefix + '/t4t/find/' + tableName, {
|
|
36
|
+
page, limit, filters, sorter
|
|
37
|
+
})
|
|
38
|
+
rv.results = data.results
|
|
39
|
+
rv.total = data.total
|
|
40
|
+
return rv
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function download(filters, sorter) {
|
|
44
|
+
const { data } = await http.get(urlPrefix + '/t4t/find/' + tableName, {
|
|
45
|
+
page: 0,
|
|
46
|
+
limit: 0,
|
|
47
|
+
filters: filters ? JSON.stringify(filters) : '',
|
|
48
|
+
sorter: sorter ? JSON.stringify(sorter) : '',
|
|
49
|
+
csv: 1 // it is a csv
|
|
50
|
+
})
|
|
51
|
+
return data
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function findOne(__key) {
|
|
55
|
+
let rv = {}
|
|
56
|
+
const { data } = await http.get(urlPrefix + '/t4t/find-one/' + tableName, { __key }) // if multiKey, then seperate values by |, column is implied by order
|
|
57
|
+
if (data) {
|
|
58
|
+
rv = data
|
|
59
|
+
rv.__key = __key
|
|
60
|
+
}
|
|
61
|
+
return rv
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// process data for use with
|
|
65
|
+
// JSON only, multi-part form, JSON & filelist (signed URL upload)
|
|
66
|
+
function processData(record, { signedUrl = false } = { }) {
|
|
67
|
+
const rv = {
|
|
68
|
+
json: { }
|
|
69
|
+
}
|
|
70
|
+
for (const [k, v] of Object.entries(record)) {
|
|
71
|
+
if (v instanceof FileList) {
|
|
72
|
+
const fileNameArray = []
|
|
73
|
+
for (const file of v) {
|
|
74
|
+
if (signedUrl) {
|
|
75
|
+
if (!rv.files) rv.files = []
|
|
76
|
+
rv.files.push(file)
|
|
77
|
+
} else {
|
|
78
|
+
if (!rv.form) rv.form = new FormData()
|
|
79
|
+
rv.form.append('file-data', file) // add
|
|
80
|
+
}
|
|
81
|
+
fileNameArray.push(file.name)
|
|
82
|
+
}
|
|
83
|
+
rv.json[k] = fileNameArray.join(',') // array
|
|
84
|
+
} else {
|
|
85
|
+
rv.json[k] = v
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (rv.form) rv.form.append('json-data', JSON.stringify(rv.json)) // set the JSON
|
|
89
|
+
return rv
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function create(record) {
|
|
93
|
+
// const { data } = await http.patch(`/authors/${id}`, formData,
|
|
94
|
+
// { onUploadProgress: progressEvent => console.log(Math.round(progressEvent.loaded / progressEvent.total * 100) + '%') } // axios only
|
|
95
|
+
// )
|
|
96
|
+
return await http.post(urlPrefix + `/t4t/create/${tableName}`, record)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function update(__key, record, headers = null) {
|
|
100
|
+
return await http.patch(urlPrefix + `/t4t/update/${tableName}`, record, { __key }, headers)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle file removals seperately
|
|
104
|
+
async function remove(items) {
|
|
105
|
+
// console.log(items)
|
|
106
|
+
let ids = []
|
|
107
|
+
// const { pk } = config
|
|
108
|
+
// ids = pk ? items.map((item) => item[pk]) : items.map((item) => item.__key)
|
|
109
|
+
// console.log(ids)
|
|
110
|
+
ids = items
|
|
111
|
+
return await http.post(urlPrefix + '/t4t/remove/' + tableName, { ids })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// uploads a single csv for batch processing
|
|
115
|
+
async function upload(file) { // the file object
|
|
116
|
+
// TODO add exception handling
|
|
117
|
+
if (file === null) return false
|
|
118
|
+
const formData = new FormData()
|
|
119
|
+
formData.append('csv-file', file) // call it file
|
|
120
|
+
// console.log('zzz', formData instanceof FormData)
|
|
121
|
+
// for(const pair of formData.entries()) console.log(pair[0], pair[1])
|
|
122
|
+
return await http.post(urlPrefix + '/t4t/upload/' + tableName, formData)
|
|
123
|
+
// formData.append('textdata', JSON.stringify({ name: 'name', age: 25 }))
|
|
124
|
+
// const res = await fetch('/api/upload', { method: 'POST', body: formData })
|
|
125
|
+
// const { id, name, avatar } = record
|
|
126
|
+
// const json = JSON.stringify({ name })
|
|
127
|
+
// // const blob = new Blob([json], { type: 'application/json' })
|
|
128
|
+
// // console.log('json', blob)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// const autoComplete = debounce(async (e, col, _showForm) => {
|
|
133
|
+
// recordObj[_showForm][col] = e.target.value
|
|
134
|
+
|
|
135
|
+
// wrap in debounce
|
|
136
|
+
// parentColVal in use-cases where parent table column changes
|
|
137
|
+
async function autocomplete (search, col, record, parentColVal = '') {
|
|
138
|
+
let res = []
|
|
139
|
+
try {
|
|
140
|
+
const { tableName, limit, key, text, parentTableColName, parentCol } = config.cols[col].options
|
|
141
|
+
const query = { tableName, limit, key, text, search }
|
|
142
|
+
let parentTableColVal = ''
|
|
143
|
+
if (parentTableColName) {
|
|
144
|
+
parentTableColVal = parentColVal || record[parentCol] || ''
|
|
145
|
+
}
|
|
146
|
+
const { data } = await http.post(urlPrefix + '/t4t/autocomplete/' + tableName, {
|
|
147
|
+
key,
|
|
148
|
+
text,
|
|
149
|
+
search,
|
|
150
|
+
limit,
|
|
151
|
+
parentTableColName,
|
|
152
|
+
parentTableColVal
|
|
153
|
+
})
|
|
154
|
+
res = data
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.log('autocomplete', err.message)
|
|
157
|
+
}
|
|
158
|
+
return res
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
setFetch, setTableName, setParentFilter, setUrlPrefix, getConfig,
|
|
163
|
+
find, findOne, create, update, remove, upload, download, autocomplete, processData,
|
|
164
|
+
}
|