PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.38
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.38
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / widgets / Pricing_Calculator / script.js

script.js in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.38, at includes/widgets/Pricing_Calculator/script.js

1,098 lines 56.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Pricing Calculator
3 * JavaScript functionality for the Pricing Calculator widget
4 */
5 (function($) {
6 "use strict";
7
8 /**
9 * Initialize the pricing calculator
10 */
11 var initPricingCalculator = function() {
12 $('.king-pricing-calculator').each(function() {
13 var $calculator = $(this);
14 var calculatorId = $calculator.attr('id');
15
16 if ($calculator.data('initialized')) {
17 return;
18 }
19
20 $calculator.data('initialized', true);
21
22 // Calculator settings
23 var basePrice = parseFloat($calculator.data('base-price')) || 0;
24 var decimalPlaces = parseInt($calculator.data('decimal-places')) || 2;
25 var thousandSeparator = $calculator.data('thousand-separator') || ',';
26 var decimalSeparator = $calculator.data('decimal-separator') || '.';
27 var pricePrefix = $calculator.data('price-prefix') || '';
28 var priceSuffix = $calculator.data('price-suffix') || '';
29
30 // Live calculation mode
31 var isLiveMode = $calculator.hasClass('king-pricing-calculator--live');
32
33 // Cache selectors
34 var $fields = $calculator.find('.king-pricing-calculator__field');
35 var $totalPrice = $calculator.find('.king-pricing-calculator__total-price');
36 var $summaryItems = $calculator.find('.king-pricing-calculator__summary-items');
37 var $calculateButton = $calculator.find('.king-pricing-calculator__calculate-button');
38
39 // Debug info - log initial calculator settings
40 console.log('Calculator initialized with base price:', basePrice);
41
42 // Get ajaxurl from localized script if it exists, fallback to global
43 var ajaxUrl = (typeof king_addons_calculator_vars !== 'undefined') ? king_addons_calculator_vars.ajaxurl : (typeof ajaxurl !== 'undefined' ? ajaxurl : '');
44 // Get nonces from localized script
45 var addToCartNonce = (typeof king_addons_calculator_vars !== 'undefined') ? king_addons_calculator_vars.add_to_cart_nonce : '';
46 var sendEmailQuoteNonce = (typeof king_addons_calculator_vars !== 'undefined') ? king_addons_calculator_vars.send_email_quote_nonce : '';
47
48 // Debounce function for performance
49 var debounce = function(func, wait) {
50 var timeout;
51 return function() {
52 var context = this, args = arguments;
53 clearTimeout(timeout);
54 timeout = setTimeout(function() {
55 func.apply(context, args);
56 }, wait);
57 };
58 };
59
60 // Initialize field values and events
61 initFields($fields);
62
63 // Add accessibility attributes
64 addAccessibilityAttributes();
65
66 // Initial calculation
67 calculatePrice();
68
69 // Calculate button click event
70 if ($calculateButton.length && !isLiveMode) {
71 $calculateButton.on('click', calculatePrice);
72 }
73
74 // Initialize Pro feature handlers if Pro is active
75 if ($calculator.hasClass('king-pricing-calculator--pro')) {
76 initProFeatures($calculator);
77 }
78
79 /**
80 * Add ARIA attributes for accessibility
81 */
82 function addAccessibilityAttributes() {
83 // Range sliders
84 $calculator.find('input[type="range"]').each(function() {
85 var $range = $(this);
86 var fieldLabel = $range.closest('.king-pricing-calculator__field').find('.king-pricing-calculator__field-label').text();
87
88 $range.attr({
89 'role': 'slider',
90 'aria-label': fieldLabel,
91 'aria-valuemin': $range.attr('min'),
92 'aria-valuemax': $range.attr('max'),
93 'aria-valuenow': $range.val()
94 });
95
96 // Update aria values on change
97 $range.on('input change', function() {
98 $range.attr('aria-valuenow', $range.val());
99 });
100 });
101 }
102
103 /**
104 * Initialize calculator fields with events
105 */
106 function initFields($fields) {
107 $fields.each(function() {
108 var $field = $(this);
109 var fieldType = $field.data('field-type');
110
111 // Debug info - log field attributes
112 console.log('Field:', fieldType, 'Price Type:', $field.data('price-type'), 'Price:', $field.data('price'));
113
114 // Check if custom formula is used (Pro feature)
115 if ($field.data('price-type') === 'custom') {
116 // Show a one-time console message
117 if (!window.customFormulaMessageShown) {
118 console.info('Custom formula calculation requires Pro version.');
119 window.customFormulaMessageShown = true;
120 }
121 }
122
123 switch (fieldType) {
124 case 'number':
125 var $input = $field.find('input[type="number"]');
126
127 // Ensure default value is set
128 if ($input.val() === '') {
129 $input.val($input.attr('min') || 0);
130 }
131
132 // Debounce input for better performance
133 $input.on('input change', debounce(function() {
134 // Validate input
135 var value = parseFloat($input.val());
136 var min = parseFloat($input.attr('min'));
137 var max = parseFloat($input.attr('max'));
138
139 if (isNaN(value)) {
140 $input.val(min || 0);
141 value = parseFloat($input.val());
142 }
143
144 if (!isNaN(min) && value < min) {
145 $input.val(min);
146 } else if (!isNaN(max) && value > max) {
147 $input.val(max);
148 }
149
150 if (isLiveMode) {
151 calculatePrice();
152 }
153 }, 300));
154 break;
155
156 case 'range':
157 var $range = $field.find('input[type="range"]');
158 var $value = $field.find('.king-pricing-calculator__range-value');
159
160 // Ensure default value is set
161 if ($range.val() === '') {
162 $range.val($range.attr('min') || 0);
163 $value.text($range.val());
164 }
165
166 // Set initial range progress
167 updateRangeProgress($range);
168
169 // Use both input and change events without debounce for immediate feedback
170 $range.on('input', function() {
171 $value.text($range.val());
172
173 // Update range progress visual
174 updateRangeProgress($range);
175
176 // Always calculate on range change regardless of live mode
177 calculatePrice();
178 });
179 break;
180
181 case 'select':
182 var $select = $field.find('select');
183
184 // Ensure the first option is selected by default
185 if (!$select.find('option:selected').length) {
186 $select.find('option:first').prop('selected', true);
187 }
188
189 $select.on('change', function() {
190 if (isLiveMode) {
191 calculatePrice();
192 }
193 });
194 break;
195
196 case 'radio':
197 var $radios = $field.find('input[type="radio"]');
198
199 // Ensure at least one radio is checked
200 if (!$radios.filter(':checked').length) {
201 $radios.first().prop('checked', true);
202 }
203
204 $radios.on('change', function() {
205 if (isLiveMode) {
206 calculatePrice();
207 }
208 });
209 break;
210
211 case 'checkbox':
212 case 'switch':
213 var $checkbox = $field.find('input[type="checkbox"]');
214
215 $checkbox.on('change', function() {
216 if (isLiveMode) {
217 calculatePrice();
218 }
219 });
220 break;
221 }
222 });
223 }
224
225 /**
226 * Calculate the total price based on all field values
227 */
228 function calculatePrice() {
229 try {
230 // Start with base price
231 var total = basePrice;
232 var summaryHtml = '';
233
234 // Debug info
235 console.log('Starting calculation with base price:', total);
236
237 // Advanced Formula will be applied after default calculation
238
239 // First handle additions
240 $fields.each(function() {
241 var $field = $(this);
242 var fieldType = $field.data('field-type');
243 var priceType = $field.data('price-type');
244 var fieldLabel = $field.find('.king-pricing-calculator__field-label').text();
245 var fieldValue = 0;
246 var fieldPrice = 0;
247 var subTotal = 0;
248
249 if (priceType !== 'add') {
250 return; // Skip non-addition fields on first pass
251 }
252
253 try {
254 switch (fieldType) {
255 case 'number':
256 var $input = $field.find('input[type="number"]');
257 fieldValue = parseFloat($input.val());
258 fieldPrice = parseFloat($field.data('price'));
259
260 if (!isNaN(fieldValue) && !isNaN(fieldPrice)) {
261 subTotal = fieldValue * fieldPrice;
262 total += subTotal;
263 }
264 break;
265
266 case 'range':
267 var $range = $field.find('input[type="range"]');
268 fieldValue = parseFloat($range.val());
269 fieldPrice = parseFloat($field.data('price'));
270
271 if (!isNaN(fieldValue) && !isNaN(fieldPrice)) {
272 subTotal = fieldValue * fieldPrice;
273 total += subTotal;
274 }
275 break;
276
277 case 'select':
278 var $select = $field.find('select');
279 var $option = $select.find('option:selected');
280
281 fieldValue = $option.text();
282 fieldPrice = parseFloat($option.data('price'));
283
284 if (!isNaN(fieldPrice)) {
285 subTotal = fieldPrice;
286 total += subTotal;
287 }
288 break;
289
290 case 'radio':
291 var $radio = $field.find('input[type="radio"]:checked');
292
293 if ($radio.length) {
294 fieldValue = $radio.siblings('label').text();
295 fieldPrice = parseFloat($radio.data('price'));
296
297 if (!isNaN(fieldPrice)) {
298 subTotal = fieldPrice;
299 total += subTotal;
300 }
301 }
302 break;
303
304 case 'checkbox':
305 case 'switch':
306 var $checkbox = $field.find('input[type="checkbox"]');
307
308 if ($checkbox.is(':checked')) {
309 fieldValue = 'Yes';
310 fieldPrice = parseFloat($field.data('price'));
311
312 if (!isNaN(fieldPrice)) {
313 subTotal = fieldPrice;
314 total += subTotal;
315 }
316 } else {
317 fieldValue = 'No';
318 }
319 break;
320 }
321
322 // Add to summary if there's a value and subtotal
323 if (fieldValue !== '' && fieldValue !== 0 && subTotal !== 0) {
324 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
325 '<span class="king-pricing-calculator__summary-label">' + fieldLabel + ': ' + fieldValue + '</span>' +
326 '<span class="king-pricing-calculator__summary-value">' + formatPrice(subTotal) + '</span>' +
327 '</div>';
328 }
329
330 // Debug info
331 console.log('Field:', fieldLabel, 'Type:', fieldType, 'Value:', fieldValue, 'Price:', fieldPrice, 'Subtotal:', subTotal);
332
333 } catch (fieldError) {
334 console.error('Error processing field for addition:', fieldError);
335 }
336 });
337
338 // Debug info
339 console.log('After additions, total is:', total);
340
341 // Then handle multiplications
342 $fields.each(function() {
343 var $field = $(this);
344 var fieldType = $field.data('field-type');
345 var priceType = $field.data('price-type');
346 var fieldLabel = $field.find('.king-pricing-calculator__field-label').text();
347 var fieldValue = 0;
348 var fieldPrice = 0;
349 var beforeMult = total;
350
351 if (priceType !== 'multiply') {
352 return; // Skip non-multiplication fields on second pass
353 }
354
355 try {
356 switch (fieldType) {
357 case 'number':
358 var $input = $field.find('input[type="number"]');
359 fieldValue = parseFloat($input.val());
360
361 if (!isNaN(fieldValue) && fieldValue !== 0) {
362 beforeMult = total;
363 total *= fieldValue;
364
365 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
366 '<span class="king-pricing-calculator__summary-label">' + fieldLabel + ': ' + fieldValue + '</span>' +
367 '<span class="king-pricing-calculator__summary-value">' + formatPrice(total - beforeMult) + '</span>' +
368 '</div>';
369 }
370 break;
371
372 case 'range':
373 var $range = $field.find('input[type="range"]');
374 fieldValue = parseFloat($range.val());
375
376 if (!isNaN(fieldValue) && fieldValue !== 0) {
377 beforeMult = total;
378 total *= fieldValue;
379
380 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
381 '<span class="king-pricing-calculator__summary-label">' + fieldLabel + ': ' + fieldValue + '</span>' +
382 '<span class="king-pricing-calculator__summary-value">' + formatPrice(total - beforeMult) + '</span>' +
383 '</div>';
384 }
385 break;
386
387 case 'select':
388 var $select = $field.find('select');
389 var $option = $select.find('option:selected');
390
391 fieldValue = $option.text();
392 fieldPrice = parseFloat($option.data('price'));
393
394 if (!isNaN(fieldPrice) && fieldPrice !== 0) {
395 beforeMult = total;
396 total *= fieldPrice;
397
398 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
399 '<span class="king-pricing-calculator__summary-label">' + fieldLabel + ': ' + fieldValue + '</span>' +
400 '<span class="king-pricing-calculator__summary-value">' + formatPrice(total - beforeMult) + '</span>' +
401 '</div>';
402 }
403 break;
404
405 case 'radio':
406 var $radio = $field.find('input[type="radio"]:checked');
407
408 if ($radio.length) {
409 fieldValue = $radio.siblings('label').text();
410 fieldPrice = parseFloat($radio.data('price'));
411
412 if (!isNaN(fieldPrice) && fieldPrice !== 0) {
413 beforeMult = total;
414 total *= fieldPrice;
415
416 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
417 '<span class="king-pricing-calculator__summary-label">' + fieldLabel + ': ' + fieldValue + '</span>' +
418 '<span class="king-pricing-calculator__summary-value">' + formatPrice(total - beforeMult) + '</span>' +
419 '</div>';
420 }
421 }
422 break;
423
424 case 'checkbox':
425 case 'switch':
426 var $checkbox = $field.find('input[type="checkbox"]');
427
428 if ($checkbox.is(':checked')) {
429 fieldValue = 'Yes';
430 fieldPrice = parseFloat($field.data('price'));
431
432 if (!isNaN(fieldPrice) && fieldPrice !== 0) {
433 beforeMult = total;
434 total *= fieldPrice;
435
436 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
437 '<span class="king-pricing-calculator__summary-label">' + fieldLabel + ': ' + fieldValue + '</span>' +
438 '<span class="king-pricing-calculator__summary-value">' + formatPrice(total - beforeMult) + '</span>' +
439 '</div>';
440 }
441 }
442 break;
443 }
444
445 // Debug info
446 console.log('Field (multiply):', fieldLabel, 'Type:', fieldType, 'Value:', fieldValue, 'BeforeMult:', beforeMult, 'After:', total);
447
448 } catch (fieldError) {
449 console.error('Error processing field for multiplication:', fieldError);
450 }
451 });
452
453 // Then handle custom formula (fallback to addition in free version)
454 $fields.each(function() {
455 var $field = $(this);
456 var fieldType = $field.data('field-type');
457 var priceType = $field.data('price-type');
458
459 if (priceType !== 'custom') {
460 return; // Skip non-custom fields
461 }
462
463 try {
464 // In the free version, just treat custom formula as add
465 var fieldLabel = $field.find('.king-pricing-calculator__field-label').text();
466 var fieldValue, fieldPrice, subTotal = 0;
467
468 switch (fieldType) {
469 case 'number':
470 case 'range':
471 var $input = fieldType === 'number' ?
472 $field.find('input[type="number"]') :
473 $field.find('input[type="range"]');
474
475 fieldValue = parseFloat($input.val());
476 fieldPrice = parseFloat($field.data('price'));
477
478 if (!isNaN(fieldValue) && !isNaN(fieldPrice)) {
479 // Fallback to addition in free version
480 subTotal = fieldValue * fieldPrice;
481 total += subTotal;
482
483 summaryHtml += '<div class="king-pricing-calculator__summary-item">' +
484 '<span class="king-pricing-calculator__summary-label">' +
485 fieldLabel + ': ' + fieldValue +
486 ' <small>(Pro formula fallback)</small>' +
487 '</span>' +
488 '<span class="king-pricing-calculator__summary-value">' + formatPrice(subTotal) + '</span>' +
489 '</div>';
490 }
491 break;
492
493 case 'select':
494 case 'radio':
495 case 'checkbox':
496 case 'switch':
497 // Handle similar to add, with pro notice
498 // Implementation follows similar pattern to the addition case
499 break;
500 }
501
502 console.log('Custom formula field (free fallback):', fieldLabel, 'Added:', subTotal);
503
504 } catch (fieldError) {
505 console.error('Error processing custom formula field:', fieldError);
506 }
507 });
508
509 // Advanced Formula override (Pro) after default calculation loops
510 if ($calculator.hasClass('king-pricing-calculator--advanced-formula')) {
511 var formulaType = $calculator.data('formulaType') || 'standard';
512 // Map fields for formula execution
513 var fieldsMap = {};
514 var aliasMap = {}; // Create a user-friendly alias map using custom field IDs
515 $fields.each(function() {
516 var $field = $(this);
517 var id = $field.data('field-id');
518 var type = $field.data('field-type');
519 var value;
520 if (type === 'checkbox' || type === 'switch') {
521 value = $field.find('input[type="checkbox"]').is(':checked');
522 } else if (type === 'radio') {
523 value = $field.find('input[type="radio"]:checked').val();
524 } else {
525 var v = $field.find('input, select').val();
526 var num = parseFloat(v);
527 value = isNaN(num) ? v : num;
528 }
529 fieldsMap[id] = value;
530
531 // If this is a custom field ID (starting with 'king-calc-'), create an alias
532 if (id.indexOf('king-calc-') === 0) {
533 var customId = id.replace('king-calc-', '');
534 aliasMap[customId] = value;
535 }
536 });
537
538 // For debugging custom formulas
539 console.log('Available field IDs:', fieldsMap);
540 console.log('User-friendly field aliases:', aliasMap);
541
542 // Apply chosen formula on the aggregated total
543 var newTotal = total;
544 if (formulaType === 'exponential') {
545 var expBase = parseFloat($calculator.data('exponentialBase')) || 1;
546 newTotal = Math.pow(total, expBase);
547 } else if (formulaType === 'logarithmic') {
548 var logBase = parseFloat($calculator.data('logarithmicBase')) || Math.E;
549 // Avoid log(0) or log of negative
550 newTotal = total > 0 ? Math.log(total) / Math.log(logBase) : 0;
551 } else if (formulaType === 'custom') {
552 var customCode = $calculator.data('customFormula') || '';
553 try {
554 // Security fix: Only allow safe mathematical operations
555 if (!isCustomFormulaSafe(customCode)) {
556 console.error('Custom formula contains unsafe code');
557 return;
558 }
559 // Pass both the full field map and user-friendly aliases
560 var fn = new Function('fields', 'basePrice', 'aliases', customCode);
561 newTotal = fn(fieldsMap, basePrice, aliasMap);
562 } catch (e) {
563 console.error('Custom formula error:', e);
564 }
565
566 function isCustomFormulaSafe(formula) {
567 // Only allow safe mathematical operations
568 var safePatterns = [
569 /^[0-9+\-*/().\s]+$/, // Basic math
570 /Math\.(abs|round|ceil|floor|min|max|pow|sqrt|sin|cos|tan|log|exp|PI|E)/g, // Safe Math functions
571 ];
572
573 for (var i = 0; i < safePatterns.length; i++) {
574 if (safePatterns[i].test(formula)) {
575 return true;
576 }
577 }
578
579 // Check for dangerous patterns
580 var dangerousPatterns = [
581 /eval|Function|new|this|window|document|script|alert|prompt|confirm/g,
582 /[\[\]{}]/g, // Object/array notation
583 /\w+\s*\(/g, // Function calls (except Math functions already checked)
584 ];
585
586 for (var i = 0; i < dangerousPatterns.length; i++) {
587 if (dangerousPatterns[i].test(formula)) {
588 return false;
589 }
590 }
591
592 return true;
593 }
594 }
595 // Override total with formula result
596 total = (typeof newTotal === 'number' && !isNaN(newTotal)) ? newTotal : total;
597 }
598
599 // Final check to prevent NaN
600 if (isNaN(total)) {
601 console.error('Total calculation resulted in NaN');
602 total = basePrice;
603 }
604
605 // Debug info
606 console.log('Final total:', total);
607
608 // Update the total price
609 $totalPrice.text(formatPrice(total));
610
611 // Update the summary items
612 $summaryItems.html(summaryHtml);
613 } catch(e) {
614 console.error('Calculation error:', e);
615 $totalPrice.text(formatPrice(basePrice));
616 }
617 }
618
619 /**
620 * Format a price number with proper separators
621 */
622 function formatPrice(number) {
623 if (isNaN(number)) {
624 console.error('Attempting to format NaN as price');
625 number = 0;
626 }
627 var parts = number.toFixed(decimalPlaces).toString().split('.');
628 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
629 return pricePrefix + parts.join(decimalSeparator) + priceSuffix;
630 }
631
632 /**
633 * Update the visual appearance of range slider progress
634 */
635 function updateRangeProgress($range) {
636 var min = parseFloat($range.attr('min')) || 0;
637 var max = parseFloat($range.attr('max')) || 100;
638 var value = parseFloat($range.val()) || 0;
639 var progress = ((value - min) / (max - min)) * 100;
640
641 // Set CSS variable for the progress
642 $range.css('--range-progress', progress + '%');
643 }
644 });
645 };
646
647 /**
648 * Initialize Pro feature handlers for Pricing Calculator
649 * @param jQuery $calculator
650 */
651 function initProFeatures($calculator) {
652 var calculatorId = $calculator.attr('id');
653 var $summaryItems = $calculator.find('.king-pricing-calculator__summary-items');
654 var $totalPrice = $calculator.find('.king-pricing-calculator__total-price');
655
656 // Use localized nonces if available, fallback to data attributes
657 var addToCartNonce = addToCartNonce || $calculator.data('addToCartNonce');
658 var sendEmailNonce = sendEmailQuoteNonce || $calculator.data('sendEmailNonce');
659 var recipientEmail = $calculator.data('recipientEmail');
660 var emailSubject = $calculator.data('emailSubject');
661
662 // Try to get conditional rules from data attribute, accounting for possible casing issues
663 var conditionalRules;
664 try {
665 // First try direct data attribute access
666 conditionalRules = $calculator.data('conditionalRules');
667
668 // If that doesn't work, try to get the raw attribute and parse it
669 if (!conditionalRules && $calculator.attr('data-conditional-rules')) {
670 console.log('Found data-conditional-rules attribute, trying to parse:', $calculator.attr('data-conditional-rules'));
671 conditionalRules = JSON.parse($calculator.attr('data-conditional-rules'));
672 }
673
674 console.log('Conditional rules loaded:', conditionalRules);
675 } catch (e) {
676 console.error('Error parsing conditional rules:', e);
677 }
678
679 // Add to Cart Integration
680 $calculator.find('.king-pricing-calculator-pro__add-to-cart-button').on('click', function() {
681 var price = parseFloat($totalPrice.text().replace(/[^0-9\.\-]/g, '')) || 0;
682 var productId = $(this).data('productId') || '';
683 var includeDetails = $(this).data('includeDetails') === 'yes';
684 var productType = productId ? 'specific' : 'dynamic';
685 var data = {
686 action: 'king_addons_add_to_cart',
687 nonce: addToCartNonce,
688 price: price,
689 product_type: productType,
690 product_id: productId,
691 quantity: 1,
692 use_as_budget: includeDetails
693 };
694 $.post(ajaxUrl, data, function(response) {
695 if (response.success) {
696 alert(response.data.message);
697 if (response.data.fragments) {
698 $.each(response.data.fragments, function(key, html) {
699 $(key).replaceWith(html);
700 });
701 }
702 } else {
703 alert(response.data.message || 'Error adding to cart.');
704 }
705 });
706 });
707
708 // Email Quote Option
709 var $emailButton = $calculator.find('.king-pricing-calculator-pro__email-button');
710 var $emailForm = $calculator.find('.king-pricing-calculator-pro__email-form');
711 $emailButton.on('click', function() {
712 $emailForm.toggle();
713 });
714 $emailForm.find('.king-pricing-calculator-pro__cancel-email-button').on('click', function() {
715 $emailForm.hide();
716 });
717 $emailForm.find('.king-pricing-calculator-pro__send-email-button').on('click', function() {
718 var name = $emailForm.find('#quote_name').val();
719 var email = $emailForm.find('#quote_email').val();
720 var message = $emailForm.find('#quote_message').val();
721 var quoteHtml = $summaryItems.html();
722 var totalHtml = '<div class="king-pricing-calculator__summary-item"><span class="king-pricing-calculator__summary-label">Total</span><span class="king-pricing-calculator__summary-value">' + $totalPrice.text() + '</span></div>';
723 var data = {
724 action: 'king_addons_send_email_quote',
725 nonce: sendEmailNonce,
726 name: name,
727 email: email,
728 message: message,
729 quote_data: quoteHtml + totalHtml,
730 recipient: recipientEmail,
731 subject: emailSubject
732 };
733 $.post(ajaxUrl, data, function(response) {
734 if (response.success) {
735 alert(response.data.message);
736 $emailForm.hide();
737 } else {
738 alert(response.data.message || 'Error sending quote.');
739 }
740 });
741 });
742
743 // Generate PDF Quote (print to PDF)
744 $calculator.find('.king-pricing-calculator-pro__pdf-button').on('click', function() {
745 // Create a more styled printable version with better formatting
746 var calculatorTitle = $calculator.find('.king-pricing-calculator__title').text() || document.title;
747 var calculatorDesc = $calculator.find('.king-pricing-calculator__description').text() || '';
748 var logo = $calculator.data('companyLogo') || '';
749 var logoHtml = logo ? '<img src="' + logo + '" alt="Company Logo" style="max-width: 200px; margin-bottom: 20px;">' : '';
750
751 // Get current date in a nice format
752 var today = new Date();
753 var date = today.toLocaleDateString();
754
755 // Create a well-formatted HTML document for printing
756 var content = '<html><head>' +
757 '<title>' + calculatorTitle + ' - Quote</title>' +
758 '<style>' +
759 'body { font-family: Arial, sans-serif; padding: 30px; max-width: 800px; margin: 0 auto; color: #333; }' +
760 '.quote-header { margin-bottom: 30px; border-bottom: 1px solid #eee; padding-bottom: 20px; }' +
761 '.quote-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; color: #2c3e50; }' +
762 '.quote-date { font-size: 14px; color: #7f8c8d; margin-bottom: 15px; }' +
763 '.quote-description { margin-bottom: 20px; }' +
764 '.quote-items { margin-bottom: 30px; }' +
765 '.quote-item { padding: 10px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }' +
766 '.quote-item:last-child { border-bottom: none; }' +
767 '.quote-label { font-weight: normal; }' +
768 '.quote-value { font-weight: bold; }' +
769 '.quote-total { padding: 15px 0; border-top: 2px solid #2c3e50; display: flex; justify-content: space-between; font-weight: bold; }' +
770 '.quote-total .quote-label { font-size: 18px; }' +
771 '.quote-total .quote-value { font-size: 18px; color: #2c3e50; }' +
772 '</style>' +
773 '</head><body>' +
774 '<div class="quote-header">' +
775 logoHtml +
776 '<div class="quote-title">' + calculatorTitle + '</div>' +
777 '<div class="quote-date">Date: ' + date + '</div>' +
778 (calculatorDesc ? '<div class="quote-description">' + calculatorDesc + '</div>' : '') +
779 '</div>' +
780 '<div class="quote-items">';
781
782 // Add all line items
783 $summaryItems.find('.king-pricing-calculator__summary-item').each(function() {
784 var label = $(this).find('.king-pricing-calculator__summary-label').text();
785 var value = $(this).find('.king-pricing-calculator__summary-value').text();
786 content += '<div class="quote-item">' +
787 '<span class="quote-label">' + label + '</span>' +
788 '<span class="quote-value">' + value + '</span>' +
789 '</div>';
790 });
791
792 // Add the total
793 content += '</div>' +
794 '<div class="quote-total">' +
795 '<span class="quote-label">Total</span>' +
796 '<span class="quote-value">' + $totalPrice.text() + '</span>' +
797 '</div>' +
798 '</body></html>';
799
800 // Open a new window, write the content, and trigger print
801 var win = window.open('', '_blank');
802 win.document.write(content);
803 win.document.close();
804
805 // Slight delay to ensure content is loaded before printing
806 setTimeout(function() {
807 win.focus();
808 win.print();
809 // Don't close automatically to allow users to save as PDF
810 // win.close();
811 }, 250);
812 });
813
814 // Save/Load Calculations using localStorage
815 var storageKey = calculatorId + '_data';
816
817 $calculator.find('.king-pricing-calculator-pro__save-button').on('click', function() {
818 try {
819 var fieldsData = {};
820 var hasValues = false;
821
822 // Collect all field values
823 $calculator.find('.king-pricing-calculator__field').each(function() {
824 var $field = $(this);
825 var id = $field.data('field-id');
826 var type = $field.data('field-type');
827 var val;
828
829 if (type === 'checkbox' || type === 'switch') {
830 val = $field.find('input[type="checkbox"]').prop('checked');
831 } else if (type === 'radio') {
832 val = $field.find('input[type="radio"]:checked').val();
833 } else {
834 val = $field.find('input, select').val();
835 }
836
837 if (val !== undefined && val !== null && val !== '') {
838 hasValues = true;
839 }
840
841 fieldsData[id] = val;
842 });
843
844 // Add metadata
845 fieldsData._meta = {
846 date: new Date().toISOString(),
847 total: $totalPrice.text(),
848 calculatorId: calculatorId
849 };
850
851 if (!hasValues) {
852 alert('No values to save. Please fill in the calculator fields first.');
853 return;
854 }
855
856 // Save to localStorage
857 localStorage.setItem(storageKey, JSON.stringify(fieldsData));
858 alert('Calculation saved successfully.');
859 } catch (error) {
860 console.error('Error saving calculation:', error);
861 alert('There was an error saving your calculation. Please try again.');
862 }
863 });
864
865 $calculator.find('.king-pricing-calculator-pro__load-button').on('click', function() {
866 try {
867 var saved = localStorage.getItem(storageKey);
868 if (!saved) {
869 alert('No saved calculation found for this calculator.');
870 return;
871 }
872
873 var fieldsData = JSON.parse(saved);
874 var foundFields = false;
875
876 // Check if metadata matches this calculator
877 if (fieldsData._meta && fieldsData._meta.calculatorId && fieldsData._meta.calculatorId !== calculatorId) {
878 alert('The saved calculation is for a different calculator. Unable to load.');
879 return;
880 }
881
882 // Apply the saved values to fields
883 $.each(fieldsData, function(id, val) {
884 // Skip metadata
885 if (id === '_meta') return;
886
887 var $field = $calculator.find('[data-field-id="' + id + '"]');
888 if ($field.length) {
889 foundFields = true;
890 var type = $field.data('field-type');
891
892 if (type === 'checkbox' || type === 'switch') {
893 $field.find('input[type="checkbox"]').prop('checked', val);
894 } else if (type === 'radio') {
895 $field.find('input[type="radio"]').filter('[value="' + val + '"]').prop('checked', true);
896 } else {
897 $field.find('input, select').val(val).trigger('change');
898 }
899 }
900 });
901
902 if (!foundFields) {
903 alert('No saved values could be applied to this calculator.');
904 return;
905 }
906
907 // Recalculate with the loaded values
908 calculatePrice();
909
910 // Show success message with saved date if available
911 var savedDate = fieldsData._meta && fieldsData._meta.date ?
912 new Date(fieldsData._meta.date).toLocaleString() : '';
913
914 if (savedDate) {
915 alert('Calculation loaded successfully. (Saved on: ' + savedDate + ')');
916 } else {
917 alert('Calculation loaded successfully.');
918 }
919 } catch (error) {
920 console.error('Error loading calculation:', error);
921 alert('There was an error loading your calculation. The saved data may be corrupted.');
922 }
923 });
924
925 // Conditional Logic
926 if (conditionalRules) {
927 console.log('Conditional rules found:', conditionalRules);
928 function evaluateCondition(val, operator, expected) {
929 try {
930 // Convert values for proper comparison
931 var compVal = val;
932 var compExpected = expected;
933
934 // For numeric comparisons, explicitly convert to numbers
935 if (operator === 'greater' || operator === 'less') {
936 compVal = parseFloat(val);
937 compExpected = parseFloat(expected);
938
939 // Check if we have valid numbers
940 if (isNaN(compVal) || isNaN(compExpected)) {
941 console.warn('Invalid numeric comparison with NaN:', val, operator, expected);
942 return false;
943 }
944 }
945
946 // Special handling for boolean values
947 if (operator === 'is_checked' || operator === 'is_not_checked') {
948 // These operators don't use the expected value, they check the val directly
949 return operator === 'is_checked' ? compVal === true : compVal === false;
950 }
951
952 // Log the comparison
953 console.log('Comparing:', compVal, operator, compExpected);
954
955 // Perform the comparison
956 switch (operator) {
957 case 'equal': return compVal == compExpected;
958 case 'not_equal': return compVal != compExpected;
959 case 'greater': return compVal > compExpected;
960 case 'less': return compVal < compExpected;
961 case 'contains': return ('' + compVal).indexOf(compExpected) !== -1;
962 default: return false;
963 }
964 } catch (e) {
965 console.error('Error evaluating condition:', e);
966 return false;
967 }
968 }
969 $.each(conditionalRules, function(_, rule) {
970 console.log('Processing rule:', rule);
971
972 // Improve field selection to handle both custom IDs and auto-generated IDs
973 var $ifField, $targetField;
974
975 // Try to find the "if" field, first by exact ID, then by custom ID
976 $ifField = $calculator.find('[data-field-id="' + rule.if_field + '"]');
977 if (!$ifField.length) {
978 // Try finding a field with a custom ID (king-calc-*)
979 $ifField = $calculator.find('[data-field-id="king-calc-' + rule.if_field + '"]');
980 }
981
982 // Try to find the target field, first by exact ID, then by custom ID
983 $targetField = $calculator.find('[data-field-id="' + rule.target_field + '"]');
984 if (!$targetField.length) {
985 // Try finding a field with a custom ID (king-calc-*)
986 $targetField = $calculator.find('[data-field-id="king-calc-' + rule.target_field + '"]');
987 }
988
989 console.log('Condition field found:', $ifField.length > 0, 'Target field found:', $targetField.length > 0);
990
991 // Log actual field IDs to help debug
992 if ($ifField.length > 0) {
993 console.log('If field ID:', $ifField.data('field-id'));
994 }
995 if ($targetField.length > 0) {
996 console.log('Target field ID:', $targetField.data('field-id'));
997 }
998
999 // If either field is not found, skip this rule
1000 if (!$ifField.length || !$targetField.length) {
1001 console.warn('Could not find one or both fields for this rule, skipping');
1002 return true; // Continue to next rule
1003 }
1004
1005 function applyRule() {
1006 var val;
1007 var type = $ifField.data('field-type');
1008
1009 // Log the field type to debug
1010 console.log('Evaluating field type:', type);
1011
1012 if (type === 'checkbox' || type === 'switch') {
1013 val = $ifField.find('input[type="checkbox"]').prop('checked');
1014 } else if (type === 'radio') {
1015 val = $ifField.find('input[type="radio"]:checked').val();
1016 } else {
1017 val = $ifField.find('input, select').val();
1018 }
1019
1020 // Log the actual field value
1021 console.log('Field value:', val, 'comparing with:', rule.value, 'using operator:', rule.operator);
1022
1023 var result = evaluateCondition(val, rule.operator, rule.value);
1024 console.log('Condition evaluation result:', result);
1025
1026 // Apply the action based on the result
1027 try {
1028 switch (rule.action) {
1029 case 'show':
1030 // Use CSS display property to properly show/hide
1031 if (result) {
1032 $targetField.css('display', ''); // Use default display
1033 } else {
1034 $targetField.css('display', 'none');
1035 }
1036 break;
1037 case 'hide':
1038 // Use CSS display property to properly show/hide (inverse of show)
1039 if (!result) {
1040 $targetField.css('display', ''); // Use default display
1041 } else {
1042 $targetField.css('display', 'none');
1043 }
1044 break;
1045 case 'enable':
1046 $targetField.find('input, select').prop('disabled', !result);
1047 if (!result) {
1048 $targetField.addClass('king-pricing-calculator__field--disabled');
1049 } else {
1050 $targetField.removeClass('king-pricing-calculator__field--disabled');
1051 }
1052 break;
1053 case 'disable':
1054 $targetField.find('input, select').prop('disabled', result);
1055 if (result) {
1056 $targetField.addClass('king-pricing-calculator__field--disabled');
1057 } else {
1058 $targetField.removeClass('king-pricing-calculator__field--disabled');
1059 }
1060 break;
1061 case 'set_value':
1062 if (result) {
1063 var $input = $targetField.find('input, select');
1064 $input.val(rule.set_value).trigger('change');
1065 console.log('Set value to:', rule.set_value);
1066 }
1067 break;
1068 }
1069 } catch (e) {
1070 console.error('Error applying action:', e);
1071 }
1072
1073 // Log the action taken
1074 console.log('Applied action:', rule.action, 'with result:', result);
1075 }
1076 $ifField.on('change input', applyRule);
1077
1078 // Also execute immediately once to set initial state
1079 applyRule();
1080 });
1081 }
1082 }
1083
1084 // Initialize on document ready
1085 $(document).ready(function() {
1086 initPricingCalculator();
1087 });
1088
1089 // Initialize after Elementor frontend init
1090 $(window).on('elementor/frontend/init', function() {
1091 if (typeof elementorFrontend !== 'undefined') {
1092 elementorFrontend.hooks.addAction('frontend/element_ready/king-addons-pricing-calculator.default', function() {
1093 initPricingCalculator();
1094 });
1095 }
1096 });
1097
1098 })(jQuery);