1/** 2 * Global Toast Notification Utility 3 * Wrapper around Toastr.js with consistent styling across the app 4 */ 5 6const AppToast = { 7 /** 8 * Canonical Toastr options. Returned fresh on every call so the auto-close 9 * timer is guaranteed to be applied regardless of script/init load order. 10 */ 11 _options() { 12 return { 13 closeButton: true, 14 debug: false, 15 newestOnTop: true, 16 progressBar: true, 17 positionClass: 'toast-top-right', 18 preventDuplicates: false, 19 onclick: null, 20 showDuration: 300, 21 hideDuration: 1000, 22 timeOut: 3000, 23 extendedTimeOut: 1000, 24 showEasing: 'swing', 25 hideEasing: 'linear', 26 showMethod: 'fadeIn', 27 hideMethod: 'fadeOut' 28 }; 29 }, 30 31 init() { 32 if (typeof toastr === 'undefined') { 33 console.warn('Toastr.js is not loaded. Toast notifications will not work.'); 34 return; 35 } 36 toastr.options = this._options(); 37 }, 38 39 /** 40 * Normalizes flexible arguments into { message, title }. 41 * Accepts either AppToast.x('message', 'title') or AppToast.x({ title, message, text }). 42 * When an object provides only a title, it is shown as the message so text always renders. 43 */ 44 _normalize(message, title) { 45 if (message && typeof message === 'object') { 46 const opts = message; 47 let msg = opts.message || opts.text || ''; 48 let ttl = opts.title || ''; 49 if (!msg) { 50 msg = ttl; 51 ttl = ''; 52 } 53 return { message: msg, title: ttl }; 54 } 55 return { message: message || '', title: title || '' }; 56 }, 57 58 success(message, title = '') { 59 const n = this._normalize(message, title); 60 if (typeof toastr === 'undefined') { 61 console.log('[Success]', n.title || 'Success', n.message); 62 return; 63 } 64 toastr.success(n.message, n.title, this._options()); 65 }, 66 67 error(message, title = '') { 68 const n = this._normalize(message, title); 69 if (typeof toastr === 'undefined') { 70 console.error('[Error]', n.title || 'Error', n.message); 71 return; 72 } 73 toastr.error(n.message, n.title, this._options()); 74 }, 75 76 warning(message, title = '') { 77 const n = this._normalize(message, title); 78 if (typeof toastr === 'undefined') { 79 console.warn('[Warning]', n.title || 'Warning', n.message); 80 return; 81 } 82 toastr.warning(n.message, n.title, this._options()); 83 }, 84 85 info(message, title = '') { 86 const n = this._normalize(message, title); 87 if (typeof toastr === 'undefined') { 88 console.info('[Info]', n.title || 'Info', n.message); 89 return; 90 } 91 toastr.info(n.message, n.title, this._options()); 92 }, 93 94 /** 95 * Confirmation dialog. Accepts either positional args 96 * confirm('message', 'title', onConfirm, onCancel) 97 * or a single options object 98 * confirm({ message, title, onConfirm, onCancel, confirmText, cancelText }). 99 */ 100 confirm(message, title = 'Confirm Action', onConfirm = null, onCancel = null) { 101 let confirmText = 'Yes, Confirm'; 102 let cancelText = 'Cancel'; 103 104 if (message && typeof message === 'object') { 105 const opts = message; 106 title = opts.title || 'Confirm Action'; 107 message = opts.message || opts.text || ''; 108 onConfirm = opts.onConfirm || null; 109 onCancel = opts.onCancel || null; 110 confirmText = opts.confirmText || confirmText; 111 cancelText = opts.cancelText || cancelText; 112 } 113 114 const confirmModal = document.createElement('div'); 115 confirmModal.className = 'app-toast-confirm-modal'; 116 confirmModal.innerHTML = ` 117 <div class="app-toast-confirm-backdrop"></div> 118 <div class="app-toast-confirm-box"> 119 <h5 class="app-toast-confirm-title">${title}</h5> 120 <p class="app-toast-confirm-message">${message}</p> 121 <div class="app-toast-confirm-buttons"> 122 <button class="app-toast-confirm-btn app-toast-confirm-cancel">${cancelText}</button> 123 <button class="app-toast-confirm-btn app-toast-confirm-yes">${confirmText}</button> 124 </div> 125 </div> 126 `; 127 128 document.body.appendChild(confirmModal); 129 130 const confirmBtn = confirmModal.querySelector('.app-toast-confirm-yes'); 131 const cancelBtn = confirmModal.querySelector('.app-toast-confirm-cancel'); 132 const backdrop = confirmModal.querySelector('.app-toast-confirm-backdrop'); 133 134 const cleanup = () => confirmModal.remove(); 135 136 confirmBtn.addEventListener('click', () => { 137 cleanup(); 138 if (onConfirm) onConfirm(); 139 }); 140 141 const handleCancel = () => { 142 cleanup(); 143 if (onCancel) onCancel(); 144 }; 145 cancelBtn.addEventListener('click', handleCancel); 146 backdrop.addEventListener('click', handleCancel); 147 }, 148 149 clear() { 150 if (typeof toastr === 'undefined') { 151 return; 152 } 153 toastr.clear(); 154 } 155}; 156 157document.addEventListener('DOMContentLoaded', function() {
158 AppToast.init(); 159}); 160 161// Add CSS for confirmation modal 162const style = document.createElement('style'); 163style.textContent = ` 164 .app-toast-confirm-modal { 165 position: fixed; 166 top: 0; 167 left: 0; 168 width: 100%; 169 height: 100%; 170 z-index: 9999; 171 display: flex; 172 align-items: center; 173 justify-content: center; 174 } 175 176 .app-toast-confirm-backdrop { 177 position: absolute; 178 inset: 0; 179 background: rgba(0, 0, 0, 0.5); 180 animation: fadeIn 0.3s ease; 181 } 182 183 .app-toast-confirm-box { 184 position: relative; 185 background: white; 186 border-radius: 12px; 187 padding: 24px; 188 max-width: 400px; 189 width: 90%; 190 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); 191 animation: slideUp 0.3s ease; 192 } 193 194 .app-toast-confirm-title { 195 margin: 0 0 12px 0; 196 font-size: 18px; 197 font-weight: 600; 198 color: #1c1a18; 199 } 200 201 .app-toast-confirm-message { 202 margin: 0 0 20px 0; 203 font-size: 14px; 204 color: #666; 205 line-height: 1.5; 206 } 207 208 .app-toast-confirm-buttons { 209 display: flex; 210 gap: 12px; 211 justify-content: flex-end; 212 } 213 214 .app-toast-confirm-btn { 215 padding: 8px 16px; 216 border: none; 217 border-radius: 6px; 218 cursor: pointer; 219 font-size: 14px; 220 font-weight: 500; 221 transition: all 0.2s ease; 222 } 223 224 .app-toast-confirm-cancel { 225 background: #e5e7eb; 226 color: #374151; 227 } 228 229 .app-toast-confirm-cancel:hover { 230 background: #d1d5db; 231 } 232 233 .app-toast-confirm-yes { 234 background: #ef4444; 235 color: white; 236 } 237 238 .app-toast-confirm-yes:hover { 239 background: #dc2626; 240 } 241 242 @keyframes fadeIn { 243 from { opacity: 0; } 244 to { opacity: 1; } 245 } 246 247 @keyframes slideUp { 248 from { transform: translateY(20px); opacity: 0; } 249 to { transform: translateY(0); opacity: 1; } 250 } 251`; 252document.head.appendChild(style);
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.