PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
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
← All changes | includes/widgets/Pricing_Calculator/script.js +988 -0 51.1.2 → 51.1.86 View file →
@@ -1,0 +1,988 @@
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);