PageSourceSearch

https://coinlaw.io/wp-content/themes/hodl-this-design/assets/scripts/newsletter.js?ver=3.168.1

js coinlaw.io collected 2026-10-02 02:18:08 UTC 8,969 bytes, 218 lines download raw bytes

1/**
2 * Universal Newsletter Form Handler
3 *
4 * Handles all newsletter form submissions via AJAX.
5 * Expects each form to be inside a .newsletter-form-wrap container
6 * with a data-form-id attribute.
7 *
8 * @package ChildTheme
9 * @since   1.0.0
10 */
11
12// SVG icons for notification messages (WCAG 2.1 SC 1.4.1 compliance)
13const notificationIcons = {
14	success: '<svg aria-hidden="true" class="icon-success" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.7 6.3l-4 4c-.2.2-.4.3-.7.3-.3 0-.5-.1-.7-.3l-2-2c-.4-.4-.4-1 0-1.4.4-.4 1-.4 1.4 0L7 8.2l3.3-3.3c.4-.4 1-.4 1.4 0 .4.4.4 1 0 1.4z" fill="currentColor"/></svg>',
15	error: '<svg aria-hidden="true" class="icon-error" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 10.1c.4.4.4 1 0 1.4-.2.2-.4.3-.7.3-.3 0-.5-.1-.7-.3L8 9.4l-2.1 2.1c-.2.2-.4.3-.7.3-.3 0-.5-.1-.7-.3-.4-.4-.4-1 0-1.4L6.6 8 4.5 5.9c-.4-.4-.4-1 0-1.4.4-.4 1-.4 1.4 0L8 6.6l2.1-2.1c.4-.4 1-.4 1.4 0 .4.4.4 1 0 1.4L9.4 8l2.1 2.1z" fill="currentColor"/></svg>'
16};
17
18function setNewsletterMessage(messageDiv, status, text) {
19	if (!messageDiv) return;
20
21	const icon = status === 'success' ? notificationIcons.success : notificationIcons.error;
22	const span = document.createElement('span');
23	span.textContent = text;
24
25	messageDiv.textContent = '';
26	messageDiv.insertAdjacentHTML('beforeend', icon);
27	messageDiv.appendChild(span);
28}
29
30document.addEventListener('DOMContentLoaded', function () {
31	const wrappers = document.querySelectorAll('.newsletter-form-wrap');
32
33	wrappers.forEach(function (wrapper) {
34		const form = wrapper.querySelector('form');
35		if (!form) return;
36
37		const formId = wrapper.getAttribute('data-form-id');
38		const messageDiv = wrapper.querySelector('.newsletter-message');
39
40		// Generate unique ID for message div to support aria-describedby
41		const messageId = 'newsletter-msg-' + (formId || Math.random().toString(36).substr(2, 9));
42		if (messageDiv) {
43			messageDiv.id = messageId;
44		}
45
46		// Add autocomplete hint for email autofill and accessible label.
47		const emailField = form.querySelector('input[type="email"]');
48		if (emailField) {
49			if (!emailField.hasAttribute('autocomplete')) {
50				emailField.autocomplete = 'email';
51			}
52			if (!emailField.hasAttribute('aria-label') && (!emailField.id || !document.querySelector('label[for="' + emailField.id + '"]'))) {
53				emailField.setAttribute('aria-label', 'Email address');
54			}
55		}
56
57		// Inject honeypot field (hidden from real users, bots will fill it)
58		const honeypot = document.createElement('input');
59		honeypot.type = 'text';
60		honeypot.name = 'website_url';
61		honeypot.value = '';
62		honeypot.autocomplete = 'off';
63		honeypot.tabIndex = -1;
64		honeypot.setAttribute('aria-hidden', 'true');
65		honeypot.setAttribute('aria-label', 'Leave empty');
66		honeypot.style.cssText = 'position:absolute;left:-9999px;top:-9999px;height:0;width:0;overflow:hidden;';
67		form.appendChild(honeypot);
68
69		// Cloudflare Turnstile — only when a site key is configured. Rendered here rather
70		// than in the form templates so all six placements get it from one place. The
71		// widget is invisible unless Cloudflare decides a visitor needs an interaction.
72		let turnstileWidget = null;
73		let turnstileResolve = null;
74		const turnstileKey = (typeof childThemeNewsletter !== 'undefined' && childThemeNewsletter.turnstileSiteKey) || '';
75		if (turnstileKey) {
76			const holder = document.createElement('div');
77			holder.className = 'newsletter-turnstile';
78			form.appendChild(holder);
79			const renderTurnstile = function () {
80				if (!window.turnstile || turnstileWidget !== null) return;
81				turnstileWidget = window.turnstile.render(holder, {
82					sitekey: turnstileKey,
83					// Run at submit, not page load, and stay invisible unless Cloudflare
84					// actually needs the visitor to do something.
85					execution: 'execute',
86					appearance: 'interaction-only',
87					action: 'newsletter',
88					callback: function (token) {
89						if (turnstileResolve) { turnstileResolve(token || ''); turnstileResolve = null; }
90					},
91					'error-callback': function () {
92						// Fail OPEN in the browser too: the server treats an empty token as
93						// "not configured" only when no secret is set, so this simply lets the
94						// request through to the server's own decision.
95						if (turnstileResolve) { turnstileResolve(''); turnstileResolve = null; }
96					}
97				});
98			};
99			// api.js may still be loading; poll briefly rather than depending on load order.
100			if (window.turnstile) {
101				renderTurnstile();
102			} else {
103				let waited = 0;
104				const poll = setInterval(function () {
105					waited += 200;
106					if (window.turnstile || waited > 8000) {
107						clearInterval(poll);
108						renderTurnstile();
109					}
110				}, 200);
111			}
112		}
113
114		form.addEventListener('submit', function (e) {
115			e.preventDefault();
116
117			const emailInput = form.querySelector('input[type="email"]');
118			if (!emailInput) return;
119
120			const email = emailInput.value;
121			const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
122
123			// Resolve a token before submitting. If Cloudflare decides this visitor must
124			// interact, the widget appears HERE and we wait for them to finish rather than
125			// posting an empty token and telling them to reload. Empty string when Turnstile
126			// is not configured — the server treats that as "feature off", not a failure.
127			const getTurnstileToken = function () {
128				return new Promise(function (resolve) {
129					if (turnstileWidget === null || !window.turnstile) { resolve(''); return; }
130					const existing = window.turnstile.getResponse(turnstileWidget) || '';
131					if (existing) { resolve(existing); return; }
132					turnstileResolve = resolve;
133					try {
134						window.turnstile.execute(turnstileWidget);
135					} catch (err) {
136						turnstileResolve = null;
137						resolve('');
138						return;
139					}
140					// Never leave the form stuck if the challenge never resolves.
141					setTimeout(function () {
142						if (turnstileResolve) { turnstileResolve(''); turnstileResolve = null; }
143					}, 20000);
144				});
145			};
146
147			// Link email input to message div for screen readers
148			if (messageDiv) {
149				emailInput.setAttribute('aria-describedby', messageId);
150			}
151
152			// Clear previous error state
153			emailInput.removeAttribute('aria-invalid');
154
155			if (messageDiv) {
156				messageDiv.className = 'newsletter-message is-loading';
157				messageDiv.setAttribute('role', 'status');
158				messageDiv.textContent = 'Subscribing...';
159			}
160
161			if (submitBtn) {
162				submitBtn.disabled = true;
163			}
164
165			// Submit the subscription. The endpoint is nonce-free by design
166			// (see includes/ajax.php): a nonce on an anonymous, cached form
167			// only produces stale-cache "Session expired" failures without
168			// adding protection.
169			getTurnstileToken().then(function (turnstileToken) {
170				return fetch(childThemeNewsletter.ajaxUrl, {
171					method: 'POST',
172					headers: {
173						'Content-Type': 'application/x-www-form-urlencoded'
174					},
175					body: `action=subscribe_newsletter&email=${encodeURIComponent(email)}&form_id=${encodeURIComponent(formId)}&website_url=${encodeURIComponent(honeypot.value)}&cf_turnstile_response=${encodeURIComponent(turnstileToken)}`
176				})
177					.then(function (response) { return response.json(); })
178					.then(function (data) {
179						if (messageDiv) {
180							const isSuccess = data && data.success;
181							const errorMsg = data && data.data ? data.data : 'Something went wrong. Please refresh the page and try again.';
182							const text = isSuccess ? data.data : 'Error: ' + errorMsg;
183							messageDiv.className = 'newsletter-message ' + (isSuccess ? 'is-success' : 'is-error');
184							messageDiv.setAttribute('role', isSuccess ? 'status' : 'alert');
185							setNewsletterMessage(messageDiv, isSuccess ? 'success' : 'error', text);
186						}
187						// Turnstile tokens are single-use — reset so a second attempt has a fresh one.
188						if (turnstileWidget !== null && window.turnstile) {
189							window.turnstile.reset(turnstileWidget);
190						}
191						if (data && data.success) {
192							emailInput.removeAttribute('aria-invalid');
193							emailInput.removeAttribute('aria-describedby');
194							form.reset();
195						} else {
196						// Mark input as invalid for screen readers
197							emailInput.setAttribute('aria-invalid', 'true');
198							if (submitBtn) {
199								submitBtn.disabled = false;
200							}
201						}
202					})
203					.catch(function () {
204						if (messageDiv) {
205							messageDiv.className = 'newsletter-message is-error';
206							messageDiv.setAttribute('role', 'alert');
207							setNewsletterMessage(messageDiv, 'error', 'Error: An error occurred. Please try again.');
208						}
209						// Mark input as invalid for screen readers
210						emailInput.setAttribute('aria-invalid', 'true');
211						if (submitBtn) {
212							submitBtn.disabled = false;
213						}
214					});
215			});
216		});
217	});
218});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.