|
1 | | -import { escapeHtml } from '../sanitize.js'; |
2 | | - |
3 | 1 | /** |
4 | 2 | * Toast Notification Manager |
5 | 3 | * Singleton class for managing toast notifications across the application |
| 4 | + * Note: Using textContent for text insertion provides automatic XSS protection |
6 | 5 | */ |
7 | 6 | class NotificationManager { |
8 | 7 | constructor() { |
@@ -67,30 +66,50 @@ class NotificationManager { |
67 | 66 |
|
68 | 67 | const icon = this.getIcon(type); |
69 | 68 |
|
70 | | - let toastHTML = ` |
71 | | - <div class="toast-icon">${icon}</div> |
72 | | - <div class="toast-message">${escapeHtml(message)}</div> |
73 | | - <button class="toast-close" aria-label="Close toast">✕</button> |
74 | | - <div class="toast-progress"></div> |
75 | | - `; |
| 69 | + // Build toast structure |
| 70 | + const toastIcon = document.createElement('div'); |
| 71 | + toastIcon.className = 'toast-icon'; |
| 72 | + toastIcon.textContent = icon; |
| 73 | + |
| 74 | + const toastMessage = document.createElement('div'); |
| 75 | + toastMessage.className = 'toast-message'; |
| 76 | + toastMessage.textContent = message; |
| 77 | + |
| 78 | + const closeBtn = document.createElement('button'); |
| 79 | + closeBtn.className = 'toast-close'; |
| 80 | + closeBtn.setAttribute('aria-label', 'Close toast'); |
| 81 | + closeBtn.textContent = '✕'; |
| 82 | + |
| 83 | + const progressBar = document.createElement('div'); |
| 84 | + progressBar.className = 'toast-progress'; |
76 | 85 |
|
| 86 | + // Append elements in order |
| 87 | + toast.appendChild(toastIcon); |
| 88 | + toast.appendChild(toastMessage); |
| 89 | + |
| 90 | + // Add action button if provided |
77 | 91 | if (action) { |
78 | | - const actionButton = `<button class="toast-action" data-action="${action.id}">${escapeHtml(action.text)}</button>`; |
79 | | - toastHTML = toastHTML.replace('</div><button class="toast-close">', `</div>${actionButton}<button class="toast-close">`); |
| 92 | + const actionBtn = document.createElement('button'); |
| 93 | + actionBtn.className = 'toast-action'; |
| 94 | + actionBtn.setAttribute('data-action', action.id); |
| 95 | + actionBtn.textContent = action.text; |
| 96 | + toast.appendChild(actionBtn); |
80 | 97 | } |
81 | 98 |
|
82 | | - toast.innerHTML = toastHTML; |
| 99 | + toast.appendChild(closeBtn); |
| 100 | + toast.appendChild(progressBar); |
83 | 101 |
|
84 | 102 | // Add event listeners |
85 | | - const closeBtn = toast.querySelector('.toast-close'); |
86 | 103 | closeBtn.addEventListener('click', () => this.remove(id)); |
87 | 104 |
|
88 | | - const actionBtn = toast.querySelector('.toast-action'); |
89 | | - if (actionBtn && action) { |
90 | | - actionBtn.addEventListener('click', () => { |
91 | | - action.handler(); |
92 | | - this.remove(id); |
93 | | - }); |
| 105 | + if (action) { |
| 106 | + const actionBtn = toast.querySelector('.toast-action'); |
| 107 | + if (actionBtn) { |
| 108 | + actionBtn.addEventListener('click', () => { |
| 109 | + action.handler(); |
| 110 | + this.remove(id); |
| 111 | + }); |
| 112 | + } |
94 | 113 | } |
95 | 114 |
|
96 | 115 | return toast; |
|
0 commit comments