Skills › Marketing Skills › conversion-tracking › webflow-forms.js
webflow-forms.js
55 lines · skills/conversion-tracking/assets/snippets/webflow-forms.js at 88c79f6
1/*! Webflow Forms conversion detection (MIT)
2 * Detects: form submission. Pushes dataLayer event: webflow_form_submitted
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 if (window.__ctWebflowForms) return;
6 window.__ctWebflowForms = true;
7 window.dataLayer = window.dataLayer || [];
8 var listeners = [], timeout = 30000;
9 window.__ctWebflowSubscribe = function (listener) {
10 if (listener && typeof listener === 'object') listeners.push(listener);
11 };
12 function isForm(form) {
13 try { return !!(form && String(form.tagName || '').toLowerCase() === 'form' && form.closest && form.closest('.w-form')); }
14 catch (e) { return false; }
15 }
16 function notify(phase, form, candidate) {
17 var handled = false;
18 for (var i=0;i<listeners.length;i++) {
19 try { var fn=listeners[i] && listeners[i][phase]; if (typeof fn==='function' && fn(form,candidate)===true) handled=true; } catch(e) {}
20 }
21 return handled;
22 }
23 function visible(el) { try { return !!el && window.getComputedStyle(el).display !== 'none'; } catch(e) { return false; } }
24 function emit(form) {
25 var name=''; try { name=form.getAttribute('data-name') || form.id || ''; } catch(e) {}
26 if (!notify('onCandidate', form, true)) window.dataLayer.push({event:'webflow_form_submitted',form_name:name});
27 }
28 function submit(event) {
29 try {
30 var form=event.target; if (!isForm(form)) return;
31 notify('beforeAttempt', form, false);
32 var wrap=form.closest('.w-form'), done=wrap.querySelector('.w-form-done'), fail=wrap.querySelector('.w-form-fail');
33 if (!done) return;
34 var old=form.__ctWebflowWatch;
35 if (old) { notify('onOverlap', form, false); old.cancel(false); form.__ctWebflowWatch=null; }
36 var staleFail=visible(fail), settled=false, observer=null, poll=null, kill=null, jq=window.jQuery;
37 function stop(rejected) { if(settled)return; settled=true; if(observer)try{observer.disconnect();}catch(e){} if(jq)jq(document).off('ajaxComplete',check); if(poll)clearInterval(poll); if(kill)clearTimeout(kill); if(form.__ctWebflowWatch===state)form.__ctWebflowWatch=null; if(rejected)notify('onRejected',form,false); }
38 var state={cancel:function(){stop(false);}};
39 function check() {
40 if(settled)return;
41 // Webflow sets success only after its server accepts the form. A redirect
42 // skips the success box, so check that state after its AJAX callback.
43 var wf=jq && jq(form).data('.w-form');
44 if(wf && wf.form && wf.form[0]===form && wf.redirect && wf.success===true){ stop(); emit(form); return; }
45 if(visible(done)){ stop(); emit(form); return; }
46 if(!visible(fail)){staleFail=false;return;}
47 if(!staleFail)stop(true);
48 }
49 if(typeof MutationObserver==='function'){observer=new MutationObserver(check);observer.observe(wrap,{attributes:true,attributeFilter:['style','class'],subtree:true,childList:true});}
50 if(jq)jq(document).on('ajaxComplete',check);
51 poll=setInterval(check,300); kill=setTimeout(function(){stop(true);},timeout); form.__ctWebflowWatch=state;
52 } catch(e) {}
53 }
54 if(typeof document!=='undefined'&&document.addEventListener)document.addEventListener('submit',submit,true);
55})();