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.