wick-dom-observer 1.0.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.
@@ -0,0 +1,847 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>clickSpinner test page</title>
8
+ <style>
9
+ :root {
10
+ --bg: #0b1220;
11
+ --bg-card: rgba(15, 23, 42, 0.88);
12
+ --text: #e2e8f0;
13
+ --muted: #94a3b8;
14
+ --primary: #6366f1;
15
+ --primary-2: #8b5cf6;
16
+ --accent: #22d3ee;
17
+ --ring: rgba(99, 102, 241, 0.45);
18
+ --border: rgba(148, 163, 184, 0.2);
19
+ --shadow: 0 10px 30px rgba(2, 6, 23, 0.45);
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ min-height: 100vh;
29
+ font-family: Inter, Segoe UI, Roboto, Arial, sans-serif;
30
+ color: var(--text);
31
+ background:
32
+ radial-gradient(circle at 10% 20%, rgba(34, 211, 238, 0.12), transparent 35%),
33
+ radial-gradient(circle at 90% 15%, rgba(139, 92, 246, 0.16), transparent 38%),
34
+ linear-gradient(160deg, #020617, #0f172a 55%, #111827);
35
+ display: grid;
36
+ place-items: center;
37
+ padding: 24px;
38
+ }
39
+
40
+ .page-grid {
41
+ width: min(1080px, 100%);
42
+ display: grid;
43
+ gap: 18px;
44
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
45
+ align-items: start;
46
+ }
47
+
48
+ .panel {
49
+ width: min(820px, 100%);
50
+ background: var(--bg-card);
51
+ border: 1px solid var(--border);
52
+ border-radius: 18px;
53
+ box-shadow: var(--shadow);
54
+ backdrop-filter: blur(6px);
55
+ overflow: hidden;
56
+ }
57
+
58
+ .panel-header {
59
+ padding: 20px 24px 10px;
60
+ border-bottom: 1px solid var(--border);
61
+ background: linear-gradient(180deg, rgba(99, 102, 241, 0.09), transparent);
62
+ }
63
+
64
+ .title {
65
+ margin: 0;
66
+ font-size: 1.2rem;
67
+ font-weight: 700;
68
+ letter-spacing: 0.2px;
69
+ }
70
+
71
+ .subtitle {
72
+ margin: 8px 0 0;
73
+ color: var(--muted);
74
+ font-size: 0.93rem;
75
+ }
76
+
77
+ .panel-body {
78
+ padding: 22px 30px 24px;
79
+ display: grid;
80
+ gap: 16px;
81
+ }
82
+
83
+ #spinnerPlaygroundBody {
84
+ position: relative;
85
+ }
86
+
87
+ .controls-group {
88
+ display: grid;
89
+ gap: 12px;
90
+ align-items: start;
91
+ }
92
+
93
+ .button-row {
94
+ display: flex;
95
+ flex-direction: column;
96
+ align-items: flex-start;
97
+ gap: 10px;
98
+ }
99
+
100
+ button {
101
+ border: 1px solid transparent;
102
+ border-radius: 12px;
103
+ font-size: 0.95rem;
104
+ font-weight: 600;
105
+ padding: 10px 14px;
106
+ color: #f8fafc;
107
+ background: linear-gradient(135deg, var(--primary), var(--primary-2));
108
+ cursor: pointer;
109
+ transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
110
+ box-shadow: 0 8px 18px rgba(79, 70, 229, 0.35);
111
+ }
112
+
113
+ button:hover {
114
+ transform: translateY(-1px);
115
+ box-shadow: 0 12px 20px rgba(79, 70, 229, 0.4);
116
+ }
117
+
118
+ button:focus-visible {
119
+ outline: none;
120
+ box-shadow: 0 0 0 3px var(--ring), 0 12px 20px rgba(79, 70, 229, 0.4);
121
+ }
122
+
123
+ #spinnerBtn5 {
124
+ background: linear-gradient(135deg, rgba(6, 95, 70, 0.95), rgba(22, 163, 74, 0.95));
125
+ }
126
+
127
+ #spinnerBtn5.is-loading {
128
+ background: linear-gradient(135deg, rgba(153, 27, 27, 0.95), rgba(220, 38, 38, 0.95));
129
+ }
130
+
131
+ #spinnerBtn6,
132
+ #toastBtn3 {
133
+ background: linear-gradient(135deg, #334155, #1f2937);
134
+ box-shadow: 0 8px 18px rgba(31, 41, 55, 0.4);
135
+ }
136
+
137
+ #toastBtn1 {
138
+ box-shadow: 0 8px 18px rgba(6, 78, 59, 0.55);
139
+ }
140
+
141
+ .canvas-wrap {
142
+ border: 1px solid var(--border);
143
+ border-radius: 14px;
144
+ background: rgba(2, 6, 23, 0.4);
145
+ padding: 12px;
146
+ width: fit-content;
147
+ }
148
+
149
+ #spinnerCanvas,
150
+ #toastCanvas {
151
+ display: block;
152
+ border-radius: 10px;
153
+ border: 1px solid rgba(148, 163, 184, 0.35);
154
+ background:
155
+ linear-gradient(135deg, rgba(56, 189, 248, 0.16), rgba(99, 102, 241, 0.16)),
156
+ #0f172a;
157
+ }
158
+
159
+ .hint {
160
+ margin: 0;
161
+ font-size: 0.86rem;
162
+ color: var(--muted);
163
+ }
164
+
165
+ .spinner-layer {
166
+ position: relative;
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 8px;
170
+ min-height: 46px;
171
+ align-items: flex-start;
172
+ pointer-events: none;
173
+ }
174
+
175
+ .toast-layer {
176
+ position: relative;
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: 8px;
180
+ min-height: 46px;
181
+ align-items: flex-end;
182
+ pointer-events: none;
183
+ }
184
+
185
+ .spinner {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: 8px;
189
+ padding: 10px 14px;
190
+ border-radius: 12px;
191
+ border: 1px solid rgba(251, 191, 36, 0.75);
192
+ color: #fff7ed;
193
+ background: linear-gradient(135deg, rgba(120, 53, 15, 0.95), rgba(194, 65, 12, 0.95));
194
+ box-shadow: 0 10px 24px rgba(194, 65, 12, 0.45);
195
+ font-size: 0.95rem;
196
+ font-weight: 700;
197
+ }
198
+
199
+ .spinner.spinner-removed {
200
+ color: #111827;
201
+ border-color: rgba(250, 204, 21, 0.9);
202
+ background: linear-gradient(135deg, rgba(250, 204, 21, 0.95), rgba(234, 179, 8, 0.95));
203
+ box-shadow: 0 10px 24px rgba(234, 179, 8, 0.45);
204
+ }
205
+
206
+ .spinner.spinner-removed::before {
207
+ border: 2px solid rgba(17, 24, 39, 0.25);
208
+ border-top-color: #111827;
209
+ }
210
+
211
+ .spinner.spinner-overlay {
212
+ position: absolute;
213
+ inset: 0;
214
+ width: 100%;
215
+ height: 100%;
216
+ justify-content: center;
217
+ align-items: center;
218
+ border: 0;
219
+ border-radius: 0;
220
+ background: transparent;
221
+ box-shadow: none;
222
+ text-indent: -9999px;
223
+ overflow: hidden;
224
+ pointer-events: none;
225
+ z-index: 40;
226
+ }
227
+
228
+ .spinner.spinner-overlay::before {
229
+ width: 130px;
230
+ height: 130px;
231
+ border: 0;
232
+ background: conic-gradient(
233
+ from 0deg,
234
+ rgba(250, 204, 21, 0.14) 0deg,
235
+ rgba(250, 204, 21, 0.24) 140deg,
236
+ #fde047 250deg,
237
+ #f59e0b 320deg,
238
+ rgba(250, 204, 21, 0.14) 360deg
239
+ );
240
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 6px));
241
+ mask: radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 6px));
242
+ animation: spin 900ms linear infinite;
243
+ }
244
+
245
+ .spinner.spinner-spokes-orange {
246
+ position: absolute;
247
+ inset: 0;
248
+ width: 100%;
249
+ height: 100%;
250
+ min-width: 0;
251
+ padding: 0;
252
+ gap: 0;
253
+ border: 0;
254
+ background: transparent;
255
+ box-shadow: none;
256
+ color: transparent;
257
+ text-indent: -9999px;
258
+ overflow: visible;
259
+ display: flex;
260
+ justify-content: center;
261
+ align-items: center;
262
+ z-index: 35;
263
+ pointer-events: none;
264
+ }
265
+
266
+ .spinner.spinner-spokes-orange::before {
267
+ content: "";
268
+ position: relative;
269
+ width: 220px;
270
+ height: 220px;
271
+ border-radius: 999px;
272
+ background: repeating-conic-gradient(
273
+ from -90deg,
274
+ rgba(251, 146, 60, 1) 0deg 16deg,
275
+ rgba(234, 88, 12, 0.22) 16deg 30deg
276
+ );
277
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 20px), #000 calc(100% - 19px));
278
+ mask: radial-gradient(farthest-side, transparent calc(100% - 20px), #000 calc(100% - 19px));
279
+ animation: spin 900ms linear infinite;
280
+ }
281
+
282
+ .spinner.spinner-inline {
283
+ padding: 4px 10px;
284
+ font-size: 0.82rem;
285
+ box-shadow: none;
286
+ border-color: rgba(251, 191, 36, 0.55);
287
+ min-width: auto;
288
+ }
289
+
290
+ .button-inline-spinner {
291
+ display: inline-block;
292
+ width: 16px;
293
+ height: 16px;
294
+ border-radius: 999px;
295
+ border: 2px solid rgba(255, 255, 255, 0.45);
296
+ border-top-color: #ffffff;
297
+ animation: spin 700ms linear infinite;
298
+ flex: 0 0 auto;
299
+ }
300
+
301
+ .toast {
302
+ display: inline-flex;
303
+ align-items: center;
304
+ gap: 8px;
305
+ padding: 10px 14px;
306
+ border-radius: 12px;
307
+ border: 1px solid rgba(110, 231, 183, 0.75);
308
+ color: #ecfdf5;
309
+ background: linear-gradient(135deg, rgba(6, 95, 70, 0.95), rgba(22, 163, 74, 0.95));
310
+ box-shadow: 0 10px 24px rgba(22, 163, 74, 0.45);
311
+ font-size: 0.95rem;
312
+ font-weight: 700;
313
+ min-width: 220px;
314
+ justify-content: space-between;
315
+ pointer-events: auto;
316
+ transform-origin: right center;
317
+ animation: toastIn 220ms ease-out;
318
+ }
319
+
320
+ .toast[data-from="toastBtn1"] {
321
+ box-shadow: 0 10px 24px rgba(6, 78, 59, 0.55);
322
+ }
323
+
324
+ .toast::before {
325
+ content: "";
326
+ width: 10px;
327
+ height: 10px;
328
+ border-radius: 999px;
329
+ background: #86efac;
330
+ box-shadow: 0 0 0 2px rgba(134, 239, 172, 0.2);
331
+ flex: 0 0 auto;
332
+ }
333
+
334
+ .toast-message {
335
+ flex: 1 1 auto;
336
+ min-width: 0;
337
+ white-space: nowrap;
338
+ overflow: hidden;
339
+ text-overflow: ellipsis;
340
+ }
341
+
342
+ .toast-close {
343
+ margin-left: 8px;
344
+ border: 0;
345
+ width: 22px;
346
+ height: 22px;
347
+ border-radius: 999px;
348
+ background: rgba(6, 78, 59, 0.8);
349
+ color: #ecfdf5;
350
+ font-size: 14px;
351
+ line-height: 1;
352
+ display: inline-flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ cursor: pointer;
356
+ box-shadow: none;
357
+ padding: 0;
358
+ }
359
+
360
+ .toast-close:hover {
361
+ transform: none;
362
+ box-shadow: none;
363
+ background: rgba(6, 78, 59, 1);
364
+ }
365
+
366
+ .toast-close:focus-visible {
367
+ box-shadow: 0 0 0 2px rgba(110, 231, 183, 0.5);
368
+ }
369
+
370
+ .toast-closing {
371
+ animation: toastOut 180ms ease-in forwards;
372
+ }
373
+
374
+ .toast-hidden {
375
+ display: none;
376
+ }
377
+
378
+ .spinner::before {
379
+ content: "";
380
+ width: 12px;
381
+ height: 12px;
382
+ border-radius: 999px;
383
+ border: 2px solid rgba(255, 237, 213, 0.45);
384
+ border-top-color: #fde68a;
385
+ animation: spin 700ms linear infinite;
386
+ flex: 0 0 auto;
387
+ }
388
+
389
+ .loading {
390
+ opacity: 1;
391
+ }
392
+
393
+ .spinner-hidden {
394
+ position: absolute;
395
+ opacity: 0;
396
+ visibility: hidden;
397
+ transform: translateY(-4px);
398
+ transition: opacity 180ms ease, transform 180ms ease;
399
+ pointer-events: none;
400
+ }
401
+
402
+ @keyframes spin {
403
+ to {
404
+ transform: rotate(360deg);
405
+ }
406
+ }
407
+
408
+ @keyframes toastIn {
409
+ from {
410
+ opacity: 0;
411
+ transform: translateX(30px) scaleX(0.8);
412
+ }
413
+
414
+ to {
415
+ opacity: 1;
416
+ transform: translateX(0) scaleX(1);
417
+ }
418
+ }
419
+
420
+ @keyframes toastOut {
421
+ from {
422
+ opacity: 1;
423
+ transform: translateX(0) scaleX(1);
424
+ }
425
+
426
+ to {
427
+ opacity: 0;
428
+ transform: translateX(24px) scaleX(0.86);
429
+ }
430
+ }
431
+ </style>
432
+ </head>
433
+
434
+ <body>
435
+ <div class="page-grid">
436
+ <main class="panel">
437
+ <header class="panel-header">
438
+ <h1 class="title">Spinner Playground</h1>
439
+ <p class="subtitle">Use these controls for <code>clickAndWatchForElement()</code> tests</p>
440
+ </header>
441
+
442
+ <section class="panel-body" id="spinnerPlaygroundBody">
443
+ <div class="controls-group">
444
+ <div class="button-row">
445
+ <button id="spinnerBtn1">Spinner shows after 1000ms - removed after 1500ms</button>
446
+ <button id="spinnerBtn2">Spinner shows immediatelly - removed after 2500ms</button>
447
+ <button id="spinnerBtn3">Spinner shows after 500ms - hidden after 3500ms</button>
448
+ <!--<button id="spinnerBtn4">Spinner shows immediatelly - hidden after 1800ms</button>-->
449
+ <button id="spinnerBtn5">Spinner shows INSIDE BUTTON inmediatelly - text restored after 1800ms</button>
450
+ <!--<button id="spinnerBtn6">No spinner</button>-->
451
+ </div>
452
+
453
+ <div class="canvas-wrap">
454
+ <canvas id="spinnerCanvas" width="390" height="100"></canvas>
455
+ </div>
456
+ </div>
457
+
458
+ <div class="spinner-layer" aria-live="polite">
459
+ <div class="spinner-persistent spinner-hidden" data-from="spinnerBtn4" aria-hidden="true">Waiting</div>
460
+ </div>
461
+ <div class="spinner-persistent spinner-spokes-orange spinner-hidden" data-from="spinnerBtn3" aria-hidden="true">Loading</div>
462
+
463
+ <p class="hint">Save/Slow/Canvas show a temporary spinner. Hide Spinner becomes hidden. Inline Spinner replaces button text. NoSpinner does nothing.</p>
464
+ </section>
465
+ </main>
466
+
467
+ <main class="panel">
468
+ <header class="panel-header">
469
+ <h1 class="title">Toast Playground</h1>
470
+ <p class="subtitle">Use these controls for <code>clickAndWatchForElement()</code> tests</p>
471
+ </header>
472
+
473
+ <section class="panel-body">
474
+ <div class="controls-group">
475
+ <div class="button-row">
476
+ <button id="toastBtn1">Toast shows after 1800ms - removed after 1000ms</button>
477
+ <button id="toastBtn2">Toast shows immediatelly - hidden after 3000ms</button>
478
+ <button id="toastBtn3">No toast</button>
479
+ </div>
480
+
481
+ <div class="canvas-wrap">
482
+ <canvas id="toastCanvas" width="390" height="100"></canvas>
483
+ </div>
484
+ </div>
485
+
486
+ <div class="toast-layer" aria-live="polite">
487
+ <div class="toast toast-persistent toast-hidden" data-from="toastBtn2" aria-hidden="true">
488
+ <span class="toast-message">Toast shows immediatelly - hidden after 3000ms</span>
489
+ <button class="toast-close" type="button" aria-label="Close toast">×</button>
490
+ </div>
491
+ <div class="toast toast-persistent toast-hidden" data-from="toastCanvas" aria-hidden="true">
492
+ <span class="toast-message">Toast shows after 1000ms - removed after 110ms</span>
493
+ <button class="toast-close" type="button" aria-label="Close toast">×</button>
494
+ </div>
495
+ </div>
496
+
497
+ <p class="hint">Save/Slow/Canvas show a green toast that expands from the right and auto-dismisses (available X icon to close). No toast does nothing.</p>
498
+ </section>
499
+ </main>
500
+ </div>
501
+
502
+ <script>
503
+ (function () {
504
+ function addSpinner(id, lifetime, delay = 0, hideInsteadOfRemove = false) {
505
+ lifetime = lifetime !== undefined ? lifetime : 120
506
+ var layer = document.querySelector('.spinner-layer')
507
+ var spinnerPlayground = document.getElementById('spinnerPlaygroundBody') || layer
508
+
509
+ var startSpinner = function () {
510
+ if (hideInsteadOfRemove) {
511
+ var persistent = spinnerPlayground.querySelector('.spinner-persistent[data-from="' + id + '"]')
512
+ if (!persistent) {
513
+ persistent = document.createElement('div')
514
+ persistent.className = 'spinner-persistent spinner-hidden'
515
+ persistent.setAttribute('data-from', id)
516
+ persistent.setAttribute('aria-hidden', 'true')
517
+ persistent.textContent = id === 'spinnerBtn4' ? 'Waiting' : 'Loading'
518
+ if (id === 'spinnerBtn3') {
519
+ persistent.classList.add('spinner-spokes-orange')
520
+ spinnerPlayground.appendChild(persistent)
521
+ } else {
522
+ layer.appendChild(persistent)
523
+ }
524
+ }
525
+
526
+ if (persistent._hideTimer) {
527
+ clearTimeout(persistent._hideTimer)
528
+ persistent._hideTimer = null
529
+ }
530
+
531
+ if (id === 'spinnerBtn3') {
532
+ persistent.classList.add('spinner-spokes-orange')
533
+ }
534
+
535
+ persistent.classList.add('spinner')
536
+ persistent.classList.remove('spinner-hidden')
537
+ persistent.classList.add('loading')
538
+ persistent.setAttribute('aria-hidden', 'false')
539
+ persistent._hideTimer = setTimeout(function () {
540
+ persistent.classList.add('spinner-hidden')
541
+ persistent.classList.remove('spinner')
542
+ persistent.classList.remove('loading')
543
+ persistent.setAttribute('aria-hidden', 'true')
544
+ }, lifetime)
545
+ return
546
+ }
547
+
548
+ var created = document.createElement('div')
549
+ created.className = 'spinner spinner-removed loading'
550
+ var targetLayer = layer
551
+ if (id === 'spinnerBtn2') {
552
+ created.classList.add('spinner-overlay')
553
+ targetLayer = document.getElementById('spinnerPlaygroundBody') || layer
554
+ }
555
+ created.setAttribute('data-from', id)
556
+ created.textContent = 'Loading'
557
+ targetLayer.appendChild(created)
558
+ setTimeout(function () { created.remove() }, lifetime)
559
+ }
560
+
561
+ if (delay > 0) {
562
+ setTimeout(startSpinner, delay)
563
+ return
564
+ }
565
+
566
+ startSpinner()
567
+ }
568
+
569
+ function wireToastClose(el) {
570
+ if (!el || el._closeWired) return
571
+ var closeBtn = el.querySelector('.toast-close')
572
+ if (!closeBtn) return
573
+ closeBtn.addEventListener('click', function () {
574
+ dismissToast(el, el.classList.contains('toast-persistent'))
575
+ })
576
+ el._closeWired = true
577
+ }
578
+
579
+ function addToast(id, message, lifetime, delay, reuseExisting) {
580
+ lifetime = lifetime !== undefined ? lifetime : 3200
581
+ delay = delay !== undefined ? delay : 0
582
+ reuseExisting = reuseExisting !== undefined ? reuseExisting : false
583
+ var layer = document.querySelector('.toast-layer')
584
+
585
+ var el = null
586
+ if (reuseExisting) {
587
+ el = layer.querySelector('.toast-persistent[data-from="' + id + '"]')
588
+ if (!el) return
589
+ var msgEl = el.querySelector('.toast-message')
590
+ if (msgEl) {
591
+ msgEl.textContent = message || 'Success'
592
+ }
593
+ wireToastClose(el)
594
+ } else {
595
+ var stale = layer.querySelectorAll('.toast[data-from="' + id + '"]:not(.toast-persistent)')
596
+ stale.forEach(function (node) { node.remove() })
597
+
598
+ el = document.createElement('div')
599
+ el.className = 'toast'
600
+ el.setAttribute('data-from', id)
601
+
602
+ var msg = document.createElement('span')
603
+ msg.className = 'toast-message'
604
+ msg.textContent = message || 'Success'
605
+
606
+ var closeBtn = document.createElement('button')
607
+ closeBtn.className = 'toast-close'
608
+ closeBtn.setAttribute('type', 'button')
609
+ closeBtn.setAttribute('aria-label', 'Close toast')
610
+ closeBtn.textContent = '×'
611
+
612
+ el.appendChild(msg)
613
+ el.appendChild(closeBtn)
614
+ wireToastClose(el)
615
+ }
616
+
617
+ var showToast = function () {
618
+ if (el._hideTimer) {
619
+ clearTimeout(el._hideTimer)
620
+ el._hideTimer = null
621
+ }
622
+ el.classList.remove('toast-hidden')
623
+ el.classList.remove('toast-closing')
624
+ el.setAttribute('aria-hidden', 'false')
625
+ if (!el.parentNode) {
626
+ layer.appendChild(el)
627
+ }
628
+ el._dismissTimer = setTimeout(function () {
629
+ dismissToast(el, reuseExisting)
630
+ }, lifetime)
631
+ }
632
+
633
+ if (delay > 0) {
634
+ if (el._showTimer) {
635
+ clearTimeout(el._showTimer)
636
+ el._showTimer = null
637
+ }
638
+ el._showTimer = setTimeout(showToast, delay)
639
+ return
640
+ }
641
+
642
+ showToast()
643
+ }
644
+
645
+ function removeToastNode(el) {
646
+ if (!el) return
647
+ if (el._removeTimer) {
648
+ clearTimeout(el._removeTimer)
649
+ el._removeTimer = null
650
+ }
651
+ if (el.isConnected) {
652
+ el.remove()
653
+ }
654
+ }
655
+
656
+ function dismissToast(el, keepInDom) {
657
+ if (!el) return
658
+ var retractMs = 180
659
+ keepInDom = keepInDom !== undefined ? keepInDom : false
660
+ if (el._showTimer) {
661
+ clearTimeout(el._showTimer)
662
+ el._showTimer = null
663
+ }
664
+ if (el._dismissTimer) {
665
+ clearTimeout(el._dismissTimer)
666
+ el._dismissTimer = null
667
+ }
668
+ if (el._hideTimer) {
669
+ clearTimeout(el._hideTimer)
670
+ el._hideTimer = null
671
+ }
672
+ if (el.classList.contains('toast-closing')) {
673
+ if (!keepInDom && !el._removeTimer) {
674
+ el._removeTimer = setTimeout(function () {
675
+ removeToastNode(el)
676
+ }, 180)
677
+ }
678
+ return
679
+ }
680
+ el.classList.add('toast-closing')
681
+ if (keepInDom) {
682
+ el._hideTimer = setTimeout(function () {
683
+ el.classList.add('toast-hidden')
684
+ el.classList.remove('toast-closing')
685
+ el.setAttribute('aria-hidden', 'true')
686
+ }, retractMs)
687
+ return
688
+ }
689
+ var handledByAnimation = false
690
+ var onAnimationEnd = function () {
691
+ handledByAnimation = true
692
+ el.removeEventListener('animationend', onAnimationEnd)
693
+ removeToastNode(el)
694
+ }
695
+ el.addEventListener('animationend', onAnimationEnd)
696
+ el._removeTimer = setTimeout(function () {
697
+ if (!handledByAnimation) {
698
+ el.removeEventListener('animationend', onAnimationEnd)
699
+ }
700
+ removeToastNode(el)
701
+ }, retractMs + 30)
702
+ }
703
+
704
+ function showInlineSpinnerInButton(buttonId, lifetime, delay) {
705
+ var button = document.getElementById(buttonId)
706
+ if (!button) return
707
+
708
+ var visibleFor = lifetime !== undefined ? lifetime : 1200
709
+ var waitBeforeShow = delay !== undefined ? delay : 0
710
+ var originalText = button.getAttribute('data-original-text') || button.textContent
711
+ button.setAttribute('data-original-text', originalText)
712
+
713
+ setTimeout(function () {
714
+ var spinnerEl = document.createElement('span')
715
+ spinnerEl.className = 'button-inline-spinner'
716
+ spinnerEl.setAttribute('data-from', buttonId)
717
+ spinnerEl.setAttribute('aria-hidden', 'true')
718
+ var buttonRect = button.getBoundingClientRect()
719
+ button.style.width = buttonRect.width + 'px'
720
+ button.style.height = buttonRect.height + 'px'
721
+
722
+ button.classList.add('is-loading')
723
+ button.textContent = ''
724
+ button.appendChild(spinnerEl)
725
+
726
+ setTimeout(function () {
727
+ spinnerEl.remove()
728
+ button.textContent = originalText
729
+ button.classList.remove('is-loading')
730
+ button.style.width = ''
731
+ button.style.height = ''
732
+ }, visibleFor)
733
+ }, waitBeforeShow)
734
+ }
735
+
736
+ // SPINNERS EXAMPLES
737
+ // ------------------------------------------------------------
738
+
739
+ // Spinner after 1000ms - removed after 1500ms
740
+ document.getElementById('spinnerBtn1').addEventListener('click', function () {
741
+ addSpinner('spinnerBtn1', 1500, 1000)
742
+ })
743
+
744
+ // Spinner immediatelly - removed after 6000ms
745
+ document.getElementById('spinnerBtn2').addEventListener('click', function () {
746
+ addSpinner('spinnerBtn2', 2500)
747
+ })
748
+
749
+ // Spinner after 500ms - hidden after 3500ms
750
+ document.getElementById('spinnerBtn3').addEventListener('click', function () {
751
+ addSpinner('spinnerBtn3', 3500, 500, true)
752
+ })
753
+
754
+ // // Spinner immediately - hidden after 1800ms
755
+ // document.getElementById('spinnerBtn4').addEventListener('click', function () {
756
+ // addSpinner('spinnerBtn4', 1800, 0, true)
757
+ // })
758
+
759
+ // Spinner inside button immediately - text restored after 1800ms.
760
+ document.getElementById('spinnerBtn5').addEventListener('click', function () {
761
+ showInlineSpinnerInButton('spinnerBtn5', 1300, 0)
762
+ })
763
+
764
+ // // No spinner intentionally
765
+ // document.getElementById('spinnerBtn6').addEventListener('click', function () { })
766
+
767
+ // Spinner shows immediatelly and then removed after 20ms
768
+ document.getElementById('spinnerCanvas').addEventListener('click', function () {
769
+ addSpinner('spinnerCanvas', 20)
770
+ })
771
+
772
+ // TOASTS EXAMPLES
773
+ // ------------------------------------------------------------
774
+
775
+ // Toast after 1800ms - removed after 1000ms
776
+ document.getElementById('toastBtn1').addEventListener('click', function () {
777
+ addToast('toastBtn1', 'Toast after 1800ms - removed after 1000ms', 1000, 1800, false)
778
+ })
779
+
780
+ // Spinner immediately - hidden after 1800ms
781
+ document.getElementById('toastBtn2').addEventListener('click', function () {
782
+ addToast('toastBtn2', 'Toast immediatelly - hidden after 3000ms', 3000, 0, true)
783
+ })
784
+
785
+ // No toast intentionally
786
+ document.getElementById('toastBtn3').addEventListener('click', function () { })
787
+
788
+ // Toast shows after 30ms and then hidden after 110ms
789
+ document.getElementById('toastCanvas').addEventListener('click', function () {
790
+ addToast('toastCanvas', 'Toast after 30ms - hidden after 110ms', 110, 30, true)
791
+ })
792
+
793
+ function paintCanvasHint(canvasId, text) {
794
+ var canvas = document.getElementById(canvasId)
795
+ var ctx = canvas.getContext('2d')
796
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
797
+ ctx.fillStyle = '#1e2b4a'
798
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
799
+ ctx.fillStyle = '#f8fafc'
800
+ ctx.font = '700 12px Segoe UI, Arial, sans-serif'
801
+ ctx.textAlign = 'center'
802
+ ctx.textBaseline = 'top'
803
+ ctx.strokeStyle = 'rgba(15, 23, 42, 0.7)'
804
+ ctx.lineWidth = 2
805
+ var words = String(text || '').split(' ')
806
+ var lines = []
807
+ var currentLine = words[0] || ''
808
+ var maxWidth = canvas.width - 20
809
+ var lineHeight = 16
810
+
811
+ for (var i = 1; i < words.length; i += 1) {
812
+ var candidate = currentLine + ' ' + words[i]
813
+ if (ctx.measureText(candidate).width <= maxWidth) {
814
+ currentLine = candidate
815
+ } else {
816
+ lines.push(currentLine)
817
+ currentLine = words[i]
818
+ }
819
+ }
820
+ lines.push(currentLine)
821
+
822
+ var totalHeight = lines.length * lineHeight
823
+ var startY = (canvas.height - totalHeight) / 2
824
+
825
+ for (var j = 0; j < lines.length; j += 1) {
826
+ var y = startY + j * lineHeight
827
+ ctx.strokeText(lines[j], canvas.width / 2, y)
828
+ ctx.fillText(lines[j], canvas.width / 2, y)
829
+ }
830
+ }
831
+
832
+ function syncSpinnerButtonWidths() {
833
+ var referenceBtn = document.getElementById('spinnerBtn2')
834
+ var targetBtn = document.getElementById('spinnerBtn5')
835
+ if (!referenceBtn || !targetBtn) return
836
+ targetBtn.style.width = referenceBtn.getBoundingClientRect().width + 'px'
837
+ }
838
+
839
+ paintCanvasHint('spinnerCanvas', 'Spinner shows immediatelly - removed after 20ms (Canvas)')
840
+ paintCanvasHint('toastCanvas', 'Toast shows after 30ms - hidden after 110ms (Canvas)')
841
+ syncSpinnerButtonWidths()
842
+ window.addEventListener('resize', syncSpinnerButtonWidths)
843
+ })()
844
+ </script>
845
+ </body>
846
+
847
+ </html>