Skills › Marketing Skills › conversion-tracking › wix-forms.js
wix-forms.js
230 lines · skills/conversion-tracking/assets/snippets/wix-forms.js at 88c79f6
1/*! Wix Forms conversion detection (MIT)
2 * Detects: form submission. Pushes dataLayer event: wix_form_submitted
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 if (window.__ctDetectWixForms) return;
6 window.__ctDetectWixForms = true;
7 window.dataLayer = window.dataLayer || [];
9 // Wait for Wix's inline confirmation without reading form answers.
10 var SUCCESS_WINDOW_MS = 60000;
12 function fireConversion(formId) {
13 window.dataLayer.push({
14 event: 'wix_form_submitted',
15 form_id: formId || ''
16 });
17 }
19 // ---- Form detection ----
21 // New Wix Forms carry data-hook markers: the form itself is
22 // data-hook="form-<uuid>", fields are wrapped in
23 // data-hook="form-field-...", and the submit control is
24 // data-hook="submit-button".
25 function isNewWixForm(root) {
26 if (!root) return false;
27 try {
28 var hook = root.getAttribute ? root.getAttribute('data-hook') : null;
29 if (hook && hook.indexOf('form-') === 0 && hook.indexOf('form-field-') !== 0) {
30 return true;
31 }
32 if (root.querySelector &&
33 (root.querySelector('[data-hook^="form-field-"]') ||
34 root.querySelector('[data-hook="submit-button"]'))) {
35 return true;
36 }
37 } catch (e) {
38 // fall through
39 }
40 return false;
41 }
43 // Legacy Forms have no observed accepted-only browser verdict in this pack.
44 // Their native submit event is an attempt, so coverage is deliberately withheld.
46 // New Wix Forms arm on a click and confirm with Wix's success marker.
48 // Watchers currently in flight, keyed by form root, so duplicate click
49 // events Wix emits for a single Submit press don't arm two watchers. A
50 // re-click on a watched root REFRESHES the existing watcher instead of
51 // being dropped, so a visitor who fails validation, fixes a field, and
52 // clicks again inside the window gets a fresh verdict.
53 var activeWatchers = [];
55 function findWatcher(root) {
56 for (var w = 0; w < activeWatchers.length; w++) {
57 if (activeWatchers[w].root === root) return activeWatchers[w];
58 }
59 return null;
60 }
62 function findNewFormRoot(btn) {
63 var root = btn.closest ? btn.closest('form') : null;
64 if (root && isNewWixForm(root)) return root;
65 if (btn.closest) {
66 var alt = btn.closest('[data-hook="form-root"]') || btn.closest('[data-hook^="form-"]');
67 if (alt && isNewWixForm(alt)) return alt;
68 }
69 return null;
70 }
72 // Wix FormViewerWidget renders this hook after its submission action
73 // returns a submission. Generic classes and navigation are not acceptance.
74 function classifySuccessNode(node) {
75 if (!node || node.nodeType !== 1) return null;
76 return node.getAttribute &&
77 node.getAttribute('data-hook') === 'submit-form-success-message'
78 ? 'structural' : null;
79 }
81 function isNodeHidden(el) {
82 try {
83 if (el.hidden) return true;
84 if (el.getAttribute && el.getAttribute('aria-hidden') === 'true') return true;
85 var view = el.ownerDocument && el.ownerDocument.defaultView;
86 if (view && view.getComputedStyle) {
87 var cs = view.getComputedStyle(el);
88 if (cs && (cs.display === 'none' || cs.visibility === 'hidden')) return true;
89 }
90 } catch (e) {
91 // fall through
92 }
93 return false;
94 }
96 // Ancestor-aware hiding check, capped at the observed container.
97 function isConcealed(el, stopAt) {
98 var node = el;
99 var depth = 0;
100 while (node && node.nodeType === 1 && depth < 12) {
101 if (isNodeHidden(node)) return true;
102 if (node === stopAt) break;
103 node = node.parentNode;
104 depth++;
105 }
106 return false;
107 }
109 function watchForSuccess(root, formId) {
110 var entry = { root: root, refresh: refresh, cancel: cleanup };
111 activeWatchers.push(entry);
113 var fired = false;
114 var timer = null;
115 var observer = null;
116 function startTimer() {
117 if (timer) clearTimeout(timer);
118 timer = setTimeout(function() {
119 if (!fired) cleanup(); // No success signal within the window.
120 }, SUCCESS_WINDOW_MS);
121 }
123 // A later click on the same root can be a duplicate click event for one
124 // press, or a real re-submit after the visitor corrected a failed
125 // field. Everything reflects the LATEST attempt.
126 function refresh(nextFormId) {
127 if (fired) return;
128 formId = nextFormId;
129 startTimer();
130 }
132 function cleanup() {
133 if (timer) { clearTimeout(timer); timer = null; }
134 if (observer) { try { observer.disconnect(); } catch (e) {} observer = null; }
135 var idx = activeWatchers.indexOf(entry);
136 if (idx !== -1) activeWatchers.splice(idx, 1);
137 discardMethod2(formId, root);
138 }
140 function fire() {
141 if (fired) return;
142 fired = true;
143 var handled = false;
144 try {
145 if (typeof window.__ctWixMethod2Emit === 'function') {
146 handled = window.__ctWixMethod2Emit(formId, root) === true;
147 }
148 } catch (e) {}
149 if (!handled) fireConversion(formId);
150 cleanup();
151 }
153 function noteEvidence(node) {
154 if (!node || node === scope) return false;
155 if (classifySuccessNode(node) === 'structural' && !isConcealed(node, scope)) {
156 fire();
157 return true;
158 }
159 if (node.querySelectorAll) {
160 var markers = node.querySelectorAll('[data-hook="submit-form-success-message"]');
161 for (var i = 0; i < markers.length; i++) {
162 if (noteEvidence(markers[i])) return true;
163 }
164 }
165 return false;
166 }
168 function onMutations(muts) {
169 if (fired) return;
170 for (var i = 0; i < muts.length; i++) {
171 var added = muts[i].addedNodes || [];
172 for (var j = 0; j < added.length; j++) {
173 var node = added[j];
174 if (noteEvidence(node)) return;
175 }
176 }
177 }
179 // The inline confirmation belongs to this form only.
180 var scope = root;
181 try {
182 observer = new MutationObserver(onMutations);
183 observer.observe(scope, {
184 childList: true,
185 subtree: true
186 });
187 } catch (e) {
188 // Without observation, no conversion is inferred.
189 }
191 startTimer();
192 }
194 function handleClick(event) {
195 try {
196 var target = event.target;
197 if (!target || !target.closest) return;
199 var btn = target.closest('[data-hook="submit-button"]');
200 if (!btn) return;
202 var root = findNewFormRoot(btn);
203 if (!root) return;
205 var formId = root.getAttribute ? (root.getAttribute('data-hook') || root.id || '') : (root.id || '');
207 var existing = findWatcher(root);
208 if (existing) {
209 existing.refresh(formId);
210 return;
211 }
213 watchForSuccess(root, formId);
214 } catch (e) {
215 // Never throw back into Wix's own click handling.
216 }
217 }
219 if (typeof document !== 'undefined' && document.addEventListener) {
220 document.addEventListener('click', handleClick, true); // NEW forms
221 }
223 function discardMethod2(formId, root) {
224 try {
225 if (typeof window.__ctWixMethod2Discard === 'function') {
226 window.__ctWixMethod2Discard(formId, root);
227 }
228 } catch (e) {}
229 }
230})();