/* RedCommerce Popup — Trigger & show/hide logic Vanilla JS — no jQuery, no external dependencies */ (function () { 'use strict'; var config = window.rcPopupConfig || {}; var desktopTrigger = config.desktopTrigger || 'exit-intent'; var mobileTrigger = config.mobileTrigger || 'delay'; var desktopOverlay = config.desktopOverlay || 'blur'; var delay = (config.delay || 8) * 1000; var scrollDepth = config.scrollDepth || 50; var maxShows = config.maxShows || 1; var excludedPages = config.excludedPages || []; var gaMeasurementId = config.gaMeasurementId || ''; var STORAGE_KEY = 'rc_popup_shown'; // ── GA4 helper ─────────────────────────────────────────────────────────── function gaEvent( eventName, params ) { if ( typeof gtag === 'function' ) { gtag( 'event', eventName, params || {} ); } // GTM dataLayer alongside — harmless if not used. if ( window.dataLayer ) { window.dataLayer.push( Object.assign( { event: eventName }, params || {} ) ); } } // ── Stats tracking ──────────────────────────────────────────────────── function trackEvent( eventType ) { if ( ! config.ajaxUrl || ! config.trackNonce ) { return; } var xhr = new XMLHttpRequest(); xhr.open( 'POST', config.ajaxUrl, true ); xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ); var pageUrl = window.location.origin + window.location.pathname; xhr.send( 'action=rc_popup_track' + '&nonce=' + encodeURIComponent( config.trackNonce ) + '&event=' + encodeURIComponent( eventType ) + '&page=' + encodeURIComponent( pageUrl ) + ( window.rcPopupAttrParam ? window.rcPopupAttrParam() : '' ) ); } // ── Marketing attribution (UTM / Google Click ID) ─────────────────────────────────────── // First ad click wins for 90 days (Google's click window); a NEW gclid // restarts attribution. Marketing metadata only — never form values. // Storage key is shared with RC Premium on purpose (one record per site). var ATTR_KEY = 'rcp_attr'; var ATTR_TTL = 90 * 24 * 60 * 60 * 1000; var ATTR_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'gbraid', 'wbraid']; function readStoredAttr() { try { var raw = localStorage.getItem( ATTR_KEY ); if ( ! raw ) return null; var obj = JSON.parse( raw ); if ( ! obj || ! obj.at || ( Date.now() - obj.at ) > ATTR_TTL ) return null; return obj; } catch ( e ) { return null; } } function captureAttribution() { var stored = readStoredAttr(); var d = {}; var has = false; var sp = null; try { sp = new URLSearchParams( window.location.search || '' ); } catch ( e ) { /* very old browser */ } if ( sp ) { ATTR_PARAMS.forEach( function ( k ) { var v = sp.get( k ); if ( v ) { d[k] = String( v ).slice( 0, 200 ); has = true; } } ); } var extRef = ''; try { if ( document.referrer && document.referrer.indexOf( window.location.origin ) !== 0 ) { extRef = String( document.referrer ).slice( 0, 500 ); } } catch ( e ) { /* noop */ } var newClick = d.gclid && ( ! stored || d.gclid !== ( ( stored.d || {} ).gclid || '' ) ); if ( stored && ! newClick ) return stored; // first touch wins // Direct visit (no UTM, no external referrer): fall through and still record a // landing page, so every booking carries one. First-touch wins — an existing // record already returned above, so this only writes on the very first pageview. var rec = { v: 1, at: Date.now(), d: d, ref: extRef, lp: String( window.location.pathname || '/' ).slice( 0, 500 ) }; try { localStorage.setItem( ATTR_KEY, JSON.stringify( rec ) ); } catch ( e ) { /* private mode */ } return rec; } var attribution = captureAttribution(); // ── On-site journey (landing → … → booking page) ──────────────────────────── // Separate localStorage key from rcp_attr, so the first-touch / gclid logic above // stays untouched. Records the deduped page route (path) plus an uncapped total // page-view count (n). rcp_journey is a shared-namespace key with RC Premium — // do not repurpose it without coordinating. var JOURNEY_KEY = 'rcp_journey'; var JOURNEY_MAX = 20; // path entries kept (landing page always survives) var JOURNEY_ENTRY_MAX = 100; // chars per path entry (20 × 100 ≤ 2000 pages_path budget) var JOURNEY_PATH_MAX = 2000; // chars in the joined pages_path function recordJourney() { var here = String( window.location.pathname || '/' ).slice( 0, JOURNEY_ENTRY_MAX ); var j; try { var raw = localStorage.getItem( JOURNEY_KEY ); j = raw ? JSON.parse( raw ) : null; } catch ( e ) { return null; } // private mode etc — feature silently off if ( ! j || ! j.at || ( Date.now() - j.at ) > ATTR_TTL || ! ( j.path instanceof Array ) ) { j = { v: 1, at: Date.now(), n: 0, path: [] }; // fresh journey (or 90-day TTL expiry) } j.n = ( typeof j.n === 'number' ? j.n : 0 ) + 1; // total page views — never capped, never deduped if ( j.path[ j.path.length - 1 ] !== here ) { // dedupe consecutive identical pages j.path.push( here ); if ( j.path.length > JOURNEY_MAX ) { j.path.splice( 1, 1 ); // drop the 2nd entry; landing (index 0) survives } } try { localStorage.setItem( JOURNEY_KEY, JSON.stringify( j ) ); } catch ( e ) { /* private mode */ } return j; } var journey = recordJourney(); function journeyPathStr() { if ( ! journey || ! ( journey.path instanceof Array ) || ! journey.path.length ) return ''; var s = journey.path.join( '>' ); if ( s.length > JOURNEY_PATH_MAX ) { s = '…' + s.slice( s.length - ( JOURNEY_PATH_MAX - 1 ) ); // keep most recent pages, mark truncation } return s; } function attrJson() { if ( ! attribution ) return ''; var out = {}; var d = attribution.d || {}; for ( var i = 0; i < ATTR_PARAMS.length; i++ ) { if ( d[ ATTR_PARAMS[i] ] ) out[ ATTR_PARAMS[i] ] = d[ ATTR_PARAMS[i] ]; } if ( attribution.ref ) out.referrer = attribution.ref; if ( attribution.lp ) out.landing_page = attribution.lp; if ( attribution.at ) out.attr_at = new Date( attribution.at ).toISOString(); var pp = journeyPathStr(); if ( pp ) out.pages_path = pp; if ( journey && journey.n ) out.pages_count = journey.n; return JSON.stringify( out ); } function attrParam() { var j = attrJson(); return j ? '&attr=' + encodeURIComponent( j ) : ''; } // multistep.js (separate file scope) reads these. window.rcPopupAttrParam = attrParam; window.rcPopupAttrJson = attrJson; // Ride attribution along on CF7 submissions: a hidden rc_attr input on every // CF7 form. The Log module stores it server-side; CF7 itself ignores it. function injectAttrFields() { var j = attrJson(); if ( ! j ) return; document.querySelectorAll( 'form.wpcf7-form' ).forEach( function ( form ) { var input = form.querySelector( 'input[name="rc_attr"]' ); if ( ! input ) { input = document.createElement( 'input' ); input.type = 'hidden'; input.name = 'rc_attr'; form.appendChild( input ); } input.value = j; input.setAttribute( 'value', j ); } ); } injectAttrFields(); // CF7 fires wpcf7mailsent / wpcf7submit (no colon); re-inject so a second // submission in the same pageview still carries attribution. document.addEventListener( 'wpcf7mailsent', injectAttrFields ); document.addEventListener( 'wpcf7submit', injectAttrFields ); // ── Guards ────────────────────────────────────────────────────────────── function shouldSkip() { var shown = parseInt( sessionStorage.getItem(STORAGE_KEY) || '0', 10 ); if ( shown >= maxShows ) return true; // Normalize trailing slashes so /contact matches /contact/ and vice versa. var path = window.location.pathname.replace( /\/$/, '' ) || '/'; for ( var i = 0; i < excludedPages.length; i++ ) { var entry = ( excludedPages[i] || '' ).replace( /\/$/, '' ); if ( entry && path === entry ) return true; } return false; } // ── DOM refs ───────────────────────────────────────────────────────────── var overlay = document.getElementById('rc-popup-overlay'); var popup = document.getElementById('rc-popup'); var closeBtn = document.getElementById('rc-popup-close'); var waBtn = document.getElementById('rc-cta-whatsapp'); var callBtn = document.getElementById('rc-cta-call'); var fab = document.getElementById('rc-fab'); var isDesktop = window.innerWidth >= 768; var triggered = false; var loadTime = Date.now(); if ( !popup ) return; // ── Show / Hide ────────────────────────────────────────────────────────── function showPopup() { // Hide FAB while popup is open. if ( fab ) { fab.classList.remove('rc-fab--visible'); } if ( !triggered ) { var shown = parseInt( sessionStorage.getItem(STORAGE_KEY) || '0', 10 ); sessionStorage.setItem( STORAGE_KEY, String(shown + 1) ); } triggered = true; if ( isDesktop && overlay && desktopOverlay !== 'none' ) { overlay.style.display = 'block'; } popup.style.display = 'flex'; requestAnimationFrame(function () { popup.classList.add('rc-popup--visible'); }); gaEvent( 'rc_popup_shown', { ga_measurement_id: gaMeasurementId || undefined } ); trackEvent( 'popup_shown' ); if ( overlay ) overlay.addEventListener('click', closePopup); document.addEventListener('keydown', onKeyDown); } function closePopup() { popup.classList.remove('rc-popup--visible'); popup.classList.add('rc-popup--hiding'); gaEvent('rc_popup_dismissed'); setTimeout(function () { popup.style.display = 'none'; popup.classList.remove('rc-popup--hiding'); if ( overlay ) { overlay.style.display = 'none'; overlay.removeEventListener('click', closePopup); } document.removeEventListener('keydown', onKeyDown); // Show the floating WhatsApp FAB so the user can reopen. if ( fab ) { fab.classList.add('rc-fab--visible'); } }, 250); } function onKeyDown(e) { if ( e.key === 'Escape' ) closePopup(); } // ── Click tracking ─────────────────────────────────────────────────────── if ( waBtn ) { waBtn.addEventListener('click', function () { gaEvent('rc_popup_whatsapp_click'); trackEvent('whatsapp_click'); // Close popup after WhatsApp click — the link opens in a new tab. setTimeout( closePopup, 300 ); }); } if ( callBtn ) { callBtn.addEventListener('click', function () { gaEvent('rc_popup_call_click'); trackEvent('call_click'); }); } if ( closeBtn ) closeBtn.addEventListener('click', closePopup); // ── FAB (floating WhatsApp button) — reopens popup on click ───────────── if ( fab ) { fab.addEventListener('click', function () { fab.classList.remove('rc-fab--visible'); showPopup(); gaEvent('rc_fab_click'); trackEvent('fab_click'); }); } // ── Trigger functions ──────────────────────────────────────────────────── function setupDelayTrigger() { setTimeout(function () { if ( !shouldSkip() ) showPopup(); }, delay); } function setupExitIntentTrigger() { document.documentElement.addEventListener('mouseleave', function handler(e) { if ( e.clientY > 0 ) return; if ( Date.now() - loadTime < 3000 ) return; document.documentElement.removeEventListener('mouseleave', handler); if ( !shouldSkip() ) showPopup(); }); } function setupScrollTrigger() { var scrollFired = false; window.addEventListener('scroll', function handler() { if ( scrollFired ) return; var scrollTop = window.pageYOffset || document.documentElement.scrollTop; var docHeight = document.documentElement.scrollHeight; var winHeight = window.innerHeight; var pct = ((scrollTop + winHeight) / docHeight) * 100; if ( pct >= scrollDepth ) { scrollFired = true; window.removeEventListener('scroll', handler); if ( !shouldSkip() ) showPopup(); } }, { passive: true }); } // ── Boot ───────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', function () { if ( shouldSkip() ) return; isDesktop = window.innerWidth >= 768; if ( isDesktop ) { if ( desktopTrigger === 'exit-intent' ) { setupExitIntentTrigger(); } else if ( desktopTrigger === 'scroll' ) { setupScrollTrigger(); } else { setupDelayTrigger(); } } else { if ( mobileTrigger === 'scroll' ) { setupScrollTrigger(); } else { setupDelayTrigger(); } } }); // ── Urgency Timer ──────────────────────────────────────────────────────── function initUrgencyTimer() { var tc = config.timer; var el = document.getElementById('rc-urgency-timer'); if ( !tc || !tc.enabled || !el ) return; var endMs; var TIMER_KEY = 'rc_urgency_timer_end'; if ( tc.type === 'fixed' ) { endMs = tc.endTimestamp; } else { var stored = localStorage.getItem( TIMER_KEY ); if ( stored ) { endMs = parseInt( stored, 10 ); if ( endMs < Date.now() ) { endMs = Date.now() + tc.rollingMs; localStorage.setItem( TIMER_KEY, endMs ); } } else { endMs = Date.now() + tc.rollingMs; localStorage.setItem( TIMER_KEY, endMs ); } } var hEl = document.getElementById('rc-timer-h'); var mEl = document.getElementById('rc-timer-m'); var sEl = document.getElementById('rc-timer-s'); if ( !hEl || !mEl || !sEl ) return; function pad(n) { return n < 10 ? '0' + n : '' + n; } function tick() { var rem = endMs - Date.now(); if ( rem <= 0 ) { if ( tc.expiredText ) { el.querySelector('.rc-timer-display').innerHTML = '' + tc.expiredText + ''; } else { el.style.display = 'none'; } return; } hEl.textContent = pad( Math.floor( rem / 3600000 ) ); mEl.textContent = pad( Math.floor( (rem % 3600000) / 60000 ) ); sEl.textContent = pad( Math.floor( (rem % 60000) / 1000 ) ); setTimeout( tick, 1000 ); } tick(); } document.addEventListener('DOMContentLoaded', initUrgencyTimer); // ── Source tagging ────────────────────────────────────────────────────── // Overwrite the hidden CF7 field `rc_source` so the mail template // can distinguish popup submissions from regular page submissions. document.addEventListener('DOMContentLoaded', function () { var forms = popup ? popup.querySelectorAll('.wpcf7 form') : []; for ( var i = 0; i < forms.length; i++ ) { var field = forms[i].querySelector('input[name="rc_source"]'); if ( field ) { field.value = 'RedCommerce Popup'; } } }); }());