1/** 2 * Toast Notification System for Garden Planning Tool 3 * Provides toast notifications with undo and action links 4 * Progressive Plants Garden Planning Tool 5 */ 6const ToastNotifications = { 7 container: null, 8 activeToasts: [], 9 defaultDuration: 4000, 10 11 /** 12 * Initialize the toast container 13 */ 14 init() { 15 if (this.container) return; 16 17 this.container = document.createElement('div'); 18 this.container.id = 'toast-container'; 19 this.container.className = 'toast-container'; 20 this.container.setAttribute('aria-live', 'polite'); 21 this.container.setAttribute('aria-atomic', 'true'); 22 document.body.appendChild(this.container); 23 }, 24 25 /** 26 * Show a toast notification 27 * @param {Object} options - Toast configuration 28 * @returns {Object} Toast instance for programmatic control 29 */ 30 show(options = {}) { 31 this.init(); 32 33 const { 34 message = '', 35 type = 'info', // 'success', 'error', 'warning', 'info' 36 duration = this.defaultDuration, 37 actions = [], // [{label: 'Undo', onClick: fn}, {label: 'View', onClick: fn}] 38 onClose = null 39 } = options; 40 41 const toast = document.createElement('div'); 42 toast.className = `toast toast-${type}`; 43 toast.setAttribute('role', 'alert'); 44 45 // Build toast content 46 let contentHTML = ` 47 <div class="toast-content"> 48 <span class="toast-icon">${this.getIcon(type)}</span> 49 <span class="toast-message">${message}</span> 50 </div> 51 `; 52 53 // Add action buttons if provided 54 if (actions.length > 0) { 55 contentHTML += `<div class="toast-actions">`; 56 actions.forEach((action, index) => { 57 contentHTML += `<button class="toast-action-btn" data-action-index="${index}">${action.label}</button>`; 58 }); 59 contentHTML += `</div>`; 60 } 61 62 // Add close button 63 contentHTML += `<button class="toast-close" aria-label="Close notification">×</button>`; 64 65 toast.innerHTML = contentHTML; 66 67 // Add to container 68 this.container.appendChild(toast); 69 70 // Trigger entrance animation 71 requestAnimationFrame(() => { 72 toast.classList.add('toast-enter'); 73 }); 74 75 // Setup event handlers 76 const closeBtn = toast.querySelector('.toast-close'); 77 closeBtn.addEventListener('click', () => this.dismiss(toast, onClose)); 78 79 // Setup action button handlers 80 actions.forEach((action, index) => { 81 const btn = toast.querySelector(`[data-action-index="${index}"]`); 82 if (btn && action.onClick) { 83 btn.addEventListener('click', () => { 84 action.onClick(); 85 this.dismiss(toast, onClose); 86 }); 87 } 88 }); 89 90 // Auto-dismiss after duration 91 const timeoutId = setTimeout(() => { 92 this.dismiss(toast, onClose); 93 }, duration); 94 95 // Track toast 96 const toastInstance = { element: toast, timeoutId }; 97 this.activeToasts.push(toastInstance); 98 99 return toastInstance; 100 }, 101 102 /** 103 * Dismiss a toast 104 */ 105 dismiss(toast, onClose = null) { 106 if (!toast || !toast.parentNode) return; 107 108 toast.classList.add('toast-exit'); 109 110 setTimeout(() => { 111 if (toast.parentNode) { 112 toast.parentNode.removeChild(toast); 113 } 114 115 // Remove from tracking 116 this.activeToasts = this.activeToasts.filter(t => t.element !== toast); 117 118 if (onClose) onClose(); 119 }, 300); 120 }, 121 122 /** 123 * Show success toast for adding plant to project 124 */ 125 showPlantAdded(plantName, projectName, undoCallback = null, viewProjectCallback = null) { 126 const actions = []; 127 128 if (undoCallback) { 129 actions.push({ label: 'Undo', onClick: undoCallback }); 130 } 131 if (viewProjectCallback) { 132 actions.push({ label: 'View Project', onClick: viewProjectCallback }); 133 } 134 135 return this.show({ 136 message: `Added "${plantName}" to ${projectName}`, 137 type: 'success', 138 actions 139 }); 140 }, 141 142 /** 143 * Show toast prompting to create/select project 144 */ 145 showNoProject(createCallback = null, selectCallback = null) { 146 const actions = []; 147 148 if (createCallback) { 149 actions.push({ label: 'Create Project', onClick: createCallback }); 150 } 151 if (selectCallback) { 152 actions.push({ label: 'Select Project', onClick: selectCallback }); 153 } 154 155 return this.show({ 156 message: 'Create or select a project to save favorites', 157 type: 'warning', 158 duration: 5000, 159 actions 160 }); 161 }, 162 163 /** 164 * Show error toast 165 */ 166 showError(message) { 167 return this.show({ 168 message: message, 169 type: 'error', 170 duration: 5000 171 }); 172 }, 173 174 /** 175 * Show info toast 176 */ 177 showInfo(message, duration = 3000) { 178 return this.show({ 179 message: message, 180 type: 'info', 181 duration: duration 182 }); 183 }, 184 185 /** 186 * Get icon for toast type 187 */ 188 getIcon(type) { 189 const icons = { 190 success: 'â', 191 error: 'â', 192 warning: 'â ', 193 info: 'â¹' 194 }; 195 return icons[type] || icons.info; 196 }, 197 198 /** 199 * Clear all active toasts 200 */ 201 clearAll() {
202 this.activeToasts.forEach(toast => { 203 clearTimeout(toast.timeoutId); 204 this.dismiss(toast.element); 205 }); 206 } 207}; 208 209// Export for global use 210window.ToastNotifications = ToastNotifications;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.