PageSourceSearch

https://minhaaonet.com.br/packages/devrabiul/laravel-toaster-magic/js/laravel-toaster-magic.js

js minhaaonet.com.br collected 2026-10-02 01:18:12 UTC 46,909 bytes, 980 lines download raw bytes

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.