1/*! 2 * Laravel Toaster Magic â shared runtime 3 * 4 * This is the single source of truth for toast behaviour. The Livewire build 5 * loads this same file and layers a thin event bridge on top 6 * (livewire-v3/livewire-toaster-magic-v3.js), so both integrations render 7 * identically by construction rather than by convention. 8 * 9 * Security note: no user-controlled value is ever concatenated into an HTML 10 * string. Text goes through `textContent` and URLs go through `setAttribute`, 11 * which makes attribute breakout structurally impossible. HTML is rendered only 12 * when a caller explicitly opts in with `html: true`. 13 */ 14(function () { 15 "use strict"; 16 17 var TOAST_TYPES = ["success", "error", "warning", "info"]; 18 19 // Protocols that may appear in a toast action link. `javascript:` and 20 // `data:` are deliberately absent â those are the executable ones. 21 var LINK_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"]; 22 // Avatars are images; only real network schemes make sense. 23 var IMAGE_PROTOCOLS = ["http:", "https:"]; 24 25 // =============================== 26 // Config access 27 // =============================== 28 // Read lazily on every access. The runtime therefore does not care whether 29 // it is evaluated before or after the inline config block, which is what 30 // previously left every configured value stuck on its default. 31 function getConfig() { 32 return window.toastMagicConfig || {}; 33 } 34 35 function getStyleVars() { 36 return window.toastMagicStyleVars || {}; 37 } 38 39 function configValue(key, fallback) { 40 var config = getConfig(); 41 return config[key] === undefined || config[key] === null ? fallback : config[key]; 42 } 43 44 // =============================== 45 // Utility: URL sanitizer 46 // =============================== 47 // Returns a safe URL string, or null when the value cannot be trusted. 48 // 49 // Because every URL produced here is applied with `setAttribute()`, quotes 50 // and angle brackets cannot escape the attribute no matter what they are. 51 // That reduces this function's job to one question: does the URL resolve to 52 // a protocol that can execute code? Anything that does is rejected outright. 53 function sanitizeUrl(value, allowedProtocols) { 54 if (typeof value !== "string") return null; 55 56 var raw = value.trim(); 57 if (!raw) return null; 58 59 // Control characters and raw whitespace inside the value are the 60 // classic way to smuggle a scheme past a naive check. Reject them. 61 // eslint-disable-next-line no-control-regex -- rejecting control characters is the point 62 if (/[\u0000-\u0020\u007F]/.test(raw)) return null; 63 64 var parsed; 65 try { 66 parsed = new URL(raw, document.baseURI); 67 } catch { 68 return null; 69 } 70 71 if (allowedProtocols.indexOf(parsed.protocol) === -1) return null; 72 73 // A relative or fragment URL that already resolved to a safe protocol is 74 // returned in the author's original form â resolving it to an absolute 75 // URL would work too, but needlessly rewrites what ends up in the DOM. 76 if (raw.charAt(0) === "#" || raw.charAt(0) === "/") return raw; 77 78 return parsed.href; 79 } 80 81 // =============================== 82 // Utility: safe text rendering 83 // =============================== 84 // Escaped by default. Newlines become real <br> elements, so multi-line 85 // messages keep working without ever interpreting caller content as markup. 86 function renderText(target, value, allowHtml) { 87 var text = value === null || value === undefined ? "" : String(value); 88 89 if (allowHtml) { 90 // Explicit opt-in. The caller owns the safety of this string. 91 target.innerHTML = text; 92 return; 93 } 94 95 var lines = text.split(/\r\n|\r|\n/); 96 for (var i = 0; i < lines.length; i++) { 97 if (i > 0) target.appendChild(document.createElement("br")); 98 if (lines[i] !== "") target.appendChild(document.createTextNode(lines[i])); 99 } 100 } 101 102 // Parse a trusted SVG string into an element. Used only for the bundled 103 // icons, which are package constants and never caller input. 104 function buildIcon(markup) { 105 var wrapper = document.createElement("span"); 106 wrapper.innerHTML = markup; 107 108 var svg = wrapper.querySelector("svg"); 109 if (!svg) return null; 110 111 // Icons are decorative: the toast text already carries the meaning, so 112 // they must not be announced or focusable. 113 svg.setAttribute("aria-hidden", "true"); 114 svg.setAttribute("focusable", "false"); 115 return svg; 116 } 117 118 // =============================== 119 // Utility: accessible live regions 120 // =============================== 121 // A live region only announces mutations that happen *inside* a region the 122 // assistive technology is already observing. Inserting an already-populated 123 // role="alert" element â which is what this package used to do â is widely 124 // unreliable. So two empty regions are created up front and the toast text 125 // is written into the one matching the severity. 126 var liveRegions = null; 127 128 function ensureLiveRegions() { 129 if (liveRegions && liveRegions.polite.isConnected) return liveRegions; 130 if (!document.body) return null; 131 132 // Adopt regions already in the DOM (a second evaluation of this file, or 133 // a server-rendered pair) instead of appending a duplicate set. 134 var existing = document.querySelectorAll(".toast-magic-live-region"); 135 if (existing.length >= 2) { 136 liveRegions = { polite: existing[0], assertive: existing[1] }; 137 return liveRegions; 138 } 139 140 function build(role, live) { 141 var region = document.createElement("div"); 142 region.className = "toast-magic-live-region"; 143 region.setAttribute("role", role); 144 region.setAttribute("aria-live", live); 145 region.setAttribute("aria-atomic", "true"); 146 document.body.appendChild(region); 147 return region; 148 } 149 150 liveRegions = { 151 polite: build("status", "polite"), 152 assertive: build("alert", "assertive") 153 }; 154 155 return liveRegions; 156 } 157 158 function announce(type, heading, description) { 159 var regions = ensureLiveRegions(); 160 if (!regions) return; 161 162 // Errors interrupt; everything else waits for a natural pause. 163 var region = type === "error" ? regions.assertive : regions.polite; 164 var message = [heading, description] 165 .filter(function (part) { return part; }) 166 .join(". "); 167 168 if (!message) return; 169 170 // Clearing first guarantees a mutation even when two identical toasts 171 // are announced back to back. 172 region.textContent = ""; 173 window.setTimeout(function () { 174 region.textContent = message; 175 }, 50); 176 } 177 178 // =============================== 179 // Utility: close toast 180 // =============================== 181 function closeToastMagicItem(toast) { 182 if (!toast || toast.dataset.tmClosing) return; // guard against double-close 183 toast.dataset.tmClosing = "1"; 184 185 if (typeof toast._tmDispose === "function") toast._tmDispose(); 186 187 var container = toast.closest(".toast-container"); 188 189 // Pin the closing toast in place (out of flow) so the flex gap collapses 190 // immediately, then glide the remaining toasts up to fill the space â all 191 // while it slides/fades out. Keeps the stack moving smoothly. 192 flipReflow(container, function () { 193 var rect = toast.getBoundingClientRect(); 194 toast.style.translate = ""; // drop any in-progress reflow offset 195 toast.style.position = "fixed"; 196 toast.style.top = rect.top + "px"; 197 toast.style.left = rect.left + "px"; 198 toast.style.width = rect.width + "px"; 199 toast.style.height = rect.height + "px"; 200 toast.style.margin = "0"; 201 }); 202 203 toast.classList.remove("show"); 204 // `hide` drives the exit transform, including the RTL-specific rule that 205 // was previously unreachable because nothing ever added this class. 206 toast.classList.add("hide"); 207 208 window.setTimeout(function () { 209 if (toast.parentNode) toast.remove(); 210 }, 500); 211 } 212 213 function prefersReducedMotion() { 214 return !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); 215 } 216 217 // =============================== 218 // Utility: Smooth stack reflow (FLIP) 219 // =============================== 220 // When a toast is added or removed, the others glide to their new positions 221 // instead of jumping. Reflow uses the independent `translate` CSS property so it 222 // never fights the entrance/exit animation, which uses `transform` â that means 223 // a toast can slide in AND reflow vertically at the same time without conflict. 224 function flipReflow(container, mutate) { 225 if (!container || prefersReducedMotion()) { 226 mutate(); 227 return; 228 } 229 230 var reflowTransition = "translate .5s cubic-bezier(0.22, 0.61, 0.36, 1), transform .5s ease-in-out, opacity .5s ease-in-out"; 231 232 var snapshot = Array.prototype.slice.call(container.querySelectorAll(".toast-item")) 233 .filter(function (el) { return !el.dataset.tmClosing; }) 234 .map(function (el) { return { el: el, top: el.getBoundingClientRect().top }; }); 235 236 mutate(); 237 238 snapshot.forEach(function (entry) { 239 var el = entry.el; 240 if (!el.isConnected) return; 241 242 // Cancel any reflow still animating on this element so its stale 243 // cleanup can't wipe the new one mid-glide â that race is what made 244 // the stack teleport when entrances and exits overlapped. 245 if (el._tmReflowCleanup) { 246 el._tmReflowCleanup(); 247 } 248 249 el.style.transition = "none"; 250 el.style.translate = "0px"; 251 var delta = entry.top - el.getBoundingClientRect().top; 252 if (!delta) { 253 el.style.transition = ""; 254 el.style.translate = ""; 255 return; 256 } 257 258 el.style.translate = "0 " + delta + "px"; 259 void el.offsetHeight; // flush the inverted position before playing 260 261 window.requestAnimationFrame(function () { 262 el.style.transition = reflowTransition; 263 el.style.translate = "0 0"; 264 265 var settled = false;
266 var fallbackTimer = null; 267 268 // Single cleanup path shared by transitionend and the timeout. 269 // Without the timeout an interrupted transition would leave the 270 // inline transition/translate styles stuck on the element. 271 var cleanup = function () { 272 if (settled) return; 273 settled = true; 274 if (fallbackTimer) window.clearTimeout(fallbackTimer); 275 el.style.transition = ""; 276 el.style.translate = ""; 277 el.removeEventListener("transitionend", onEnd); 278 el._tmReflowCleanup = null; 279 }; 280 281 var onEnd = function (event) { 282 if (event.propertyName !== "translate") return; 283 cleanup(); 284 }; 285 286 fallbackTimer = window.setTimeout(cleanup, 700); 287 el._tmReflowCleanup = cleanup; 288 el.addEventListener("transitionend", onEnd); 289 }); 290 }); 291 } 292 293 // =============================== 294 // Utility: Apply configured style variables 295 // =============================== 296 // The spacing/typography config is resolved server-side into CSS custom 297 // properties (see ToastMagic::resolveStyleVariables) and published as 298 // `window.toastMagicStyleVars`. 299 function applyToastMagicStyleVars(container) { 300 if (!container) return; 301 var vars = getStyleVars(); 302 for (var name in vars) { 303 if (Object.prototype.hasOwnProperty.call(vars, name)) { 304 container.style.setProperty(name, vars[name]); 305 } 306 } 307 } 308 309 // =============================== 310 // Utility: Icon markup 311 // =============================== 312 // Preset icons (v2.6). Stroke-based rather than filled, so individual parts 313 // can be moved and drawn on with stroke-dashoffset â the four type icons 314 // below are filled paths and are deliberately left exactly as they were. 315 // 316 // Like every other icon in this file these are package constants. The 317 // registry is keyed by name and a caller can only ever supply a *key*, never 318 // markup, so the trusted-icon parser gains no new attack surface. 319 // Icon parts are painted from four custom properties rather than a single 320 // `currentColor`, which is what makes a preset icon multi-coloured: 321 // 322 // --tm-i1 primary stroke (the body of the icon) 323 // --tm-i2 accent (wheels, stripes, the filled part) 324 // --tm-i3 secondary accent 325 // --tm-ic reserved for a preset's fourth part 326 // 327 // Every one falls back to `currentColor`, so an icon with no palette behind 328 // it still renders in the type accent exactly as the built-in icons do â 329 // and setting them all back to `currentColor` returns the whole set to 330 // monochrome. The palettes live in the stylesheet, per preset, with dark 331 // mode variants. 332 var ICON_ATTRS = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" ' 333 + 'fill="none" stroke="var(--tm-i1, currentColor)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'; 334 335 // Shorthands for the per-part paint attributes. 336 var S2 = ' stroke="var(--tm-i2, currentColor)"'; 337 var S3 = ' stroke="var(--tm-i3, currentColor)"'; 338 var F2 = ' fill="var(--tm-i2, currentColor)"' + S2; 339 var F3 = ' fill="var(--tm-i3, currentColor)"' + S3; 340 341 // Icon geometry is Lucide v1.33.0, used unmodified except where an animation 342 // needs a part addressable on its own (see `trash` and `download` below). 343 // 344 // Lucide â ISC License, Copyright (c) 2026 Lucide Icons and Contributors 345 // `check`, `download`, `upload` and `trash-2` derive from Feather â 346 // MIT License, Copyright (c) 2013-present Cole Bemis 347 // 348 // Both licences permit redistribution, which is what makes these safe to 349 // ship inside an MIT package. Icon sets that forbid redistribution or 350 // require attribution from *your* end users deliberately are not used here. 351 var PRESET_ICONS = {}; 352 353 // =============================== 354 // Preset registry 355 // =============================== 356 // A preset is a presentation layer *on top of* a type: it swaps the icon and 357 // gives it motion. The type still owns colour, the progress bar, the theme 358 // treatment and â importantly â screen-reader urgency, which is why nothing 359 // here can change how a toast is announced. 360 // 361 // Closed registry by design. A name that is not a key here is ignored and the 362 // toast falls back to its type icon, matching how every other enumerated 363 // value in this package degrades. 364 // Filled by preset packs, which are separate files loaded after this one. 365 // The core runtime ships no presets: a project that uses none pays nothing 366 // for them, and a project loads only the packs it asks for in config. 367 var TOAST_PRESETS = {}; 368 369 // =============================== 370 // Preset pack registration 371 // =============================== 372 // A pack is a plain script that calls register() with its own icons and 373 // preset definitions. The paint shorthands are handed back out so a pack 374 // does not have to restate them, and so every pack paints from the same
375 // four custom properties the core documents. 376 // 377 // Registering is additive and last-write-wins per key, which lets an 378 // application ship its own pack that overrides one preset without having to 379 // replace the whole file. 380 // Which pack each preset came from. Not used for rendering â it exists so a 381 // page can report what it actually loaded, rather than trusting a list that 382 // drifts from the files. 383 var PRESET_PACK_OF = {}; 384 385 window.ToastMagicPresets = { 386 attrs: ICON_ATTRS, 387 s2: S2, 388 s3: S3, 389 f2: F2, 390 f3: F3, 391 392 register: function (icons, presets, pack) { 393 var name; 394 395 for (name in icons) { 396 if (Object.prototype.hasOwnProperty.call(icons, name)) { 397 PRESET_ICONS[name] = icons[name]; 398 } 399 } 400 401 for (name in presets) { 402 if (Object.prototype.hasOwnProperty.call(presets, name)) { 403 TOAST_PRESETS[name] = presets[name]; 404 if (pack) PRESET_PACK_OF[name] = String(pack); 405 } 406 } 407 } 408 }; 409 410 // Returns the preset definition, or null for anything unregistered. The 411 // hasOwnProperty guard keeps inherited property names ("constructor", 412 // "toString") from resolving to something that is not a preset. 413 function resolvePreset(name) { 414 if (typeof name !== "string" || name === "") return null; 415 416 return Object.prototype.hasOwnProperty.call(TOAST_PRESETS, name) 417 ? TOAST_PRESETS[name] 418 : null; 419 } 420 421 function getToasterIcon(key) { 422 var name = String(key); 423 424 // Preset icons first; the type and close icons keep their own markup. 425 if (Object.prototype.hasOwnProperty.call(PRESET_ICONS, name)) { 426 return PRESET_ICONS[name]; 427 } 428 429 switch (name) { 430 case "success": 431 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="20" height="20"><path fill="currentColor" d="M405.333,0H106.667C47.786,0.071,0.071,47.786,0,106.667v298.667C0.071,464.214,47.786,511.93,106.667,512h298.667C464.214,511.93,511.93,464.214,512,405.333V106.667C511.93,47.786,464.214,0.071,405.333,0z M426.667,172.352L229.248,369.771c-16.659,16.666-43.674,16.671-60.34,0.012c-0.004-0.004-0.008-0.008-0.012-0.012l-83.563-83.541c-8.348-8.348-8.348-21.882,0-30.229s21.882-8.348,30.229,0l83.541,83.541l197.44-197.419c8.348-8.318,21.858-8.294,30.176,0.053C435.038,150.524,435.014,164.034,426.667,172.352z"/></svg>'; 432 case "error": 433 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="m19,0H5C2.243,0,0,2.243,0,5v14c0,2.757,2.243,5,5,5h14c2.757,0,5-2.243,5-5V5c0-2.757-2.243-5-5-5Zm-1.231,6.641l-4.466,5.359,4.466,5.359c.354.425.296,1.056-.128,1.409-.188.155-.414.231-.64.231-.287,0-.571-.122-.77-.359l-4.231-5.078-4.231,5.078c-.198.237-.482.359-.77.359-.226,0-.452-.076-.64-.231-.424-.354-.481-.984-.128-1.409l4.466-5.359-4.466-5.359c-.354-.425-.296-1.056.128-1.409.426-.353,1.056-.296,1.409.128l4.231,5.078,4.231-5.078c.354-.424.983-.48,1.409-.128.424.354.481.984.128,1.409Z"/></svg>'; 434 case "info": 435 return '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="20" height="20" viewBox="0 0 1920 1920"><path d="M960 0c530.193 0 960 429.807 960 960s-429.807 960-960 960S0 1490.193 0 960 429.807 0 960 0Zm223.797 707.147c-28.531-29.561-67.826-39.944-109.227-39.455-55.225.657-114.197 20.664-156.38 40.315-100.942 47.024-178.395 130.295-242.903 219.312-11.616 16.025-17.678 34.946 2.76 49.697 17.428 12.58 29.978 1.324 40.49-9.897l.69-.74c.801-.862 1.591-1.72 2.37-2.565 11.795-12.772 23.194-25.999 34.593-39.237l2.85-3.31 2.851-3.308c34.231-39.687 69.056-78.805 115.144-105.345 27.4-15.778 47.142 8.591 42.912 35.963-2.535 16.413-11.165 31.874-17.2 47.744-21.44 56.363-43.197 112.607-64.862 168.888-23.74 61.7-47.405 123.425-70.426 185.398l-2 5.38-1.998 5.375c-20.31 54.64-40.319 108.872-53.554 165.896-10.575 45.592-24.811 100.906-4.357 145.697 11.781 25.8 36.77 43.532 64.567 47.566 37.912 5.504 78.906 6.133 116.003-2.308 19.216-4.368 38.12-10.07 56.57-17.005 56.646-21.298 108.226-54.146 154.681-92.755 47.26-39.384 88.919-85.972 126.906-134.292 12.21-15.53 27.004-32.703 31.163-52.596 3.908-18.657-12.746-45.302-34.326-34.473-11.395 5.718-19.929 19.867-28.231 29.27-10.42 11.798-21.044 23.423-31.786 34.92-21.488 22.987-43.513 45.463-65.634 67.831-13.54 13.692-30.37 25.263-47.662 33.763-21.59 10.609-38.785-1.157-36.448-25.064 2.144-21.954 7.515-44.145 15.046-64.926 30.306-83.675 61.19-167.135 91.834-250.686 19.157-52.214 38.217-104.461 56.999-156.816 17.554-48.928 32.514-97.463 38.834-149.3 4.357-35.71-4.9-72.647-30.269-98.937Zm63.72-401.498c-91.342-35.538-200.232 25.112-218.574 121.757-13.25 69.784 13.336 131.23 67.998 157.155 105.765 50.16 232.284-29.954 232.29-147.084.005-64.997-28.612-111.165-81.715-131.828Z" fill-rule="evenodd"/></svg>'; 436 case "warning": 437 return '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="20" height="20" viewBox="0 0 52 52"><path d="m51.4 42.5l-22.9-37c-1.2-2-3.8-2-5 0l-22.9 37c-1.4 2.3 0 5.5 2.5 5.5h45.8c2.5 0 4-3.2 2.5-5.5z m-25.4-2.5c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z m3-9c0 0.6-0.4 1-1 1h-4c-0.6 0-1-0.4-1-1v-13c0-0.6 0.4-1 1-1h4c0.6 0 1 0.4 1 1v13z"/></svg>'; 438 case "close": 439 return '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="currentColor"/></svg>'; 440 default: 441 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
441<path fill="currentColor" d="m19,0H5C2.243,0,0,2.243,0,5v14c0,2.757,2.243,5,5,5h14c2.757,0,5-2.243,5-5V5c0-2.757-2.243-5-5-5Zm-8,6c0-.553.447-1,1-1s1,.447,1,1v7.5c0,.553-.447,1-1,1s-1-.447-1-1v-7.5Zm1,13c-.828,0-1.5-.672-1.5-1.5s.672-1.5,1.5-1.5,1.5.672,1.5,1.5-.672,1.5-1.5,1.5Z"/></svg>'; 442 } 443 } 444 445 function typeClasses(type) { 446 switch (type) { 447 case "success": return { item: "toast-success", basic: "success" }; 448 case "error": return { item: "toast-danger", basic: "danger" }; 449 case "warning": return { item: "toast-warning", basic: "warning" }; 450 default: return { item: "toast-info", basic: "info" }; 451 } 452 } 453 454 // =============================== 455 // ToastMagic class 456 // =============================== 457 if (typeof window.ToastMagic === "undefined") { 458 window.ToastMagic = function ToastMagic() { 459 this.toastContainer = null; 460 this._pending = []; 461 this._ready = false; 462 this._init(); 463 }; 464 465 window.ToastMagic.prototype._init = function () { 466 var self = this; 467 468 if (document.readyState === "loading") { 469 // Placing the scripts in <head> used to throw, because the 470 // constructor reached for document.body during parse. Now the 471 // container is built when the DOM is ready and any toast raised 472 // before that point is queued rather than lost. 473 document.addEventListener("DOMContentLoaded", function () { self._boot(); }); 474 return; 475 } 476 477 this._boot(); 478 }; 479 480 window.ToastMagic.prototype._boot = function () { 481 if (this._ready) return; 482 483 this.ensureContainer(); 484 ensureLiveRegions(); 485 this._ready = true; 486 487 var pending = this._pending; 488 this._pending = []; 489 for (var i = 0; i < pending.length; i++) { 490 this.show(pending[i]); 491 } 492 }; 493 494 // Create the container if needed and (re)apply every container-level 495 // modifier from the current config. Modifiers are toggled individually 496 // rather than by overwriting className, so classes added by the host 497 // application survive. 498 window.ToastMagic.prototype.ensureContainer = function () { 499 if (!document.body) return null; 500 501 if (!this.toastContainer || !this.toastContainer.isConnected) { 502 this.toastContainer = document.querySelector(".toast-container"); 503 } 504 505 if (!this.toastContainer) { 506 this.toastContainer = document.createElement("div"); 507 this.toastContainer.className = "toast-container"; 508 document.body.appendChild(this.toastContainer); 509 } 510 511 var container = this.toastContainer; 512 var position = configValue("positionClass", "toast-top-end"); 513 var theme = configValue("theme", "default"); 514 515 [ 516 "toast-top-start", "toast-top-end", "toast-top-center", 517 "toast-bottom-start", "toast-bottom-end", "toast-bottom-center" 518 ].forEach(function (name) { 519 container.classList.toggle(name, name === position); 520 }); 521 522 Array.prototype.slice.call(container.classList).forEach(function (name) { 523 if (name.indexOf("theme-") === 0 && name !== "theme-" + theme) { 524 container.classList.remove(name); 525 } 526 }); 527 container.classList.add("theme-" + theme); 528 529 container.classList.toggle("toast-gradient-enable", !!getConfig().gradient_enable); 530 container.classList.toggle("toast-color-true", !!getConfig().color_mode); 531 532 if (!container.hasAttribute("aria-label")) { 533 container.setAttribute("aria-label", configValue("containerLabel", "Notifications")); 534 } 535 536 applyToastMagicStyleVars(container); 537 538 return container; 539 }; 540 541 window.ToastMagic.prototype.show = function (options) { 542 var settings = options || {}; 543 544 if (!this._ready) { 545 this._pending.push(settings); 546 return; 547 } 548 549 var container = this.ensureContainer(); 550 if (!container) return; 551 552 var config = getConfig(); 553 var type = TOAST_TYPES.indexOf(settings.type) === -1 ? "info" : settings.type; 554 var heading = settings.heading === undefined || settings.heading === null ? "" : String(settings.heading); 555 var description = settings.description === undefined || settings.description === null ? "" : String(settings.description); 556 557 // Escaping is the default. `html: true` per toast, or a global 558 // `escape_html: false`, opts back into raw HTML rendering. 559 var allowHtml = settings.html === true || config.escape_html === false; 560 561 var showCloseBtn = settings.showCloseBtn === undefined 562 ? !!config.closeButton 563 : !!settings.showCloseBtn; 564 565 var customBtnText = settings.customBtnText ? String(settings.customBtnText) : ""; 566 var customBtnLink = settings.customBtnLink ? String(settings.customBtnLink) : ""; 567 var avatar = settings.avatar ? String(settings.avatar) : ""; 568 569 // An unregistered preset is ignored rather than rejected: the toast 570 // still renders, with its type icon and no icon animation. 571 var preset = resolvePreset(settings.preset); 572 var presetName = preset ? String(settings.preset) : ""; 573 574 // Skip rendering if an identical toast is already visible and 575 // duplicate prevention is enabled in the config. 576 // 577 // The preset is part of the key: "Saved" as `settings-saved` and 578 // "Saved" as `clipboard-copy` are different toasts, and collapsing 579 // them would silently drop the second one's icon. 580 var duplicateKey = type + "|" + presetName + "|" + heading + "|" + description; 581 if (config.preventDuplicates) { 582 var existing = Array.prototype.slice.call(container.querySelectorAll(".toast-item")); 583 for (var d = 0; d < existing.length; d++) { 584 if (existing[d].dataset.toastKey === duplicateKey && !existing[d].dataset.tmClosing) return; 585 } 586 } 587 588 var names = typeClasses(type); 589 590 var showDuration = typeof settings.showDuration === "number" 591 ? settings.showDuration 592 : (typeof config.showDuration === "number" ? config.showDuration : 300); 593
594 // `timeOut: 0` means "stay until dismissed" â a WCAG 2.2.1 escape 595 // hatch. Only a strictly positive number starts the dismiss timer. 596 var timeOut = typeof settings.timeOut === "number" 597 ? settings.timeOut 598 : (typeof config.timeOut === "number" ? config.timeOut : 5000); 599 600 var toast = this._buildToast({ 601 type: type, 602 names: names, 603 heading: heading, 604 description: description, 605 allowHtml: allowHtml, 606 showCloseBtn: showCloseBtn, 607 customBtnText: customBtnText, 608 customBtnLink: customBtnLink, 609 avatar: avatar, 610 preset: preset, 611 presetName: presetName, 612 duplicateKey: duplicateKey, 613 timeOut: timeOut, 614 showDuration: showDuration 615 }); 616 617 var position = configValue("positionClass", "toast-top-end"); 618 var atBottom = String(position).indexOf("bottom") !== -1; 619 620 // Cap the stack before inserting. The container is `position: fixed` 621 // with no scroll and `pointer-events: none`, so anything pushed past 622 // the edge of the viewport is neither readable nor closable â it just 623 // waits out its timer off-screen. Roughly six toasts fit on a phone. 624 var maxVisible = Math.max(0, parseInt(configValue("maxVisible", 15), 10) || 0); 625 if (maxVisible > 0) { 626 var live = Array.prototype.slice.call(container.querySelectorAll(".toast-item")) 627 .filter(function (el) { return !el.dataset.tmClosing; }); 628 629 // The newest sits closest to the anchored corner, so the oldest are 630 // at the far end: the tail when we prepend, the head when we append. 631 for (var i = 0; i < live.length - (maxVisible - 1); i++) { 632 closeToastMagicItem(atBottom ? live[i] : live[live.length - 1 - i]); 633 } 634 } 635 636 // Newest toast always appears closest to its anchored corner: on top 637 // for top positions (older toasts move down), at the bottom for bottom 638 // positions (older toasts move up). 639 if (atBottom) { 640 flipReflow(container, function () { container.append(toast); }); 641 } else { 642 flipReflow(container, function () { container.prepend(toast); }); 643 } 644 645 window.setTimeout(function () { toast.classList.add("show"); }, showDuration); 646 647 announce(type, heading, description); 648 649 this._startDismissTimer(toast, timeOut); 650 651 return toast; 652 }; 653 654 window.ToastMagic.prototype._buildToast = function (spec) { 655 var config = getConfig(); 656 657 var toast = document.createElement("div"); 658 toast.className = "toast-item " + spec.names.item; 659 toast.dataset.toastKey = spec.duplicateKey; 660 // Deliberately NOT `data-toast-type`: that attribute is the trigger 661 // API, and the delegated handler finds triggers with 662 // closest("[data-toast-type]"). Stamping it here made every rendered 663 // toast its own trigger, so clicking one spawned a duplicate. 664 toast.dataset.toastItemType = spec.type; 665 666 var animation = config.animation; 667 if (animation && animation !== "default") { 668 toast.classList.add("toast-animate-" + animation); 669 } 670 671 // The toast is a labelled group rather than its own live region â 672 // announcement is handled by the persistent regions above, and 673 // duplicating it here would make screen readers read it twice. 674 toast.setAttribute("role", "group"); 675 toast.setAttribute( 676 "aria-label", 677 (config.typeLabels && config.typeLabels[spec.type]) || spec.type + " notification" 678 ); 679 680 // Drive the progress bar from the real dismiss time instead of the 681 // hardcoded 3s it used to assume. 682 if (spec.timeOut > 0) { 683 toast.style.setProperty("--tm-toast-duration", spec.timeOut + "ms"); 684 toast.style.setProperty("--tm-toast-delay", spec.showDuration + "ms"); 685 } else { 686 toast.classList.add("toast-no-timeout"); 687 } 688 689 var positionRelative = document.createElement("div"); 690 positionRelative.className = "toast-magic-relative"; 691 692 // `shake` is the one preset animation that moves more than the icon. 693 // It cannot live on .toast-item: `transform` there is owned by the 694 // entrance/exit transition and `translate` by the FLIP stack reflow, 695 // so a third animation would fight both. This inner wrapper is not 696 // transformed by anything, and leaving .toast-item alone keeps the 697 // progress bar (a pseudo-element on it) steady while the content moves. 698 if (spec.preset && spec.preset.shake) { 699 positionRelative.classList.add("tm-anim-shake"); 700 } 701 702 var content = document.createElement("div"); 703 content.className = "toast-item-content-center"; 704 705 var body = document.createElement("div"); 706 body.className = "toast-body" + (spec.avatar ? " toast-body-avatar" : ""); 707 708 var iconContainer = document.createElement("span"); 709 iconContainer.className = "toast-body-icon-container toast-text-" + spec.names.basic; 710 711 var avatarUrl = spec.avatar ? sanitizeUrl(spec.avatar, IMAGE_PROTOCOLS) : null; 712 if (avatarUrl) { 713 var img = document.createElement("img"); 714 // setAttribute cannot break out of the attribute, whatever the value. 715 img.setAttribute("src", avatarUrl); 716 img.setAttribute("alt", ""); 717 img.className = "toast-avatar"; 718 iconContainer.appendChild(img); 719 } else { 720 if (spec.avatar) body.classList.remove("toast-body-avatar"); 721 722 // A preset swaps the type icon for its own. Note the order: an 723 // avatar still wins, so a rejected avatar URL falls back to the
724 // preset icon when there is one and to the type icon otherwise. 725 var icon = buildIcon(getToasterIcon(spec.preset ? spec.preset.icon : spec.type)); 726 727 if (spec.preset) { 728 iconContainer.classList.add( 729 "tm-preset", 730 "tm-preset-" + spec.presetName, 731 "tm-anim-" + spec.preset.anim 732 ); 733 734 // setAttribute rather than classList: SVGAnimatedString is 735 // read-only for `className` in older engines, and these 736 // elements carry no class of their own to preserve. 737 if (icon) icon.setAttribute("class", "tm-icon-base"); 738 } 739 740 if (icon) iconContainer.appendChild(icon); 741 742 if (spec.preset && spec.preset.particles) { 743 for (var p = 0; p < spec.preset.particles; p++) { 744 var particle = document.createElement("i"); 745 particle.className = "tm-particle"; 746 particle.setAttribute("aria-hidden", "true"); 747 iconContainer.appendChild(particle); 748 } 749 } 750 } 751 752 var bodyContainer = document.createElement("div"); 753 bodyContainer.className = "toast-body-container"; 754 755 if (spec.heading) { 756 var titleWrap = document.createElement("div"); 757 titleWrap.className = "toast-body-title"; 758 var title = document.createElement("h4"); 759 renderText(title, spec.heading, spec.allowHtml); 760 titleWrap.appendChild(title); 761 bodyContainer.appendChild(titleWrap); 762 } 763 764 if (spec.description) { 765 var paragraph = document.createElement("p"); 766 paragraph.className = "toast-body-description"; 767 renderText(paragraph, spec.description, spec.allowHtml); 768 bodyContainer.appendChild(paragraph); 769 } 770 771 body.appendChild(iconContainer); 772 body.appendChild(bodyContainer); 773 774 var end = document.createElement("div"); 775 end.className = "toast-body-end"; 776 777 if (spec.showCloseBtn) { 778 var closeBtn = document.createElement("button"); 779 closeBtn.setAttribute("type", "button"); 780 closeBtn.className = "toast-close-btn"; 781 closeBtn.setAttribute("aria-label", configValue("closeButtonLabel", "Close notification")); 782 var closeIcon = buildIcon(getToasterIcon("close")); 783 if (closeIcon) closeBtn.appendChild(closeIcon); 784 end.appendChild(closeBtn); 785 } 786 787 if (spec.customBtnText && spec.customBtnLink) { 788 var link = document.createElement("a"); 789 // An unusable URL degrades to "#", matching the documented 790 // contract, and is safe because it is applied as an attribute. 791 link.setAttribute("href", sanitizeUrl(spec.customBtnLink, LINK_PROTOCOLS) || "#"); 792 link.className = "toast-custom-btn toast-btn-bg-" + spec.names.basic; 793 renderText(link, spec.customBtnText, spec.allowHtml); 794 end.appendChild(link); 795 } 796 797 content.appendChild(body); 798 content.appendChild(end); 799 positionRelative.appendChild(content); 800 toast.appendChild(positionRelative); 801 802 return toast; 803 }; 804 805 // Auto-dismiss with pause on hover *and* keyboard focus. Pausing the 806 // timer also pauses the progress bar, which previously kept running. 807 window.ToastMagic.prototype._startDismissTimer = function (toast, timeOut) { 808 if (!(timeOut > 0)) { 809 toast._tmDispose = function () {}; 810 return; 811 } 812 813 var config = getConfig(); 814 var pauseOnHover = config.pauseOnHover !== false; 815 var remaining = timeOut; 816 var startedAt = Date.now(); 817 var timer = window.setTimeout(function () { closeToastMagicItem(toast); }, remaining); 818 var paused = false;
819 820 function pause() { 821 if (paused) return; 822 paused = true; 823 window.clearTimeout(timer); 824 remaining -= Date.now() - startedAt; 825 toast.classList.add("toast-paused"); 826 } 827 828 function resume() { 829 if (!paused) return; 830 paused = false; 831 startedAt = Date.now(); 832 toast.classList.remove("toast-paused"); 833 timer = window.setTimeout(function () { closeToastMagicItem(toast); }, Math.max(remaining, 0)); 834 } 835 836 toast._tmDispose = function () { 837 window.clearTimeout(timer); 838 toast.removeEventListener("mouseenter", pause); 839 toast.removeEventListener("mouseleave", resume); 840 toast.removeEventListener("focusin", pause); 841 toast.removeEventListener("focusout", resume); 842 }; 843 844 if (pauseOnHover) { 845 toast.addEventListener("mouseenter", pause); 846 toast.addEventListener("mouseleave", resume); 847 } 848 849 // Focus pausing is not optional: a keyboard user who has tabbed into 850 // a toast must not have it yanked away mid-interaction. 851 toast.addEventListener("focusin", pause); 852 toast.addEventListener("focusout", resume); 853 }; 854 855 // Accepts either the positional signature kept for backward 856 // compatibility, or a single options object (what the PHP side emits). 857 window.ToastMagic.prototype._parseArgs = function (args) { 858 if (args.length === 1 && args[0] && typeof args[0] === "object") { 859 return args[0]; 860 } 861 862 return { 863 heading: args[0] !== undefined ? args[0] : "", 864 description: args[1] !== undefined ? args[1] : "", 865 showCloseBtn: args[2] !== undefined ? args[2] : undefined, 866 customBtnText: args[3] !== undefined ? args[3] : "", 867 customBtnLink: args[4] !== undefined ? args[4] : "", 868 timeOut: args[5] !== undefined ? args[5] : null, 869 showDuration: args[6] !== undefined ? args[6] : null, 870 avatar: args[7] !== undefined ? args[7] : "" 871 }; 872 }; 873 874 TOAST_TYPES.forEach(function (type) { 875 window.ToastMagic.prototype[type] = function () { 876 var parsed = this._parseArgs(Array.prototype.slice.call(arguments)); 877 parsed.type = type; 878 return this.show(parsed); 879 }; 880 }); 881 882 // Programmatically dismiss every currently visible toast. 883 window.ToastMagic.prototype.clear = function () { 884 if (!this.toastContainer) return; 885 Array.prototype.slice.call(this.toastContainer.querySelectorAll(".toast-item")) 886 .forEach(function (toast) { closeToastMagicItem(toast); }); 887 }; 888 889 window.ToastMagic.prototype.dismissAll = function () { 890 this.clear(); 891 }; 892 } 893 894 // =============================== 895 // Single shared instance 896 // =============================== 897 if (typeof window.toastMagic === "undefined") { 898 window.toastMagic = new window.ToastMagic(); 899 } 900 901 // =============================== 902 // Delegated document listeners (bound once) 903 // =============================== 904 if (!window._toastMagicListenersBound) { 905 window._toastMagicListenersBound = true; 906 907 document.addEventListener("click", function (event) { 908 var closeBtn = event.target.closest && event.target.closest(".toast-close-btn"); 909 if (closeBtn) { 910 closeToastMagicItem(closeBtn.closest(".toast-item")); 911 return; 912 } 913 914 var trigger = event.target.closest && event.target.closest("[data-toast-type]"); 915 if (!trigger) return; 916 917 // A rendered toast must never act as a trigger. The marker attribute 918 // no longer collides, but this also covers an application that puts 919 // its own data-toast-type inside a toast â clicking a toast should 920 // dismiss or do nothing, never spawn another one. 921 if (trigger.closest(".toast-container") || trigger.classList.contains("toast-item")) return; 922 923 // Only real toast types are accepted. Previously any truthy property 924 // name â "show", "clear", "constructor" â was invoked here. 925 var type = trigger.getAttribute("data-toast-type"); 926 if (TOAST_TYPES.indexOf(type) === -1) type = "info"; 927 928 window.toastMagic[type]({ 929 heading: trigger.getAttribute("data-toast-heading") || "Notification", 930 description: trigger.getAttribute("data-toast-description") || "", 931 showCloseBtn: trigger.hasAttribute("data-toast-close-btn"), 932 customBtnText: trigger.getAttribute("data-toast-btn-text") || "", 933 customBtnLink: trigger.getAttribute("data-toast-btn-l
933ink") || "", 934 avatar: trigger.getAttribute("data-toast-avatar") || "", 935 // Validated in show() against the preset registry, so an 936 // unregistered name here is ignored rather than rendered. 937 preset: trigger.getAttribute("data-toast-preset") || "" 938 }); 939 }); 940 941 // Escape dismisses the most recent toast, giving keyboard users a way 942 // out that does not depend on the close button being enabled. 943 document.addEventListener("keydown", function (event) { 944 if (event.key !== "Escape" && event.key !== "Esc") return; 945 946 var container = document.querySelector(".toast-container"); 947 if (!container) return; 948 949 var toasts = Array.prototype.slice.call(container.querySelectorAll(".toast-item")) 950 .filter(function (toast) { return !toast.dataset.tmClosing; }); 951 952 if (!toasts.length) return; 953 954 var position = configValue("positionClass", "toast-top-end"); 955 // The newest toast is nearest the anchored corner: first child for 956 // top positions, last child for bottom ones. 957 var newest = String(position).indexOf("bottom") !== -1 958 ? toasts[toasts.length - 1] 959 : toasts[0]; 960 961 closeToastMagicItem(newest); 962 }); 963 } 964 965 // Exposed for the test suite and for integrations that need the same 966 // guarantees (the Livewire bridge reuses `show`). 967 window.ToastMagicInternals = { 968 sanitizeUrl: sanitizeUrl, 969 renderText: renderText, 970 closeToast: closeToastMagicItem, 971 TOAST_TYPES: TOAST_TYPES, 972 LINK_PROTOCOLS: LINK_PROTOCOLS, 973 IMAGE_PROTOCOLS: IMAGE_PROTOCOLS, 974 // The Livewire bridge reads this rather than keeping its own copy of the 975 // preset list, so the two cannot drift apart. 976 TOAST_PRESETS: TOAST_PRESETS, 977 PRESET_PACK_OF: PRESET_PACK_OF, 978 resolvePreset: resolvePreset 979 }; 980})();
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.