| @@ -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, '<') | |
| 70 | + .replace(/>/g, '>') | |
| 71 | + .replace(/"/g, '"') | |
| 72 | + .replace(/'/g, '''); | |
| 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); | |