1/** 2 * 30A listing carousel tracking (2026-08-03). 3 * 4 * Pushes GA4 recommended-ecommerce events to the GTM dataLayer for the three 5 * pinned listing carousels. Site-side output is dataLayer ONLY -- the tags, 6 * triggers and variables that consume these pushes are configured in the GTM UI 7 * (container GTM-5KF8VS9B -> GA4 G-95KMBQLCNH). Nothing here creates a tag. 8 * 9 * Events 10 * view_item_list once per carousel per pageview, at 30% visibility 11 * select_item on any click inside a card 12 * 13 * Contract -- all values are server-rendered as data attributes: 14 * <section data-gtm-list-id data-gtm-list-name> 15 * <div class="rental-slide" 16 * data-gtm-item-id WP post ID (numeric, stable) 17 * data-gtm-item-name listing title 18 * data-gtm-item-brand "Listed by" company / agency (may be absent) 19 * data-gtm-item-category "Rental" | "Real Estate" 20 * data-gtm-index 1-based render position 21 * data-gtm-price digits only, Real Estate only (may be absent) 22 * 23 * An attribute that would have been empty is OMITTED server-side, so a missing 24 * dataset key means "no value", never "". 25 * 26 * Vanilla JS on purpose. The theme loads jQuery three times (hardcoded CDN tags 27 * in header.php and footer.php plus the WP-bundled copy) -- this file must not 28 * depend on any of them. It is also loaded site-wide from footer.php, so it has 29 * to be inert on the ~99% of pages that have no carousel. 30 * 31 * @package WordPress 32 * @subpackage 30A 33 */ 34( function () { 35 'use strict'; 36 37 window.dataLayer = window.dataLayer || []; 38 39 var SECTION_SELECTOR = '[data-gtm-list-id]'; 40 var CARD_SELECTOR = '.rental-slide'; 41 42 /* 43 * Clicks on carousel navigation must not read as listing clicks. 44 * .swiper-pagination matters most: the per-card photo slider renders its 45 * dots INSIDE .rental-slide, so closest() alone would not exclude them. 46 * The outer .realestate-button-* arrows sit outside the card and would bail 47 * on closest() anyway; they are listed for completeness. 48 */ 49 var NAV_SELECTOR = [ 50 '.swiper-button-next', 51 '.swiper-button-prev', 52 '.realestate-button-prev', 53 '.realestate-button-next', 54 '.swiper-pagination' 55 ].join( ',' ); 56 57 /** 58 * Build one GA4 item object from a card and its owning section. 59 * Keys whose source attribute is absent are omitted, never sent empty. 60 * 61 * @param {Element} card The .rental-slide element. 62 * @param {Element} section The carousel <section>. 63 * @return {Object} GA4 item. 64 */ 65 function buildItem( card, section ) { 66 var card_data = card.dataset; 67 var list_data = section.dataset; 68 var item = {}; 69 70 if ( card_data.gtmItemId ) { 71 item.item_id = card_data.gtmItemId; 72 } 73 if ( card_data.gtmItemName ) { 74 item.item_name = card_data.gtmItemName; 75 } 76 if ( card_data.gtmItemBrand ) { 77 item.item_brand = card_data.gtmItemBrand; 78 } 79 if ( card_data.gtmItemCategory ) { 80 item.item_category = card_data.gtmItemCategory; 81 } 82 if ( list_data.gtmListId ) { 83 item.item_list_id = list_data.gtmListId; 84 } 85 if ( list_data.gtmListName ) { 86 item.item_list_name = list_data.gtmListName; 87 } 88 // index and price are numbers in GA4, not strings. 89 if ( card_data.gtmIndex ) { 90 item.index = Number( card_data.gtmIndex ); 91 } 92 if ( card_data.gtmPrice ) { 93 item.price = Number( card_data.gtmPrice ); 94 } 95 96 return item; 97 } 98 99 /** 100 * Push a GA4 ecommerce event, clearing the previous ecommerce object first 101 * so values never bleed between pushes. 102 * 103 * @param {string} event_name GA4 event name. 104 * @param {Element} section The carousel <section>. 105 * @param {Array} items GA4 item objects. 106 */ 107 function pushEcommerce( event_name, section, items ) { 108 var list_data = section.dataset; 109 var ecommerce = {}; 110 111 if ( list_data.gtmListId ) { 112 ecommerce.item_list_id = list_data.gtmListId; 113 } 114 if ( list_data.gtmListName ) { 115 ecommerce.item_list_name = list_data.gtmListName; 116 } 117 ecommerce.items = items; 118 119 window.dataLayer.push( { ecommerce: null } ); 120 window.dataLayer.push( { 121 event: event_name, 122 ecommerce: ecommerce 123 } ); 124 } 125
126 function init() { 127 var sections = document.querySelectorAll( SECTION_SELECTOR ); 128 129 // No carousel on this page: bind nothing, observe nothing. 130 if ( ! sections.length ) { 131 return; 132 } 133 134 // --- select_item: one delegated listener on document --- 135 document.addEventListener( 'click', function ( event ) { 136 var target = event.target; 137 138 if ( ! target || 'function' !== typeof target.closest ) { 139 return; 140 } 141 if ( target.closest( NAV_SELECTOR ) ) { 142 return; 143 } 144 145 var card = target.closest( CARD_SELECTOR ); 146 if ( ! card ) { 147 return; 148 } 149 150 var section = card.closest( SECTION_SELECTOR ); 151 if ( ! section ) { 152 return; 153 } 154 155 pushEcommerce( 'select_item', section, [ buildItem( card, section ) ] ); 156 } ); 157 158 // --- view_item_list: once per carousel per pageview --- 159 // No fallback if IntersectionObserver is missing: a load-time fire would 160 // report impressions that never happened. 161 if ( 'function' !== typeof window.IntersectionObserver ) { 162 return; 163 } 164 165 var observer = new IntersectionObserver( function ( entries, self ) { 166 var i; 167 for ( i = 0; i < entries.length; i++ ) { 168 if ( ! entries[ i ].isIntersecting ) { 169 continue; 170 } 171 172 var section = entries[ i ].target; 173 self.unobserve( section ); 174 175 var cards = section.querySelectorAll( CARD_SELECTOR ); 176 if ( ! cards.length ) { 177 continue; 178 } 179 180 var items = []; 181 var j; 182 for ( j = 0; j < cards.length; j++ ) { 183 items.push( buildItem( cards[ j ], section ) ); 184 } 185 186 pushEcommerce( 'view_item_list', section, items ); 187 } 188 }, { threshold: 0.3 } ); 189 190 var k; 191 for ( k = 0; k < sections.length; k++ ) { 192 observer.observe( sections[ k ] ); 193 } 194 } 195 196 if ( 'loading' === document.readyState ) { 197 document.addEventListener( 'DOMContentLoaded', init ); 198 } else { 199 init(); 200 } 201}() );
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.