PageSourceSearch

https://57hours.com/wp-content/plugins/woocommerce-product-addons/assets/js/frontend/addons.js?ver=6.8.0.3

js 57hours.com collected 2026-10-02 06:16:44 UTC 54,313 bytes, 1,860 lines download raw bytes

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, "&lt;" ).replace( />/g, "&gt;" ) + '</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.