PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
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 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Pricing_Calculator / script.js

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

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