odac 1.2.0 → 1.4.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/memory.md +16 -1
- package/.github/workflows/release.yml +27 -5
- package/.husky/pre-push +3 -3
- package/.releaserc.js +2 -2
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/bin/odac.js +187 -8
- package/client/odac.js +243 -178
- package/docs/ai/README.md +49 -0
- package/docs/ai/skills/SKILL.md +39 -0
- package/docs/ai/skills/backend/authentication.md +67 -0
- package/docs/ai/skills/backend/config.md +32 -0
- package/docs/ai/skills/backend/controllers.md +62 -0
- package/docs/ai/skills/backend/cron.md +50 -0
- package/docs/ai/skills/backend/database.md +21 -0
- package/docs/ai/skills/backend/forms.md +19 -0
- package/docs/ai/skills/backend/ipc.md +55 -0
- package/docs/ai/skills/backend/mail.md +34 -0
- package/docs/ai/skills/backend/request_response.md +35 -0
- package/docs/ai/skills/backend/routing.md +51 -0
- package/docs/ai/skills/backend/storage.md +43 -0
- package/docs/ai/skills/backend/streaming.md +34 -0
- package/docs/ai/skills/backend/structure.md +57 -0
- package/docs/ai/skills/backend/translations.md +42 -0
- package/docs/ai/skills/backend/utilities.md +24 -0
- package/docs/ai/skills/backend/validation.md +53 -0
- package/docs/ai/skills/backend/views.md +61 -0
- package/docs/ai/skills/frontend/core.md +66 -0
- package/docs/ai/skills/frontend/forms.md +21 -0
- package/docs/ai/skills/frontend/navigation.md +20 -0
- package/docs/ai/skills/frontend/realtime.md +47 -0
- package/docs/backend/04-routing/09-websocket.md +14 -1
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +2 -0
- package/docs/backend/10-authentication/05-session-management.md +25 -3
- package/package.json +13 -13
- package/src/Auth.js +100 -15
- package/src/Database.js +1 -1
- package/src/Mail.js +19 -9
- package/src/Odac.js +17 -14
- package/src/Request.js +5 -1
- package/src/Route/Internal.js +21 -18
- package/src/Route/MimeTypes.js +56 -0
- package/src/Route.js +136 -92
- package/src/Validator.js +23 -14
- package/src/View/Form.js +91 -51
- package/src/View.js +15 -10
- package/src/WebSocket.js +45 -12
- package/test/Auth.test.js +249 -0
- package/test/Client.test.js +29 -0
- package/test/Odac.test.js +4 -2
- package/test/Route.test.js +104 -0
- package/test/View/Form.test.js +37 -0
- package/test/WebSocket.test.js +141 -3
- package/.github/workflows/test-publish.yml +0 -36
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,67 +126,72 @@ 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
|
|
151
|
-
|
|
145
|
+
const contentTypeHeader = xhr.getResponseHeader('Content-Type')
|
|
146
|
+
const isJson = dataType === 'json' || (contentTypeHeader && contentTypeHeader.includes('application/json'))
|
|
147
|
+
|
|
148
|
+
if (isJson) {
|
|
152
149
|
try {
|
|
153
150
|
responseData = JSON.parse(responseData)
|
|
154
151
|
} catch (e) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
if (dataType === 'json') {
|
|
153
|
+
console.error('JSON parse error:', e)
|
|
154
|
+
error(xhr, 'parseerror', e)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
158
157
|
}
|
|
159
158
|
}
|
|
160
|
-
|
|
159
|
+
|
|
161
160
|
document.dispatchEvent(
|
|
162
161
|
new CustomEvent('odac:ajaxSuccess', {
|
|
163
162
|
detail: {response: responseData, status: xhr.statusText, xhr, requestUrl: url}
|
|
164
163
|
})
|
|
165
164
|
)
|
|
166
|
-
|
|
165
|
+
|
|
167
166
|
success(responseData, xhr.statusText, xhr)
|
|
168
167
|
} else {
|
|
169
168
|
error(xhr, xhr.statusText)
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
|
-
|
|
171
|
+
|
|
173
172
|
xhr.onerror = () => error(xhr, 'error')
|
|
174
173
|
xhr.onloadend = () => complete()
|
|
175
174
|
xhr.send(data)
|
|
176
175
|
}
|
|
177
|
-
|
|
176
|
+
|
|
178
177
|
#fade(element, type, duration = 400, callback) {
|
|
179
178
|
const isIn = type === 'in'
|
|
180
179
|
const startOpacity = isIn ? 0 : 1
|
|
181
180
|
const endOpacity = isIn ? 1 : 0
|
|
182
|
-
|
|
181
|
+
|
|
183
182
|
element.style.opacity = startOpacity
|
|
184
183
|
if (isIn) {
|
|
185
184
|
element.style.display = 'block'
|
|
186
185
|
}
|
|
187
|
-
|
|
186
|
+
|
|
188
187
|
let startTime = null
|
|
189
|
-
|
|
188
|
+
|
|
190
189
|
const animate = currentTime => {
|
|
191
190
|
if (!startTime) startTime = currentTime
|
|
192
191
|
const progress = currentTime - startTime
|
|
193
192
|
const opacity = startOpacity + (endOpacity - startOpacity) * Math.min(progress / duration, 1)
|
|
194
193
|
element.style.opacity = opacity
|
|
195
|
-
|
|
194
|
+
|
|
196
195
|
if (progress < duration) {
|
|
197
196
|
requestAnimationFrame(animate)
|
|
198
197
|
} else {
|
|
@@ -204,15 +203,15 @@ if (typeof window !== 'undefined') {
|
|
|
204
203
|
}
|
|
205
204
|
requestAnimationFrame(animate)
|
|
206
205
|
}
|
|
207
|
-
|
|
206
|
+
|
|
208
207
|
#fadeIn(element, duration, callback) {
|
|
209
208
|
this.#fade(element, 'in', duration, callback)
|
|
210
209
|
}
|
|
211
|
-
|
|
210
|
+
|
|
212
211
|
#fadeOut(element, duration, callback) {
|
|
213
212
|
this.#fade(element, 'out', duration, callback)
|
|
214
213
|
}
|
|
215
|
-
|
|
214
|
+
|
|
216
215
|
#on(element, event, selector, handler) {
|
|
217
216
|
element.addEventListener(event, e => {
|
|
218
217
|
let target = e.target.closest(selector)
|
|
@@ -221,7 +220,7 @@ if (typeof window !== 'undefined') {
|
|
|
221
220
|
}
|
|
222
221
|
})
|
|
223
222
|
}
|
|
224
|
-
|
|
223
|
+
|
|
225
224
|
#serialize(form) {
|
|
226
225
|
const params = []
|
|
227
226
|
form.querySelectorAll('input, select, textarea').forEach(el => {
|
|
@@ -243,22 +242,21 @@ if (typeof window !== 'undefined') {
|
|
|
243
242
|
})
|
|
244
243
|
return params.join('&')
|
|
245
244
|
}
|
|
246
|
-
|
|
245
|
+
|
|
247
246
|
action(obj) {
|
|
248
247
|
if (obj.function) for (let func in obj.function) this.fn[func] = obj.function[func]
|
|
249
|
-
|
|
248
|
+
|
|
250
249
|
if (obj.navigate !== undefined && obj.navigate !== false) {
|
|
251
250
|
let selector, elements, callback
|
|
252
|
-
|
|
251
|
+
|
|
253
252
|
if (typeof obj.navigate === 'string') {
|
|
254
253
|
selector = 'a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)'
|
|
255
254
|
elements = {content: obj.navigate}
|
|
256
255
|
callback = null
|
|
257
|
-
}
|
|
258
|
-
else if (typeof obj.navigate === 'object') {
|
|
256
|
+
} else if (typeof obj.navigate === 'object') {
|
|
259
257
|
let baseSelector = obj.navigate.links || obj.navigate.selector || 'a[href^="/"]'
|
|
260
258
|
selector = `${baseSelector}:not([data-navigate="false"]):not(.no-navigate)`
|
|
261
|
-
|
|
259
|
+
|
|
262
260
|
if (obj.navigate.update) {
|
|
263
261
|
elements = typeof obj.navigate.update === 'string' ? {content: obj.navigate.update} : obj.navigate.update
|
|
264
262
|
} else if (obj.navigate.elements) {
|
|
@@ -266,15 +264,14 @@ if (typeof window !== 'undefined') {
|
|
|
266
264
|
} else {
|
|
267
265
|
elements = {content: 'main'}
|
|
268
266
|
}
|
|
269
|
-
|
|
267
|
+
|
|
270
268
|
callback = obj.navigate.on || obj.navigate.callback || null
|
|
271
|
-
}
|
|
272
|
-
else if (obj.navigate === true) {
|
|
269
|
+
} else if (obj.navigate === true) {
|
|
273
270
|
selector = 'a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)'
|
|
274
271
|
elements = {content: 'main'}
|
|
275
272
|
callback = null
|
|
276
273
|
}
|
|
277
|
-
|
|
274
|
+
|
|
278
275
|
if (document.readyState === 'loading') {
|
|
279
276
|
document.addEventListener('DOMContentLoaded', () => {
|
|
280
277
|
this.loader(selector, elements, callback)
|
|
@@ -283,7 +280,7 @@ if (typeof window !== 'undefined') {
|
|
|
283
280
|
this.loader(selector, elements, callback)
|
|
284
281
|
}
|
|
285
282
|
}
|
|
286
|
-
|
|
283
|
+
|
|
287
284
|
if (obj.start) document.addEventListener('DOMContentLoaded', () => obj.start())
|
|
288
285
|
if (obj.load) {
|
|
289
286
|
if (!this.actions.load) this.actions.load = []
|
|
@@ -312,12 +309,11 @@ if (typeof window !== 'undefined') {
|
|
|
312
309
|
if (typeof obj[key][key2] == 'function') {
|
|
313
310
|
this.#on(document, key, key2, obj[key][key2])
|
|
314
311
|
} else {
|
|
315
|
-
let func = ''
|
|
316
312
|
let split = ''
|
|
317
313
|
if (obj[key][key2].includes('.')) split = '.'
|
|
318
314
|
else if (obj[key][key2].includes('#')) split = '#'
|
|
319
315
|
else if (obj[key][key2].includes(' ')) split = ' '
|
|
320
|
-
func = split != '' ? obj[key][key2].split(split) : [obj[key][key2]]
|
|
316
|
+
const func = split != '' ? obj[key][key2].split(split) : [obj[key][key2]]
|
|
321
317
|
if (func != '') {
|
|
322
318
|
let getfunc = obj
|
|
323
319
|
func.forEach(function (item) {
|
|
@@ -329,12 +325,12 @@ if (typeof window !== 'undefined') {
|
|
|
329
325
|
}
|
|
330
326
|
}
|
|
331
327
|
}
|
|
332
|
-
|
|
328
|
+
|
|
333
329
|
client() {
|
|
334
330
|
if (!document.cookie.includes('odac_client=')) return null
|
|
335
331
|
return document.cookie.split('odac_client=')[1].split(';')[0]
|
|
336
332
|
}
|
|
337
|
-
|
|
333
|
+
|
|
338
334
|
data(key) {
|
|
339
335
|
if (!this.#data) {
|
|
340
336
|
const script = document.getElementById('odac-data')
|
|
@@ -346,30 +342,30 @@ if (typeof window !== 'undefined') {
|
|
|
346
342
|
}
|
|
347
343
|
}
|
|
348
344
|
}
|
|
349
|
-
|
|
345
|
+
|
|
350
346
|
if (this.#data) {
|
|
351
347
|
if (key) return this.#data[key] ?? null
|
|
352
348
|
return this.#data
|
|
353
349
|
}
|
|
354
|
-
|
|
350
|
+
|
|
355
351
|
return null
|
|
356
352
|
}
|
|
357
|
-
|
|
353
|
+
|
|
358
354
|
form(obj, callback) {
|
|
359
355
|
if (typeof obj != 'object') obj = {form: obj}
|
|
360
356
|
const formSelector = obj.form
|
|
361
|
-
|
|
357
|
+
|
|
362
358
|
if (this.#formSubmitHandlers.has(formSelector)) {
|
|
363
359
|
const oldHandler = this.#formSubmitHandlers.get(formSelector)
|
|
364
360
|
document.removeEventListener('submit', oldHandler)
|
|
365
361
|
}
|
|
366
|
-
|
|
362
|
+
|
|
367
363
|
const handler = e => {
|
|
368
364
|
const formElement = e.target.closest(formSelector)
|
|
369
365
|
if (!formElement) return
|
|
370
|
-
|
|
366
|
+
|
|
371
367
|
e.preventDefault()
|
|
372
|
-
|
|
368
|
+
|
|
373
369
|
if (obj.messages !== false) {
|
|
374
370
|
if (obj.messages == undefined || obj.messages == true || obj.messages.includes('error')) {
|
|
375
371
|
formElement.querySelectorAll('*[odac-form-error]').forEach(el => (el.style.display = 'none'))
|
|
@@ -378,29 +374,31 @@ if (typeof window !== 'undefined') {
|
|
|
378
374
|
formElement.querySelectorAll('*[odac-form-success]').forEach(el => (el.style.display = 'none'))
|
|
379
375
|
}
|
|
380
376
|
}
|
|
381
|
-
|
|
377
|
+
|
|
382
378
|
const inputs = formElement.querySelectorAll('input:not([type="hidden"]), textarea, select')
|
|
383
379
|
let isValid = true
|
|
384
380
|
let firstInvalidInput = null
|
|
385
|
-
|
|
381
|
+
|
|
386
382
|
const showError = (input, errorType) => {
|
|
387
383
|
isValid = false
|
|
388
384
|
firstInvalidInput = input
|
|
389
|
-
|
|
385
|
+
|
|
390
386
|
if (input.type !== 'checkbox' && input.type !== 'radio') {
|
|
391
387
|
input.style.borderColor = '#dc3545'
|
|
392
388
|
}
|
|
393
|
-
|
|
389
|
+
|
|
394
390
|
const customMessage = input.getAttribute(`data-error-${errorType}`)
|
|
395
391
|
if (customMessage) {
|
|
396
392
|
let errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
|
|
397
|
-
|
|
393
|
+
|
|
398
394
|
if (!errorSpan) {
|
|
399
395
|
errorSpan = document.createElement('span')
|
|
400
396
|
errorSpan.setAttribute('odac-form-error', input.name)
|
|
401
397
|
if ((input.type === 'checkbox' || input.type === 'radio') && input.id) {
|
|
402
398
|
const label = formElement.querySelector(`label[for="${input.id}"]`)
|
|
403
|
-
label
|
|
399
|
+
label
|
|
400
|
+
? label.parentNode.insertBefore(errorSpan, label.nextSibling)
|
|
401
|
+
: input.parentNode.insertBefore(errorSpan, input.nextSibling)
|
|
404
402
|
} else {
|
|
405
403
|
input.parentNode.insertBefore(errorSpan, input.nextSibling)
|
|
406
404
|
}
|
|
@@ -409,7 +407,7 @@ if (typeof window !== 'undefined') {
|
|
|
409
407
|
errorSpan.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
410
408
|
}
|
|
411
409
|
}
|
|
412
|
-
|
|
410
|
+
|
|
413
411
|
for (const input of inputs) {
|
|
414
412
|
input.style.borderColor = ''
|
|
415
413
|
const errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
|
|
@@ -417,49 +415,62 @@ if (typeof window !== 'undefined') {
|
|
|
417
415
|
errorSpan.style.display = 'none'
|
|
418
416
|
errorSpan.textContent = ''
|
|
419
417
|
}
|
|
420
|
-
|
|
418
|
+
|
|
421
419
|
if (input.hasAttribute('required')) {
|
|
422
420
|
const isEmpty = input.type === 'checkbox' || input.type === 'radio' ? !input.checked : !input.value.trim()
|
|
423
|
-
if (isEmpty) {
|
|
421
|
+
if (isEmpty) {
|
|
422
|
+
showError(input, 'required')
|
|
423
|
+
break
|
|
424
|
+
}
|
|
424
425
|
}
|
|
425
|
-
|
|
426
|
+
|
|
426
427
|
if (input.hasAttribute('minlength') && input.value && input.value.trim().length < parseInt(input.getAttribute('minlength'))) {
|
|
427
|
-
showError(input, 'minlength')
|
|
428
|
+
showError(input, 'minlength')
|
|
429
|
+
break
|
|
428
430
|
}
|
|
429
|
-
|
|
431
|
+
|
|
430
432
|
if (input.hasAttribute('maxlength') && input.value && input.value.trim().length > parseInt(input.getAttribute('maxlength'))) {
|
|
431
|
-
showError(input, 'maxlength')
|
|
433
|
+
showError(input, 'maxlength')
|
|
434
|
+
break
|
|
432
435
|
}
|
|
433
|
-
|
|
436
|
+
|
|
434
437
|
if (input.hasAttribute('pattern') && input.value) {
|
|
435
|
-
if (!new RegExp(input.getAttribute('pattern')).test(input.value.trim())) {
|
|
438
|
+
if (!new RegExp(input.getAttribute('pattern')).test(input.value.trim())) {
|
|
439
|
+
showError(input, 'pattern')
|
|
440
|
+
break
|
|
441
|
+
}
|
|
436
442
|
}
|
|
437
|
-
|
|
443
|
+
|
|
438
444
|
if (input.type === 'email' && input.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value.trim())) {
|
|
439
|
-
showError(input, 'email')
|
|
445
|
+
showError(input, 'email')
|
|
446
|
+
break
|
|
440
447
|
}
|
|
441
448
|
}
|
|
442
|
-
|
|
449
|
+
|
|
443
450
|
if (!isValid) {
|
|
444
451
|
if (firstInvalidInput) firstInvalidInput.focus()
|
|
445
452
|
return
|
|
446
453
|
}
|
|
447
|
-
|
|
454
|
+
|
|
448
455
|
if (this.actions.odac?.form?.input?.class?.invalid) {
|
|
449
456
|
const invalidClass = this.actions.odac.form.input.class.invalid
|
|
450
457
|
formElement.querySelectorAll(`.${invalidClass}`).forEach(el => el.classList.remove(invalidClass))
|
|
451
458
|
}
|
|
452
|
-
|
|
459
|
+
|
|
453
460
|
let datastring, cache, contentType, processData
|
|
454
461
|
if (formElement.querySelector('input[type=file]')) {
|
|
455
462
|
datastring = new FormData(formElement)
|
|
456
463
|
datastring.append('token', this.token())
|
|
457
|
-
cache = false
|
|
464
|
+
cache = false
|
|
465
|
+
contentType = false
|
|
466
|
+
processData = false
|
|
458
467
|
} else {
|
|
459
468
|
datastring = this.#serialize(formElement) + '&_token=' + this.token()
|
|
460
|
-
cache = true
|
|
469
|
+
cache = true
|
|
470
|
+
contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
471
|
+
processData = true
|
|
461
472
|
}
|
|
462
|
-
|
|
473
|
+
|
|
463
474
|
const submitButtons = formElement.querySelectorAll('button[type="submit"], input[type="submit"]')
|
|
464
475
|
submitButtons.forEach(btn => {
|
|
465
476
|
btn.disabled = true
|
|
@@ -469,9 +480,9 @@ if (typeof window !== 'undefined') {
|
|
|
469
480
|
btn.textContent = loadingText
|
|
470
481
|
}
|
|
471
482
|
})
|
|
472
|
-
|
|
483
|
+
|
|
473
484
|
formElement.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach(el => (el.disabled = true))
|
|
474
|
-
|
|
485
|
+
|
|
475
486
|
this.#ajax({
|
|
476
487
|
type: formElement.getAttribute('method'),
|
|
477
488
|
url: formElement.getAttribute('action'),
|
|
@@ -494,32 +505,36 @@ if (typeof window !== 'undefined') {
|
|
|
494
505
|
span.innerHTML = this.textToHtml(data.result.message)
|
|
495
506
|
formElement.appendChild(span)
|
|
496
507
|
}
|
|
497
|
-
|
|
508
|
+
|
|
498
509
|
if (data.result._token) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
510
|
+
const tokenInput = formElement.querySelector('input[name="_odac_form_token"]')
|
|
511
|
+
if (tokenInput) tokenInput.value = data.result._token
|
|
512
|
+
|
|
513
|
+
const formTokenAttr = formElement.getAttribute('data-odac-form')
|
|
514
|
+
if (formTokenAttr) {
|
|
515
|
+
formElement.setAttribute('data-odac-form', data.result._token)
|
|
516
|
+
if (!formElement.matches(formSelector)) {
|
|
517
|
+
if (this.#formSubmitHandlers.has(formSelector)) {
|
|
518
|
+
document.removeEventListener('submit', this.#formSubmitHandlers.get(formSelector))
|
|
519
|
+
this.#formSubmitHandlers.delete(formSelector)
|
|
520
|
+
}
|
|
521
|
+
const newObj = {...obj, form: `form[data-odac-form="${data.result._token}"]`}
|
|
522
|
+
this.form(newObj, callback)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
514
525
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
526
|
+
|
|
527
|
+
if (obj.clear !== false && formElement.getAttribute('clear') !== 'false' && !data.result.redirect) {
|
|
528
|
+
formElement
|
|
529
|
+
.querySelectorAll(
|
|
530
|
+
'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([readonly]), textarea, select'
|
|
531
|
+
)
|
|
532
|
+
.forEach(el => {
|
|
533
|
+
if (el.type === 'checkbox' || el.type === 'radio') el.checked = false
|
|
534
|
+
else if (el.tagName === 'SELECT') el.selectedIndex = 0
|
|
535
|
+
else el.value = ''
|
|
521
536
|
})
|
|
522
|
-
|
|
537
|
+
}
|
|
523
538
|
} else if (!data.result.success && data.errors) {
|
|
524
539
|
Object.entries(data.errors).forEach(([name, message]) => {
|
|
525
540
|
if (message) {
|
|
@@ -537,10 +552,12 @@ if (typeof window !== 'undefined') {
|
|
|
537
552
|
errorEl.setAttribute('odac-form-error', name)
|
|
538
553
|
errorEl.innerHTML = this.textToHtml(message)
|
|
539
554
|
errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
540
|
-
|
|
555
|
+
|
|
541
556
|
if ((inputEl.type === 'checkbox' || inputEl.type === 'radio') && inputEl.id) {
|
|
542
557
|
const label = formElement.querySelector(`label[for="${inputEl.id}"]`)
|
|
543
|
-
label
|
|
558
|
+
label
|
|
559
|
+
? label.parentNode.insertBefore(errorEl, label.nextSibling)
|
|
560
|
+
: inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
544
561
|
} else {
|
|
545
562
|
inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
546
563
|
}
|
|
@@ -559,12 +576,19 @@ if (typeof window !== 'undefined') {
|
|
|
559
576
|
const inputEl = formElement.querySelector(`*[name="${name}"]`)
|
|
560
577
|
if (inputEl) {
|
|
561
578
|
if (inputEl.type !== 'checkbox' && inputEl.type !== 'radio') inputEl.style.borderColor = '#dc3545'
|
|
562
|
-
inputEl.addEventListener(
|
|
579
|
+
inputEl.addEventListener(
|
|
580
|
+
'focus',
|
|
581
|
+
function handler() {
|
|
563
582
|
inputEl.style.borderColor = ''
|
|
564
583
|
const errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
|
|
565
|
-
if (errorEl) {
|
|
584
|
+
if (errorEl) {
|
|
585
|
+
errorEl.style.display = 'none'
|
|
586
|
+
errorEl.textContent = ''
|
|
587
|
+
}
|
|
566
588
|
inputEl.removeEventListener('focus', handler)
|
|
567
|
-
|
|
589
|
+
},
|
|
590
|
+
{once: true}
|
|
591
|
+
)
|
|
568
592
|
}
|
|
569
593
|
})
|
|
570
594
|
}
|
|
@@ -577,10 +601,14 @@ if (typeof window !== 'undefined') {
|
|
|
577
601
|
}
|
|
578
602
|
},
|
|
579
603
|
xhr: () => {
|
|
580
|
-
|
|
581
|
-
xhr.upload.addEventListener(
|
|
582
|
-
|
|
583
|
-
|
|
604
|
+
const xhr = new window.XMLHttpRequest()
|
|
605
|
+
xhr.upload.addEventListener(
|
|
606
|
+
'progress',
|
|
607
|
+
evt => {
|
|
608
|
+
if (evt.lengthComputable && obj.loading) obj.loading(parseInt((100 / evt.total) * evt.loaded))
|
|
609
|
+
},
|
|
610
|
+
false
|
|
611
|
+
)
|
|
584
612
|
return xhr
|
|
585
613
|
},
|
|
586
614
|
error: () => console.error('Odac:', 'Request failed', '\nForm: ' + obj.form + '\nRequest: ' + formElement.getAttribute('action')),
|
|
@@ -588,35 +616,38 @@ if (typeof window !== 'undefined') {
|
|
|
588
616
|
submitButtons.forEach(btn => {
|
|
589
617
|
btn.disabled = false
|
|
590
618
|
const originalText = btn.getAttribute('data-original-text')
|
|
591
|
-
if (originalText) {
|
|
619
|
+
if (originalText) {
|
|
620
|
+
btn.textContent = originalText
|
|
621
|
+
btn.removeAttribute('data-original-text')
|
|
622
|
+
}
|
|
592
623
|
})
|
|
593
624
|
formElement.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach(el => (el.disabled = false))
|
|
594
625
|
}
|
|
595
626
|
})
|
|
596
627
|
}
|
|
597
|
-
|
|
628
|
+
|
|
598
629
|
document.addEventListener('submit', handler)
|
|
599
630
|
this.#formSubmitHandlers.set(formSelector, handler)
|
|
600
631
|
}
|
|
601
|
-
|
|
632
|
+
|
|
602
633
|
get(url, callback) {
|
|
603
634
|
url = url + '?_token=' + this.token()
|
|
604
635
|
this.#ajax({url: url, success: callback})
|
|
605
636
|
}
|
|
606
|
-
|
|
637
|
+
|
|
607
638
|
page() {
|
|
608
639
|
if (!this.#page) {
|
|
609
640
|
this.#page = document.documentElement.dataset.odacPage || ''
|
|
610
641
|
}
|
|
611
642
|
return this.#page
|
|
612
643
|
}
|
|
613
|
-
|
|
644
|
+
|
|
614
645
|
storage(key, value) {
|
|
615
646
|
if (value === undefined) return localStorage.getItem(key)
|
|
616
647
|
else if (value === null) return localStorage.removeItem(key)
|
|
617
648
|
else localStorage.setItem(key, value)
|
|
618
649
|
}
|
|
619
|
-
|
|
650
|
+
|
|
620
651
|
token() {
|
|
621
652
|
if (!this.#token.listener) {
|
|
622
653
|
document.addEventListener('odac:ajaxSuccess', event => {
|
|
@@ -625,8 +656,8 @@ if (typeof window !== 'undefined') {
|
|
|
625
656
|
try {
|
|
626
657
|
const token = detail.xhr.getResponseHeader('X-Odac-Token')
|
|
627
658
|
if (token) {
|
|
628
|
-
|
|
629
|
-
|
|
659
|
+
this.#token.hash.push(token)
|
|
660
|
+
if (this.#token.hash.length > 2) this.#token.hash.shift()
|
|
630
661
|
}
|
|
631
662
|
} catch (e) {
|
|
632
663
|
console.error('Error in ajaxSuccess token handler:', e)
|
|
@@ -635,77 +666,87 @@ if (typeof window !== 'undefined') {
|
|
|
635
666
|
this.#token.listener = true
|
|
636
667
|
}
|
|
637
668
|
if (!this.#token.hash.length) {
|
|
638
|
-
|
|
669
|
+
const req = new XMLHttpRequest()
|
|
639
670
|
req.open('GET', '/', false)
|
|
640
671
|
req.setRequestHeader('X-Odac', 'token')
|
|
641
672
|
req.setRequestHeader('X-Odac-Client', this.client())
|
|
642
673
|
req.send(null)
|
|
643
|
-
|
|
674
|
+
const req_data = JSON.parse(req.response)
|
|
644
675
|
if (req_data.token) this.#token.hash.push(req_data.token)
|
|
645
676
|
}
|
|
646
|
-
this.#token.hash.filter(n => n)
|
|
647
|
-
|
|
677
|
+
this.#token.hash = this.#token.hash.filter(n => n)
|
|
678
|
+
const return_token = this.#token.hash.shift()
|
|
648
679
|
if (!this.#token.hash.length)
|
|
649
680
|
this.#ajax({
|
|
650
681
|
url: '/',
|
|
651
682
|
type: 'GET',
|
|
652
683
|
headers: {'X-Odac': 'token', 'X-Odac-Client': this.client()},
|
|
653
684
|
success: data => {
|
|
654
|
-
|
|
655
|
-
if (result.token) this.#token.hash.push(result.token)
|
|
685
|
+
if (data.token) this.#token.hash.push(data.token)
|
|
656
686
|
}
|
|
657
687
|
})
|
|
658
688
|
return return_token
|
|
659
689
|
}
|
|
660
|
-
|
|
690
|
+
|
|
661
691
|
textToHtml(str) {
|
|
662
692
|
if (typeof str !== 'string') return str
|
|
663
693
|
return str
|
|
664
|
-
.replace(/&/g, '&')
|
|
665
|
-
.replace(
|
|
694
|
+
.replace(/&/g, '&')
|
|
695
|
+
.replace(/</g, '<')
|
|
696
|
+
.replace(/>/g, '>')
|
|
697
|
+
.replace(/"/g, '"')
|
|
698
|
+
.replace(/'/g, ''')
|
|
699
|
+
.replace(/\n/g, '<br>')
|
|
666
700
|
}
|
|
667
|
-
|
|
701
|
+
|
|
668
702
|
load(url, callback, push = true) {
|
|
669
703
|
if (this.#isNavigating) return false
|
|
670
|
-
|
|
704
|
+
|
|
671
705
|
const currentUrl = window.location.href
|
|
672
706
|
url = new URL(url, currentUrl).href
|
|
673
|
-
if (url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.includes('#'))
|
|
674
|
-
|
|
707
|
+
if (url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.includes('#'))
|
|
708
|
+
return false
|
|
709
|
+
|
|
675
710
|
this.#isNavigating = true
|
|
676
|
-
|
|
711
|
+
|
|
677
712
|
const currentSkeleton = document.documentElement.dataset.odacSkeleton
|
|
678
713
|
const elements = Object.entries(this.#loader.elements)
|
|
679
|
-
|
|
714
|
+
|
|
680
715
|
const elementsToUpdate = []
|
|
681
716
|
elements.forEach(([key, selector]) => {
|
|
682
717
|
const element = document.querySelector(selector)
|
|
683
718
|
if (element) elementsToUpdate.push({key, element})
|
|
684
719
|
})
|
|
685
|
-
|
|
686
|
-
let ajaxData = null,
|
|
687
|
-
|
|
720
|
+
|
|
721
|
+
let ajaxData = null,
|
|
722
|
+
ajaxXhr = null,
|
|
723
|
+
fadeOutComplete = false,
|
|
724
|
+
ajaxComplete = false
|
|
725
|
+
|
|
688
726
|
const applyUpdate = () => {
|
|
689
727
|
if (!fadeOutComplete || !ajaxComplete || !ajaxData) return
|
|
690
|
-
|
|
728
|
+
|
|
691
729
|
const finalUrl = ajaxXhr.responseURL || url
|
|
692
|
-
if (ajaxData.skeletonChanged) {
|
|
730
|
+
if (ajaxData.skeletonChanged) {
|
|
731
|
+
window.location.href = finalUrl
|
|
732
|
+
return
|
|
733
|
+
}
|
|
693
734
|
if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
|
|
694
|
-
|
|
735
|
+
|
|
695
736
|
const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
|
|
696
737
|
if (newPage !== null) {
|
|
697
738
|
this.#page = newPage
|
|
698
739
|
document.documentElement.dataset.odacPage = newPage
|
|
699
740
|
}
|
|
700
|
-
|
|
741
|
+
|
|
701
742
|
if (ajaxData.data) this.#data = ajaxData.data
|
|
702
743
|
if (ajaxData.title) document.title = ajaxData.title
|
|
703
|
-
|
|
744
|
+
|
|
704
745
|
if (elementsToUpdate.length === 0) {
|
|
705
746
|
this.#handleLoadComplete(ajaxData, callback)
|
|
706
747
|
return
|
|
707
748
|
}
|
|
708
|
-
|
|
749
|
+
|
|
709
750
|
let completed = 0
|
|
710
751
|
elementsToUpdate.forEach(({key, element}) => {
|
|
711
752
|
if (ajaxData.output && ajaxData.output[key] !== undefined) element.innerHTML = ajaxData.output[key]
|
|
@@ -715,7 +756,7 @@ if (typeof window !== 'undefined') {
|
|
|
715
756
|
})
|
|
716
757
|
})
|
|
717
758
|
}
|
|
718
|
-
|
|
759
|
+
|
|
719
760
|
if (elementsToUpdate.length > 0) {
|
|
720
761
|
let fadeOutCount = 0
|
|
721
762
|
elementsToUpdate.forEach(({element}) => {
|
|
@@ -730,7 +771,7 @@ if (typeof window !== 'undefined') {
|
|
|
730
771
|
} else {
|
|
731
772
|
fadeOutComplete = true
|
|
732
773
|
}
|
|
733
|
-
|
|
774
|
+
|
|
734
775
|
this.#ajax({
|
|
735
776
|
url: url,
|
|
736
777
|
type: 'GET',
|
|
@@ -741,7 +782,10 @@ if (typeof window !== 'undefined') {
|
|
|
741
782
|
},
|
|
742
783
|
dataType: 'json',
|
|
743
784
|
success: (data, status, xhr) => {
|
|
744
|
-
ajaxData = data
|
|
785
|
+
ajaxData = data
|
|
786
|
+
ajaxXhr = xhr
|
|
787
|
+
ajaxComplete = true
|
|
788
|
+
applyUpdate()
|
|
745
789
|
},
|
|
746
790
|
error: () => {
|
|
747
791
|
this.#isNavigating = false
|
|
@@ -749,52 +793,72 @@ if (typeof window !== 'undefined') {
|
|
|
749
793
|
}
|
|
750
794
|
})
|
|
751
795
|
}
|
|
752
|
-
|
|
796
|
+
|
|
753
797
|
#handleLoadComplete(data, callback) {
|
|
754
|
-
if (this.actions.load)
|
|
755
|
-
|
|
756
|
-
|
|
798
|
+
if (this.actions.load)
|
|
799
|
+
(Array.isArray(this.actions.load) ? this.actions.load : [this.actions.load]).forEach(fn => fn(this.page(), data.variables))
|
|
800
|
+
if (this.actions.page && this.actions.page[this.page()])
|
|
801
|
+
(Array.isArray(this.actions.page[this.page()]) ? this.actions.page[this.page()] : [this.actions.page[this.page()]]).forEach(fn =>
|
|
802
|
+
fn(data.variables)
|
|
803
|
+
)
|
|
804
|
+
|
|
757
805
|
if (callback && typeof callback === 'function') callback(this.page(), data.variables)
|
|
758
|
-
|
|
806
|
+
|
|
759
807
|
window.scrollTo({top: 0, behavior: 'smooth'})
|
|
760
808
|
this.#isNavigating = false
|
|
761
809
|
}
|
|
762
|
-
|
|
810
|
+
|
|
763
811
|
loader(selector, elements, callback) {
|
|
764
812
|
this.#loader.elements = elements
|
|
765
813
|
this.#loader.callback = callback
|
|
766
814
|
const odacInstance = this
|
|
767
|
-
|
|
815
|
+
|
|
768
816
|
this.#on(document, 'click', selector, function (e) {
|
|
769
817
|
if (e.ctrlKey || e.metaKey) return
|
|
770
818
|
const anchor = this
|
|
771
819
|
if (!anchor) return
|
|
772
820
|
const url = anchor.getAttribute('href')
|
|
773
821
|
const target = anchor.getAttribute('target')
|
|
774
|
-
if (
|
|
822
|
+
if (
|
|
823
|
+
!url ||
|
|
824
|
+
url === '' ||
|
|
825
|
+
url.startsWith('javascript:') ||
|
|
826
|
+
url.startsWith('data:') ||
|
|
827
|
+
url.startsWith('vbscript:') ||
|
|
828
|
+
url.startsWith('#')
|
|
829
|
+
)
|
|
830
|
+
return
|
|
775
831
|
const isExternal = url.includes('://') && !url.includes(window.location.host)
|
|
776
832
|
if ((target === null || target === '_self') && !isExternal) {
|
|
777
833
|
e.preventDefault()
|
|
778
834
|
odacInstance.load(url, callback)
|
|
779
835
|
}
|
|
780
836
|
})
|
|
781
|
-
|
|
837
|
+
|
|
782
838
|
window.addEventListener('popstate', () => {
|
|
783
839
|
this.load(window.location.href, callback, false)
|
|
784
840
|
})
|
|
785
841
|
}
|
|
786
|
-
|
|
842
|
+
|
|
787
843
|
listen(url, onMessage, options = {}) {
|
|
788
844
|
const {onError = null, onOpen = null, autoReconnect = false, reconnectDelay = 3000} = options
|
|
789
|
-
let eventSource = null,
|
|
790
|
-
|
|
845
|
+
let eventSource = null,
|
|
846
|
+
reconnectTimer = null,
|
|
847
|
+
isClosed = false
|
|
848
|
+
|
|
791
849
|
const connect = () => {
|
|
792
850
|
if (isClosed) return
|
|
793
851
|
const urlWithToken = url + (url.includes('?') ? '&' : '?') + '_token=' + encodeURIComponent(this.token())
|
|
794
852
|
eventSource = new EventSource(urlWithToken)
|
|
795
|
-
eventSource.onopen = e => {
|
|
853
|
+
eventSource.onopen = e => {
|
|
854
|
+
if (onOpen) onOpen(e)
|
|
855
|
+
}
|
|
796
856
|
eventSource.onmessage = e => {
|
|
797
|
-
try {
|
|
857
|
+
try {
|
|
858
|
+
onMessage(JSON.parse(e.data))
|
|
859
|
+
} catch {
|
|
860
|
+
onMessage(e.data)
|
|
861
|
+
}
|
|
798
862
|
}
|
|
799
863
|
eventSource.onerror = e => {
|
|
800
864
|
if (onError) onError(e)
|
|
@@ -805,24 +869,26 @@ if (typeof window !== 'undefined') {
|
|
|
805
869
|
}
|
|
806
870
|
}
|
|
807
871
|
connect()
|
|
808
|
-
|
|
872
|
+
|
|
809
873
|
return {
|
|
810
874
|
close: () => {
|
|
811
875
|
isClosed = true
|
|
812
876
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
813
877
|
if (eventSource) eventSource.close()
|
|
814
878
|
},
|
|
815
|
-
send: () => {
|
|
879
|
+
send: () => {
|
|
880
|
+
throw new Error('SSE is one-way. Use POST requests to send data.')
|
|
881
|
+
}
|
|
816
882
|
}
|
|
817
883
|
}
|
|
818
|
-
|
|
884
|
+
|
|
819
885
|
ws(path, options = {}) {
|
|
820
886
|
const {autoReconnect = true, reconnectDelay = 3000, maxReconnectAttempts = 10, shared = false, token = true} = options
|
|
821
|
-
|
|
887
|
+
|
|
822
888
|
if (shared && typeof SharedWorker !== 'undefined') {
|
|
823
889
|
return this.#createSharedWebSocket(path, {autoReconnect, reconnectDelay, maxReconnectAttempts, token})
|
|
824
890
|
}
|
|
825
|
-
|
|
891
|
+
|
|
826
892
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
827
893
|
const wsUrl = `${protocol}//${window.location.host}${path}`
|
|
828
894
|
const protocols = []
|
|
@@ -830,10 +896,10 @@ if (typeof window !== 'undefined') {
|
|
|
830
896
|
const csrfToken = this.token()
|
|
831
897
|
if (csrfToken) protocols.push(`odac-token-${csrfToken}`)
|
|
832
898
|
}
|
|
833
|
-
|
|
899
|
+
|
|
834
900
|
return new OdacWebSocket(wsUrl, protocols, options)
|
|
835
901
|
}
|
|
836
|
-
|
|
902
|
+
|
|
837
903
|
#createSharedWebSocket(path, options) {
|
|
838
904
|
const scriptUrl = (() => {
|
|
839
905
|
if (document.currentScript) return document.currentScript.src
|
|
@@ -846,11 +912,11 @@ if (typeof window !== 'undefined') {
|
|
|
846
912
|
const worker = new SharedWorker(scriptUrl, `odac-ws-${path}`)
|
|
847
913
|
const handlers = {}
|
|
848
914
|
let isConnected = false
|
|
849
|
-
|
|
915
|
+
|
|
850
916
|
const emit = (event, ...args) => {
|
|
851
917
|
if (handlers[event]) handlers[event].forEach(fn => fn(...args))
|
|
852
918
|
}
|
|
853
|
-
|
|
919
|
+
|
|
854
920
|
worker.port.onmessage = e => {
|
|
855
921
|
const {type, data} = e.data
|
|
856
922
|
switch (type) {
|
|
@@ -870,11 +936,11 @@ if (typeof window !== 'undefined') {
|
|
|
870
936
|
break
|
|
871
937
|
}
|
|
872
938
|
}
|
|
873
|
-
|
|
939
|
+
|
|
874
940
|
worker.port.start()
|
|
875
|
-
|
|
941
|
+
|
|
876
942
|
const token = options.token ? this.token() : null
|
|
877
|
-
|
|
943
|
+
|
|
878
944
|
worker.port.postMessage({
|
|
879
945
|
type: 'connect',
|
|
880
946
|
path,
|
|
@@ -883,7 +949,7 @@ if (typeof window !== 'undefined') {
|
|
|
883
949
|
token,
|
|
884
950
|
options
|
|
885
951
|
})
|
|
886
|
-
|
|
952
|
+
|
|
887
953
|
return {
|
|
888
954
|
on: (event, handler) => {
|
|
889
955
|
if (!handlers[event]) handlers[event] = []
|
|
@@ -916,9 +982,8 @@ if (typeof window !== 'undefined') {
|
|
|
916
982
|
}
|
|
917
983
|
}
|
|
918
984
|
}
|
|
919
|
-
|
|
985
|
+
|
|
920
986
|
window.Odac = new _odac()
|
|
921
|
-
|
|
922
987
|
;(function initAutoNavigate() {
|
|
923
988
|
const init = () => {
|
|
924
989
|
const contentEl = document.querySelector('[data-odac-navigate="content"]')
|
|
@@ -928,9 +993,9 @@ if (typeof window !== 'undefined') {
|
|
|
928
993
|
}
|
|
929
994
|
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
|
|
930
995
|
})()
|
|
931
|
-
|
|
996
|
+
|
|
932
997
|
document.addEventListener('DOMContentLoaded', () => {
|
|
933
|
-
['register', 'login'].forEach(type => {
|
|
998
|
+
;['register', 'login'].forEach(type => {
|
|
934
999
|
document.querySelectorAll(`form.odac-${type}-form[data-odac-${type}]`).forEach(form => {
|
|
935
1000
|
const token = form.getAttribute(`data-odac-${type}`)
|
|
936
1001
|
window.Odac.form({form: `form[data-odac-${type}="${token}"]`})
|
|
@@ -965,8 +1030,8 @@ if (typeof window !== 'undefined') {
|
|
|
965
1030
|
socket.on('close', e => broadcast('close', {code: e?.code, reason: e?.reason, wasClean: e?.wasClean}))
|
|
966
1031
|
socket.on('error', e => broadcast('error', {message: e?.message || 'WebSocket error'}))
|
|
967
1032
|
} else if (socket.connected) {
|
|
968
|
-
|
|
969
|
-
|
|
1033
|
+
// If already connected, notify the new port immediately
|
|
1034
|
+
port.postMessage({type: 'open'})
|
|
970
1035
|
}
|
|
971
1036
|
break
|
|
972
1037
|
case 'send':
|