|
1 | 1 | <script> |
2 | | -; |
3 | | -(() => { |
| 2 | +;(function() { |
4 | 3 | const SELECTORS = { |
5 | 4 | form: '#feedbackForm', |
6 | 5 | thumbs: '.thumb', |
|
10 | 9 | submitButton: '#submitButton', |
11 | 10 | captchaHint: '#captchaHint', |
12 | 11 | captchaField: 'textarea[name="g-recaptcha-response"]', |
13 | | - toast: '#feedback-toast', |
14 | | - modalOverlay: '#modal-overlay', |
| 12 | + successMessage: '#feedback-toast', |
| 13 | + successMessageToc: '#feedback-toast-thumbs-toc', |
15 | 14 | thumbsToc: '#thumbs-toc' |
16 | 15 | }; |
17 | 16 |
|
18 | 17 | const POLL_INTERVAL = 300; // ms between captcha checks |
19 | | - const POLL_TIMEOUT = 5 * 60 * 1000; // stop polling after 5 minutes |
| 18 | + const POLL_TIMEOUT = 5 * 60 * 1000; // ms to give up on captcha polling |
20 | 19 | const SCROLL_HIDE_OFFSET = 700; // px before bottom to hide thumbs |
21 | 20 |
|
22 | 21 | let positive = null; |
| 22 | + let fromToc = false; // track if last click was in TOC |
23 | 23 |
|
24 | 24 | document.addEventListener('DOMContentLoaded', () => { |
25 | | - const form = document.querySelector(SELECTORS.form); |
26 | | - if (!form) return; |
27 | | - const thumbs = document.querySelectorAll(SELECTORS.thumbs); |
28 | | - const details = document.querySelector(SELECTORS.feedbackDetails); |
29 | | - const options = document.querySelector(SELECTORS.feedbackOptions); |
30 | | - const prompt = document.querySelector(SELECTORS.feedbackPrompt); |
| 25 | + const form = document.querySelector(SELECTORS.form); |
31 | 26 | const submitBtn = document.querySelector(SELECTORS.submitButton); |
32 | | - const hint = document.querySelector(SELECTORS.captchaHint); |
33 | | - const toast = document.querySelector(SELECTORS.toast); |
34 | | -
|
35 | | - // Hide thumbs when scrolling to the bottom |
36 | | - document.addEventListener( |
37 | | - 'scroll', |
38 | | - event => requestAnimationFrame(() => { |
39 | | - const toc = document.querySelector(SELECTORS.thumbsToc); |
40 | | - if (!toc) return; |
41 | | - const docH = document.body.scrollHeight; |
42 | | - const scrollPos = window.scrollY + window.innerHeight; |
43 | | - toc.style.display = (scrollPos + SCROLL_HIDE_OFFSET > docH) ? 'none' : 'block'; |
44 | | - }), |
45 | | - { passive: true } |
46 | | - ); |
| 27 | + const hint = document.querySelector(SELECTORS.captchaHint); |
| 28 | + const successMsg = document.querySelector(SELECTORS.successMessage); |
| 29 | + const successMsgToc = document.querySelector(SELECTORS.successMessageToc); |
| 30 | + if (!form) return; |
47 | 31 |
|
48 | | - thumbs.forEach(thumb => { |
| 32 | + // Show form on thumbs click |
| 33 | + document.querySelectorAll(SELECTORS.thumbs).forEach(thumb => { |
49 | 34 | thumb.addEventListener('click', () => { |
50 | 35 | positive = thumb.id.includes('up'); |
| 36 | + fromToc = !!thumb.closest(SELECTORS.thumbsToc); |
51 | 37 | form.elements['positiveFeedback'].value = positive; |
52 | | -
|
53 | | - // Safely build radio options |
54 | 38 | const items = positive |
55 | | - ? [ 'Solved my problem', 'Easy to understand', 'Other' ] |
56 | | - : [ 'Not helpful', 'Too complex', 'Other' ]; |
57 | | - options.innerHTML = items.map((txt, i) => |
58 | | - `<label><input type="radio" name="feedback" value="${txt}"${i===0? ' checked':''}> ${txt}</label>` |
59 | | - ).join(''); |
60 | | -
|
61 | | - prompt.textContent = positive |
62 | | - ? 'Let us know what we do well:' |
63 | | - : 'Let us know what could be improved:'; |
64 | | -
|
65 | | - details.classList.remove('hidden'); |
| 39 | + ? ['Solved my problem','Easy to understand','Other'] |
| 40 | + : ['Not helpful','Too complex','Other']; |
| 41 | + document.querySelector(SELECTORS.feedbackOptions).innerHTML = |
| 42 | + items.map((txt,i) => |
| 43 | + `<label><input type=\"radio\" name=\"feedback\" value=\"${txt}\"${i===0?' checked':''}> ${txt}</label>` |
| 44 | + ).join(''); |
| 45 | + document.querySelector(SELECTORS.feedbackPrompt).textContent = |
| 46 | + positive ? 'Let us know what we do well:' : 'Let us know what could be improved:'; |
| 47 | + document.querySelector(SELECTORS.feedbackDetails).classList.remove('hidden'); |
| 48 | + form.hidden = false; |
66 | 49 | form.classList.remove('hidden'); |
67 | 50 | }); |
68 | 51 | }); |
69 | 52 |
|
70 | | - form.addEventListener('submit', () => { |
71 | | - const version = form.dataset.version || ''; |
72 | | - const beta = form.dataset.beta === 'true'; |
73 | | - form.elements['url'].value = window.location.href; |
74 | | - form.elements['positiveFeedback'].value = positive; |
75 | | - form.elements['version'].value = version; |
76 | | - form.elements['beta'].value = beta; |
77 | | - form.elements['date'].value = new Date().toISOString(); |
78 | | - form.elements['navigator'].value = `${navigator.userAgent}, ${navigator.language}`; |
79 | | - // Store feedback type |
80 | | - sessionStorage.setItem('feedbackType', positive ? 'positive' : 'negative'); |
81 | | - }); |
82 | | -
|
83 | | - window.closeForm = event => { |
84 | | - event.preventDefault(); |
85 | | - form.classList.add('hidden'); |
86 | | - }; |
| 53 | + // Hide thumbs near bottom |
| 54 | + document.addEventListener('scroll', () => requestAnimationFrame(() => { |
| 55 | + const toc = document.querySelector(SELECTORS.thumbsToc); |
| 56 | + if (!toc) return; |
| 57 | + const docH = document.body.scrollHeight; |
| 58 | + const scrollPos = window.scrollY + window.innerHeight; |
| 59 | + toc.style.display = (scrollPos + SCROLL_HIDE_OFFSET > docH) ? 'none' : 'block'; |
| 60 | + }), { passive: true }); |
87 | 61 |
|
| 62 | + // Poll reCAPTCHA token |
88 | 63 | if (submitBtn && hint) { |
89 | 64 | submitBtn.disabled = true; |
90 | 65 | hint.style.display = 'block'; |
91 | 66 | const start = Date.now(); |
92 | | - // Enable the submit button when the reCAPTCHA token is available |
93 | | - // https://answers.netlify.com/t/recaptcha-integration-in-forms-configuration/6181/3 |
94 | | - const intervalId = setInterval(() => { |
| 67 | + const checkId = setInterval(() => { |
95 | 68 | const captcha = document.querySelector(SELECTORS.captchaField); |
96 | | - if (captcha && captcha.value.trim() !== '') { |
| 69 | + if (captcha && captcha.value.trim()) { |
97 | 70 | submitBtn.disabled = false; |
98 | 71 | hint.style.display = 'none'; |
99 | | - clearInterval(intervalId); |
| 72 | + clearInterval(checkId); |
100 | 73 | } |
101 | 74 | if (Date.now() - start > POLL_TIMEOUT) { |
102 | | - clearInterval(intervalId); |
103 | | - console.warn('reCAPTCHA token polling timed out'); |
| 75 | + clearInterval(checkId); |
| 76 | + console.warn('reCAPTCHA polling timed out'); |
104 | 77 | } |
105 | 78 | }, POLL_INTERVAL); |
106 | 79 | } |
107 | 80 |
|
108 | | - if (toast && window.location.search.includes('success')) { |
109 | | - const modal = document.querySelector(SELECTORS.form); |
110 | | - modal.classList.add('hidden'); |
111 | | - toast.classList.remove('hidden'); |
112 | | - setTimeout(() => toast.classList.add('hidden'), 5000); |
| 81 | + // AJAX submit with URL-encoded body |
| 82 | + form.addEventListener('submit', async e => { |
| 83 | + e.preventDefault(); |
| 84 | + submitBtn.disabled = true; |
113 | 85 |
|
114 | | - const url = new URL(window.location.href); |
115 | | - url.searchParams.delete('success'); |
116 | | - window.history.replaceState({}, document.title, url); |
117 | | - } |
| 86 | + // Copy selected radio into hidden feedback input |
| 87 | + const checked = Array.from( |
| 88 | + form.querySelectorAll('input[name="feedback"]:checked') |
| 89 | + ).map(el => el.value); |
| 90 | + // pick the first non-empty |
| 91 | + const first = checked.find(v => v.trim() !== '') || ''; |
| 92 | + form.elements['feedback'].value = first; |
| 93 | +
|
| 94 | + // Populate hidden metadata |
| 95 | + form.elements['url'].value = window.location.href; |
| 96 | + form.elements['positiveFeedback'].value = positive; |
| 97 | + form.elements['version'].value = form.dataset.version || ''; |
| 98 | + form.elements['beta'].value = form.dataset.beta === 'true'; |
| 99 | + form.elements['date'].value = new Date().toISOString(); |
| 100 | + form.elements['navigator'].value = `${navigator.userAgent}, ${navigator.language}`; |
| 101 | + sessionStorage.setItem('feedbackType', positive ? 'positive' : 'negative'); |
| 102 | +
|
| 103 | + // Build URL-encoded body per Netlify AJAX requirements |
| 104 | + const formData = new FormData(form); |
| 105 | + const body = new URLSearchParams(); |
| 106 | + for (const [key, value] of formData.entries()) { |
| 107 | + body.append(key, value); |
| 108 | + } |
| 109 | +
|
| 110 | + try { |
| 111 | + const resp = await fetch(window.location.pathname, { |
| 112 | + method: 'POST', |
| 113 | + headers: { |
| 114 | + 'Content-Type': 'application/x-www-form-urlencoded', |
| 115 | + 'Accept': 'application/json' |
| 116 | + }, |
| 117 | + body: body.toString() |
| 118 | + }); |
| 119 | + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); |
| 120 | + // On success, show custom message |
| 121 | + form.hidden = true; |
| 122 | + if (successMsg) successMsg.classList.add('hidden'); |
| 123 | + if (successMsgToc) successMsgToc.classList.add('hidden'); |
| 124 | + const toastToShow = fromToc ? successMsgToc : successMsg; |
| 125 | + if (toastToShow) { |
| 126 | + toastToShow.classList.remove('hidden'); |
| 127 | + setTimeout(() => toastToShow.classList.add('hidden'), 5000); |
| 128 | + } |
| 129 | + } catch (err) { |
| 130 | + console.error(err); |
| 131 | + alert('Submission failed—please try again later.'); |
| 132 | + } finally { |
| 133 | + submitBtn.disabled = false; |
| 134 | + } |
| 135 | + }); |
| 136 | +
|
| 137 | + window.closeForm = ev => { |
| 138 | + ev.preventDefault(); |
| 139 | + form.classList.add('hidden'); |
| 140 | + }; |
118 | 141 | }); |
119 | 142 | })(); |
120 | 143 | </script> |
0 commit comments