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

framer-forms.js

94 lines · skills/conversion-tracking/assets/snippets/framer-forms.js at 88c79f6

1/*! Framer Forms conversion detection (MIT)
2 * Detects: form submission. Pushes dataLayer event: framer_form_submitted
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 if (window.__ctFramerForms) return;
6 window.__ctFramerForms = true;
7 window.dataLayer = window.dataLayer || [];
8 var listeners = [];
9 var pending = [];
10 window.__ctFramerSubscribe = function (listener) {
11 if (listener && typeof listener === 'object') listeners.push(listener);
12 };
13 function isFramerForm(form) {
14 try {
15 if (!form || String(form.tagName || '').toLowerCase() !== 'form') return false;
16 if (form.hasAttribute && form.hasAttribute('data-framer-name')) return true;
17 if (typeof form.className === 'string' && /(^|\s)framer-[A-Za-z0-9_-]+/.test(form.className)) return true;
18 return !!(form.closest && (form.closest('[data-framer-component-type]') || form.closest('[data-framer-name]')));
19 } catch (e) { return false; }
20 }
21 function notify(phase, form, candidate) {
22 var handled = false;
23 for (var i = 0; i < listeners.length; i++) {
24 try {
25 var fn = listeners[i] && listeners[i][phase];
26 if (typeof fn === 'function' && fn(form, candidate) === true) handled = true;
27 } catch (e) {}
28 }
29 return handled;
30 }
31 function remember(form) {
32 try {
33 if (!isFramerForm(form)) return;
34 for (var i = 0; i < pending.length; i++) {
35 if (pending[i].form === form) return;
36 }
37 pending.push({ form: form, time: Date.now(), claimed: false });
38 notify('beforeAttempt', form, false);
39 } catch (e) {}
40 }
41 function finish(attempts, accepted) {
42 for (var i = 0; i < attempts.length; i++) {
43 var index = pending.indexOf(attempts[i]);
44 if (index !== -1) pending.splice(index, 1);
45 }
46 if (!accepted) return;
47 // Concurrent forms cannot be attributed without inspecting answers.
48 // Keep those submissions event-only, with no guessed form name.
49 var form = attempts.length === 1 ? attempts[0].form : null;
50 var name = form ? form.getAttribute('data-framer-name') || form.id || '' : '';
51 if (!form || !notify('onCandidate', form, true)) {
52 window.dataLayer.push({ event: 'framer_form_submitted', form_name: name });
53 }
54 }
55 if (typeof window.fetch === 'function') {
56 var originalFetch = window.fetch;
57 window.fetch = function (input, options) {
58 var attempts = null;
59 try {
60 var url = typeof input === 'string' ? input : input && input.url;
61 var method = options && options.method || input && input.method || 'GET';
62 if (String(method).toUpperCase() === 'POST' && /^https:\/\/api\.framer\.com\/forms\/v1\/forms\/[a-f0-9-]+\/submit(?:\?.*)?$/.test(url || '')) {
63 attempts = [];
64 var now = Date.now();
65 pending = pending.filter(function (attempt) { return now - attempt.time < 60000; });
66 for (var i = 0; i < pending.length; i++) {
67 if (!pending[i].claimed) {
68 pending[i].claimed = true;
69 attempts.push(pending[i]);
70 }
71 }
72 }
73 } catch (e) {}
74 var result = originalFetch.apply(this, arguments);
75 if (attempts) result.then(function (response) {
76 try { finish(attempts, response.ok === true); } catch (e) {}
77 }, function () {
78 try { finish(attempts, false); } catch (e) {}
79 });
80 return result;
81 };
82 }
83 if (typeof document !== 'undefined' && document.addEventListener) {
84 document.addEventListener('submit', function (event) { remember(event.target); }, true);
85 // Framer handles Enter directly instead of dispatching native submit.
86 document.addEventListener('keydown', function (event) {
87 var target = event.target;
88 if (event.key === 'Enter' && target && String(target.tagName).toLowerCase() !== 'textarea') {
89 var form = target.form;
90 if (isFramerForm(form) && form.matches(':valid')) remember(form);
91 }
92 }, true);
93 }
94})();