1/* eslint camelcase: [2, {properties: "never"}] */ 2/* global woocommerce_addons_params, jQuery, accounting */ 3( function( $, window ) { 4 5 // This script is not yet ready to be publicly stored in the window. 6 WC_PAO = window.WC_PAO || {}; 7 8 WC_PAO.initialized_forms = []; 9 10 WC_PAO.Helper = { 11 12 /** 13 * Escapes HTML. 14 * 15 * @param html 16 * @returns {*} 17 */ 18 escapeHtml: function( html ) { 19 return document 20 .createElement( 'div' ) 21 .appendChild(document.createTextNode( html ) ).parentNode 22 .innerHTML; 23 }, 24 25 /** 26 * Determines if a subscription is selected in a grouped product. 27 * 28 * @returns {boolean} 29 */ 30 isGroupedSubsSelected: function () { 31 var group = $( '.product-type-grouped' ), 32 subs = false; 33 34 if ( group.length ) { 35 group.find( '.group_table tr.product' ).each( function () { 36 if ( 0 < $(this).find( '.input-text.qty' ).val() ) { 37 if ( 38 $(this).find( '.entry-summary .subscription-details' ) 39 .length 40 ) { 41 subs = true; 42 return false; 43 } 44 } 45 }); 46 } 47 48 return subs; 49 }, 50 51 /** 52 * Function to add minutes on a given Date object. 53 * 54 * @param {Date} date The Date object to be converted. 55 * @param {integer} minutes Number of minutes. 56 * @return {Date} The new Date including added minutes. 57 */ 58 addMinutes: function( date, minutes ) { 59 return new Date( date.getTime() + minutes * 60000 ); 60 }, 61 62 /** 63 * Determines if a product is a mixed or grouped product type. 64 * 65 * @returns {boolean} 66 */ 67 isGroupedMixedProductType: function() { 68 var group = $( '.product-type-grouped' ), 69 subs = 0, 70 simple = 0; 71 72 if ( group.length ) { 73 group.find( '.group_table tr.product' ).each( function () { 74 if ( 0 < $(this).find( '.input-text.qty' ).val() ) { 75 // For now only checking between simple and subs. 76 if ( 77 $(this).find( '.entry-summary .subscription-details' ) 78 .length 79 ) { 80 subs++; 81 } else { 82 simple++; 83 } 84 } 85 }); 86 87 if ( 0 < subs && 0 < simple ) { 88 return true; 89 } 90 } 91 92 return false; 93 }, 94 95 /** 96 * Delays the execution of the callback function by ms. 97 * 98 * @param callback 99 * @param ms 100 */ 101 delay: function( callback, ms ) { 102 var timer = 0; 103 104 clearTimeout( timer ); 105 timer = setTimeout( callback, ms ); 106 }, 107 }; 108 109 WC_PAO.Form = ( function () { 110 111 /** 112 * Addons Form Controller. 113 * 114 * @param object $form 115 */ 116 function Form( $form ) { 117 // Make sure is called as a constructor. 118 if ( ! ( this instanceof Form ) ) { 119 return new Form( $form ); 120 } 121 122 if ( ! $form.length ) { 123 return false; 124 } 125 126 // Holds the jQuery instance. 127 this.$el = $form; 128 this.$addons = this.$el.find( '.wc-pao-addon-field' ); 129 130 if ( ! this.$addons.length ) { 131 this.$addons = false; 132 return false; 133 } 134 135 this.is_rtl = 'rtl' === document.documentElement.dir; 136 this.validation = new Validation( this ); 137 this.totals = new Totals( this ); 138 this.show_incomplete_subtotals = this.totals.showIncompleteSubtotals(); 139 this.contains_required = this.containsRequired(); 140 141 this.setupEvents(); 142 143 this.validation.validate(); 144 this.updateTotals(); 145 146 $( '.wc-pao-addon-image-swatch' ).tipTip({ delay: 200 } ); 147 148 WC_PAO.initialized_forms.push( this ); 149 } 150 151 /** 152 * Sets up event listeners. 153 */ 154 Form.prototype.setupEvents = function() { 155 156 var self = this; 157 158 // Validate addons on form submit. 159 self.$el.find( 'button[type="submit"]' ).on( 'click', function () { 160 161 if ( self.validation.validate( true ) ) { 162 return true; 163 } 164 165 // Scroll viewport to the first invalid configured addon, if it not currently in viewport. 166 var $messages = self.$el.find( '.wc-pao-validation-notice' ); 167 168 if( $messages.length > 0 ) { 169 var $first_invalid_addon = self.$el.find( $messages[0].closest( '.wc-pao-addon-container' ) ); 170 171 if ( $first_invalid_addon.length > 0 && ! self.is_in_viewport( $first_invalid_addon ) ) { 172 $first_invalid_addon[0].scrollIntoView(); 173 } 174 } 175 176 return false;
177 }); 178 179 /** 180 * Addons value changed. 181 */ 182 self.$el.on( 183 'blur change', 184 '.wc-pao-addon input, .wc-pao-addon textarea, .wc-pao-addon select, .wc-pao-addon-custom-text', 185 function () { 186 187 self.validation.validateAddon( $(this), true ); 188 self.updateTotals(); 189 } 190 ); 191 192 self.$el.on( 193 'keyup', 194 '.wc-pao-addon input, .wc-pao-addon textarea, .wc-pao-addon-custom-text', 195 function () { 196 var $addon = $(this); 197 198 WC_PAO.Helper.delay( function() { 199 self.validation.validateAddon( $addon, true ); 200 self.updateTotals(); 201 }, 300 ); 202 } 203 ); 204 205 // Product quantity changed. 206 self.$el.on( 207 'change', 208 'input.qty', 209 function () { 210 self.updateTotals(); 211 } 212 ); 213 214 // Special handling for image swatches. 215 // When a user clicks on an image swatch, the selection is transferred to a hidden select element. 216 var touchTime; 217 218 self.$el.on( 219 'touchstart', 220 '.wc-pao-addon-image-swatch', 221 function (e) { 222 touchTime = new Date(); 223 } 224 ); 225 226 self.$el.on( 227 'click touchend', 228 '.wc-pao-addon-image-swatch', 229 function (e) { 230 e.preventDefault(); 231 232 if ( 'touchend' === e.type && touchTime ) { 233 var diff = new Date() - touchTime; 234 235 if ( diff > 100 ) { 236 // This is a scroll event and not a tap, so skip. 237 return; 238 } 239 } 240 241 var selected_value = $(this).data( 'value' ), 242 $parent = $(this).parents( '.wc-pao-addon-wrap' ), 243 label = $.parseHTML( $(this).data( 'price' ) ), 244 $selected = $parent.find( '.wc-pao-addon-image-swatch-selected-swatch' ); 245 246 // Clear selected swatch. 247 $selected.html( '' ); 248 249 // Clear all selected. 250 $parent 251 .find( '.wc-pao-addon-image-swatch' ) 252 .removeClass( 'selected' ); 253 254 // Select this swatch. 255 $(this).addClass( 'selected' ); 256 257 // Set the value in hidden select field. 258 $parent 259 .find( '.wc-pao-addon-image-swatch-select' ) 260 .val( selected_value ); 261 262 // Display selected label below swatches. 263 $selected.html( label ); 264 265 self.validation.validateAddon( $parent.find( 'select.wc-pao-addon-field' ), true ); 266 self.updateTotals(); 267 } 268 ); 269 270 /** 271 * Variable Products. 272 */ 273 274 // Reset addon totals when the variation selection is cleared. The form is not valid until a variation is selected. 275 self.$el.on( 'click', '.reset_variations', function () { 276 self.totals.reset(); 277 }); 278 279 // When the variation form initially loads. 280 self.$el.on( 'wc_variation_form', function () { 281 self.validation.validate(); 282 self.updateTotals(); 283 }); 284 285 // When a new variation is selected, validate the form and update the addons totals. 286 self.$el.on( 'found_variation', function ( event, variation ) {
287 self.totals.updateVariation( variation ); 288 self.validation.validate(); 289 self.updateTotals(); 290 }); 291 292 // When a variation selection is cleared by selecting "Choose an option...", reset totals as the form becomes invalid. 293 self.$el.on( 'hide_variation', function ( event ) { 294 self.updateTotals(); 295 }); 296 297 self.$el.on( 'woocommerce-product-addons-update', function () { 298 self.validation.validate(); 299 self.updateTotals(); 300 }); 301 302 /** 303 * Add-ons Datepicker extend. 304 */ 305 var $datepickers = self.$el.find( '.datepicker' ); 306 307 $datepickers.each( function() { 308 309 // Cache local instances. 310 var $datepicker = $( this ), 311 $container = $datepicker.parent(), 312 $clear_button = $container.find( '.reset_date' ), 313 $timestamp_input = $container.find( 'input[name="' + $datepicker.attr( 'name' ) + '-wc-pao-date"]' ), 314 $offset_gmt_input = $container.find( 'input[name="' + $datepicker.attr( 'name' ) + '-wc-pao-date-gmt-offset"]' ); 315 316 // Make Template backwards compatible. 317 if ( ! $offset_gmt_input.length ) { 318 $offset_gmt_input = $( '<input/>' ); 319 $offset_gmt_input.attr( 'type', 'hidden' ); 320 $offset_gmt_input.attr( 'name', $datepicker.attr( 'name' ) + '-wc-pao-date-gmt-offset' ); 321 $container.append( $offset_gmt_input ); 322 } 323 324 // Fill GMT offset. 325 var now = new Date(), 326 gmt_offset = parseFloat( woocommerce_addons_params.gmt_offset, 10 ), 327 client_offset = now.getTimezoneOffset() / 60, 328 diff = client_offset - gmt_offset; 329 330 if ( 'default' === woocommerce_addons_params.date_input_timezone_reference ) { 331 332 $offset_gmt_input.val( client_offset ); 333 } else if ( 'store' === woocommerce_addons_params.date_input_timezone_reference ) { 334 335 var hours_now = now.getHours() + now.getMinutes() / 60, 336 day_factor = hours_now + diff; 337 338 $offset_gmt_input.val( gmt_offset ); 339 } 340 341 // Init datepicker. 342 $datepicker.datepicker( { 343 beforeShow: function( input, el ) { 344 if ( woocommerce_addons_params.datepicker_class ) { 345 $('#ui-datepicker-div').removeClass( woocommerce_addons_params.datepicker_class ); 346 $('#ui-datepicker-div').addClass( woocommerce_addons_params.datepicker_class ); 347 } 348 }, 349 dateFormat: woocommerce_addons_params.datepicker_date_format, 350 changeMonth: true, 351 changeYear: true, 352 yearRange: "c-100:c+10", 353 } ); 354 355 // Fill hidden inputs with selected date if any. 356 var currentDate = $datepicker.datepicker( 'getDate' ); 357 if ( null !== currentDate && typeof currentDate.getTime === 'function' ) { 358 359 // Append current time. 360 currentDate.setHours( now.getHours(), now.getMinutes() ); 361 362 if ( 'store' === woocommerce_addons_params.date_input_timezone_reference ) { 363 currentDate = WC_PAO.Helper.addMinutes( currentDate, -1 * client_offset * 60 ); 364 currentDate = WC_PAO.Helper.addMinutes( currentDate, gmt_offset * 60 ); 365 } 366 367 $timestamp_input.val( currentDate.getTime() / 1000 ); 368 $clear_button.show(); 369 } 370 371 // On Change. 372 $datepicker.on( 'change', function() { 373 374 var selectedDate = $datepicker.datepicker( 'getDate' ); 375 if ( null !== selectedDate && typeof selectedDate.getTime === 'function' ) { 376 377 // Append current time. 378 var now = new Date(); 379 selectedDate.setHours( now.getHours(), now.getMinutes() ); 380 381 if ( 'store' === woocommerce_addons_params.date_input_timezone_reference ) { 382 selectedDate = WC_PAO.Helper.addMinutes( selectedDate, -1 * client_offset * 60 ); 383 selectedDate = WC_PAO.Helper.addMinutes( selectedDate, gmt_offset * 60 ); 384 } 385 386 $timestamp_input.val( selectedDate.getTime() / 1000 ); 387 $clear_button.show(); 388 389 } else { 390 $clear_button.hide(); 391 $timestamp_input.val( '' ); 392 } 393 394 } ); 395 396 // On clear date. 397 $clear_button.on( 'click', function( event ) { 398 event.preventDefault(); 399 // Sanity clear. 400 $timestamp_input.val( '' ); 401 // Trigger change. 402 $datepicker.val( '' ).trigger( 'change' ); 403 } ); 404 405 } ); 406 407 /** 408 * Integrations. 409 */ 410 411 // Compatibility with Smart Coupons self declared gift amount purchase. 412 // 413 // CAUTION: This code is unstable. 414 $( '#credit_called' ).on( 'keyup', function () { 415 self.validation.validate(); 416 self.updateTotals(); 417 }); 418 } 419 420 /** 421 * Updates addons totals if the form is valid or resets them otherwise. 422 */ 423 Form.prototype.updateTotals = function() { 424 425 this.totals.calculate(); 426 427 // Hide totals the product contains only optional items and none have been selected or when the form is invali
427d. 428 var display_totals = ( this.show_incomplete_subtotals || this.isValid() ) && ( this.contains_required || this.totals.$totals.data( 'price_data' ).length ); 429 430 if ( display_totals ) { 431 this.totals.render(); 432 } else { 433 this.totals.reset() 434 } 435 } 436 437 /** 438 * Determines if the form is valid. 439 * @returns boolean 440 */ 441 Form.prototype.isValid = function() { 442 443 var valid = true, 444 $add_to_cart_button = this.$el.find( 'button.single_add_to_cart_button' ); 445 446 if ( $add_to_cart_button.is( '.disabled' ) ) { 447 valid = false; 448 return valid; 449 } 450 451 $.each( this.validation.getValidationState(), function() { 452 453 if ( ! this.validity ) { 454 valid = false; 455 return false; 456 } 457 }); 458 459 return valid; 460 } 461 462 /** 463 * Determines if the product contains required add-ons 464 * @returns boolean 465 */ 466 Form.prototype.containsRequired = function() { 467 468 var contains_required = false; 469 470 this.$addons.each( function () { 471 var validation_rules = $(this).data( 'restrictions' ) 472 473 if ( ! $.isEmptyObject( validation_rules ) ) { 474 475 if ( 'required' in validation_rules ) { 476 if ( 'yes' === validation_rules.required ) { 477 return contains_required = true; 478 } 479 } 480 } 481 } ); 482 483 return contains_required; 484 } 485 486 /** 487 * Element-in-viewport check with partial element detection & direction support. 488 * Credit: Sam Sehnert - https://github.com/customd/jquery-visible 489 */ 490 Form.prototype.is_in_viewport = function( element, partial, hidden, direction ) { 491 492 var $w = $( window ); 493 494 if ( element.length < 1 ) { 495 return; 496 } 497 498 var $t = element.length > 1 ? element.eq(0) : element, 499 t = $t.get(0), 500 vpWidth = $w.width(), 501 vpHeight = $w.height(), 502 clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true; 503 504 direction = (direction) ? direction : 'vertical'; 505 506 if ( typeof t.getBoundingClientRect === 'function' ) { 507 508 // Use this native browser method, if available. 509 var rec = t.getBoundingClientRect(), 510 tViz = rec.top >= 0 && rec.top < vpHeight, 511 bViz = rec.bottom > 0 && rec.bottom <= vpHeight, 512 mViz = rec.top < 0 && rec.bottom > vpHeight, 513 lViz = rec.left >= 0 && rec.left < vpWidth, 514 rViz = rec.right > 0 && rec.right <= vpWidth, 515 vVisible = partial ? tViz || bViz || mViz : tViz && bViz, 516 hVisible = partial ? lViz || rViz : lViz && rViz; 517 518 if ( direction === 'both' ) { 519 return clientSize && vVisible && hVisible; 520 } else if ( direction === 'vertical' ) { 521 return clientSize && vVisible; 522 } else if ( direction === 'horizontal' ) { 523 return clientSize && hVisible; 524 } 525 526 } else { 527 528 var viewTop = $w.scrollTop(), 529 viewBottom = viewTop + vpHeight, 530 viewLeft = $w.scrollLeft(), 531 viewRight = viewLeft + vpWidth, 532 offset = $t.offset(), 533 _top = offset.top, 534 _bottom = _top + $t.height(), 535 _left = offset.left, 536 _right = _left + $t.width(), 537 compareTop = partial === true ? _bottom : _top, 538 compareBottom = partial === true ? _top : _bottom, 539 compareLeft = partial === true ? _right : _left, 540 compareRight = partial === true ? _left : _right; 541 542 if ( direction === 'both' ) { 543 return !!clientSize && ( ( compareBottom <= viewBottom ) && ( compareTop >= viewTop ) ) && ( ( compareRight <= viewRight ) && ( compareLeft >= viewLeft ) ); 544 } else if ( direction === 'vertical' ) { 545 return !!clientSize && ( ( compareBottom <= viewBottom ) && ( compareTop >= viewTop ) ); 546 } else if ( direction === 'horizontal' ) { 547 return !!clientSize && ( ( compareRight <= viewRight ) && ( compareLeft >= viewLeft ) ); 548 } 549 } 550 }; 551 552 /** 553 * Addons Totals Controller. 554 * 555 * @param object Form 556 */ 557 function Totals( Form ) { 558 // Make sure is called as a constructor. 559 if ( ! ( this instanceof Totals ) ) { 560 return new Totals( Form ); 561 } 562 563 if ( $.isEmptyObject( Form ) ) { 564 return false; 565 } 566 567 // Holds the jQuery instance. 568 this.$form = Form.$el; 569 this.$addons = Form.$addons; 570 571 // Parameters. 572 this.$variation_input = this.$form.hasClass( 'variations_form' ) 573 ? this.$form.find( 574 'input[name="variation_id"], input.variation_id' 575 ) 576 : false, 577 this.is_variable = this.$variation_input && this.$variation_input.length > 0, 578 this.$totals = this.$form.find( '#product-addons-total' ), 579 this.product_id = this.is_variable ? this.$variation_input.val() : this.$totals.data( 'product-id' ); 580 581 if ( ! this.product_id ) { 582 return false;
583 } 584 585 // The product base price. For Variable Products, this is the minimum variation price. 586 this.base_price = this.$totals.data( 'price' ), 587 this.raw_price = this.$totals.data( 'raw-price' ), 588 this.product_type = this.$totals.data( 'type' ), 589 this.qty = parseFloat( this.$form.find( 'input.qty' ).val() ), 590 this.addons_price_data = []; 591 this.$subscription_plans = this.$form.find( '.wcsatt-options-product' ), 592 this.has_subscription_plans = this.$subscription_plans.length > 0; 593 this.is_rtl = Form.is_rtl; 594 this.total = 0; 595 this.total_raw = 0; 596 this.show_subtotal_panel = true; 597 } 598 599 /** 600 * Determines if addons subtotals should be visible even if validation fails. 601 * 602 * @returns boolean 603 */ 604 Totals.prototype.showIncompleteSubtotals = function() { 605 return this.$totals.data( 'show-incomplete-sub-total' ) === 1; 606 } 607 608 /** 609 * Update addon totals when a new variation is selected. 610 * 611 * @param variation 612 */ 613 Totals.prototype.updateVariation = function( variation ) { 614 615 // Handle multiple variation dropdowns in a single form -- for example, a Bundle with many Variable bundled items. 616 this.$variation_input = this.$form.hasClass( 'variations_form' ) 617 ? this.$form.find( 618 'input[name="variation_id"], input.variation_id' 619 ) 620 : false; 621 this.product_id = variation.variation_id; 622 623 this.$totals.data( 'product-id', this.product_id ); 624 625 if ( typeof variation.display_price !== 'undefined' ) { 626 this.base_price = variation.display_price; 627 } else if ( 628 $( variation.price_html ).find( '.amount' ).last().length 629 ) { 630 631 this.base_price = $( variation.price_html ) 632 .find( '.amount' ) 633 .last() 634 .text(); 635 636 this.base_price = this.base_price.replace( 637 woocommerce_addons_params.currency_format_symbol, 638 '' 639 ); 640 641 this.base_price = this.base_price.replace( 642 woocommerce_addons_params.currency_format_thousand_sep, 643 '' 644 ); 645 646 this.base_price = this.base_price.replace( 647 woocommerce_addons_params.currency_format_decimal_sep, 648 '.' 649 ); 650 651 this.base_price = this.base_price.replace( /[^0-9\.]/g, '' ); 652 this.base_price = parseFloat( this.base_price ); 653 654 } 655 656 this.$totals.data( 'price', this.base_price ); 657 }; 658 659 /** 660 * Calculates addon totals based on configured addons. 661 */ 662 Totals.prototype.calculate = function() { 663 664 var self = this; 665
666 self.qty = parseFloat( self.$form.find( 'input.qty' ).val() ); 667 self.addons_price_data = []; 668 self.total = 0; 669 self.total_raw = 0; 670 self.base_price = self.$totals.data( 'price' ); 671 self.raw_price = self.$totals.data( 'raw-price' ); 672 self.product_id = self.is_variable ? self.$variation_input.val() : self.$totals.data( 'product-id' ); 673 674 /** 675 * Compatibility with Smart Coupons self declared gift amount purchase. 676 * 677 * CAUTION: This code is unstable. 678 * A dedicated Smart Coupons event should be used to change the base price based on the gift card amount. 679 */ 680 if ( 681 '' === self.base_price && 682 'undefined' !== typeof custom_gift_card_amount && 683 custom_gift_card_amount.length && 684 0 < custom_gift_card_amount.val() 685 ) { 686 self.base_price = custom_gift_card_amount.val(); 687 } 688 689 /** 690 * Compatibility with Bookings. 691 * 692 * CAUTION: This code is unstable. 693 * A dedicated Bookings event should be used to change the base price based on the bookings cost. 694 */ 695 if ( 696 woocommerce_addons_params.is_bookings && 697 $( '.wc-bookings-booking-cost' ).length 698 ) { 699 self.base_price = parseFloat( 700 $( '.wc-bookings-booking-cost' ).attr( 701 'data-raw-price' 702 ) 703 ); 704 } 705 706 /** 707 * Calculates totals of selected addons. 708 * 709 */ 710 this.$addons.each( function () { 711 712 if ( ! $( this ).val() ) { 713 return; 714 } 715 716 var $addon = $( this ), 717 parent_container = $addon.parents( '.wc-pao-addon' ), 718 name = parent_container.find( '.wc-pao-addon-name' ) 719 .length 720 ? parent_container 721 .find( '.wc-pao-addon-name' ) 722 .data( 'addon-name' ) 723 : '', 724 value_label = '', 725 addon_cost = 0, 726 addon_cost_raw = 0, 727 price_type = $addon.data( 'price-type' ), 728 is_custom_price = false, 729 addon_data = {}, 730 has_per_person_pricing = parent_container.find( 731 '.wc-pao-addon-name' 732 ).length 733 ? parent_container 734 .find( '.wc-pao-addon-name' ) 735 .data( 'has-per-person-pricing' ) 736 : false, 737 has_per_block_pricing = parent_container.find( 738 '.wc-pao-addon-name' 739 ).length 740 ? parent_container 741 .find( '.wc-pao-addon-name' ) 742 .data( 'has-per-block-pricing' ) 743 : false; 744 745 if ( $addon.is( '.wc-pao-addon-custom-price' ) ) { 746 747 is_custom_price = true; 748 addon_cost = $addon.val(); 749 addon_cost_raw = $addon.val(); 750 price_type = 'quantity_based'; 751 752 } else if ( 753 $addon.is( '.wc-pao-addon-input-multiplier' ) 754 ) { 755 // Avoid converting empty strings to 0. 756 if ( '' !== $addon.val() ) { 757 $addon.val( Math.ceil( $addon.val() ) ); 758 759 addon_cost = $addon.data( 'price' ) * $addon.val(); 760 addon_cost_raw = $addon.data('raw-price') * $addon.val(); 761 } 762 } else if ( 763 $addon.is( 764 '.wc-pao-addon-checkbox, .wc-pao-addon-radio' 765 ) 766 ) { 767 768 if ( ! $addon.is( ':checked' ) ) { 769 return; 770 } 771 772 value_label = $addon.data( 'label' ); 773 addon_cost = $addon.data( 'price' ); 774 addon_cost_raw = $addon.data('raw-price'); 775 } else if ( 776 $addon.is( 777 '.wc-pao-addon-image-swatch-select, .wc-pao-addon-select' 778 ) 779 ) { 780 781 if ( 782 ! $addon.find( 'option:selected' ) || 783 '' === $addon.find( 'option:selected' ).val() 784 ) { 785 return; 786 } 787 788 price_type = $addon.find( 'option:selected' ).data( 'price-type' ); 789 value_label = $addon.find( 'option:selected' ).data( 'label' ); 790 addon_cost = $addon.find( 'option:selected' ).data( 'price' ); 791 addon_cost_raw = $addon.find( 'option:selected' ).data( 'raw-price' ); 792 793 } else { 794 795 if ( ! $addon.val() ) { 796 return; 797 } 798 799 addon_cost = $addon.data('price'); 800 addon_cost_raw = $addon.data('raw-price'); 801 } 802 803 if ( ! addon_cost ) { 804 addon_cost = 0; 805 } 806 if ( ! addon_cost_raw ) { 807 addon_cost_raw = 0; 808 } 809 810 /** 811 * Compatibility with Bookings. 812 * 813 * CAUTION: This code is unstable. 814 * A dedicated Bookings/Accomodation Bookings event should be used to change the base price based on the bookings duration, persons and cost. 815 */ 816 if ( 817 ( 'booking' === self.product_type || 818 'accommodation-booking' === self.product_type ) && 819 woocommerce_addons_params.is_bookings 820 ) { 821 self.qty = 0; 822 823 // Duration field. 824 var block_qty = 0; 825 if ( 826 'undefined' !== 827 typeof $( '#wc_bookings_field_duration' ) && 828 0 < $( '#wc_bookings_field_duration' ).val() 829 ) { 830 block_qty = $( 831 '#wc_bookings_field_duration' 832 ).val(); 833 } 834 835 // Duration fields with start and end time. 836 if ( 837 'undefined' !== 838 typeof $( '#wc-bookings-form-end-time' ) && 839 0 < $( '#wc-bookings-form-end-time' ).val() 840 ) { 841 block_qty = $( 842 '#wc-bookings-form-end-time' 843 ).val(); 844 } 845 846 // Persons field(s). 847 var single_persons_input = $( '#wc_bookings_field_persons' ), 848 person_qty = 0; 849 850 if ( 1 === single_persons_input.length ) { 851 // Persons field when person types is disabled. 852 person_qty = 853 parseInt( person_qty, 10 ) + 854 parseInt( single_persons_input.val(), 10 ); 855 } else { 856 // Persons fields for multiple person types. 857 $( '.wc-bookings-booking-form' )
858 .find( 'input' ) 859 .each( function () { 860 // There could be more than one persons field. 861 var field = this.id.match( 862 /wc_bookings_field_persons_(\d+)/ 863 ); 864 865 if ( 866 null !== field && 867 'undefined' !== typeof field && 868 $( '#' + field[0] ).length 869 ) { 870 person_qty = 871 parseInt( person_qty, 10 ) + 872 parseInt( 873 $( '#' + field[0]).val(), 874 10 875 ); 876 } 877 }); 878 } 879 880 if ( 881 0 === self.qty && 882 $( '.wc-bookings-booking-cost' ).length 883 ) { 884 self.qty = 1; 885 } 886 887 // Apply person/block quantities. 888 if ( has_per_person_pricing && person_qty ) { 889 self.qty *= person_qty; 890 } 891 if ( has_per_block_pricing && block_qty ) { 892 self.qty *= block_qty; 893 } 894 } 895 896 // Format addon totals based on their type. 897 switch ( price_type ) { 898 case 'flat_fee': 899 addon_data.cost = parseFloat( addon_cost ); 900 addon_data.cost_raw = parseFloat( addon_cost_raw ); 901 break; 902 case 'quantity_based': 903 addon_data.cost_pu = parseFloat( addon_cost ); 904 addon_data.cost_raw_pu = parseFloat( addon_cost_raw ); 905 addon_data.cost = addon_data.cost_pu * self.qty; 906 addon_data.cost_raw = addon_data.cost_raw_pu * self.qty; 907 break; 908 case 'percentage_based': 909 addon_data.cost_pct = parseFloat( addon_cost ) / 100; 910 addon_data.cost_raw_pct = parseFloat( addon_cost_raw ) / 100; 911 addon_data.cost = 912 parseFloat( self.base_price ) * 913 addon_data.cost_pct * 914 self.qty; 915 addon_data.cost_raw = 916 parseFloat( self.raw_price ) * 917 addon_data.cost_raw_pct * 918 self.qty; 919 break; 920 } 921 922 // added by 57 923 var woocs_exists = true; 924 925 try { 926 if (woocs_current_currency) 927 woocs_exists = true; 928 } catch (e) { 929 woocs_exists = false; 930 } 931 932 if (woocs_exists) { 933 if ( 934 woocs_current_currency !== undefined && 935 woocs_current_currency['rate'] !== undefined && 936 addon_cost !== undefined 937 ) { 938 // Get product and user currency info 939 var product_currency = F7H_Product_Data.product_currency; 940 var user_currency = F7H_Product_Data.user_currency; 941 var product_currency_rate = parseFloat(F7H_Product_Data.product_currency_rate); 942 var user_currency_rate = parseFloat(F7H_Product_Data.user_currency_rate); 943 944 var exchange_rate = woocs_current_currency['rate']; // Default rate 945 946 if (product_currency && user_currency) { 947 if (product_currency === user_currency) { 948 exchange_rate = 1; // If the product currency matches the user's currency, no conversion needed 949 } else { 950 exchange_rate = user_currency_rate / product_currency_rate; // Direct conversion 951 } 952 } 953 954 // Apply correct exchange rate 955 addon_data.cost = addon_data.cost * exchange_rate; 956 addon_data.cost_raw = addon_data.cost_raw * exchange_rate; 957 // added by 57 958 } 959 } 960 961 962 self.total += addon_data.cost || 0; 963 self.total_raw += addon_data.cost_raw || 0; 964 965 /** 966 * Formats addon names to include user input. 967 * The formatted addon name will be displayed in the addons subtotal table. 968 */ 969 if ( 'undefined' !== typeof value_label ) { 970 if ( 971 'number' === typeof value_label || 972 value_label.length 973 ) { 974 addon_data.name = 975 name + 976 ( value_label ? ' - ' + value_label : '' ); 977 } else { 978 var user_input = $addon.val(), 979 trimCharacters = parseInt( 980 woocommerce_addons_params.trim_user_input_characters, 981 10 982 ); 983 984 // Check if type is file upload. 985 if ( $addon.is( '.wc-pao-addon-file-upload' ) ) { 986 user_input = user_input.replace( 987 /^.*[\\\/]/, 988 '' 989 ); 990 } 991 992 if ( trimCharacters < user_input.length ) { 993 user_input = 994 user_input.slice( 0, trimCharacters ) + 995 '...'; 996 } 997 998 addon_data.name = 999 name + 1000 ' - ' + 1001 WC_PAO.Helper.escapeHtml( user_input ); 1002 } 1003 1004 addon_data.is_custom_price = is_custom_price; 1005 addon_data.price_type = price_type; 1006 1007 self.addons_price_data.push( addon_data ); 1008 } 1009 }); 1010 1011 // Save prices for 3rd party access. 1012 self.$totals.data( 'price_data', self.addons_price_data ); 1013 self.$form.trigger( 'updated_addons' ); 1014 }; 1015 1016 /** 1017 * Renders addon totals. 1018 */ 1019 Totals.prototype.render = function() { 1020 1021 var self = this; 1022 1023 // Early exit if another plugin has determined that Product Addon totals should remain hidden. 1024 if ( ! self.$totals.data( 'show-sub-total' ) ) { 1025 self.$totals.empty(); 1026 self.$form.trigger( 'updated_addons' ); 1027 return; 1028 } 1029 1030 if ( self.qty ) { 1031 1032 var product_total_price, 1033 formatted_sub_total, 1034 $subscription_details, 1035 subscription_details_html, 1036 html, 1037 formatted_addon_total = self.formatMoney( self.total ), 1038 has_custom_price_with_taxes = false;
1039 1040 if ( 'undefined' !== typeof self.base_price && self.product_id ) { 1041 // If it is a bookable product. 1042 if ( $( '.wc-bookings-booking-form' ).length ) { 1043 product_total_price = ! isNaN( self.base_price ) ? parseFloat( self.base_price ) : 0; 1044 } else { 1045 product_total_price = parseFloat( self.base_price * self.qty ); 1046 } 1047 1048 formatted_sub_total = self.formatMoney( product_total_price + self.total ); 1049 } 1050 1051 /** 1052 * Compatibility with Subscribe All The Things/All Products for WooCommerce Subscriptions. 1053 * 1054 * CAUTION: This code is unstable. 1055 * An All Products for WooCommerce Subscriptions specific event should be used to get 1056 * subscription details when a new subscription plan is selected. 1057 */ 1058 if ( self.has_subscription_plans ) { 1059 var satt = self.$form.data( 'satt_script' ); 1060 1061 if ( satt && satt.schemes_model.get_active_scheme_key() ) { 1062 var $selected_plan = self.$subscription_plans.find( 'input:checked' ); 1063 1064 if ( $selected_plan.val() ) { 1065 $subscription_details = $selected_plan.parent().find( '.subscription-details' ); 1066 } 1067 } 1068 } else if ( self.$form.parent().find( '.subscription-details' ).length ) { 1069 // Add-Ons added at bundle level only affect the up-front price. 1070 if ( ! self.$form.hasClass( 'bundle_data' ) ) { 1071 $subscription_details = self.$form.parent().find( '.subscription-details' ); 1072 1073 /* 1074 * Check if product is a variable 1075 * because the subscription_details HTML element will 1076 * be located in different area. 1077 */ 1078 if ( self.$variation_input && self.$variation_input.length > 0 ) { 1079 $subscription_details = self.$form.parent().find( '.woocommerce-variation .subscription-details' ); 1080 } 1081 } 1082 } 1083 1084 if ( $subscription_details && $subscription_details.length > 0 ) { 1085 // Space is needed here in some cases. 1086 subscription_details_html = 1087 ' ' + 1088 $subscription_details 1089 .clone() 1090 .wrap( '<p>' ) 1091 .parent() 1092 .html(); 1093 } 1094 1095 /** 1096 * Compatibility with Grouped and subscription products. 1097 * 1098 * CAUTION: This code is unstable. 1099 * This code needs to be moved to a grouped/subscription-specific function. 1100 */ 1101 if ( 'grouped' === self.product_type ) { 1102 if ( subscription_details_html && ! WC_PAO.Helper.isGroupedMixedProductType() && WC_PAO.Helper.isGroupedSubsSelected() ) { 1103 formatted_addon_total += subscription_details_html; 1104 1105 if ( formatted_sub_total ) { 1106 formatted_sub_total += subscription_details_html; 1107 } 1108 } 1109 } else if ( subscription_details_html ) { 1110 if ( formatted_sub_total ) { 1111 formatted_sub_total += subscription_details_html; 1112 } 1113 } 1114 1115 /** 1116 * Render addon subtotals in a table-like format above the Add to Cart button. 1117 * As the first line item, display the main product followed by each total price (base price * quantity). 1118 * Then, display one line item for each selected addon followed by each price (given that one exists). 1119 */ 1120 if ( formatted_sub_total ) { 1121 var product_name = self.$form.find( '.wc-pao-addon-container' ).data( 'product-name' ), 1122 product_price = self.formatMoney( product_total_price ), 1123 product_tax_status = self.$form.find( '.wc-pao-addon-container' ).data( 'product-tax-status' ); 1124 1125 var woocs_exists = true; 1126 1127 try { 1128 if (woocs_current_currency) 1129 woocs_exists = true; 1130 } catch (e) { 1131 woocs_exists = false; 1132 } 1133 1134 if (woocs_exists) { 1135 if ( 1136 woocs_current_currency != undefined && 1137 woocs_current_currency['rate'] != undefined && 1138 woocommerce_addons_params.woocs_is_multiple == 0 1139 ) { 1140 product_price = product_price * woocs_current_currency['rate']; 1141 } 1142 } 1143 1144 /** 1145 * Bookings compatibility code. 1146 * 1147 * CAUTION: This code is unstable. 1148 * This code does not change addon totals for booking products if the form is right to left. 1149 */ 1150 if ( $( '.wc-bookings-booking-form' ).length ) { 1151 html = 1152 '<div class="product-addon-totals"><ul><li><div class="wc-pao-col1"><strong>' + 1153 product_name + 1154 '</strong></div><div class="wc-pao-col2"><strong><span class="amount">' + 1155 product_price + 1156 '</span></strong></div></li>'; 1157 } else { 1158 // Display the base product as the first line item in the addons subtotals table. 1159 var quantity_string = self.is_rtl 1160 ? woocommerce_addons_params.quantity_symbol + self.qty 1161 : self.qty + woocommerce_addons_params.quantity_symbol; 1162 1163 html = 1164 '<div class="product-addon-totals"><ul><li><div class="wc-pao-col1"><strong><span>' + 1165 quantity_string + 1166 '</span> ' + 1167 product_name + 1168 '</strong></div><div class="wc-pao-col2"><strong><span class="amount">' + 1169 product_price + 1170 '</span></strong></div></li>'; 1171 } 1172 1173 if ( self.addons_price_data.length ) { 1174 $.each( self.addons_price_data, function ( i, addon ) { 1175 var cost = addon.cost, 1176 formatted_value; 1177 1178 if ( 0 === cost ) { 1179 formatted_value = '-'; 1180 } else if ( cost > 0 ) { 1181 formatted_value = self.formatMoney( cost ); 1182 } else { 1183 formatted_value = '-' + self.formatMoney( Math.abs(cost) ); 1184 } 1185 1186 html = 1187 html + 1188 '<li><div class="wc-pao-col1"><strong>' + 1189 addon.name + 1190 '</strong></div><div class="wc-pao-col2"><span class="amount">' + 1191 formatted_value + 1192 '</span></div></li>'; 1193 1194 if ( woocommerce_addons_params.tax_enabled && addon.is_custom_price ) { 1195 has_custom_price_with_taxes = true; 1196 } 1197 }); 1198 } 1199 1200 // To show our "price display suffix" we have to do some magic since the string can contain variables (excl/incl tax values) 1201 // so we have to take our sub total and find out what the tax value is, which we can do via an ajax call 1202 // if its a simple string, or no string at all, we can output the string without an extra call 1203 var price_display_suffix = '', 1204 sub_total_string = 1205 typeof self.$totals.data( 'i18n_sub_total' ) === 'undefined' 1206 ? woocommerce_addons_params.i18n_sub_total 1207 : self.$totals.data( 'i18n_sub_total' ); 1208 1209 // No suffix is present, so we can just output the total. 1210 if ( 1211 ! has_custom_price_with_taxes && 1212 ( ! woocommerce_addons_params.price_display_suffix || 1213 ! woocommerce_addons_params.tax_enabled ) 1214 ) { 1215 html = 1216 html + 1217 '<li class="wc-pao-subtotal-line"><p class="price">' + 1218 sub_total_string + 1219 ' <span class="amount">' + 1220 formatted_sub_total + 1221 '</span></p></li></ul></div>'; 1222 1223 if ( self.show_subtotal_panel ) { 1224 self.$totals.html( html ); 1225 } else { 1226 self.$totals.html( '' ); 1227 } 1228 1229 self.$form.trigger( 'updated_addons' ); 1230 return; 1231 } 1232 1233 // A suffix is present, but no special labels are used - meaning we don't need to figure out any other special values - just display the plain text value 1234 if ( 1235 'taxable' === product_tax_status && 1236 ! has_custom_price_with_taxes && 1237 false === woocommerce_addons_params.price_display_suffix.indexOf( '{price_including_tax}' ) > -1 && 1238 false === woocommerce_addons_params.price_display_suffix.indexOf( '{price_excluding_tax}' ) > -1 1239 ) { 1240 html = 1241 html + 1242 '<li class="wc-pao-subtotal-line"><strong>' + 1243 sub_total_string + 1244 ' <span class="amount">' + 1245 formatted_sub_total + 1246 '</span> ' + 1247 woocommerce_addons_params.price_display_suffix + 1248 '</strong></li></ul></div>'; 1249 1250 if ( self.show_subtotal_panel ) { 1251 self.$totals.html( html ); 1252 } else { 1253 self.$totals.html( '' ); 1254 } 1255 1256 self.$form.trigger( 'updated_addons' ); 1257 return; 1258 } 1259 1260 // Based on the totals/info and settings we have, we need to use the get_price_*_tax functions 1261 // to get accurate totals. We can get these values with a special Ajax function 1262 $.ajax({ 1263 type: 'POST', 1264 url: woocommerce_addons_params.ajax_url, 1265 data: { 1266 action: 'wc_product_addons_calculate_tax', 1267 product_id: self.product_id, 1268 add_on_total: self.total, 1269 add_on_total_raw: self.total_raw, 1270 qty: self.qty, 1271 }, 1272 success: function ( result ) { 1273 if ( result.result == 'SUCCESS' ) { 1274 price_display_suffix = 1275 '<small class="woocommerce-price-suffix">' + 1276 woocommerce_addons_params.price_display_suffix + 1277 '</small>'; 1278 1279 var formatted_price_including_tax = self.formatMoney( result.price_including_tax ), 1280 formatted_price_excluding_tax = self.formatMoney( result.price_excluding_tax ); 1281 1282 price_display_suffix = 1283 price_display_suffix.replace( 1284 '{price_including_tax}', 1285 '<span class="amount">' + 1286 formatted_price_including_tax + 1287 '</span>' 1288 ); 1289 price_display_suffix = 1290 price_display_suffix.replace( 1291 '{price_excluding_tax}', 1292 '<span class="amount">' + 1293 formatted_price_excluding_tax + 1294 '</span>' 1295 ); 1296 1297 var subtotal = woocommerce_addons_params.display_include_tax 1298 ? formatted_price_including_tax 1299 : formatted_price_excluding_tax; 1300 1301 html = 1302 html + 1303 '<li class="wc-pao-subtotal-line"><p class="price">' + 1304 sub_total_string + 1305 ' <span class="amount">' + 1306 subtotal + 1307 '</span> ' + 1308 price_display_suffix + 1309 ' </p></li></ul></div>'; 1310 1311 if ( self.show_subtotal_panel ) { 1312 self.$totals.html( html ); 1313 } else { 1314 self.$totals.html( '' ); 1315 } 1316 1317 self.$form.trigger( 'updated_addons' ); 1318 } else { 1319 html = 1320 html + 1321 '<li class="wc-pao-subtotal-line"><p class="price">' + 1322 sub_total_string + 1323 ' <span class="amount">' + 1324 formatted_sub_total + 1325 '</span></p></li></ul></div>'; 1326 if ( self.show_subtotal_panel ) { 1327 self.$totals.html( html ); 1328 } else { 1329 self.$totals.html( '' ); 1330 } 1331 self.$form.trigger( 'updated_addons' ); 1332 } 1333 }, 1334 error: function () { 1335 html = 1336 html + 1337 '<li class="wc-pao-subtotal-line"><p class="price">' + 1338 sub_total_string + 1339 ' <span class="amount">' + 1340 formatted_sub_total + 1341 '</span></p></li></ul></div>'; 1342 1343 if ( self.show_subtotal_panel ) { 1344 self.$totals.html( html ); 1345 } else { 1346 self.$totals.html( '' ); 1347 } 1348 self.$form.trigger( 'updated_addons' ); 1349 }, 1350 }); 1351 } else { 1352 self.$totals.empty(); 1353 self.$form.trigger( 'updated_addons' ); 1354 } 1355 } else { 1356 self.$totals.empty(); 1357 self.$form.trigger( 'updated_addons' ); 1358 } 1359 }; 1360 1361 /** 1362 * Resets and hides addon totals. 1363 */ 1364 Totals.prototype.reset = function() { 1365 this.$totals.empty(); 1366 this.$totals.html( '' ); 1367 this.$form.trigger( 'updated_addons' ); 1368 } 1369 1370 /** 1371 * Formats addon prices. 1372 * 1373 * @param amount 1374 * @returns {[]|*} 1375 */ 1376 Totals.prototype.formatMoney = function ( amount ) { 1377 let formatNumDecimal = woocommerce_addons_params.currency_format_num_decimals; 1378 1379 // Remove trailing zeros. 1380 if ( woocommerce_addons_params.trim_trailing_zeros ) { 1381 const amountIsInteger = parseFloat( amount ) % 1 === 0; 1382 1383 // Remove zeros. 1384 // if float, 4.6500 => 4.65 1385 // if integer, 4.0000 => 4 1386 amount = parseFloat( amount ); 1387 1388 // Set precision value (mandatory to be passed). 1389 if ( amountIsInteger ) { 1390 // Set 0 decimal precision for integers. 1391 formatNumDecimal = 0; 1392 } else { 1393 // Count decimal from amount (zeros skipped already) and set as precision. 1394 // 4.655 => 3 digits after decimal point. 1395 formatNumDecimal = amount.toString().split( '.' )[ 1 ].length; 1396 } 1397 } 1398 1399 // added by 57 1400 if (formatNumDecimal !== 0 && formatNumDecimal !== "0") { 1401 formatNumDecimal = 2; 1402 } 1403 // added by 57 1404 1405 return accounting.formatMoney(amount, { 1406 symbol: woocommerce_addons_params.currency_format_symbol,
1407 decimal: woocommerce_addons_params.currency_format_decimal_sep, 1408 thousand: 1409 woocommerce_addons_params.currency_format_thousand_sep, 1410 precision: formatNumDecimal, 1411 format: woocommerce_addons_params.currency_format, 1412 }); 1413 }; 1414 1415 /** 1416 * Addons Validation Controller. 1417 * 1418 * @param object Form 1419 */ 1420 function Validation( Form ) { 1421 1422 // Make sure is called as a constructor. 1423 if ( ! ( this instanceof Validation ) ) { 1424 return new Validation( Form ); 1425 } 1426 1427 if ( ! Form.$addons.length ) { 1428 return false; 1429 } 1430 1431 // Holds the jQuery instance. 1432 this.$form = Form.$el; 1433 this.$addons = Form.$addons; 1434 1435 // An object that holds the validation state and message of each addon. 1436 this.validationState = this.getInitialState(); 1437 1438 } 1439 1440 /** 1441 * Gets the initial validation state. All addons are valid in this state. 1442 */ 1443 Validation.prototype.getInitialState = function() { 1444 1445 var schema = {}; 1446 1447 $.each( this.$addons, function() { 1448 schema[ $(this).attr( 'id' ) ] = { validity: true, message: '' }; 1449 } ); 1450 1451 return schema; 1452 } 1453 1454 /** 1455 * Gets the current validation state. 1456 */ 1457 Validation.prototype.getValidationState = function() { 1458 return this.validationState; 1459 } 1460 1461 /** 1462 * Validates a single addon and conditionally prints a validation message. 1463 * 1464 * @param jQuery object $addon 1465 * @return bool 1466 */ 1467 Validation.prototype.validateAddon = function( $addon, printMessages = false ) { 1468 1469 var validation_rules = $addon.data( 'restrictions' ), 1470 id = $addon.attr( 'id' ), 1471 validity = true; 1472 1473 if ( ! $.isEmptyObject( validation_rules ) ) { 1474 1475 if ( 'required' in validation_rules ) { 1476 if ( 'yes' === validation_rules.required ) { 1477 validity = this.validateRequired( $addon ); 1478 } 1479 } 1480 1481 if ( validity && 'content' in validation_rules ) { 1482 if ( 'only_letters' === validation_rules.content ) { 1483 validity = this.validateLetters( $addon ); 1484 } else if ( 'only_numbers' === validation_rules.content ) { 1485 validity = this.validateNumbers( $addon ); 1486 } else if ( 'only_letters_numbers' === validation_rules.content ) { 1487 validity = this.validateLettersNumbers( $addon ); 1488 } else if ( 'email' === validation_rules.content ) { 1489 validity = this.validateEmail( $addon ); 1490 } 1491 } 1492 1493 if ( validity && 'min' in validation_rules ) { 1494 validity = this.validateMin( $addon, validation_rules.min ); 1495 } 1496 1497 if ( validity && 'max' in validation_rules ) { 1498 validity = this.validateMax( $addon, validation_rules.max ); 1499 } 1500 } 1501 1502 if ( printMessages ) { 1503 this.printMessage( $addon ); 1504 } 1505 1506 return this.validationState[id].validity; 1507 }; 1508 1509 /** 1510 * Validates all addons and conditionally prints validation messages. 1511 * 1512 * @return bool 1513 */ 1514 Validation.prototype.validate = function( printMessages = false ) { 1515 1516 var validity = true, 1517 self = this; 1518 1519 $.each( self.$addons, function() { 1520 if ( ! self.validateAddon( $(this), printMessages ) ) { 1521 validity = false; 1522 } 1523 }); 1524 1525 return validity; 1526 }; 1527 1528 /** 1529 * Outputs validation message for specific addon. 1530 * @param jQuery object $addon 1531 */ 1532 Validation.prototype.printMessage = function( $addon ) { 1533 1534 var id = $addon.attr( 'id' ), 1535 element = this.$form.find( '#' + id ), 1536 formattedElementID = id + '-validation-notice', 1537 message = this.validationState[id].message; 1538 1539 // For radio buttons, display a single notice after all radio buttons. 1540 if ( element.is( ':radio' ) || element.is( ':checkbox' ) ) { 1541 1542 var $container_element = element.closest( '.wc-pao-addon-container .wc-pao-addon-wrap' ); 1543 1544 $container_element.find( '.wc-pao-validation-notice' ).remove(); 1545 1546 if ( ! this.validationState[id].validity ) { 1547 $container_element.append( '<small id="' + formattedElementID + '" class="wc-pao-validation-notice">' + message + '</small>' ); 1548 } 1549 1550 // For the rest addon types, display a notice under each addon. 1551 } else { 1552 element.closest( '.wc-pao-addon-container' ).find( '.wc-pao-validation-notice' ).remove(); 1553 if ( ! this.validationState[id].validity ) { 1554 element.after( '<small id="' + formattedElementID + '" class="wc-pao-validation-notice">' + message.replace( /</g, "<" ).replace( />/g, ">" ) + '</small>' ); 1555 } 1556 } 1557 }; 1558 1559 /** 1560 * Validates if required addons are configured. 1561 * @param jQuery object $element 1562 * @return boolean 1563 */ 1564 Validation.prototype.validateRequired = function( $element ) { 1565
1566 var validity = true, 1567 message = '', 1568 reason = '', 1569 id = $element.attr( 'id'); 1570 1571 if ( $element.is( ':checkbox' ) || $element.is( ':radio' ) ) { 1572 1573 var $container_element = $element.closest( '.wc-pao-addon-container' ), 1574 $options = $container_element.find( '.wc-pao-addon-field' ), 1575 self = this; 1576 1577 validity = false; 1578 1579 $.each( $options, function() { 1580 if ( $( this ).is( ':checked' ) ) { 1581 validity = true; 1582 return; 1583 } 1584 } ); 1585 1586 if ( ! validity ) { 1587 message = woocommerce_addons_params.i18n_validation_required_select; 1588 } else { 1589 1590 // For groups of options, like radio buttons/checkboxes, if at least 1 option is selected, then consider all options as valid. 1591 $.each( $options, function() { 1592 var option_id = $(this).attr( 'id'); 1593 self.validationState[ option_id ] = { validity: validity, message: message, reason: reason }; 1594 } ); 1595 1596 return; 1597 } 1598 1599 } else if ( $element.hasClass( 'wc-pao-addon-image-swatch-select' ) ) { 1600 var $container_element = $element.closest( '.wc-pao-addon-container' ); 1601 1602 validity = false; 1603 1604 $.each( $container_element.find( '.wc-pao-addon-image-swatch' ), function() { 1605 1606 if ( $( this ).hasClass( 'selected' ) ) { 1607 validity = true; 1608 return; 1609 } 1610 } ); 1611 1612 if ( ! validity ) { 1613 message = woocommerce_addons_params.i18n_validation_required_select; 1614 } 1615 } else { 1616 1617 if ( ! $element.val() ) { 1618 validity = false; 1619 1620 if ( 'file' === $element.attr( 'type' ) ) { 1621 message = woocommerce_addons_params.i18n_validation_required_file; 1622 } else if ( 'number' === $element.attr( 'type' ) ) { 1623 message = woocommerce_addons_params.i18n_validation_required_number; 1624 } else if ( $element.is( 'input' ) || $element.is( 'textarea' ) ) { 1625 message = woocommerce_addons_params.i18n_validation_required_input; 1626 } else if ( $element.is( 'select' ) ) { 1627 message = woocommerce_addons_params.i18n_validation_required_select; 1628 } 1629 } 1630 } 1631 1632 if ( ! validity ) { 1633 reason = 'required'; 1634 } 1635 1636 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1637 1638 return this.validationState[id].validity; 1639 }; 1640 1641 /** 1642 * Validates if input contains only letters. 1643 * @param jQuery object $element 1644 * @return boolean 1645 */ 1646 Validation.prototype.validateLetters = function( $element ) { 1647 1648 var validity = ! ( /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~\d]/g.test( $element.val() ) ), 1649 message = '', 1650 reason = '', 1651 id = $element.attr( 'id' ); 1652 1653 if ( ! $element.val() ){ 1654 validity = true; 1655 } 1656 1657 if ( ! validity ) { 1658 message = woocommerce_addons_params.i18n_validation_letters_only; 1659 reason = 'letters'; 1660 } 1661 1662 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1663 1664 return this.validationState[id].validity; 1665 1666 }; 1667 1668 /** 1669 * Validates if input contains only numbers. 1670 * @param jQuery object $element 1671 * @return boolean 1672 */ 1673 Validation.prototype.validateNumbers = function( $element ) { 1674 1675 var validity = /^[0-9]*$/g.test( $element.val() ), 1676 message = '', 1677 reason = '', 1678 id = $element.attr( 'id'); 1679 1680 if ( ! $element.val() ){ 1681 validity = true; 1682 } 1683 1684 if ( ! validity ) { 1685 message = woocommerce_addons_params.i18n_validation_numbers_only; 1686 reason = 'numbers'; 1687 } 1688 1689 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1690 1691 return this.validationState[id].validity; 1692 }; 1693 1694 /** 1695 * Validates if input contains only letters and numbers. 1696 * @param jQuery object $element 1697 * @return boolean 1698 */ 1699 Validation.prototype.validateLettersNumbers = function( $element ) { 1700 1701 var validity = ! ( /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/g.test( $element.val() ) ), 1702 message = '', 1703 reason = '', 1704 id = $element.attr( 'id'); 1705 1706 if ( ! $element.val() ){ 1707 validity = true; 1708 } 1709 1710 if ( ! validity ) { 1711 message = woocommerce_addons_params.i18n_validation_letters_and_numbers_only; 1712 reason = 'letters_numbers'; 1713 } 1714 1715 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1716 1717 return this.validationState[id].validity; 1718 1719 }; 1720 1721 /** 1722 * Validates if input contains a valid email address. 1723 * @param jQuery object $element 1724 * @return boolean 1725 */ 1726 Validation.prototype.validateEmail = function( $element ) { 1727
1728 var validity = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/.test( $element.val() ), 1729 message = '', 1730 reason = '', 1731 id = $element.attr( 'id'); 1732 1733 if ( ! $element.val() ){ 1734 validity = true; 1735 } 1736 1737 if ( ! validity ) { 1738 message = woocommerce_addons_params.i18n_validation_email_only; 1739 reason = 'email'; 1740 } 1741 1742 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1743 1744 return this.validationState[id].validity; 1745 1746 }; 1747 1748 /** 1749 * Validates if the min length and min number restrictions are violated. 1750 * @param jQuery object $element 1751 * @param int min 1752 * @return boolean 1753 */ 1754 Validation.prototype.validateMin = function( $element, min ) { 1755 1756 var validity = true, 1757 message = '', 1758 reason = '', 1759 id = $element.attr( 'id'); 1760 1761 if ( ! $element.val() ){ 1762 validity = true; 1763 } else if ( 'number' === $element.attr( 'type' ) ) { 1764 var value = $element.val(); 1765 1766 if ( value.includes( '.' ) || value.includes( ',' ) ) { 1767 value = parseFloat( value ); 1768 } else { 1769 value = parseInt( value ); 1770 } 1771 1772 if ( value < min ) { 1773 validity = false; 1774 message = woocommerce_addons_params.i18n_validation_min_number.replace( '%c', min ); 1775 } 1776 1777 } else if ( 'text' === $element.attr( 'type' ) || $element.is( 'textarea' ) ) { 1778 1779 if ( $element.val().length < min ) { 1780 validity = false; 1781 message = woocommerce_addons_params.i18n_validation_min_characters.replace( '%c', min ); 1782 } 1783 } 1784 1785 if ( ! validity ) { 1786 reason = 'min'; 1787 } 1788 1789 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1790 1791 return this.validationState[id].validity; 1792 }; 1793 1794 /** 1795 * Validates if the max length and max number restrictions are violated. 1796 * @param jQuery object $element 1797 * @param int max 1798 * @return boolean 1799 */ 1800 Validation.prototype.validateMax = function( $element, max ) { 1801 1802 var validity = true, 1803 message = '', 1804 reason = reason, 1805 id = $element.attr( 'id'); 1806 1807 if ( ! $element.val() ){ 1808 validity = true; 1809 } else if ( 'number' === $element.attr( 'type' ) ) { 1810 var value = $element.val(); 1811 1812 if ( value.includes( '.' ) || value.includes( ',' ) ) { 1813 value = parseFloat( value ); 1814 } else { 1815 value = parseInt( value ); 1816 } 1817 1818 if ( value > max ) { 1819 validity = false; 1820 message = woocommerce_addons_params.i18n_validation_max_number.replace( '%c', max ); 1821 } 1822 1823 } else if ( 'text' === $element.attr( 'type' ) || $element.is( 'textarea' ) ) { 1824 if ( $element.val().length > max ) { 1825 validity = false; 1826 message = woocommerce_addons_params.i18n_validation_max_characters.replace( '%c', max ); 1827 } 1828 } 1829 1830 if ( ! validity ) { 1831 reason = 'max'; 1832 } 1833 1834 this.validationState[id] = { validity: validity, message: message, reason: reason }; 1835 1836 return this.validationState[id].validity; 1837 }; 1838 1839 return Form; 1840 } )(); 1841 1842 $(function () { 1843 // Quick view. 1844 $('body').on('quick-view-displayed', function () { 1845 $(this) 1846 .find('.cart:not(.cart_group)') 1847 .each(function () { 1848 var $form = new WC_PAO.Form( $(this) ); 1849 }); 1850 }); 1851 1852 // Initialize addon totals. 1853 $('body') 1854 .find('.cart:not(.cart_group)') 1855 .each(function () { 1856 var $form = new WC_PAO.Form( $(this) ); 1857 }); 1858 }); 1859 1860})( jQuery, window );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.