1/*jshint esversion: 6 */ 2 3 4jQuery(function($) { 5 const shiftDigital = JSON.parse($('[data-class=ShiftDigital]').text()); 6 7 console.log(JSON.stringify(window.shiftdigital, null, 2)); 8 9 if (!window.shiftdigital.pushEvent) { 10 window.sd = function () { 11 console.log('shiftDigital', JSON.stringify([arguments], null, 2)); 12 } 13 } 14 15 sd('create', shiftDigital.sd_oemcode, shiftDigital.sd_retailer_id, shiftDigital.sd_provider_id); 16 sd('dataLayer', shiftDigital.data_layer); 17 sd('send', 'pageview'); 18 19 function asShiftDigitalFormType(magnetisFormType) { 20 return shiftDigital.formTypes[magnetisFormType]; 21 } 22 23 function getForm(dom) { 24 let form = dom; 25 26 if (dom.name !== 'form') { 27 form = $(dom).parents('form')[0]; 28 } 29 30 return form; 31 } 32 33 function getFormField(form, fieldName) { 34 return $(form).find('[name=' + fieldName + ']').val(); 35 } 36 37 function linkPathname(href) { 38 try { 39 return new URL(href, window.location.href).pathname.replace(/\/+$/, '').toLowerCase(); 40 } catch (e) { 41 return null; 42 } 43 } 44 45 // Booking pages declared by the dealer through the ShiftDigital `pagetype` field. 46 const scheduleServicePaths = (shiftDigital.scheduleServiceUrls || []).map(linkPathname).filter(Boolean); 47 48 // Matching on the link text missed CTAs labelled "Make an appointment" and fired on 49 // every unrelated link mentioning "service", so match the destination instead. 50 $(document).on('click', 'a[href]', function() { 51 const path = linkPathname($(this).attr('href')); 52 53 if (!path || scheduleServicePaths.indexOf(path) === -1) { 54 return; 55 } 56 57 sd('dataLayer', { 58 events: 'scheduleServiceClick' 59 }); 60 61 sd('send'); 62 }); 63 64 65 $(document).on('magnetis.form_visible', 'form', function(event) { 66 const formType = $(this).find('[name=magnetisFormType]').val() 67 68 sd('dataLayer', { 69 events: 'formShown', 70 formType: asShiftDigitalFormType(formType), 71 formVehicle: JSON.parse($('[data-class=ShiftDigitalVehicle]').text()) 72 }); 73 74 sd('send'); 75 }); 76 77 $(document).ready(function() { 78 $('form:visible').each(function(frm) { 79 const formType = $(this).find('[name=magnetisFormType]').val() 80 if (!formType) { 81 return; 82 } 83 84 85 sd('dataLayer', { 86 events: 'formShown', 87 formType: asShiftDigitalFormType(formType), 88 formVehicle: JSON.parse($('[data-class=ShiftDigitalVehicle]').text()) 89 }); 90 91 sd('send'); 92 }); 93 }); 94 95 96 97 98 // Every surface that renders an offer: dealer promotions (listing and slider), 99 // manufacturer offers, and the Mazda services template. 100 const PROMOTION_CONTAINERS = '.promotion_content, .vehicle_promo, .promotions_slider_slide, .promotion'; 101 102 function readPromotion(link) { 103 // In-page anchors inside an offer block are affordances (legal text toggle), not CTAs. 104 if ((link.getAttribute('href') || '').charAt(0) === '#') { 105 return null; 106 } 107 108 const container = link.closest(PROMOTION_CONTAINERS); 109 const payload = container && container.querySelector('script[data-type=Promotion]'); 110 111 if (!payload) { 112 return null; 113 } 114 115 try { 116 return JSON.parse(payload.textContent); 117 } catch (e) { 118 return null; 119 } 120 } 121 122 function isServiceOffer(promotion) { 123 return [promotion.shiftdigital_type, promotion.shiftdigital_category] 124 .some(function(value) { return String(value || '').toLowerCase() === 'service'; }); 125 } 126 127 // Manufacturer offers carry the vehicle they apply to; dealer promotions don't. 128 function offerVehicleProps(promotion) { 129 const props = { 130 vehicleModel: promotion.model, 131 vehicleTrim: promotion.trim, 132 vehicleYear: promotion.year, 133 vehicleBodyStyle: promotion.body_type, 134 vehicleEngine: promotion.engine, 135 vehicleMsrp: promotion.msrp 136 }; 137 138 Object.keys(props).forEach(function(key) { 139 if (props[key] === undefined || props[key] === null || props[key] === '') { 140 delete props[key]; 141 } 142 }); 143 144 return props; 145 } 146 147 document.addEventListener('click', function(event) { 148 const link = event.target.closest('a[href]'); 149 const promotion = link && readPromotion(link); 150 151 if (!promotion) { 152 return; 153 } 154 155 const offer = { 156 offerId: promotion.offer_id || null, 157 offerName: promotion.title, 158 offerCategory: promotion.shiftdigital_category, 159 offerType: promotion.shiftdigital_type, 160 offerExpire: promotion.end_date 161 }; 162 163 if (isServiceOffer(promotion)) { 164 offer.offerStatus = 'active'; 165 sd('dataLayer', { events: 'serviceOfferClick', serviceOfferClick: offer }); 166 } else { 167 sd('dataLayer', { events: 'offerClick', offerClick: Object.assign(offer, offerVehicleProps(promotion)) }); 168 } 169 170 sd('send'); 171 }); 172 173 function getInputsValue(inputs) {
174 return inputs.reduce((acc, input) => { 175 if (input.name.includes('[]')) { 176 const name = input.name.replace('[]', ''); 177 if (!acc[name]) { 178 acc[name] = []; 179 } 180 acc[name].push(input.value); 181 } else { 182 acc[input.name] = input.value; 183 } 184 return acc; 185 }, {}); 186 } 187 188 // WPCF7 events 189 const phoneMap = { 190 'courriel': 'email', 191 'message texte': 'text', 192 'appel téléphonique': 'phone', 193 'email': 'email', 194 'text message': 'text', 195 'phone call': 'phone', 196 }; 197 198 document.addEventListener( 'wpcf7mailsent', function( event ) { 199 200 const fields = getInputsValue(event.detail.inputs); 201 202 const formType = fields.formType ?? fields.magnetisFormType ?? 'other'; 203 204 const eventFields = { 205 events: 'formSubmission', 206 leadId: fields.form_render_id ?? null, 207 leadType: 'lead', 208 formOptIn: 'in-implicit', 209 prefContact: phoneMap[String(fields['methode-contact'] ?? null).toLowerCase()] ?? 'phone', 210 formType: asShiftDigitalFormType(formType), 211 formVehicle: JSON.parse($('[data-class=ShiftDigitalVehicle]').text()), 212 }; 213 214 if (formType.startsWith('trade_in')) { 215 eventFields.tradeIn = { 216 year: fields['trade_vehicle_year'] ?? '', 217 make: fields['trade_vehicle_make'] ?? '', 218 model: fields['trade_vehicle_model'] ?? '', 219 } 220 } 221 222 sd('dataLayer', eventFields); 223 224 sd('send'); 225 226 }, false ); 227 228 ['wpcf7mailfailed', 'wpcf7invalid', 'wpcf7spam'].forEach((eventName) => { 229 document.addEventListener(eventName, function (event) { 230 const fields = getInputsValue(event.detail.inputs); 231 console.log(event.detail.status); 232 233 const formType = fields.formType ?? fields.magnetisFormType ?? 'other'; 234 const displayType = $(event.target).closest('.modal').length ? 'modal' : 'in-page'; 235 236 const eventFields = { 237 events: 'formSubmissionError', 238 displayType: displayType, 239 formType: asShiftDigitalFormType(formType), 240 formError: (event.detail.status + ' - ') + (event.detail.apiResponse && event.detail.apiResponse.message ? event.detail.apiResponse.message : null), 241 formVehicle: JSON.parse($('[data-class=ShiftDigitalVehicle]').text()), 242 }; 243 244 sd('dataLayer', eventFields); 245 sd('send'); 246 247 }, false); 248 }); 249 250 251 252 // Click to call events 253 $(document).on('click', 'a.shiftdigital_clicktocall', function(event){ 254 var callednum = $(this).attr('href').replace(/\D+/g, ''); 255 var departement = $(this).data('shiftdigital-type'); 256 257 sd('dataLayer', { 258 events: 'clickToCall', 259 clickToCallDepartment: departement, 260 }); 261 sd('send'); 262 }); 263 264 $('a.shiftdigital_getdirection').on('click', function(event){ 265 sd('dataLayer', { 266 events: 'getDirections' 267 }); 268 sd('send'); 269 }); 270 271 $(document).on('open', '#incentive_calculator', () => { 272 // Disabled as requested https://www.wrike.com/open.htm?id=1524356197 273 // sd('dataLayer', { 274 // events: 'formShown', 275 // formType: asShiftDigitalFormType('payment_estimator'), 276 // formVehicle: JSON.parse($('[data-class=ShiftDigitalVehicle]').text()) 277 // }); 278 // 279 // sd('send'); 280 }); 281 282 // Carousel Click Events 283 document.addEventListener('click', function(event) { 284 const target = event.target.closest('.carousel-item a[href], a[href].carousel-item, .item a[href]'); 285 if (!target) return; 286 287 const node = (target.classList.contains('carousel-item') || target.classList.contains('item')) ? target : target.parentElement; 288 const siblings = Array.from(node.parentElement.children); 289 290 sd('dataLayer', { 291 events: 'carouselClick', 292 carouselClick: { 293 assetPosition: siblings.indexOf(node) + 1, 294 assetName: target.getAttribute('href'), 295 } 296 }); 297 298 sd('send'); 299 }); 300 301 $(document).on('focus click keyup', '.wpcf7-form input, .wpcf7-form textarea, .wpcf7-form select', triggerFormInitiation); 302 303 // Form field interaction 304 document.addEventListener('change', function(event) { 305 const field = event.target; 306 if (!field.closest('.wpcf7-form')) return; 307 if (!['INPUT', 'TEXTAREA', 'SELECT'].includes(field.tagName)) return; 308 309 const form = field.closest('form'); 310 const formType = getFormField(form, 'formType') || getFormField(form, 'magnetisFormType') || 'other'; 311 const displayType = field.closest('.modal') ? 'modal' : 'in-page'; 312 313 sd('dataLayer', { 314 events: 'formFieldInteraction', 315 formType: asShiftDigitalFormType(formType), 316 displayType: displayType, 317 formFieldName: field.name, 318 formVehicle: JSON.parse($('[data-class=ShiftDigitalVehicle]').text()), 319 }); 320 321 sd('send'); 322 }); 323 324 function triggerFormInitiation(event) 325 { 326 const form = getForm(event.target); 327 328 if ($(form).data('initialTriggered')) { 329 return; 330 } 331 332 $(form).data('initialTriggered', true); 333 334 // Find the form type field 335 const formType = getFormField(form, 'formType') || getFormField(form, 'magnetisFormType') || 'other'; 336 337 sd('dataLayer',{ 338 events: 'formInitiation', 339 formType: asShiftDigitalFormType(formType), 340 }); 341 342 sd('send'); 343 } 344 345 $(document).on('vehicle-listing.type-search', (e, props) => { 346 sd('dataLayer', { 347 events: 'typedSearch', 348 typedSearchContent: props.search 349 }); 350 sd('send'); 351 }); 352 353 /** 354 * Shift digital search metric. 355 * 356 * @param {object} eventData - Detail from the 'resultsUpdated' CustomEvent. 357 * activeFilters and totalResults are read directly from the event. 358 */ 359 function sd_search(eventData) 360 { 361 if (typeof( sd ) != 'function') return; 362 if (!eventData || eventData.activeFilters == null) return; 363 364 var filters = eventData.activeFilters; 365 var vehiclesCount = eventData.totalResults; 366 367 function getFilterMulti(filterKey) { 368 var v = filters[filterKey]; 369 return Array.isArray(v) ? v : (v ? [String(v)] : []); 370 } 371 372 function getFilterSingle(filterKey) { 373 if (filters[filterKey] !== undefined) { 374 return filters[filterKey]; 375 } 376 // Range filters use nested {min, max} objects (e.g. filters.price.min). 377 if (filterKey.endsWith('_min')) { 378 var rangeObj = filters[filterKey.slice(0, -4)]; 379 if (rangeObj && typeof rangeObj === 'object') return rangeObj.min !== undefined ? rangeObj.min : null; 380 } 381 if (filterKey.endsWith('_max')) { 382 var rangeObj = filters[filterKey.slice(0, -4)]; 383 if (rangeObj && typeof rangeObj === 'object') return rangeObj.max !== undefined ? rangeObj.max : null; 384 } 385 return null; 386 } 387 388 var year = ''; 389 var year_min = getFilterSingle('year_min'); 390 var year_max = getFilterSingle('year_max'); 391 392 // We have at least one year filtered 393 if ( year_min || year_max ) { 394 var curr_year = new Date().getFullYear(); 395 396 if ( year_min ) { 397 year_min = parseInt( year_min ); 398 } 399 400 if ( year_max ) { 401 year_max = parseInt( year_max ); 402 403 if ( ! year_min ) { 404 // Limit to 10 years older? 405 year_min = year_max - 10; 406 } 407 } 408 else { 409 // Up to next year I guess 410 year_max = curr_year + 1; 411 } 412 413 // Build list of years with pipe delimiters 414 // Range with min/max 415 if( year_min && year_max ) { 416 var i; 417 for ( i = year_min; i <= year_max; i++ ) { 418 year += i + '|'; 419 } 420 421 // Remove last pipe
422 year = year.substring(0, year.length - 1); 423 } 424 } 425 else { 426 year = 'All'; 427 } 428 429 var make = getFilterMulti('make'); 430 var model = getFilterMulti('model'); 431 var exterior_color = getFilterMulti('exterior_color_filter'); 432 var bodyStyle = getFilterMulti('body'); 433 var drive = getFilterMulti('drive'); 434 var transmission = getFilterMulti('transmission'); 435 436 var features = [].concat(drive, transmission); 437 438 function isFilterOn(filterKey) { 439 var value = getFilterSingle(filterKey); 440 return value === true || value === 1 || value === '1' || value === 'true'; 441 } 442 443 // Demos are reported as new: unregistered vehicles, sold as new. 444 function distinctStatuses(values) { 445 return [...new Set(values.filter(Boolean).map((value) => (String(value) === 'demo' ? 'new' : String(value))))]; 446 } 447 448 /** 449 * SD only accepts New, Used, CPO or Mixed (guide, appendix 3) â 'All' is rejected. 450 * What the page is filtered on wins over what it rendered: a listing legitimately 451 * comes back empty, and a hidden filter (is_demo, is_certified) still states intent. 452 */ 453 function searchStatus() { 454 if (isFilterOn('is_certified')) { 455 return 'CPO'; 456 } 457 458 var filtered = distinctStatuses(getFilterMulti('status')); 459 460 if (filtered.length > 1) { 461 return 'Mixed'; 462 } 463 464 if (filtered.length === 1) { 465 return stringUcFirst(filtered[0]); 466 } 467 468 if (isFilterOn('is_demo')) { 469 return 'New'; 470 } 471 472 var rendered = distinctStatuses(getSrpVehicles().map((vehicle) => vehicle.status)); 473 474 return rendered.length === 1 ? stringUcFirst(rendered[0]) : 'Mixed'; 475 } 476 477 sd('dataLayer', { 478 events: 'filterSearch', 479 countSearchResults: vehiclesCount, 480 filterSearch: { 481 status: searchStatus(), 482 year: year, 483 make: make.join('|') || 'All', 484 model: model.join('|') || 'All', 485 trim: 'All', 486 color : exterior_color.join('|') || 'All', 487 driveTrain: drive, 488 minPrice : getFilterSingle('price_min') || 'All', 489 maxPrice: getFilterSingle('price_max') || 'All', 490 bodyStyle : bodyStyle.join('|') || 'All', 491 features : features.join('|') || 'All' 492 } 493 }); 494 495 sd('send'); 496 } 497 498 window.addEventListener('resultsUpdated', function(e) { 499 sd('send', 'pageview'); 500 sd_search(e.detail); 501 }); 502 503 document.addEventListener('vehicle-listing.type-search', function(e) { 504 sd('dataLayer', { events: 'typedSearch', typedSearchContent: e.detail && e.detail.search }); 505 sd('send'); 506 }); 507 document.addEventListener('magnetis.form_visible', function(e) { 508 var formType = e.target.querySelector('[name=magnetisFormType]') && e.target.querySelector('[name=magnetisFormType]').value; 509 if (!formType) return; 510 // Prefer vehicle data passed via event detail (e.g. from SRP popup CTA). 511 // Fall back to the global ShiftDigitalVehicle script (set on VDP pages). 512 var formVehicle = (e.detail && e.detail.vehicle) || JSON.parse($('[data-class=ShiftDigitalVehicle]').text()); 513 sd('dataLayer', { 514 events: 'formShown', 515 formType: asShiftDigitalFormType(formType), 516 formVehicle: formVehicle 517 }); 518 sd('send'); 519 }); 520 521 522 /* 523 * Build the search payload from the SRP data the server renders on first load. 524 */ 525 function getInitialSearchData() { 526 const filterEl = document.querySelector('script[data-type=Filter]'); 527 if (!filterEl) { 528 return null; 529 } 530 531 try { 532 const activeFilters = JSON.parse(filterEl.textContent); 533 const metaEl = document.getElementById('search_result_meta'); 534 const totalResults = metaEl ? JSON.parse(metaEl.textContent).vehicles_count : null; 535 536 return { activeFilters: activeFilters, totalResults: totalResults }; 537 } catch (e) { 538 return null; 539 } 540 } 541 542 sd_search(getInitialSearchData()); // Run on page load 543 544}); 545
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.