PageSourceSearch

https://www.brewerairporttoyota.com/app/themes/dv-framework/media/js/dv-events.js?v=1.789.527

js brewerairporttoyota.com collected 2026-09-26 11:03:45 UTC 17,896 bytes, 555 lines download raw bytes

1/**
2 * Detecting on click events
3 * @param {Event} event - The native click event being wrapped.
4 * @param {Element} target - Replacement analytics target for the proxy event.
5 */
6function createTargetProxy(event, target) {
7	return new Proxy(event, {
8		get(obj, prop) {
9			return prop === 'target' ? target : obj[prop];
10		}
11	});
12}
13
14/**
15 * Normalize click targets so nested span clicks and tel links resolve to the
16 * anchor element used by the analytics handlers.
17 * @param {Event} event - Native click event.
18 * @returns {{target: Element|null, normalizedEvent: Event, telLink: HTMLAnchorElement|null}}
19 */
20function normalizeClickTarget(event) {
21	let target = event.target;
22	let normalizedEvent = event;
23
24	if (target.tagName === 'SPAN') {
25		const anchorTag = target.closest('a');
26		if (anchorTag) {
27			target = anchorTag;
28			normalizedEvent = createTargetProxy(event, anchorTag);
29		}
30	}
31
32	const telLink = target.closest && target.closest('a[href^="tel:"]');
33	if (telLink) {
34		target = telLink;
35		normalizedEvent = createTargetProxy(event, telLink);
36	}
37
38	return {
39		target,
40		normalizedEvent,
41		telLink
42	};
43}
44
45/**
46 * Safely return the analytics target from an event-like object.
47 * @param {Object} node - Native event or proxy event.
48 * @returns {Element|null}
49 */
50function getAnalyticsTarget(node) {
51	if (!node || !node.target || typeof node.target.closest !== 'function') {
52		return null;
53	}
54
55	return node.target;
56}
57
58/**
59 * Read the analytics set-prop attribute from the current target, regardless of
60 * which legacy attribute name was used.
61 * @param {Element|null} target - Clicked analytics target.
62 * @returns {string}
63 */
64function getEventSetProp(target) {
65	if (!target) return '';
66
67	return target.getAttribute('data-ag-event-set-prop')
68		|| target.getAttribute('data-aga-event-set-prop')
69		|| target.getAttribute('data-sd-event-set-prop')
70		|| target.getAttribute('data-sda-event-set-prop')
71			|| '';
72}
73
74/**
75 * Resolve the click-to-call department from explicit analytics attributes
76 * first, then fall back to link text/title heuristics.
77 * @param {Element|null} target - Clicked analytics target.
78 * @param {string} agEvent - Existing analytics event value.
79 * @returns {string}
80 */
81function getClickToCallDepartment(target, agEvent) {
82	if (agEvent && agEvent.includes('clickToCall')) {
83		const eventParts = agEvent.split('|');
84		if (eventParts[1]) {
85			return eventParts[1];
86		}
87	}
88
89	const setProp = getEventSetProp(target);
90	if (setProp) {
91		const propParts = setProp.split('|');
92		if (propParts[1]) {
93			return propParts[1];
94		}
95	}
96
97	const linkText = ((target && (target.innerText || target.textContent)) || '').toLowerCase();
98	const linkTitle = ((target && target.getAttribute('title')) || '').toLowerCase();
99	const departmentText = linkText + ' ' + linkTitle;
100
101	if (departmentText.includes('service')) {
102		return 'Service';
103	}
104
105	if (departmentText.includes('parts')) {
106		return 'Parts';
107	}
108
109	return 'Sales';
110}
111
112/**
113 * Determine which Shift Digital event should be emitted for the click based on
114 * explicit analytics metadata and global fallback link patterns.
115 * @param {string} agEvent - Existing analytics event value.
116 * @param {Element|null} target - Clicked analytics target.
117 * @param {HTMLAnchorElement|null} telLink - Resolved tel anchor, if any.
118 * @param {HTMLAnchorElement|null} googleMapsLink - Resolved Google Maps anchor, if any.
119 * @param {Element|null} scheduleServiceTrigger - Resolved schedule-service trigger, if any.
120 * @returns {string}
121 */
122function resolveSDEvent(agEvent, target, telLink, googleMapsLink, scheduleServiceTrigger) {
123	if (telLink && (!agEvent || !agEvent.includes('clickToCall'))) {
124		return 'clickToCall|' + getClickToCallDepartment(target, agEvent);
125	}
126
127	if (typeof sd !== 'undefined' && googleMapsLink && (!agEvent || !agEvent.includes('getDirections'))) {
128		return 'getDirections';
129	}
130
131	if (typeof sd !== 'undefined' && scheduleServiceTrigger && (!agEvent || !agEvent.includes('scheduleServiceClick'))) {
132		return 'scheduleServiceClick';
133	}
134
135	return agEvent;
136}
137
138/**
139 * Determine whether an anchor points to a known Google Maps host used for
140 * directions links across the platform.
141 * @param {Element|null} target - Clicked analytics target.
142 * @returns {HTMLAnchorElement|null}
143 */
144function getGoogleMapsLink(target) {
145	if (!target || typeof target.closest !== 'function') {
146		return null;
147	}
148
149	const anchor = target.closest('a[href]');
150	if (!anchor || !anchor.href) {
151		return null;
152	}
153
154	try {
155		const url = new URL(anchor.href, window.location.origin);
156		const hostname = url.hostname.toLowerCase();
157		const pathname = url.pathname.toLowerCase();
158
159		if (hostname === 'maps.app.goo.gl' || hostname === 'maps.google.com') {
160			return anchor;
161		}
162
163		if ((hostname === 'www.google.com' || hostname === 'google.com') && pathname.startsWith('/maps')) {
164			return anchor;
165		}
166	} catch (error) {
167		return null;
168	}
169
170	return null;
171}
172
173/**
174 * Determine whether the clicked element is a schedule-service trigger, either
175 * through a direct service URL or the Toyota modal trigger class.
176 * @param {Element|null} target - Clicked analytics target.
177 * @returns {Element|null}
178 */
179function getScheduleServiceTrigger(target) {
180	if (!target || typeof target.closest !== 'function') {
181		return null;
182	}
183
184	return target.closest('.catched-schedule-service-btn, a[href*="service/schedule-service"]');
185}
186
187/**
188 * Send a simple Shift Digital event with only the event name in the dataLayer.
189 * @param {string} eventName - Shift Digital event name.
190 * @returns {void}
191 */
192function sendSimpleSDEvent(eventName) {
193	sd('dataLayer', {
194		events: eventName
195	});
196	sd('send');
197}
198
199/**
200 * Track formFieldInteraction once per field element so a single field does not
201 * emit on every keystroke, while still allowing other fields in the same form
202 * to emit their own first interaction.
203 * @param {Element|null} fieldTarget - Interacted form field element.
204 * @returns {boolean}
205 */
206function shouldTrackShiftDigitalFieldInteraction(fieldTarget) {
207	if (!fieldTarget || !fieldTarget.dataset) {
208		return false;
209	}
210
211	if (fieldTarget.dataset.sdFieldInteractionTracked === 'true') {
212		return false;
213	}
214
215	fieldTarget.dataset.sdFieldInteractionTracked = 'true';
216	return true;
217}
218
219/**
220 * Normalize raw form field identifiers into Shift Digital's canonical
221 * formFieldName values so Gravity Forms field ids do not leak into payloads.
222 * @param {Element|null} fieldTarget - Interacted field element.
223 * @returns {string}
224 */
225function getShiftDigitalFormFieldName(fieldTarget) {
226	if (!fieldTarget || typeof fieldTarget.getAttribute !== 'function') {
227		return 'other';
228	}
229
230	const gfield = fieldTarget.closest ? fieldTarget.closest('.gfield') : null;
231	const fieldPart = fieldTarget.closest ? fieldTarget.closest('.name_first, .name_last, .name_middle, .ginput_left, .ginput_right, .ginput_full') : null;
232	const autocomplete = (fieldTarget.getAttribute('autocomplete') || '').toLowerCase();
233	const signals = [
234		autocomplete,
235		fieldTarget.getAttribute('aria-label') || '',
236		fieldPart && fieldPart.className ? fieldPart.className : '',
237		fieldPart && fieldPart.querySelector('label') ? fieldPart.querySelector('label').textContent : '',
238		gfield && gfield.querySelector('.gfield_label') ? gfield.querySelector('.gfield_label').textContent : '',
239		fieldTarget.getAttribute('placeholder') || '',
240		fieldTarget.getAttribute('name') || '',
241		fieldTarget.getAttribute('id') || ''
242	].join(' ').toLowerCase();
243
244	if (signals.includes('full name')) return 'firstname';
245	if (autocomplete === 'given-name' || signals.includes('first')) return 'firstname';
246	if (autocomplete === 'family-name' || signals.includes('last')) return 'lastname';
247	if (autocomplete === 'additional-name' || signals.includes('middle')) return 'middlename';
248	if (autocomplete === 'email' || signals.includes('mail')) return 'emailaddress';
249	if (autocomplete === 'tel' || signals.includes('phone') || signals.includes('tel')) return 'phonenumber';
250	if (autocomplete === 'postal-code' || signals.includes('zip') || signals.includes('postal')) return 'zipcode';
251	if (autocomplete === 'address-level2' || signals.includes('city')) return 'city';
252	if (autocomplete === 'address-level1' || signals.includes('state') || signals.includes('province') || signals.includes('region')) return 'state';
253	if (autocomplete === 'street-address' || signals.includes('address') || signals.includes('street')) return 'address';
254	if (signals.includes('comment') || signals.includes('message') || signals.includes('question') || signals.includes('notes')) return 'comments';
255
256	return 'other';
257}
258
259/**
260 * Send a Shift Digital form event with a shared payload shape so form event
261 * tracking stays consistent across static forms, quick forms, SRP, and VDP
262 * modal implementations.
263 * @param {Object} config - Shared Shift Digital form event configuration.
264 * @param {string} config.formType - Normalized Shift Digital form type.
265 * @param {string} config.displayType - Display type such as in-page or modal.
266 * @param {string} config.eventName - Shift Digital form event name.
267 * @param {string} [config.leadType] - Optional lead type when the sender has it.
268 * @param {Element} [config.fieldTarget] - Interacted form element for field-name extraction.
269 * @param {string}
269 [config.formFieldName] - Optional precomputed field name that skips extraction.
270 * @param {Object} [config.formVehicle] - Optional normalized vehicle payload.
271 * @returns {void}
272 */
273function sendShiftDigitalFormEvent(config) {
274	if (typeof(sd) === "undefined") return;
275
276	const payload = {
277		formType: config.formType,
278		displayType: config.displayType,
279		events: config.eventName
280	};
281
282	if (config.leadType) {
283		payload.leadType = config.leadType;
284	}
285
286	if (config.eventName === 'formFieldInteraction' && (config.formFieldName || config.fieldTarget)) {
287		payload.formFieldName = config.formFieldName || getShiftDigitalFormFieldName(config.fieldTarget);
288	}
289
290	if (config.formVehicle) {
291		payload.formVehicle = config.formVehicle;
292	}
293
294	sd('dataLayer', payload);
295	sd('send');
296}
297
298/**
299 * Build the common Dealer Alchemist commerce payload shared by multiple CTA
300 * event builders.
301 * @param {Object} context - Normalized analytics context values.
302 * @returns {Object}
303 */
304function buildAutoGoBaseArgs(context) {
305	return {
306		'item_id': context.vehicleId,
307		'item_number': context.vehicleStockNum,
308		'item_price': context.vehiclePrice,
309		'item_condition': context.vehicleCondition,
310		'item_year': context.vehicleYear,
311		'item_make': context.vehicleMake,
312		'item_model': context.vehicleModel,
313		'item_variant': context.vehicleVariant,
314		'item_color': context.vehicleColor,
315		'item_type': context.vehicleType,
316		'item_category': context.vehicleCategory,
317		'item_fuel_type': context.vehicleFuelType,
318		'item_inventory_date': context.vehicleInvDate,
319	};
320}
321
322/**
323 * Build the Dealer Alchemist payload for team member vCard/media interactions.
324 * @param {Element|null} analyticsTarget - Clicked analytics target.
325 * @param {Object} context - Normalized analytics context values.
326 * @returns {Object}
327 */
328function buildTeamContactArgs(analyticsTarget, context) {
329	const teamContactBtn = analyticsTarget ? analyticsTarget.closest('.dv-team-member-contact-card') : null;
330	if (!teamContactBtn) {
331		return {};
332	}
333
334	return {
335		'event': 'asc_media_interaction',
336		'event_owner': 'DealerAlchemist',
337		'page_type': 'contact',
338		'event_action_result': 'download',
339		'media_type': 'vcard',
340		'affiliation': 'DealerAlchemist',
341		'link_url': teamContactBtn.getAttribute('href'),
342		'creative_name': teamContactBtn.getAttribute('title'),
343		'contact_name': teamContactBtn.getAttribute('data-contactname'),
344		'contact_team': teamContactBtn.getAttribute('data-contactteam'),
345		'contact_phone': teamContactBtn.getAttribute('data-contactphone'),
346		...buildAutoGoBaseArgs(context),
347	};
348}
349
350/**
351 * Build the Dealer Alchemist payload for team member email interactions.
352 * @param {Element|null} analyticsTarget - Clicked analytics target.
353 * @param {string} eventValue - Parsed department or event qualifier.
354 * @param {Object} context - Normalized analytics context values.
355 * @returns {Object}
356 */
357function buildClickToEmailArgs(analyticsTarget, eventValue, context) {
358	const teamContactBtn = analyticsTarget ? analyticsTarget.closest('.dv-team-member-email') : null;
359	if (!teamContactBtn) {
360		return {};
361	}
362
363	return {
364		'event': 'asc_click_to_email',
365		'event_owner': 'DealerAlchemist',
366		'page_type': window.location.pathname,
367		'department': eventValue,
368		'affiliation': 'DealerAlchemist',
369		'contact_name': teamContactBtn.getAttribute('data-contactname'),
370		'contact_team': teamContactBtn.getAttribute('data-contactteam'),
371		'contact_phone': teamContactBtn.getAttribute('data-contactphone'),
372		'contact_email': teamContactBtn.getAttribute('data-contactemail'),
373		...buildAutoGoBaseArgs(context),
374	};
375}
376
377function dispatchClickAnalytics(clickTarget, clickNode) {
378	let target = clickTarget;
379	let setnode = clickNode;
380
381	setTimeout(function () {
382		const telLink = target && typeof target.closest === 'function' ? target.closest('a[href^="tel:"]') : null;
383		const googleMapsLink = getGoogleMapsLink(target);
384		const scheduleServiceTrigger = getScheduleServiceTrigger(target);
385		let agEvent = target.getAttribute('data-ag-event') || target.getAttribute('data-aga-event') || target.getAttribute('data-sd-event') || target.getAttribute('data-sda-event');
386
387		if (!agEvent && !telLink && !(typeof sd !== 'undefined' && googleMapsLink) && !(typeof sd !== 'undefined' && scheduleServiceTrigger)) return;
388
389		if (googleMapsLink) {
390			target = googleMapsLink;
391			setnode = createTargetProxy(clickNode, googleMapsLink);
392		}
393
394		if (scheduleServiceTrigger) {
395			target = scheduleServiceTrigger;
396			setnode = createTargetProxy(clickNode, scheduleServiceTrigger);
397		}
398
399		const sdEvent = resolveSDEvent(agEvent, target, telLink, googleMapsLink, scheduleServiceTrigger);
400
401		console.log(sdEvent, 'chromecdp');
402
403		if (sdEvent) {
404			RunSDAnalytics(sdEvent, setnode);
405		}
406
407		if (agEvent) {
408			RunAutoGOAnalytics(agEvent, setnode);
409		} else if (sdEvent) {
410			RunAutoGOAnalytics(sdEvent, setnode);
411		}
412	}, 0);
413}
414
415window.addEventListener('click', function (event) {
416	const initialNormalizedClick = normalizeClickTarget(event);
417	dispatchClickAnalytics(initialNormalizedClick.target, initialNormalizedClick.normalizedEvent);
418}, true);
419
420/**
421 * Send AG Analytics with the event object
422 * @param {Object} ev - The event attribute object
423 * @param {Object} node - The event object
424 */
425function RunAutoGOAnalytics(ev, node){
426	// send the code to GA4
427	window.dataLayer = window.dataLayer || [];
428	let analyticsTarget = getAnalyticsTarget(node);
429	let vehicleId = '';
430	let vehicleVin = '';
431	let vehicleYear = '';
432	let vehicleMake = '';
433	let vehicleModel = '';
434	let vehicleStockNum = '';
435	let vehiclePrice = '';
436	let vehicleDiscount = '';
437	let vehicleCondition = '';
438	let vehicleVariant = '';
439	let vehicleColor = '';
440	let vehicleType = '';
441	let vehicleCategory = '';
442	let vehicleFuelType = '';
443	let vehicleInvDate = '';
444	let dealerName = '';
445	let analyticsContext = {
446		vehicleId,
447		vehicleVin,
448		vehicleYear,
449		vehicleMake,
450		vehicleModel,
451		vehicleStockNum,
452		vehiclePrice,
453		vehicleDiscount,
454		vehicleCondition,
455		vehicleVariant,
456		vehicleColor,
457		vehicleType,
458		vehicleCategory,
459		vehicleFuelType,
460		vehicleInvDate,
461		dealerName
462	};
463
464	let data = ev.split("|");
465	let eventName = data[0];
466	let eventValue = data[1];
467
468	if (ev.includes('teamContact')){
469		let args = buildTeamContactArgs(analyticsTarget, analyticsContext);
470		if (isEmpty(args) === false){
471			window.dataLayer.push(args);
472		}
473	};
474
475	// GA4 dataLayer push for clickToCall intentionally removed from here (was
476	// buildClickToCallArgs(), scoped to .dv-team-member-phone only): it duplicated
477	// EventsManager's own click-to-call tracking (components/EventsManager/js/GA4/
478	// modules/platform.js, clickToCall()), which fires for every tel: link on the
479	// page including team-member phone cards, and now carries the same
480	// contact_name/contact_team/contact_phone fields this used to add here.
481	// Shift Digital's click-to-call dispatch (RunSDAnalytics(), below) is
482	// untouched — EventsManager doesn't send anything to Shift Digital.
483
484	if (ev.includes('clickToEmail')) {
485		let args = buildClickToEmailArgs(analyticsTarget, eventValue, analyticsContext);
486		if (isEmpty(args) === false){
487			window.dataLayer.push(args);
488		}
489	};
490
491	
492
493	// if (ev.includes('getDirections'))  {
494	// 	console.log('send getdirections event');
495	// };
496};
497
498/**
499 * Send SD Analytics with the event object
500 * @param {Object} ev - The event attribute
501 * @param {Object} node - The event object
502 */
503function RunSDAnalytics(ev, node){
504	// Check if SD is undefined
505	if (typeof(sd) === "undefined") return;
506
507	const analyticsTarget = getAnalyticsTarget(node);
508	if (!analyticsTarget) return;
509	
510	let data = ev.split("|");
511	let eventName = data[0];
512	let eventValue = data[1];
513
514	if(!eventValue){
515		let attributeValue = analyticsTarget.getAttribute('data-ag-event-set-prop') || analyticsTarget.getAttribute('data-aga-event-set-prop') || analyticsTarget.getAttribute('data-sd-event-set-prop') || analyticsTarget.getAttribute('data-sda-event-set-prop');
516		if (attributeValue) {
517			data = attributeValue.split("|");
518			eventName = data[0];
519			eventValue = data[1];
520		} else if (!ev.includes('getDirections') && !ev.includes('scheduleServiceClick')) {
521			return;
522		}
523	}
524
525	
526	if (ev.includes('clickToCall')) {
527		sd('dataLayer',{
528			events: eventName,
529			clickToCallDepartment: eventValue,
530		});
531		sd('send');
532	}
533
534	if (ev.includes('getDirections')) {
535		sendSimpleSDEvent(eventName);
536	}
537
538	if (ev.includes('scheduleServiceClick')) {
539		sendSimpleSDEvent(eventName);
540	}
541	
542};
543
544/**
545 * Iteriate over object to see if its empty
546 * @param {Object} ev - object
547 */
548function isEmpty(obj) {
549	for(var prop in obj) {
550		if(Object.prototype.hasOwnProperty.call(obj, prop)) {
551		return false;
552		}
553	}
554	return JSON.stringify(obj) === JSON.stringify({});
555};

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.