(() => { const language = window.TruckStopLanguage || {text: value => value, path: value => value}; const tr = language.text; const popover = document.getElementById('park-truck-popover'); if (!popover) return; const closeButton = popover.querySelector('.popover-close'); const actionEl = popover.querySelector('#park-popover-action'); const records = { '307': {name:'La Familia Salvimex', cuisine:'Salvadoran & Mexican', hours:'Mon–Tue: 5–10PM\nWed: Closed\nThu–Fri: 5–10PM\nSat–Sun: 12–10PM', page:'/la-familia-salvimex/'}, '306': {name:'Avila Food Truck', cuisine:'Venezuelan', hours:'Mon–Thu: 6–11PM\nFri–Sat: 6PM–12AM\nSun: 6–11PM', page:'/avila-food-truck/'}, '304': {name:'Taquizas Ari', cuisine:'Mexican Street Food', hours:'Daily: 6PM–2AM', page:'/taquizas-ari/'}, '302': {name:'Hood Hibachi', cuisine:'Hibachi & Teppanyaki', hours:'Daily: 7PM–3AM', page:'/hood-hibachi/'} }; const bySpace = {familia:'307', avila:'306', ari:'304', hood:'302'}; const leasing = {name:'Your Truck Here', cuisine:'Power, water & trash included', hours:null}; Object.values(records).forEach(record => { record.cuisine = tr(record.cuisine); record.hours = tr(record.hours); record.page = language.path(record.page); }); leasing.name = tr(leasing.name); leasing.cuisine = tr(leasing.cuisine); let active = null; let hideTimer = null; function cancelHide() { if (hideTimer !== null) window.clearTimeout(hideTimer); hideTimer = null; } function positionNear(target) { const tag = target.getBoundingClientRect(); const width = popover.offsetWidth, height = popover.offsetHeight; const vw = document.documentElement.clientWidth, vh = window.innerHeight; const gutter = 12, gap = 0; let left = tag.left + tag.width / 2 - width / 2; // Always place the popover above its target — never flip below — // clamping into the viewport if the target sits too high to fit. let top = tag.top - height - gap; popover.style.left = Math.max(gutter, Math.min(vw - width - gutter, left)) + 'px'; popover.style.top = Math.max(gutter, Math.min(vh - height - gutter, top)) + 'px'; } function setHours(text) { const row = popover.querySelector('#park-popover-hours-row'); const value = popover.querySelector('#park-popover-hours'); if (text) { value.innerHTML = text.split('\n').map(line => line.replace(/&/g, '&').replace(/'); row.hidden = false; } else { value.textContent = ''; row.hidden = true; } } function open(target, space, record) { cancelHide(); active = target; popover.querySelector('#park-popover-space').textContent = space ? `${tr('Space')} ${space}` : 'Truck Stop 77077'; const exposureEl = popover.querySelector('#park-traffic-exposure'); if (record) { popover.querySelector('#park-popover-title').textContent = record.name; popover.querySelector('#park-popover-fact-label').textContent = tr('Cuisine'); popover.querySelector('#park-popover-fact').textContent = record.cuisine; setHours(record.hours); actionEl.textContent = tr('Menu & More'); actionEl.href = record.page; actionEl.removeAttribute('target'); actionEl.removeAttribute('rel'); actionEl.onclick = null; popover.dataset.page = record.page; // Leased trucks already have their own page for this — the traffic // stat is a leasing pitch aimed at the still-available spots. if (exposureEl) exposureEl.hidden = true; } else { popover.querySelector('#park-popover-title').textContent = leasing.name; popover.querySelector('#park-popover-fact-label').textContent = tr('Included'); popover.querySelector('#park-popover-fact').textContent = leasing.cuisine; setHours(leasing.hours); actionEl.textContent = tr('Join Us'); actionEl.href = '#join-us'; actionEl.removeAttribute('target'); actionEl.removeAttribute('rel'); actionEl.onclick = null; popover.dataset.page = '#join-us'; // Every available space gets the Westheimer traffic-count pitch. if (exposureEl) exposureEl.hidden = false; } popover.hidden = false; positionNear(target); } function close() { cancelHide(); active = null; popover.hidden = true; } function scheduleHide() { cancelHide(); hideTimer = window.setTimeout(() => { hideTimer = null; if (popover.matches(':hover') || popover.contains(document.activeElement)) return; if (active && (active.matches(':hover') || active === document.activeElement)) return; close(); }, 200); } function wireTrigger(el, space, record) { el.addEventListener('pointerenter', event => { if (event.pointerType !== 'mouse') return; if (el === active) cancelHide(); else open(el, space, record); }); el.addEventListener('pointerleave', event => { if (event.pointerType === 'mouse') scheduleHide(); }); el.addEventListener('focus', () => open(el, space, record)); el.addEventListener('blur', scheduleHide); // Touch: rely on Pointer Events (which every current iOS browser app — // Safari and Chrome-for-iOS alike — dispatches the same way, since both // wrap the same WebKit engine) plus a plain click fallback below. An // earlier version also duplicated this with the legacy Touch Events API // as a second redundant layer; that was removed after it coincided with // taps going dead entirely, which pointed at the two layers stepping on // each other rather than fixing anything. open() is idempotent, so the // pointer layer and the click fallback overlapping is harmless. let pointerTouchStart = null; let touchHandledAt = 0; el.addEventListener('pointerdown', event => { pointerTouchStart = event.pointerType === 'touch' ? {x: event.clientX, y: event.clientY} : null; }, {passive: true}); el.addEventListener('pointerup', event => { if (event.pointerType !== 'touch' || !pointerTouchStart) return; const dx = event.clientX - pointerTouchStart.x, dy = event.clientY - pointerTouchStart.y; pointerTouchStart = null; if (Math.hypot(dx, dy) < 12) { event.preventDefault(); // A tap now opens the popover first — for trucks AND vacant // spaces alike, matching behavior across the board. The open // popover's own action button ("Menu & More" / "Join Us"), or // tapping anywhere else on the card via the popover's own // click-through below, is what actually navigates from there. open(el, space, record); // Per the Pointer Events spec, canceling pointerup (unlike // pointerdown) does NOT suppress the compatibility "click" the // browser still fires a moment later on this same element — so // on Android that trailing click was reaching the branch below // and silently sending trucks straight to their page a beat // after we'd just opened the popover (defeating the point of it, // and on some devices racing/canceling itself out entirely, // which is almost certainly what the "can't open the truck // pages" report was). Mark this element as just-handled so that // trailing click is ignored instead of firing a second, competing // action. touchHandledAt = Date.now(); } }); // Plain buttons (map pins, lineup cards) have no destination of their own — // a tap/click should open this popup directly (this covers pointer types // the checks above don't, e.g. pen). Real links (vacant-space "Available" // cards) already go straight to #join-us on click, so leave those alone. if (el.tagName !== 'A') { el.addEventListener('click', event => { if (Date.now() - touchHandledAt < 500) return; event.preventDefault(); // This is the desktop mouse-click path (touch is handled above, // and its trailing compatibility click is ignored by the guard // above). Desktop already previews the popover on hover, so a // real truck's click still goes straight to its page from there; // a vacant space still opens the popover, since it has no page // of its own to send you to. if (record && record.page) { window.location.href = record.page; } else { open(el, space, record); } }); } } function init() { document.querySelectorAll('.map-pin[data-map-space]').forEach(marker => { const space = marker.dataset.mapSpace; wireTrigger(marker, space, records[space]); }); document.querySelectorAll('.truck-button[data-truck]').forEach(card => { const space = bySpace[card.dataset.truck]; if (!space) return; wireTrigger(card, space, records[space]); }); document.querySelectorAll('.truck-button:not([data-truck])').forEach(card => { wireTrigger(card, card.dataset.space || null, null); }); popover.addEventListener('pointerenter', cancelHide); popover.addEventListener('pointerleave', scheduleHide); popover.addEventListener('focusin', cancelHide); popover.addEventListener('focusout', scheduleHide); popover.addEventListener('click', event => { if (event.target.closest('.popover-close')) return; if (event.target.closest('a')) return; const page = popover.dataset.page; if (page) window.location.href = page; }); if (closeButton) closeButton.addEventListener('click', close); // Belt-and-suspenders for the one tap that actually matters here: don't // rely solely on the browser's native anchor click-through to carry a // real touch tap on "Menu & More" / "Join Us" into a navigation. That's // the same native-click-after-touch machinery that turned out to be // unreliable above, and it's the exact last step an Android user needs // in order to reach a truck's page, so drive it explicitly from the tap // itself instead of trusting it to happen on its own. if (actionEl) { let actionTouchStart = null; actionEl.addEventListener('pointerdown', event => { actionTouchStart = event.pointerType === 'touch' ? {x: event.clientX, y: event.clientY} : null; }, {passive: true}); actionEl.addEventListener('pointerup', event => { if (event.pointerType !== 'touch' || !actionTouchStart) return; const dx = event.clientX - actionTouchStart.x, dy = event.clientY - actionTouchStart.y; actionTouchStart = null; if (Math.hypot(dx, dy) < 12 && actionEl.getAttribute('href')) { event.preventDefault(); const href = actionEl.getAttribute('href'); if (href.charAt(0) === '#') { document.querySelector(href)?.scrollIntoView({behavior: 'smooth'}); } else { window.location.href = href; } } }); } document.addEventListener('pointerdown', event => { if (!popover.contains(event.target) && !event.target.closest('.map-pin') && !event.target.closest('.truck-button')) close(); }); document.addEventListener('keydown', event => { if (event.key === 'Escape' && active) { event.preventDefault(); close(); } }); window.addEventListener('resize', () => { if (active) positionNear(active); }); window.addEventListener('scroll', () => { if (!popover.hidden && active) positionNear(active); }, true); if (window.ResizeObserver) new ResizeObserver(() => { if (active) positionNear(active); }).observe(document.body); } if (document.readyState === 'complete') init(); else window.addEventListener('load', init); })();