Skill Tracer
Skills › Marketing Skills › conversion-tracking › contact-form-7.js

contact-form-7.js

99 lines · skills/conversion-tracking/assets/snippets/contact-form-7.js at 88c79f6

1/*! Contact Form 7 conversion detection (MIT)
2 * Detects: form submission. Pushes dataLayer event: contact_form_7_submitted
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 'use strict';
6
7 // One shared runtime owns eligibility, accepted event identity, and the ID-only fallback.
8 // A legacy recipe guard without this runtime is unknown code, so do not stack a new listener.
9 var runtime = window.__ctCf7Runtime;
10 if (runtime && runtime.installed) return;
11 if (!runtime && window.__ctDetectContactForm7) return;
12 runtime = runtime || { installed: false };
13 window.__ctCf7Runtime = runtime;
14 window.__ctDetectContactForm7 = true;
15 window.dataLayer = window.dataLayer || [];
16 var seenEvents = typeof WeakSet === 'function' ? new WeakSet() : null;
17 var seenEventFallback = [];
19 function isContactForm7Form(formEl) {
20 if (!formEl || (formEl.tagName || '').toLowerCase() !== 'form') return false;
21 try {
22 if (formEl.classList && formEl.classList.contains('wpcf7-form')) return true;
23 return !!(formEl.closest && formEl.closest('.wpcf7'));
24 } catch (e) {
25 return false;
26 }
27 }
29 function validFormId(value) {
30 if (typeof value === 'number' && Number.isFinite(value) && Math.floor(value) === value && value > 0 && value <= 9007199254740991) return String(value);
31 if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) return '';
32 var number = Number(value);
33 return Number.isFinite(number) && number <= 9007199254740991 && String(number) === value ? value : '';
34 }
36 function readFormId(formEl, event) {
37 // CF7 6.1.6's event detail has contactFormId; read no other detail keys.
38 var detailId = '';
39 try {
40 detailId = validFormId(event && event.detail && event.detail.contactFormId);
41 } catch (e) {}
42 if (detailId) return detailId;
44 // Compatibility fallback for markup where the event detail omits the ID.
45 try {
46 var directId = validFormId(formEl.getAttribute && formEl.getAttribute('data-wpcf7-id'));
47 if (directId) return directId;
48 var wrap = formEl.closest && formEl.closest('.wpcf7');
49 return validFormId(wrap && wrap.getAttribute && wrap.getAttribute('data-wpcf7-id'));
50 } catch (e) {
51 return '';
52 }
53 }
55 // CF7 can send both clicks while the first request is still pending.
56 // Group those requests without blocking or changing the form submission.
57 document.addEventListener('wpcf7beforesubmit', function (event) {
58 var formEl = event && event.target;
59 if (!isContactForm7Form(formEl)) return;
60 if (!formEl.__ctCf7Attempt || formEl.getAttribute('data-status') !== 'submitting') {
61 formEl.__ctCf7Attempt = { emitted: false };
62 }
63 }, false);
65 document.addEventListener('wpcf7mailsent', function (event) {
66 var formEl = event && event.target;
67 if (!isContactForm7Form(formEl)) return;
68 if (seenEvents) {
69 if (seenEvents.has(event)) return;
70 seenEvents.add(event);
71 } else {
72 if (seenEventFallback.indexOf(event) !== -1) return;
73 seenEventFallback.push(event);
74 if (seenEventFallback.length > 50) seenEventFallback.shift();
75 }
77 var formId = readFormId(formEl, event);
78 if (!formId) return;
79 var attempt = formEl.__ctCf7Attempt;
80 if (attempt && attempt.emitted) return;
81 if (attempt) attempt.emitted = true;
83 // The optional Method 2 adapter may enrich this accepted event. It must
84 // return literal true only after pushing; otherwise the shared core emits
85 // the same event with IDs only. Neither path inspects event.detail values.
86 var enriched = false;
87 try {
88 if (typeof window.__ctCf7Method2Emit === 'function') {
89 enriched = window.__ctCf7Method2Emit(formEl, event, formId) === true;
90 }
91 } catch (e) {
92 enriched = false;
93 }
94 if (!enriched) {
95 window.dataLayer.push({ event: 'contact_form_7_submitted', form_id: formId });
96 }
97 }, false);
98 runtime.installed = true;
99})();