fernotify 1.2.1 → 1.2.8

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.
@@ -13,52 +13,39 @@
13
13
  */
14
14
 
15
15
  // Importar el código UMD y ejecutarlo para generar window.notify
16
- /**
17
- * Sistema de Notificaciones Modernas
18
- * Librería ligera de notificaciones con animaciones fluidas
19
- * Dependencias automáticas: anime.js, Boxicons
20
- */
21
-
22
- (function ensureAnimeDependency() {
23
- if (typeof anime !== 'undefined') {
24
- initFerNotify();
25
- } else {
26
- const script = document.createElement('script');
27
- script.src = 'https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js';
28
- script.onload = initFerNotify;
29
- script.onerror = () => {
30
- console.error('FerNotify: No se pudo cargar anime.js. Por favor, cargalo manualmente.');
31
- };
32
- document.head.appendChild(script);
33
- }
34
-
35
- function initFerNotify() {
36
- class NotificationSystem {
37
- constructor() {
38
- this.currentNotification = null;
39
- this._lastActiveElement = null;
40
- this._currentLoadingPromise = null;
41
- this.injectStyles();
42
- this.loadBoxicons();
43
- }
44
-
45
- /**
46
- * Cargar Boxicons CSS si no está presente
47
- */
48
- loadBoxicons() {
49
- if (!document.querySelector('link[href*="boxicons"]')) {
50
- const link = document.createElement('link');
51
- link.rel = 'stylesheet';
52
- link.href = 'https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css';
53
- document.head.appendChild(link);
54
- }
55
- }
56
-
57
- /**
58
- * Inyectar estilos CSS
59
- */
60
- injectStyles() {
61
- const style = document.createElement('style');
16
+ "use strict";
17
+ (function ensureAnimeDependency() {
18
+ if (typeof anime !== 'undefined') {
19
+ initFerNotify();
20
+ }
21
+ else {
22
+ const script = document.createElement('script');
23
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js';
24
+ script.onload = initFerNotify;
25
+ script.onerror = () => {
26
+ console.error('FerNotify: No se pudo cargar anime.js. Por favor, cargalo manualmente.');
27
+ };
28
+ document.head.appendChild(script);
29
+ }
30
+ function initFerNotify() {
31
+ class NotificationSystem {
32
+ constructor() {
33
+ this.currentNotification = null;
34
+ this._lastActiveElement = null;
35
+ this._currentLoadingPromise = null;
36
+ this.injectStyles();
37
+ this.loadBoxicons();
38
+ }
39
+ loadBoxicons() {
40
+ if (!document.querySelector('link[href*="boxicons"]')) {
41
+ const link = document.createElement('link');
42
+ link.rel = 'stylesheet';
43
+ link.href = 'https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css';
44
+ document.head.appendChild(link);
45
+ }
46
+ }
47
+ injectStyles() {
48
+ const style = document.createElement('style');
62
49
  style.textContent = `
63
50
  .notification-overlay {
64
51
  position: fixed;
@@ -228,6 +215,14 @@
228
215
  .notification-icon.info::before {
229
216
  background: #3b82f6;
230
217
  }
218
+ .notification-icon.question {
219
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
220
+ color: white;
221
+ }
222
+
223
+ .notification-icon.question::before {
224
+ background: #3b82f6;
225
+ }
231
226
 
232
227
  .notification-title {
233
228
  font-size: 24px;
@@ -264,6 +259,15 @@
264
259
  transform: translateY(0);
265
260
  }
266
261
 
262
+ /* group container for multiple action buttons */
263
+ .notification-button-group {
264
+ display: flex;
265
+ gap: 12px;
266
+ justify-content: center;
267
+ flex-wrap: wrap;
268
+ margin-top: 10px;
269
+ }
270
+
267
271
  .notification-icon-checkmark {
268
272
  animation: checkmark-draw 0.6s ease-in-out;
269
273
  }
@@ -335,587 +339,520 @@
335
339
  .dark .notification-loading-text {
336
340
  color: #cbd5e1;
337
341
  }
338
- `;
339
- document.head.appendChild(style);
340
- }
341
-
342
- /**
343
- * Obtener ícono según el tipo
344
- */
345
- getIcon(type) {
346
- // Return a Boxicons markup string — presentation/index.php now imports the CSS.
347
- const icons = {
348
- 'success': '<i class="bx bx-check" aria-hidden="true"></i>',
349
- 'error': '<i class="bx bx-x" aria-hidden="true"></i>',
350
- 'warning': '<i class="bx bx-error" aria-hidden="true"></i>',
351
- 'info': '<i class="bx bx-info-circle" aria-hidden="true"></i>'
352
- };
353
- return icons[type] || icons.info;
354
- }
355
-
356
- /**
357
- * Obtener título por defecto según el tipo
358
- */
359
- getDefaultTitle(type) {
360
- const titles = {
361
- 'success': '¡Éxito!',
362
- 'error': 'Error',
363
- 'warning': 'Advertencia',
364
- 'info': 'Información'
365
- };
366
- return titles[type] || 'Notificación';
367
- }
368
-
369
- /**
370
- * Obtener gradiente de botón según el tipo
371
- */
372
- getButtonGradient(type) {
373
- const gradients = {
374
- 'success': 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
375
- 'error': 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
376
- 'warning': 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
377
- 'info': 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)'
378
- };
379
- return gradients[type] || gradients.info;
380
- }
381
-
382
- /**
383
- * Obtener color de sombra según el tipo
384
- */
385
- getButtonShadow(type) {
386
- const shadows = {
387
- 'success': 'rgba(16, 185, 129, 0.4)',
388
- 'error': 'rgba(239, 68, 68, 0.4)',
389
- 'warning': 'rgba(245, 158, 11, 0.4)',
390
- 'info': 'rgba(59, 130, 246, 0.4)'
391
- };
392
- return shadows[type] || shadows.info;
393
- }
394
-
395
- /**
396
- * Mostrar notificación
397
- *
398
- * @param {Object} options - Opciones de la notificación
399
- * @param {string} options.type - Tipo: 'success', 'error', 'warning', 'info'
400
- * @param {string} options.title - Título (opcional, usa título por defecto)
401
- * @param {string} options.message - Mensaje a mostrar
402
- * @param {string} options.buttonText - Texto del botón (opcional, default: 'OK')
403
- * @param {string} options.buttonColor - Color del botón en formato CSS (opcional, usa color del tipo por defecto)
404
- * @param {Function} options.onClose - Callback al cerrar (opcional)
405
- * @param {number} options.timer - Auto-cerrar después de X ms (opcional)
406
- * @param {boolean} options.allowOutsideClick - Permitir cerrar haciendo click fuera (default: true)
407
- * @param {boolean} options.allowEscapeKey - Permitir cerrar con tecla ESC (default: true)
408
- */
409
- show(options = {}) {
410
- // Cerrar notificación existente si hay (esperar a que termine)
411
- if (this.currentNotification) {
412
- // Asegurar que se elimine completamente antes de continuar
413
- const oldOverlay = this.currentNotification;
414
- this.currentNotification = null;
415
- try {
416
- if (oldOverlay && oldOverlay.parentNode) {
417
- oldOverlay.parentNode.removeChild(oldOverlay);
418
- }
419
- } catch (e) { }
420
- }
421
-
422
- const {
423
- type = 'info',
424
- title = this.getDefaultTitle(type),
425
- message = '',
426
- buttonText = 'OK',
427
- buttonColor = null,
428
- onClose = null,
429
- timer = null,
430
- allowOutsideClick = true,
431
- allowEscapeKey = true,
432
- // New option: hideButton true -> do not render action button
433
- hideButton = false
434
- } = options;
435
-
436
- // Option to show a small close 'X' in the corner
437
- const showCloseButton = options.showCloseButton === true;
438
-
439
- // Bloquear scroll del body y root (más robusto)
440
- try { document.body.style.overflow = 'hidden'; } catch (e) { }
441
- try { document.documentElement.style.overflow = 'hidden'; } catch (e) { }
442
-
443
- // Crear overlay
444
- const overlay = document.createElement('div');
445
- overlay.className = 'notification-overlay';
446
- // Accessibility: make overlay focusable and a dialog
447
- overlay.tabIndex = -1;
448
- overlay.setAttribute('role', 'dialog');
449
- overlay.setAttribute('aria-modal', 'true');
450
- // Ensure overlay receives pointer events
451
- overlay.style.pointerEvents = 'auto';
452
-
453
- // Crear box
454
- const box = document.createElement('div');
455
- box.className = 'notification-box';
456
-
457
- // Crear ícono
458
- const icon = document.createElement('div');
459
- icon.className = `notification-icon ${type}`;
460
-
461
- // Si la opción hideButton es true Y message contiene patrón de loading, mostrar spinner
462
- if (hideButton && type === 'info') {
463
- // Mostrar spinner para notificación de carga
464
- icon.className = 'notification-loading-container';
465
- icon.innerHTML = '<div class="notification-spinner"></div>';
466
- icon.style.background = 'transparent';
467
- icon.style.boxShadow = 'none';
468
- // Mantener tamaño adecuado para el spinner
469
- icon.style.width = '100px';
470
- icon.style.height = '100px';
471
- } else {
472
- icon.innerHTML = this.getIcon(type);
473
- }
474
-
475
- // Crear título
476
- const titleElement = document.createElement('h3');
477
- titleElement.className = 'notification-title';
478
- titleElement.textContent = title;
479
-
480
- // Crear mensaje
481
- const messageElement = document.createElement('p');
482
- messageElement.className = 'notification-message';
483
- messageElement.textContent = message;
484
-
485
- // Custom content support: options.html (string) or options.content (HTMLElement)
486
- let customContent = null;
487
- if (options.html || options.content) {
488
- customContent = document.createElement('div');
489
- customContent.className = 'notification-content';
490
- if (options.html) {
491
- try { customContent.innerHTML = options.html; } catch (e) { customContent.textContent = options.html; }
492
- } else if (options.content && options.content instanceof HTMLElement) {
493
- customContent.appendChild(options.content);
494
- }
495
- }
496
-
497
- // Crear (opcional) botón. Si hideButton === true o buttonText es falsy, no renderizamos.
498
- let button = null;
499
- if (!hideButton && buttonText) {
500
- button = document.createElement('button');
501
- button.className = 'notification-button';
502
- button.textContent = buttonText;
503
-
504
- // Aplicar color del botón (personalizado o automático según tipo)
505
- const finalButtonColor = buttonColor || this.getButtonGradient(type);
506
- const buttonShadowColor = this.getButtonShadow(type);
507
- button.style.background = finalButtonColor;
508
- button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
509
- }
510
-
511
- // Close 'X' button in corner (optional)
512
- let closeBtn = null;
513
- if (showCloseButton) {
514
- closeBtn = document.createElement('button');
515
- closeBtn.setAttribute('aria-label', 'Cerrar');
516
- closeBtn.className = 'notification-close';
517
- closeBtn.innerHTML = '&times;';
518
- closeBtn.addEventListener('click', (e) => {
519
- e.stopPropagation();
520
- closeHandler();
521
- });
522
- }
523
-
524
- // Ensamblar
525
- box.appendChild(icon);
526
- // If customContent provided, prefer it. Otherwise render title/message as before.
527
- if (customContent) {
528
- // For accessibility, link aria-describedby to the content
529
- const descId = 'notify-desc-' + Date.now();
530
- customContent.id = descId;
531
- overlay.setAttribute('aria-describedby', descId);
532
- box.appendChild(customContent);
533
- } else {
534
- box.appendChild(titleElement);
535
- box.appendChild(messageElement);
536
- }
537
- // Append close button last so it's visually on top
538
- if (closeBtn) box.appendChild(closeBtn);
539
- if (button) box.appendChild(button);
540
- overlay.appendChild(box);
541
- document.body.appendChild(overlay);
542
-
543
- // Provide a Promise that resolves when this notification is closed.
544
- const closePromise = new Promise((resolveClose) => {
545
- try { overlay._externalResolve = resolveClose; } catch (e) { /* ignore */ }
546
- });
547
-
548
- // If page provides a live region, update it for screen readers
549
- try {
550
- const live = document.getElementById('notify-live');
551
- if (live) {
552
- live.textContent = `${title}: ${message}`;
553
- }
554
- } catch (e) { }
555
-
556
- // Save current focused element to restore later
557
- try { this._lastActiveElement = document.activeElement; } catch (e) { this._lastActiveElement = null; }
558
-
559
- this.currentNotification = overlay;
560
-
561
- // Move focus into the notification for accessibility
562
- // Focus management: focus first focusable element inside box, otherwise the button, otherwise overlay
563
- try {
564
- const focusable = box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])');
565
- if (focusable && focusable.length) {
566
- focusable[0].focus();
567
- } else if (button) {
568
- button.focus();
569
- } else {
570
- overlay.focus();
571
- }
572
- } catch (e) { try { overlay.focus(); } catch (err) { } }
573
-
574
- // Implement focus trap (Tab/Shift+Tab) inside the box
575
- const focusTrap = (e) => {
576
- if (e.key !== 'Tab') return;
577
- const focusable = Array.from(box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'))
578
- .filter(el => el.offsetParent !== null);
579
- if (!focusable.length) {
580
- e.preventDefault();
581
- return;
582
- }
583
- const first = focusable[0];
584
- const last = focusable[focusable.length - 1];
585
- if (!e.shiftKey && document.activeElement === last) {
586
- e.preventDefault();
587
- first.focus();
588
- } else if (e.shiftKey && document.activeElement === first) {
589
- e.preventDefault();
590
- last.focus();
591
- }
592
- };
593
- overlay._focusTrap = focusTrap;
594
- document.addEventListener('keydown', focusTrap);
595
-
596
- // Allow optional animation overrides via options.anim
597
- const anim = options.anim || {};
598
- const overlayDuration = typeof anim.overlayDuration === 'number' ? anim.overlayDuration : 150;
599
- const overlayEasing = anim.overlayEasing || 'easeOutQuad';
600
- const boxDuration = typeof anim.boxDuration === 'number' ? anim.boxDuration : 200;
601
- const boxDelay = typeof anim.boxDelay === 'number' ? anim.boxDelay : 50;
602
- const boxEasing = anim.boxEasing || 'easeOutBack';
603
- const boxStartScale = typeof anim.boxStartScale === 'number' ? anim.boxStartScale : 0.8;
604
- const iconDuration = typeof anim.iconDuration === 'number' ? anim.iconDuration : 250;
605
- const iconDelay = typeof anim.iconDelay === 'number' ? anim.iconDelay : 100;
606
- const iconRotate = (typeof anim.iconRotate === 'number') ? anim.iconRotate : (type === 'success' ? -90 : type === 'error' ? 90 : 0);
607
- if (typeof anim.overlayOpacity === 'number') {
608
- overlay.style.backgroundColor = `rgba(0,0,0,${anim.overlayOpacity})`;
609
- }
610
-
611
- // Animación de entrada con anime.js - configurable
612
- anime({
613
- targets: overlay,
614
- opacity: [0, 1],
615
- duration: overlayDuration,
616
- easing: overlayEasing
617
- });
618
-
619
- anime({
620
- targets: box,
621
- scale: [boxStartScale, 1],
622
- opacity: [0, 1],
623
- duration: boxDuration,
624
- easing: boxEasing,
625
- delay: boxDelay
626
- });
627
-
628
- anime({
629
- targets: icon,
630
- scale: [0, 1],
631
- rotate: [iconRotate, 0],
632
- duration: iconDuration,
633
- easing: boxEasing,
634
- delay: iconDelay
635
- });
636
-
637
- // Efecto hover y listener del botón (solo si existe)
638
- const closeHandler = () => {
639
- this.close(onClose);
640
- };
641
- if (button) {
642
- const buttonShadowColor = this.getButtonShadow(type);
643
- button.addEventListener('mouseenter', () => {
644
- button.style.boxShadow = `0 6px 16px ${buttonShadowColor}`;
645
- });
646
- button.addEventListener('mouseleave', () => {
647
- button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
648
- });
649
- button.addEventListener('click', (e) => {
650
- e.stopPropagation(); // Evitar que el evento llegue al overlay
651
- e.preventDefault();
652
- closeHandler();
653
- });
654
- }
655
-
656
- // Click en overlay para cerrar (solo si está permitido)
657
- if (allowOutsideClick) {
658
- overlay.addEventListener('click', (e) => {
659
- // Close when clicking outside the box (more robust than e.target === overlay)
660
- if (!box.contains(e.target)) {
661
- closeHandler();
662
- }
663
- });
664
- }
665
-
666
- // Auto-cerrar si hay timer
667
- if (timer) {
668
- setTimeout(() => {
669
- closeHandler();
670
- }, timer);
671
- }
672
-
673
- // Tecla ESC para cerrar (solo si está permitido)
674
- if (allowEscapeKey) {
675
- const escHandler = (e) => {
676
- if (e.key === 'Escape') {
677
- closeHandler();
678
- document.removeEventListener('keydown', escHandler);
679
- }
680
- };
681
- // Store handler reference on overlay so close() can remove it if needed
682
- overlay._escHandler = escHandler;
683
- document.addEventListener('keydown', escHandler);
684
- }
685
-
686
- // Return a Promise that resolves when the notification is closed
687
- return closePromise;
688
- }
689
-
690
- /**
691
- * Cerrar notificación actual
692
- */
693
- close(callback = null) {
694
- if (!this.currentNotification) {
695
- return Promise.resolve();
696
- }
697
-
698
- const overlay = this.currentNotification;
699
- const box = overlay.querySelector('.notification-box');
700
-
701
- // CRÍTICO: Limpiar la referencia INMEDIATAMENTE para evitar conflictos
702
- this.currentNotification = null;
703
-
704
- // Animación de salida
705
- anime({
706
- targets: box,
707
- scale: 0.8,
708
- opacity: 0,
709
- duration: 100,
710
- easing: 'easeInQuad'
711
- });
712
-
713
- return new Promise((resolve) => {
714
- anime({
715
- targets: overlay,
716
- opacity: 0,
717
- duration: 100,
718
- easing: 'easeInQuad',
719
- complete: () => {
720
- // Remove keydown handler if present
721
- try {
722
- if (overlay && overlay._escHandler) {
723
- document.removeEventListener('keydown', overlay._escHandler);
724
- overlay._escHandler = null;
725
- }
726
- } catch (e) { }
727
-
728
- // Remove focus trap if present
729
- try {
730
- if (overlay && overlay._focusTrap) {
731
- document.removeEventListener('keydown', overlay._focusTrap);
732
- overlay._focusTrap = null;
733
- }
734
- } catch (e) { }
735
-
736
- // Resolve external Promise returned by show(), if present
737
- try {
738
- if (overlay && typeof overlay._externalResolve === 'function') {
739
- try { overlay._externalResolve(); } catch (er) { }
740
- overlay._externalResolve = null;
741
- }
742
- } catch (e) { }
743
-
744
- // CRÍTICO: Asegurar eliminación completa del overlay
745
- try {
746
- if (overlay && overlay.parentNode) {
747
- overlay.parentNode.removeChild(overlay);
748
- }
749
- } catch (e) {
750
- try { overlay.remove(); } catch (er) { }
751
- }
752
-
753
- // Restaurar scroll SOLO si NO hay otra notificación activa
754
- if (!this.currentNotification) {
755
- try { document.body.style.overflow = ''; } catch (e) { }
756
- try { document.documentElement.style.overflow = ''; } catch (e) { }
757
- }
758
-
759
- // Restore previous focus if possible
760
- try {
761
- if (this._lastActiveElement && typeof this._lastActiveElement.focus === 'function') {
762
- this._lastActiveElement.focus();
763
- }
764
- } catch (e) { }
765
- this._lastActiveElement = null;
766
-
767
- if (callback) callback();
768
- resolve();
769
- }
770
- });
771
- });
772
- }
773
-
774
- /**
775
- * Métodos de acceso rápido
776
- */
777
- success(message, title = null, options = {}) {
778
- this.show({
779
- type: 'success',
780
- title: title || this.getDefaultTitle('success'),
781
- message,
782
- ...options
783
- });
784
- }
785
-
786
- error(message, title = null, options = {}) {
787
- this.show({
788
- type: 'error',
789
- title: title || this.getDefaultTitle('error'),
790
- message,
791
- ...options
792
- });
793
- }
794
-
795
- warning(message, title = null, options = {}) {
796
- this.show({
797
- type: 'warning',
798
- title: title || this.getDefaultTitle('warning'),
799
- message,
800
- ...options
801
- });
802
- }
803
-
804
- info(message, title = null, options = {}) {
805
- this.show({
806
- type: 'info',
807
- title: title || this.getDefaultTitle('info'),
808
- message,
809
- ...options
810
- });
811
- }
812
-
813
- /**
814
- * Mostrar notificación de carga con spinner
815
- * Útil para operaciones async, cargas desde backend, etc
816
- *
817
- * @param {string} message - Mensaje a mostrar
818
- * @param {string} title - Título (opcional, default: 'Cargando...')
819
- * @param {Object} options - Opciones adicionales
820
- * @param {number} options.timer - Auto-cerrar después de X ms (opcional)
821
- * @param {boolean} options.allowOutsideClick - Permitir cerrar haciendo click fuera (default: false)
822
- * @param {boolean} options.allowEscapeKey - Permitir cerrar con tecla ESC (default: false)
823
- * @param {Function} options.onClose - Callback al cerrar (opcional)
824
- * @returns {Promise} Promesa que se resuelve cuando se cierre la notificación
825
- *
826
- * @example
827
- * // Usar como promise
828
- * notify.loading('Procesando...', 'Espera')
829
- * .then(() => console.log('Completado'));
830
- *
831
- * // Cerrar manualmente
832
- * const loadingPromise = notify.loading('Subiendo archivo...');
833
- * setTimeout(() => notify.closeLoading(), 3000);
834
- *
835
- * // Con respuesta de backend
836
- * notify.loading('Obteniendo datos...');
837
- * fetch('/api/datos')
838
- * .then(res => res.json())
839
- * .then(data => {
840
- * notify.closeLoading();
841
- * notify.success('Datos cargados');
842
- * })
843
- * .catch(err => {
844
- * notify.closeLoading();
845
- * notify.error('Error: ' + err.message);
846
- * });
847
- */
848
- loading(message = 'Cargando...', title = 'Espera', options = {}) {
849
- // No cierra al hacer click fuera ni ESC por defecto
850
- const loadingOptions = {
851
- type: 'info',
852
- title,
853
- message,
854
- hideButton: true,
855
- allowOutsideClick: false,
856
- allowEscapeKey: false,
857
- ...options
858
- };
859
-
860
- // Crear una promesa wrapper para poder guardar la referencia y resolver después
861
- const loadingPromise = this.show(loadingOptions);
862
-
863
- // Guardar referencia a esta promesa para poder cerrarla después
864
- this._currentLoadingPromise = loadingPromise;
865
-
866
- return loadingPromise;
867
- }
868
-
869
- /**
870
- * Cerrar la notificación de carga actual
871
- *
872
- * @param {Function} callback - Callback al cerrar (opcional)
873
- * @returns {Promise} Promesa que se resuelve cuando se cierre
874
- *
875
- * @example
876
- * notify.closeLoading();
877
- * // o con callback
878
- * notify.closeLoading(() => console.log('Loading cerrado'));
879
- */
880
- closeLoading(callback = null) {
881
- this._currentLoadingPromise = null;
882
- return this.close(callback);
883
- }
884
-
885
- /**
886
- * Alias para cerrar/ocultar la notificación actual
887
- */
888
- hide(callback = null) {
889
- return this.close(callback);
890
- }
891
-
892
- /**
893
- * Compatibilidad con nombre mal escrito 'hiden'
894
- */
895
- hiden(callback = null) {
896
- return this.close(callback);
897
- }
898
-
899
- /**
900
- * Formatea segundos a mm:ss
901
- */
902
- _formatTime(seconds) {
903
- const s = Math.max(0, Math.floor(seconds));
904
- const mm = Math.floor(s / 60).toString().padStart(2, '0');
905
- const ss = (s % 60).toString().padStart(2, '0');
906
- return `${mm}:${ss}`;
907
- }
908
-
909
- }
910
-
911
- // Crear instancia global
912
- window.notify = new NotificationSystem();
913
-
914
- // Exponer también como objeto global para compatibilidad
915
- window.Notification = window.notify;
916
- }
917
- })();
918
-
342
+ `;
343
+ document.head.appendChild(style);
344
+ }
345
+ getIcon(type) {
346
+ const icons = {
347
+ 'success': '<i class="bx bx-check" aria-hidden="true"></i>',
348
+ 'error': '<i class="bx bx-x" aria-hidden="true"></i>',
349
+ 'warning': '<i class="bx bx-error" aria-hidden="true"></i>',
350
+ 'info': '<i class="bx bx-info-circle" aria-hidden="true"></i>',
351
+ 'question': '<i class="bx bx-question-mark" aria-hidden="true"></i>'
352
+ };
353
+ return icons[type] || icons.info;
354
+ }
355
+ getDefaultTitle(type) {
356
+ const titles = {
357
+ 'success': '¡Éxito!',
358
+ 'error': 'Error',
359
+ 'warning': 'Advertencia',
360
+ 'info': 'Información',
361
+ 'question': 'Pregunta'
362
+ };
363
+ return titles[type] || 'Notificación';
364
+ }
365
+ getButtonGradient(type) {
366
+ const gradients = {
367
+ 'success': 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
368
+ 'error': 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
369
+ 'warning': 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
370
+ 'info': 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
371
+ 'question': 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'
372
+ };
373
+ return gradients[type] || gradients.info;
374
+ }
375
+ getButtonShadow(type) {
376
+ const shadows = {
377
+ 'success': 'rgba(16, 185, 129, 0)',
378
+ 'error': 'rgba(239, 68, 68, 0)',
379
+ 'warning': 'rgba(245, 159, 11, 0)',
380
+ 'info': 'rgba(59, 131, 246, 0)',
381
+ 'question': 'rgba(108, 99, 245, 0)'
382
+ };
383
+ return shadows[type] || shadows.info;
384
+ }
385
+ show(options = {}) {
386
+ // Cerrar notificación existente si hay (esperar a que termine)
387
+ if (this.currentNotification) {
388
+ const oldOverlay = this.currentNotification;
389
+ this.currentNotification = null;
390
+ try {
391
+ if (oldOverlay && oldOverlay.parentNode) {
392
+ oldOverlay.parentNode.removeChild(oldOverlay);
393
+ }
394
+ }
395
+ catch (e) { }
396
+ }
397
+ const { type = 'info', title = this.getDefaultTitle(type), message = '', buttonText = 'OK', buttonColor = null, onClose = null, timer = null, allowOutsideClick = true, allowEscapeKey = true, hideButton = false, buttons = null } = options;
398
+ const showCloseButton = options.showCloseButton === true;
399
+ try {
400
+ document.body.style.overflow = 'hidden';
401
+ }
402
+ catch (e) { }
403
+ try {
404
+ document.documentElement.style.overflow = 'hidden';
405
+ }
406
+ catch (e) { }
407
+ const overlay = document.createElement('div');
408
+ overlay.className = 'notification-overlay';
409
+ overlay.tabIndex = -1;
410
+ overlay.setAttribute('role', 'dialog');
411
+ overlay.setAttribute('aria-modal', 'true');
412
+ overlay.style.pointerEvents = 'auto';
413
+ const box = document.createElement('div');
414
+ box.className = 'notification-box';
415
+ const icon = document.createElement('div');
416
+ icon.className = `notification-icon ${type}`;
417
+ if (hideButton && type === 'info') {
418
+ icon.className = 'notification-loading-container';
419
+ icon.innerHTML = '<div class="notification-spinner"></div>';
420
+ icon.style.background = 'transparent';
421
+ icon.style.boxShadow = 'none';
422
+ icon.style.width = '100px';
423
+ icon.style.height = '100px';
424
+ }
425
+ else {
426
+ icon.innerHTML = this.getIcon(type);
427
+ }
428
+ const titleElement = document.createElement('h3');
429
+ titleElement.className = 'notification-title';
430
+ titleElement.textContent = title;
431
+ const messageElement = document.createElement('p');
432
+ messageElement.className = 'notification-message';
433
+ messageElement.textContent = message;
434
+ let customContent = null;
435
+ if (options.html || options.content) {
436
+ customContent = document.createElement('div');
437
+ customContent.className = 'notification-content';
438
+ if (options.html) {
439
+ try {
440
+ customContent.innerHTML = options.html;
441
+ }
442
+ catch (e) {
443
+ customContent.textContent = options.html;
444
+ }
445
+ }
446
+ else if (options.content && options.content instanceof HTMLElement) {
447
+ customContent.appendChild(options.content);
448
+ }
449
+ }
450
+ const closeHandler = () => {
451
+ return this.close(onClose);
452
+ };
453
+ let button = null;
454
+ let buttonContainer = null;
455
+ if (!hideButton) {
456
+ if (Array.isArray(buttons) && buttons.length) {
457
+ buttonContainer = document.createElement('div');
458
+ buttonContainer.className = 'notification-button-group';
459
+ buttons.forEach((btn) => {
460
+ const btnEl = document.createElement('button');
461
+ btnEl.className = 'notification-button';
462
+ btnEl.textContent = btn.text || 'OK';
463
+ const finalBtnColor = btn.color || this.getButtonGradient(type);
464
+ const btnShadow = btn.shadowColor || this.getButtonShadow(type);
465
+ btnEl.style.background = finalBtnColor;
466
+ btnEl.style.boxShadow = `0 4px 12px ${btnShadow}`;
467
+ btnEl.addEventListener('click', (e) => {
468
+ e.stopPropagation();
469
+ e.preventDefault();
470
+ try {
471
+ closeHandler().then(() => {
472
+ if (typeof btn.onClick === 'function') {
473
+ try {
474
+ const res = btn.onClick();
475
+ if (res && typeof res.then === 'function') {
476
+ res.catch((err) => console.error(err));
477
+ }
478
+ }
479
+ catch (err) {
480
+ console.error(err);
481
+ }
482
+ }
483
+ }).catch(() => { });
484
+ }
485
+ catch (err) {
486
+ console.error(err);
487
+ }
488
+ });
489
+ btnEl.addEventListener('mouseenter', () => {
490
+ btnEl.style.boxShadow = `0 6px 16px ${btnShadow}`;
491
+ });
492
+ btnEl.addEventListener('mouseleave', () => {
493
+ btnEl.style.boxShadow = `0 4px 12px ${btnShadow}`;
494
+ });
495
+ buttonContainer.appendChild(btnEl);
496
+ });
497
+ }
498
+ else if (options.onConfirm || options.onCancel || options.confirmText || options.cancelText) {
499
+ buttonContainer = document.createElement('div');
500
+ buttonContainer.className = 'notification-button-group';
501
+ const cancelText = options.cancelText || 'Cancelar';
502
+ const confirmText = options.confirmText || 'Aceptar';
503
+ const cancelBtn = document.createElement('button');
504
+ cancelBtn.className = 'notification-button';
505
+ cancelBtn.textContent = cancelText;
506
+ const cancelColor = options.cancelColor || 'linear-gradient(135deg, #9ca3af 0%, #6b7280 100%)';
507
+ const cancelShadow = options.cancelShadow || 'rgba(107,114,128,0.25)';
508
+ cancelBtn.style.background = cancelColor;
509
+ cancelBtn.style.boxShadow = `0 4px 12px ${cancelShadow}`;
510
+ cancelBtn.addEventListener('click', (e) => {
511
+ e.stopPropagation();
512
+ e.preventDefault();
513
+ closeHandler().then(() => {
514
+ try {
515
+ if (typeof options.onCancel === 'function') {
516
+ const res = options.onCancel();
517
+ if (res && typeof res.then === 'function') {
518
+ res.catch((err) => console.error(err));
519
+ }
520
+ }
521
+ }
522
+ catch (err) {
523
+ console.error(err);
524
+ }
525
+ }).catch(() => { });
526
+ });
527
+ cancelBtn.addEventListener('mouseenter', () => { cancelBtn.style.boxShadow = `0 6px 16px ${cancelShadow}`; });
528
+ cancelBtn.addEventListener('mouseleave', () => { cancelBtn.style.boxShadow = `0 4px 12px ${cancelShadow}`; });
529
+ const confirmBtn = document.createElement('button');
530
+ confirmBtn.className = 'notification-button';
531
+ confirmBtn.textContent = confirmText;
532
+ const confirmColor = options.confirmColor || this.getButtonGradient(type);
533
+ const confirmShadow = options.confirmShadow || this.getButtonShadow(type);
534
+ confirmBtn.style.background = confirmColor;
535
+ confirmBtn.style.boxShadow = `0 4px 12px ${confirmShadow}`;
536
+ confirmBtn.addEventListener('click', async (e) => {
537
+ e.stopPropagation();
538
+ e.preventDefault();
539
+ try {
540
+ await closeHandler();
541
+ if (typeof options.onConfirm === 'function') {
542
+ const res = options.onConfirm();
543
+ if (res && typeof res.then === 'function') {
544
+ await res;
545
+ }
546
+ }
547
+ }
548
+ catch (err) {
549
+ console.error(err);
550
+ }
551
+ });
552
+ confirmBtn.addEventListener('mouseenter', () => { confirmBtn.style.boxShadow = `0 6px 16px ${confirmShadow}`; });
553
+ confirmBtn.addEventListener('mouseleave', () => { confirmBtn.style.boxShadow = `0 4px 12px ${confirmShadow}`; });
554
+ buttonContainer.appendChild(cancelBtn);
555
+ buttonContainer.appendChild(confirmBtn);
556
+ }
557
+ else if (buttonText) {
558
+ button = document.createElement('button');
559
+ button.className = 'notification-button';
560
+ button.textContent = buttonText;
561
+ const finalButtonColor = buttonColor || this.getButtonGradient(type);
562
+ const buttonShadowColor = this.getButtonShadow(type);
563
+ button.style.background = finalButtonColor;
564
+ button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
565
+ }
566
+ }
567
+ let closeBtn = null;
568
+ if (showCloseButton) {
569
+ closeBtn = document.createElement('button');
570
+ closeBtn.setAttribute('aria-label', 'Cerrar');
571
+ closeBtn.className = 'notification-close';
572
+ closeBtn.innerHTML = '&times;';
573
+ closeBtn.addEventListener('click', (e) => {
574
+ e.stopPropagation();
575
+ closeHandler();
576
+ });
577
+ }
578
+ box.appendChild(icon);
579
+ if (customContent) {
580
+ const descId = 'notify-desc-' + Date.now();
581
+ customContent.id = descId;
582
+ overlay.setAttribute('aria-describedby', descId);
583
+ box.appendChild(customContent);
584
+ }
585
+ else {
586
+ box.appendChild(titleElement);
587
+ box.appendChild(messageElement);
588
+ }
589
+ if (closeBtn)
590
+ box.appendChild(closeBtn);
591
+ if (buttonContainer) {
592
+ box.appendChild(buttonContainer);
593
+ }
594
+ else if (button) {
595
+ box.appendChild(button);
596
+ }
597
+ overlay.appendChild(box);
598
+ document.body.appendChild(overlay);
599
+ const overlayMeta = overlay;
600
+ const closePromise = new Promise((resolveClose) => {
601
+ try {
602
+ overlayMeta._externalResolve = resolveClose;
603
+ }
604
+ catch (e) { }
605
+ });
606
+ try {
607
+ const live = document.getElementById('notify-live');
608
+ if (live) {
609
+ live.textContent = `${title}: ${message}`;
610
+ }
611
+ }
612
+ catch (e) { }
613
+ try {
614
+ this._lastActiveElement = document.activeElement;
615
+ }
616
+ catch (e) {
617
+ this._lastActiveElement = null;
618
+ }
619
+ this.currentNotification = overlay;
620
+ try {
621
+ const focusable = box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])');
622
+ if (focusable && focusable.length) {
623
+ focusable[0].focus();
624
+ }
625
+ else if (button) {
626
+ button.focus();
627
+ }
628
+ else {
629
+ overlay.focus();
630
+ }
631
+ }
632
+ catch (e) {
633
+ try {
634
+ overlay.focus();
635
+ }
636
+ catch (err) { }
637
+ }
638
+ const focusTrap = (e) => {
639
+ if (e.key !== 'Tab')
640
+ return;
641
+ const focusableNodes = Array.from(box.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'));
642
+ const focusable = focusableNodes.filter((el) => el instanceof HTMLElement && el.offsetParent !== null);
643
+ if (!focusable.length) {
644
+ e.preventDefault();
645
+ return;
646
+ }
647
+ const first = focusable[0];
648
+ const last = focusable[focusable.length - 1];
649
+ if (!e.shiftKey && document.activeElement === last) {
650
+ e.preventDefault();
651
+ first.focus();
652
+ }
653
+ else if (e.shiftKey && document.activeElement === first) {
654
+ e.preventDefault();
655
+ last.focus();
656
+ }
657
+ };
658
+ overlayMeta._focusTrap = focusTrap;
659
+ document.addEventListener('keydown', focusTrap);
660
+ const anim = options.anim || {};
661
+ const overlayDuration = typeof anim.overlayDuration === 'number' ? anim.overlayDuration : 150;
662
+ const overlayEasing = anim.overlayEasing || 'easeOutQuad';
663
+ const boxDuration = typeof anim.boxDuration === 'number' ? anim.boxDuration : 200;
664
+ const boxDelay = typeof anim.boxDelay === 'number' ? anim.boxDelay : 50;
665
+ const boxEasing = anim.boxEasing || 'easeOutBack';
666
+ const boxStartScale = typeof anim.boxStartScale === 'number' ? anim.boxStartScale : 0.8;
667
+ const iconDuration = typeof anim.iconDuration === 'number' ? anim.iconDuration : 250;
668
+ const iconDelay = typeof anim.iconDelay === 'number' ? anim.iconDelay : 100;
669
+ const iconRotate = (typeof anim.iconRotate === 'number') ? anim.iconRotate : (type === 'success' ? -90 : type === 'error' ? 90 : 0);
670
+ if (typeof anim.overlayOpacity === 'number') {
671
+ overlay.style.backgroundColor = `rgba(0,0,0,${anim.overlayOpacity})`;
672
+ }
673
+ anime({
674
+ targets: overlay,
675
+ opacity: [0, 1],
676
+ duration: overlayDuration,
677
+ easing: overlayEasing
678
+ });
679
+ anime({
680
+ targets: box,
681
+ scale: [boxStartScale, 1],
682
+ opacity: [0, 1],
683
+ duration: boxDuration,
684
+ easing: boxEasing,
685
+ delay: boxDelay
686
+ });
687
+ anime({
688
+ targets: icon,
689
+ scale: [0, 1],
690
+ rotate: [iconRotate, 0],
691
+ duration: iconDuration,
692
+ easing: boxEasing,
693
+ delay: iconDelay
694
+ });
695
+ if (button) {
696
+ const buttonShadowColor = this.getButtonShadow(type);
697
+ button.addEventListener('mouseenter', () => {
698
+ button.style.boxShadow = `0 6px 16px ${buttonShadowColor}`;
699
+ });
700
+ button.addEventListener('mouseleave', () => {
701
+ button.style.boxShadow = `0 4px 12px ${buttonShadowColor}`;
702
+ });
703
+ button.addEventListener('click', (e) => {
704
+ e.stopPropagation();
705
+ e.preventDefault();
706
+ closeHandler().catch(() => { });
707
+ });
708
+ }
709
+ if (allowOutsideClick) {
710
+ overlay.addEventListener('click', (e) => {
711
+ if (!box.contains(e.target)) {
712
+ closeHandler();
713
+ }
714
+ });
715
+ }
716
+ if (timer) {
717
+ setTimeout(() => {
718
+ closeHandler();
719
+ }, timer);
720
+ }
721
+ if (allowEscapeKey) {
722
+ const escHandler = (e) => {
723
+ if (e.key === 'Escape') {
724
+ closeHandler();
725
+ document.removeEventListener('keydown', escHandler);
726
+ }
727
+ };
728
+ overlayMeta._escHandler = escHandler;
729
+ document.addEventListener('keydown', escHandler);
730
+ }
731
+ return closePromise;
732
+ }
733
+ close(callback = null) {
734
+ if (!this.currentNotification) {
735
+ return Promise.resolve();
736
+ }
737
+ const overlay = this.currentNotification;
738
+ const overlayMeta = overlay;
739
+ const box = overlay.querySelector('.notification-box');
740
+ this.currentNotification = null;
741
+ anime({
742
+ targets: box,
743
+ scale: 0.8,
744
+ opacity: 0,
745
+ duration: 100,
746
+ easing: 'easeInQuad'
747
+ });
748
+ return new Promise((resolve) => {
749
+ anime({
750
+ targets: overlay,
751
+ opacity: 0,
752
+ duration: 100,
753
+ easing: 'easeInQuad',
754
+ complete: () => {
755
+ try {
756
+ if (overlayMeta && overlayMeta._escHandler) {
757
+ document.removeEventListener('keydown', overlayMeta._escHandler);
758
+ overlayMeta._escHandler = undefined;
759
+ }
760
+ }
761
+ catch (e) { }
762
+ try {
763
+ if (overlayMeta && overlayMeta._focusTrap) {
764
+ document.removeEventListener('keydown', overlayMeta._focusTrap);
765
+ overlayMeta._focusTrap = undefined;
766
+ }
767
+ }
768
+ catch (e) { }
769
+ try {
770
+ if (overlayMeta && typeof overlayMeta._externalResolve === 'function') {
771
+ try {
772
+ overlayMeta._externalResolve();
773
+ }
774
+ catch (er) { }
775
+ overlayMeta._externalResolve = undefined;
776
+ }
777
+ }
778
+ catch (e) { }
779
+ try {
780
+ if (overlay && overlay.parentNode) {
781
+ overlay.parentNode.removeChild(overlay);
782
+ }
783
+ }
784
+ catch (e) {
785
+ try {
786
+ overlay.remove();
787
+ }
788
+ catch (er) { }
789
+ }
790
+ if (!this.currentNotification) {
791
+ try {
792
+ document.body.style.overflow = '';
793
+ }
794
+ catch (e) { }
795
+ try {
796
+ document.documentElement.style.overflow = '';
797
+ }
798
+ catch (e) { }
799
+ }
800
+ try {
801
+ if (this._lastActiveElement && typeof this._lastActiveElement.focus === 'function') {
802
+ this._lastActiveElement.focus();
803
+ }
804
+ }
805
+ catch (e) { }
806
+ this._lastActiveElement = null;
807
+ if (callback)
808
+ callback();
809
+ resolve();
810
+ }
811
+ });
812
+ });
813
+ }
814
+ success(message, title = null, options = {}) {
815
+ this.show(Object.assign({ type: 'success', title: title || this.getDefaultTitle('success'), message }, options));
816
+ }
817
+ error(message, title = null, options = {}) {
818
+ this.show(Object.assign({ type: 'error', title: title || this.getDefaultTitle('error'), message }, options));
819
+ }
820
+ warning(message, title = null, options = {}) {
821
+ this.show(Object.assign({ type: 'warning', title: title || this.getDefaultTitle('warning'), message }, options));
822
+ }
823
+ question(message, title = null, options = {}) {
824
+ this.show(Object.assign({ type: 'question', title: title || this.getDefaultTitle('question'), message }, options));
825
+ }
826
+ info(message, title = null, options = {}) {
827
+ this.show(Object.assign({ type: 'info', title: title || this.getDefaultTitle('info'), message }, options));
828
+ }
829
+ loading(message = 'Cargando...', title = 'Espera', options = {}) {
830
+ const loadingOptions = Object.assign({ type: 'info', title,
831
+ message, hideButton: true, allowOutsideClick: false, allowEscapeKey: false }, options);
832
+ const loadingPromise = this.show(loadingOptions);
833
+ this._currentLoadingPromise = loadingPromise;
834
+ return loadingPromise;
835
+ }
836
+ closeLoading(callback = null) {
837
+ this._currentLoadingPromise = null;
838
+ return this.close(callback);
839
+ }
840
+ hide(callback = null) { return this.close(callback); }
841
+ hiden(callback = null) { return this.close(callback); }
842
+ _formatTime(seconds) {
843
+ const s = Math.max(0, Math.floor(seconds));
844
+ const mm = Math.floor(s / 60).toString().padStart(2, '0');
845
+ const ss = (s % 60).toString().padStart(2, '0');
846
+ return `${mm}:${ss}`;
847
+ }
848
+ }
849
+ const notifyInstance = new NotificationSystem();
850
+ const w = window;
851
+ w.notify = notifyInstance;
852
+ w.Notification = notifyInstance;
853
+ }
854
+ })();
855
+ //# sourceMappingURL=notification-system.js.map
919
856
 
920
857
  // Extraer la clase desde la instancia global
921
858
  const NotificationSystem = window.notify?.constructor || function() {