PageSourceSearch

https://progressiveplants.com/2026projects/updategardenplan/js/toast-notifications.js

js progressiveplants.com collected 2026-09-30 17:23:29 UTC 5,883 bytes, 210 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.