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/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 ? label.parentNode.insertBefore(errorSpan, label.nextSibling) : input.parentNode.insertBefore(errorSpan, input.nextSibling)
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) { showError(input, 'required'); break; }
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'); break;
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'); break;
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())) { showError(input, 'pattern'); break; }
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'); break;
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; contentType = false; processData = 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; contentType = 'application/x-www-form-urlencoded; charset=UTF-8'; processData = 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
- const tokenInput = formElement.querySelector('input[name="_odac_form_token"]')
500
- if (tokenInput) tokenInput.value = data.result._token
501
-
502
- const formTokenAttr = formElement.getAttribute('data-odac-form')
503
- if (formTokenAttr) {
504
- formElement.setAttribute('data-odac-form', data.result._token)
505
- if (!formElement.matches(formSelector)) {
506
- if (this.#formSubmitHandlers.has(formSelector)) {
507
- document.removeEventListener('submit', this.#formSubmitHandlers.get(formSelector))
508
- this.#formSubmitHandlers.delete(formSelector)
509
- }
510
- const newObj = {...obj, form: `form[data-odac-form="${data.result._token}"]`}
511
- this.form(newObj, callback)
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
- if (obj.clear !== false && formElement.getAttribute('clear') !== 'false' && !data.result.redirect) {
517
- formElement.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([readonly]), textarea, select').forEach(el => {
518
- if (el.type === 'checkbox' || el.type === 'radio') el.checked = false
519
- else if (el.tagName === 'SELECT') el.selectedIndex = 0
520
- else el.value = ''
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 ? label.parentNode.insertBefore(errorEl, label.nextSibling) : inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
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('focus', function handler() {
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) { errorEl.style.display = 'none'; errorEl.textContent = '' }
579
+ if (errorEl) {
580
+ errorEl.style.display = 'none'
581
+ errorEl.textContent = ''
582
+ }
566
583
  inputEl.removeEventListener('focus', handler)
567
- }, {once: true})
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('progress', evt => {
582
- if (evt.lengthComputable && obj.loading) obj.loading(parseInt((100 / evt.total) * evt.loaded))
583
- }, false)
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) { btn.textContent = originalText; btn.removeAttribute('data-original-text') }
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
- this.#token.hash.push(token)
629
- if (this.#token.hash.length > 2) this.#token.hash.shift()
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
665
- .replace(/"/g, '&quot;').replace(/'/g, '&#039;').replace(/\n/g, '<br>')
690
+ .replace(/&/g, '&amp;')
691
+ .replace(/</g, '&lt;')
692
+ .replace(/>/g, '&gt;')
693
+ .replace(/"/g, '&quot;')
694
+ .replace(/'/g, '&#039;')
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('#')) return false
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, ajaxXhr = null, fadeOutComplete = false, ajaxComplete = false
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) { window.location.href = finalUrl; return }
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; ajaxXhr = xhr; ajaxComplete = true; applyUpdate()
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) (Array.isArray(this.actions.load) ? this.actions.load : [this.actions.load]).forEach(fn => fn(this.page(), data.variables))
755
- if (this.actions.page && this.actions.page[this.page()]) (Array.isArray(this.actions.page[this.page()]) ? this.actions.page[this.page()] : [this.actions.page[this.page()]]).forEach(fn => fn(data.variables))
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 (!url || url === '' || url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('vbscript:') || url.startsWith('#')) return
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, reconnectTimer = null, isClosed = false
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 => { if (onOpen) onOpen(e) }
849
+ eventSource.onopen = e => {
850
+ if (onOpen) onOpen(e)
851
+ }
796
852
  eventSource.onmessage = e => {
797
- try { onMessage(JSON.parse(e.data)) } catch { onMessage(e.data) }
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: () => { throw new Error('SSE is one-way. Use POST requests to send data.') }
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
- // If already connected, notify the new port immediately
969
- port.postMessage({type: 'open'})
1029
+ // If already connected, notify the new port immediately
1030
+ port.postMessage({type: 'open'})
970
1031
  }
971
1032
  break
972
1033
  case 'send':