1let survey = null; 2let formUuid = null; 3 4async function saveForm(sender) { 5 const url = `${window.NUCLEUS_API}/websites/${window.SITE_ID}/pages/${window.PAGE_ID}/form/${formUuid}`; 6 7 try { 8 const response = await window.axios.post(url, survey.data, { 9 headers: { 'Content-Type': 'application/json' } 10 }); 11 12 return { success: true }; 13 } catch (error) { 14 if (error?.response?.data?.failed_recaptchas) { 15 return { success: false, failed_recaptchas: error.response.data.failed_recaptchas }; 16 } 17 18 if (error?.response?.data?.message) { 19 return { success: false, message: error.response.data.message }; 20 } 21 22 return { success: false }; 23 } 24} 25 26async function getForm() { 27 const url = `${window.NUCLEUS_API}/websites/${window.SITE_ID}/pages/${window.PAGE_ID}/form`; 28 29 try { 30 const response = await window.axios.get(url); 31 32 formUuid = response.data.uuid; 33 34 let successMsg = response.data.success_message; 35 const submitButtonText = response.data.submit_button_text; 36 const pageNextText = response.data.next_button_text; 37 const pagePrevText = response.data.back_button_text; 38 const redirectUrl = response.data.redirect_url; 39 40 if (redirectUrl) { 41 successMsg = '<b style="font-size:30px;">' + successMsg + '</b><br><br><b style="font-size:24px;">You will be redirected shortly</b>'; 42 } 43 44 const formJson = { 45 completeText: submitButtonText, 46 pagePrevText: pagePrevText, 47 pageNextText: pageNextText, 48 pages: response.data.pages, 49 }; 50 51 let stateFieldName = ''; 52 53 response.data.pages.forEach(page => { 54 const field = page.elements.find(field => 55 field.title && field.title.trim().toLowerCase() === 'state' 56 ); 57 58 if (field) { 59 stateFieldName = field.name; 60 } 61 }); 62 63 // set widths for each input 64 formJson.pages.forEach((page) => { 65 const elements = page.elements; 66 elements.forEach((element) => { 67 element.startWithNewLine = false; 68 const width = element?.widthColumns; 69 if (width !== 12) { 70 if (width === 9) { 71 element.width = '75%'; 72 } else if (width === 6) { 73 element.width = '50%'; 74 } else if (width === 3) { 75 element.width = '25%'; 76 } else { 77 element.width = '100%'; 78 } 79 } else { 80 element.width = '100%'; 81 } 82 83 if (stateFieldName && (element.type === 'county-dropdown' || element.title === 'County Dropdown')) { 84 element.choicesByUrl = { 85 "url": window.NUCLEUS_API + "/autocomplete/counties?states={" + stateFieldName + "}", 86 "path": "data", 87 "valueName": "id", 88 "titleName": "county_name" 89 }; 90 } 91 }); 92 }); 93 94 survey = new Survey.Model(formJson); 95 96 survey.completedHtml = successMsg; 97 98 survey.onCurrentPageChanged.add(function (sender, options) { 99 const currentPage = sender.currentPage; 100 101 for (const question of currentPage.questions) { 102 if (question.jsonObj.isRecaptcha) { 103 const token = sender.getValue(question.name + '-recaptchaToken'); 104 105 if (token) { 106 question.html = '<div style="color:#2e7d32;background-color:#e8f5e9;padding:8px 12px;border-radius:4px;margin-top:8px;font-size:0.9em;border:1px solid #2e7d32;">Recaptcha verified</div>'; 107 } 108 } 109 } 110 }); 111 112 survey.onAfterRenderQuestion.add(function (sender, options) { 113 const question = sender.getQuestionByName(options.question.name); 114 const isRecaptcha = question.jsonObj.isRecaptcha; 115 116 if (isRecaptcha) { 117 const verified = /recaptcha verified/i.test(question.html); 118 119 if (! verified) { 120 const waitForGrecaptcha = () => { 121 if (typeof grecaptcha !== 'undefined' && grecaptcha.render) { 122 grecaptcha.render(question.name + '-recaptcha', { 123 sitekey: window.RECAPTCHA_TENANTS_SITE_KEY, 124 callback: function (token) { 125 sender.setValue(question.name + '-recaptchaToken', token); 126 options.question.value = token; 127 } 128 }); 129 130 clearTimeout(waitForGrecaptcha); 131 } else { 132 setTimeout(waitForGrecaptcha, 100); 133 } 134 }; 135 136 waitForGrecaptcha(); 137 } 138 } 139 140 const isDisclaimer = question.jsonObj.isDisclaimer; 141 142 if (isDisclaimer) { 143 const pTags = options.htmlElement.querySelectorAll('p'); 144 145 for (const p of pTags) { 146 p.classList.add("!tw-text-[11px]", "!tw-text-gray-400", "!tw-leading-tight"); 147 148 const aTags = p.querySelectorAll('a'); 149 150 for (const a of aTags) { 151 a.classList.add("!tw-text-["+window.THEME_COLORS_PRIMARY+"]"); 152 } 153 } 154 } 155 }); 156 157 // set classes & theme colors 158 function hexToRGBA(hex, opacity = 1) { 159 let h = hex.slice(hex.startsWith("#") ? 1 : 0); 160 if (h.length === 3) { 161 h = [...h].map(x => x + x).join(""); 162 } 163 const rgb = parseInt(h, 16); 164 const r = (rgb >>> 16) & 255; 165 const g = (rgb >>> 8) & 255; 166 const b = rgb & 255; 167 return `rgba(${r}, ${g}, ${b}, ${opacity})`; 168 }; 169 170 survey.applyTheme({ 171 "cssVariables": { 172 "--primary": window.THEME_COLORS_PRIMARY, 173 "--sjs-primary-backcolor": window.THEME_COLORS_PRIMARY, 174 "--sjs-primary-backcolor-light": hexToRGBA(window.THEME_COLORS_PRIMARY,.1) 175 }, 176 "isPanelless": true 177 }); 178
179 survey.onUpdateQuestionCssClasses.add(function(sender, options) { 180 const classes = options.cssClasses; 181 const type = options.question.getType(); 182 if (type === "text" || type === "email" || type === "number" || type === "comment") { 183 classes.root = "tw-placeholder:text-gray-400 tw-block tw-w-full tw-rounded-md tw-border-none tw-bg-white tw-px-3.5 tw-py-2 tw-text-base tw-text-gray-900 tw-outline tw-outline-1 tw--outline-offset-1 tw-outline-gray-300 focus:tw-outline focus:tw-outline-2 focus:tw--outline-offset-2 focus:tw-outline-[var(--color-primary)]"; 184 } 185 if (type === "dropdown") { 186 classes.control += " tw-pl-3.5 tw-outline tw-outline-2 tw--outline-offset-1 tw-outline-transparent focus-within:tw--outline-offset-2 focus-within:tw-outline-[var(--color-primary)] tw-h-[40px] !tw-shadow-none tw-border tw-border-solid tw-border-gray-300 tw-rounded-md tw-bg-white"; 187 classes.controlValue += " !tw-m-0 tw-flex tw-items-center"; 188 } 189 if (type === "file") { 190 classes.fileDecorator += " tw-rounded-lg"; 191 } 192 if (type === "signaturepad") { 193 classes.root += " tw-rounded-lg"; 194 } 195 if (type === "checkbox" || type === "radiogroup") { 196 classes.materialDecorator += " tw-bg-white tw-shadow-none tw-border tw-border-solid tw-border-gray-300"; 197 classes.item += " tw-px-0.5 tw-py-1.5"; 198 } 199 if (type === "boolean") { 200 classes.checkboxMaterialDecorator += " tw-bg-white tw-shadow-none tw-border tw-border-solid tw-border-gray-300"; 201 classes.content += " tw-px-0.5 tw-py-1.5"; 202 } 203 204 if (type === 'html' && options.question.jsonObj.isDisclaimer) { 205 classes.root += ' sm:tw--mt-8 tw--mt-4'; 206 } 207 208 classes.mainRoot = "tw-w-full tw-p-0 tw-bg-transparent tw-shadow-none tw-border-none tw-outline-none"; 209 classes.header = "tw-p-0 tw-m-0 tw-mb-2.5"; 210 classes.title = "tw-block !tw-text-sm/6 !tw-font-semibold !tw-text-gray-900"; 211 }); 212 213 survey.render(document.getElementById('nucleus-form-container')); 214 215 let hasCompleted = false; 216 survey.onCompleting.add(function(sender, options) { 217 if (hasCompleted) { 218 return; 219 } 220 221 options.allowComplete = false; 222 223 // Clear any previous global form error message. 224 const previousError = document.getElementById('nucleus-form-error'); 225 226 if (previousError && previousError.parentNode) { 227 previousError.parentNode.removeChild(previousError); 228 } 229 230 saveForm(sender).then((result) => { 231 if (result.success) { 232 hasCompleted = true; 233 options.allowComplete = true; 234 sender.doComplete(); 235 } else if (result.failed_recaptchas) { 236 options.allowComplete = false; 237 238 for (const recaptcha of result.failed_recaptchas) { 239 const question = sender.getQuestionByName(recaptcha); 240 241 if (question) { 242 question.clearErrors(); 243 244 question.addError('Invalid recaptcha provided'); 245 246 const page = sender.pages.findIndex(p => 247 p.elements.some(q => q.name === recaptcha) 248 ); 249 250 if (page !== -1) { 251 sender.currentPageNo = page; 252 } 253 } 254 } 255 } else if (result.message) { 256 // Error handling. 257 258 // I've put 'allow' here and not 'allowComplete' because 259 // 'allowComplete' is deprecated, according to survey-code docs. 260 options.allow = false;
261 262 const container = document.getElementById('nucleus-form-container'); 263 264 if (container) { 265 const div = document.createElement('div'); 266 267 div.id = 'nucleus-form-error'; 268 div.className = 'tw-text-red-600 tw-mt-3 tw-mb-2 tw-text-sm tw-w-full tw-basis-full tw-block'; 269 div.textContent = result.message; 270 271 // Always place the error directly below the Submit button. 272 // Use a strict selector so we don't match other UI buttons (e.g., dropdown toggles). 273 const submitButton = container.querySelector('button[data-name="complete"], button.sd-navigation__complete-btn'); 274 275 if (submitButton) { 276 // Prefer placing the error after the Submit button's navigation container 277 // to ensure it renders on its own line below the button in flex layouts. 278 const navContainer = submitButton.closest('.sd-action-bar, .sd-navigation, .sd-footer, .sd-footer__navigation'); 279 const pointOfReference = navContainer ?? submitButton; 280 pointOfReference.insertAdjacentElement('afterend', div); 281 } else { 282 // Fallback: keep it inside the form container at the bottom. 283 container.appendChild(div); 284 } 285 } 286 } 287 }); 288 }); 289 290 survey.onCompleting.add(function(sender, options) { 291 sender.getAllQuestions().forEach((q) => { 292 if (q.jsonObj.isRecaptcha) { 293 const token = sender.getValue(q.name + '-recaptchaToken'); 294 295 if (! token) { 296 q.clearErrors(); 297 options.allowComplete = false; 298 q.addError('Invalid recaptcha provided'); 299 } 300 } 301 }); 302 }); 303 304 survey.onValidateQuestion.add(function(sender, options) { 305 const question = sender.getQuestionByName(options.name); 306 307 if (question) { 308 if (question.isJson && options.value !== undefined) { 309 try { 310 JSON.parse(options.value); 311 } catch (e) { 312 options.error = 'Please enter valid JSON'; 313 } 314 } 315 316 if (question.getType() === "boolean" && ! question.value) { 317 options.error = 'You must accept the terms and conditions'; 318 } 319 } 320 }); 321 322 survey.onValueChanged.add(function(sender, options) { 323 const question = sender.getQuestionByName(options.name); 324 325 if (question && question.jsonObj.isRecaptcha) { 326 question.clearErrors(); 327 } 328 329 const match = options.name.match(/^(.*)-(date|time)$/); 330 331 if (! match) { 332 return; 333 } 334 335 const field = match[1]; 336 const type = match[2]; 337 338 const date = sender.getValue(field + '-date'); 339 const time = sender.getValue(field + '-time'); 340 341 let datetime = null; 342 343 if (date) { 344 if (time) { 345 datetime = `${date} ${time}:00`; 346 } else { 347 datetime = `${date}`; 348 } 349 350 sender.setValue(field, datetime); 351 } 352 }); 353 354 survey.onComplete.add(function (sender, options) { 355 sender.getAllQuestions().forEach((q) => { 356 if (q.inputType === 'date' || q.inputType === 'time') { 357 delete sender.data[q.name]; 358 } 359 }); 360 361 if (redirectUrl) { 362 setTimeout(() => { 363 window.location.href = redirectUrl; 364 }, 2000); 365 } 366 }); 367 368 survey.onServerValidateQuestions.add(async function (sender, options) { 369 for (const page of sender.pages) { 370 for (const element of page.elements) { 371 if (element.isDateTime) { 372 const date = sender.getValue(element.name + '-date'); 373 const time = sender.getValue(element.name + '-time'); 374 375 if (date) { 376 let datetime = null; 377 378 if (time) { 379 datetime = `${date} ${time}:00`; 380 } else { 381 datetime = `${date}`; 382 } 383 384 const result = await checkUnique([datetime, element.handle, element.name, true]); 385 if (! result) { 386 const question = sender.getQuestionByName(element.name + '-date'); 387 388 if (question) { 389 question.clearErrors(); 390 391 if (time) { 392 options.errors[element.name + '-date'] = 'The date and time is already taken'; 393 } else { 394 options.errors[element.name + '-date'] = 'The date is already taken'; 395 } 396 397 const page = sender.pages.findIndex(p => p.elements.some(q => q.name === element.name + '-date')); 398 399 if (page !== -1) { 400 setTimeout(() => { 401 sender.currentPageNo = page; 402 }, 0); 403 } 404 } 405 } 406 } 407 } 408 } 409 } 410 411 options.complete(); 412 }); 413 } catch (error) { 414 console.error(error); 415 } 416} 417 418async function checkUnique([value, handle, hash, isDateTime = false]) { 419 if (value === undefined) { 420 return; 421 } 422 423 const url = `${window.NUCLEUS_API}/websites/${window.SITE_ID}/pages/${window.PAGE_ID}/form/${formUuid}/unique`; 424 425 try { 426 const response = await window.axios.post(url, { value, handle, hash }); 427 428 if (isDateTime) { 429 return response.data; 430 } else { 431 this.returnResult(response.data); 432 } 433 } catch (error) { 434 console.error(error); 435 } 436} 437 438Survey.FunctionFactory.Instance.register('checkUnique', checkUnique, true); 439 440Survey.Serializer.addProperty('comment', { 441 name: 'isJson:boolean', 442 default: false 443}); 444 445Survey.Serializer.addProperty('comment', { 446 name: 'isRecaptcha:boolean', 447 default: false 448}); 449 450Survey.Serializer.addProperty('text', { 451 name: 'isDateTime:boolean', 452 default: false 453}); 454 455Survey.Serializer.addProperty('html', { 456 name: 'isDisclaimer:boolean', 457 default: false 458}); 459 460Survey.Serializer.addProperty('text', { 461 name: 'handle:string', 462 default: '' 463}); 464 465Survey.Serializer.addProperty('question', { 466 name: 'widthColumns:number', 467 default: 6 468}); 469 470Survey.Serializer.addProperty('question', { 471 name: 'labelLocation:string', 472 default: 'floating' 473}); 474 475document.addEventListener('DOMContentLoaded', function () { 476 getForm(); 477});
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.