1// This file will be run in the footer on every page. 2// It should be used for content that relates 3// to your site as a whole, it should not be used for 4// plugin-specific js 5 6require(["site_gtm", "plugins_common_custom_ui_watcher", "sv_load!plugins_dynamic_content", "sv_site", "domReady!"], function(gtm, UIWatcher, dynamic, site) { 7 gtm.update(); 8 9 // SEM tracking 10 var uiEvents = new UIWatcher({}, null, document); 11 uiEvents.on("change-page", handlePageChange); 12 uiEvents.on("view-change", handleFilterChange); 13 uiEvents.on("sort-change", handleFilterChange); 14 uiEvents.on("value-change", handleFilterChange); 15 uiEvents.on("toggle-show-more", handleShowMoreToggle); 16 uiEvents.on("scroll-into-view", handleScrollIntoView); 17 uiEvents.on("item-click", handleItemClick); 18 19 20 function handleScrollIntoView(payload){ 21 var event = payload.eventContext, 22 widget = payload.widgetContext; 23 } 24 25 function handleShowMoreToggle(payload){ 26 var event = payload.eventContext; 27 let ga4Data = { 28 eventName: "filter", 29 filterType: `${event?.refTitle} | ${event?.displayText}`, 30 itemType: event?.itemType, 31 interactionSource: event?.interactionSource, 32 interactionText: event?.interactionText, 33 } 34 if (ga4Data.eventName && ga4Data.filterType && ga4Data.itemType) 35 gtm.sendEvents({ tClient_ga4: ga4Data }); 36 } 37 38 function handlePageChange(payload){ 39 var event = payload.eventContext; 40 let ga4Data = { 41 eventName: "filter", 42 filterType: `${event?.refTitle} | ${event?.displayText}`, 43 itemType: event?.itemType, 44 interactionSource: event?.interactionSource, 45 interactionText: event?.interactionText, 46 } 47 if (ga4Data.eventName && ga4Data.filterType && ga4Data.itemType) 48 gtm.sendEvents({ tClient_ga4: ga4Data }); 49 } 50 51 function handleItemClick(payload){ 52 var event = payload.eventContext; 53 if (event?.name === "Quickview") { 54 var ga4Data = { 55 eventName: "filter", 56 filterType: "Open Quickview", 57 itemType: event?.itemType, 58 itemName: event?.itemName ?? event?.refTitle, 59 interactionSource: event?.interactionSource, 60 interactionText: event?.interactionText, 61 } 62 if (ga4Data.eventName && ga4Data.filterType && ga4Data.itemType) 63 gtm.sendEvents({tClient_ga4: ga4Data}); 64 } 65 else { 66 var ga4Data = { 67 eventName: event?.eventName, 68 itemType: event?.itemType, 69 itemName: event?.itemName ?? event?.refTitle, 70 linkObject: event?.linkObject, 71 itemCategory: event?.crmc, 72 interactionSource: event?.interactionSource, 73 interactionText: event?.interactionText, 74 interactionUrl: event?.interactionUrl, 75 itemCity: event?.itemCity, 76 itemRegion: event?.itemRegion, 77 itemId: event?.itemId, 78 acctId: event?.acctId, 79 } 80 if (event?.crmsc) 81 ga4Data.itemSubcategory = event?.crmsc; 82 if (event?.eventName === 'partner_referral') { 83 ga4Data.value = 5; 84 ga4Data.referralType = event?.referralType; 85 } 86 if (ga4Data.eventName && ga4Data.itemType && ga4Data.itemName && ga4Data.linkObject) 87 gtm.sendEvents({tClient_ga4: ga4Data}); 88 } 89 } 90 91 function handleMapPinClick(payload){ 92 var event = payload.eventContext, 93 widget = payload.widgetContext; 94 } 95 96 function handleFilterChange (payload) { 97 var event = payload.eventContext; 98 let ga4Data = { 99 eventName: "filter", 100 filterType: `${event?.refTitle} | ${event?.displayText}`, 101 itemType: event?.itemType, 102 interactionSource: event?.interactionSource, 103 interactionText: event?.interactionText, 104 } 105 if (ga4Data.eventName && ga4Data.filterType && ga4Data.itemType) 106 gtm.sendEvents({ tClient_ga4: ga4Data }); 107 } 108 // end SEM tracking 109}); 110 111// Lazy-load js for widgets that have the [data-js] attribute on their root node 112(function() { 113 function initDataJs(searchScope, selector = '[data-js]' ){ 114 require(['plugins_common_custom_lazyload'], function(lazyload) { 115 var widgetsToLoad = []; 116 let scope = searchScope ? searchScope : document; 117 var els = scope.querySelectorAll(selector); 118 119 for (var i = 0; i < els.length; i++) { 120 widgetsToLoad.push(els[i]); 121 } 122 123 lazyload.lazy(widgetsToLoad, function(entry, io) { 124 var target = entry.target; 125 var jsFile = target.dataset.js; 126 var guid = target.dataset.guid; 127 128 require([jsFile], function(widget) { 129 widget.init(guid); 130 io.unobserve(target); 131 }); 132 }); 133 }); 134 }; 135 136 initDataJs(); 137 138 let eventToSelectorMap = { 139 // each event needs a slightly different selector 140 "dynamicContentLoaded": "[data-dynamic-chosen] [data-js]", 141 "abContentLoaded": "[data-ab-chosen] [data-js]" 142 } 143
144 Object.entries(eventToSelectorMap).forEach( event => { 145 const [eventName, selector] = event; 146 window.addEventListener(eventName, e => { 147 const collections = document.querySelector(`[data-guid="${e}"]`); 148 initDataJs(collections, selector); 149 }); 150 }); 151})(); 152 153// Wrap core_textbox iframes to prevent horizontal overflow 154(function() { 155 const textBoxes = document.querySelectorAll('[data-sv-core-textbox]'); 156 if (textBoxes.length) { 157 require(['plugins_common_custom_utils'], function(utils) { 158 textBoxes.forEach(utils.wrapIframes); 159 }); 160 } 161})(); 162 163// Remove critical styles from head after all CSS files have loaded 164// This prevents a situation where the critical styles cause an override of the CSS in files 165const criticalStyles = document.querySelectorAll('[data-critical-styles]'); 166if (criticalStyles) { 167 window.addEventListener('load', e => { 168 criticalStyles.forEach(element => { 169 element.remove() 170 }); 171 }); 172} 173 174require(["plugins_core", "sv_site", "domReady!"], function (core, site) { 175 (async function () { 176 if (!(site?.siteConfig?.plugins?.events?.active)) { return; } 177 178 // Gets all recId elements on page that are not in Detail or Events Layout List widget, as those are handled separately. 179 const recIdElements = document.querySelectorAll('[data-recid]:not(.layoutjs [data-recid]):not(.shared-detail)'); 180 const recIdsArray = Array.from(recIdElements).map(element => element.getAttribute('data-recid')); 181 if (recIdsArray.length === 0) { return; } 182 183 async function fetchItems(matchCondition) { 184 const aggregateQuery = [ 185 { 186 $match: matchCondition 187 }, 188 { 189 $sort: { 190 _updated: -1 191 } 192 }, 193 { 194 $group: { 195 _id: "$event", 196 mostRecentDocument: { $first: "$$ROOT" } 197 } 198 }, 199 { 200 $replaceRoot: { 201 newRoot: "$mostRecentDocument" 202 } 203 } 204 ]; 205 206 const params = new URLSearchParams({ 'token': core.simpleToken, 'json': JSON.stringify({ pipeline: aggregateQuery }) }); 207 const resource = "plugins_collections_type_custom_premium_link_override"; 208 const aggregateEndpoint = `/includes/rest_v2/${resource}/aggregate/?` + params; 209 210 try { 211 const response = await fetch(aggregateEndpoint); 212 213 if (!response.ok) { 214 console.warn(`Error: Failed to fetch data from the endpoint. Status: ${response.status}`); 215 return; 216 } 217 } catch (error) { 218 console.error("An error occurred during the fetch request:", error); 219 } 220 221 const aggregateResponse = await fetch(aggregateEndpoint);
222 const aggregateResultData = await aggregateResponse.json(); 223 224 if (aggregateResultData.docs.length > 0) { 225 const findQuery = { 226 filter: { 227 _id: { 228 $in: aggregateResultData.docs.map(doc => ({ $oid: doc._id }) ) 229 }, 230 }, 231 options: { 232 castDocs: false, 233 }, 234 }; 235 236 const findParams = new URLSearchParams({ 'token': core.simpleToken, 'json': JSON.stringify(findQuery) }); 237 const findEndpoint = `/includes/rest_v2/${resource}/find/?` + findParams; 238 const findResponse = await fetch(findEndpoint); 239 const findResultData = await findResponse.json(); 240 241 return findResultData.docs || []; 242 } else { 243 return []; 244 } 245 } 246 247 const matchCondition = { event: { $in: recIdsArray } }; 248 const premiumLinks = await fetchItems(matchCondition); 249 if (premiumLinks.length > 0) { 250 const eventMap = new Map(); 251 premiumLinks.forEach(premiumLink => { 252 eventMap.set(premiumLink.event, premiumLink.button_link.url); 253 }) 254 recIdElements.forEach(element => { 255 // Check if the element itself or any of its descendants have a data-premium-link attribute 256 const link = element.querySelector('[data-premium-link]') || (element.hasAttribute('data-premium-link') ? element : null); 257 if (link === null) return; 258 const recId = element.getAttribute('data-recid'); 259 if (eventMap.has(recId)) { 260 let linkToUse = eventMap.get(recId); 261 link.setAttribute('href', linkToUse); 262 let gtmVars = link.getAttribute('data-gtm-vars'); 263 let gtmVarsObj = JSON.parse(gtmVars); 264 gtmVarsObj.tClient_ga4.interactionUrl = encodeURIComponent(linkToUse); 265 let updatedGtmVars = JSON.stringify(gtmVarsObj); 266 link.setAttribute('data-gtm-vars', updatedGtmVars); 267 } 268 else if (link.getAttribute('href').includes("javascript:void(0)")) { 269 link.remove(); 270 } 271 }); 272 } 273 })(); 274});
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.