1/** 2 * Frontend Conditional Logic for Visual Forms 3 * 4 * Handles dynamic field visibility based on conditional logic rules. 5 * 6 * @package Charitable/Visual Forms 7 * @since 1.8.8 8 */ 9(function($) { 10 'use strict'; 11 12 // Immediate log to confirm script is loaded (before jQuery/DOM ready) 13 (function() { 14 var search = window.location.search; 15 var isDebug = (search.indexOf('visual_debug=true') !== -1) || (search.indexOf('debug_visual=true') !== -1); 16 if (isDebug) { 17 console.log('[ConditionalLogic] Script file loaded and executing'); 18 console.log('[ConditionalLogic] Current URL:', window.location.href); 19 } 20 })(); 21 22 var CharitableConditionalLogic = { 23 initialized: false, 24 debugMode: false, 25 forms: {}, 26 rules: {}, 27 debugHistory: [], // Track all debug events 28 fieldValueHistory: {}, // Track field value changes 29 30 /** 31 * Initialize conditional logic 32 */ 33 init: function() { 34 if (this.initialized) { 35 return; 36 } 37 38 // Check for debug mode (support both parameter names) 39 this.debugMode = this.isDebugMode(); 40 41 if (this.debugMode) { 42 console.log('[ConditionalLogic] Frontend script loaded'); 43 console.log('[ConditionalLogic] Debug mode enabled'); 44 console.log('[ConditionalLogic] jQuery available:', typeof $ !== 'undefined'); 45 console.log('[ConditionalLogic] CHARITABLE available:', typeof CHARITABLE !== 'undefined'); 46 } 47 48 // Wait for DOM ready 49 $(document).ready($.proxy(this.onReady, this)); 50 }, 51 52 /** 53 * Get URL parameter 54 */ 55 getUrlParameter: function(name) { 56 name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); 57 var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 58 var results = regex.exec(location.search); 59 return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); 60 }, 61 62 /** 63 * Check if debug mode is enabled (supports both parameter names) 64 */ 65 isDebugMode: function() { 66 return this.getUrlParameter('visual_debug') === 'true' || this.getUrlParameter('debug_visual') === 'true'; 67 }, 68 69 /** 70 * DOM ready handler 71 */ 72 onReady: function() { 73 if (this.debugMode) { 74 console.log('[ConditionalLogic] DOM ready'); 75 } 76 77 // Check if conditional logic rules are available 78 if (typeof charitable_conditional_logic === 'undefined') { 79 if (this.debugMode) { 80 console.warn('[ConditionalLogic] No conditional logic rules found'); 81 } 82 return; 83 } 84 85 this.rules = charitable_conditional_logic; 86 87 if (this.debugMode) { 88 console.log('[ConditionalLogic] Rules loaded:', this.rules); 89 console.log('[ConditionalLogic] Forms with conditionals:', Object.keys(this.rules)); 90 } 91 92 // Initialize each form 93 for (var formId in this.rules) { 94 if (this.rules.hasOwnProperty(formId)) { 95 this.initForm(formId, this.rules[formId]); 96 } 97 } 98 99 // Listen for donation amount changes (from charitable.js) 100 $(document.body).on('charitable:form:amount:changed', $.proxy(this.onDonationAmountChanged, this)); 101 102 // Listen directly to donation amount radio button changes 103 $(document).on('change', '.charitable-visual-donation-form input[name="donation_amount"]', $.proxy(this.onDonationAmountRadioChange, this)); 104 105 // Listen to custom donation amount input changes 106 $(document).on('change input', '.charitable-visual-donation-form input.custom-donation-input, .charitable-visual-donation-form input[name="custom_donation_amount"]', $.proxy(this.onCustomAmountChange, this)); 107 108 // Listen for field changes 109 $(document).on('change input', '.charitable-visual-donation-form input, .charitable-visual-donation-form select, .charitable-visual-donation-form textarea', $.proxy(this.onFieldChange, this)); 110 111 // Debug: Monitor all user interactions 112 if (this.debugMode) { 113 this.setupDebugMonitoring(); 114 } 115 116 // Initial evaluation (with delay to ensure form is initialized) 117 var self = this; 118 setTimeout(function() { 119 if (self.debugMode) { 120 console.log('[ConditionalLogic] Running initial evaluation'); 121 } 122 123 // Read all field values from the DOM on initial load 124 self.readAllFieldValues(); 125 126 // Print initial form state 127 if (self.debugMode) { 128 self.printFormState('Initial Load'); 129 } 130 131 // Evaluate all conditionals 132 self.evaluateAll(); 133 }, 200); // Increased delay to ensure form is fully initialized 134 135 this.initialized = true; 136 137 if (this.debugMode) { 138 console.log('[ConditionalLogic] Initialization complete'); 139 } 140 }, 141 142 /** 143 * Initialize a form 144 */ 145 initForm: function(formId, formRules) { 146 if (this.debugMode) { 147 console.log('[ConditionalLogic] Initializing form:', formId, formRules); 148 } 149 150 this.forms[formId] = { 151 rules: formRules, 152 formElement: null 153 }; 154 155 // Find form element 156 var $form = $('.charitable-visual-donation-form'); 157 if ($form.length) { 158 this.forms[formId].formElement = $form.first(); 159 } 160 }, 161 162 /** 163 * Get current donation amount from form 164 */ 165 getDonationAmount: function() { 166 var amount = 0; 167 168 // Try to get from checked radio button first 169 var $amountInput = $('.charitable-visual-donation-form input[name="donation_amount"]:checked'); 170 if ($amountInput.length) { 171 var amountVal = $amountInput.val(); 172 if (amountVal && amountVal !== 'custom') { 173 amount = parseFloat(amountVal) || 0; 174 } else if (amountVal === 'custom') { 175 // Custom amount selected, get from custom input 176 var $customAmount = $('.charitable-visual-donation-form input.custom-donation-input, .charitable-visual-donation-form input[name="custom_donation_amount"]'); 177 if ($customAmount.length && $customAmount.val()) { 178 var customVal = $customAmount.val().replace(/[^0-9.]/g, ''); 179 if (customVal) { 180 amount = parseFloat(customVal) || 0; 181 } 182 } 183 } 184 } else { 185 // No radio selected, check custom amount input directly 186 var $customAmount = $('.charitable-visual-donation-form input.custom-donation-input, .charitable-visual-donation-form input[name="custom_donation_amount"]'); 187 if ($customAmount.length && $customAmount.val()) { 188 var customVal = $customAmount.val().replace(/[^0-9.]/g, ''); 189 if (customVal) { 190 amount = parseFloat(customVal) || 0; 191 } 192 } 193 } 194 195 return amount; 196 }, 197 198 /** 199 * Handle donation amount change (from charitable:form:amount:changed event) 200 */ 201 onDonationAmountChanged: function(event, helper) { 202 if (this.debugMode) { 203 console.log('[ConditionalLogic] Donation amount changed event received'); 204 } 205 206 // Get donation amount
207 var amount = 0; 208 if (helper && typeof helper.get_amount === 'function') { 209 amount = parseFloat(helper.get_amount()) || 0; 210 } else { 211 amount = this.getDonationAmount(); 212 } 213 214 if (this.debugMode) { 215 console.log('[ConditionalLogic] Donation amount:', amount); 216 } 217 218 // Update donation_amount field value for evaluation 219 this.updateFieldValue('donation_amount', amount); 220 221 if (this.debugMode) { 222 this.logDebugEvent('donation_amount_updated', { 223 amount: amount, 224 source: 'charitable:form:amount:changed', 225 timestamp: new Date().toISOString() 226 }); 227 } 228 229 // Re-evaluate all conditionals 230 this.evaluateAll(); 231 232 if (this.debugMode) { 233 this.printFormState('After donation amount change event'); 234 } 235 }, 236 237 /** 238 * Handle donation amount radio button change 239 */ 240 onDonationAmountRadioChange: function(event) { 241 if (this.debugMode) { 242 console.log('[ConditionalLogic] Donation amount radio changed:', $(event.target).val()); 243 } 244 245 var amount = this.getDonationAmount(); 246 if (this.debugMode) { 247 console.log('[ConditionalLogic] Donation amount from radio:', amount); 248 } 249 250 this.updateFieldValue('donation_amount', amount); 251 252 if (this.debugMode) { 253 this.logDebugEvent('donation_amount_updated', { 254 amount: amount, 255 source: 'radio_change', 256 timestamp: new Date().toISOString() 257 }); 258 } 259 260 this.evaluateAll(); 261 262 if (this.debugMode) { 263 this.printFormState('After donation amount radio change'); 264 } 265 }, 266 267 /** 268 * Handle custom donation amount input change 269 */ 270 onCustomAmountChange: function(event) { 271 if (this.debugMode) { 272 console.log('[ConditionalLogic] Custom donation amount changed'); 273 } 274 275 // Only process if custom radio is selected 276 var $customRadio = $('.charitable-visual-donation-form input[name="donation_amount"][value="custom"]'); 277 if ($customRadio.length && $customRadio.is(':checked')) { 278 var amount = this.getDonationAmount(); 279 if (this.debugMode) { 280 console.log('[ConditionalLogic] Custom donation amount:', amount); 281 } 282 283 this.updateFieldValue('donation_amount', amount); 284 285 if (this.debugMode) { 286 this.logDebugEvent('donation_amount_updated', { 287 amount: amount, 288 source: 'custom_input_change', 289 timestamp: new Date().toISOString() 290 }); 291 } 292 293 this.evaluateAll(); 294 295 if (this.debugMode) { 296 this.printFormState('After custom donation amount change'); 297 } 298 } 299 }, 300 301 /** 302 * Handle field change 303 */ 304 onFieldChange: function(event) { 305 var $field = $(event.target); 306 var fieldName = $field.attr('name'); 307 var fieldType = $field.attr('type'); 308 309 var fieldValue; 310 var actualFieldName = fieldName; 311 312 // Handle checkbox groups (name ends with []) 313 if (fieldType === 'checkbox' && fieldName && fieldName.indexOf('[]') !== -1) { 314 // Get all checkboxes in this group 315 var $allCheckboxes = $('.charitable-visual-donation-form input[type="checkbox"][name="' + fieldName + '"]'); 316 var checkedValues = []; 317 $allCheckboxes.each(function() { 318 if ($(this).is(':checked')) { 319 checkedValues.push($(this).val()); 320 } 321 }); 322 // Store with base name (without []) 323 actualFieldName = fieldName.replace(/\[\]$/, ''); 324 fieldValue = checkedValues; 325 if (this.debugMode) { 326 console.log('[ConditionalLogic] Checkbox group changed:', actualFieldName, '=', fieldValue); 327 } 328 this.updateFieldValue(actualFieldName, fieldValue); 329 } else { 330 // Regular field (single value) 331 fieldValue = this.getFieldValue($field); 332 333 if (this.debugMode) { 334 console.log('[ConditionalLogic] Field changed:', fieldName, '=', fieldValue); 335 } 336 337 // Update field value for evaluation 338 if (fieldName) { 339 this.updateFieldValue(fieldName, fieldValue); 340 } 341 } 342 343 if (this.debugMode) { 344 this.logDebugEvent('field_value_updated', { 345 fieldName: actualFieldName, 346 fieldValue: fieldValue, 347 timestamp: new Date().toISOString() 348 }); 349 } 350 351 // Re-evaluate all conditionals 352 this.evaluateAll(); 353 354 if (this.debugMode) { 355 this.printFormState('After field change: ' + fieldName); 356 } 357 }, 358 359 /** 360 * Get field value 361 */ 362 getFieldValue: function($field) { 363 var type = $field.attr('type'); 364 var tagName = $field.prop('tagName').toLowerCase(); 365 var fieldName = $field.attr('name'); 366 367 if (tagName === 'select') { 368 var val = $field.val(); 369 // Multi-select returns an array, single-select returns a string 370 return val || (Array.isArray(val) ? [] : ''); 371 } else if (tagName === 'textarea') { 372 return $field.val() || ''; 373 } else if (type === 'checkbox') { 374 // Check if this is part of a checkbox group (name ends with []) 375 if (fieldName && fieldName.indexOf('[]') !== -1) { 376 // Get all checkboxes with this name and return array of checked values 377 var baseName = fieldName.replace(/\[\]$/, ''); 378 var $allCheckboxes = $('.charitable-visual-donation-form input[type="checkbox"][name="' + fieldName + '"]'); 379 var checkedValues = []; 380 $allCheckboxes.each(function() { 381 if ($(this).is(':checked')) { 382 checkedValues.push($(this).val()); 383 } 384 }); 385 return checkedValues; 386 } else { 387 // Single checkbox 388 return $field.is(':checked') ? $field.val() : ''; 389 } 390 } else if (type === 'radio') { 391 return $field.is(':checked') ? $field.val() : ''; 392 } else { 393 return $field.val() || ''; 394 } 395 }, 396 397 /** 398 * Update field value in form data 399 */ 400 updateFieldValue: function(fieldName, value) { 401 // Store in a temporary data structure for evaluation 402 if (typeof this.fieldValues === 'undefined') { 403 this.fieldValues = {}; 404 } 405 this.fieldValues[fieldName] = value; 406 407 if (this.debugMode) { 408 console.log('[ConditionalLogic] Updated field value:', fieldName, '=', value); 409 } 410 }, 411 412 /** 413 * Read all field values from the DOM on initial load 414 * This ensures we have current values for all fields before evaluating conditionals 415 */ 416 readAllFieldValues: function() { 417 if (this.debugMode) { 418 console.log('[ConditionalLogic] Reading all field values from DOM'); 419 } 420 421 // Initialize fieldValues if needed 422 if (typeof this.fieldValues === 'undefined') { 423 this.fieldValues = {}; 424 } 425 426 // Read donation amount (special handling) 427 var donationAmount = this.getDonationAmount(); 428 this.updateFieldValue('donation_amount', donationAmount); 429 if (this.debugMode) { 430 console.log('[ConditionalLogic] Initial donation amount:', donationAmount); 431 432 // Debug: Show all donation amount inputs 433 var $allAmounts = $('.charitable-visual-donation-form input[name="donation_amount"]'); 434 console.log('[ConditionalLogic] All donation amount inputs:', $allAmounts.length); 435 $allAmounts.each(function() { 436 var $input = $(this); 437 console.log('[ConditionalLogic] - Value:', $input.val(), 'Checked:', $input.is(':checked'), 'ID:', $input.attr('id')); 438 }); 439 440 var $customAmount = $('.charitable-visual-donation-form input.custom-donation-input, .charitable-visual-donation-form input[name="custom_donation_amount"]'); 441 if ($customAmount.length) { 442 console.log('[ConditionalLogic] Custom amount input found, value:', $customAmount.val()); 443 } 444 } 445 446 // Read all other form fields 447 var $form = $('.charitable-visual-donation-form'); 448 if ($form.length) { 449 if (this.debugMode) { 450 console.log('[ConditionalLogic] Form found, scanning fields...'); 451 } 452 453 // Track which checkbox groups we've already processed 454 var processedCheckboxGroups = {}; 455 456 // Read all inputs, selects, and textareas 457 $form.find('input, select, textarea').each(function() { 458 var $field = $(this); 459 var fieldName = $field.attr('name'); 460 var fieldId = $field.attr('id'); 461 var fieldType = $field.attr('type'); 462 463 // Skip donation amount (already handled) 464 if (fieldName === 'donation_amount' || fieldName === 'custom_donation_amount' || fieldName === 'charitable_donation_amount') { 465 return; 466 } 467 468 // Skip hidden fields and system fields 469 if (fieldType === 'hidden' || 470 fieldName === '_charitable_donation_nonce' || 471 fieldName === '_wp_http_referer' || 472 fieldName === 'charitable_action' || 473 fieldName === 'campaign_id' || 474 fieldName === 'description' || 475 fieldName === 'ID' || 476 fieldName === 'gateway' || 477 fieldName === 'stripe_payment_method') { 478 return; 479 } 480 481 if (fieldName) { 482 // Handle checkbox groups (name ends with []) - only process once per group 483 if (fieldType === 'checkbox' && fieldName.indexOf('[]') !== -1) { 484 if (processedCheckboxGroups[fieldName]) { 485 return; // Already processed this checkbox group 486 } 487 processedCheckboxGroups[fieldName] = true; 488 // Get all checkboxes in this group 489 var $allCheckboxes = $form.find('input[type="checkbox"][name="' + fieldName + '"]'); 490 var checkedValues = []; 491 $allCheckboxes.each(function() { 492 if ($(this).is(':checked')) { 493 checkedValues.push($(this).val()); 494 } 495 }); 496 // Store with base name (without []) 497 var baseName = fieldName.replace(/\[\]$/, ''); 498 this.updateFieldValue(baseName, checkedValues); 499 if (this.debugMode) { 500 console.log('[ConditionalLogic] Read checkbox group value:', baseName, '=', checkedValues); 501 } 502 } else { 503 // Regular field (single value) 504 var value = this.getFieldValue($field); 505 this.updateFieldValue(fieldName, value); 506 507 if (this.debugMode) { 508 console.log('[ConditionalLogic] Read field value:', fieldName, '=', value, 'Type:', fieldType, 'ID:', fieldId); 509 } 510 } 511 } 512 }.bind(this)); 513 } else { 514 if (this.debugMode) { 515 console.warn('[ConditionalLogic] Form not found! Selector: .charitable-visual-donation-form'); 516 } 517 } 518 519 if (this.debugMode) { 520 console.log('[ConditionalLogic] Field values read:', Object.keys(this.fieldValues || {})); 521 console.log('[ConditionalLogic] Field values cache:', this.fieldValues); 522 } 523 }, 524 525 /** 526 * Get current field value 527 */ 528 getCurrentFieldValue: function(fieldId) { 529 // Map field ID to HTML format for consistent lookup 530 var mappedId = this.mapFieldIdToHtml ? this.mapFieldIdToHtml(fieldId) : fieldId; 531 532 if (this.debugMode && mappedId !== fieldId) { 533 console.log('[ConditionalLogic] Mapping field ID:', fieldId, '->', mappedId); 534 } 535
536 // Check stored values first (from readAllFieldValues or updates) 537 // Try both original and mapped IDs since values might be stored with either format 538 // Prefer mapped ID if it exists and has a value, otherwise try original ID 539 if (this.fieldValues) { 540 // First check mapped ID (more likely to have the actual value) 541 if (mappedId !== fieldId && this.fieldValues.hasOwnProperty(mappedId)) { 542 var mappedValue = this.fieldValues[mappedId]; 543 // Arrays are always valid (even if empty), so check for arrays first 544 if (Array.isArray(mappedValue)) { 545 if (this.debugMode) { 546 console.log('[ConditionalLogic] Found array in cache with mapped ID:', mappedId, '=', mappedValue); 547 } 548 return mappedValue; 549 } 550 // For non-arrays, only return if it has a value 551 if (mappedValue !== '' && mappedValue !== null && mappedValue !== undefined) { 552 if (this.debugMode) { 553 console.log('[ConditionalLogic] Found value in cache with mapped ID:', mappedId, '=', mappedValue); 554 } 555 return mappedValue; 556 } 557 } 558 // Then check original ID 559 if (this.fieldValues.hasOwnProperty(fieldId)) { 560 var originalValue = this.fieldValues[fieldId]; 561 // Arrays are always valid (even if empty) 562 if (Array.isArray(originalValue)) { 563 if (this.debugMode) { 564 console.log('[ConditionalLogic] Found array in cache with original ID:', fieldId, '=', originalValue); 565 } 566 return originalValue; 567 } 568 if (this.debugMode) { 569 console.log('[ConditionalLogic] Found value in cache with original ID:', fieldId, '=', originalValue); 570 } 571 // Only return if it has a value, otherwise continue to check mapped ID 572 if (originalValue !== '' && originalValue !== null && originalValue !== undefined) { 573 return originalValue; 574 } 575 } 576 // If mapped ID exists but was empty, return it anyway (empty is a valid value for strings) 577 if (mappedId !== fieldId && this.fieldValues.hasOwnProperty(mappedId)) { 578 if (this.debugMode) { 579 console.log('[ConditionalLogic] Found empty value in cache with mapped ID:', mappedId); 580 } 581 return this.fieldValues[mappedId]; 582 } 583 if (this.debugMode) { 584 console.log('[ConditionalLogic] Value not found in cache for:', fieldId, 'or', mappedId, 'Available keys:', Object.keys(this.fieldValues)); 585 } 586 } 587 588 // Special handling for donation_amount 589 if (fieldId === 'donation_amount' || fieldId === 'charitable_donation_amount') { 590 return this.getDonationAmount(); 591 } 592 593 // Try to find field by name or ID 594 var $field = $('.charitable-visual-donation-form [name="' + fieldId + '"], .charitable-visual-donation-form #charitable_field_' + fieldId + '_element, .charitable-visual-donation-form [data-field-id="' + fieldId + '"]'); 595 596 // Also try mapped field ID (for PHP field IDs like "6" -> "field_6") 597 if (!$field.length) { 598 $field = $('.charitable-visual-donation-form [name="' + mappedId + '"], .charitable-visual-donation-form #charitable_field_' + mappedId + '_element, .charitable-visual-donation-form #charitable_field_field_' + mappedId.replace('field_', '') + '_element'); 599 } 600 601 if ($field.length) { 602 var value = this.getFieldValue($field.first()); 603 // Cache it for future use 604 this.updateFieldValue(fieldId, value); 605 return value; 606 } 607 608 if (this.debugMode) { 609 console.warn('[ConditionalLogic] Field value not found for:', fieldId); 610 } 611 612 return ''; 613 }, 614 615 /** 616 * Evaluate all conditional rules 617 */ 618 evaluateAll: function() { 619 if (this.debugMode) { 620 console.log('[ConditionalLogic] Evaluating all conditionals'); 621 } 622 623 for (var formId in this.rules) { 624 if (this.rules.hasOwnProperty(formId)) { 625 this.evaluateForm(formId, this.rules[formId]); 626 } 627 } 628 }, 629 630 /** 631 * Evaluate conditional rules for a form 632 */ 633 evaluateForm: function(formId, formRules) { 634 if (this.debugMode) { 635 console.log('[ConditionalLogic] Evaluating form:', formId, 'Fields with rules:', Object.keys(formRules)); 636 } 637 638 for (var fieldId in formRules) { 639 if (formRules.hasOwnProperty(fieldId)) { 640 if (this.debugMode) { 641 console.log('[ConditionalLogic] Processing field:', fieldId, 'Rules:', formRules[fieldId]); 642 } 643 this.evaluateField(fieldId, formRules[fieldId]); 644 } 645 } 646 }, 647 648 /** 649 * Evaluate conditional rules for a field 650 */ 651 evaluateField: function(fieldId, fieldRules) { 652 if (!fieldRules || !fieldRules.conditionals) { 653 if (this.debugMode) { 654 console.warn('[ConditionalLogic] Field', fieldId, 'has no conditionals or invalid rules:', fieldRules); 655 } 656 return; 657 } 658 659 var conditionalType = fieldRules.conditional_type || 'show'; 660 var conditionals = fieldRules.conditionals; 661 662 if (this.debugMode) { 663 console.log('[ConditionalLogic] ===== Evaluating field:', fieldId, '====='); 664 console.log('[ConditionalLogic] Conditional type:', conditionalType); 665 console.log('[ConditionalLogic] Conditionals structure:', conditionals); 666 console.log('[ConditionalLogic] Number of groups:', Object.keys(conditionals).length); 667 } 668 669 // Evaluate groups (OR logic - any group passing means field should be shown) 670 var shouldShow = false;
671 var hasValidGroups = false; 672 673 for (var groupId in conditionals) { 674 if (conditionals.hasOwnProperty(groupId)) { 675 var group = conditionals[groupId]; 676 677 // Skip empty groups 678 if (!Array.isArray(group) || group.length === 0) { 679 continue; 680 } 681 682 hasValidGroups = true; 683 684 if (this.debugMode) { 685 console.log('[ConditionalLogic] Evaluating group', groupId, 'with', group.length, 'rules'); 686 } 687 688 var groupPasses = this.evaluateGroup(group); 689 690 if (this.debugMode) { 691 console.log('[ConditionalLogic] Group', groupId, 'passes:', groupPasses); 692 } 693 694 if (groupPasses) { 695 shouldShow = true; 696 if (this.debugMode) { 697 console.log('[ConditionalLogic] Group', groupId, 'passed, field should be shown (OR logic)'); 698 } 699 break; // OR logic - one passing group is enough 700 } 701 } 702 } 703 704 // Handle conditional_type logic 705 if (conditionalType === 'hide') { 706 // Hide: Hide when condition is met, show otherwise 707 shouldShow = !shouldShow; 708 if (this.debugMode) { 709 console.log('[ConditionalLogic] Conditional type is "hide", inverting result:', shouldShow); 710 } 711 } else { 712 // Show (default): Show when condition is met, hide otherwise 713 // No special handling needed - if condition fails, field is hidden 714 if (this.debugMode && !shouldShow && hasValidGroups) { 715 console.log('[ConditionalLogic] Condition failed with "show" type, field will be hidden'); 716 } 717 } 718 719 if (this.debugMode) { 720 console.log('[ConditionalLogic] Final decision for field', fieldId, '- should show:', shouldShow); 721 } 722 723 // Show or hide the field 724 this.toggleField(fieldId, shouldShow); 725 726 // Debug: Log the decision 727 if (this.debugMode) { 728 this.logDebugEvent('field_visibility_decision', { 729 fieldId: fieldId, 730 shouldShow: shouldShow, 731 conditionalType: conditionalType, 732 fieldValue: this.getCurrentFieldValue(fieldId), 733 timestamp: new Date().toISOString() 734 }); 735 } 736 }, 737 738 /** 739 * Evaluate a group of rules (AND logic - all rules must pass) 740 */ 741 evaluateGroup: function(group) { 742 if (!Array.isArray(group) || group.length === 0) { 743 return false; 744 } 745 746 for (var i = 0; i < group.length; i++) { 747 var rule = group[i]; 748 if (!this.evaluateRule(rule)) { 749 return false; // AND logic - all rules must pass 750 } 751 } 752 753 return true; 754 }, 755 756 /** 757 * Evaluate a single rule 758 */ 759 evaluateRule: function(rule) { 760 if (!rule || !rule.field || !rule.operator) { 761 if (this.debugMode) { 762 console.warn('[ConditionalLogic] Invalid rule:', rule); 763 } 764 return false; 765 } 766 767 var fieldValue = this.getCurrentFieldValue(rule.field); 768 var operator = rule.operator; 769 var ruleValue = rule.value || ''; 770 771 if (this.debugMode) { 772 console.log('[ConditionalLogic] --- Evaluating rule ---'); 773 console.log('[ConditionalLogic] Trigger field:', rule.field); 774 console.log('[ConditionalLogic] Operator:', operator); 775 console.log('[ConditionalLogic] Rule value:', ruleValue); 776 console.log('[ConditionalLogic] Actual field value:', fieldValue); 777 console.log('[ConditionalLogic] Value type:', typeof fieldValue); 778 console.log('[ConditionalLogic] Is array?', Array.isArray(fieldValue)); 779 } 780 781 // Handle arrays (from checkboxes or multi-select) 782 var isArray = Array.isArray(fieldValue); 783 784 // Convert to appropriate types for comparison 785 var isNumeric = !isArray && !isNaN(parseFloat(fieldValue)) && isFinite(fieldValue); 786 var fieldValueNum = isNumeric ? parseFloat(fieldValue) : 0; 787 var ruleValueNum = !isNaN(parseFloat(ruleValue)) ? parseFloat(ruleValue) : 0; 788 789 var fieldValueStr = isArray ? '' : String(fieldValue).toLowerCase().trim(); 790 var ruleValueStr = String(ruleValue).toLowerCase().trim(); 791 792 if (this.debugMode) { 793 console.log('[ConditionalLogic] Numeric comparison?', isNumeric); 794 if (isNumeric) { 795 console.log('[ConditionalLogic] Numeric values - Field:', fieldValueNum, 'Rule:', ruleValueNum); 796 } else { 797 console.log('[ConditionalLogic] String values - Field:', fieldValueStr, 'Rule:', ruleValueStr); 798 } 799 } 800 801 var result = false;
802 switch (operator) { 803 case '==': 804 if (isArray) { 805 // For arrays, check if rule value is in the array 806 result = fieldValue.indexOf(ruleValue) !== -1 || fieldValue.indexOf(ruleValueStr) !== -1; 807 } else { 808 result = fieldValueStr === ruleValueStr; 809 } 810 break; 811 case '!=': 812 if (isArray) { 813 // For arrays, check if rule value is NOT in the array 814 result = fieldValue.indexOf(ruleValue) === -1 && fieldValue.indexOf(ruleValueStr) === -1; 815 } else { 816 result = fieldValueStr !== ruleValueStr; 817 } 818 break; 819 case '>': 820 result = isNumeric && fieldValueNum > ruleValueNum; 821 break; 822 case '>=': 823 result = isNumeric && fieldValueNum >= ruleValueNum; 824 break; 825 case '<': 826 result = isNumeric && fieldValueNum < ruleValueNum; 827 break; 828 case '<=': 829 result = isNumeric && fieldValueNum <= ruleValueNum; 830 break; 831 case 'contains': 832 if (isArray) { 833 // For arrays, check if any element contains the rule value 834 result = fieldValue.some(function(val) { 835 return String(val).toLowerCase().indexOf(ruleValueStr) !== -1; 836 }); 837 } else { 838 result = fieldValueStr.indexOf(ruleValueStr) !== -1; 839 } 840 break; 841 case 'starts_with': 842 if (isArray) { 843 // For arrays, check if any element starts with the rule value 844 result = fieldValue.some(function(val) { 845 return String(val).toLowerCase().indexOf(ruleValueStr) === 0; 846 }); 847 } else { 848 result = fieldValueStr.indexOf(ruleValueStr) === 0; 849 } 850 break; 851 case 'ends_with': 852 if (isArray) { 853 // For arrays, check if any element ends with the rule value 854 result = fieldValue.some(function(val) { 855 var valStr = String(val).toLowerCase(); 856 return valStr.indexOf(ruleValueStr) === valStr.length - ruleValueStr.length; 857 }); 858 } else { 859 result = fieldValueStr.indexOf(ruleValueStr) === fieldValueStr.length - ruleValueStr.length; 860 } 861 break; 862 case 'e': 863 case 'empty': 864 if (isArray) { 865 result = fieldValue.length === 0; 866 } else { 867 result = !fieldValue || fieldValueStr === ''; 868 } 869 break; 870 case '!e': 871 case '!empty': 872 if (isArray) { 873 result = fieldValue.length > 0; 874 } else { 875 result = fieldValue && fieldValueStr !== ''; 876 } 877 break; 878 default: 879 if (this.debugMode) { 880 console.warn('[ConditionalLogic] Unknown operator:', operator); 881 } 882 result = false;
883 } 884 885 if (this.debugMode) { 886 console.log('[ConditionalLogic] Rule result:', result); 887 } 888 889 return result; 890 }, 891 892 /** 893 * Map field ID from conditional logic format to HTML format 894 * Conditional logic uses PHP field IDs (like "6") or sequential IDs (like "field_6") 895 * HTML uses field names like "field_6" or IDs like "charitable_field_field_6" 896 */ 897 mapFieldIdToHtml: function(fieldId) { 898 // If already in field_X format, use as is 899 if (typeof fieldId === 'string' && fieldId.indexOf('field_') === 0) { 900 return fieldId; 901 } 902 903 // If numeric, convert to field_X format 904 var numericId = parseInt(fieldId, 10); 905 if (!isNaN(numericId)) { 906 return 'field_' + numericId; 907 } 908 909 // Return as is if can't determine 910 return fieldId; 911 }, 912 913 /** 914 * Toggle field visibility 915 */ 916 toggleField: function(fieldId, show) { 917 // Map field ID to HTML format 918 var mappedFieldId = this.mapFieldIdToHtml(fieldId); 919 var numericId = typeof fieldId === 'string' && fieldId.indexOf('field_') === 0 ? fieldId.replace('field_', '') : fieldId; 920 921 if (this.debugMode) { 922 console.log('[ConditionalLogic] Toggling field - Original ID:', fieldId, 'Mapped ID:', mappedFieldId, 'Numeric ID:', numericId); 923 } 924 925 // Try multiple selectors to find the field 926 var selectors = [ 927 // Try by data attributes 928 '[data-field-id="' + fieldId + '"]', 929 '[data-field-id="' + mappedFieldId + '"]', 930 '[data-field-id="' + numericId + '"]', 931 '[data-charitable-field-id="' + fieldId + '"]', 932 '[data-charitable-field-id="' + mappedFieldId + '"]', 933 // Try by input name 934 'input[name="' + mappedFieldId + '"]', 935 'input[name="field_' + numericId + '"]', 936 // Try by element ID 937 '#charitable_field_' + mappedFieldId + '_element', 938 '#charitable_field_field_' + numericId + '_element', 939 '#charitable-form-field-' + numericId, 940 // Try by fieldset containing field with this name 941 '.charitable-fieldset:has(input[name="' + mappedFieldId + '"])', 942 '.charitable-fieldset:has(#charitable_field_' + mappedFieldId + '_element)', 943 '.charitable-fieldset:has(#charitable_field_field_' + numericId + '_element)' 944 ]; 945 946 var $field = null; 947 for (var i = 0; i < selectors.length; i++) { 948 try { 949 var $found = $(selectors[i]); 950 if ($found.length) { 951 // If we found an input, get its parent fieldset 952 if ($found.is('input, select, textarea')) { 953 $field = $found.closest('.charitable-fieldset'); 954 if (!$field.length) { 955 // Fallback: get parent div with charitable-form-field class 956 $field = $found.closest('.charitable-form-field').closest('.charitable-fieldset'); 957 if (!$field.length) { 958 // Last fallback: get the input's parent container 959 $field = $found.closest('fieldset, .charitable-fieldset, .charitable-form-field'); 960 } 961 } 962 } else { 963 // Already a fieldset or container 964 $field = $found; 965 } 966 if ($field.length) { 967 if (this.debugMode) { 968 console.log('[ConditionalLogic] Field found using selector:', selectors[i]); 969 } 970 break; 971 } 972 } 973 } catch (e) { 974 // Some selectors like :has() might not work in older browsers, skip 975 if (this.debugMode) { 976 console.warn('[ConditionalLogic] Selector failed:', selectors[i], e); 977 } 978 } 979 } 980 981 // If still not found, try to find by searching all fieldsets for the input 982 if (!$field || !$field.length) { 983 var $allInputs = $('.charitable-visual-donation-form input[name="' + mappedFieldId + '"], .charitable-visual-donation-form #charitable_field_' + mappedFieldId + '_element, .charitable-visual-donation-form #charitable_field_field_' + numericId + '_element'); 984 if ($allInputs.length) { 985 $field = $allInputs.first().closest('.charitable-fieldset'); 986 if (!$field.length) { 987 $field = $allInputs.first().closest('.charitable-form-field').parent(); 988 } 989 } 990 } 991 992 if (!$field || !$field.length) { 993 if (this.debugMode) { 994 console.warn('[ConditionalLogic] Field not found:', fieldId, 'Mapped:', mappedFieldId, 'Selectors tried:', selectors.length); 995 console.warn('[ConditionalLogic] Available fields:', $('.charitable-visual-donation-form .charitable-fieldset').map(function() { 996 var $fs = $(this); 997 var name = $fs.find('input, select, textarea').first().attr('name'); 998 var id = $fs.find('input, select, textarea').first().attr('id'); 999 return name || id || 'unknown'; 1000 }).get()); 1001 } 1002 return; 1003 } 1004 1005 if (this.debugMode) { 1006 console.log('[ConditionalLogic] Toggling field:', fieldId, 'Show:', show, 'Element:', $field, 'Element classes:', $field.attr('class')); 1007 } 1008 1009 // Check if field was pending (initially hidden to prevent flash) 1010 var wasPending = $field.hasClass('charitable-conditional-field-pending'); 1011 1012 // Remove pending class - field has been evaluated, no longer need to hide it initially 1013 $field.removeClass('charitable-conditional-field-pending'); 1014 1015 // Show or hide with animation 1016 if (show) { 1017 $field.stop(true, true).fadeIn(300); 1018 $field.removeClass('charitable-conditional-hidden'); 1019 // Re-enable required if field is shown and was originally required 1020 // Check both data attribute (stored when hidden) and field's required-field class 1021 $field.find('input, select, textarea').each(function() { 1022 var $input = $(this);
1023 // Check if field was originally required (stored in data attribute when hidden) 1024 if ($input.attr('data-original-required') === 'true' || $input.data('original-required') === true) { 1025 $input.prop('required', true); 1026 } 1027 // Also check if field wrapper has required-field class (indicates field is required) 1028 else if ($field.hasClass('required-field')) { 1029 $input.prop('required', true); 1030 } 1031 }); 1032 } else { 1033 // Store original required state before hiding 1034 $field.find('input, select, textarea').each(function() { 1035 var $input = $(this); 1036 // Store original required state if not already stored 1037 if (!$(this).data('original-required-set')) { 1038 var wasRequired = $input.prop('required') || $input.is('[required]'); 1039 if (wasRequired) { 1040 $input.attr('data-original-required', 'true'); 1041 $input.data('original-required', true); 1042 } else { 1043 $input.attr('data-original-required', 'false'); 1044 $input.data('original-required', false); 1045 } 1046 $input.data('original-required-set', true); 1047 } 1048 }); 1049 // If field was pending, hide immediately without animation to prevent flash 1050 if (wasPending) { 1051 $field.hide(); 1052 } else { 1053 $field.stop(true, true).fadeOut(300); 1054 } 1055 $field.addClass('charitable-conditional-hidden'); 1056 // Remove required attribute if hidden (HTML5 validation should not apply to hidden fields) 1057 $field.find('input, select, textarea').prop('required', false); 1058 } 1059 }, 1060 1061 /** 1062 * Setup comprehensive debug monitoring 1063 */ 1064 setupDebugMonitoring: function() { 1065 var self = this; 1066 1067 // Monitor all clicks inside the form 1068 $(document).on('click', '.charitable-visual-donation-form', function(e) { 1069 var $target = $(e.target); 1070 var tagName = $target.prop('tagName'); 1071 var type = $target.attr('type'); 1072 var name = $target.attr('name'); 1073 var value = $target.attr('value') || $target.val(); 1074 var id = $target.attr('id'); 1075 1076 var eventData = { 1077 event: 'click', 1078 tag: tagName, 1079 type: type, 1080 name: name, 1081 value: value, 1082 id: id, 1083 classes: $target.attr('class'), 1084 timestamp: new Date().toISOString() 1085 }; 1086 1087 self.logDebugEvent('user_interaction', eventData); 1088 1089 // Special handling for donation amount radios 1090 if (name === 'donation_amount') { 1091 setTimeout(function() { 1092 var checked = $('.charitable-visual-donation-form input[name="donation_amount"]:checked'); 1093 var checkedValue = checked.length ? checked.val() : 'none'; 1094 var amount = self.getDonationAmount(); 1095 1096 self.logDebugEvent('donation_amount_selected', { 1097 selectedValue: checkedValue, 1098 calculatedAmount: amount, 1099 timestamp: new Date().toISOString() 1100 }); 1101 1102 // Show current form state 1103 self.printFormState('After donation amount selection'); 1104 }, 50); 1105 } 1106 }); 1107 1108 // Monitor all change events 1109 $(document).on('change input', '.charitable-visual-donation-form *', function(e) { 1110 var $target = $(e.target); 1111 var fieldName = $target.attr('name'); 1112 var oldValue = self.fieldValueHistory[fieldName] || ''; 1113 var newValue = $target.val() || ''; 1114 1115 if (oldValue !== newValue) { 1116 self.logDebugEvent('field_value_changed', { 1117 fieldName: fieldName, 1118 oldValue: oldValue, 1119 newValue: newValue, 1120 type: $target.attr('type'), 1121 tag: $target.prop('tagName'), 1122 id: $target.attr('id'), 1123 timestamp: new Date().toISOString() 1124 }); 1125 1126 // Update history 1127 self.fieldValueHistory[fieldName] = newValue; 1128 } 1129 }); 1130 1131 // Expose debug functions globally 1132 window.dumpConditionalLogicState = function() { 1133 self.printFormState('Manual dump'); 1134 }; 1135 1136 window.getConditionalLogicHistory = function() { 1137 return self.debugHistory; 1138 }; 1139 1140 window.clearConditionalLogicHistory = function() { 1141 self.debugHistory = []; 1142 self.fieldValueHistory = {}; 1143 console.log('[ConditionalLogic] Debug history cleared'); 1144 }; 1145 }, 1146 1147 /** 1148 * Log a debug event 1149 */ 1150 logDebugEvent: function(eventType, data) { 1151 if (!this.debugMode) { 1152 return; 1153 } 1154 1155 var event = { 1156 type: eventType, 1157 data: data, 1158 timestamp: new Date().toISOString() 1159 }; 1160 1161 this.debugHistory.push(event); 1162 1163 // Keep history to last 100 events 1164 if (this.debugHistory.length > 100) { 1165 this.debugHistory.shift(); 1166 } 1167 1168 // Console output with structured format 1169 console.log('[ConditionalLogic] [' + eventType + ']', data); 1170 }, 1171 1172 /** 1173 * Print current form state for debugging 1174 */ 1175 printFormState: function(context) { 1176 if (!this.debugMode) { 1177 return; 1178 } 1179 1180 console.group('[ConditionalLogic] ===== FORM STATE: ' + (context || 'Current') + ' ====='); 1181 1182 // Current field values 1183 console.group('ð Current Field Values:'); 1184 var fieldValues = {}; 1185 for (var fieldName in this.fieldValues) { 1186 if (this.fieldValues.hasOwnProperty(fieldName)) { 1187 fieldValues[fieldName] = this.fieldValues[fieldName]; 1188 } 1189 } 1190 // Also get donation amount
1191 fieldValues['donation_amount'] = this.getDonationAmount(); 1192 console.table(fieldValues); 1193 console.groupEnd(); 1194 1195 // Conditional logic rules 1196 console.group('ð Conditional Logic Rules:'); 1197 for (var formId in this.rules) { 1198 if (this.rules.hasOwnProperty(formId)) { 1199 console.group('Form: ' + formId); 1200 for (var fieldId in this.rules[formId]) { 1201 if (this.rules[formId].hasOwnProperty(fieldId)) { 1202 var fieldRules = this.rules[formId][fieldId]; 1203 console.group('Field ' + fieldId + ' (' + (fieldRules.conditional_type || 'show') + '):'); 1204 console.log('Rules:', fieldRules); 1205 1206 // Show current value for trigger field 1207 if (fieldRules.conditionals) { 1208 for (var groupId in fieldRules.conditionals) { 1209 if (fieldRules.conditionals.hasOwnProperty(groupId)) { 1210 var group = fieldRules.conditionals[groupId]; 1211 console.group('Group ' + groupId + ':'); 1212 for (var i = 0; i < group.length; i++) { 1213 var rule = group[i]; 1214 var triggerValue = this.getCurrentFieldValue(rule.field); 1215 console.log('Rule ' + (i + 1) + ':', { 1216 triggerField: rule.field, 1217 triggerValue: triggerValue, 1218 operator: rule.operator, 1219 compareValue: rule.value, 1220 passes: this.evaluateRule(rule) 1221 }); 1222 } 1223 console.groupEnd(); 1224 } 1225 } 1226 } 1227 console.groupEnd(); 1228 } 1229 } 1230 console.groupEnd(); 1231 } 1232 } 1233 console.groupEnd(); 1234 1235 // Field visibility status 1236 console.group('ðï¸ Field Visibility Status:'); 1237 for (var formId in this.rules) { 1238 if (this.rules.hasOwnProperty(formId)) { 1239 for (var fieldId in this.rules[formId]) { 1240 if (this.rules[formId].hasOwnProperty(fieldId)) { 1241 var $field = this.findFieldElement(fieldId); 1242 var isVisible = $field && $field.length && $field.is(':visible'); 1243 console.log('Field ' + fieldId + ':', isVisible ? 'â VISIBLE' : 'â HIDDEN'); 1244 } 1245 } 1246 } 1247 } 1248 console.groupEnd(); 1249 1250 // Recent debug events (last 10) 1251 if (this.debugHistory.length > 0) { 1252 console.group('ð Recent Events (last 10):'); 1253 var recentEvents = this.debugHistory.slice(-10); 1254 recentEvents.forEach(function(event, index) { 1255 console.log('[' + (index + 1) + ']', event.type, 'at', event.timestamp, ':', event.data); 1256 }); 1257 console.groupEnd(); 1258 } 1259 1260 console.groupEnd(); 1261 }, 1262 1263 /** 1264 * Find field element by ID (helper for debugging) 1265 */ 1266 findFieldElement: function(fieldId) { 1267 var mappedFieldId = this.mapFieldIdToHtml(fieldId); 1268 var numericId = typeof fieldId === 'string' && fieldId.indexOf('field_') === 0 ? fieldId.replace('field_', '') : fieldId; 1269 1270 var selectors = [ 1271 '[data-field-id="' + fieldId + '"]', 1272 '[data-field-id="' + mappedFieldId + '"]', 1273 '[data-charitable-field-id="' + fieldId + '"]', 1274 'input[name="' + mappedFieldId + '"]', 1275 '#charitable_field_' + mappedFieldId + '_element' 1276 ]; 1277 1278 for (var i = 0; i < selectors.length; i++) { 1279 var $found = $(selectors[i]); 1280 if ($found.length) { 1281 if ($found.is('input, select, textarea')) { 1282 return $found.closest('.charitable-fieldset'); 1283 } 1284 return $found; 1285 } 1286 } 1287 1288 return $(); 1289 } 1290 }; 1291 1292 // Initialize on load 1293 CharitableConditionalLogic.init(); 1294 1295 // Expose globally for debugging 1296 window.CharitableConditionalLogic = CharitableConditionalLogic; 1297 1298})(jQuery); 1299
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.