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