1/** 2 * Floating feedback button with modal 3 * Allows users to submit feedback which is stored on the server 4 */ 5 6(function() { 7 'use strict'; 8 9 // Create expandable FAB with tour and feedback options 10 function createExpandableFAB() { 11 const fabContainer = document.createElement('div'); 12 fabContainer.className = 'help-fab-container'; 13 fabContainer.innerHTML = ` 14 <!-- Expanded menu (hidden by default) --> 15 <div class="fab-menu" id="fab-menu"> 16 <div class="fab-menu-item" data-action="tour"> 17 <span class="fab-icon">ð¯</span> 18 <span class="fab-label">Take Tour</span> 19 </div> 20 <div class="fab-menu-item" data-action="feedback"> 21 <span class="fab-icon">ð¬</span> 22 <span class="fab-label">Send Feedback</span> 23 </div> 24 </div> 25 26 <!-- Main FAB button --> 27 <button class="help-fab" id="help-fab" title="Help & Feedback" aria-label="Help and Feedback Menu"> 28 <svg class="fab-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 29 <circle cx="12" cy="12" r="10"></circle> 30 <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path> 31 <line x1="12" y1="17" x2="12.01" y2="17"></line> 32 </svg> 33 </button> 34 `; 35 36 document.body.appendChild(fabContainer); 37 attachFABListeners(); 38 } 39 40 // Attach event listeners to FAB 41 function attachFABListeners() { 42 const fabButton = document.getElementById('help-fab'); 43 const fabMenu = document.getElementById('fab-menu'); 44 const menuItems = document.querySelectorAll('.fab-menu-item'); 45 46 // Toggle menu on FAB click 47 fabButton.addEventListener('click', (e) => { 48 e.stopPropagation(); 49 toggleFABMenu(); 50 }); 51 52 // Handle menu item clicks 53 menuItems.forEach(item => { 54 item.addEventListener('click', (e) => { 55 e.stopPropagation(); 56 const action = item.getAttribute('data-action'); 57 58 // Close menu 59 closeFABMenu(); 60 61 // Execute action 62 if (action === 'tour') { 63 if (window.startTour) { 64 window.startTour(); 65 } 66 } else if (action === 'feedback') { 67 openFeedbackModal(); 68 } 69 }); 70 }); 71 72 // Close menu when clicking outside 73 document.addEventListener('click', (e) => { 74 if (!e.target.closest('.help-fab-container')) { 75 closeFABMenu(); 76 } 77 }); 78 79 // Close menu on Escape key 80 document.addEventListener('keydown', (e) => { 81 if (e.key === 'Escape') { 82 closeFABMenu(); 83 } 84 }); 85 } 86 87 // Toggle FAB menu open/closed 88 function toggleFABMenu() { 89 const menu = document.getElementById('fab-menu'); 90 menu.classList.toggle('open'); 91 } 92 93 // Close FAB menu 94 function closeFABMenu() { 95 const menu = document.getElementById('fab-menu'); 96 if (menu) { 97 menu.classList.remove('open'); 98 } 99 } 100 101 // Create feedback modal 102 function createFeedbackModal() { 103 const modal = document.createElement('div'); 104 modal.id = 'feedback-modal'; 105 modal.className = 'feedback-modal'; 106 modal.innerHTML = ` 107 <div class="feedback-modal-content"> 108 <div class="feedback-modal-header"> 109 <h3>Send Feedback</h3> 110 <button class="feedback-close" onclick="window.closeFeedbackModal()">×</button> 111 </div> 112 <div class="feedback-modal-body"> 113 <p>Help us improve! Share your thoughts, suggestions, or report issues.</p> 114 <textarea 115 id="feedback-textarea" 116 placeholder="Enter your feedback here..." 117 rows="6" 118 maxlength="1000"></textarea> 119 <div class="feedback-char-count">
120 <span id="feedback-char-counter">0</span> / 1000 characters 121 </div> 122 </div> 123 <div class="feedback-modal-footer"> 124 <button class="feedback-btn feedback-btn-cancel" onclick="window.closeFeedbackModal()">Cancel</button> 125 <button class="feedback-btn feedback-btn-submit" onclick="window.submitFeedback()">Submit</button> 126 </div> 127 <div id="feedback-status" class="feedback-status"></div> 128 </div> 129 `; 130 document.body.appendChild(modal); 131 132 // Character counter 133 const textarea = modal.querySelector('#feedback-textarea'); 134 const counter = modal.querySelector('#feedback-char-counter'); 135 textarea.addEventListener('input', function() { 136 counter.textContent = textarea.value.length; 137 }); 138 139 // Close modal on outside click 140 modal.addEventListener('click', function(e) { 141 if (e.target === modal) { 142 closeFeedbackModal(); 143 } 144 }); 145 146 // Close modal on Escape key 147 document.addEventListener('keydown', function(e) { 148 if (e.key === 'Escape' && modal.classList.contains('show')) { 149 closeFeedbackModal(); 150 } 151 }); 152 } 153 154 // Open modal 155 function openFeedbackModal() { 156 const modal = document.getElementById('feedback-modal'); 157 if (!modal) { 158 createFeedbackModal(); 159 } 160 setTimeout(() => { 161 document.getElementById('feedback-modal').classList.add('show'); 162 document.getElementById('feedback-textarea').focus(); 163 }, 10); 164 } 165 166 // Close modal 167 function closeFeedbackModal() { 168 const modal = document.getElementById('feedback-modal'); 169 if (modal) { 170 modal.classList.remove('show'); 171 // Clear textarea and status after animation 172 setTimeout(() => { 173 document.getElementById('feedback-textarea').value = ''; 174 document.getElementById('feedback-char-counter').textContent = '0'; 175 document.getElementById('feedback-status').textContent = ''; 176 document.getElementById('feedback-status').className = 'feedback-status'; 177 }, 300); 178 } 179 } 180 181 // Submit feedback 182 function submitFeedback() { 183 const textarea = document.getElementById('feedback-textarea'); 184 const statusDiv = document.getElementById('feedback-status'); 185 const submitBtn = document.querySelector('.feedback-btn-submit'); 186 187 const feedback = textarea.value.trim(); 188 189 // Validation 190 if (!feedback) { 191 statusDiv.textContent = 'â ï¸ Please enter your feedback before submitting.'; 192 statusDiv.className = 'feedback-status error'; 193 return; 194 } 195 196 if (feedback.length < 10) { 197 statusDiv.textContent = 'â ï¸ Feedback must be at least 10 characters.'; 198 statusDiv.className = 'feedback-status error'; 199 return; 200 } 201 202 // Disable button during submission 203 submitBtn.disabled = true; 204 submitBtn.textContent = 'Sending...'; 205 statusDiv.textContent = 'ð¤ Sending feedback...'; 206 statusDiv.className = 'feedback-status info'; 207 208 // Gather metadata 209 const metadata = { 210 feedback: feedback, 211 url: window.location.href, 212 pathname: window.location.pathname, 213 timestamp: new Date().toISOString(), 214 userAgent: navigator.userAgent, 215 screenResolution: `${window.screen.width}x${window.screen.height}`, 216 viewportSize: `${window.innerWidth}x${window.innerHeight}` 217 }; 218 219 // Send to server 220 fetch('/api/feedback', { 221 method: 'POST', 222 headers: { 223 'Content-Type': 'application/json', 224 }, 225 body: JSON.stringify(metadata) 226 }) 227 .then(response => { 228 if (!response.ok) { 229 throw new Error('Network response was not ok'); 230 } 231 return response.json(); 232 }) 233 .then(data => { 234 statusDiv.textContent = 'â Thank you! Your feedback has been submitted.'; 235 statusDiv.className = 'feedback-status success'; 236 submitBtn.disabled = false;
237 submitBtn.textContent = 'Submit'; 238 239 // Close modal after 2 seconds 240 setTimeout(() => { 241 closeFeedbackModal(); 242 }, 2000); 243 }) 244 .catch(error => { 245 console.error('Feedback submission error:', error); 246 statusDiv.textContent = 'â Failed to submit feedback. Please try again.'; 247 statusDiv.className = 'feedback-status error'; 248 submitBtn.disabled = false; 249 submitBtn.textContent = 'Submit'; 250 }); 251 } 252 253 // Expose functions globally for onclick handlers 254 window.openFeedbackModal = openFeedbackModal; 255 window.closeFeedbackModal = closeFeedbackModal; 256 window.submitFeedback = submitFeedback; 257 258 // Initialize when DOM is ready 259 if (document.readyState === 'loading') { 260 document.addEventListener('DOMContentLoaded', createExpandableFAB); 261 } else { 262 createExpandableFAB(); 263 } 264 265})();
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.