odac 1.1.0 → 1.3.0
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/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +42 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/release.yml +68 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +1 -1
- package/CHANGELOG.md +99 -0
- package/README.md +16 -0
- package/bin/odac.js +196 -38
- package/client/odac.js +238 -171
- package/docs/backend/01-overview/03-development-server.md +38 -45
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
- package/docs/backend/03-config/00-configuration-overview.md +6 -6
- package/docs/backend/03-config/01-database-connection.md +2 -2
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/04-routing/09-websocket.md +14 -1
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/03-controller-classes.md +40 -20
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +2 -2
- package/docs/backend/10-authentication/03-register.md +1 -1
- package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
- package/docs/backend/10-authentication/05-session-management.md +15 -1
- package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
- package/docs/backend/10-authentication/07-magic-links.md +1 -1
- package/docs/index.json +5 -1
- package/jest.config.js +1 -1
- package/package.json +18 -14
- package/src/Auth.js +58 -23
- package/src/Config.js +7 -7
- package/src/Database.js +1 -1
- package/src/Env.js +3 -1
- package/src/Ipc.js +7 -0
- package/src/Lang.js +9 -2
- package/src/Mail.js +19 -9
- package/src/Odac.js +56 -44
- package/src/Request.js +6 -2
- package/src/Route/Cron.js +58 -17
- package/src/Route/Internal.js +1 -1
- package/src/Route.js +375 -125
- package/src/Server.js +40 -3
- package/src/Storage.js +4 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +24 -15
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +17 -11
- package/src/View.js +68 -12
- package/src/WebSocket.js +45 -12
- package/template/package.json +3 -1
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +88 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +105 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/WebSocket.test.js +238 -0
- package/test/scripts/check-coverage.js +4 -4
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Odac.test.js +0 -234
- package/test/core/Process.test.js +0 -156
- package/test/framework/WebSocket.test.js +0 -100
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- package/test/server/__mocks__/tls.js +0 -229
- /package/template/{config.json → odac.json} +0 -0
package/client/odac.js
CHANGED
|
@@ -23,9 +23,7 @@ class OdacWebSocket {
|
|
|
23
23
|
connect() {
|
|
24
24
|
if (this.#isClosed) return
|
|
25
25
|
|
|
26
|
-
this.#socket = this.#protocols.length > 0
|
|
27
|
-
? new WebSocket(this.#url, this.#protocols)
|
|
28
|
-
: new WebSocket(this.#url)
|
|
26
|
+
this.#socket = this.#protocols.length > 0 ? new WebSocket(this.#url, this.#protocols) : new WebSocket(this.#url)
|
|
29
27
|
|
|
30
28
|
this.#socket.onopen = () => {
|
|
31
29
|
this.#reconnectAttempts = 0
|
|
@@ -43,11 +41,7 @@ class OdacWebSocket {
|
|
|
43
41
|
|
|
44
42
|
this.#socket.onclose = e => {
|
|
45
43
|
this.emit('close', e)
|
|
46
|
-
if (
|
|
47
|
-
this.#options.autoReconnect &&
|
|
48
|
-
!this.#isClosed &&
|
|
49
|
-
this.#reconnectAttempts < this.#options.maxReconnectAttempts
|
|
50
|
-
) {
|
|
44
|
+
if (this.#options.autoReconnect && !this.#isClosed && this.#reconnectAttempts < this.#options.maxReconnectAttempts) {
|
|
51
45
|
this.#reconnectAttempts++
|
|
52
46
|
this.#reconnectTimer = setTimeout(() => this.connect(), this.#options.reconnectDelay)
|
|
53
47
|
}
|
|
@@ -112,13 +106,13 @@ if (typeof window !== 'undefined') {
|
|
|
112
106
|
#formSubmitHandlers = new Map()
|
|
113
107
|
#loader = {elements: {}, callback: null}
|
|
114
108
|
#isNavigating = false
|
|
115
|
-
|
|
109
|
+
|
|
116
110
|
constructor() {
|
|
117
111
|
// In constructor we can't call this.data() easily if it uses 'this' for caching properly before init
|
|
118
112
|
// But based on original code logic:
|
|
119
113
|
this.#data = this.data()
|
|
120
114
|
}
|
|
121
|
-
|
|
115
|
+
|
|
122
116
|
#ajax(options) {
|
|
123
117
|
const {
|
|
124
118
|
url,
|
|
@@ -132,19 +126,19 @@ if (typeof window !== 'undefined') {
|
|
|
132
126
|
contentType = 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
133
127
|
xhr: xhrFactory
|
|
134
128
|
} = options
|
|
135
|
-
|
|
129
|
+
|
|
136
130
|
const xhr = xhrFactory ? xhrFactory() : new XMLHttpRequest()
|
|
137
|
-
|
|
131
|
+
|
|
138
132
|
xhr.open(type, url, true)
|
|
139
|
-
|
|
133
|
+
|
|
140
134
|
Object.keys(headers).forEach(key => {
|
|
141
135
|
xhr.setRequestHeader(key, headers[key])
|
|
142
136
|
})
|
|
143
|
-
|
|
137
|
+
|
|
144
138
|
if (contentType && !(data instanceof FormData)) {
|
|
145
139
|
xhr.setRequestHeader('Content-Type', contentType)
|
|
146
140
|
}
|
|
147
|
-
|
|
141
|
+
|
|
148
142
|
xhr.onload = () => {
|
|
149
143
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
150
144
|
let responseData = xhr.responseText
|
|
@@ -157,42 +151,42 @@ if (typeof window !== 'undefined') {
|
|
|
157
151
|
return
|
|
158
152
|
}
|
|
159
153
|
}
|
|
160
|
-
|
|
154
|
+
|
|
161
155
|
document.dispatchEvent(
|
|
162
156
|
new CustomEvent('odac:ajaxSuccess', {
|
|
163
157
|
detail: {response: responseData, status: xhr.statusText, xhr, requestUrl: url}
|
|
164
158
|
})
|
|
165
159
|
)
|
|
166
|
-
|
|
160
|
+
|
|
167
161
|
success(responseData, xhr.statusText, xhr)
|
|
168
162
|
} else {
|
|
169
163
|
error(xhr, xhr.statusText)
|
|
170
164
|
}
|
|
171
165
|
}
|
|
172
|
-
|
|
166
|
+
|
|
173
167
|
xhr.onerror = () => error(xhr, 'error')
|
|
174
168
|
xhr.onloadend = () => complete()
|
|
175
169
|
xhr.send(data)
|
|
176
170
|
}
|
|
177
|
-
|
|
171
|
+
|
|
178
172
|
#fade(element, type, duration = 400, callback) {
|
|
179
173
|
const isIn = type === 'in'
|
|
180
174
|
const startOpacity = isIn ? 0 : 1
|
|
181
175
|
const endOpacity = isIn ? 1 : 0
|
|
182
|
-
|
|
176
|
+
|
|
183
177
|
element.style.opacity = startOpacity
|
|
184
178
|
if (isIn) {
|
|
185
179
|
element.style.display = 'block'
|
|
186
180
|
}
|
|
187
|
-
|
|
181
|
+
|
|
188
182
|
let startTime = null
|
|
189
|
-
|
|
183
|
+
|
|
190
184
|
const animate = currentTime => {
|
|
191
185
|
if (!startTime) startTime = currentTime
|
|
192
186
|
const progress = currentTime - startTime
|
|
193
187
|
const opacity = startOpacity + (endOpacity - startOpacity) * Math.min(progress / duration, 1)
|
|
194
188
|
element.style.opacity = opacity
|
|
195
|
-
|
|
189
|
+
|
|
196
190
|
if (progress < duration) {
|
|
197
191
|
requestAnimationFrame(animate)
|
|
198
192
|
} else {
|
|
@@ -204,15 +198,15 @@ if (typeof window !== 'undefined') {
|
|
|
204
198
|
}
|
|
205
199
|
requestAnimationFrame(animate)
|
|
206
200
|
}
|
|
207
|
-
|
|
201
|
+
|
|
208
202
|
#fadeIn(element, duration, callback) {
|
|
209
203
|
this.#fade(element, 'in', duration, callback)
|
|
210
204
|
}
|
|
211
|
-
|
|
205
|
+
|
|
212
206
|
#fadeOut(element, duration, callback) {
|
|
213
207
|
this.#fade(element, 'out', duration, callback)
|
|
214
208
|
}
|
|
215
|
-
|
|
209
|
+
|
|
216
210
|
#on(element, event, selector, handler) {
|
|
217
211
|
element.addEventListener(event, e => {
|
|
218
212
|
let target = e.target.closest(selector)
|
|
@@ -221,7 +215,7 @@ if (typeof window !== 'undefined') {
|
|
|
221
215
|
}
|
|
222
216
|
})
|
|
223
217
|
}
|
|
224
|
-
|
|
218
|
+
|
|
225
219
|
#serialize(form) {
|
|
226
220
|
const params = []
|
|
227
221
|
form.querySelectorAll('input, select, textarea').forEach(el => {
|
|
@@ -243,22 +237,21 @@ if (typeof window !== 'undefined') {
|
|
|
243
237
|
})
|
|
244
238
|
return params.join('&')
|
|
245
239
|
}
|
|
246
|
-
|
|
240
|
+
|
|
247
241
|
action(obj) {
|
|
248
242
|
if (obj.function) for (let func in obj.function) this.fn[func] = obj.function[func]
|
|
249
|
-
|
|
243
|
+
|
|
250
244
|
if (obj.navigate !== undefined && obj.navigate !== false) {
|
|
251
245
|
let selector, elements, callback
|
|
252
|
-
|
|
246
|
+
|
|
253
247
|
if (typeof obj.navigate === 'string') {
|
|
254
248
|
selector = 'a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)'
|
|
255
249
|
elements = {content: obj.navigate}
|
|
256
250
|
callback = null
|
|
257
|
-
}
|
|
258
|
-
else if (typeof obj.navigate === 'object') {
|
|
251
|
+
} else if (typeof obj.navigate === 'object') {
|
|
259
252
|
let baseSelector = obj.navigate.links || obj.navigate.selector || 'a[href^="/"]'
|
|
260
253
|
selector = `${baseSelector}:not([data-navigate="false"]):not(.no-navigate)`
|
|
261
|
-
|
|
254
|
+
|
|
262
255
|
if (obj.navigate.update) {
|
|
263
256
|
elements = typeof obj.navigate.update === 'string' ? {content: obj.navigate.update} : obj.navigate.update
|
|
264
257
|
} else if (obj.navigate.elements) {
|
|
@@ -266,15 +259,14 @@ if (typeof window !== 'undefined') {
|
|
|
266
259
|
} else {
|
|
267
260
|
elements = {content: 'main'}
|
|
268
261
|
}
|
|
269
|
-
|
|
262
|
+
|
|
270
263
|
callback = obj.navigate.on || obj.navigate.callback || null
|
|
271
|
-
}
|
|
272
|
-
else if (obj.navigate === true) {
|
|
264
|
+
} else if (obj.navigate === true) {
|
|
273
265
|
selector = 'a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)'
|
|
274
266
|
elements = {content: 'main'}
|
|
275
267
|
callback = null
|
|
276
268
|
}
|
|
277
|
-
|
|
269
|
+
|
|
278
270
|
if (document.readyState === 'loading') {
|
|
279
271
|
document.addEventListener('DOMContentLoaded', () => {
|
|
280
272
|
this.loader(selector, elements, callback)
|
|
@@ -283,7 +275,7 @@ if (typeof window !== 'undefined') {
|
|
|
283
275
|
this.loader(selector, elements, callback)
|
|
284
276
|
}
|
|
285
277
|
}
|
|
286
|
-
|
|
278
|
+
|
|
287
279
|
if (obj.start) document.addEventListener('DOMContentLoaded', () => obj.start())
|
|
288
280
|
if (obj.load) {
|
|
289
281
|
if (!this.actions.load) this.actions.load = []
|
|
@@ -312,12 +304,11 @@ if (typeof window !== 'undefined') {
|
|
|
312
304
|
if (typeof obj[key][key2] == 'function') {
|
|
313
305
|
this.#on(document, key, key2, obj[key][key2])
|
|
314
306
|
} else {
|
|
315
|
-
let func = ''
|
|
316
307
|
let split = ''
|
|
317
308
|
if (obj[key][key2].includes('.')) split = '.'
|
|
318
309
|
else if (obj[key][key2].includes('#')) split = '#'
|
|
319
310
|
else if (obj[key][key2].includes(' ')) split = ' '
|
|
320
|
-
func = split != '' ? obj[key][key2].split(split) : [obj[key][key2]]
|
|
311
|
+
const func = split != '' ? obj[key][key2].split(split) : [obj[key][key2]]
|
|
321
312
|
if (func != '') {
|
|
322
313
|
let getfunc = obj
|
|
323
314
|
func.forEach(function (item) {
|
|
@@ -329,12 +320,12 @@ if (typeof window !== 'undefined') {
|
|
|
329
320
|
}
|
|
330
321
|
}
|
|
331
322
|
}
|
|
332
|
-
|
|
323
|
+
|
|
333
324
|
client() {
|
|
334
325
|
if (!document.cookie.includes('odac_client=')) return null
|
|
335
326
|
return document.cookie.split('odac_client=')[1].split(';')[0]
|
|
336
327
|
}
|
|
337
|
-
|
|
328
|
+
|
|
338
329
|
data(key) {
|
|
339
330
|
if (!this.#data) {
|
|
340
331
|
const script = document.getElementById('odac-data')
|
|
@@ -346,30 +337,30 @@ if (typeof window !== 'undefined') {
|
|
|
346
337
|
}
|
|
347
338
|
}
|
|
348
339
|
}
|
|
349
|
-
|
|
340
|
+
|
|
350
341
|
if (this.#data) {
|
|
351
342
|
if (key) return this.#data[key] ?? null
|
|
352
343
|
return this.#data
|
|
353
344
|
}
|
|
354
|
-
|
|
345
|
+
|
|
355
346
|
return null
|
|
356
347
|
}
|
|
357
|
-
|
|
348
|
+
|
|
358
349
|
form(obj, callback) {
|
|
359
350
|
if (typeof obj != 'object') obj = {form: obj}
|
|
360
351
|
const formSelector = obj.form
|
|
361
|
-
|
|
352
|
+
|
|
362
353
|
if (this.#formSubmitHandlers.has(formSelector)) {
|
|
363
354
|
const oldHandler = this.#formSubmitHandlers.get(formSelector)
|
|
364
355
|
document.removeEventListener('submit', oldHandler)
|
|
365
356
|
}
|
|
366
|
-
|
|
357
|
+
|
|
367
358
|
const handler = e => {
|
|
368
359
|
const formElement = e.target.closest(formSelector)
|
|
369
360
|
if (!formElement) return
|
|
370
|
-
|
|
361
|
+
|
|
371
362
|
e.preventDefault()
|
|
372
|
-
|
|
363
|
+
|
|
373
364
|
if (obj.messages !== false) {
|
|
374
365
|
if (obj.messages == undefined || obj.messages == true || obj.messages.includes('error')) {
|
|
375
366
|
formElement.querySelectorAll('*[odac-form-error]').forEach(el => (el.style.display = 'none'))
|
|
@@ -378,29 +369,31 @@ if (typeof window !== 'undefined') {
|
|
|
378
369
|
formElement.querySelectorAll('*[odac-form-success]').forEach(el => (el.style.display = 'none'))
|
|
379
370
|
}
|
|
380
371
|
}
|
|
381
|
-
|
|
372
|
+
|
|
382
373
|
const inputs = formElement.querySelectorAll('input:not([type="hidden"]), textarea, select')
|
|
383
374
|
let isValid = true
|
|
384
375
|
let firstInvalidInput = null
|
|
385
|
-
|
|
376
|
+
|
|
386
377
|
const showError = (input, errorType) => {
|
|
387
378
|
isValid = false
|
|
388
379
|
firstInvalidInput = input
|
|
389
|
-
|
|
380
|
+
|
|
390
381
|
if (input.type !== 'checkbox' && input.type !== 'radio') {
|
|
391
382
|
input.style.borderColor = '#dc3545'
|
|
392
383
|
}
|
|
393
|
-
|
|
384
|
+
|
|
394
385
|
const customMessage = input.getAttribute(`data-error-${errorType}`)
|
|
395
386
|
if (customMessage) {
|
|
396
387
|
let errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
|
|
397
|
-
|
|
388
|
+
|
|
398
389
|
if (!errorSpan) {
|
|
399
390
|
errorSpan = document.createElement('span')
|
|
400
391
|
errorSpan.setAttribute('odac-form-error', input.name)
|
|
401
392
|
if ((input.type === 'checkbox' || input.type === 'radio') && input.id) {
|
|
402
393
|
const label = formElement.querySelector(`label[for="${input.id}"]`)
|
|
403
|
-
label
|
|
394
|
+
label
|
|
395
|
+
? label.parentNode.insertBefore(errorSpan, label.nextSibling)
|
|
396
|
+
: input.parentNode.insertBefore(errorSpan, input.nextSibling)
|
|
404
397
|
} else {
|
|
405
398
|
input.parentNode.insertBefore(errorSpan, input.nextSibling)
|
|
406
399
|
}
|
|
@@ -409,7 +402,7 @@ if (typeof window !== 'undefined') {
|
|
|
409
402
|
errorSpan.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
410
403
|
}
|
|
411
404
|
}
|
|
412
|
-
|
|
405
|
+
|
|
413
406
|
for (const input of inputs) {
|
|
414
407
|
input.style.borderColor = ''
|
|
415
408
|
const errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
|
|
@@ -417,49 +410,62 @@ if (typeof window !== 'undefined') {
|
|
|
417
410
|
errorSpan.style.display = 'none'
|
|
418
411
|
errorSpan.textContent = ''
|
|
419
412
|
}
|
|
420
|
-
|
|
413
|
+
|
|
421
414
|
if (input.hasAttribute('required')) {
|
|
422
415
|
const isEmpty = input.type === 'checkbox' || input.type === 'radio' ? !input.checked : !input.value.trim()
|
|
423
|
-
if (isEmpty) {
|
|
416
|
+
if (isEmpty) {
|
|
417
|
+
showError(input, 'required')
|
|
418
|
+
break
|
|
419
|
+
}
|
|
424
420
|
}
|
|
425
|
-
|
|
421
|
+
|
|
426
422
|
if (input.hasAttribute('minlength') && input.value && input.value.trim().length < parseInt(input.getAttribute('minlength'))) {
|
|
427
|
-
showError(input, 'minlength')
|
|
423
|
+
showError(input, 'minlength')
|
|
424
|
+
break
|
|
428
425
|
}
|
|
429
|
-
|
|
426
|
+
|
|
430
427
|
if (input.hasAttribute('maxlength') && input.value && input.value.trim().length > parseInt(input.getAttribute('maxlength'))) {
|
|
431
|
-
showError(input, 'maxlength')
|
|
428
|
+
showError(input, 'maxlength')
|
|
429
|
+
break
|
|
432
430
|
}
|
|
433
|
-
|
|
431
|
+
|
|
434
432
|
if (input.hasAttribute('pattern') && input.value) {
|
|
435
|
-
if (!new RegExp(input.getAttribute('pattern')).test(input.value.trim())) {
|
|
433
|
+
if (!new RegExp(input.getAttribute('pattern')).test(input.value.trim())) {
|
|
434
|
+
showError(input, 'pattern')
|
|
435
|
+
break
|
|
436
|
+
}
|
|
436
437
|
}
|
|
437
|
-
|
|
438
|
+
|
|
438
439
|
if (input.type === 'email' && input.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value.trim())) {
|
|
439
|
-
showError(input, 'email')
|
|
440
|
+
showError(input, 'email')
|
|
441
|
+
break
|
|
440
442
|
}
|
|
441
443
|
}
|
|
442
|
-
|
|
444
|
+
|
|
443
445
|
if (!isValid) {
|
|
444
446
|
if (firstInvalidInput) firstInvalidInput.focus()
|
|
445
447
|
return
|
|
446
448
|
}
|
|
447
|
-
|
|
449
|
+
|
|
448
450
|
if (this.actions.odac?.form?.input?.class?.invalid) {
|
|
449
451
|
const invalidClass = this.actions.odac.form.input.class.invalid
|
|
450
452
|
formElement.querySelectorAll(`.${invalidClass}`).forEach(el => el.classList.remove(invalidClass))
|
|
451
453
|
}
|
|
452
|
-
|
|
454
|
+
|
|
453
455
|
let datastring, cache, contentType, processData
|
|
454
456
|
if (formElement.querySelector('input[type=file]')) {
|
|
455
457
|
datastring = new FormData(formElement)
|
|
456
458
|
datastring.append('token', this.token())
|
|
457
|
-
cache = false
|
|
459
|
+
cache = false
|
|
460
|
+
contentType = false
|
|
461
|
+
processData = false
|
|
458
462
|
} else {
|
|
459
463
|
datastring = this.#serialize(formElement) + '&_token=' + this.token()
|
|
460
|
-
cache = true
|
|
464
|
+
cache = true
|
|
465
|
+
contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
466
|
+
processData = true
|
|
461
467
|
}
|
|
462
|
-
|
|
468
|
+
|
|
463
469
|
const submitButtons = formElement.querySelectorAll('button[type="submit"], input[type="submit"]')
|
|
464
470
|
submitButtons.forEach(btn => {
|
|
465
471
|
btn.disabled = true
|
|
@@ -469,9 +475,9 @@ if (typeof window !== 'undefined') {
|
|
|
469
475
|
btn.textContent = loadingText
|
|
470
476
|
}
|
|
471
477
|
})
|
|
472
|
-
|
|
478
|
+
|
|
473
479
|
formElement.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach(el => (el.disabled = true))
|
|
474
|
-
|
|
480
|
+
|
|
475
481
|
this.#ajax({
|
|
476
482
|
type: formElement.getAttribute('method'),
|
|
477
483
|
url: formElement.getAttribute('action'),
|
|
@@ -494,39 +500,46 @@ if (typeof window !== 'undefined') {
|
|
|
494
500
|
span.innerHTML = this.textToHtml(data.result.message)
|
|
495
501
|
formElement.appendChild(span)
|
|
496
502
|
}
|
|
497
|
-
|
|
503
|
+
|
|
498
504
|
if (data.result._token) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
505
|
+
const tokenInput = formElement.querySelector('input[name="_odac_form_token"]')
|
|
506
|
+
if (tokenInput) tokenInput.value = data.result._token
|
|
507
|
+
|
|
508
|
+
const formTokenAttr = formElement.getAttribute('data-odac-form')
|
|
509
|
+
if (formTokenAttr) {
|
|
510
|
+
formElement.setAttribute('data-odac-form', data.result._token)
|
|
511
|
+
if (!formElement.matches(formSelector)) {
|
|
512
|
+
if (this.#formSubmitHandlers.has(formSelector)) {
|
|
513
|
+
document.removeEventListener('submit', this.#formSubmitHandlers.get(formSelector))
|
|
514
|
+
this.#formSubmitHandlers.delete(formSelector)
|
|
515
|
+
}
|
|
516
|
+
const newObj = {...obj, form: `form[data-odac-form="${data.result._token}"]`}
|
|
517
|
+
this.form(newObj, callback)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
514
520
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
+
|
|
522
|
+
if (obj.clear !== false && formElement.getAttribute('clear') !== 'false' && !data.result.redirect) {
|
|
523
|
+
formElement
|
|
524
|
+
.querySelectorAll(
|
|
525
|
+
'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([readonly]), textarea, select'
|
|
526
|
+
)
|
|
527
|
+
.forEach(el => {
|
|
528
|
+
if (el.type === 'checkbox' || el.type === 'radio') el.checked = false
|
|
529
|
+
else if (el.tagName === 'SELECT') el.selectedIndex = 0
|
|
530
|
+
else el.value = ''
|
|
521
531
|
})
|
|
522
|
-
|
|
532
|
+
}
|
|
523
533
|
} else if (!data.result.success && data.errors) {
|
|
524
534
|
Object.entries(data.errors).forEach(([name, message]) => {
|
|
525
535
|
if (message) {
|
|
526
536
|
let errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
|
|
527
537
|
if (errorEl) {
|
|
528
538
|
errorEl.innerHTML = this.textToHtml(message)
|
|
529
|
-
errorEl.style.
|
|
539
|
+
errorEl.style.display = 'block'
|
|
540
|
+
if (!errorEl.style.color) errorEl.style.color = '#dc3545'
|
|
541
|
+
if (!errorEl.style.fontSize) errorEl.style.fontSize = '0.875rem'
|
|
542
|
+
if (!errorEl.style.marginTop) errorEl.style.marginTop = '0.25rem'
|
|
530
543
|
} else {
|
|
531
544
|
const inputEl = formElement.querySelector(`*[name="${name}"]`)
|
|
532
545
|
if (inputEl) {
|
|
@@ -534,10 +547,12 @@ if (typeof window !== 'undefined') {
|
|
|
534
547
|
errorEl.setAttribute('odac-form-error', name)
|
|
535
548
|
errorEl.innerHTML = this.textToHtml(message)
|
|
536
549
|
errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
537
|
-
|
|
550
|
+
|
|
538
551
|
if ((inputEl.type === 'checkbox' || inputEl.type === 'radio') && inputEl.id) {
|
|
539
552
|
const label = formElement.querySelector(`label[for="${inputEl.id}"]`)
|
|
540
|
-
label
|
|
553
|
+
label
|
|
554
|
+
? label.parentNode.insertBefore(errorEl, label.nextSibling)
|
|
555
|
+
: inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
541
556
|
} else {
|
|
542
557
|
inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
543
558
|
}
|
|
@@ -545,7 +560,10 @@ if (typeof window !== 'undefined') {
|
|
|
545
560
|
const errorEl = document.createElement('div')
|
|
546
561
|
errorEl.setAttribute('odac-form-error', name)
|
|
547
562
|
errorEl.innerHTML = this.textToHtml(message)
|
|
548
|
-
errorEl.style.
|
|
563
|
+
errorEl.style.display = 'block'
|
|
564
|
+
errorEl.style.color = '#dc3545'
|
|
565
|
+
errorEl.style.fontSize = '0.875rem'
|
|
566
|
+
errorEl.style.marginBottom = '1rem'
|
|
549
567
|
formElement.insertBefore(errorEl, formElement.firstChild)
|
|
550
568
|
}
|
|
551
569
|
}
|
|
@@ -553,12 +571,19 @@ if (typeof window !== 'undefined') {
|
|
|
553
571
|
const inputEl = formElement.querySelector(`*[name="${name}"]`)
|
|
554
572
|
if (inputEl) {
|
|
555
573
|
if (inputEl.type !== 'checkbox' && inputEl.type !== 'radio') inputEl.style.borderColor = '#dc3545'
|
|
556
|
-
inputEl.addEventListener(
|
|
574
|
+
inputEl.addEventListener(
|
|
575
|
+
'focus',
|
|
576
|
+
function handler() {
|
|
557
577
|
inputEl.style.borderColor = ''
|
|
558
578
|
const errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
|
|
559
|
-
if (errorEl) {
|
|
579
|
+
if (errorEl) {
|
|
580
|
+
errorEl.style.display = 'none'
|
|
581
|
+
errorEl.textContent = ''
|
|
582
|
+
}
|
|
560
583
|
inputEl.removeEventListener('focus', handler)
|
|
561
|
-
|
|
584
|
+
},
|
|
585
|
+
{once: true}
|
|
586
|
+
)
|
|
562
587
|
}
|
|
563
588
|
})
|
|
564
589
|
}
|
|
@@ -572,9 +597,13 @@ if (typeof window !== 'undefined') {
|
|
|
572
597
|
},
|
|
573
598
|
xhr: () => {
|
|
574
599
|
var xhr = new window.XMLHttpRequest()
|
|
575
|
-
xhr.upload.addEventListener(
|
|
576
|
-
|
|
577
|
-
|
|
600
|
+
xhr.upload.addEventListener(
|
|
601
|
+
'progress',
|
|
602
|
+
evt => {
|
|
603
|
+
if (evt.lengthComputable && obj.loading) obj.loading(parseInt((100 / evt.total) * evt.loaded))
|
|
604
|
+
},
|
|
605
|
+
false
|
|
606
|
+
)
|
|
578
607
|
return xhr
|
|
579
608
|
},
|
|
580
609
|
error: () => console.error('Odac:', 'Request failed', '\nForm: ' + obj.form + '\nRequest: ' + formElement.getAttribute('action')),
|
|
@@ -582,35 +611,38 @@ if (typeof window !== 'undefined') {
|
|
|
582
611
|
submitButtons.forEach(btn => {
|
|
583
612
|
btn.disabled = false
|
|
584
613
|
const originalText = btn.getAttribute('data-original-text')
|
|
585
|
-
if (originalText) {
|
|
614
|
+
if (originalText) {
|
|
615
|
+
btn.textContent = originalText
|
|
616
|
+
btn.removeAttribute('data-original-text')
|
|
617
|
+
}
|
|
586
618
|
})
|
|
587
619
|
formElement.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach(el => (el.disabled = false))
|
|
588
620
|
}
|
|
589
621
|
})
|
|
590
622
|
}
|
|
591
|
-
|
|
623
|
+
|
|
592
624
|
document.addEventListener('submit', handler)
|
|
593
625
|
this.#formSubmitHandlers.set(formSelector, handler)
|
|
594
626
|
}
|
|
595
|
-
|
|
627
|
+
|
|
596
628
|
get(url, callback) {
|
|
597
629
|
url = url + '?_token=' + this.token()
|
|
598
630
|
this.#ajax({url: url, success: callback})
|
|
599
631
|
}
|
|
600
|
-
|
|
632
|
+
|
|
601
633
|
page() {
|
|
602
634
|
if (!this.#page) {
|
|
603
635
|
this.#page = document.documentElement.dataset.odacPage || ''
|
|
604
636
|
}
|
|
605
637
|
return this.#page
|
|
606
638
|
}
|
|
607
|
-
|
|
639
|
+
|
|
608
640
|
storage(key, value) {
|
|
609
641
|
if (value === undefined) return localStorage.getItem(key)
|
|
610
642
|
else if (value === null) return localStorage.removeItem(key)
|
|
611
643
|
else localStorage.setItem(key, value)
|
|
612
644
|
}
|
|
613
|
-
|
|
645
|
+
|
|
614
646
|
token() {
|
|
615
647
|
if (!this.#token.listener) {
|
|
616
648
|
document.addEventListener('odac:ajaxSuccess', event => {
|
|
@@ -619,8 +651,8 @@ if (typeof window !== 'undefined') {
|
|
|
619
651
|
try {
|
|
620
652
|
const token = detail.xhr.getResponseHeader('X-Odac-Token')
|
|
621
653
|
if (token) {
|
|
622
|
-
|
|
623
|
-
|
|
654
|
+
this.#token.hash.push(token)
|
|
655
|
+
if (this.#token.hash.length > 2) this.#token.hash.shift()
|
|
624
656
|
}
|
|
625
657
|
} catch (e) {
|
|
626
658
|
console.error('Error in ajaxSuccess token handler:', e)
|
|
@@ -651,55 +683,66 @@ if (typeof window !== 'undefined') {
|
|
|
651
683
|
})
|
|
652
684
|
return return_token
|
|
653
685
|
}
|
|
654
|
-
|
|
686
|
+
|
|
655
687
|
textToHtml(str) {
|
|
656
688
|
if (typeof str !== 'string') return str
|
|
657
689
|
return str
|
|
658
|
-
.replace(/&/g, '&')
|
|
659
|
-
.replace(
|
|
690
|
+
.replace(/&/g, '&')
|
|
691
|
+
.replace(/</g, '<')
|
|
692
|
+
.replace(/>/g, '>')
|
|
693
|
+
.replace(/"/g, '"')
|
|
694
|
+
.replace(/'/g, ''')
|
|
695
|
+
.replace(/\n/g, '<br>')
|
|
660
696
|
}
|
|
661
|
-
|
|
697
|
+
|
|
662
698
|
load(url, callback, push = true) {
|
|
663
699
|
if (this.#isNavigating) return false
|
|
664
|
-
|
|
700
|
+
|
|
665
701
|
const currentUrl = window.location.href
|
|
666
702
|
url = new URL(url, currentUrl).href
|
|
667
|
-
if (url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.includes('#'))
|
|
668
|
-
|
|
703
|
+
if (url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.includes('#'))
|
|
704
|
+
return false
|
|
705
|
+
|
|
669
706
|
this.#isNavigating = true
|
|
670
|
-
|
|
707
|
+
|
|
671
708
|
const currentSkeleton = document.documentElement.dataset.odacSkeleton
|
|
672
709
|
const elements = Object.entries(this.#loader.elements)
|
|
673
|
-
|
|
710
|
+
|
|
674
711
|
const elementsToUpdate = []
|
|
675
712
|
elements.forEach(([key, selector]) => {
|
|
676
713
|
const element = document.querySelector(selector)
|
|
677
714
|
if (element) elementsToUpdate.push({key, element})
|
|
678
715
|
})
|
|
679
|
-
|
|
680
|
-
let ajaxData = null,
|
|
681
|
-
|
|
716
|
+
|
|
717
|
+
let ajaxData = null,
|
|
718
|
+
ajaxXhr = null,
|
|
719
|
+
fadeOutComplete = false,
|
|
720
|
+
ajaxComplete = false
|
|
721
|
+
|
|
682
722
|
const applyUpdate = () => {
|
|
683
723
|
if (!fadeOutComplete || !ajaxComplete || !ajaxData) return
|
|
684
|
-
|
|
724
|
+
|
|
685
725
|
const finalUrl = ajaxXhr.responseURL || url
|
|
686
|
-
if (ajaxData.skeletonChanged) {
|
|
726
|
+
if (ajaxData.skeletonChanged) {
|
|
727
|
+
window.location.href = finalUrl
|
|
728
|
+
return
|
|
729
|
+
}
|
|
687
730
|
if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
|
|
688
|
-
|
|
731
|
+
|
|
689
732
|
const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
|
|
690
733
|
if (newPage !== null) {
|
|
691
734
|
this.#page = newPage
|
|
692
735
|
document.documentElement.dataset.odacPage = newPage
|
|
693
736
|
}
|
|
694
|
-
|
|
737
|
+
|
|
695
738
|
if (ajaxData.data) this.#data = ajaxData.data
|
|
696
739
|
if (ajaxData.title) document.title = ajaxData.title
|
|
697
|
-
|
|
740
|
+
|
|
698
741
|
if (elementsToUpdate.length === 0) {
|
|
699
742
|
this.#handleLoadComplete(ajaxData, callback)
|
|
700
743
|
return
|
|
701
744
|
}
|
|
702
|
-
|
|
745
|
+
|
|
703
746
|
let completed = 0
|
|
704
747
|
elementsToUpdate.forEach(({key, element}) => {
|
|
705
748
|
if (ajaxData.output && ajaxData.output[key] !== undefined) element.innerHTML = ajaxData.output[key]
|
|
@@ -709,7 +752,7 @@ if (typeof window !== 'undefined') {
|
|
|
709
752
|
})
|
|
710
753
|
})
|
|
711
754
|
}
|
|
712
|
-
|
|
755
|
+
|
|
713
756
|
if (elementsToUpdate.length > 0) {
|
|
714
757
|
let fadeOutCount = 0
|
|
715
758
|
elementsToUpdate.forEach(({element}) => {
|
|
@@ -724,7 +767,7 @@ if (typeof window !== 'undefined') {
|
|
|
724
767
|
} else {
|
|
725
768
|
fadeOutComplete = true
|
|
726
769
|
}
|
|
727
|
-
|
|
770
|
+
|
|
728
771
|
this.#ajax({
|
|
729
772
|
url: url,
|
|
730
773
|
type: 'GET',
|
|
@@ -735,7 +778,10 @@ if (typeof window !== 'undefined') {
|
|
|
735
778
|
},
|
|
736
779
|
dataType: 'json',
|
|
737
780
|
success: (data, status, xhr) => {
|
|
738
|
-
ajaxData = data
|
|
781
|
+
ajaxData = data
|
|
782
|
+
ajaxXhr = xhr
|
|
783
|
+
ajaxComplete = true
|
|
784
|
+
applyUpdate()
|
|
739
785
|
},
|
|
740
786
|
error: () => {
|
|
741
787
|
this.#isNavigating = false
|
|
@@ -743,52 +789,72 @@ if (typeof window !== 'undefined') {
|
|
|
743
789
|
}
|
|
744
790
|
})
|
|
745
791
|
}
|
|
746
|
-
|
|
792
|
+
|
|
747
793
|
#handleLoadComplete(data, callback) {
|
|
748
|
-
if (this.actions.load)
|
|
749
|
-
|
|
750
|
-
|
|
794
|
+
if (this.actions.load)
|
|
795
|
+
(Array.isArray(this.actions.load) ? this.actions.load : [this.actions.load]).forEach(fn => fn(this.page(), data.variables))
|
|
796
|
+
if (this.actions.page && this.actions.page[this.page()])
|
|
797
|
+
(Array.isArray(this.actions.page[this.page()]) ? this.actions.page[this.page()] : [this.actions.page[this.page()]]).forEach(fn =>
|
|
798
|
+
fn(data.variables)
|
|
799
|
+
)
|
|
800
|
+
|
|
751
801
|
if (callback && typeof callback === 'function') callback(this.page(), data.variables)
|
|
752
|
-
|
|
802
|
+
|
|
753
803
|
window.scrollTo({top: 0, behavior: 'smooth'})
|
|
754
804
|
this.#isNavigating = false
|
|
755
805
|
}
|
|
756
|
-
|
|
806
|
+
|
|
757
807
|
loader(selector, elements, callback) {
|
|
758
808
|
this.#loader.elements = elements
|
|
759
809
|
this.#loader.callback = callback
|
|
760
810
|
const odacInstance = this
|
|
761
|
-
|
|
811
|
+
|
|
762
812
|
this.#on(document, 'click', selector, function (e) {
|
|
763
813
|
if (e.ctrlKey || e.metaKey) return
|
|
764
814
|
const anchor = this
|
|
765
815
|
if (!anchor) return
|
|
766
816
|
const url = anchor.getAttribute('href')
|
|
767
817
|
const target = anchor.getAttribute('target')
|
|
768
|
-
if (
|
|
818
|
+
if (
|
|
819
|
+
!url ||
|
|
820
|
+
url === '' ||
|
|
821
|
+
url.startsWith('javascript:') ||
|
|
822
|
+
url.startsWith('data:') ||
|
|
823
|
+
url.startsWith('vbscript:') ||
|
|
824
|
+
url.startsWith('#')
|
|
825
|
+
)
|
|
826
|
+
return
|
|
769
827
|
const isExternal = url.includes('://') && !url.includes(window.location.host)
|
|
770
828
|
if ((target === null || target === '_self') && !isExternal) {
|
|
771
829
|
e.preventDefault()
|
|
772
830
|
odacInstance.load(url, callback)
|
|
773
831
|
}
|
|
774
832
|
})
|
|
775
|
-
|
|
833
|
+
|
|
776
834
|
window.addEventListener('popstate', () => {
|
|
777
835
|
this.load(window.location.href, callback, false)
|
|
778
836
|
})
|
|
779
837
|
}
|
|
780
|
-
|
|
838
|
+
|
|
781
839
|
listen(url, onMessage, options = {}) {
|
|
782
840
|
const {onError = null, onOpen = null, autoReconnect = false, reconnectDelay = 3000} = options
|
|
783
|
-
let eventSource = null,
|
|
784
|
-
|
|
841
|
+
let eventSource = null,
|
|
842
|
+
reconnectTimer = null,
|
|
843
|
+
isClosed = false
|
|
844
|
+
|
|
785
845
|
const connect = () => {
|
|
786
846
|
if (isClosed) return
|
|
787
847
|
const urlWithToken = url + (url.includes('?') ? '&' : '?') + '_token=' + encodeURIComponent(this.token())
|
|
788
848
|
eventSource = new EventSource(urlWithToken)
|
|
789
|
-
eventSource.onopen = e => {
|
|
849
|
+
eventSource.onopen = e => {
|
|
850
|
+
if (onOpen) onOpen(e)
|
|
851
|
+
}
|
|
790
852
|
eventSource.onmessage = e => {
|
|
791
|
-
try {
|
|
853
|
+
try {
|
|
854
|
+
onMessage(JSON.parse(e.data))
|
|
855
|
+
} catch {
|
|
856
|
+
onMessage(e.data)
|
|
857
|
+
}
|
|
792
858
|
}
|
|
793
859
|
eventSource.onerror = e => {
|
|
794
860
|
if (onError) onError(e)
|
|
@@ -799,24 +865,26 @@ if (typeof window !== 'undefined') {
|
|
|
799
865
|
}
|
|
800
866
|
}
|
|
801
867
|
connect()
|
|
802
|
-
|
|
868
|
+
|
|
803
869
|
return {
|
|
804
870
|
close: () => {
|
|
805
871
|
isClosed = true
|
|
806
872
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
807
873
|
if (eventSource) eventSource.close()
|
|
808
874
|
},
|
|
809
|
-
send: () => {
|
|
875
|
+
send: () => {
|
|
876
|
+
throw new Error('SSE is one-way. Use POST requests to send data.')
|
|
877
|
+
}
|
|
810
878
|
}
|
|
811
879
|
}
|
|
812
|
-
|
|
880
|
+
|
|
813
881
|
ws(path, options = {}) {
|
|
814
882
|
const {autoReconnect = true, reconnectDelay = 3000, maxReconnectAttempts = 10, shared = false, token = true} = options
|
|
815
|
-
|
|
883
|
+
|
|
816
884
|
if (shared && typeof SharedWorker !== 'undefined') {
|
|
817
885
|
return this.#createSharedWebSocket(path, {autoReconnect, reconnectDelay, maxReconnectAttempts, token})
|
|
818
886
|
}
|
|
819
|
-
|
|
887
|
+
|
|
820
888
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
821
889
|
const wsUrl = `${protocol}//${window.location.host}${path}`
|
|
822
890
|
const protocols = []
|
|
@@ -824,10 +892,10 @@ if (typeof window !== 'undefined') {
|
|
|
824
892
|
const csrfToken = this.token()
|
|
825
893
|
if (csrfToken) protocols.push(`odac-token-${csrfToken}`)
|
|
826
894
|
}
|
|
827
|
-
|
|
895
|
+
|
|
828
896
|
return new OdacWebSocket(wsUrl, protocols, options)
|
|
829
897
|
}
|
|
830
|
-
|
|
898
|
+
|
|
831
899
|
#createSharedWebSocket(path, options) {
|
|
832
900
|
const scriptUrl = (() => {
|
|
833
901
|
if (document.currentScript) return document.currentScript.src
|
|
@@ -840,11 +908,11 @@ if (typeof window !== 'undefined') {
|
|
|
840
908
|
const worker = new SharedWorker(scriptUrl, `odac-ws-${path}`)
|
|
841
909
|
const handlers = {}
|
|
842
910
|
let isConnected = false
|
|
843
|
-
|
|
911
|
+
|
|
844
912
|
const emit = (event, ...args) => {
|
|
845
913
|
if (handlers[event]) handlers[event].forEach(fn => fn(...args))
|
|
846
914
|
}
|
|
847
|
-
|
|
915
|
+
|
|
848
916
|
worker.port.onmessage = e => {
|
|
849
917
|
const {type, data} = e.data
|
|
850
918
|
switch (type) {
|
|
@@ -864,11 +932,11 @@ if (typeof window !== 'undefined') {
|
|
|
864
932
|
break
|
|
865
933
|
}
|
|
866
934
|
}
|
|
867
|
-
|
|
935
|
+
|
|
868
936
|
worker.port.start()
|
|
869
|
-
|
|
937
|
+
|
|
870
938
|
const token = options.token ? this.token() : null
|
|
871
|
-
|
|
939
|
+
|
|
872
940
|
worker.port.postMessage({
|
|
873
941
|
type: 'connect',
|
|
874
942
|
path,
|
|
@@ -877,7 +945,7 @@ if (typeof window !== 'undefined') {
|
|
|
877
945
|
token,
|
|
878
946
|
options
|
|
879
947
|
})
|
|
880
|
-
|
|
948
|
+
|
|
881
949
|
return {
|
|
882
950
|
on: (event, handler) => {
|
|
883
951
|
if (!handlers[event]) handlers[event] = []
|
|
@@ -910,9 +978,8 @@ if (typeof window !== 'undefined') {
|
|
|
910
978
|
}
|
|
911
979
|
}
|
|
912
980
|
}
|
|
913
|
-
|
|
981
|
+
|
|
914
982
|
window.Odac = new _odac()
|
|
915
|
-
|
|
916
983
|
;(function initAutoNavigate() {
|
|
917
984
|
const init = () => {
|
|
918
985
|
const contentEl = document.querySelector('[data-odac-navigate="content"]')
|
|
@@ -922,9 +989,9 @@ if (typeof window !== 'undefined') {
|
|
|
922
989
|
}
|
|
923
990
|
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
|
|
924
991
|
})()
|
|
925
|
-
|
|
992
|
+
|
|
926
993
|
document.addEventListener('DOMContentLoaded', () => {
|
|
927
|
-
['register', 'login'].forEach(type => {
|
|
994
|
+
;['register', 'login'].forEach(type => {
|
|
928
995
|
document.querySelectorAll(`form.odac-${type}-form[data-odac-${type}]`).forEach(form => {
|
|
929
996
|
const token = form.getAttribute(`data-odac-${type}`)
|
|
930
997
|
window.Odac.form({form: `form[data-odac-${type}="${token}"]`})
|
|
@@ -956,11 +1023,11 @@ if (typeof window !== 'undefined') {
|
|
|
956
1023
|
socket = new OdacWebSocket(wsUrl, protocols, options)
|
|
957
1024
|
socket.on('open', () => broadcast('open'))
|
|
958
1025
|
socket.on('message', data => broadcast('message', data))
|
|
959
|
-
socket.on('close', e => broadcast('close', e))
|
|
960
|
-
socket.on('error', e => broadcast('error', e))
|
|
1026
|
+
socket.on('close', e => broadcast('close', {code: e?.code, reason: e?.reason, wasClean: e?.wasClean}))
|
|
1027
|
+
socket.on('error', e => broadcast('error', {message: e?.message || 'WebSocket error'}))
|
|
961
1028
|
} else if (socket.connected) {
|
|
962
|
-
|
|
963
|
-
|
|
1029
|
+
// If already connected, notify the new port immediately
|
|
1030
|
+
port.postMessage({type: 'open'})
|
|
964
1031
|
}
|
|
965
1032
|
break
|
|
966
1033
|
case 'send':
|