1/** 2 * Menu Popup 3 * Shows a modal carousel of promotional slides on menu page load, 4 * with dismissal memory (localStorage/sessionStorage). 5 * 6 * Expects: 7 * window.MENU_POPUP - config from menu_popup.json (or null) 8 * window.STORE_SLUG - store slug in multi-store, else null 9 * #menuPopupModal - DOM element rendered by server if slides exist 10 */ 11(function () { 12 'use strict'; 13 14 const MenuPopup = { 15 config: null, 16 modalEl: null, 17 modal: null, 18 carousel: null, 19 autoplayTimer: null, 20 isPaused: false, 21 22 init() { 23 this.config = window.MENU_POPUP || null; 24 this.modalEl = document.getElementById('menuPopupModal'); 25 26 // Server decides whether to render the modal. If absent, nothing to do. 27 if (!this.modalEl) return; 28 29 // Config gate (redundant safety â if modal rendered, config was valid server-side) 30 if (!this.config || this.config.enabled === false) return; 31 32 // Dismissal gate 33 if (this.isDismissed()) return; 34 35 // Bootstrap instances 36 if (typeof bootstrap === 'undefined') { 37 console.warn('[MenuPopup] Bootstrap not loaded'); 38 return; 39 } 40 41 this.modal = new bootstrap.Modal(this.modalEl, { 42 backdrop: true, 43 keyboard: true, 44 focus: true 45 }); 46 47 const carouselEl = this.modalEl.querySelector('#menuPopupCarousel'); 48 if (carouselEl) { 49 this.carousel = new bootstrap.Carousel(carouselEl, { 50 interval: false, // we control autoplay manually 51 ride: false, 52 touch: true, 53 wrap: true 54 }); 55 } 56 57 this.bindEvents(); 58 this.show(); 59 }, 60 61 bindEvents() { 62 // Persist dismissal when modal closes (any trigger: X, backdrop, ESC) 63 this.modalEl.addEventListener('hidden.bs.modal', () => { 64 this.markDismissed(); 65 this.stopAutoplay(); 66 }); 67 68 const carouselEl = this.modalEl.querySelector('#menuPopupCarousel'); 69 if (!carouselEl) return; 70 71 // Pause autoplay on hover, resume on leave 72 carouselEl.addEventListener('mouseenter', () => { this.isPaused = true; }); 73 carouselEl.addEventListener('mouseleave', () => { this.isPaused = false; }); 74 75 // Reset autoplay timer on any manual navigation (clicks on controls or indicators) 76 const resetTimer = () => { 77 if (this.autoplayTimer) this.startAutoplay(); 78 }; 79 carouselEl.querySelectorAll('.carousel-control-prev, .carousel-control-next, .carousel-indicators button').forEach(el => { 80 el.addEventListener('click', resetTimer); 81 }); 82 }, 83 84 show() { 85 this.modal.show(); 86 87 // Start autoplay only if carousel present (i.e. multiple slides) 88 const isMulti = this.modalEl.querySelectorAll('.carousel-item').length > 1; 89 if (isMulti && this.config.autoplay !== false) { 90 this.startAutoplay(); 91 } 92 }, 93 94 startAutoplay() { 95 this.stopAutoplay(); 96 const interval = Math.max(2000, parseInt(this.config.autoplay_interval, 10) || 5000); 97 this.autoplayTimer = setInterval(() => { 98 if (this.isPaused || !this.carousel) return; 99 this.carousel.next(); 100 }, interval); 101 }, 102 103 stopAutoplay() { 104 if (this.autoplayTimer) { 105 clearInterval(this.autoplayTimer); 106 this.autoplayTimer = null; 107 } 108 }, 109 110 /* -------- Dismissal memory -------- */ 111 112 storageKey() { 113 const slug = window.STORE_SLUG || '_default'; 114 return `menu_popup_dismissed_${slug}`; 115 }, 116 117 isDismissed() { 118 // Session-only mode 119 if (this.config.show_once_per_session) { 120 try { 121 return sessionStorage.getItem(this.storageKey()) === '1'; 122 } catch (e) { 123 return false;
124 } 125 } 126 127 // remember_days mode (default) 128 const days = parseInt(this.config.remember_days, 10); 129 if (!days || days <= 0) return false; // 0 = always show 130 131 try { 132 const stored = localStorage.getItem(this.storageKey()); 133 if (!stored) return false; 134 135 const dismissedAt = parseInt(stored, 10); 136 if (!dismissedAt) return false; 137 138 const msSinceDismiss = Date.now() - dismissedAt; 139 const msInDays = days * 24 * 60 * 60 * 1000; 140 return msSinceDismiss < msInDays; 141 } catch (e) { 142 return false; 143 } 144 }, 145 146 markDismissed() { 147 try { 148 if (this.config.show_once_per_session) { 149 sessionStorage.setItem(this.storageKey(), '1'); 150 } else { 151 localStorage.setItem(this.storageKey(), Date.now().toString()); 152 } 153 } catch (e) { 154 // localStorage/sessionStorage may be unavailable in private mode â fail silently 155 } 156 } 157 }; 158 159 // Auto-init on DOM ready 160 if (document.readyState === 'loading') { 161 document.addEventListener('DOMContentLoaded', () => MenuPopup.init()); 162 } else { 163 MenuPopup.init(); 164 } 165 166 // Expose for debugging / manual reset 167 window.MenuPopup = MenuPopup; 168})();
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.