PageSourceSearch

https://www.iglta.org/includes/public/managed/v_a2e6ae52_e071b31d/core/libs/sv/gtmLib/public/index.js

js iglta.org collected 2026-10-01 11:27:07 UTC 17,575 bytes, 594 lines download raw bytes

1define(function(require, exports, module) {
2	let $ = require("jquery");
3	let lodash = require("lodash");
4	let validator = require("@sv/validator");
5	
6	let GTM = function(args) {
7		let self = this;
8		
9		args = args || {};
10		
11		validator.validate(args, {
12			type: "object",
13			schema: [
14				{ name: "dataLayerName", type: "string", default: "gtmDataLayer", regex: /^[A-Za-z0-9_]+$/ },
15				{ name: "addArgs", type: "object", default: function() { return {} } },
16				{ name: "sendEventsHook", type: "function", default: function() { return function(args) { return args; } } },
17				{ name: "namespace", type: "string", default: "gtm" }
18			],
19			allowExtraKeys: false,
20			throwOnInvalid: true
21		});
22		
23		self._args = args;
24		
25		self._dataLayer = window[self._args.dataLayerName] = window[self._args.dataLayerName] || [];
26		self._onclickBoundAttr = "data-" + self._args.namespace + "-onclick-bound";
27		self._clickDataAttr = "data-" + self._args.namespace + "-click";
28		self._clickDataSelector = "[" + self._clickDataAttr + "]";
29		self._clickDocumentSelector = self._clickDataSelector + "[" + self._onclickBoundAttr + "!=true]";
30		self._clickDocumentEvent = "click.gtmLib_" + self._args.namespace;
31		self._varsDataAttr = "data-" + self._args.namespace + "-vars";
32		self._varsDataCollectedAttr = self._varsDataAttr + "-collected";
33		self._varsDataSelector = "[" + self._varsDataAttr + "]";
34		self._triggerRegex = /^(tClient|tCore|tClient_[a-zA-Z0-9_]+|tCore_[a-zA-Z0-9_]+)$/;
35		self._highlightInited = false;
36		
37		self._documentHandler = function(event) {
38			self._handleClick($(this), "document", event);
39		};
40		
41		$(document).off(self._clickDocumentEvent).on(self._clickDocumentEvent, self._clickDocumentSelector, self._documentHandler);
42	};
43	
44	// returns the html of just the opening part of the element that includes the data-gtm declarations, needed for debugging
45	GTM.prototype._getSnippet = function(node) {		
46		return node.get(0).outerHTML.replace(/>[\s\S]*/, ">");
47	};
48	
49	GTM.prototype._getClickArgs = function(node) {
50		let self = this;
51		
52		let clickArgs = self._getAttrData(node, self._clickDataAttr);
53		if (clickArgs instanceof Error) {
54			return new Error("Invalid JSON in " + self._clickDataAttr + " on element " + self._getSnippet(node) + ".");
55		}
56		
57		clickArgs = clickArgs || {};
58		
59		let valid = validator.validate(clickArgs, {
60			type: "object",
61			schema: [
62				{ name: "inherit", type: "boolean", default: true },
63				{ name: "collect", type: "boolean" },
64				{ name: "follow", type: "boolean" },
65				{ name: "onclick", type: "boolean" }
66			],
67			allowExtraKeys: false
68		});
69		
70		if (valid.err) {
71			return new Error("Invalid keys in " + self._clickDataAttr + " on element " + self._getSnippet(node) + ". " + valid.err.message);
72		}
73		
74		return clickArgs;
75	};
76	
77	GTM.prototype._getVars = function(args) {
78		let self = this;
79		
80		args = args || {};
81		
82		validator.validate(args, {
83			type: "object",
84			schema: [
85				{ name: "node", type: "class", class: $, required: true },
86				{ name: "inherit", type: "boolean", required: true }
87			],
88			allowExtraKeys: false,
89			throwOnInvalid: true
90		});
91		
92		if (args.node.length !== 1) {
93			return new Error("Expected 1 node, but received " + args.node.length);
94		}
95		
96		// create nodes object in order of top level element down to node
97		let nodes = $();
98		
99		if (args.inherit === true) {
100			let collectedData = self._getAttrData(args.node, self._varsDataCollectedAttr);
101			if (collectedData instanceof Error) {
102				return collectedData;
103			}
104			
105			if (collectedData !== undefined) {
106				// we have already collected data, simply return it
107				return collectedData;
108			}
109			
110			// if we are doing inheritance, add the parent ancestry
111			nodes = nodes.add(args.node.parents(self._varsDataSelector));
112		}
113		
114		// our node should be last so add it at the end
115		nodes = nodes.add(args.node);
116		
117		let data = [ true, {}];
118		let errors = [];
119		
120		nodes.each(function(i, val) {
121			let node = $(val);
122			let attrData = self._getAttrData(node, self._varsDataAttr) || {};
123			if (attrData instanceof Error) {
124				errors.push("Invalid JSON in '" + self._varsDataAttr + "' on element " + self._getSnippet(node) + ".");
125				return true;
126			}
127			
128			let err = self._validateVarData(attrData);
129			if (err instanceof Error) {
130				errors.push("Invalid var data in '" + self._varsDataAttr + "' on element " + self._getSnippet(node) + ". " + err.message);
131				return true;
132			}
133			
134			attrData = self._decodeVars(attrData);
135			if (attrData instanceof Error) {
136				errors.push(attrData.message);
137				return true;
138			}
139			
140			data.push(attrData);
141		});
142		
143		if (errors.length > 0) {
144			return new Error(errors.join(" "))
145		}
146		
147		let mergedData = $.extend.apply(null, data);
148		
149		// using the last object in the list we can determine what the valid triggers are for this object
150		let validTriggers = Object.keys(data[data.length - 1]);
151		
152		$.each(mergedData, function(i, val) {
153			if (validTriggers.indexOf(i) === -1) {
154				delete mergedData[i];
155			}
156		});
157		
158		return mergedData;
159	};
160	
161	GTM.prototype._encodeVars = function(data) {		
162		Object.keys(data).forEach(function(event) {
163			Object.keys(data[event]).forEach(function(key) {
164				if (typeof data[event][key] !== "string") { return; }
165				
166				data[event][key] = encodeURIComponent(data[event][key]);
167			});
168		});
169		
170		return data;
171	}
172	
173	GTM.prototype._decodeVars = function(data) {		
174		let err;
175		
176		Object.keys(data).every(function(event, i2) {
177			return Object.keys(data[event]).every(function(key, i3) {
178				if (typeof data[event][key] !== "string") { return true; }
179				
180				try {
181					data[event][key] = decodeURIComponent(data[event][key]);
182				} catch (e) {
183					err = new Error("Data at '" + event + "." + key + "' was not properly percent encoded with encodeURIComponent or goatee's {{%%key}}");
184					return false;
185				}
186				
187				return true;
188			});
189		});
190		
191		if (err) {
192			return err;
193		}
194		
195		return data;
196	}
197	
198	GTM.prototype._handleClick = function(node, type, event) {
199		let self = this;
200		
201		let vars = self._getAndPrepareNode(node);
202		if (vars instanceof Error) {
203			event.preventDefault();
204			event.stopImmediatePropagation();
205			throw vars;
206		}
207		
208		// we know this can't return an error since getAndPrepare already checks it
209		let clickArgs = self._getClickArgs(node);
210		
211		let href = node.attr("href");
212		let follow = (href !== undefined && clickArgs.follow === true);
213		
214		if (follow) {
215			// if following we need to preventDefault() so that it will wait for the async send to GA to complete
216			// if you return false or stopImmediatePropagation, you stop the event bubble
217			event.preventDefault();
218		}
219		
220		self._sendEvents(vars, type, function() {
221			if (follow) {
222				// if we are following, set the window.location
223				window.location = href;
224			}
225		});
226	};
227	
228	GTM.prototype._getAndPrepareNode = function(node) {
229		let self = this;
230		
231		let clickArgs = self._getClickArgs(node);
232		if (clickArgs instanceof Error) { return clickArgs; }
233		
234		let vars = self._getVars({ node: node, inherit: clickArgs.inherit });
235		if (vars instanceof Error) { return vars; }
236		
237		vars = self._prepareEvents(vars);
238		if (vars instanceof Error) { return vars; }
239		
240		return vars;
241	}
242	
243	// takes the entire page and scrapes all elements on it that have click tracking
244	// this allows a report to be built based on all click handlers on the page
245	GTM.prototype.scrape = function() {
246		let self = this;
247		
248		let results = {
249			errors: [],
250			valid: []
251		};
252		
253		$(self._clickDataSelector).each(function(i, val) {
254			let vars = self._getAndPrepareNode($(this));
255			let isErr = vars instanceof Error;
256			results[isErr ? "errors" : "valid"].push({
257				node: $(this),
258				el: this, // return the element so that hovering in the console window highlights the div
259				data: vars
260			});
261		});
262		
263		return results;
264	}
265	
266	// highlight all dom nodes which have click handlers bound to them. Allows easy debugging by highlighting the elements
267	// shows the data they will transmit on hover
268	GTM.prototype.highlight = function() {
269		let self = this;
270
271		if (self._highlightInited === true) {
272			return;
273		}
274
275		self._highlightInited = true;
276
277		function addOutline() {
278			let temp = self.scrape();
279		
280			$(self._clickDataSelector).css({ outline: "2px solid green" });
281			
282			temp.errors.forEach(function(val) {
283				val.node.css({ outline: "2px solid red" });
284			});
285		}
286
287		let helper = $("<div data-gtm-highlighter></div>");
288		helper.css({
289			display: "none",
290			position: "fixed",
291			padding: "5px",
292			background: "#fff",
293			color: "#000",
294			fontSize: "11px",
295			boxShadow: "0px 1px 5px #444",
296			zIndex: 99999
297		});
298
299		$("body").append(helper);
300
301		$("body").on("mouseenter", self._clickDataSelector, function(e) {
302			let vars = self._getAndPrepareNode($(this));
303			
304			helper.html(vars instanceof Error ? vars.message : JSON.stringify(vars, null, " "));
305			helper.show().css({ left: e.clientX + 15, top: e.clientY + 15 });
306		});
307
308		$("body").on("mouseleave", self._clickDataSelector, function() {
309			helper.hide();
310		});
311
312		addOutline();
313
314		let observer = new MutationObserver(lodash.debounce(addOutline), 100);
315		observer.observe(window.document.documentElement, { childList: true, subtree: true });
316	}
317	
318	// update ensures that elements which have collect and/or onclick will be able to properly execute
319	// in the event of malformed arg data, update() will not throw, instead it will console.log() and continue to the next element
320	GTM.prototype.update = function(args) {
321		let self = this;
322		
323		args = args || {};
324		
325		validator.validate(args, {
326			type: "object",
327			schema: [
328				{ name: "node", type: "class", class: $, default: $(document) }
329			],
330			allowExtraKeys: false,
331			throwOnInvalid: true
332		});
333		
334		// if passed in a node, we want to update that node and all of it's children
335		// this way if collect is on a child node it will be updated
336		let nodes = args.node.add(args.node.find(self._clickDataSelector));
337		
338		nodes.each(function(i, val) {
339			let node = $(val);
340			
341			let clickArgs = self._getClickArgs(node);
342			if (clickArgs instanceof Error) {
343				self._log(clickArgs);
344				return true;
345			}
346			
347			if (clickArgs.collect === true && clickArgs.inherit === true) {
348				node.removeAttr(self._varsDataCollectedAttr); // clear out previously collected data
349				
350				let collectedData = self._getVars({ node: node, inherit: clickArgs.inherit });
351				if (collectedData instanceof Error) {
352					self._log(collectedData);
353					return true;
354				}
355				
356				self._setAttrData(node, self._varsDataCollectedAttr, collectedData);
357			}
358			
359			if (clickArgs.onclick === true && node.attr(self._onclickBoundAttr) === undefined) {
360				if (node.attr("onclick") !== undefined) {
361					// if the element already has an onclick not related to gtm we need to log because both can't exist
362					self._log(new Error("non-gtmLib onclick already exists on element " + self._getSnippet(node) + "."));
363					return true;
364				}
365				
366				node.data("sv_gtmLib_" + self._args.namespace, self);
367				node.attr(self._onclickBoundAttr, true);
368				node.attr("onclick", "return $(this).data('sv_gtmLib_" + self._args.namespace + "')._handleClick($(this), 'onclick', event || window.event);"); // event || window.event for < IE9
369			}
370		});
371	};
372	
373	GTM.prototype._prepareEvents = function(events) {
374		let self = this;
375		
376		$.each(events, function(i, val) {
377			// fold in addArgs for each trigger, do not overwrite existing keys
378			events[i] = $.extend({}, self._args.addArgs[i] || {}, val);
379		});
380		
381		// execute the sendEventsHook
382		let returnData = self._args.sendEventsHook(events);
383		
384		if (typeof returnData !== "object") {
385			return new Error("sendEventsHook must return args object");
386		}
387		
388		// validate after both are done we still have valid data
389		let err = self._validateVarData(returnData);
390		if (err instanceof Error) {
391			return err;
392		}
393		
394		return returnData;
395	}
396	
397	GTM.prototype.sendEvents = function(events, cb) {
398		let self = this;
399		
400		cb = cb || function(err) {
401			if (err) { throw err; }
402		}
403		
404		var events = self._prepareEvents(events);
405		if (events instanceof Error) {
406			return cb(events);
407		}
408		
409		self._sendEvents(events, "sendEvents", cb);
410	};
411	
412	GTM.prototype._sendEvents = function(events, type, cb) {
413		let self = this;
414		
415		let pushArr = [];
416		let cbCount = 0;
417		
418		let done = function() {
419			cb(null);
420			
421			// only for unit testing, DO NOT USE in production. Must come after cb so that follow can occur before this
422			$(window).trigger("gtmLib_eventCallback", { gtm: self, type: type, events: events });
423		};
424		
425		let eventCallback = function() {
426			cbCount++;
427			
428			if (cbCount === pushArr.length) {
429				done();
430			}
431		};
432		
433		$.each(events, function(eventName, eventObj) {
434			pushArr.push({
435				event: eventName,
436				// append a callback so that we can guarantee the tracking is completed before the browser navigates away
437				eventCallback: eventCallback,
438				// namespace all keys in GTM except "event" and "eventCallback" under "sv." to make it easy to identify our variables in GTM from native variables
439				// this also allows us to empty out the sv object between pushes to prevent persistence
440				sv: eventObj
441			});
442		});
443		
444		if (pushArr.length === 0) {
445			// bounce off the event loop to ensure this function is async always
446			return setTimeout(done, 0);
447		}
448		
449		pushArr.forEach(function(val) {
450			self._dataLayer.push(val);
451			
452			// we need to empty out the sv part of the data layer between pushes that way variables don't persist between the pushes
453			self._dataLayer.push({ sv: undefined });
454		});
455	};
456	
457	// returns the vars for a single node for all triggers
458	GTM.prototype.getVars = function(args) {
459		let self = this;
460		
461		args = args || {};
462		
463		validator.validate(args, {
464			type: "object",
465			schema: [
466				{ name: "node", type: "class", class: $, required: true },
467				{ name: "inherit", type: "boolean", default: false }
468			],
469			allowExtraKeys: false,
470			throwOnInvalid: true
471		});
472		
473		let data = self._getVars({ node: args.node, inherit: args.inherit });
474		if (data instanceof Error) {
475			throw data;
476		}
477		
478		return data;
479	};
480	
481	// sets the vars for a single node for all triggers
482	GTM.prototype.setVars = function(args) {
483		let self = this;
484		
485		args = args || {};
486		
487		validator.validate(args, {
488			type: "object",
489			schema: [
490				{ name: "node", type: "class", class: $, required: true },
491				{ name: "data", type: "object", required: true }
492			],
493			allowExtraKeys: false,
494			throwOnInvalid: true
495		});
496		
497		let err = self._validateVarData(args.data);
498		if (err instanceof Error) {
499			throw new Error(err);
500		}
501		
502		args.data = self._encodeVars(args.data);
503		self._setAttrData(args.node, self._varsDataAttr, args.data);
504		self.update({ node: args.node });
505	};
506	
507	// sets or increments a single var for a single node for a single trigger
508	GTM.prototype.setVar = function(args) {
509		let self = this;
510		
511		args = args || {};
512		
513		validator.validate(args, {
514			type: "object",
515			schema: [
516				{ name: "node", type: "class", class: $, required: true },
517				{ name: "trigger", type: "string", required: true },
518				{ name: "key", type: "string", required: true },
519				{ name: "value", type: "any" },
520				{ name: "inc", type: "number" } // if "inc" is passed, ignore "value"
521			],
522			allowExtraKeys: false,
523			throwOnInvalid: true
524		});
525		
526		let data = self._getVars({ node: args.node, inherit: false });
527		if (data instanceof Error) {
528			throw data;
529		}
530		
531		let trigger = data[args.trigger] = data[args.trigger] || {};
532		if (args.inc !== undefined) {
533			// default the value of the trigger to 0 if it doesn't exist
534			trigger[args.key] = (trigger[args.key] !== undefined) ? trigger[args.key] : 0;
535			
536			if (typeof trigger[args.key] !== "number") {
537				throw new Error("Cannot increment '" + args.key + "', previous value '" + trigger[args.key] + "' not a number.");
538			}
539			
540			trigger[args.key] += args.inc; 
541		} else if (args.value === undefined) {
542			delete trigger[args.key];
543		} else {
544			trigger[args.key] = args.value;
545		}
546		
547		self.setVars({ node: args.node, data: data });
548	};
549	
550	// read an attribute and convert from JSON to object
551	GTM.prototype._getAttrData = function(node, attr) {		
552		let attrData = node.attr(attr);
553		let returnData;
554		
555		if (attrData === undefined || attrData === "") { return; }
556		
557		try {
558			returnData = JSON.parse(attrData);
559		} catch (e) {
560			returnData = e;
561		}
562		
563		return returnData;
564	}
565	
566	// set an attribute from object to JSON
567	GTM.prototype._setAttrData = function(node, attr, data) {		
568		node.attr(attr, JSON.stringify(data));
569	}
570	
571	GTM.prototype._validateVarData = function(data) {
572		let self = this;
573		
574		let err;
575		
576		$.each(data, function(trigger, val) {
577			if (trigger.match(self._triggerRegex) === null) {
578				err = new Error("Invalid trigger name '" + trigger + "'.");
579				return false;
580			}
581		});
582		
583		return err;
584	}
585	
586	// wrapper for console.log allowing easier monkey-patching in unit tests
587	GTM.prototype._log = function(str) {
588		console.log(str);
589	}
590	
591	module.exports = {
592		GTM: GTM
593	}
594});

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.