PageSourceSearch

https://ihr.org/wp-content/plugins/charitable-pro/assets/js/frontend/conditional-logic.js?ver=1.8.16.4

js ihr.org collected 2026-09-24 09:06:49 UTC 42,477 bytes, 1,299 lines download raw bytes

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.