Skill Tracer
Skills › Marketing Skills › conversion-tracking › ninja-forms.js

ninja-forms.js

152 lines · skills/conversion-tracking/assets/snippets/ninja-forms.js at 88c79f6

1/*! Ninja Forms conversion detection (MIT)
2 * Detects: form submission. Pushes dataLayer event: ninja_forms_submitted
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 'use strict';
6 var runtime = window.__ctNinjaFormsRuntime;
7 if (runtime && runtime.installed) return;
8
9 // Preserve the recipe's public duplicate-install guard. If a superseded
10 // snippet already set it without this runtime, fail closed: don't layer a
11 // second listener over an unknown old detector.
12 if (!runtime && window.__ctDetectNinjaForms) return;
14 if (!runtime) {
15 runtime = { seenEntryIds: Object.create(null), installed: false };
16 window.__ctNinjaFormsRuntime = runtime;
17 }
18 window.__ctDetectNinjaForms = true;
19 window.dataLayer = window.dataLayer || [];
21 var MISSING = {};
22 var MAX_SAFE_INTEGER = 9007199254740991;
24 // Read only own data properties. Callback payloads are normally parsed JSON,
25 // but a filter or another page script can supply accessor-backed objects.
26 // Do not execute getters while deciding whether a response proves a save.
27 function ownDataValue(object, key) {
28 if (!object || typeof object !== 'object' || !Object.prototype.hasOwnProperty.call(object, key)) return MISSING;
29 if (typeof Object.getOwnPropertyDescriptor !== 'function') return MISSING;
30 var descriptor = Object.getOwnPropertyDescriptor(object, key);
31 if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) return MISSING;
32 return descriptor.value;
33 }
35 function emptyErrors(value) {
36 if (Object.prototype.toString.call(value) === '[object Array]') return value.length === 0;
37 if (!value || typeof value !== 'object') return false;
38 for (var key in value) {
39 if (Object.prototype.hasOwnProperty.call(value, key)) return false;
40 }
41 return true;
42 }
44 // Accept only primitive safe integers or canonical decimal strings. Never
45 // coerce booleans, arrays, objects, boxed numbers, or accessor-backed IDs.
46 function positiveIntegerId(value) {
47 if (typeof value === 'number') {
48 if (!isFinite(value) || value <= 0 || Math.floor(value) !== value || value > MAX_SAFE_INTEGER) return null;
49 return String(value);
50 }
51 if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) return null;
52 var number = Number(value);
53 if (!isFinite(number) || number > MAX_SAFE_INTEGER || String(number) !== value) return null;
54 return value;
55 }
57 function canonicalFormId(value) {
58 var formId;
59 if (typeof value === 'number') {
60 if (!isFinite(value) || value <= 0 || Math.floor(value) !== value || value > MAX_SAFE_INTEGER) return null;
61 formId = String(value);
62 } else if (typeof value === 'string') {
63 formId = value;
64 } else {
65 return null;
66 }
67 if (!/^[1-9]\d*(?:_(?:0|[1-9]\d*))*$/.test(formId)) return null;
68 var parts = formId.split('_');
69 for (var i = 0; i < parts.length; i++) {
70 var part = Number(parts[i]);
71 if (!isFinite(part) || part > MAX_SAFE_INTEGER || String(part) !== parts[i]) return null;
72 }
73 return formId;
74 }
76 function inspectResponse(response, callbackFormId) {
77 var errors = ownDataValue(response, 'errors');
78 if (errors === MISSING || !emptyErrors(errors)) return null;
79 var data = ownDataValue(response, 'data');
80 if (!data || typeof data !== 'object') return null;
81 var halt = ownDataValue(data, 'halt');
82 if (halt !== MISSING && halt) return null;
83 var nestedErrors = ownDataValue(data, 'errors');
84 if (nestedErrors !== MISSING && !emptyErrors(nestedErrors)) return null;
85 var formValue = ownDataValue(data, 'form_id');
86 var formId = formValue === MISSING ? null : canonicalFormId(formValue);
87 if (!formId || canonicalFormId(callbackFormId) !== formId) return null;
88 var actions = ownDataValue(data, 'actions');
89 if (!actions || typeof actions !== 'object') return null;
90 var save = ownDataValue(actions, 'save');
91 if (!save || typeof save !== 'object') return null;
92 var saveValue = ownDataValue(save, 'sub_id');
93 var entryId = saveValue === MISSING ? null : positiveIntegerId(saveValue);
94 if (!entryId) return null;
95 return { formId: formId, entryId: entryId };
96 }
98 runtime.inspectResponse = inspectResponse;
100 function handleSubmitResponse(response, textStatus, jqXHR, callbackFormId) {
101 var accepted = inspectResponse(response, callbackFormId);
102 if (!accepted) {
103 if (typeof window.__ctNinjaMethod2AfterResponse === 'function') {
104 window.__ctNinjaMethod2AfterResponse(callbackFormId, false, false);
105 }
106 return;
107 }
109 var key = '$' + accepted.entryId;
110 if (runtime.seenEntryIds[key]) {
111 if (typeof window.__ctNinjaMethod2AfterResponse === 'function') {
112 window.__ctNinjaMethod2AfterResponse(accepted.formId, false, true);
113 }
114 return;
115 }
116 runtime.seenEntryIds[key] = true;
118 var enriched = false;
119 if (typeof window.__ctNinjaMethod2Emit === 'function') {
120 try {
121 enriched = window.__ctNinjaMethod2Emit(accepted.formId, accepted.entryId) === true;
122 } catch (e) {
123 enriched = false;
124 }
125 }
126 if (!enriched) {
127 window.dataLayer.push({ event: 'ninja_forms_submitted', form_id: accepted.formId });
128 }
129 if (typeof window.__ctNinjaMethod2AfterResponse === 'function') {
130 window.__ctNinjaMethod2AfterResponse(accepted.formId, true, false);
131 }
132 }
134 function hookNfRadio() {
135 if (runtime.installed) return true;
136 var radio = typeof nfRadio !== 'undefined' && nfRadio.channel ? nfRadio :
137 (typeof Backbone !== 'undefined' && Backbone.Radio ? Backbone.Radio : null);
138 if (!radio) return false;
139 try {
140 radio.channel('forms').on('submit:response', handleSubmitResponse);
141 runtime.installed = true;
142 return true;
143 } catch (e) {
144 return false;
145 }
146 }
148 if (!hookNfRadio()) {
149 document.addEventListener('DOMContentLoaded', hookNfRadio);
150 window.addEventListener('load', hookNfRadio);
151 }
152})();