PageSourceSearch

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

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

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.