odac 1.2.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/memory.md +10 -1
- package/.github/workflows/release.yml +27 -1
- package/.husky/pre-push +3 -3
- package/.releaserc.js +2 -2
- package/CHANGELOG.md +32 -0
- package/bin/odac.js +22 -6
- package/client/odac.js +228 -167
- package/docs/backend/04-routing/09-websocket.md +14 -1
- package/package.json +13 -13
- 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.js +96 -29
- package/src/Validator.js +23 -14
- package/src/View.js +7 -7
- package/src/WebSocket.js +45 -12
- package/test/Odac.test.js +4 -2
- package/test/Route.test.js +104 -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,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,32 +500,36 @@ 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) {
|
|
@@ -537,10 +547,12 @@ if (typeof window !== 'undefined') {
|
|
|
537
547
|
errorEl.setAttribute('odac-form-error', name)
|
|
538
548
|
errorEl.innerHTML = this.textToHtml(message)
|
|
539
549
|
errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
540
|
-
|
|
550
|
+
|
|
541
551
|
if ((inputEl.type === 'checkbox' || inputEl.type === 'radio') && inputEl.id) {
|
|
542
552
|
const label = formElement.querySelector(`label[for="${inputEl.id}"]`)
|
|
543
|
-
label
|
|
553
|
+
label
|
|
554
|
+
? label.parentNode.insertBefore(errorEl, label.nextSibling)
|
|
555
|
+
: inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
544
556
|
} else {
|
|
545
557
|
inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
546
558
|
}
|
|
@@ -559,12 +571,19 @@ if (typeof window !== 'undefined') {
|
|
|
559
571
|
const inputEl = formElement.querySelector(`*[name="${name}"]`)
|
|
560
572
|
if (inputEl) {
|
|
561
573
|
if (inputEl.type !== 'checkbox' && inputEl.type !== 'radio') inputEl.style.borderColor = '#dc3545'
|
|
562
|
-
inputEl.addEventListener(
|
|
574
|
+
inputEl.addEventListener(
|
|
575
|
+
'focus',
|
|
576
|
+
function handler() {
|
|
563
577
|
inputEl.style.borderColor = ''
|
|
564
578
|
const errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
|
|
565
|
-
if (errorEl) {
|
|
579
|
+
if (errorEl) {
|
|
580
|
+
errorEl.style.display = 'none'
|
|
581
|
+
errorEl.textContent = ''
|
|
582
|
+
}
|
|
566
583
|
inputEl.removeEventListener('focus', handler)
|
|
567
|
-
|
|
584
|
+
},
|
|
585
|
+
{once: true}
|
|
586
|
+
)
|
|
568
587
|
}
|
|
569
588
|
})
|
|
570
589
|
}
|
|
@@ -578,9 +597,13 @@ if (typeof window !== 'undefined') {
|
|
|
578
597
|
},
|
|
579
598
|
xhr: () => {
|
|
580
599
|
var xhr = new window.XMLHttpRequest()
|
|
581
|
-
xhr.upload.addEventListener(
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
)
|
|
584
607
|
return xhr
|
|
585
608
|
},
|
|
586
609
|
error: () => console.error('Odac:', 'Request failed', '\nForm: ' + obj.form + '\nRequest: ' + formElement.getAttribute('action')),
|
|
@@ -588,35 +611,38 @@ if (typeof window !== 'undefined') {
|
|
|
588
611
|
submitButtons.forEach(btn => {
|
|
589
612
|
btn.disabled = false
|
|
590
613
|
const originalText = btn.getAttribute('data-original-text')
|
|
591
|
-
if (originalText) {
|
|
614
|
+
if (originalText) {
|
|
615
|
+
btn.textContent = originalText
|
|
616
|
+
btn.removeAttribute('data-original-text')
|
|
617
|
+
}
|
|
592
618
|
})
|
|
593
619
|
formElement.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach(el => (el.disabled = false))
|
|
594
620
|
}
|
|
595
621
|
})
|
|
596
622
|
}
|
|
597
|
-
|
|
623
|
+
|
|
598
624
|
document.addEventListener('submit', handler)
|
|
599
625
|
this.#formSubmitHandlers.set(formSelector, handler)
|
|
600
626
|
}
|
|
601
|
-
|
|
627
|
+
|
|
602
628
|
get(url, callback) {
|
|
603
629
|
url = url + '?_token=' + this.token()
|
|
604
630
|
this.#ajax({url: url, success: callback})
|
|
605
631
|
}
|
|
606
|
-
|
|
632
|
+
|
|
607
633
|
page() {
|
|
608
634
|
if (!this.#page) {
|
|
609
635
|
this.#page = document.documentElement.dataset.odacPage || ''
|
|
610
636
|
}
|
|
611
637
|
return this.#page
|
|
612
638
|
}
|
|
613
|
-
|
|
639
|
+
|
|
614
640
|
storage(key, value) {
|
|
615
641
|
if (value === undefined) return localStorage.getItem(key)
|
|
616
642
|
else if (value === null) return localStorage.removeItem(key)
|
|
617
643
|
else localStorage.setItem(key, value)
|
|
618
644
|
}
|
|
619
|
-
|
|
645
|
+
|
|
620
646
|
token() {
|
|
621
647
|
if (!this.#token.listener) {
|
|
622
648
|
document.addEventListener('odac:ajaxSuccess', event => {
|
|
@@ -625,8 +651,8 @@ if (typeof window !== 'undefined') {
|
|
|
625
651
|
try {
|
|
626
652
|
const token = detail.xhr.getResponseHeader('X-Odac-Token')
|
|
627
653
|
if (token) {
|
|
628
|
-
|
|
629
|
-
|
|
654
|
+
this.#token.hash.push(token)
|
|
655
|
+
if (this.#token.hash.length > 2) this.#token.hash.shift()
|
|
630
656
|
}
|
|
631
657
|
} catch (e) {
|
|
632
658
|
console.error('Error in ajaxSuccess token handler:', e)
|
|
@@ -657,55 +683,66 @@ if (typeof window !== 'undefined') {
|
|
|
657
683
|
})
|
|
658
684
|
return return_token
|
|
659
685
|
}
|
|
660
|
-
|
|
686
|
+
|
|
661
687
|
textToHtml(str) {
|
|
662
688
|
if (typeof str !== 'string') return str
|
|
663
689
|
return str
|
|
664
|
-
.replace(/&/g, '&')
|
|
665
|
-
.replace(
|
|
690
|
+
.replace(/&/g, '&')
|
|
691
|
+
.replace(/</g, '<')
|
|
692
|
+
.replace(/>/g, '>')
|
|
693
|
+
.replace(/"/g, '"')
|
|
694
|
+
.replace(/'/g, ''')
|
|
695
|
+
.replace(/\n/g, '<br>')
|
|
666
696
|
}
|
|
667
|
-
|
|
697
|
+
|
|
668
698
|
load(url, callback, push = true) {
|
|
669
699
|
if (this.#isNavigating) return false
|
|
670
|
-
|
|
700
|
+
|
|
671
701
|
const currentUrl = window.location.href
|
|
672
702
|
url = new URL(url, currentUrl).href
|
|
673
|
-
if (url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.includes('#'))
|
|
674
|
-
|
|
703
|
+
if (url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.includes('#'))
|
|
704
|
+
return false
|
|
705
|
+
|
|
675
706
|
this.#isNavigating = true
|
|
676
|
-
|
|
707
|
+
|
|
677
708
|
const currentSkeleton = document.documentElement.dataset.odacSkeleton
|
|
678
709
|
const elements = Object.entries(this.#loader.elements)
|
|
679
|
-
|
|
710
|
+
|
|
680
711
|
const elementsToUpdate = []
|
|
681
712
|
elements.forEach(([key, selector]) => {
|
|
682
713
|
const element = document.querySelector(selector)
|
|
683
714
|
if (element) elementsToUpdate.push({key, element})
|
|
684
715
|
})
|
|
685
|
-
|
|
686
|
-
let ajaxData = null,
|
|
687
|
-
|
|
716
|
+
|
|
717
|
+
let ajaxData = null,
|
|
718
|
+
ajaxXhr = null,
|
|
719
|
+
fadeOutComplete = false,
|
|
720
|
+
ajaxComplete = false
|
|
721
|
+
|
|
688
722
|
const applyUpdate = () => {
|
|
689
723
|
if (!fadeOutComplete || !ajaxComplete || !ajaxData) return
|
|
690
|
-
|
|
724
|
+
|
|
691
725
|
const finalUrl = ajaxXhr.responseURL || url
|
|
692
|
-
if (ajaxData.skeletonChanged) {
|
|
726
|
+
if (ajaxData.skeletonChanged) {
|
|
727
|
+
window.location.href = finalUrl
|
|
728
|
+
return
|
|
729
|
+
}
|
|
693
730
|
if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
|
|
694
|
-
|
|
731
|
+
|
|
695
732
|
const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
|
|
696
733
|
if (newPage !== null) {
|
|
697
734
|
this.#page = newPage
|
|
698
735
|
document.documentElement.dataset.odacPage = newPage
|
|
699
736
|
}
|
|
700
|
-
|
|
737
|
+
|
|
701
738
|
if (ajaxData.data) this.#data = ajaxData.data
|
|
702
739
|
if (ajaxData.title) document.title = ajaxData.title
|
|
703
|
-
|
|
740
|
+
|
|
704
741
|
if (elementsToUpdate.length === 0) {
|
|
705
742
|
this.#handleLoadComplete(ajaxData, callback)
|
|
706
743
|
return
|
|
707
744
|
}
|
|
708
|
-
|
|
745
|
+
|
|
709
746
|
let completed = 0
|
|
710
747
|
elementsToUpdate.forEach(({key, element}) => {
|
|
711
748
|
if (ajaxData.output && ajaxData.output[key] !== undefined) element.innerHTML = ajaxData.output[key]
|
|
@@ -715,7 +752,7 @@ if (typeof window !== 'undefined') {
|
|
|
715
752
|
})
|
|
716
753
|
})
|
|
717
754
|
}
|
|
718
|
-
|
|
755
|
+
|
|
719
756
|
if (elementsToUpdate.length > 0) {
|
|
720
757
|
let fadeOutCount = 0
|
|
721
758
|
elementsToUpdate.forEach(({element}) => {
|
|
@@ -730,7 +767,7 @@ if (typeof window !== 'undefined') {
|
|
|
730
767
|
} else {
|
|
731
768
|
fadeOutComplete = true
|
|
732
769
|
}
|
|
733
|
-
|
|
770
|
+
|
|
734
771
|
this.#ajax({
|
|
735
772
|
url: url,
|
|
736
773
|
type: 'GET',
|
|
@@ -741,7 +778,10 @@ if (typeof window !== 'undefined') {
|
|
|
741
778
|
},
|
|
742
779
|
dataType: 'json',
|
|
743
780
|
success: (data, status, xhr) => {
|
|
744
|
-
ajaxData = data
|
|
781
|
+
ajaxData = data
|
|
782
|
+
ajaxXhr = xhr
|
|
783
|
+
ajaxComplete = true
|
|
784
|
+
applyUpdate()
|
|
745
785
|
},
|
|
746
786
|
error: () => {
|
|
747
787
|
this.#isNavigating = false
|
|
@@ -749,52 +789,72 @@ if (typeof window !== 'undefined') {
|
|
|
749
789
|
}
|
|
750
790
|
})
|
|
751
791
|
}
|
|
752
|
-
|
|
792
|
+
|
|
753
793
|
#handleLoadComplete(data, callback) {
|
|
754
|
-
if (this.actions.load)
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
|
|
757
801
|
if (callback && typeof callback === 'function') callback(this.page(), data.variables)
|
|
758
|
-
|
|
802
|
+
|
|
759
803
|
window.scrollTo({top: 0, behavior: 'smooth'})
|
|
760
804
|
this.#isNavigating = false
|
|
761
805
|
}
|
|
762
|
-
|
|
806
|
+
|
|
763
807
|
loader(selector, elements, callback) {
|
|
764
808
|
this.#loader.elements = elements
|
|
765
809
|
this.#loader.callback = callback
|
|
766
810
|
const odacInstance = this
|
|
767
|
-
|
|
811
|
+
|
|
768
812
|
this.#on(document, 'click', selector, function (e) {
|
|
769
813
|
if (e.ctrlKey || e.metaKey) return
|
|
770
814
|
const anchor = this
|
|
771
815
|
if (!anchor) return
|
|
772
816
|
const url = anchor.getAttribute('href')
|
|
773
817
|
const target = anchor.getAttribute('target')
|
|
774
|
-
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
|
|
775
827
|
const isExternal = url.includes('://') && !url.includes(window.location.host)
|
|
776
828
|
if ((target === null || target === '_self') && !isExternal) {
|
|
777
829
|
e.preventDefault()
|
|
778
830
|
odacInstance.load(url, callback)
|
|
779
831
|
}
|
|
780
832
|
})
|
|
781
|
-
|
|
833
|
+
|
|
782
834
|
window.addEventListener('popstate', () => {
|
|
783
835
|
this.load(window.location.href, callback, false)
|
|
784
836
|
})
|
|
785
837
|
}
|
|
786
|
-
|
|
838
|
+
|
|
787
839
|
listen(url, onMessage, options = {}) {
|
|
788
840
|
const {onError = null, onOpen = null, autoReconnect = false, reconnectDelay = 3000} = options
|
|
789
|
-
let eventSource = null,
|
|
790
|
-
|
|
841
|
+
let eventSource = null,
|
|
842
|
+
reconnectTimer = null,
|
|
843
|
+
isClosed = false
|
|
844
|
+
|
|
791
845
|
const connect = () => {
|
|
792
846
|
if (isClosed) return
|
|
793
847
|
const urlWithToken = url + (url.includes('?') ? '&' : '?') + '_token=' + encodeURIComponent(this.token())
|
|
794
848
|
eventSource = new EventSource(urlWithToken)
|
|
795
|
-
eventSource.onopen = e => {
|
|
849
|
+
eventSource.onopen = e => {
|
|
850
|
+
if (onOpen) onOpen(e)
|
|
851
|
+
}
|
|
796
852
|
eventSource.onmessage = e => {
|
|
797
|
-
try {
|
|
853
|
+
try {
|
|
854
|
+
onMessage(JSON.parse(e.data))
|
|
855
|
+
} catch {
|
|
856
|
+
onMessage(e.data)
|
|
857
|
+
}
|
|
798
858
|
}
|
|
799
859
|
eventSource.onerror = e => {
|
|
800
860
|
if (onError) onError(e)
|
|
@@ -805,24 +865,26 @@ if (typeof window !== 'undefined') {
|
|
|
805
865
|
}
|
|
806
866
|
}
|
|
807
867
|
connect()
|
|
808
|
-
|
|
868
|
+
|
|
809
869
|
return {
|
|
810
870
|
close: () => {
|
|
811
871
|
isClosed = true
|
|
812
872
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
813
873
|
if (eventSource) eventSource.close()
|
|
814
874
|
},
|
|
815
|
-
send: () => {
|
|
875
|
+
send: () => {
|
|
876
|
+
throw new Error('SSE is one-way. Use POST requests to send data.')
|
|
877
|
+
}
|
|
816
878
|
}
|
|
817
879
|
}
|
|
818
|
-
|
|
880
|
+
|
|
819
881
|
ws(path, options = {}) {
|
|
820
882
|
const {autoReconnect = true, reconnectDelay = 3000, maxReconnectAttempts = 10, shared = false, token = true} = options
|
|
821
|
-
|
|
883
|
+
|
|
822
884
|
if (shared && typeof SharedWorker !== 'undefined') {
|
|
823
885
|
return this.#createSharedWebSocket(path, {autoReconnect, reconnectDelay, maxReconnectAttempts, token})
|
|
824
886
|
}
|
|
825
|
-
|
|
887
|
+
|
|
826
888
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
827
889
|
const wsUrl = `${protocol}//${window.location.host}${path}`
|
|
828
890
|
const protocols = []
|
|
@@ -830,10 +892,10 @@ if (typeof window !== 'undefined') {
|
|
|
830
892
|
const csrfToken = this.token()
|
|
831
893
|
if (csrfToken) protocols.push(`odac-token-${csrfToken}`)
|
|
832
894
|
}
|
|
833
|
-
|
|
895
|
+
|
|
834
896
|
return new OdacWebSocket(wsUrl, protocols, options)
|
|
835
897
|
}
|
|
836
|
-
|
|
898
|
+
|
|
837
899
|
#createSharedWebSocket(path, options) {
|
|
838
900
|
const scriptUrl = (() => {
|
|
839
901
|
if (document.currentScript) return document.currentScript.src
|
|
@@ -846,11 +908,11 @@ if (typeof window !== 'undefined') {
|
|
|
846
908
|
const worker = new SharedWorker(scriptUrl, `odac-ws-${path}`)
|
|
847
909
|
const handlers = {}
|
|
848
910
|
let isConnected = false
|
|
849
|
-
|
|
911
|
+
|
|
850
912
|
const emit = (event, ...args) => {
|
|
851
913
|
if (handlers[event]) handlers[event].forEach(fn => fn(...args))
|
|
852
914
|
}
|
|
853
|
-
|
|
915
|
+
|
|
854
916
|
worker.port.onmessage = e => {
|
|
855
917
|
const {type, data} = e.data
|
|
856
918
|
switch (type) {
|
|
@@ -870,11 +932,11 @@ if (typeof window !== 'undefined') {
|
|
|
870
932
|
break
|
|
871
933
|
}
|
|
872
934
|
}
|
|
873
|
-
|
|
935
|
+
|
|
874
936
|
worker.port.start()
|
|
875
|
-
|
|
937
|
+
|
|
876
938
|
const token = options.token ? this.token() : null
|
|
877
|
-
|
|
939
|
+
|
|
878
940
|
worker.port.postMessage({
|
|
879
941
|
type: 'connect',
|
|
880
942
|
path,
|
|
@@ -883,7 +945,7 @@ if (typeof window !== 'undefined') {
|
|
|
883
945
|
token,
|
|
884
946
|
options
|
|
885
947
|
})
|
|
886
|
-
|
|
948
|
+
|
|
887
949
|
return {
|
|
888
950
|
on: (event, handler) => {
|
|
889
951
|
if (!handlers[event]) handlers[event] = []
|
|
@@ -916,9 +978,8 @@ if (typeof window !== 'undefined') {
|
|
|
916
978
|
}
|
|
917
979
|
}
|
|
918
980
|
}
|
|
919
|
-
|
|
981
|
+
|
|
920
982
|
window.Odac = new _odac()
|
|
921
|
-
|
|
922
983
|
;(function initAutoNavigate() {
|
|
923
984
|
const init = () => {
|
|
924
985
|
const contentEl = document.querySelector('[data-odac-navigate="content"]')
|
|
@@ -928,9 +989,9 @@ if (typeof window !== 'undefined') {
|
|
|
928
989
|
}
|
|
929
990
|
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
|
|
930
991
|
})()
|
|
931
|
-
|
|
992
|
+
|
|
932
993
|
document.addEventListener('DOMContentLoaded', () => {
|
|
933
|
-
['register', 'login'].forEach(type => {
|
|
994
|
+
;['register', 'login'].forEach(type => {
|
|
934
995
|
document.querySelectorAll(`form.odac-${type}-form[data-odac-${type}]`).forEach(form => {
|
|
935
996
|
const token = form.getAttribute(`data-odac-${type}`)
|
|
936
997
|
window.Odac.form({form: `form[data-odac-${type}="${token}"]`})
|
|
@@ -965,8 +1026,8 @@ if (typeof window !== 'undefined') {
|
|
|
965
1026
|
socket.on('close', e => broadcast('close', {code: e?.code, reason: e?.reason, wasClean: e?.wasClean}))
|
|
966
1027
|
socket.on('error', e => broadcast('error', {message: e?.message || 'WebSocket error'}))
|
|
967
1028
|
} else if (socket.connected) {
|
|
968
|
-
|
|
969
|
-
|
|
1029
|
+
// If already connected, notify the new port immediately
|
|
1030
|
+
port.postMessage({type: 'open'})
|
|
970
1031
|
}
|
|
971
1032
|
break
|
|
972
1033
|
case 'send':
|