toastify-pro 1.5.0 → 1.7.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.
@@ -14,11 +14,18 @@
14
14
  * - Responsive design for mobile devices
15
15
  * - Framework agnostic (works with React, Vue, Angular, etc.)
16
16
  * - Confirmation dialogs with customizable buttons and callbacks
17
+ * - Input prompts with validation and async support
17
18
  * - Confirmation overlay with blur effect for focus
18
19
  * - Center position support for enhanced focus
19
20
  * - Independent positioning for confirmations
21
+ * - Action buttons in toasts with customizable callbacks
22
+ * - Pause on hover functionality
23
+ * - Queue management (maxToasts, newestOnTop)
24
+ * - Full accessibility support (ARIA, keyboard navigation, reduced motion)
25
+ * - Focus management for confirmation and input dialogs
26
+ * - Improved dismiss handling (no hover interference)
20
27
  *
21
- * @version 1.5.0
28
+ * @version 1.7.0
22
29
  * @author ToastifyPro Team
23
30
  * @license MIT
24
31
  */
@@ -36,6 +43,10 @@ class ToastifyPro {
36
43
  * @param {number} options.maxLength - Maximum message length
37
44
  * @param {string} options.primaryColor - Primary color for custom() method
38
45
  * @param {string} options.secondaryColor - Secondary color for gradient in custom() method
46
+ * @param {boolean} options.pauseOnHover - Pause timeout when hovering over toast (default: true)
47
+ * @param {number} options.maxToasts - Maximum number of visible toasts (0 for unlimited)
48
+ * @param {boolean} options.newestOnTop - Show newest toasts on top (default: true)
49
+ * @param {boolean} options.ariaLive - ARIA live region setting: 'polite' or 'assertive' (default: 'polite')
39
50
  */
40
51
  constructor(options = {}) {
41
52
  // Validate options parameter
@@ -52,7 +63,14 @@ class ToastifyPro {
52
63
  maxLength: options.maxLength || 100,
53
64
  primaryColor: options.primaryColor || null, // Custom primary color for custom() method
54
65
  secondaryColor: options.secondaryColor || null, // Custom secondary color for gradient
66
+ pauseOnHover: options.pauseOnHover !== false, // default true - pause timeout on hover
67
+ maxToasts: options.maxToasts || 0, // 0 = unlimited
68
+ newestOnTop: options.newestOnTop !== false, // default true
69
+ ariaLive: options.ariaLive || 'polite', // 'polite' or 'assertive'
55
70
  };
71
+
72
+ // Track active toasts for queue management
73
+ this.activeToasts = [];
56
74
 
57
75
  // Validate position
58
76
  const validPositions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'center'];
@@ -85,6 +103,46 @@ class ToastifyPro {
85
103
 
86
104
  // Inject styles once
87
105
  this.injectStyles();
106
+
107
+ // Setup global keyboard event listener for accessibility
108
+ this.setupKeyboardNavigation();
109
+ }
110
+
111
+ /**
112
+ * Sets up keyboard navigation for accessibility
113
+ * - Escape key dismisses the most recent toast or confirmation
114
+ * - Tab key cycles through focusable elements in confirmations
115
+ */
116
+ setupKeyboardNavigation() {
117
+ // Only setup once globally
118
+ if (window._toastifyProKeyboardSetup) return;
119
+ window._toastifyProKeyboardSetup = true;
120
+
121
+ document.addEventListener('keydown', (e) => {
122
+ // Escape key - dismiss toast or confirmation
123
+ if (e.key === 'Escape') {
124
+ // First check for active confirmation
125
+ if (globalActiveConfirmation && globalActiveConfirmation.element) {
126
+ const loadingBtn = globalActiveConfirmation.element.querySelector('.toast-btn-confirm.loading');
127
+ if (!loadingBtn) {
128
+ globalActiveConfirmation.close();
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Otherwise dismiss the most recent toast
134
+ const containers = document.querySelectorAll('.toastify-pro-container');
135
+ containers.forEach(container => {
136
+ const toasts = container.querySelectorAll('.toastify-pro:not(.confirmation)');
137
+ if (toasts.length > 0) {
138
+ const lastToast = toasts[toasts.length - 1];
139
+ if (lastToast && lastToast._toastInstance) {
140
+ lastToast._toastInstance.removeToast(lastToast);
141
+ }
142
+ }
143
+ });
144
+ }
145
+ });
88
146
  }
89
147
 
90
148
  /**
@@ -208,19 +266,19 @@ class ToastifyPro {
208
266
  @keyframes airdropPop {
209
267
  0% {
210
268
  opacity: 0;
211
- transform: scale(0.3) rotateY(-20deg);
269
+ transform: scale(0.5) rotateY(-15deg) translateY(20px);
212
270
  }
213
- 30% {
214
- opacity: 0.8;
215
- transform: scale(1.1) rotateY(10deg);
271
+ 50% {
272
+ opacity: 0.95;
273
+ transform: scale(1.03) rotateY(5deg) translateY(-3px);
216
274
  }
217
- 60% {
275
+ 75% {
218
276
  opacity: 1;
219
- transform: scale(0.98) rotateY(-3deg);
277
+ transform: scale(0.99) rotateY(-1deg) translateY(1px);
220
278
  }
221
279
  100% {
222
280
  opacity: 1;
223
- transform: scale(1) rotateY(0deg);
281
+ transform: scale(1) rotateY(0deg) translateY(0);
224
282
  }
225
283
  }
226
284
 
@@ -229,13 +287,13 @@ class ToastifyPro {
229
287
  opacity: 1;
230
288
  transform: scale(1) translateY(0);
231
289
  }
232
- 15% {
290
+ 12% {
233
291
  opacity: 1;
234
- transform: scale(1.02) translateY(-8px);
292
+ transform: scale(1.015) translateY(-6px);
235
293
  }
236
294
  100% {
237
295
  opacity: 0;
238
- transform: scale(0.8) translateY(200px);
296
+ transform: scale(0.85) translateY(150px);
239
297
  }
240
298
  }
241
299
 
@@ -244,13 +302,13 @@ class ToastifyPro {
244
302
  opacity: 1;
245
303
  transform: scale(1) translateY(0);
246
304
  }
247
- 15% {
305
+ 12% {
248
306
  opacity: 1;
249
- transform: scale(1.02) translateY(8px);
307
+ transform: scale(1.015) translateY(6px);
250
308
  }
251
309
  100% {
252
310
  opacity: 0;
253
- transform: scale(0.8) translateY(-200px);
311
+ transform: scale(0.85) translateY(-150px);
254
312
  }
255
313
  }
256
314
 
@@ -259,13 +317,13 @@ class ToastifyPro {
259
317
  opacity: 1;
260
318
  transform: scale(1) translateX(0);
261
319
  }
262
- 15% {
320
+ 12% {
263
321
  opacity: 1;
264
- transform: scale(1.02) translateX(8px);
322
+ transform: scale(1.015) translateX(6px);
265
323
  }
266
324
  100% {
267
325
  opacity: 0;
268
- transform: scale(0.8) translateX(-300px);
326
+ transform: scale(0.85) translateX(-250px);
269
327
  }
270
328
  }
271
329
 
@@ -274,13 +332,13 @@ class ToastifyPro {
274
332
  opacity: 1;
275
333
  transform: scale(1) translateX(0);
276
334
  }
277
- 15% {
335
+ 12% {
278
336
  opacity: 1;
279
- transform: scale(1.02) translateX(-8px);
337
+ transform: scale(1.015) translateX(-6px);
280
338
  }
281
339
  100% {
282
340
  opacity: 0;
283
- transform: scale(0.8) translateX(300px);
341
+ transform: scale(0.85) translateX(250px);
284
342
  }
285
343
  }
286
344
 
@@ -289,13 +347,13 @@ class ToastifyPro {
289
347
  opacity: 1;
290
348
  transform: scale(1) translateY(0);
291
349
  }
292
- 15% {
350
+ 12% {
293
351
  opacity: 1;
294
- transform: scale(1.02) translateY(-5px);
352
+ transform: scale(1.015) translateY(-4px);
295
353
  }
296
354
  100% {
297
355
  opacity: 0;
298
- transform: scale(0.6) translateY(150px);
356
+ transform: scale(0.7) translateY(120px);
299
357
  }
300
358
  }
301
359
 
@@ -332,7 +390,7 @@ class ToastifyPro {
332
390
  .toastify-pro.show {
333
391
  opacity: 1;
334
392
  transform: scale(1);
335
- animation: airdropPop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
393
+ animation: airdropPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
336
394
  }
337
395
 
338
396
  .toastify-pro.success {
@@ -411,9 +469,9 @@ class ToastifyPro {
411
469
  }
412
470
 
413
471
  @keyframes iconBounce {
414
- 0% { transform: scale(0.2) rotate(-15deg); }
415
- 40% { transform: scale(1.2) rotate(8deg); }
416
- 70% { transform: scale(0.95) rotate(-3deg); }
472
+ 0% { transform: scale(0.3) rotate(-10deg); opacity: 0; }
473
+ 50% { transform: scale(1.1) rotate(5deg); opacity: 1; }
474
+ 70% { transform: scale(0.97) rotate(-2deg); }
417
475
  100% { transform: scale(1) rotate(0deg); }
418
476
  }
419
477
 
@@ -422,12 +480,12 @@ class ToastifyPro {
422
480
  transform: scale(1) rotate(0deg);
423
481
  opacity: 1;
424
482
  }
425
- 20% {
426
- transform: scale(1.1) rotate(-10deg);
427
- opacity: 0.9;
483
+ 25% {
484
+ transform: scale(1.08) rotate(-8deg);
485
+ opacity: 0.85;
428
486
  }
429
487
  100% {
430
- transform: scale(0.3) rotate(180deg);
488
+ transform: scale(0.4) rotate(120deg);
431
489
  opacity: 0;
432
490
  }
433
491
  }
@@ -436,6 +494,11 @@ class ToastifyPro {
436
494
  0% { transform: rotate(0deg); }
437
495
  100% { transform: rotate(360deg); }
438
496
  }
497
+
498
+ @keyframes spinFast {
499
+ 0% { transform: rotate(0deg); }
500
+ 100% { transform: rotate(360deg); }
501
+ }
439
502
 
440
503
  .toastify-pro .toast-icon svg {
441
504
  width: 18px;
@@ -479,6 +542,7 @@ class ToastifyPro {
479
542
  transition: all 0.2s ease;
480
543
  flex-shrink: 0;
481
544
  width: 32px;
545
+ border: none;
482
546
  height: 32px;
483
547
  display: flex;
484
548
  align-items: center;
@@ -710,6 +774,9 @@ class ToastifyPro {
710
774
  }
711
775
 
712
776
  .toast-btn-confirm {
777
+ display: flex;
778
+ align-items: center;
779
+ justify-content: center;
713
780
  color: white;
714
781
  font-weight: 700;
715
782
  border: 2px solid rgba(255, 255, 255, 0.4);
@@ -748,17 +815,20 @@ class ToastifyPro {
748
815
 
749
816
  .toast-btn-confirm .btn-spinner {
750
817
  display: none;
751
- width: 16px;
752
- height: 16px;
753
- border: 2px solid rgba(255, 255, 255, 0.3);
754
- border-top-color: white;
755
- border-radius: 50%;
756
- animation: spin 0.6s linear infinite;
757
- margin-right: 8px;
818
+ align-items: center;
819
+ justify-content: center;
820
+ margin-left: 8px;
821
+ }
822
+
823
+ .toast-btn-confirm .btn-spinner svg {
824
+ width: 25px;
825
+ height: 25px;
826
+ animation: spinFast 0.5s linear infinite;
827
+ color: currentColor;
758
828
  }
759
829
 
760
830
  .toast-btn-confirm.loading .btn-spinner {
761
- display: inline-block;
831
+ display: inline-flex;
762
832
  }
763
833
 
764
834
  .toast-btn-confirm.loading .btn-text {
@@ -848,6 +918,356 @@ class ToastifyPro {
848
918
  .toastify-pro-overlay.show {
849
919
  opacity: 1;
850
920
  }
921
+
922
+ /* Action Button Styles */
923
+ .toastify-pro .toast-action {
924
+ display: inline-flex;
925
+ align-items: center;
926
+ gap: 6px;
927
+ padding: 6px 12px;
928
+ margin-top: 8px;
929
+ border: none;
930
+ border-radius: 8px;
931
+ font-weight: 600;
932
+ font-size: 13px;
933
+ cursor: pointer;
934
+ transition: all 0.2s ease;
935
+ background: rgba(255, 255, 255, 0.2);
936
+ color: inherit;
937
+ backdrop-filter: blur(10px);
938
+ }
939
+
940
+ .toastify-pro .toast-action:hover {
941
+ background: rgba(255, 255, 255, 0.3);
942
+ transform: translateY(-1px);
943
+ }
944
+
945
+ .toastify-pro .toast-action:active {
946
+ transform: translateY(0);
947
+ }
948
+
949
+ .toastify-pro.light .toast-action {
950
+ background: rgba(15, 23, 42, 0.1);
951
+ }
952
+
953
+ .toastify-pro.light .toast-action:hover {
954
+ background: rgba(15, 23, 42, 0.15);
955
+ }
956
+
957
+ /* Paused state - pause progress bar */
958
+ .toastify-pro.paused::after {
959
+ animation-play-state: paused;
960
+ }
961
+
962
+ /* Progress restart - used after hover to restart progress bar only */
963
+ .toastify-pro.progress-restart::after {
964
+ animation: none;
965
+ }
966
+
967
+ /* Focus styles for accessibility */
968
+ .toastify-pro .close-btn:focus,
969
+ .toastify-pro .toast-action:focus,
970
+ .toast-btn:focus {
971
+ outline: 1px solid rgba(255, 255, 255, 0.8);
972
+ }
973
+
974
+ .toastify-pro.light .close-btn:focus,
975
+ .toastify-pro.light .toast-action:focus {
976
+ outline-color: 1px solid rgba(15, 23, 42, 0.5);
977
+ }
978
+
979
+ /* Screen reader only class */
980
+ .sr-only {
981
+ position: absolute;
982
+ width: 1px;
983
+ height: 1px;
984
+ padding: 0;
985
+ margin: -1px;
986
+ overflow: hidden;
987
+ clip: rect(0, 0, 0, 0);
988
+ white-space: nowrap;
989
+ border: 0;
990
+ }
991
+
992
+ /* ===== INPUT TOAST STYLES ===== */
993
+ .toastify-pro.input-toast {
994
+ min-width: 340px;
995
+ max-width: 450px;
996
+ padding: 24px 24px 20px;
997
+ flex-direction: column;
998
+ align-items: stretch;
999
+ gap: 16px;
1000
+ }
1001
+
1002
+ .toastify-pro.input-toast .toast-input-wrapper {
1003
+ display: flex;
1004
+ flex-direction: column;
1005
+ gap: 16px;
1006
+ width: 100%;
1007
+ }
1008
+
1009
+ .toastify-pro.input-toast .toast-input-header {
1010
+ display: flex;
1011
+ align-items: flex-start;
1012
+ gap: 12px;
1013
+ }
1014
+
1015
+ .toastify-pro.input-toast .toast-input {
1016
+ width: 100%;
1017
+ padding: 12px 16px;
1018
+ border: 1.5px solid rgba(255, 255, 255, 0.2);
1019
+ border-radius: 12px;
1020
+ background: rgba(255, 255, 255, 0.08);
1021
+ backdrop-filter: blur(12px);
1022
+ color: inherit;
1023
+ font-family: inherit;
1024
+ font-size: 14px;
1025
+ font-weight: 450;
1026
+ outline: none;
1027
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1028
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
1029
+ }
1030
+
1031
+ .toastify-pro.input-toast .toast-input::placeholder {
1032
+ color: rgba(255, 255, 255, 0.5);
1033
+ font-weight: 400;
1034
+ }
1035
+
1036
+ .toastify-pro.input-toast .toast-input:focus {
1037
+ border-color: rgba(255, 255, 255, 0.45);
1038
+ background: rgba(255, 255, 255, 0.12);
1039
+ box-shadow:
1040
+ inset 0 1px 2px rgba(0, 0, 0, 0.1),
1041
+ 0 0 0 3px rgba(255, 255, 255, 0.08);
1042
+ }
1043
+
1044
+ .toastify-pro.input-toast .toast-input:hover:not(:focus) {
1045
+ border-color: rgba(255, 255, 255, 0.3);
1046
+ background: rgba(255, 255, 255, 0.1);
1047
+ }
1048
+
1049
+ .toastify-pro.input-toast.light .toast-input {
1050
+ border-color: rgba(15, 23, 42, 0.15);
1051
+ background: rgba(15, 23, 42, 0.04);
1052
+ box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06);
1053
+ }
1054
+
1055
+ .toastify-pro.input-toast.light .toast-input::placeholder {
1056
+ color: rgba(15, 23, 42, 0.45);
1057
+ }
1058
+
1059
+ .toastify-pro.input-toast.light .toast-input:focus {
1060
+ border-color: rgba(15, 23, 42, 0.35);
1061
+ background: rgba(15, 23, 42, 0.06);
1062
+ box-shadow:
1063
+ inset 0 1px 2px rgba(15, 23, 42, 0.06),
1064
+ 0 0 0 3px rgba(15, 23, 42, 0.06);
1065
+ }
1066
+
1067
+ .toastify-pro.input-toast.light .toast-input:hover:not(:focus) {
1068
+ border-color: rgba(15, 23, 42, 0.25);
1069
+ background: rgba(15, 23, 42, 0.06);
1070
+ }
1071
+
1072
+ .toastify-pro.input-toast .toast-input-actions {
1073
+ display: flex;
1074
+ gap: 10px;
1075
+ justify-content: flex-end;
1076
+ }
1077
+
1078
+ .toastify-pro.input-toast .input-btn {
1079
+ padding: 10px 20px;
1080
+ border-radius: 10px;
1081
+ font-size: 14px;
1082
+ font-weight: 550;
1083
+ cursor: pointer;
1084
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1085
+ border: 1.5px solid transparent;
1086
+ position: relative;
1087
+ overflow: hidden;
1088
+ }
1089
+
1090
+ .toastify-pro.input-toast .input-btn-cancel {
1091
+ background: rgba(255, 255, 255, 0.1);
1092
+ color: rgba(255, 255, 255, 0.9);
1093
+ border-color: rgba(255, 255, 255, 0.2);
1094
+ }
1095
+
1096
+ .toastify-pro.input-toast .input-btn-cancel:hover {
1097
+ background: rgba(255, 255, 255, 0.18);
1098
+ border-color: rgba(255, 255, 255, 0.3);
1099
+ }
1100
+
1101
+ .toastify-pro.input-toast .input-btn-submit {
1102
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%);
1103
+ color: #1e293b;
1104
+ border-color: rgba(255, 255, 255, 0.4);
1105
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
1106
+ }
1107
+
1108
+ .toastify-pro.input-toast .input-btn-submit:hover {
1109
+ background: linear-gradient(135deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.95) 100%);
1110
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
1111
+ transform: translateY(-1px);
1112
+ }
1113
+
1114
+ .toastify-pro.input-toast .input-btn-submit:active {
1115
+ transform: translateY(0);
1116
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
1117
+ }
1118
+
1119
+ .toastify-pro.input-toast.light .input-btn-cancel {
1120
+ background: rgba(15, 23, 42, 0.06);
1121
+ color: rgba(15, 23, 42, 0.85);
1122
+ border-color: rgba(15, 23, 42, 0.15);
1123
+ }
1124
+
1125
+ .toastify-pro.input-toast.light .input-btn-cancel:hover {
1126
+ background: rgba(15, 23, 42, 0.1);
1127
+ border-color: rgba(15, 23, 42, 0.25);
1128
+ }
1129
+
1130
+ .toastify-pro.input-toast.light .input-btn-submit {
1131
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
1132
+ color: white;
1133
+ border-color: rgba(15, 23, 42, 0.3);
1134
+ }
1135
+
1136
+ .toastify-pro.input-toast.light .input-btn-submit:hover {
1137
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
1138
+ box-shadow: 0 4px 16px rgba(15, 23, 42, 0.25);
1139
+ }
1140
+
1141
+ /* Input toast loading state */
1142
+ .toastify-pro.input-toast .input-btn-submit.loading {
1143
+ opacity: 0.7;
1144
+ cursor: not-allowed;
1145
+ pointer-events: none;
1146
+ }
1147
+
1148
+ .toastify-pro.input-toast .input-btn-submit .btn-spinner {
1149
+ display: none;
1150
+ align-items: center;
1151
+ justify-content: center;
1152
+ margin-left: 6px;
1153
+ }
1154
+
1155
+ .toastify-pro.input-toast .input-btn-submit .btn-spinner svg {
1156
+ width: 16px;
1157
+ height: 16px;
1158
+ animation: spinFast 0.5s linear infinite;
1159
+ }
1160
+
1161
+ .toastify-pro.input-toast .input-btn-submit.loading .btn-spinner {
1162
+ display: inline-flex;
1163
+ }
1164
+
1165
+ .toastify-pro.input-toast .input-btn-submit.loading .btn-text {
1166
+ opacity: 0.7;
1167
+ }
1168
+
1169
+ /* Input validation error state */
1170
+ .toastify-pro.input-toast .toast-input.error {
1171
+ border-color: rgba(239, 68, 68, 0.6);
1172
+ background: rgba(239, 68, 68, 0.08);
1173
+ animation: inputShake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97);
1174
+ }
1175
+
1176
+ .toastify-pro.input-toast .toast-input-error {
1177
+ color: #f87171;
1178
+ font-size: 12px;
1179
+ font-weight: 450;
1180
+ margin-top: -8px;
1181
+ opacity: 0;
1182
+ transform: translateY(-4px);
1183
+ transition: all 0.2s ease;
1184
+ }
1185
+
1186
+ .toastify-pro.input-toast .toast-input-error.visible {
1187
+ opacity: 1;
1188
+ transform: translateY(0);
1189
+ }
1190
+
1191
+ .toastify-pro.input-toast.light .toast-input.error {
1192
+ border-color: rgba(239, 68, 68, 0.5);
1193
+ background: rgba(239, 68, 68, 0.06);
1194
+ }
1195
+
1196
+ .toastify-pro.input-toast.light .toast-input-error {
1197
+ color: #dc2626;
1198
+ }
1199
+
1200
+ @keyframes inputShake {
1201
+ 0%, 100% { transform: translateX(0); }
1202
+ 20%, 60% { transform: translateX(-6px); }
1203
+ 40%, 80% { transform: translateX(6px); }
1204
+ }
1205
+
1206
+ /* Hide progress bar for input toasts */
1207
+ .toastify-pro.input-toast::after {
1208
+ display: none;
1209
+ }
1210
+
1211
+ @media (max-width: 640px) {
1212
+ .toastify-pro.input-toast {
1213
+ min-width: 280px;
1214
+ max-width: calc(100vw - 32px);
1215
+ }
1216
+
1217
+ .toastify-pro.input-toast .toast-input-actions {
1218
+ flex-direction: column;
1219
+ }
1220
+
1221
+ .toastify-pro.input-toast .input-btn {
1222
+ width: 100%;
1223
+ }
1224
+ }
1225
+
1226
+ /* Reduced motion support */
1227
+ @media (prefers-reduced-motion: reduce) {
1228
+ .toastify-pro {
1229
+ transition: opacity 0.3s ease;
1230
+ transform: none !important;
1231
+ }
1232
+
1233
+ .toastify-pro.show {
1234
+ animation: none !important;
1235
+ opacity: 1;
1236
+ transform: none !important;
1237
+ }
1238
+
1239
+ .toastify-pro .toast-icon {
1240
+ animation: none !important;
1241
+ }
1242
+
1243
+ .toastify-pro::before {
1244
+ animation: none !important;
1245
+ }
1246
+
1247
+ .toastify-pro::after {
1248
+ animation: progress var(--duration, 5s) linear !important;
1249
+ }
1250
+
1251
+ .toastify-pro-overlay {
1252
+ transition: opacity 0.2s ease;
1253
+ }
1254
+
1255
+ .toast-btn::after {
1256
+ display: none;
1257
+ }
1258
+
1259
+ .toast-btn:hover {
1260
+ transform: none;
1261
+ }
1262
+
1263
+ .toastify-pro.confirmation .conf-close-btn:hover {
1264
+ transform: scale(1.05);
1265
+ }
1266
+
1267
+ .btn-spinner svg {
1268
+ animation: spinFast 0.5s linear infinite !important;
1269
+ }
1270
+ }
851
1271
  `;
852
1272
  document.head.appendChild(style);
853
1273
  } catch (error) {
@@ -864,6 +1284,9 @@ class ToastifyPro {
864
1284
  * @param {number} opts.timeout - Override default timeout
865
1285
  * @param {boolean} opts.allowClose - Override close button setting
866
1286
  * @param {number} opts.maxLength - Override max message length
1287
+ * @param {Object} opts.action - Action button configuration { label, onClick }
1288
+ * @param {boolean} opts.pauseOnHover - Pause timeout on hover
1289
+ * @param {string} opts.ariaLive - ARIA live region type ('polite' or 'assertive')
867
1290
  */
868
1291
  show(message, type = "dark", opts = {}) {
869
1292
  // Input validation
@@ -893,10 +1316,30 @@ class ToastifyPro {
893
1316
  const options = { ...this.defaultOptions, ...opts };
894
1317
 
895
1318
  try {
1319
+ // Queue management - remove oldest toasts if limit exceeded
1320
+ if (options.maxToasts > 0 && this.activeToasts.length >= options.maxToasts) {
1321
+ const toastsToRemove = this.activeToasts.length - options.maxToasts + 1;
1322
+ for (let i = 0; i < toastsToRemove; i++) {
1323
+ const oldestToast = this.activeToasts.shift();
1324
+ if (oldestToast && oldestToast.element) {
1325
+ this.removeToast(oldestToast.element);
1326
+ }
1327
+ }
1328
+ }
1329
+
896
1330
  // Create toast element
897
1331
  const toast = document.createElement("div");
898
1332
  toast.className = `toastify-pro ${type}`;
899
1333
 
1334
+ // Store reference to this instance for keyboard navigation
1335
+ toast._toastInstance = this;
1336
+
1337
+ // ARIA accessibility attributes
1338
+ const ariaLive = type === 'error' || type === 'warning' ? 'assertive' : (options.ariaLive || 'polite');
1339
+ toast.setAttribute('role', type === 'error' ? 'alert' : 'status');
1340
+ toast.setAttribute('aria-live', ariaLive);
1341
+ toast.setAttribute('aria-atomic', 'true');
1342
+
900
1343
  // Set duration for progress bar animation
901
1344
  if (options.timeout > 0) {
902
1345
  toast.style.setProperty('--duration', `${options.timeout}ms`);
@@ -905,6 +1348,7 @@ class ToastifyPro {
905
1348
  // Create icon wrapper
906
1349
  const iconWrapper = document.createElement("div");
907
1350
  iconWrapper.className = "toast-icon";
1351
+ iconWrapper.setAttribute('aria-hidden', 'true');
908
1352
  iconWrapper.innerHTML = this.getIconSVG(type);
909
1353
  toast.appendChild(iconWrapper);
910
1354
 
@@ -926,20 +1370,51 @@ class ToastifyPro {
926
1370
  contentWrapper.appendChild(descriptionElement);
927
1371
  }
928
1372
 
1373
+ // Action button support
1374
+ if (options.action && typeof options.action === 'object') {
1375
+ const actionBtn = document.createElement("button");
1376
+ actionBtn.className = "toast-action";
1377
+ actionBtn.textContent = options.action.label || 'Action';
1378
+ actionBtn.setAttribute('type', 'button');
1379
+ if (typeof options.action.onClick === 'function') {
1380
+ actionBtn.onclick = (e) => {
1381
+ e.stopPropagation();
1382
+ options.action.onClick({ close: () => this.removeToast(toast), event: e });
1383
+ };
1384
+ }
1385
+ contentWrapper.appendChild(actionBtn);
1386
+ }
1387
+
929
1388
  toast.appendChild(contentWrapper);
930
1389
 
931
1390
  // Add close button if enabled
932
1391
  if (options.allowClose) {
933
- const closeBtn = document.createElement("span");
1392
+ const closeBtn = document.createElement("button");
934
1393
  closeBtn.className = "close-btn";
935
1394
  closeBtn.innerHTML = "&times;";
1395
+ closeBtn.setAttribute('type', 'button');
936
1396
  closeBtn.setAttribute('aria-label', 'Close notification');
937
1397
  closeBtn.onclick = () => this.removeToast(toast);
938
1398
  toast.appendChild(closeBtn);
939
1399
  }
940
1400
 
941
- // Add toast to container
942
- this.container.appendChild(toast);
1401
+ // Add toast to container (respect newestOnTop setting)
1402
+ if (options.newestOnTop && this.container.firstChild) {
1403
+ this.container.insertBefore(toast, this.container.firstChild);
1404
+ } else {
1405
+ this.container.appendChild(toast);
1406
+ }
1407
+
1408
+ // Track toast for queue management
1409
+ const toastData = {
1410
+ element: toast,
1411
+ timeout: null,
1412
+ remainingTime: options.timeout,
1413
+ startTime: null,
1414
+ isPaused: false,
1415
+ isRemoving: false // Flag to prevent hover interference during removal
1416
+ };
1417
+ this.activeToasts.push(toastData);
943
1418
 
944
1419
  // Apple AirDrop-style entrance animation
945
1420
  setTimeout(() => {
@@ -951,16 +1426,94 @@ class ToastifyPro {
951
1426
  }
952
1427
  }, 10);
953
1428
 
1429
+ // Pause on hover functionality
1430
+ if (options.pauseOnHover && options.timeout > 0) {
1431
+ toast.addEventListener('mouseenter', () => {
1432
+ // Don't pause if toast is being removed
1433
+ if (toastData.isRemoving) return;
1434
+ if (toastData.timeout) {
1435
+ clearTimeout(toastData.timeout);
1436
+ toastData.isPaused = true;
1437
+ toastData.remainingTime -= (Date.now() - toastData.startTime);
1438
+ toast.classList.add('paused');
1439
+ }
1440
+ });
1441
+
1442
+ toast.addEventListener('mouseleave', () => {
1443
+ // Don't restart timer if toast is being removed
1444
+ if (toastData.isRemoving) return;
1445
+ if (toastData.isPaused && toastData.remainingTime > 0) {
1446
+ toastData.isPaused = false;
1447
+ toastData.startTime = Date.now();
1448
+ toast.classList.remove('paused');
1449
+ // Update CSS variable for remaining progress
1450
+ toast.style.setProperty('--duration', `${toastData.remainingTime}ms`);
1451
+ // Restart the progress bar animation only (not the main toast animation)
1452
+ // Using class toggle to reset pseudo-element animation without affecting main element
1453
+ toast.classList.add('progress-restart');
1454
+ void toast.offsetHeight; // Force reflow
1455
+ toast.classList.remove('progress-restart');
1456
+
1457
+ toastData.timeout = setTimeout(() => this.removeToast(toast), toastData.remainingTime);
1458
+ }
1459
+ });
1460
+ }
1461
+
954
1462
  // Auto-remove after timeout
955
1463
  if (options.timeout > 0) {
956
- setTimeout(() => this.removeToast(toast), options.timeout);
1464
+ toastData.startTime = Date.now();
1465
+ toastData.timeout = setTimeout(() => this.removeToast(toast), options.timeout);
957
1466
  }
958
1467
 
959
- return toast; // Return element for potential future manipulation
1468
+ // Return toast control object
1469
+ return {
1470
+ element: toast,
1471
+ dismiss: () => this.removeToast(toast),
1472
+ update: (newMessage, newOpts) => this.updateToast(toast, newMessage, newOpts)
1473
+ };
960
1474
  } catch (error) {
961
1475
  console.error('ToastifyPro: Failed to create toast:', error);
962
1476
  }
963
1477
  }
1478
+
1479
+ /**
1480
+ * Updates an existing toast's content
1481
+ * @param {HTMLElement} toast - Toast element to update
1482
+ * @param {string} message - New message text
1483
+ * @param {Object} opts - Options to update
1484
+ */
1485
+ updateToast(toast, message, opts = {}) {
1486
+ if (!toast || !toast.parentNode) return;
1487
+
1488
+ const messageEl = toast.querySelector('.toast-message');
1489
+ const descEl = toast.querySelector('.toast-description');
1490
+
1491
+ if (message && messageEl) {
1492
+ messageEl.textContent = message;
1493
+ }
1494
+
1495
+ if (opts.description && descEl) {
1496
+ descEl.textContent = opts.description;
1497
+ } else if (opts.description) {
1498
+ const descriptionElement = document.createElement("div");
1499
+ descriptionElement.className = "toast-description";
1500
+ descriptionElement.textContent = opts.description;
1501
+ toast.querySelector('.toast-content')?.appendChild(descriptionElement);
1502
+ }
1503
+
1504
+ // Update type/style if provided
1505
+ if (opts.type) {
1506
+ const validTypes = ['success', 'error', 'info', 'warning', 'dark', 'light'];
1507
+ if (validTypes.includes(opts.type)) {
1508
+ validTypes.forEach(t => toast.classList.remove(t));
1509
+ toast.classList.add(opts.type);
1510
+ const iconWrapper = toast.querySelector('.toast-icon');
1511
+ if (iconWrapper) {
1512
+ iconWrapper.innerHTML = this.getIconSVG(opts.type);
1513
+ }
1514
+ }
1515
+ }
1516
+ }
964
1517
 
965
1518
  /**
966
1519
  * Removes a toast with position-aware car swipe animation
@@ -973,6 +1526,25 @@ class ToastifyPro {
973
1526
  }
974
1527
 
975
1528
  try {
1529
+ // Remove from active toasts tracking and set removal flag
1530
+ const toastIndex = this.activeToasts.findIndex(t => t.element === toast);
1531
+ if (toastIndex > -1) {
1532
+ const toastData = this.activeToasts[toastIndex];
1533
+ // Prevent hover events from interfering during removal
1534
+ toastData.isRemoving = true;
1535
+ if (toastData.timeout) {
1536
+ clearTimeout(toastData.timeout);
1537
+ }
1538
+ this.activeToasts.splice(toastIndex, 1);
1539
+ }
1540
+
1541
+ // Mark the toast element as removing to prevent double-removal
1542
+ if (toast.dataset.removing === 'true') return;
1543
+ toast.dataset.removing = 'true';
1544
+
1545
+ // Disable pointer events during exit animation to prevent hover issues
1546
+ toast.style.pointerEvents = 'none';
1547
+
976
1548
  // Detect position to choose the right swipe direction
977
1549
  const container = toast.parentNode;
978
1550
  const position = container.className.split(' ')[1]; // get position class
@@ -993,12 +1565,12 @@ class ToastifyPro {
993
1565
  }
994
1566
 
995
1567
  // Apply fast car swipe animation with improved easing
996
- toast.style.animation = `${swipeAnimation} 0.4s cubic-bezier(0.4, 0.0, 1, 1) forwards`;
1568
+ toast.style.animation = `${swipeAnimation} 0.35s cubic-bezier(0.55, 0.0, 0.85, 0.36) forwards`;
997
1569
 
998
1570
  // Add spinning icon animation for extra polish
999
1571
  const icon = toast.querySelector('.toast-icon');
1000
1572
  if (icon) {
1001
- icon.style.animation = 'iconCarExit 0.4s cubic-bezier(0.4, 0.0, 1, 1) forwards';
1573
+ icon.style.animation = 'iconCarExit 0.35s cubic-bezier(0.55, 0.0, 0.85, 0.36) forwards';
1002
1574
  }
1003
1575
 
1004
1576
  // Remove element after animation completes
@@ -1006,7 +1578,7 @@ class ToastifyPro {
1006
1578
  if (toast.parentNode) {
1007
1579
  toast.remove();
1008
1580
  }
1009
- }, 400);
1581
+ }, 350);
1010
1582
  } catch (error) {
1011
1583
  console.error('ToastifyPro: Error removing toast:', error);
1012
1584
  // Fallback: remove immediately if animation fails
@@ -1015,6 +1587,33 @@ class ToastifyPro {
1015
1587
  }
1016
1588
  }
1017
1589
  }
1590
+
1591
+ /**
1592
+ * Dismisses all active toasts
1593
+ * @param {string} type - Optional: only dismiss toasts of this type
1594
+ */
1595
+ dismissAll(type = null) {
1596
+ const toastsCopy = [...this.activeToasts];
1597
+ toastsCopy.forEach(toastData => {
1598
+ if (toastData.element) {
1599
+ if (type) {
1600
+ if (toastData.element.classList.contains(type)) {
1601
+ this.removeToast(toastData.element);
1602
+ }
1603
+ } else {
1604
+ this.removeToast(toastData.element);
1605
+ }
1606
+ }
1607
+ });
1608
+ }
1609
+
1610
+ /**
1611
+ * Gets the count of active toasts
1612
+ * @returns {number} Number of active toasts
1613
+ */
1614
+ getActiveCount() {
1615
+ return this.activeToasts.length;
1616
+ }
1018
1617
 
1019
1618
  /**
1020
1619
  * Shows a success toast notification
@@ -1189,6 +1788,14 @@ class ToastifyPro {
1189
1788
  const toast = document.createElement("div");
1190
1789
  toast.className = `toastify-pro custom${options.customTextLight ? ' light-text' : ''}`;
1191
1790
 
1791
+ // Store reference to this instance
1792
+ toast._toastInstance = this;
1793
+
1794
+ // ARIA accessibility attributes
1795
+ toast.setAttribute('role', 'status');
1796
+ toast.setAttribute('aria-live', options.ariaLive || 'polite');
1797
+ toast.setAttribute('aria-atomic', 'true');
1798
+
1192
1799
  // Apply custom gradient
1193
1800
  if (options.customGradient) {
1194
1801
  toast.style.background = options.customGradient;
@@ -1201,6 +1808,7 @@ class ToastifyPro {
1201
1808
  // Create icon wrapper
1202
1809
  const iconWrapper = document.createElement("div");
1203
1810
  iconWrapper.className = "toast-icon";
1811
+ iconWrapper.setAttribute('aria-hidden', 'true');
1204
1812
  iconWrapper.innerHTML = this.getIconSVG('success'); // Use success icon for custom
1205
1813
  toast.appendChild(iconWrapper);
1206
1814
 
@@ -1223,15 +1831,53 @@ class ToastifyPro {
1223
1831
  toast.appendChild(contentWrapper);
1224
1832
 
1225
1833
  if (options.allowClose) {
1226
- const closeBtn = document.createElement("span");
1834
+ const closeBtn = document.createElement("button");
1227
1835
  closeBtn.className = "close-btn";
1228
1836
  closeBtn.innerHTML = "&times;";
1837
+ closeBtn.setAttribute('type', 'button');
1229
1838
  closeBtn.setAttribute('aria-label', 'Close notification');
1230
1839
  closeBtn.onclick = () => this.removeToast(toast);
1231
1840
  toast.appendChild(closeBtn);
1232
1841
  }
1233
1842
 
1234
- this.container.appendChild(toast);
1843
+ // Add toast to container (respect newestOnTop setting)
1844
+ if (options.newestOnTop && this.container.firstChild) {
1845
+ this.container.insertBefore(toast, this.container.firstChild);
1846
+ } else {
1847
+ this.container.appendChild(toast);
1848
+ }
1849
+
1850
+ // Track toast for queue management
1851
+ const toastData = {
1852
+ element: toast,
1853
+ timeout: null,
1854
+ remainingTime: options.timeout,
1855
+ startTime: null,
1856
+ isPaused: false
1857
+ };
1858
+ this.activeToasts.push(toastData);
1859
+
1860
+ // Pause on hover functionality
1861
+ if (options.pauseOnHover && options.timeout > 0) {
1862
+ toast.addEventListener('mouseenter', () => {
1863
+ if (toastData.timeout) {
1864
+ clearTimeout(toastData.timeout);
1865
+ toastData.isPaused = true;
1866
+ toastData.remainingTime -= (Date.now() - toastData.startTime);
1867
+ toast.classList.add('paused');
1868
+ }
1869
+ });
1870
+
1871
+ toast.addEventListener('mouseleave', () => {
1872
+ if (toastData.isPaused && toastData.remainingTime > 0) {
1873
+ toastData.isPaused = false;
1874
+ toastData.startTime = Date.now();
1875
+ toast.classList.remove('paused');
1876
+ toast.style.setProperty('--duration', `${toastData.remainingTime}ms`);
1877
+ toastData.timeout = setTimeout(() => this.removeToast(toast), toastData.remainingTime);
1878
+ }
1879
+ });
1880
+ }
1235
1881
 
1236
1882
  setTimeout(() => {
1237
1883
  toast.classList.add("show");
@@ -1242,10 +1888,15 @@ class ToastifyPro {
1242
1888
  }, 10);
1243
1889
 
1244
1890
  if (options.timeout > 0) {
1245
- setTimeout(() => this.removeToast(toast), options.timeout);
1891
+ toastData.startTime = Date.now();
1892
+ toastData.timeout = setTimeout(() => this.removeToast(toast), options.timeout);
1246
1893
  }
1247
1894
 
1248
- return toast;
1895
+ return {
1896
+ element: toast,
1897
+ dismiss: () => this.removeToast(toast),
1898
+ update: (newMessage, newOpts) => this.updateToast(toast, newMessage, newOpts)
1899
+ };
1249
1900
  } catch (error) {
1250
1901
  console.error('ToastifyPro: Failed to create custom toast:', error);
1251
1902
  }
@@ -1555,9 +2206,10 @@ class ToastifyPro {
1555
2206
  }
1556
2207
 
1557
2208
  // Create close button for confirmation
1558
- const closeBtn = document.createElement("span");
2209
+ const closeBtn = document.createElement("button");
1559
2210
  closeBtn.className = "conf-close-btn";
1560
2211
  closeBtn.innerHTML = "&times;";
2212
+ closeBtn.setAttribute('type', 'button');
1561
2213
  closeBtn.setAttribute('aria-label', 'Cancel confirmation');
1562
2214
  closeBtn.onclick = () => {
1563
2215
  if (!isLoading) {
@@ -1573,6 +2225,7 @@ class ToastifyPro {
1573
2225
  // Create icon wrapper
1574
2226
  const iconWrapper = document.createElement("div");
1575
2227
  iconWrapper.className = "toast-icon";
2228
+ iconWrapper.setAttribute('aria-hidden', 'true');
1576
2229
  iconWrapper.innerHTML = this.getIconSVG('info'); // Default to info icon
1577
2230
  if (confirmOptions.primaryColor) {
1578
2231
  iconWrapper.style.color = textColor;
@@ -1593,8 +2246,9 @@ class ToastifyPro {
1593
2246
  contentWrapper.appendChild(messageElement);
1594
2247
 
1595
2248
  // Optional description
2249
+ let descriptionElement = null;
1596
2250
  if (description) {
1597
- const descriptionElement = document.createElement("div");
2251
+ descriptionElement = document.createElement("div");
1598
2252
  descriptionElement.className = "toast-description";
1599
2253
  descriptionElement.textContent = description.substring(0, this.defaultOptions.maxLength * 2);
1600
2254
  if (confirmOptions.primaryColor) {
@@ -1612,6 +2266,7 @@ class ToastifyPro {
1612
2266
  // Cancel button
1613
2267
  const cancelBtn = document.createElement("button");
1614
2268
  cancelBtn.className = "toast-btn toast-btn-cancel";
2269
+ cancelBtn.setAttribute('type', 'button');
1615
2270
  cancelBtn.textContent = confirmOptions.cancelText;
1616
2271
  cancelBtn.onclick = () => {
1617
2272
  if (!isLoading) {
@@ -1633,18 +2288,25 @@ class ToastifyPro {
1633
2288
  // Confirm button
1634
2289
  const confirmBtn = document.createElement("button");
1635
2290
  confirmBtn.className = `toast-btn toast-btn-confirm`;
1636
-
1637
- // Create spinner element
1638
- const spinner = document.createElement("span");
1639
- spinner.className = "btn-spinner";
1640
- confirmBtn.appendChild(spinner);
1641
-
2291
+ confirmBtn.setAttribute('type', 'button');
2292
+
1642
2293
  // Create text wrapper
1643
2294
  const textWrapper = document.createElement("span");
1644
2295
  textWrapper.className = "btn-text";
1645
2296
  textWrapper.textContent = confirmOptions.confirmText;
1646
2297
  confirmBtn.appendChild(textWrapper);
1647
2298
 
2299
+ // Create spinner element with custom SVG
2300
+ const spinner = document.createElement("span");
2301
+ spinner.className = "btn-spinner";
2302
+ spinner.innerHTML = `
2303
+ <svg width="25" height="25" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
2304
+ <path d="M9.5 2.9375V5.5625M9.5 13.4375V16.0625M2.9375 9.5H5.5625M13.4375 9.5H16.0625" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" />
2305
+ <path d="M4.86011 4.85961L6.71627 6.71577M12.2847 12.2842L14.1409 14.1404M4.86011 14.1404L6.71627 12.2842M12.2847 6.71577L14.1409 4.85961" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" />
2306
+ </svg>
2307
+ `;
2308
+ confirmBtn.appendChild(spinner);
2309
+
1648
2310
  confirmBtn.onclick = () => {
1649
2311
  if (!isLoading) {
1650
2312
  handleConfirmation(true);
@@ -1695,6 +2357,53 @@ class ToastifyPro {
1695
2357
  setLoading(true);
1696
2358
  }
1697
2359
 
2360
+ // ARIA accessibility for confirmation dialog
2361
+ toast.setAttribute('role', 'alertdialog');
2362
+ toast.setAttribute('aria-modal', 'true');
2363
+ toast.setAttribute('aria-labelledby', 'toast-conf-title');
2364
+ if (description) {
2365
+ toast.setAttribute('aria-describedby', 'toast-conf-desc');
2366
+ }
2367
+ messageElement.id = 'toast-conf-title';
2368
+ if (description && descriptionElement) {
2369
+ descriptionElement.id = 'toast-conf-desc';
2370
+ }
2371
+
2372
+ // Store previously focused element for restoration
2373
+ const previouslyFocused = document.activeElement;
2374
+
2375
+ // Focus trap for confirmation dialog
2376
+ const focusableElements = [cancelBtn, confirmBtn, closeBtn].filter(Boolean);
2377
+ let currentFocusIndex = 0;
2378
+
2379
+ const handleTabKey = (e) => {
2380
+ if (e.key === 'Tab' && toastElement && toastElement.parentNode) {
2381
+ e.preventDefault();
2382
+ if (e.shiftKey) {
2383
+ currentFocusIndex = (currentFocusIndex - 1 + focusableElements.length) % focusableElements.length;
2384
+ } else {
2385
+ currentFocusIndex = (currentFocusIndex + 1) % focusableElements.length;
2386
+ }
2387
+ focusableElements[currentFocusIndex]?.focus();
2388
+ }
2389
+ };
2390
+
2391
+ document.addEventListener('keydown', handleTabKey);
2392
+
2393
+ // Store cleanup function
2394
+ const originalClose = closeConfirmation;
2395
+ const cleanupAndClose = () => {
2396
+ document.removeEventListener('keydown', handleTabKey);
2397
+ // Restore focus to previously focused element
2398
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
2399
+ setTimeout(() => previouslyFocused.focus(), 100);
2400
+ }
2401
+ originalClose();
2402
+ };
2403
+
2404
+ // Update control object with enhanced close
2405
+ controlObject.close = cleanupAndClose;
2406
+
1698
2407
  // Entrance animation
1699
2408
  setTimeout(() => {
1700
2409
  toast.classList.add("show");
@@ -1702,6 +2411,11 @@ class ToastifyPro {
1702
2411
  if (icon) {
1703
2412
  icon.style.animation = 'iconBounce 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
1704
2413
  }
2414
+
2415
+ // Focus the confirm button after animation
2416
+ setTimeout(() => {
2417
+ confirmBtn.focus();
2418
+ }, 100);
1705
2419
  }, 10);
1706
2420
 
1707
2421
  // Return control object with toast element and control functions
@@ -1720,6 +2434,527 @@ class ToastifyPro {
1720
2434
  confirm(message, descriptionOrCallback, callback) {
1721
2435
  return this.conf(message, descriptionOrCallback, callback);
1722
2436
  }
2437
+
2438
+ /**
2439
+ * Shows an input prompt toast with a text field
2440
+ * @param {string} message - Main prompt question
2441
+ * @param {string|Function|Object} descriptionOrCallback - Description text, callback function, or options object
2442
+ * @param {Function} callback - Callback function (if description provided)
2443
+ * @returns {Object} Control object with element, close, setValue, getValue methods
2444
+ *
2445
+ * Options object:
2446
+ * - description: {string} Optional description text
2447
+ * - placeholder: {string} Input placeholder text (default: 'Enter your response...')
2448
+ * - submitText: {string} Submit button text (default: 'Submit')
2449
+ * - cancelText: {string} Cancel button text (default: 'Cancel')
2450
+ * - defaultValue: {string} Default input value
2451
+ * - required: {boolean} Whether input is required (default: true)
2452
+ * - type: {string} Input type: 'text', 'email', 'number', 'password', 'url' (default: 'text')
2453
+ * - validate: {Function} Custom validation function (receives value, returns true or error string)
2454
+ * - onSubmit: {Function} Called when user submits (receives value, control object)
2455
+ * - onCancel: {Function} Called when user cancels
2456
+ * - theme: {string} Toast theme: 'dark' or 'light' (default: 'dark')
2457
+ * - position: {string} Override default position
2458
+ * - overlay: {boolean} Show overlay behind toast (default: true)
2459
+ * - primaryColor: {string} Custom primary color
2460
+ * - secondaryColor: {string} Custom secondary color for gradient
2461
+ *
2462
+ * @example
2463
+ * // Simple usage
2464
+ * toast.input('What is your name?', (value) => console.log(value));
2465
+ *
2466
+ * // With description
2467
+ * toast.input('Enter your email', 'We will send you updates', (value) => {...});
2468
+ *
2469
+ * // Full options
2470
+ * toast.input('Enter password', {
2471
+ * type: 'password',
2472
+ * placeholder: 'Minimum 8 characters',
2473
+ * validate: (val) => val.length >= 8 || 'Password must be at least 8 characters',
2474
+ * onSubmit: (value) => { ... },
2475
+ * theme: 'light'
2476
+ * });
2477
+ */
2478
+ input(message, descriptionOrCallback, callback) {
2479
+ try {
2480
+ // Parse arguments like confirm method
2481
+ let description = '';
2482
+ let options = {};
2483
+ let resultCallback = null;
2484
+
2485
+ if (typeof descriptionOrCallback === 'string') {
2486
+ description = descriptionOrCallback;
2487
+ if (typeof callback === 'function') {
2488
+ resultCallback = callback;
2489
+ } else if (typeof callback === 'object') {
2490
+ options = callback || {};
2491
+ resultCallback = options.onSubmit || null;
2492
+ }
2493
+ } else if (typeof descriptionOrCallback === 'function') {
2494
+ resultCallback = descriptionOrCallback;
2495
+ } else if (typeof descriptionOrCallback === 'object') {
2496
+ options = descriptionOrCallback || {};
2497
+ description = options.description || '';
2498
+ resultCallback = options.onSubmit || null;
2499
+ }
2500
+
2501
+ // Default options
2502
+ const inputOptions = {
2503
+ placeholder: options.placeholder || 'Enter your response...',
2504
+ submitText: options.submitText || 'Submit',
2505
+ cancelText: options.cancelText || 'Cancel',
2506
+ defaultValue: options.defaultValue || '',
2507
+ required: options.required !== false, // default true
2508
+ type: options.type || 'text',
2509
+ validate: options.validate || null,
2510
+ onCancel: options.onCancel || null,
2511
+ theme: options.theme || 'dark',
2512
+ position: options.position || this.defaultOptions.position,
2513
+ overlay: options.overlay !== false, // default true
2514
+ primaryColor: options.primaryColor || null,
2515
+ secondaryColor: options.secondaryColor || null,
2516
+ };
2517
+
2518
+ // Validate input type
2519
+ const validTypes = ['text', 'email', 'number', 'password', 'url', 'tel'];
2520
+ if (!validTypes.includes(inputOptions.type)) {
2521
+ inputOptions.type = 'text';
2522
+ }
2523
+
2524
+ // Create or get container for the specified position
2525
+ let inputContainer;
2526
+ const positionClass = inputOptions.position.replace(' ', '-');
2527
+ const existingContainer = document.querySelector(
2528
+ `.toastify-pro-container.${positionClass}`
2529
+ );
2530
+
2531
+ if (existingContainer) {
2532
+ inputContainer = existingContainer;
2533
+ } else {
2534
+ inputContainer = document.createElement("div");
2535
+ inputContainer.className = `toastify-pro-container ${positionClass}`;
2536
+ document.body.appendChild(inputContainer);
2537
+ }
2538
+
2539
+ // Create overlay if enabled
2540
+ let overlay = null;
2541
+ if (inputOptions.overlay) {
2542
+ overlay = document.createElement("div");
2543
+ overlay.className = "toastify-pro-overlay";
2544
+ document.body.appendChild(overlay);
2545
+ setTimeout(() => overlay.classList.add("visible"), 10);
2546
+ }
2547
+
2548
+ // Create input toast element
2549
+ const toast = document.createElement("div");
2550
+ toast.className = `toastify-pro input-toast ${inputOptions.theme}`;
2551
+ toast.setAttribute('role', 'dialog');
2552
+ toast.setAttribute('aria-modal', 'true');
2553
+ toast.setAttribute('aria-labelledby', 'toast-input-title');
2554
+
2555
+ // Track state
2556
+ let isLoading = false;
2557
+ let isClosed = false;
2558
+ let inputElement = null;
2559
+ let submitBtnElement = null;
2560
+ let cancelBtnElement = null;
2561
+ let errorElement = null;
2562
+
2563
+ // Helper: Check if a color is light or dark
2564
+ const isLightColor = (color) => {
2565
+ if (!color) return false;
2566
+ const hex = color.replace('#', '');
2567
+ if (hex.length === 3) {
2568
+ const r = parseInt(hex[0] + hex[0], 16);
2569
+ const g = parseInt(hex[1] + hex[1], 16);
2570
+ const b = parseInt(hex[2] + hex[2], 16);
2571
+ return (r * 299 + g * 587 + b * 114) / 1000 > 128;
2572
+ }
2573
+ const r = parseInt(hex.substr(0, 2), 16);
2574
+ const g = parseInt(hex.substr(2, 2), 16);
2575
+ const b = parseInt(hex.substr(4, 2), 16);
2576
+ return (r * 299 + g * 587 + b * 114) / 1000 > 128;
2577
+ };
2578
+
2579
+ // Apply custom colors
2580
+ let textColor = inputOptions.theme === 'light' ? '#1e293b' : 'white';
2581
+ if (inputOptions.primaryColor) {
2582
+ const primary = inputOptions.primaryColor;
2583
+ const secondary = inputOptions.secondaryColor;
2584
+
2585
+ if (secondary) {
2586
+ toast.style.background = `linear-gradient(135deg, ${primary} 0%, ${secondary} 100%)`;
2587
+ } else {
2588
+ toast.style.background = `linear-gradient(135deg, ${primary} 0%, ${primary}dd 100%)`;
2589
+ }
2590
+
2591
+ textColor = isLightColor(primary) ? '#1e293b' : 'white';
2592
+ toast.style.color = textColor;
2593
+
2594
+ const borderOpacity = isLightColor(primary) ? '0.2' : '0.15';
2595
+ toast.style.borderColor = `rgba(255, 255, 255, ${borderOpacity})`;
2596
+ }
2597
+
2598
+ // Set loading state
2599
+ const setLoading = (loading) => {
2600
+ isLoading = loading;
2601
+ if (submitBtnElement) {
2602
+ if (loading) {
2603
+ submitBtnElement.classList.add('loading');
2604
+ } else {
2605
+ submitBtnElement.classList.remove('loading');
2606
+ }
2607
+ }
2608
+ if (cancelBtnElement) {
2609
+ cancelBtnElement.disabled = loading;
2610
+ cancelBtnElement.style.opacity = loading ? '0.5' : '1';
2611
+ cancelBtnElement.style.cursor = loading ? 'not-allowed' : 'pointer';
2612
+ }
2613
+ if (inputElement) {
2614
+ inputElement.disabled = loading;
2615
+ inputElement.style.opacity = loading ? '0.6' : '1';
2616
+ }
2617
+ };
2618
+
2619
+ // Close the input toast
2620
+ const closeInput = () => {
2621
+ if (isClosed) return;
2622
+ isClosed = true;
2623
+
2624
+ // Remove overlay
2625
+ if (overlay) {
2626
+ overlay.classList.remove("visible");
2627
+ setTimeout(() => overlay.remove(), 300);
2628
+ }
2629
+
2630
+ // Exit animation
2631
+ toast.style.pointerEvents = 'none';
2632
+ toast.style.animation = 'carSwipeCenter 0.35s cubic-bezier(0.55, 0.0, 0.85, 0.36) forwards';
2633
+
2634
+ const icon = toast.querySelector('.toast-icon');
2635
+ if (icon) {
2636
+ icon.style.animation = 'iconCarExit 0.35s cubic-bezier(0.55, 0.0, 0.85, 0.36) forwards';
2637
+ }
2638
+
2639
+ setTimeout(() => {
2640
+ if (toast.parentNode) {
2641
+ toast.remove();
2642
+ }
2643
+ }, 350);
2644
+
2645
+ // Restore focus
2646
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
2647
+ setTimeout(() => previouslyFocused.focus(), 100);
2648
+ }
2649
+
2650
+ // Cleanup keyboard handler
2651
+ document.removeEventListener('keydown', handleKeyDown);
2652
+ };
2653
+
2654
+ // Validate input
2655
+ const validateInput = (value) => {
2656
+ // Required check
2657
+ if (inputOptions.required && !value.trim()) {
2658
+ return 'This field is required';
2659
+ }
2660
+
2661
+ // Type-specific validation
2662
+ if (value.trim()) {
2663
+ if (inputOptions.type === 'email') {
2664
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2665
+ if (!emailRegex.test(value)) {
2666
+ return 'Please enter a valid email address';
2667
+ }
2668
+ } else if (inputOptions.type === 'url') {
2669
+ try {
2670
+ new URL(value);
2671
+ } catch {
2672
+ return 'Please enter a valid URL';
2673
+ }
2674
+ } else if (inputOptions.type === 'number') {
2675
+ if (isNaN(Number(value))) {
2676
+ return 'Please enter a valid number';
2677
+ }
2678
+ }
2679
+ }
2680
+
2681
+ // Custom validation
2682
+ if (inputOptions.validate) {
2683
+ const customResult = inputOptions.validate(value);
2684
+ if (customResult !== true && customResult) {
2685
+ return customResult;
2686
+ }
2687
+ }
2688
+
2689
+ return null; // Valid
2690
+ };
2691
+
2692
+ // Show error message
2693
+ const showError = (message) => {
2694
+ if (errorElement) {
2695
+ errorElement.textContent = message;
2696
+ errorElement.classList.add('visible');
2697
+ inputElement.classList.add('error');
2698
+ }
2699
+ };
2700
+
2701
+ // Clear error message
2702
+ const clearError = () => {
2703
+ if (errorElement) {
2704
+ errorElement.classList.remove('visible');
2705
+ inputElement.classList.remove('error');
2706
+ }
2707
+ };
2708
+
2709
+ // Handle submit
2710
+ const handleSubmit = async () => {
2711
+ if (isLoading || isClosed) return;
2712
+
2713
+ const value = inputElement.value;
2714
+ const error = validateInput(value);
2715
+
2716
+ if (error) {
2717
+ showError(error);
2718
+ inputElement.focus();
2719
+ return;
2720
+ }
2721
+
2722
+ clearError();
2723
+
2724
+ if (resultCallback) {
2725
+ const result = resultCallback(value, { setLoading, close: closeInput, setValue: (v) => inputElement.value = v });
2726
+
2727
+ // Handle async callbacks
2728
+ if (result && typeof result.then === 'function') {
2729
+ setLoading(true);
2730
+ try {
2731
+ await result;
2732
+ if (!isClosed) {
2733
+ closeInput();
2734
+ }
2735
+ } catch (err) {
2736
+ setLoading(false);
2737
+ if (err && typeof err === 'string') {
2738
+ showError(err);
2739
+ } else if (err && err.message) {
2740
+ showError(err.message);
2741
+ }
2742
+ }
2743
+ } else if (!isLoading) {
2744
+ closeInput();
2745
+ }
2746
+ } else {
2747
+ closeInput();
2748
+ }
2749
+ };
2750
+
2751
+ // Handle cancel
2752
+ const handleCancel = () => {
2753
+ if (isLoading || isClosed) return;
2754
+
2755
+ if (inputOptions.onCancel) {
2756
+ inputOptions.onCancel();
2757
+ }
2758
+ closeInput();
2759
+ };
2760
+
2761
+ // Store previously focused element
2762
+ const previouslyFocused = document.activeElement;
2763
+
2764
+ // Keyboard handler
2765
+ const handleKeyDown = (e) => {
2766
+ if (isClosed) return;
2767
+
2768
+ if (e.key === 'Escape' && !isLoading) {
2769
+ handleCancel();
2770
+ } else if (e.key === 'Enter' && e.target === inputElement && !isLoading) {
2771
+ e.preventDefault();
2772
+ handleSubmit();
2773
+ }
2774
+ };
2775
+
2776
+ document.addEventListener('keydown', handleKeyDown);
2777
+
2778
+ // Build toast content
2779
+ // Header with icon and content
2780
+ const headerWrapper = document.createElement("div");
2781
+ headerWrapper.className = "toast-input-header";
2782
+
2783
+ // Icon
2784
+ const iconWrapper = document.createElement("div");
2785
+ iconWrapper.className = "toast-icon";
2786
+ iconWrapper.setAttribute('aria-hidden', 'true');
2787
+ iconWrapper.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2788
+ <path d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2789
+ <path d="M18.5 2.50001C18.8978 2.10219 19.4374 1.87869 20 1.87869C20.5626 1.87869 21.1022 2.10219 21.5 2.50001C21.8978 2.89784 22.1213 3.43739 22.1213 4.00001C22.1213 4.56262 21.8978 5.10219 21.5 5.50001L12 15L8 16L9 12L18.5 2.50001Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2790
+ </svg>`;
2791
+ if (inputOptions.primaryColor) {
2792
+ iconWrapper.style.color = textColor;
2793
+ }
2794
+ headerWrapper.appendChild(iconWrapper);
2795
+
2796
+ // Content wrapper
2797
+ const contentWrapper = document.createElement("div");
2798
+ contentWrapper.className = "toast-content";
2799
+
2800
+ // Message
2801
+ const messageElement = document.createElement("div");
2802
+ messageElement.className = "toast-message";
2803
+ messageElement.id = "toast-input-title";
2804
+ messageElement.textContent = message.substring(0, this.defaultOptions.maxLength);
2805
+ if (inputOptions.primaryColor) {
2806
+ messageElement.style.color = textColor;
2807
+ }
2808
+ contentWrapper.appendChild(messageElement);
2809
+
2810
+ // Description
2811
+ if (description) {
2812
+ const descriptionElement = document.createElement("div");
2813
+ descriptionElement.className = "toast-description";
2814
+ descriptionElement.id = "toast-input-desc";
2815
+ descriptionElement.textContent = description.substring(0, this.defaultOptions.maxLength * 2);
2816
+ if (inputOptions.primaryColor) {
2817
+ descriptionElement.style.color = textColor;
2818
+ }
2819
+ contentWrapper.appendChild(descriptionElement);
2820
+ toast.setAttribute('aria-describedby', 'toast-input-desc');
2821
+ }
2822
+
2823
+ headerWrapper.appendChild(contentWrapper);
2824
+
2825
+ // Input wrapper
2826
+ const inputWrapper = document.createElement("div");
2827
+ inputWrapper.className = "toast-input-wrapper";
2828
+ inputWrapper.appendChild(headerWrapper);
2829
+
2830
+ // Input field
2831
+ inputElement = document.createElement("input");
2832
+ inputElement.className = "toast-input";
2833
+ inputElement.type = inputOptions.type;
2834
+ inputElement.placeholder = inputOptions.placeholder;
2835
+ inputElement.value = inputOptions.defaultValue;
2836
+ inputElement.setAttribute('aria-label', message);
2837
+
2838
+ if (inputOptions.primaryColor) {
2839
+ const isLight = isLightColor(inputOptions.primaryColor);
2840
+ inputElement.style.borderColor = isLight ? 'rgba(15, 23, 42, 0.2)' : 'rgba(255, 255, 255, 0.2)';
2841
+ inputElement.style.background = isLight ? 'rgba(15, 23, 42, 0.06)' : 'rgba(255, 255, 255, 0.08)';
2842
+ inputElement.style.color = textColor;
2843
+ }
2844
+
2845
+ // Clear error on input
2846
+ inputElement.addEventListener('input', () => {
2847
+ if (errorElement && errorElement.classList.contains('visible')) {
2848
+ clearError();
2849
+ }
2850
+ });
2851
+
2852
+ inputWrapper.appendChild(inputElement);
2853
+
2854
+ // Error message element
2855
+ errorElement = document.createElement("div");
2856
+ errorElement.className = "toast-input-error";
2857
+ errorElement.setAttribute('role', 'alert');
2858
+ inputWrapper.appendChild(errorElement);
2859
+
2860
+ // Actions wrapper
2861
+ const actionsWrapper = document.createElement("div");
2862
+ actionsWrapper.className = "toast-input-actions";
2863
+
2864
+ // Cancel button
2865
+ cancelBtnElement = document.createElement("button");
2866
+ cancelBtnElement.className = "input-btn input-btn-cancel";
2867
+ cancelBtnElement.type = "button";
2868
+ cancelBtnElement.textContent = inputOptions.cancelText;
2869
+ cancelBtnElement.onclick = handleCancel;
2870
+
2871
+ if (inputOptions.primaryColor) {
2872
+ const isLight = isLightColor(inputOptions.primaryColor);
2873
+ cancelBtnElement.style.background = isLight ? 'rgba(15, 23, 42, 0.08)' : 'rgba(255, 255, 255, 0.1)';
2874
+ cancelBtnElement.style.color = textColor;
2875
+ cancelBtnElement.style.borderColor = isLight ? 'rgba(15, 23, 42, 0.2)' : 'rgba(255, 255, 255, 0.2)';
2876
+ }
2877
+
2878
+ // Submit button
2879
+ submitBtnElement = document.createElement("button");
2880
+ submitBtnElement.className = "input-btn input-btn-submit";
2881
+ submitBtnElement.type = "button";
2882
+
2883
+ const textWrapper = document.createElement("span");
2884
+ textWrapper.className = "btn-text";
2885
+ textWrapper.textContent = inputOptions.submitText;
2886
+ submitBtnElement.appendChild(textWrapper);
2887
+
2888
+ // Spinner
2889
+ const spinner = document.createElement("span");
2890
+ spinner.className = "btn-spinner";
2891
+ spinner.innerHTML = `<svg width="16" height="16" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
2892
+ <path d="M9.5 2.9375V5.5625M9.5 13.4375V16.0625M2.9375 9.5H5.5625M13.4375 9.5H16.0625" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" />
2893
+ <path d="M4.86011 4.85961L6.71627 6.71577M12.2847 12.2842L14.1409 14.1404M4.86011 14.1404L6.71627 12.2842M12.2847 6.71577L14.1409 4.85961" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" />
2894
+ </svg>`;
2895
+ submitBtnElement.appendChild(spinner);
2896
+
2897
+ submitBtnElement.onclick = handleSubmit;
2898
+
2899
+ if (inputOptions.primaryColor) {
2900
+ const primary = inputOptions.primaryColor;
2901
+ const isLight = isLightColor(primary);
2902
+ submitBtnElement.style.background = isLight
2903
+ ? 'linear-gradient(135deg, #1e293b 0%, #334155 100%)'
2904
+ : 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%)';
2905
+ submitBtnElement.style.color = isLight ? 'white' : '#1e293b';
2906
+ submitBtnElement.style.borderColor = isLight ? 'rgba(15, 23, 42, 0.3)' : 'rgba(255, 255, 255, 0.4)';
2907
+ }
2908
+
2909
+ actionsWrapper.appendChild(cancelBtnElement);
2910
+ actionsWrapper.appendChild(submitBtnElement);
2911
+ inputWrapper.appendChild(actionsWrapper);
2912
+
2913
+ toast.appendChild(inputWrapper);
2914
+ inputContainer.appendChild(toast);
2915
+
2916
+ // Entrance animation
2917
+ setTimeout(() => {
2918
+ toast.classList.add("show");
2919
+ const icon = toast.querySelector('.toast-icon');
2920
+ if (icon) {
2921
+ icon.style.animation = 'iconBounce 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
2922
+ }
2923
+
2924
+ // Focus input after animation
2925
+ setTimeout(() => {
2926
+ inputElement.focus();
2927
+ // Select default value if present
2928
+ if (inputOptions.defaultValue) {
2929
+ inputElement.select();
2930
+ }
2931
+ }, 150);
2932
+ }, 10);
2933
+
2934
+ // Return control object
2935
+ return {
2936
+ element: toast,
2937
+ close: closeInput,
2938
+ setLoading,
2939
+ getValue: () => inputElement.value,
2940
+ setValue: (value) => { inputElement.value = value; },
2941
+ setError: showError,
2942
+ clearError
2943
+ };
2944
+ } catch (error) {
2945
+ console.error('ToastifyPro: Failed to create input toast:', error);
2946
+ }
2947
+ }
2948
+
2949
+ /**
2950
+ * Alias for input() method - shows an input prompt
2951
+ * @param {string} message - Main prompt question
2952
+ * @param {string|Function|Object} descriptionOrCallback - Description text, callback function, or options object
2953
+ * @param {Function} callback - Callback function (if description provided)
2954
+ */
2955
+ prompt(message, descriptionOrCallback, callback) {
2956
+ return this.input(message, descriptionOrCallback, callback);
2957
+ }
1723
2958
  }
1724
2959
 
1725
2960
  /**