1/** 2 * Forminator Form Submission Listener 3 * Listens for form submissions and logs form ID to console 4 */ 5 6(function() { 7 'use strict'; 8 9 // Score mapping for legacy NIST-style Likert assessment (5 pts per question) 10 const scoreMapping = { 11 "Never": 1, 12 "Rarely": 2, 13 "Sometimes": 3, 14 "Mostly": 4, 15 "Comprehensively": 5, 16 }; 17 18 /** RSA GenAI assessment (quiz.json): option slug â points */ 19 const rsaGenaiOptionScores = { 20 'q1-yes': 0, 21 'q1-yes-stripped': 2, 22 'q1-yes-approved': 2.5, 23 'q1-never': 2.5, 24 'q2-aware': 2.5, 25 'q2-general-idea': 1.5, 26 'q2-assume-public': 0, 27 'q2-no': 0, 28 'q3-complete': 2, 29 'q3-none': 0, 30 'q3-unsure': 1, 31 'q4-allow': 0, 32 'q4-check-allow': 1, 33 'q4-deny-write': 1.5, 34 'q5-minimal': 0.5, 35 'q5-mild': 1, 36 'q5-significant': 1.5, 37 }; 38 39 const RSA_GENAI_MAX_TOTAL = 10; 40 41 function isRsaGenaiAssessmentForm(form) { 42 if (!form || !form.querySelector) return false; 43 return Boolean(form.querySelector('input[type="radio"][value^="q1-"]')); 44 } 45 46 function getRsaGenaiQuestionMaxScore(questionText) { 47 if (!questionText) return 5; 48 const rules = [ 49 ['pasted internal data into a GenAI', 2.5], 50 ['official policy regarding which Generative AI tools', 2.5], 51 ['How much visibility do you believe your security and IT teams', 2], 52 ['great GenAI task tracking app', 1.5], 53 ['disabled public LLM access tomorrow', 1.5], 54 ]; 55 for (let i = 0; i < rules.length; i++) { 56 if (questionText.includes(rules[i][0])) return rules[i][1]; 57 } 58 return 5; 59 } 60 61 function resolveRadioScore(input, selectedLabel) { 62 const value = input && input.value ? String(input.value) : ''; 63 if (Object.prototype.hasOwnProperty.call(rsaGenaiOptionScores, value)) { 64 return rsaGenaiOptionScores[value]; 65 } 66 const numeric = parseFloat(value); 67 if (value !== '' && !Number.isNaN(numeric) && /^-?[0-9]+(\.[0-9]+)?$/.test(value.trim())) { 68 return numeric; 69 } 70 if (selectedLabel) { 71 for (const [option, pts] of Object.entries(scoreMapping)) { 72 if (selectedLabel.includes(option)) return pts; 73 } 74 } 75 const fallback = parseFloat(value); 76 return Number.isNaN(fallback) ? 1 : fallback; 77 } 78 79 // Hide the hero/left column in the Marketo gated block 80 function hideMarketoHeroColumn(form) { 81 try { 82 const marketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated'); 83 if (!marketoBlock) return; 84 const columnsContainer = marketoBlock.closest('.wp-block-columns'); 85 if (!columnsContainer) return; 86 const firstColumn = columnsContainer.querySelector('.wp-block-column:first-child'); 87 if (firstColumn && firstColumn.style.display !== 'none') { 88 firstColumn.style.display = 'none'; 89 } 90 } catch (_) {} 91 } 92 93 // After submission, hide hero when the container resizes next 94 function installHideHeroOnResize(form) { 95 try { 96 const marketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated'); 97 if (!marketoBlock) return; 98 const columnsContainer = marketoBlock.closest('.wp-block-columns'); 99 if (!columnsContainer) return; 100 101 if (form._vzHideHeroObserver) return; // already installed 102 103 const observer = new ResizeObserver(() => { 104 if (form.dataset.hideHeroOnResize === 'true') { 105 hideMarketoHeroColumn(form); 106 observer.disconnect(); 107 delete form._vzHideHeroObserver; 108 } 109 }); 110 observer.observe(columnsContainer); 111 form._vzHideHeroObserver = observer; 112 } catch (_) {} 113 } 114 115 // Function to capture form data before rows are hidden 116 function captureFormData(form) { 117 const formData = { 118 selectedInputs: [], 119 questionGroups: new Map(), 120 maxTotalScore: 0 121 }; 122
123 const rsaGenaiForm = isRsaGenaiAssessmentForm(form); 124 125 // Get all radio inputs to calculate max possible scores 126 const allRadioInputs = form.querySelectorAll('input[type="radio"]'); 127 const selectedInputs = form.querySelectorAll('input[type="radio"]:checked'); 128 129 console.log('Form structure debug:'); 130 console.log('Total radio inputs found:', allRadioInputs.length); 131 console.log('Selected radio inputs found:', selectedInputs.length); 132 console.log('Form HTML:', form.outerHTML); 133 134 // Debug: Log each radio input 135 allRadioInputs.forEach((input, index) => { 136 console.log(`Radio input ${index}:`, { 137 checked: input.checked, 138 value: input.value, 139 name: input.name, 140 parentLabel: input.closest('label')?.textContent?.trim(), 141 questionRow: input.closest('.forminator-row')?.outerHTML 142 }); 143 }); 144 145 // Group questions by their container structure with proper pillar mapping 146 allRadioInputs.forEach(input => { 147 const questionRow = input.closest('.forminator-row'); 148 if (questionRow) { 149 // Try to get the actual question text from the forminator label 150 const questionText = questionRow.querySelector('.forminator-label')?.textContent?.trim() || 151 questionRow.querySelector('.forminator-field-radio .forminator-field-label')?.textContent?.trim() || 152 questionRow.querySelector('.forminator-field-label')?.textContent?.trim() || 153 questionRow.querySelector('label')?.textContent?.trim(); 154 155 if (questionText) { 156 // Map questions to their correct pillars based on the original HTML structure 157 let pillar = 'Identity Security'; 158 let subArea = 'General'; 159 if (rsaGenaiForm) { 160 pillar = 'GenAI Security'; 161 subArea = 'Assessment'; 162 } else if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?') || 163 questionText.includes('usage-based data') || 164 questionText.includes('anomalies in access usage') || 165 questionText.includes('refine and optimize roles') || 166 questionText.includes('simulate attack paths') || 167 questionText.includes('prioritizing critical access risk')) { 168 pillar = 'Intelligence & Analytics'; 169 if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?')) { 170 subArea = 'Access Dashboards'; 171 } else if (questionText.includes('usage-based') || questionText.includes('anomalies')) { 172 subArea = 'Access Intelligence'; 173 } else if (questionText.includes('refine and optimize roles')) { 174 subArea = 'Intelligent Role Optimization'; 175 } else if (questionText.includes('simulate attack paths')) { 176 subArea = 'Proactive Risk Modeling'; 177 } else if (questionText.includes('prioritizing critical access risk')) { 178 subArea = 'Risk Prioritization'; 179 } 180 } else if (questionText.includes('inventory of all identities') || 181 questionText.includes('dormant accounts') || 182 questionText.includes('emergency/break glass accounts') || 183 questionText.includes('permission inheritance') || 184 questionText.includes('service accounts and system accounts') || 185 questionText.includes('business purpose and risk justification') || 186 questionText.includes('toxic combinations') || 187 questionText.includes('enterprise-wide permission catalog') || 188 questionText.includes('cross-platform permission relationships')) { 189 pillar = 'Visibility'; 190 if (questionText.includes('inventory') || questionText.includes('dormant') || questionText.includes('emergency')) { 191 subArea = 'Identity Hygiene'; 192 } else if (questionText.includes('permission inheritance') || questionText.includes('service accounts')) { 193 subArea = 'Access Graph'; 194 } else if (questionText.includes('business purpose') || questionText.includes('toxic combinations')) { 195 subArea = 'Priority Access'; 196 } else if (questionText.includes('enterprise-wide') || questionText.includes('cross-platform')) { 197 subArea = 'Comprehensive Mapping'; 198 } 199 } else if (questionText.includes('real-time alerts') || 200 questionText.includes('continuously monitor') || 201 questionText.includes('correlate access events') || 202 questionText.includes('blast radius') || 203 questionText.includes('compliance requirements in real time')) { 204 pillar = 'Monitoring & Orchestration'; 205 if (questionText.includes('real-time alerts')) { 206 subArea = 'Privilege Elevation Alerts'; 207 } else if (questionText.includes('continuously monitor')) { 208 subArea = 'Access Monitoring'; 209 } else if (questionText.includes('correlate access events')) { 210 subArea = 'Cross-Platform Correlation'; 211 } else if (questionText.includes('blast radius')) { 212 subArea = 'Blast Radius'; 213 } else if (questionText.includes('compliance requirements')) { 214 subArea = 'Compliance Monitoring'; 215 } 216 } else if (questionText.includes('periodic access reviews') || 217 questionText.includes('usage-based and risk-prioritized') || 218 questionText.includes('self-service requests') || 219 questionText.includes('onboarding, transfers, and offboarding') || 220 questionText.includes('automated triggers to remove')) { 221 pillar = 'Workflows'; 222 if (questionText.includes('periodic access reviews') || questionText.includes('usage-based and risk-prioritized')) {
223 subArea = 'User Access Reviews'; 224 } else if (questionText.includes('self-service requests')) { 225 subArea = 'Self-Service Access'; 226 } else if (questionText.includes('onboarding, transfers, and offboarding')) { 227 subArea = 'Provisioning/Deprovisioning'; 228 } else if (questionText.includes('automated triggers to remove')) { 229 subArea = 'Automated Remediation'; 230 } 231 } else if (questionText.includes('report on access certifications') || 232 questionText.includes('unified privilege elevation controls') || 233 questionText.includes('Just-in-Time (JIT) access') || 234 questionText.includes('MFA coverage for privilege accounts') || 235 questionText.includes('role composition and SoC conflicts') || 236 questionText.includes('control effectiveness for audits')) { 237 pillar = 'Controls'; 238 if (questionText.includes('report on access certifications')) { 239 subArea = 'Compliance Reporting'; 240 } else if (questionText.includes('unified privilege elevation controls')) { 241 subArea = 'Privileged Access Assurance'; 242 } else if (questionText.includes('Just-in-Time (JIT) access')) { 243 subArea = 'JIT Access'; 244 } else if (questionText.includes('MFA coverage')) { 245 subArea = 'MFA'; 246 } else if (questionText.includes('role composition and SoC conflicts')) { 247 subArea = 'Role Assignment'; 248 } else if (questionText.includes('control effectiveness for audits')) { 249 subArea = 'Compliance Assurance'; 250 } 251 } 252 253 if (!formData.questionGroups.has(pillar)) { 254 formData.questionGroups.set(pillar, []); 255 } 256 257 // Check if this question is already added to avoid duplicates 258 const existingQuestions = formData.questionGroups.get(pillar); 259 const questionExists = existingQuestions.some(q => q.text === questionText); 260 261 if (!questionExists) { 262 existingQuestions.push({ 263 text: questionText, 264 maxScore: rsaGenaiForm ? getRsaGenaiQuestionMaxScore(questionText) : 5, 265 subArea: subArea 266 }); 267 console.log(`Added question to ${pillar}: "${questionText}"`); 268 } else { 269 console.log(`Question already exists in ${pillar}: "${questionText}"`); 270 } 271 } 272 } 273 }); 274 275 // Debug: Log what we found in question groups 276 console.log('Question groups found:', formData.questionGroups); 277 console.log('Question groups size:', formData.questionGroups.size); 278 console.log('Question groups type:', typeof formData.questionGroups); 279 console.log('Question groups constructor:', formData.questionGroups.constructor.name); 280 281 // Test Map functionality 282 console.log('Testing Map functionality:'); 283 console.log('formData.questionGroups instanceof Map:', formData.questionGroups instanceof Map); 284 console.log('formData.questionGroups.has("Visibility"):', formData.questionGroups.has("Visibility")); 285 console.log('formData.questionGroups.get("Visibility"):', formData.questionGroups.get("Visibility")); 286 287 // Debug: Log each question and its mapped pillar 288 if (formData.questionGroups && typeof formData.questionGroups.forEach === 'function') { 289 formData.questionGroups.forEach((questions, pillar) => { 290 console.log(`Pillar: ${pillar}`); 291 questions.forEach(q => { 292 console.log(` - ${q.text} (${q.subArea})`); 293 }); 294 }); 295 } else { 296 console.log('Question groups is not iterable or empty'); 297 } 298 299 // Capture selected responses
300 selectedInputs.forEach(input => { 301 const questionRow = input.closest('.forminator-row'); 302 if (questionRow) { 303 // Try to get the actual question text from the forminator label 304 const questionText = questionRow.querySelector('.forminator-label')?.textContent?.trim() || 305 questionRow.querySelector('.forminator-field-radio .forminator-field-label')?.textContent?.trim() || 306 questionRow.querySelector('.forminator-field-label')?.textContent?.trim() || 307 questionRow.querySelector('label')?.textContent?.trim(); 308 309 if (questionText) { 310 // Use the same pillar mapping logic as above 311 let pillar = 'Identity Security'; 312 let subArea = 'General'; 313 if (rsaGenaiForm) { 314 pillar = 'GenAI Security'; 315 subArea = 'Assessment'; 316 } else if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?') || 317 questionText.includes('usage-based data') || 318 questionText.includes('anomalies in access usage') || 319 questionText.includes('refine and optimize roles') || 320 questionText.includes('simulate attack paths') || 321 questionText.includes('prioritizing critical access risk')) { 322 pillar = 'Intelligence & Analytics'; 323 if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?')) { 324 subArea = 'Access Dashboards'; 325 } else if (questionText.includes('usage-based') || questionText.includes('anomalies')) { 326 subArea = 'Access Intelligence'; 327 } else if (questionText.includes('refine and optimize roles')) { 328 subArea = 'Intelligent Role Optimization'; 329 } else if (questionText.includes('simulate attack paths')) { 330 subArea = 'Proactive Risk Modeling'; 331 } else if (questionText.includes('prioritizing critical access risk')) { 332 subArea = 'Risk Prioritization'; 333 } 334 } else if (questionText.includes('inventory of all identities') || 335 questionText.includes('dormant accounts') || 336 questionText.includes('emergency/break glass accounts') || 337 questionText.includes('permission inheritance') || 338 questionText.includes('service accounts and system accounts') || 339 questionText.includes('business purpose and risk justification') || 340 questionText.includes('toxic combinations') || 341 questionText.includes('enterprise-wide permission catalog') || 342 questionText.includes('cross-platform permission relationships')) { 343 pillar = 'Visibility'; 344 if (questionText.includes('inventory') || questionText.includes('dormant') || questionText.includes('emergency')) { 345 subArea = 'Identity Hygiene'; 346 } else if (questionText.includes('permission inheritance') || questionText.includes('service accounts')) { 347 subArea = 'Access Graph'; 348 } else if (questionText.includes('business purpose') || questionText.includes('toxic combinations')) { 349 subArea = 'Priority Access'; 350 } else if (questionText.includes('enterprise-wide') || questionText.includes('cross-platform')) { 351 subArea = 'Comprehensive Mapping'; 352 } 353 } else if (questionText.includes('real-time alerts') || 354 questionText.includes('continuously monitor') || 355 questionText.includes('correlate access events') || 356 questionText.includes('blast radius') || 357 questionText.includes('compliance requirements in real time')) { 358 pillar = 'Monitoring & Orchestration'; 359 if (questionText.includes('real-time alerts')) { 360 subArea = 'Privilege Elevation Alerts'; 361 } else if (questionText.includes('continuously monitor')) { 362 subArea = 'Access Monitoring'; 363 } else if (questionText.includes('correlate access events')) { 364 subArea = 'Cross-Platform Correlation'; 365 } else if (questionText.includes('blast radius')) { 366 subArea = 'Blast Radius'; 367 } else if (questionText.includes('compliance requirements')) { 368 subArea = 'Compliance Monitoring'; 369 } 370 } else if (questionText.includes('periodic access reviews') || 371 questionText.includes('usage-based and risk-prioritized') || 372 questionText.includes('self-service requests') || 373 questionText.includes('onboarding, transfers, and offboarding') || 374 questionText.includes('automated triggers to remove')) { 375 pillar = 'Workflows'; 376 if (questionText.includes('periodic access reviews') || questionText.includes('usage-based and risk-prioritized')) {
377 subArea = 'User Access Reviews'; 378 } else if (questionText.includes('self-service requests')) { 379 subArea = 'Self-Service Access'; 380 } else if (questionText.includes('onboarding, transfers, and offboarding')) { 381 subArea = 'Provisioning/Deprovisioning'; 382 } else if (questionText.includes('automated triggers to remove')) { 383 subArea = 'Automated Remediation'; 384 } 385 } else if (questionText.includes('report on access certifications') || 386 questionText.includes('unified privilege elevation controls') || 387 questionText.includes('Just-in-Time (JIT) access') || 388 questionText.includes('MFA coverage for privilege accounts') || 389 questionText.includes('role composition and SoC conflicts') || 390 questionText.includes('control effectiveness for audits')) { 391 pillar = 'Controls'; 392 if (questionText.includes('report on access certifications')) { 393 subArea = 'Compliance Reporting'; 394 } else if (questionText.includes('unified privilege elevation controls')) { 395 subArea = 'Privileged Access Assurance'; 396 } else if (questionText.includes('Just-in-Time (JIT) access')) { 397 subArea = 'JIT Access'; 398 } else if (questionText.includes('MFA coverage')) { 399 subArea = 'MFA'; 400 } else if (questionText.includes('role composition and SoC conflicts')) { 401 subArea = 'Role Assignment'; 402 } else if (questionText.includes('control effectiveness for audits')) { 403 subArea = 'Compliance Assurance'; 404 } 405 } 406 407 const selectedLabel = input.closest('label')?.textContent?.trim(); 408 const score = resolveRadioScore(input, selectedLabel); 409 410 formData.selectedInputs.push({ 411 questionText, 412 pillar, 413 subArea, 414 score, 415 selectedLabel 416 }); 417 418 // Debug: Log the question text being captured 419 console.log(`Captured question: "${questionText}" with score ${score} (${selectedLabel})`); 420 } 421 } 422 }); 423 424 // Alternative approach: Try to get form data from FormData or form submission 425 console.log('Trying alternative form data capture...'); 426 427 // Check if there's a FormData object available 428 try { 429 const formDataObj = new FormData(form); 430 console.log('FormData entries:'); 431 for (let [key, value] of formDataObj.entries()) { 432 console.log(`${key}: ${value}`); 433 } 434 } catch (e) { 435 console.log('FormData not available:', e); 436 } 437 438 // Check if there are any hidden inputs with form data 439 const hiddenInputs = form.querySelectorAll('input[type="hidden"]'); 440 console.log('Hidden inputs found:', hiddenInputs.length); 441 hiddenInputs.forEach((input, index) => { 442 console.log(`Hidden input ${index}:`, { 443 name: input.name, 444 value: input.value 445 }); 446 }); 447 448 // Check if this is a test form or if the user actually selected options 449 if (formData.selectedInputs.length === 0) { 450 console.log('WARNING: No selected inputs found! This suggests:'); 451 console.log('1. The form was submitted without user selections'); 452 console.log('2. The form structure is different than expected'); 453 console.log('3. The form is being submitted programmatically'); 454 console.log('4. The user needs to actually select radio button options'); 455 456 // Try to find ANY form inputs that might contain data 457 const allInputs = form.querySelectorAll('input, select, textarea'); 458 console.log('All form inputs found:', allInputs.length);
459 allInputs.forEach((input, index) => { 460 console.log(`Input ${index}:`, { 461 type: input.type, 462 name: input.name, 463 value: input.value, 464 checked: input.checked, 465 tagName: input.tagName 466 }); 467 }); 468 469 // Check if this might be a different type of form 470 const formType = form.getAttribute('data-form-type') || 'unknown'; 471 console.log('Form type attribute:', formType); 472 } 473 474 // Calculate max scores 475 if (rsaGenaiForm) { 476 formData.maxTotalScore = RSA_GENAI_MAX_TOTAL; 477 } else { 478 formData.questionGroups.forEach((questions) => { 479 formData.maxTotalScore += questions.reduce((sum, q) => sum + (q.maxScore != null ? q.maxScore : 5), 0); 480 }); 481 } 482 483 // If no question groups were found, calculate based on unique questions 484 if (formData.maxTotalScore === 0) { 485 // Count unique questions by their names (q1, q2, q3, etc.) 486 const uniqueQuestions = new Set(); 487 allRadioInputs.forEach(input => { 488 if (input.name) { 489 uniqueQuestions.add(input.name); 490 } 491 }); 492 formData.maxTotalScore = uniqueQuestions.size * 5; 493 console.log(`Calculated max score from unique questions: ${uniqueQuestions.size} questions à 5 = ${formData.maxTotalScore}`); 494 } 495 496 console.log('Captured form data:', formData); 497 return formData; 498 } 499 500 // Function to calculate scores from captured form data 501 function calculateScores(form, formData) { 502 console.log('Calculating scores from captured data:', formData); 503 504 if (!formData || !formData.selectedInputs) { 505 console.log('No form data provided, returning default scores'); 506 return { 507 totalScore: 0, 508 maxTotalScore: 0, 509 pillarScores: {}, 510 maxPillarScores: {}, 511 subAreaScores: {}, 512 maxSubAreaScores: {} 513 }; 514 } 515 516 let totalScore = 0; 517 const pillarScores = {}; 518 const maxPillarScores = {}; 519 const subAreaScores = {}; 520 const maxSubAreaScores = {}; 521 522 // Calculate max scores from question groups 523 // Handle both Map objects and plain objects (from JSON parsing) 524 const perQuestionMax = q => (q.maxScore != null ? q.maxScore : 5); 525 526 if (formData.questionGroups && typeof formData.questionGroups.forEach === 'function') { 527 // It's a Map object 528 formData.questionGroups.forEach((questions, pillar) => { 529 maxPillarScores[pillar] = questions.reduce((sum, q) => sum + perQuestionMax(q), 0); 530 pillarScores[pillar] = 0; 531 532 questions.forEach(q => { 533 const subArea = q.subArea; 534 maxSubAreaScores[subArea] = (maxSubAreaScores[subArea] || 0) + perQuestionMax(q); 535 if (!subAreaScores[subArea]) subAreaScores[subArea] = 0; 536 }); 537 }); 538 } else if (formData.questionGroups && typeof formData.questionGroups === 'object') { 539 // It's a plain object (from JSON parsing) 540 Object.entries(formData.questionGroups).forEach(([pillar, questions]) => { 541 maxPillarScores[pillar] = questions.reduce((sum, q) => sum + perQuestionMax(q), 0); 542 pillarScores[pillar] = 0; 543 544 questions.forEach(q => { 545 const subArea = q.subArea; 546 maxSubAreaScores[subArea] = (maxSubAreaScores[subArea] || 0) + perQuestionMax(q); 547 if (!subAreaScores[subArea]) subAreaScores[subArea] = 0; 548 }); 549 }); 550 } else { 551 // Fallback: create default pillar structure from selected inputs 552 console.log('No question groups found, creating default structure from selected inputs'); 553 const uniquePillars = [...new Set(formData.selectedInputs.map(input => input.pillar))];
554 uniquePillars.forEach(pillar => { 555 const pillarQuestions = formData.selectedInputs.filter(input => input.pillar === pillar); 556 maxPillarScores[pillar] = pillarQuestions.length * 5; 557 pillarScores[pillar] = 0; 558 }); 559 } 560 561 // Debug: Log the pillar scores calculation 562 console.log('Max pillar scores calculated:', maxPillarScores); 563 console.log('Pillar scores initialized:', pillarScores); 564 565 // Calculate actual scores from captured responses 566 formData.selectedInputs.forEach(input => { 567 totalScore += input.score; 568 569 if (pillarScores[input.pillar] !== undefined) { 570 pillarScores[input.pillar] += input.score; 571 } else { 572 pillarScores[input.pillar] = input.score; 573 } 574 575 if (subAreaScores[input.subArea] !== undefined) { 576 subAreaScores[input.subArea] += input.score; 577 } else { 578 subAreaScores[input.subArea] = input.score; 579 } 580 }); 581 582 const maxTotal = formData.maxTotalScore > 0 ? formData.maxTotalScore : 155; 583 const result = { 584 totalScore, 585 maxTotalScore: maxTotal, 586 pillarScores, 587 maxPillarScores, 588 subAreaScores, 589 maxSubAreaScores, 590 scoreScaleMax: maxTotal, 591 }; 592 593 console.log('Calculated result:', result); 594 return result; 595 } 596 597 /** 598 * Risk bands: legacy assessment uses 0â155 scale; RSA GenAI quiz uses 0â10. 599 * @param {number} score 600 * @param {number} scaleMax 601 */ 602 function getRiskCategory(score, scaleMax) { 603 const max = scaleMax > 0 ? scaleMax : 155; 604 const isShortForm = max <= 12; 605 606 if (isShortForm) { 607 if (score >= 8) { 608 return { name: 'Adaptive', color: '#40DB35', backgroundColor: '#E9FBE8', borderColor: '#40DB35' }; 609 } 610 if (score >= 6) { 611 return { name: 'Repeatable', color: '#FFD728', backgroundColor: '#FFF8CC', borderColor: '#FFD728' }; 612 } 613 if (score >= 4) { 614 return { name: 'Risk Informed', color: '#F29025', backgroundColor: '#FFE6C7', borderColor: '#F29025' }; 615 } 616 return { name: 'Partial', color: '#F24C26', backgroundColor: '#FFE1DA', borderColor: '#F24C26' }; 617 } 618 619 if (score >= 155) { 620 return { 621 name: 'Adaptive', 622 color: '#40DB35', 623 backgroundColor: '#E9FBE8', 624 borderColor: '#40DB35', 625 }; 626 } 627 if (score >= 120) { 628 return { 629 name: 'Repeatable',
630 color: '#FFD728', 631 backgroundColor: '#FFF8CC', 632 borderColor: '#FFD728', 633 }; 634 } 635 if (score >= 80) { 636 return { 637 name: 'Risk Informed', 638 color: '#F29025', 639 backgroundColor: '#FFE6C7', 640 borderColor: '#F29025', 641 }; 642 } 643 return { 644 name: 'Partial', 645 color: '#F24C26', 646 backgroundColor: '#FFE1DA', 647 borderColor: '#F24C26', 648 }; 649 } 650 651 // Function to create and display the scoring results 652 function displayScoringResults(form, formData) { 653 console.log('Displaying scoring results for form:', form); 654 655 // Check if scoring results are already displayed to prevent duplicates 656 if (form.querySelector('.vz-scoring-results')) { 657 console.log('Scoring results already displayed, skipping'); 658 return; 659 } 660 661 const scores = calculateScores(form, formData); 662 console.log('Calculated scores:', scores); 663 664 const scaleMax = scores.scoreScaleMax != null ? scores.scoreScaleMax : scores.maxTotalScore; 665 const riskCategory = getRiskCategory(scores.totalScore, scaleMax); 666 const isShortForm = scaleMax > 0 && scaleMax <= 12; 667 const displayTotal = Math.round(scores.totalScore * 10) / 10; 668 const displayMax = scaleMax; 669 const scorePercentage = scaleMax > 0 ? Math.round((scores.totalScore / scaleMax) * 100) : 0; 670 const t = scores.totalScore; 671 const legendPartialActive = isShortForm ? t < 4 : t < 80; 672 const legendRiskInformedActive = isShortForm ? t >= 4 && t < 6 : t >= 80 && t < 120; 673 const legendRepeatableActive = isShortForm ? t >= 6 && t < 8 : t >= 120 && t < 155; 674 const legendAdaptiveActive = isShortForm ? t >= 8 : t >= 155; 675 const rangePartial = isShortForm ? '0-3.9' : '0-79'; 676 const rangeRiskInformed = isShortForm ? '4-5.9' : '80-119'; 677 const rangeRepeatable = isShortForm ? '6-7.9' : '120-154'; 678 const rangeAdaptive = isShortForm ? '8-10' : '155+'; 679 680 const resp = form.querySelector('.forminator-response-message'); 681 682 if (!resp) { 683 console.log('No response message found, cannot display scoring results'); 684 return; 685 } 686 687 // Create the scoring results HTML with dashboard-style design 688 const scoringHtml = ` 689 <div class="vz-scoring-results" style="margin-top: 2rem; padding: 0; background: #ffffff; border-radius: 1rem; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);"> 690 <!-- Header Section --> 691 <div style="padding: 2rem 2rem 1rem; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 1rem 1rem 0 0; border-bottom: 1px solid #e2e8f0;"> 692 <h3 style="font-size: 1.75rem; font-weight: 800; color: #1e293b; margin: 0 0 0.5rem; text-align: center; letter-spacing: -0.025em;"> 693 Identity Security Assessment 694 </h3> 695 <p style="font-size: 1rem; color: #64748b; margin: 0; text-align: center;"> 696 Your comprehensive security maturity evaluation 697 </p> 698 </div> 699 700 <!-- Main Score Dashboard --> 701 <div style="padding: 2rem; display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: center;"> 702 <!-- Score Display - Speedometer Style --> 703 <div style="text-align: center;"> 704 <div style="position: relative; display: inline-block; margin-bottom: 1rem;"> 705 <!-- Modern Circular Progress --> 706 <div style="position: relative; width: 200px; height: 200px; margin: 0 auto;"> 707 <!-- Background Circle --> 708 <svg width="200" height="200" style="transform: rotate(-90deg);"> 709 <circle cx="100" cy="100" r="80" 710 stroke="#f1f5f9" 711 stroke-width="8" 712 fill="none"/> 713 714 <!-- Progress Circle --> 715 <circle cx="100" cy="100" r="80" 716 stroke="${riskCategory.color}" 717 stroke-width="8" 718 fill="none" 719 stroke-linecap="round" 720 stroke-dasharray="${2 * Math.PI * 80}" 721 stroke-dashoffset="${2 * Math.PI * 80 * (1 - scorePercentage / 100)}" 722 style="transition: stroke-dashoffset 2s cubic-bezier(0.4, 0, 0.2, 1);"/> 723 </svg> 724 725 <!-- Center Content --> 726 <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;"> 727 <div style="font-size: 3rem; font-weight: 800; color: ${riskCategory.color}; line-height: 1; transition: color 0.5s ease-in-out;"> 728 ${displayTotal} 729 </div> 730 <div style="font-size: 1.125rem; color: #64748b; font-weight: 600; margin-bottom: 0.5rem;"> 731 / ${displayMax} 732 </div> 733 <div style="font-size: 1rem; color: #64748b; font-weight: 500;"> 734 ${scorePercentage}% 735 </div> 736 </div> 737 </div> 738 </div> 739 </div> 740 741 <!-- Risk Category Card --> 742 <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 1rem; padding: 1.5rem; text-align: center;"> 743 <div style="margin-bottom: 1rem;"> 744 <div style="width: 10px; height: 10px; background: ${riskCategory.color}; border-radius: 50%; display: inline-block; margin-right: 0.5rem;"></div>
745 <span style="font-size: 0.875rem; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;"> 746 Risk Maturity Level 747 </span> 748 </div> 749 <h4 style="font-size: 2rem; font-weight: 800; color: #1f2937; margin: 0 0 0.5rem; line-height: 1.1;"> 750 ${riskCategory.name} 751 </h4> 752 <p style="font-size: 0.875rem; color: #6b7280; margin: 0; line-height: 1.4;"> 753 ${riskCategory.name === 'Adaptive' ? 'Advanced security posture with continuous improvement' : 754 riskCategory.name === 'Repeatable' ? 'Strong security controls with systematic management' : 755 riskCategory.name === 'Risk Informed' ? 'Good security awareness with room for enhancement' : 756 'Basic security measures, significant improvement opportunities'} 757 </p> 758 </div> 759 </div> 760 761 <!-- Risk Level Legend --> 762 <div style="padding: 0 2rem 2rem;"> 763 <div style="background: #f8fafc; border-radius: 0.75rem; padding: 1.5rem; border: 1px solid #e2e8f0;"> 764 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;"> 765 <h5 style="font-size: 1.125rem; font-weight: 700; color: #1e293b; margin: 0;"> 766 Risk Maturity Scale 767 </h5> 768 <div style="display: flex; align-items: center; gap: 0.5rem;"> 769 <div style="width: 8px; height: 8px; background: ${riskCategory.color}; border-radius: 50%;"></div> 770 <span style="font-size: 0.875rem; color: #64748b; font-weight: 600;"> 771 Current: ${riskCategory.name} 772 </span> 773 </div> 774 </div> 775 776 <!-- Risk Level Indicators --> 777 <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem;"> 778 <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendPartialActive ? riskCategory.borderColor : '#e5e7eb'};"> 779 <div style="width: 8px; height: 8px; background: #F24C26; border-radius: 50%; margin: 0 auto 0.5rem;"></div> 780 <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Partial</h6> 781 <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangePartial}</p> 782 </div> 783 <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendRiskInformedActive ? riskCategory.borderColor : '#e5e7eb'};"> 784 <div style="width: 8px; height: 8px; background: #F29025; border-radius: 50%; margin: 0 auto 0.5rem;"></div> 785 <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Risk Informed</h6> 786 <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangeRiskInformed}</p> 787 </div> 788 <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendRepeatableActive ? riskCategory.borderColor : '#e5e7eb'};"> 789 <div style="width: 8px; height: 8px; background: #FFD728; border-radius: 50%; margin: 0 auto 0.5rem;"></div> 790 <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Repeatable</h6> 791 <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangeRepeatable}</p> 792 </div> 793 <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendAdaptiveActive ? riskCategory.borderColor : '#e5e7eb'};"> 794 <div style="width: 8px; height: 8px; background: #40DB35; border-radius: 50%; margin: 0 auto 0.5rem;"></div> 795 <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Adaptive</h6> 796 <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangeAdaptive}</p> 797 </div> 798 </div> 799 </div> 800 </div> 801 802 <!-- Detailed Pillar Breakdown --> 803 <div style="padding: 0 2rem 2rem;"> 804 <h4 style="font-size: 1.25rem; font-weight: 700; color: #1e293b; margin: 0 0 1.5rem; text-align: center;"> 805 Security Pillar Breakdown 806 </h4> 807 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;"> 808 ${Object.entries(scores.pillarScores).length > 0 ? 809 Object.entries(scores.pillarScores).map(([pillar, score]) => { 810 const maxScore = scores.maxPillarScores[pillar] || 5; 811 const displayPillarScore = Math.round(score * 10) / 10; 812 const percentage = maxScore > 0 ? (score / maxScore) * 100 : 0; 813 let maturity = ''; 814 let statusColor = '#3b82f6'; 815 816 if (percentage > 80) { 817 maturity = "Excellent maturity in this area."; 818 statusColor = '#10b981'; 819 } else if (percentage > 60) { 820 maturity = "Good progress, but some areas for optimization.";
821 statusColor = '#f59e0b'; 822 } else if (percentage > 40) { 823 maturity = "Moderate maturity, significant room for improvement."; 824 statusColor = '#f97316'; 825 } else { 826 maturity = "Early stages, focus on foundational enhancements."; 827 statusColor = '#ef4444'; 828 } 829 830 return ` 831 <div style="background: #ffffff; border: 1px solid #e2e8f0; border-radius: 0.75rem; padding: 1.5rem; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);"> 832 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> 833 <h5 style="font-size: 1.125rem; font-weight: 700; color: #1e293b; margin: 0;">${pillar}</h5> 834 <div style="display: flex; align-items: center; gap: 0.5rem;"> 835 <div style="width: 8px; height: 8px; background: ${statusColor}; border-radius: 50%;"></div> 836 <span style="font-size: 0.875rem; color: #64748b; font-weight: 600;">${Math.round(percentage)}%</span> 837 </div> 838 </div> 839 <div style="width: 100%; height: 6px; background: #e2e8f0; border-radius: 3px; margin-bottom: 1rem; overflow: hidden;"> 840 <div style="height: 100%; background: ${statusColor}; width: ${percentage}%; transition: width 0.5s ease-in-out; border-radius: 3px;"></div> 841 </div> 842 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;"> 843 <span style="font-size: 1.5rem; font-weight: 800; color: #1e293b;"> 844 ${displayPillarScore} 845 </span> 846 <span style="font-size: 0.875rem; color: #64748b; font-weight: 500;"> 847 / ${maxScore} points 848 </span> 849 </div> 850 <p style="font-size: 0.75rem; color: #475569; margin: 0; line-height: 1.4;">${maturity}</p> 851 </div> 852 `; 853 }).join('') : 854 `<div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: #64748b; background: #f8fafc; border-radius: 0.75rem; border: 1px solid #e2e8f0;"> 855 <p style="margin: 0; font-size: 1rem;">Assessment completed successfully!</p> 856 </div>` 857 } 858 </div> 859 </div> 860 861 <!-- Footer Section --> 862 <div style="padding: 2rem; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 0 0 1rem 1rem; border-top: 1px solid #e2e8f0; text-align: center;"> 863 <div style="max-width: 600px; margin: 0 auto;"> 864 <h5 style="font-size: 1.125rem; font-weight: 700; color: #1e293b; margin: 0 0 0.75rem;"> 865 Ready to strengthen your security posture? 866 </h5> 867 <p style="font-size: 0.875rem; color: #64748b; margin: 0; line-height: 1.5;"> 868 Understanding your score is the first step. Our experts can help you develop a comprehensive strategy to enhance your identity security program and move toward a more adaptive security posture. 869 </p> 870 </div> 871 </div> 872 </div> 873 `; 874 875 // Insert the scoring results after the thank you message 876 resp.insertAdjacentHTML('afterend', scoringHtml); 877 } 878 879 // Function to handle form submission 880 function handleFormSubmit(event) { 881 const form = event.target; 882 883 // Check if this is a Forminator form 884 if (form.classList.contains('forminator-custom-form')) { 885 // Get form ID from data attribute 886 const formId = form.getAttribute('data-form-id'); 887 888 if (formId) { 889 console.log('Forminator form submitted - Form ID:', formId); 890 891 // Immediately hide the left column to prevent flashing 892 const marketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated'); 893 if (marketoBlock) { 894 const columnsContainer = marketoBlock.closest('.wp-block-columns'); 895 if (columnsContainer) { 896 const firstColumn = columnsContainer.querySelector('.wp-block-column:first-child'); 897 if (firstColumn) { 898 firstColumn.style.display = 'none'; 899 console.log('Immediately hidden first column to prevent flashing'); 900 } 901 } 902 } 903 904 // After submission, mark that we want to hide hero on next resize 905 form.dataset.hideHeroOnResize = 'true'; 906 installHideHeroOnResize(form); 907 908 // Capture form data immediately when form is submitted 909 console.log('Capturing form data on submission...'); 910 const immediateFormData = captureFormData(form); 911 console.log('Immediate form data:', immediateFormData); 912 913 // Store this data for later use
914 // Convert Map to array of entries for JSON serialization 915 const serializableFormData = { 916 ...immediateFormData, 917 questionGroups: Array.from(immediateFormData.questionGroups.entries()) 918 }; 919 console.log('Serializing questionGroups:', Array.from(immediateFormData.questionGroups.entries())); 920 form.setAttribute('data-captured-form-data', JSON.stringify(serializableFormData)); 921 } 922 } 923 } 924 925 // --- Pagination + Progress -------------------- 926 function ensureSubmitButtonLabel(form) { 927 const submitBtn = form.querySelector('.forminator-row-last .forminator-button.forminator-button-submit'); 928 if (!submitBtn) return; 929 if (!submitBtn.textContent || !submitBtn.textContent.trim()) { 930 submitBtn.textContent = 'Submit'; 931 } 932 if (!submitBtn.getAttribute('aria-label')) { 933 submitBtn.setAttribute('aria-label', 'Submit'); 934 } 935 } 936 937 function setupPaginationForForm(form) { 938 // Always ensure fallback label exists, even when pagination is skipped. 939 ensureSubmitButtonLabel(form); 940 941 // Collect radio-question rows to consider as questions 942 const questionRows = Array.from(form.querySelectorAll('.forminator-row')) 943 .filter(row => row.querySelector('.forminator-field-radio')); 944 const totalQuestions = questionRows.length; 945 946 // Helper that runs after submission (also used by global observer) 947 function finalizeAfterSubmit() { 948 // Try to get stored form data first, then capture if not available 949 let formData = null; 950 const storedData = form.getAttribute('data-captured-form-data'); 951 952 if (storedData) { 953 try { 954 formData = JSON.parse(storedData); 955 956 // Convert array of entries back to Map 957 if (formData.questionGroups && Array.isArray(formData.questionGroups)) { 958 console.log('Converting questionGroups back to Map from:', formData.questionGroups); 959 formData.questionGroups = new Map(formData.questionGroups); 960 console.log('Converted questionGroups to Map:', formData.questionGroups); 961 } 962 963 console.log('Using stored form data:', formData); 964 } catch (e) { 965 console.log('Failed to parse stored form data:', e); 966 } 967 } 968 969 // If no stored data, capture it now 970 if (!formData) { 971 console.log('No stored data, capturing form data now...'); 972 formData = captureFormData(form); 973 } 974 975 // Hide navigation and progress if present 976 const nav = form.querySelector('.vz-form-nav'); 977 const progressWrap = form.querySelector('.vz-form-progress'); 978 if (nav) nav.style.display = 'none'; 979 if (progressWrap) progressWrap.style.display = 'none'; 980 // Hide all question rows 981 const allRows = form.querySelectorAll('.forminator-row'); 982 allRows.forEach(row => (row.style.display = 'none')); 983 const submitRow = form.querySelector('.forminator-row-last'); 984 if (submitRow) submitRow.style.display = 'none'; 985 // Style the thank-you message 986 const resp = form.querySelector('.forminator-response-message'); 987 if (resp) { 988 resp.classList.add('vz-thanks'); 989 // Cancel Forminator fade-out if applied 990 resp.style.removeProperty('display'); 991 resp.style.display = 'block'; 992 resp.style.opacity = '1'; 993 resp.style.visibility = 'visible'; 994 try { 995 resp.scrollIntoView({ behavior: 'smooth', block: 'start' }); 996 } catch (_) {} 997 998 // Display scoring results using captured data 999 setTimeout(() => { 1000 displayScoringResults(form, formData); 1001 }, 500); 1002 } 1003 } 1004 1005 // Attach observer to handle post-submit for this form 1006 installSubmissionObserver(form, finalizeAfterSubmit); 1007 1008 if (totalQuestions < 2) return; // no need to paginate further 1009 1010 // Compute groups: aim for ~7 questions per group by default 1011 const QUESTIONS_PER_GROUP = 7;
1012 const groupCount = Math.ceil(totalQuestions / QUESTIONS_PER_GROUP); 1013 const groupSize = Math.ceil(totalQuestions / groupCount); 1014 1015 // Build grouped step arrays 1016 const groups = []; 1017 for (let i = 0; i < groupCount; i++) { 1018 const start = i * groupSize; 1019 const end = Math.min(start + groupSize, totalQuestions); 1020 if (start < end) groups.push(questionRows.slice(start, end)); 1021 } 1022 if (groups.length < 2) return; // safety 1023 1024 // Hide default submit row and capture the real submit button 1025 const submitRow = form.querySelector('.forminator-row-last'); 1026 let submitBtn = null; 1027 if (submitRow) { 1028 submitBtn = submitRow.querySelector('.forminator-button.forminator-button-submit'); 1029 // Keep row hidden at all times; we'll use the real button in the nav 1030 submitRow.style.display = 'none'; 1031 } 1032 // Mark form as paginated for CSS overrides 1033 form.classList.add('vz-paginated'); 1034 1035 // Inject progress bar (track + bar + label) 1036 const progressWrap = document.createElement('div'); 1037 progressWrap.className = 'vz-form-progress'; 1038 progressWrap.style.cssText = 'width:100%;max-width:760px;margin:0 auto 12px;'; 1039 const progressTrack = document.createElement('div'); 1040 progressTrack.style.cssText = 'height:6px;background:#e9ecef;border-radius:6px;overflow:hidden;'; 1041 const progressBar = document.createElement('div'); 1042 progressBar.style.cssText = 'height:100%;width:0;background:#0E61FF;transition:width 200ms ease;'; 1043 progressTrack.appendChild(progressBar); 1044 const progressLabel = document.createElement('div'); 1045 progressLabel.style.cssText = 'margin:6px 0 0;text-align:center;font-size:12px;color:#565c6d;'; 1046 progressWrap.appendChild(progressTrack); 1047 progressWrap.appendChild(progressLabel); 1048 form.insertBefore(progressWrap, questionRows[0]); 1049 1050 // Inject nav controls (prev/next) and move submit button here 1051 const nav = document.createElement('div'); 1052 nav.className = 'vz-form-nav'; 1053 nav.style.cssText = 'display:flex;justify-content:space-between;gap:12px;width:100%;max-width:760px;margin:12px auto 0;'; 1054 const prevBtn = document.createElement('button'); 1055 prevBtn.type = 'button'; 1056 prevBtn.textContent = 'Previous'; 1057 prevBtn.className = 'vz-form-nav-prev forminator-button forminator-button-submit'; 1058 const nextBtn = document.createElement('button'); 1059 nextBtn.type = 'button'; 1060 nextBtn.textContent = 'Next'; 1061 nextBtn.className = 'vz-form-nav-next forminator-button forminator-button-submit'; 1062 nav.appendChild(prevBtn); 1063 nav.appendChild(nextBtn); 1064 // Move real submit button into nav (if present) 1065 if (submitBtn) { 1066 if (!submitBtn.textContent.trim()) submitBtn.textContent = 'Submit'; 1067 submitBtn.style.setProperty('display', 'none', 'important'); 1068 nav.appendChild(submitBtn); 1069 } 1070 form.insertBefore(nav, submitRow || null); 1071 1072 // State 1073 let current = 0; 1074 1075 function scrollToProgress() { 1076 const offset = 80; // approximate sticky header height 1077 const y = progressWrap.getBoundingClientRect().top + window.pageYOffset - offset; 1078 window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' }); 1079 } 1080 1081 function showStep(index) { 1082 current = Math.max(0, Math.min(index, groups.length - 1)); 1083 // Toggle visibility for all question rows 1084 questionRows.forEach(row => (row.style.display = 'none')); 1085 groups[current].forEach(row => (row.style.display = '')); 1086 1087 // Controls visibility 1088 prevBtn.style.visibility = current === 0 ? 'hidden' : 'visible'; 1089 // On last step, hide Next and show the submit button in nav 1090 const isLast = current === groups.length - 1; 1091 nextBtn.style.setProperty('display', isLast ? 'none' : 'inline-flex', 'important'); 1092 if (submitBtn) submitBtn.style.setProperty('display', isLast ? 'inline-flex' : 'none', 'important'); 1093 1094 // Progress 1095 const pct = Math.round(((current + 1) / groups.length) * 100); 1096 progressBar.style.width = pct + '%'; 1097 progressLabel.textContent = 'Step ' + (current + 1) + ' of ' + groups.length; 1098 } 1099 1100 function currentGroupAnswered() { 1101 // Every question (radio group) in this step must have a selection 1102 return groups[current].every(row => row.querySelector('input[type="radio"]:checked')); 1103 } 1104 1105 prevBtn.addEventListener('click', function() { 1106 showStep(current - 1); 1107 scrollToProgress(); 1108 }); 1109 nextBtn.addEventListener('click', function() { 1110 if (!currentGroupAnswered()) { 1111 // Highlight only unanswered cards and keep error state until selection 1112 // Clear old errors first 1113 groups[current].forEach(row => { 1114 const card = row.querySelector('.forminator-field-radio'); 1115 if (card) card.classList.remove('vz-error'); 1116 }); 1117 // Mark unanswered 1118 const unanswered = groups[current].filter(row => !row.querySelector('input[type="radio"]:checked')); 1119 unanswered.forEach(row => { 1120 const card = row.querySelector('.forminator-field-radio') || row; 1121 card.classList.add('vz-error'); 1122 // Attach one-time change listener to clear the error when answered 1123 const radios = row.querySelectorAll('input[type="radio"]'); 1124 radios.forEach(r => { 1125 r.addEventListener('change', function onChange() { 1126 card.classList.remove('vz-error'); 1127 // remove listener after first change to avoid leaks 1128 radios.forEach(rr => rr.removeEventListener('change', onChange)); 1129 }); 1130 }); 1131 }); 1132 return; 1133 } 1134 showStep(current + 1); 1135 scrollToProgress(); 1136 }); 1137 1138 // Initialize 1139 showStep(0); 1140 } 1141 1142 function installSubmissionObserver(form, onSuccess) { 1143 const resp = form.querySelector('.forminator-response-message'); 1144 if (!resp) return; 1145 const run = () => { 1146 const shown = resp.classList.contains('forminator-show') || resp.style.display === 'block'; 1147 const success = resp.classList.contains('forminator-success'); 1148 if (shown && success) { 1149 onSuccess && onSuccess(); 1150 return true; 1151 } 1152 return false;
1153 }; 1154 // If it's already visible (e.g., AJAX completed), apply immediately 1155 if (run()) return; 1156 const observer = new MutationObserver(() => { 1157 if (run()) observer.disconnect(); 1158 }); 1159 observer.observe(resp, { attributes: true, attributeFilter: ['class', 'style'] }); 1160 } 1161 1162 function setupPagination() { 1163 const forms = document.querySelectorAll('.forminator-custom-form'); 1164 forms.forEach(form => { 1165 // Always install submission observer so non-paginated forms are handled 1166 installSubmissionObserver(form, function() { 1167 // Generic finalize for simple forms 1168 // Try to get stored form data first, then capture if not available 1169 let formData = null; 1170 const storedData = form.getAttribute('data-captured-form-data'); 1171 1172 if (storedData) { 1173 try { 1174 formData = JSON.parse(storedData); 1175 1176 // Convert array of entries back to Map 1177 if (formData.questionGroups && Array.isArray(formData.questionGroups)) { 1178 console.log('Converting questionGroups back to Map from (generic):', formData.questionGroups); 1179 formData.questionGroups = new Map(formData.questionGroups); 1180 console.log('Converted questionGroups to Map (generic):', formData.questionGroups); 1181 } 1182 1183 console.log('Using stored form data (generic):', formData); 1184 } catch (e) { 1185 console.log('Failed to parse stored form data (generic):', e); 1186 } 1187 } 1188 1189 // If no stored data, capture it now 1190 if (!formData) { 1191 console.log('No stored data, capturing form data now (generic)...'); 1192 formData = captureFormData(form); 1193 } 1194 1195 // Enable hide-on-resize after success; do not hide immediately 1196 form.dataset.hideHeroOnResize = 'true'; 1197 installHideHeroOnResize(form); 1198 1199 const nav = form.querySelector('.vz-form-nav'); 1200 const progressWrap = form.querySelector('.vz-form-progress'); 1201 if (nav) nav.style.display = 'none'; 1202 if (progressWrap) progressWrap.style.display = 'none'; 1203 const rows = form.querySelectorAll('.forminator-row'); 1204 rows.forEach(r => (r.style.display = 'none')); 1205 const resp = form.querySelector('.forminator-response-message'); 1206 if (resp) { 1207 resp.classList.add('vz-thanks'); 1208 // Display scoring results after a short delay 1209 setTimeout(() => { 1210 displayScoringResults(form, formData); 1211 }, 500); 1212 } 1213 }); 1214 // Then, optionally, pagination 1215 setupPaginationForForm(form); 1216 }); 1217 } 1218 1219 // Function to handle AJAX responses 1220 function handleAjaxResponse(event) { 1221 try { 1222 const response = event.detail; 1223 1224 // Check if this is a Forminator response 1225 if (response && response.success && response.data && response.data.form_id) { 1226 const formId = response.data.form_id; 1227 console.log('Forminator form submission response - Form ID:', formId); 1228 } 1229 } catch (error) { 1230 // Silently handle any parsing errors 1231 } 1232 } 1233 1234 // Function to fetch submission ID from custom endpoint 1235 function fetchSubmissionId(formId) { 1236 // First test the endpoint 1237 const testFormData = new FormData(); 1238 testFormData.append('action', 'test_endpoint'); 1239 testFormData.append('form_id', formId); 1240 1241 fetch(ajaxurl || '/wp-admin/admin-ajax.php', { 1242 method: 'POST', 1243 body: testFormData 1244 }) 1245 .then(response => response.text()) 1246 .then(text => { 1247 try { 1248 const data = JSON.parse(text); 1249 if (data.success) { 1250 // Now try to get the submission ID 1251 return fetchSubmissionIdReal(formId); 1252 } 1253 } catch (e) { 1254 // Silently handle parsing errors 1255 } 1256 }) 1257 .catch(error => { 1258 // Silently handle errors 1259 }); 1260 } 1261 1262 // Function to actually fetch the submission ID 1263 function fetchSubmissionIdReal(formId) { 1264 const formData = new FormData(); 1265 formData.append('action', 'get_forminator_submission_id'); 1266 formData.append('form_id', formId); 1267 1268 return fetch(ajaxurl || '/wp-admin/admin-ajax.php', { 1269 method: 'POST', 1270 body: formData 1271 }) 1272 .then(response => response.text()) 1273 .then(text => { 1274 try { 1275 const data = JSON.parse(text); 1276 if (data.success && data.data.submission_id) { 1277 console.log('Submission ID:', data.data.submission_id); 1278 1279 // Trigger Marketo form submission when we get the submission ID 1280 triggerMarketoSubmission(data.data.submission_id); 1281 } 1282 } catch (e) { 1283 // Silently handle parsing errors 1284 } 1285 }) 1286 .catch(error => { 1287 // Silently handle errors 1288 }); 1289 } 1290 1291 // Function to trigger Marketo form submission 1292 function triggerMarketoSubmission(submissionId) { 1293 // Find the Marketo form gated block that contains this Forminator form 1294 const forminatorForms = document.querySelectorAll('.forminator-custom-form'); 1295 let targetMarketoBlock = null; 1296 1297 // Find which Forminator form was submitted and locate its parent Marketo block
1298 forminatorForms.forEach(form => { 1299 if (form.querySelector('.forminator-response-message.forminator-success')) { 1300 // This form was successfully submitted 1301 targetMarketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated'); 1302 if (targetMarketoBlock) { 1303 console.log('Found Marketo gated block, triggering submission'); 1304 1305 // Get the stored form data from the form's data attribute 1306 let formData = null; 1307 const storedData = form.getAttribute('data-captured-form-data'); 1308 1309 if (storedData) { 1310 try { 1311 formData = JSON.parse(storedData); 1312 1313 // Convert array of entries back to Map 1314 if (formData.questionGroups && Array.isArray(formData.questionGroups)) { 1315 console.log('Converting questionGroups back to Map from triggerMarketoSubmission:', formData.questionGroups); 1316 formData.questionGroups = new Map(formData.questionGroups); 1317 console.log('Converted questionGroups to Map in triggerMarketoSubmission:', formData.questionGroups); 1318 } 1319 1320 console.log('Using stored form data in triggerMarketoSubmission:', formData); 1321 } catch (e) { 1322 console.log('Failed to parse stored form data in triggerMarketoSubmission:', e); 1323 } 1324 } 1325 1326 // If no stored data, capture it now 1327 if (!formData) { 1328 console.log('No stored data, capturing form data now in triggerMarketoSubmission...'); 1329 formData = captureFormData(form); 1330 } 1331 1332 // Get the score data from the form 1333 const scoreData = calculateScores(form, formData); 1334 1335 // Dispatch custom event to trigger Marketo submission 1336 targetMarketoBlock.dispatchEvent(new CustomEvent('forminatorSubmissionComplete', { 1337 detail: { 1338 submissionId, 1339 formId: form.getAttribute('data-form-id'), 1340 score: scoreData.totalScore, 1341 maxScore: scoreData.maxTotalScore, 1342 pillarScores: scoreData.pillarScores 1343 } 1344 })); 1345 } 1346 } 1347 }); 1348 } 1349 1350 // Function to intercept AJAX requests to capture Forminator responses 1351 function interceptAjaxResponses() { 1352 // Override XMLHttpRequest to intercept responses 1353 const originalXHROpen = XMLHttpRequest.prototype.open; 1354 const originalXHRSend = XMLHttpRequest.prototype.send; 1355 1356 XMLHttpRequest.prototype.open = function(method, url, ...args) { 1357 this._url = url; 1358 return originalXHROpen.apply(this, [method, url, ...args]); 1359 }; 1360 1361 XMLHttpRequest.prototype.send = function(...args) { 1362 const xhr = this; 1363 1364 // Add response listener 1365 xhr.addEventListener('load', function() { 1366 try { 1367 // Check if this is a Forminator AJAX response 1368 if (xhr._url && xhr._url.includes('admin-ajax.php')) { 1369 const responseText = xhr.responseText; 1370 const response = JSON.parse(responseText); 1371 1372 if (response && response.success && response.data && response.data.form_id) { 1373 const formId = response.data.form_id; 1374 1375 // Look for submission ID or other identifiers 1376 const submissionId = response.data.submission_id || response.data.entry_id || response.data.id; 1377 if (submissionId) { 1378 console.log('Submission ID found in AJAX response:', submissionId);
1379 // Trigger Marketo submission immediately when submission ID is found 1380 triggerMarketoSubmission(submissionId); 1381 } else { 1382 // If no submission ID in response, try to fetch it 1383 fetchSubmissionId(formId); 1384 } 1385 } 1386 } 1387 } catch (error) { 1388 // Silently handle parsing errors 1389 } 1390 }); 1391 1392 return originalXHRSend.apply(this, args); 1393 }; 1394 } 1395 1396 // Function to initialize the listener 1397 function init() { 1398 // Listen for form submissions 1399 document.addEventListener('submit', handleFormSubmit, true); 1400 1401 // Listen for Forminator AJAX responses (custom event) 1402 document.addEventListener('forminator_form_submitted', handleAjaxResponse); 1403 1404 // Alternative: Listen for all AJAX responses 1405 document.addEventListener('ajax_response', handleAjaxResponse); 1406 1407 // Listen for common Forminator events 1408 document.addEventListener('forminator_success', function(e) { 1409 if (e.detail && e.detail.form_id) { 1410 // Silently handle success events 1411 } 1412 }); 1413 1414 document.addEventListener('forminator_complete', function(e) { 1415 if (e.detail && e.detail.form_id) { 1416 // Silently handle complete events 1417 } 1418 }); 1419 1420 // Intercept AJAX responses to capture Forminator form submissions 1421 interceptAjaxResponses(); 1422 1423 // Setup pagination/progress 1424 setupPagination(); 1425 1426 // Listen for DOM changes to catch dynamically added forms 1427 const observer = new MutationObserver(function(mutations) { 1428 mutations.forEach(function(mutation) { 1429 if (mutation.type === 'childList') { 1430 mutation.addedNodes.forEach(function(node) { 1431 if (node.nodeType === Node.ELEMENT_NODE) { 1432 // When new Forminator forms appear, paginate them 1433 const newForms = node.matches && node.matches('.forminator-custom-form') 1434 ? [node] 1435 : (node.querySelectorAll ? node.querySelectorAll('.forminator-custom-form') : []); 1436 if (newForms && newForms.length) { 1437 newForms.forEach(setupPaginationForForm); 1438 } 1439 } 1440 }); 1441 } 1442 }); 1443 }); 1444 1445 // Start observing 1446 observer.observe(document.body, { 1447 childList: true, 1448 subtree: true 1449 }); 1450 } 1451 1452 // Initialize when DOM is ready 1453 if (document.readyState === 'loading') { 1454 document.addEventListener('DOMContentLoaded', init); 1455 } else { 1456 init(); 1457 } 1458 1459})();
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.