PageSourceSearch

https://www.coptocam.org/wp-content/plugins/gravityforms/js/gravityforms.js?ver=2.9.31

js coptocam.org collected 2026-10-04 20:38:49 UTC 109,384 bytes, 3,337 lines download raw bytes

1/* eslint-env jquery */
2
3var gform = window.gform || {};
4
5// "prop" method fix for previous versions of jQuery (1.5 and below)
6if( typeof jQuery.fn.prop === 'undefined' ) {
7    jQuery.fn.prop = jQuery.fn.attr;
8}
9
10//Formatting free form currency fields to currency
11jQuery( document ).on( 'gform_post_render', gformBindFormatPricingFields );
12
13function gformBindFormatPricingFields(){
14	// Namespace the event and remove before adding to prevent double binding.
15    jQuery(".ginput_amount, .ginput_donation_amount").off('change.gform').on("change.gform", function(){
16        gformFormatPricingField(this);
17    });
18
19    jQuery(".ginput_amount, .ginput_donation_amount").each(function(){
20        gformFormatPricingField(this);
21    });
22}
23
24//----------------------------------------
25//------ INSTANCES -----------------------
26//----------------------------------------
27
28/**
29 * Namespace to store our JavaScript class instances
30 */
31
32gform.instances = gform.instances || {};
33
34//----------------------------------------
35//------ CONSOLE FUNCTIONS ---------------
36//----------------------------------------
37
38/**
39 * Console namespace for our safe to use and extendable console functions.
40 */
41
42gform.console = {
43    error: function( message ) {
44        if( window.console ) {
45            console.error( message );
46        }
47    },
48    info: function( message ) {
49        if( window.console ) {
50            console.info( message );
51        }
52    },
53    log: function( message ) {
54        if( window.console ) {
55            console.log( message );
56        }
57    },
58};
59
60//----------------------------------------
61//------ ADMIN UTIL FUNCTIONS ------------
62//----------------------------------------
63
64/**
65 * Namespace for our admin utlity functions
66 */
67
68gform.adminUtils = {
69
70	/**
71	 * Handle any unsaved changes to the current settings page.
72	 *
73	 * @since 2.4
74	 *
75	 * @param {string} elemId The ID of the current element to check for changes.
76	 */
77	handleUnsavedChanges: function( elemId ) {
78		var hasUnsavedChanges = null;
79
80		jQuery( elemId ).find( 'input, select, textarea' ).on( 'change keyup', function() {
81
82			if ( jQuery( this ).attr( 'onChange' ) === undefined && jQuery( this ).attr( 'onClick' ) === undefined )  {
83				hasUnsavedChanges = true;
84			}
85
86			// Don't trigger unsaved changes on the enable api access button.
87			if ( ( jQuery( this ).next().data("jsButton") || jQuery( this ).data("jsButton") ) === 'enable-api' ) {
88				hasUnsavedChanges = null;
89			}
90
91		} );
92
93		// Standalone logic for the web api settings page. Trigger unsaved changes if the setting doesn't match the checkbox state.
94		if ( this.getUrlParameter( 'subview' ) === 'gravityformswebapi' ) {
95			if ( gf_webapi_vars.api_enabled !== gf_webapi_vars.enable_api_checkbox_checked ) {
96				hasUnsavedChanges = true;
97			}
98		}
99
100		jQuery( elemId ).on( 'submit', function() {
101			hasUnsavedChanges = null;
102		} );
103
104		window.onbeforeunload = function() {
105			return hasUnsavedChanges;
106		};
107	},
108
109	getUrlParameter: function( param ) {
110		var url = window.location.search.substring( 1 );
111		var urlVariables = url.split( '&' );
112		for ( var i = 0; i < urlVariables.length; i++ ) {
113			var parameterName = urlVariables[i].split( '=' );
114			if ( parameterName[0] == param )
115			{
116				return parameterName[1];
117			}
118		}
119	},
120}
121
122window.HandleUnsavedChanges = gform.adminUtils.handleUnsavedChanges;
123
124//----------------------------------------
125//------ TOOL FUNCTIONS ------------------
126//----------------------------------------
127
128/**
129 * Tool namespace to house our common dom/function tools.
130 */
131
132gform.tools = {
133	/**
134	 * Wrapper to add debouncing to any given callback.
135	 *
136	 * @since 2.5.2
137	 *
138	 * @param {Function} fn             The callback to execute.
139	 * @param {integer}  debounceLength The amount of time for which to debounce (in milliseconds)
140	 * @param {bool}     isImmediate    Whether to fire this immediately, or at the tail end of the timeout.
141	 *
142	 * @returns {function}
143	 */
144	debounce: function( fn, debounceLength, isImmediate ) {
145		// Initialize var to hold our window timeout
146		var timeout;
147		var lastArgs;
148		var lastFn;
149
150		return function() {
151			// Initialize local versions of our context and arguments to pass to apply()
152			var callbackContext = this;
153			var args            = arguments;
154
155			// Create a deferred callback to fire if this shouldn't be immediate.
156			var deferredCallback = function() {
157				timeout = null;
158
159				if ( ! isImmediate ) {
160					fn.apply( callbackContext, args );
161				}
162			};
163
164			// Begin processing the actual callback.
165			var callNow = isImmediate && ! timeout;
166
167			// Reset timeout if it is the same method with the same args.
168			if ( args === lastArgs && ( ''+lastFn == ''+fn ) ) {
169				clearTimeout( timeout );
170			}
171
172			// Set the value of the last function call and arguments to help determine whether the next call is unique.
173			var cachePreviousCall = function( fn, args ) {
174				lastFn    = fn;
175				lastArgs = args;
176			}
177
178			timeout = setTimeout( deferredCallback, debounceLength );
179			cachePreviousCall( fn, args );
180
181			// Method should be executed on the trailing edge of the timeout. Bail for now.
182			if ( ! callNow ) {
183				return;
184			}
185
186			// Callback should be called immediately, and isn't currently debounced; execute it.
187			fn.apply( callbackContext, args );
188		};
189	},
190
191    /**
192     * @function gform.tools.defaultFor
193     * @description Returns a default if first arg is undefined. Once we start migrating to es6 or use babel can
194     * easily swap to default args
195     *
196     * @since 2.5
197     *
198     * @param {*} arg
199     * @param {*} val
200     * @returns {*}
201     */
202
203    defaultFor: function( arg, val ) {
204        return typeof arg !== 'undefined' ? arg : val;
205    },
206
207	/**
208	 * @function gform.tools.getFocusable
209	 * @description Get focusable elements inside a container and return as an array.
210	 *
211	 * @since 2.5
212	 *
213	 * @param container the parent to search for focusable elements inside of
214	 * @returns {*[]}
215	 */
216
217	getFocusable: function( container ) {
218		container = this.defaultFor( container, document );
219		var focusable = this.convertElements(
220			container.querySelectorAll(
221				'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
222			)
223		);
224		return focusable.filter( function( item ) {
225			return this.visible( item );
226		}.bind( this ) );
227	},
228
229	/**
230	 * @function gform.tools.htmlToElement
231	 *
232	 * Allows you to convert an HTML string to a DOM Object.
233	 *
234	 * @param {string} html
235	 *
236	 * @returns {ChildNode}
237	 */
238	htmlToElement: function( html ) {
239		var template       = document.createElement( 'template' );
240		html               = html.trim();
241		template.innerHTML = html;
242
243		return template.content.firstChild;
244	},
245
246	/**
247	 * @function gform.tools.elementToHTML
248	 *
249	 * Converts a DOM Element to an HTML string.
250	 *
251	 * @param {object} el
252	 *
253	 * @returns {string}
254	 */
255	elementToHTML: function( el ) {
256		return el.outerHTML;
257	},
258
259    /**
260     * @function gform.tools.convertElements
261     * @description Efficient function to convert a nodelist into a standard array.
262     * Allows you to run Array.forEach in ie11/saf on result of querySelector functions.
263     * Used by getNodes below.
264     *
265     * @since 2.5
266     *
267     * @param {Element|NodeList} elements Elements to convert
268     *
269     * @returns {Array} Of converted elements
270     */
271
272    convertElements: function( elements ) {
273        var converted = [];
274        var i         = elements.length;
275        for ( i; i--; converted.unshift( elements[ i ] ) ) ;
276
277        return converted;
278    },
279
280	/**
281	 * @function gform.tools.delegate
282	 * @description Simple jQuery on replacement. When migrating to ES6 bundle replace with npm delegate.
283	 *
284	 * @since 2.5
285	 *
286	 * @param {String} selector
287	 * @param {String} event
288	 * @param {String} childSelector
289	 * @param {Function} handler
290	 */
291
292	delegate: function( selector, event, childSelector, handler ) {
293		var is = function( el, selector ) {
294			return ( el.matches || el.msMatchesSelector ).call( el, selector );
295		};
296
297		var elements = document.querySelectorAll( selector );
298		[].forEach.call( elements, function( el, i ) {
299			el.addEventListener( event, function( e ) {
300				if ( is( e.target, childSelector ) ) {
301					handler( e );
302				}
303			} );
304		} );
305	},
306
307    /**
308     * @function gform.tools.getClosest
309     * @description Get a parent node based on selector plus passed in child element.
310     *
311     * @since 2.5
312     *
313     * @param {Element|EventTarget} el
314     * @param {String} selector
315     *
316     * @returns {null|*}
317     */
318
319    getClosest: function( el, selector ) {
320        var matchesFn;
321        var parent;
322
323        [ 'matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector' ]
324            .some( function( fn ) {
325                if ( typeof document.body[ fn ] === 'function' ) {
326                    matchesFn = fn;
327                    return true;
328                }
329                return false;
330            } );
331
332        while ( el ) {
333            parent = el.parentElement;
334            if ( parent && parent[ matchesFn ]( selector ) ) {
335                return parent;
336            }
337
338            el = parent;
339        }
340
341        return null;
342    },
343
344    /**
345     * @function gform.tools.getNodes
346     * @description Used for getting nodes. Please use the data-js attribute whenever possible.
347     *
348     * @since 2.5
349     *
350     * @param {String} selector The selector string to search for. If arg 4 is false (default) then we search for [data-js="selector"]
351     * @param {Boolean} [convert] Convert the NodeList to an array? Then we can Array.forEach directly. Uses convertElements from above.
352     * @param {Element|EventTarget|Document} [node] Parent node to search from. Defaults to document.
353     * @param {Boolean} [custom] Is this a custom selector were we don't want to use the data-js attribute?
354     *
355     * @returns {NodeList|Array}
356     */
357
358    getNodes: function( selector, convert, node, custom ) {
359        if ( ! selector ) {
360            gform.console.error( 'Please pass a selector to gform.tools.getNodes' );
361            return [];
362        }
363        node = this.defaultFor( node, document );
364        var selectorString = custom ? selector : '[data-js="' + selector + '"]';
365        var nodes          = node.querySelectorAll( selectorString );
366        if ( convert ) {
367            nodes = this.convertElements( nodes );
368        }
369        return nodes;
370    },
371
372	/**
373	 * @function gform.tools.mergeObjects
374	 * @description ES5 Object.assign. Usage: gforms.tools.mergeObjects( obj1, obj2, obj3 );
375	 *
376	 * @since 2.5
377	 *
378	 * @returns {{}}
379	 */
380
381	mergeObjects: function() {
382		var resObj = {};
383		for ( var i = 0; i < arguments.length; i += 1 ) {
384			var obj = arguments[ i ]
385			var keys = Object.keys( obj );
386			for ( var j = 0; j < keys.length; j += 1 ) {
387				resObj[ keys[ j ] ] = obj[ keys[ j ] ];
388			}
389		}
390		return resObj;
391	},
392
393    /**
394     * @function gform.tools.setAttr
395     * @description Sets attributes for a group of nodes based on a passed selector.
396     * Can apply to document or subset, and has optional delay.
397     *
398     * @since 2.5
399     *
400     * @param {String} selector A selector string, and valid js selector string for a dom element.
401     * @param {String} attr The attribute name.
402     * @param {String} value The attribute value.
403     * @param {Element|EventTarget|Document} [container] Node to search from, default is document.
404     * @param {Number} [delay] The delay to apply.
405     */
406
407    setAttr: function( selector, attr, value, container, delay ) {
408        if ( ! selector || ! attr || ! value ) {
409            gform.console.error( 'Please pass a selector, attribute and value to gform.tools.setAttr' );
410            return [];
411        }
412        container = this.defaultFor( container, document );
413        delay = this.defaultFor( delay, 0 );
414
415        setTimeout( function() {
416            gform.tools.getNodes( selector, true, container, true )
417                .forEach( function( node ) {
418                    node.setAttribute( attr, value );
419                } );
420        }, delay );
421    },
422
423	/**
424	 * @function gform.tools.isRtl
425	 * @description Determine if the page is in RTL.
426	 *
427	 * @since 2.5
428	 *
429	 */
430
431	isRtl: function() {
432		if ( jQuery( 'html' ).attr( 'dir' ) === 'rtl' ) {
433			return true;
434		}
435	},
436
437	/**
438	 * @function gform.tools.trigger
439	 * @description Trigger custom or native events on any element in a cross browser way, and pass along optional data.
440	 *
441	 * @since 2.5.1.1
442	 *
443	 * @param {String} eventName The event name.
444	 * @param {Element|EventTarget|Document} el Default document. The element to trigger the event on.
445	 * @param {Boolean} native Default fasle. Is this a custom event or native?
446	 * @param {Object} data Custom data to send along, available in event.detail on listener.
447	 */
448
449	trigger: function( eventName, el, native, data ) {
450		var event;
451		eventName =  this.defaultFor( eventName, '' );
452		el =  this.defaultFor( el, document );
453		native =  this.defaultFor( native, false );
454		data =  this.defaultFor( data, {} );
455		if ( native ) {
456			event = document.createEvent( 'HTMLEvents' );
457			event.initEvent( eventName, true, false );
458		} else {
459			try {
460				event = new CustomEvent( eventName, { detail: data } );
461			} catch ( e ) {
462				event = document.createEvent( 'CustomEvent' );
463				event.initCustomEvent( eventName, true, true, data );
464			}
465		}
466
467		el.dispatchEvent( event );
468	},
469
470	/**
471	 * @function gform.tools.uniqueId
472	 * @description Generate a unique id
473	 *
474	 * @since 2.5.5.2
475	 *
476	 * @param {String} prefix
477	 * @returns {string}
478	 */
479
480	uniqueId: function( prefix ) {
481		prefix = this.defaultFor( prefix, 'id' );
482		return prefix + '-' + Math.random().toString( 36 ).substr( 2, 9 );
483	},
484
485	/**
486	 * @function gform.tools.visible
487	 * @description Determine if an element is visible in the dom.
488	 *
489	 * @since 2.5
490	 *
491	 * @param elem The element to check
492	 * @returns {boolean}
493	 */
494
495	visible: function( elem ) {
496		return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
497	},
498
499	stripSlashes: function( str ) {
500		return (str + '').replace(/\\(.?)/g, function (s, n1) {
501			switch (n1) {
502				case '\\':
503					return '\\';
504				case '0':
505					return '\u0000';
506				case '':
507					return '';
508				default:
509					return n1;
510			}
511		});
512	},
513
514	/**
515	 * @function gform.tools.getCookie
516	 * @description Gets a specific cookie.
517	 *
518	 * @since 2.5.8
519	 *
520	 * @param name The cookie to get
521	 * @returns {boolean|string}
522	 */
523
524	getCookie: function( name ) {
525		var cookieArr = document.cookie.split( ";" );
526
527		for(var i = 0; i < cookieArr.length; i++) {
528			var cookiePair = cookieArr[i].split( "=" );
529
530			if( name == cookiePair[0].trim() ) {
531				return decodeURIComponent( cookiePair[1] );
532			}
533		}
534
535		return null;
536	},
537
538	/**
539	 * @function gform.tools.setCookie
540	 * @description Creates and sets a cookie.
541	 *
542	 * @since 2.5.8
543	 *
544	 * @param name The cookie name
545	 * @param value The cookie value
546	 * @param daysToExpire The number of days until cookie should expire. If not set,
547	 * will expire at the end of the user sessions.
548	 * @param updateExistingValue Whether or not to update the existing cookie value to include the new value.
549	 * Can be helpful for keeping cookie count lower for the browser.
550	 */
551
552	setCookie: function( name, value, daysToExpire, updateExistingValue ) {
553		var expirationDate = '';
554		var cookieValue = value;
555
556		if ( daysToExpire ) {
557			var date = new Date();
558			date.setTime( date.getTime() + ( daysToExpire * 24 * 60 * 60 * 1000 ) );
559			expirationDate = ' expires=' + date.toUTCString();
560		}
561
562		if ( updateExistingValue ) {
563			var currentValue = gform.tools.getCookie( name );
564			cookieValue = currentValue !== '' && currentValue !== null ? currentValue + ',' + value : value;
565		}
566
567		// Set cookie
568		document.cookie = encodeURIComponent( name ) + '=' + encodeURIComponent( cookieValue ) + ';' + expirationDate;
569	},
570
571	/**
572	 * @function gform.tools.removeCookie
573	 * @description Removes a cookie.
574	 *
575	 * @since 2.5.8
576	 *
577	 * @param name The cookie name to check
578	 */
579
580	removeCookie: function( name ) {
581		gform.tools.setCookie( name, '', -1 );
582	}
583};
584
585//------------------------------------------------
586//---------- A11Y FUNCTIONS ----------------------
587//------------------------------------------------
588
589/**
590 * A11y namespace to house our accessibility functions.
591 */
592
593gform.a11y = {};
594
595//------------------------------------------------
596//---------- OPTIONS -----------------------------
597//------------------------------------------------
598
599/**
600 * Options namespace to house common plugin and custom options objects for reuse across our JavaScript.
601 */
602
603gform.options = {
604
605    /**
606     * Accordions in the editor sidebar use these options. Should be applied to any accordions that want to emulate
607     * that look and feel, and patches an a11y issue with jq accordion and our custom usage.
608     */
609
610    jqEditorAccordions: {
611    	header: 'button.panel-block-tabs__toggle',
612        heightStyle: 'content',
613        collapsible: true,
614        animate: false,
615        create: function( event ) {
616            gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 );
617        },
618        activate: function( event ) {
619            gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 );
620        },
621	    beforeActivate: function( event ) {
622			// handle advanced tab operations as needed before the tab is revealed in a fields settings
623			if ( event.currentTarget.id === 'advanced_tab_toggle' ) {
624				// handle address field
625				if ( window.field && window.field.type && window.field.type === 'address' ) {
626					// regen the Autocomplete UI on every tab open to handle changes to input visibility from interactions
627					CreateAutocompleteUI( window.field );
628				}
629			}
630	    }
631    },
632
633	jqAddFieldAccordions: {
634		heightStyle: 'content',
635		collapsible: true,
636		animate: false,
637		create: function( event ) {
638			gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 );
639		},
640		activate: function( event ) {
641			gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 );
642		},
643	},
644};
645
646//------------------------------------------------
647//---------- CURRENCY ----------------------------
648//------------------------------------------------
649
650function Currency(currency){
651	console.warn( 'Currency has been deprecated since Gravity Forms 2.9. Use gform.Currency instead.' );
652	return new gform.Currency( currency );
653}
654
655/**
656 * Gets a formatted number and returns a clean "decimal dot" number.
657 *
658 * Note: Input must be formatted according to the specified parameters (symbol_right, symbol_left, decimal_separator).
659 * @example input -> $1.20, output -> 1.2
660 *
661 * @since 2.1.1.16 Modified to support additional param in Currency.toMoney.
662 *
663 * @param text              string The currency-formatted number.
664 * @param symbol_right      string The symbol used on the right.
665 * @param symbol_left       string The symbol used on the left.
666 * @param decimal_separator string The decimal separator being used.
667 *
668 * @return float The unformatted numerical value.
669 */
670function gformCleanNumber(text, symbol_right, symbol_left, decimal_separator){
671	console.warn( 'gformCleanNumber() has been deprecated since Gravity Forms 2.9. Use gform.Currency.cleanNumber() instead.' );
672	return gform.Currency.cleanNumber( text, symbol_right, symbol_left, decimal_separator );
673}
674
675function gformGetDecimalSeparator(numberFormat){
676	console.warn( 'gformGetDecimalSeparator() has been deprecated since Gravity Forms 2.9. Use gform.Currency.getDecimalSeparator() instead.' );
677	return gform.Currency.getDecimalSeparator( numberFormat );
678}
679
680function gformIsNumber(n) {
681	console.warn( 'gformIsNumber() has been deprecated since Gravity Forms 2.9. Use gform.utils.isNumber() instead.' );
682	return gform.utils.isNumber( n );
683}
684
685function gformIsNumeric(value, number_format){
686
687    switch(number_format){
688        case "decimal_dot" :
689            var r = new RegExp("^(-?[0-9]{1,3}(?:,?[0-9]{3})*(?:\.[0-9]+)?)$");
690            return r.test(value);
691        break;
692
693        case "decimal_comma" :
694            var r = new RegExp("^(-?[0-9]{1,3}(?:\.?[0-9]{3})*(?:,[0-9]+)?)$");
695            return r.test(value);
696        break;
697    }
698    return false;
699}
700
701//------------------------------------------------
702//---------- MULTI-PAGE --------------------------
703//------------------------------------------------
704function gformDeleteUploadedFile(formId, fieldId, deleteButton){
705    var parent = jQuery("#field_" + formId + "_" + fieldId);
706
707    var fileIndex = jQuery(deleteButton).parent().index();
708
709    var filePreview = jQuery( deleteButton ).closest( '.ginput_preview' )[0];
710    var fileId = filePreview.id;
711    filePreview.remove();
712
713    //removing the gform_hidden class
714    parent.find('.validation_message,#extensions_message_' + formId + '_' + fieldId).removeClass("gform_hidden");
715
716    //displaying post image label
717    parent.find(".ginput_post_image_file").show();
718
719    //clearing post image meta fields
720    parent.find("input[type=\"text\"]").val('');
721
722    //removing file from uploaded meta
723    var filesJson = jQuery('#gform_uploaded_files_' + formId).val();
724
725    if(filesJson){
726        var files = jQuery.secureEvalJSON(filesJson);
727        if(files) {
728            var inputName = "input_" + fieldId;
729            var $multfile = parent.find("#gform_multifile_upload_" + formId + "_" + fieldId );
730            if( $multfile.length > 0 ) {
731                files[inputName].splice(fileIndex, 1);
732                var settings = $multfile.data('settings');
733                var count = files[ inputName ].length;
734                if ( count === 0 ) {
735                    jQuery( '#' + settings.gf_vars.message_id ).html('');
736                    gfMultiFileUploader.toggleDisabled( settings, false );
737                } else {
738                    jQuery( '#error_' + fileId ).remove(); // Removing the file-specific validation message.
739                    var max = settings.gf_vars.max_files;
740                    if ( count < max )
741                        gfMultiFileUploader.toggleDisabled( settings, false );
742                }
743
744            } else {
745                files[inputName] = null;
746            }
747
748            jQuery('#gform_uploaded_files_' + formId).val(jQuery.toJSON(files));
749        }
750    }
751}
752
753
754//------------------------------------------------
755//---------- PRICE -------------------------------
756//------------------------------------------------
757var _gformPriceFields = new Array();
758var _anyProductSelected;
759
760function gformIsHidden(element){
761	isHidden = element.parents('.gfield').not(".gfield_hidden_product").css("display") == "none";
762
763	/**
764	 * Allows user to filter the logic for determining if a field is hidden by conditional logic..
765	 *
766	 * @since 2.8.10
767	 *
768	 * @param bool            Whether or not the field is hidden.
769	 * @param object $element jQuery object for field input.
770	 */
771	return gform.applyFilters('gform_is_hidden', isHidden, element);
772
773}
774
775/**
776 * Calculate total price when input is updated.
777 *
778 * @since 2.5.2 - This method is run through debounce() to avoid recursions.
779 *
780 */
781var gformCalculateTotalPrice =  gform.tools.debounce(function(formId){
782	if(!_gformPriceFields[formId]) {
783		return;
784	}
785	var price = 0;
786
787	_anyProductSelected = false; //Will be used by gformCalculateProductPrice().
788	for(var i=0; i<_gformPriceFields[formId].length; i++){
789		price += gformCalculateProductPrice(formId, _gformPriceFields[formId][i]);
790	}
791
792	//add shipping price if a product has been selected
793	if(_anyProductSelected){
794		//shipping price
795		var shipping = gformGetShippingPrice(formId)
796		price += shipping;
797	}
798
799	//gform_product_total filter. Allows users to perform custom price calculation
800	if(window["gform_product_total"])
801		price = window["gform_product_total"](formId, price);
802
803	price = gform.applyFilters('gform_product_total', price, formId);
804
805	gformUpdateTotalFieldPrice( formId, price );
806}, 50, false );
807
808/**
809 * Updates the value of the total field with a new price if it has changed.
810 *
811 * @since 2.5.5
812 *
813 * @param {string|number} formId The ID of the form with the total field.
814 * @param {int} price The new price to apply.
815 *
816 * @return {void}
817 */
818function gformUpdateTotalFieldPrice( formId, price ) {
819	var $totalElement = jQuery( '.ginput_total_' + formId );
820	if ( ! $totalElement.length > 0 ) {
821		return;
822	}
823
824	/**
825	 * @function priceHasChanged
826	 * @description For legacy, compare numeric values, otherwise compare currency as that's what
827	 * the input stores as value.
828	 *
829	 * @param {Object} priceData
830	 * @returns {boolean}
831	 */
832	var priceHasChanged = function( priceData ) {
833		return isLegacy
834			? priceData.current !== priceData.new
835			: priceData.current !== priceData.newFormatted;
836	}
837
838	// Check whether this form is in legacy mode.
839	var isLegacy = document.querySelector( '#gform_wrapper_' + formId + '.gform_legacy_markup_wrapper' );
840	// Input is hidden in legacy mode and comes after span that displays value, currently only the input is present and visible.
841	var $totalInput = isLegacy ? $totalElement.next() : $totalElement;
842	// Contains current value (numeric or currency formatted), new numeric value and newFormatted value
843	var priceData = {
844		current: String( $totalInput.val() ),
845		new: String( price ),
846		newFormatted: gformFormatMoney( String( price ), true ),
847	}
848
849	// New value is the same as the current value, bail before updating.
850	if ( ! priceHasChanged( priceData ) ) {
851		return;
852	}
853
854	// Legacy field
855	if ( isLegacy ) {
856		// Set input value to numeric value and trigger a change event for any js listeners in conditional logic
857		// or third party integrations.
858		$totalInput.val( priceData.new ).trigger( 'change' );
859		// Inject span with currency value for display.
860		$totalElement.html( priceData.newFormatted );
861		return;
862	}
863
864	// First set the input to the numeric value and trigger the change event so that js listeners get the value in expected format.
865	$totalInput.val( priceData.new ).trigger( 'change' );
866	// Then set the input to the currency value for display. If you have a script that wants to get the value
867	// of this input without listening to the change event you will have to also handle removing the currency formatting
868	// if expecting number in your code.
869	$totalInput.val( priceData.newFormatted );
870}
871
872function gformGetShippingPrice(formId){
873    var shippingField = jQuery(".gfield_shipping_" + formId + " input[readonly], .gfield_shipping_" + formId + " select, .gfield_shipping_" + formId + " input:checked");
874    var shipping = 0;
875    if(shippingField.length == 1 && !gformIsHidden(shippingField)){
876        if(shippingField.attr("readonly"))
877            shipping = shippingField.val();
878        else
879            shipping = gformGetPrice(shippingField.val());
880    }
881
882    return gformToNumber(shipping);
883}
884
885function gformGetFieldId(element){
886    var id = jQuery(element).attr("id");
887    var pieces = id.split("_");
888    if(pieces.length <=0)
889        return 0;
890
891    var fieldId = pieces[pieces.length-1];
892    return fieldId;
893
894}
895
896function gformCalculateProductPrice(form_id, productFieldId){
897
898    var suffix = '_' + form_id + '_' + productFieldId;
899
900
901    //Drop down auto-calculating labels
902    jQuery('.gfield_option' + suffix + ', .gfield_shipping_' + form_id).find('select').each(function(){
903
904        var dropdown_field = jQuery(this);
905        var selected_price = gformGetPrice(dropdown_field.val());
906        var field_id = dropdown_field.attr('id').split('_')[2];
907        dropdown_field.children('option').each(function(){
908            var choice_element = jQuery(this);
909            var label = gformGetOptionLabel(choice_element, choice_element.val(), selected_price, form_id, field_id);
910            choice_element.html(label);
911        });
912    });
913
914
915    //Checkboxes labels with prices
916    jQuery('.gfield_option' + suffix).find('.gfield_checkbox').find('input:checkbox').each(function(){
917        var checkbox_item = jQuery(this);
918        var id = checkbox_item.attr('id');
919        var field_id = id.split('_')[2];
920        var label_id = id.replace('choice_', '#label_');
921        var label_element = jQuery(label_id);
922        var label = gformGetOptionLabel(label_element, checkbox_item.val(), 0, form_id, field_id);
923        label_element.html(label);
924    });
925
926
927    //Radio button auto-calculating lables
928    jQuery('.gfield_option' + suffix + ', .gfield_shipping_' + form_id).find('.gfield_radio').each(function(){
929        var selected_price = 0;
930        var radio_field = jQuery(this);
931        var id = radio_field.attr('id');
932        var fieldId = id.split('_')[2];
933        var selected_value = radio_field.find('input:radio:checked').val();
934
935        if(selected_value)
936            selected_price = gformGetPrice(selected_value);
937
938        radio_field.find('input:radio').each(function(){
939            var radio_item = jQuery(this);
940            var label_id = radio_item.attr('id').replace('choice_', '#label_');
941            var label_element = jQuery(label_id);
942            if ( label_element ) {
943                var label = gformGetOptionLabel(label_element, radio_item.val(), selected_price, form_id, fieldId);
944                label_element.html(label);
945            }
946        });
947    });
948
949	var price = gformGetBasePrice(form_id, productFieldId);
950	var quantity = gformGetProductQuantity( form_id, productFieldId );
951
952	//calculating options if quantity is more than 0 (a product was selected).
953	if( quantity > 0 ) {
954
955		jQuery('.gfield_option' + suffix).find('input:checked, select').each(function(){
956			if(!gformIsHidden(jQuery(this)))
957				price += gformGetPrice(jQuery(this).val());
958		});
959
960		//setting global variable if quantity is more than 0 (a product was selected). Will be used when calculating total
961		_anyProductSelected = true;
962	}
963
964    price = price * quantity;
965
966	price = gformRoundPrice(price) ;
967
968
969    return price;
970}
971
972
973function gformGetProductQuantity(formId, productFieldId) {
974    //If product is not selected
975    if (!gformIsProductSelected(formId, productFieldId)) {
976        return 0;
977    }
978
979    var quantity,
980        quantityInput = jQuery( '#ginput_quantity_' + formId + '_' + productFieldId ),
981        numberFormat;
982
983    // New input ID starts from 2.5, for the single product and calculation fields.
984    if ( ! quantityInput.length ) {
985        quantityInput = jQuery( '#input_' + formId + '_' + productFieldId + '_1' );
986    }
987
988    if (gformIsHidden(quantityInput)) {
989        return 0;
990    }
991
992    if (quantityInput.length > 0) {
993
994        quantity = quantityInput.val();
995
996    } else {
997
998        quantityInput = jQuery('.gfield_quantity_' + formId + '_' + productFieldId + ' :input');
999        quantity = 1;
1000
1001        if (quantityInput.length > 0) {
1002            quantity = quantityInput.val();
1003
1004            var htmlId = quantityInput.attr('id'),
1005                fieldId = gf_get_input_id_by_html_id(htmlId);
1006
1007            numberFormat = gf_get_field_number_format( fieldId, formId, 'value' );
1008        }
1009
1010    }
1011
1012    if (!numberFormat)
1013        numberFormat = 'currency';
1014
1015    var decimalSeparator = gform.Currency.getDecimalSeparator(numberFormat);
1016
1017    quantity = gform.Currency.cleanNumber(quantity, '', '', decimalSeparator);
1018    if (!quantity)
1019        quantity = 0;
1020
1021    return quantity;
1022}
1023
1024
1025function gformIsProductSelected( formId, productFieldId ) {
1026
1027	var suffix = "_" + formId + "_" + productFieldId;
1028
1029	var productField = jQuery("#ginput_base_price" + suffix + ", .gfield_donation" + suffix + " input[type=\"text\"], .gfield_product" + suffix + " .ginput_amount");
1030	if( productField.val() && ! gformIsHidden(productField) ){
1031		return true;
1032	}
1033	else
1034	{
1035		productField = jQuery(".gfield_product" + suffix + " select, .gfield_product" + suffix + " input:checked, .gfield_donation" + suffix + " select, .gfield_donation" + suffix + " input:checked");
1036		if( productField.val() && ! gformIsHidden(productField) ){
1037			return true;
1038		}
1039	}
1040	return false;
1041}
1042
1043function gformGetBasePrice(formId, productFieldId){
1044
1045    var suffix = "_" + formId + "_" + productFieldId;
1046    var price = 0;
1047    var productField = jQuery("#ginput_base_price" + suffix+ ", .gfield_donation" + suffix + " input[type=\"text\"], .gfield_product" + suffix + " .ginput_amount");
1048    if(productField.length > 0){
1049        price = productField.val();
1050
1051        //If field is hidden by conditional logic, don't count it for the total
1052        if(gformIsHidden(productField)){
1053            price = 0;
1054        }
1055    }
1056    else
1057    {
1058        productField = jQuery(".gfield_product" + suffix + " select, .gfield_product" + suffix + " input:checked, .gfield_donation" + suffix + " select, .gfield_donation" + suffix + " input:checked");
1059        var val = productField.val();
1060        if(val){
1061            const value = gformParseChoiceValue( val );
1062            val = value.name;
1063            price = value.price || 0;
1064        }
1065
1066        //If field is hidden by conditional logic, don't count it for the total
1067        if(gformIsHidden(productField))
1068            price = 0;
1069
1070    }
1071
1072    var c = new gform.Currency(gf_global.gf_currency_config);
1073    price = c.toNumber(price);
1074    return price === false ? 0 : price;
1075}
1076
1077/**
1078 * @function gformParseChoiceValue
1079 * @description Parse a choice value into its name and price components.
1080 *
1081 * @since 2.9.30
1082 *
1083 * @param {string} value The choice value string in the format "name|price".
1084 *
1085 * @return {object} Returns an object in the format: { price: PRODUCT_PRICE, name: PRODUCT_NAME }
1086 */
1087function gformParseChoiceValue( value ) {
1088	if ( window.gform?.products?.parser?.parseChoiceValue ) {
1089		return window.gform.products.parser.parseChoiceValue( value );
1090	}
1091
1092	if ( ! value ) {
1093		return { name: null, price: null };
1094	}
1095
1096	const idx = value.lastIndexOf( '|' );
1097	if ( idx === -1 ) {
1098		return { name: value, price: null };
1099	}
1100
1101	const name = value.slice( 0, idx );
1102	const price = gformToNumber( value.slice( idx + 1 ) );
1103
1104	return { name, price };
1105}
1106
1107function gformFormatMoney(text, isNumeric){
1108    if(!gf_global.gf_currency_config)
1109        return text;
1110
1111    var currency = new gform.Currency(gf_global.gf_currency_config);
1112    return currency.toMoney(text, isNumeric);
1113}
1114
1115function gformFormatPricingField(element){
1116    if(gf_global.gf_currency_config){
1117        var currency = new gform.Currency(gf_global.gf_currency_config);
1118        var price = currency.toMoney(jQuery(element).val());
1119        jQuery(element).val(price);
1120    }
1121}
1122
1123function gformToNumber(text){
1124    var currency = new gform.Currency(gf_global.gf_currency_config);
1125    return currency.toNumber(text);
1126}
1127
1128function gformGetPriceDifference(currentPrice, newPrice){
1129
1130    //getting price difference
1131    var diff = parseFloat(newPrice) - parseFloat(currentPrice);
1132    price = gformFormatMoney(diff, true);
1133    if(diff > 0)
1134        price = "+" + price;
1135
1136    return price;
1137}
1138
1139function gformGetOptionLabel(element, selected_value, current_price, form_id, field_id){
1140    element = jQuery(element);
1141    var price = gformGetPrice(selected_value);
1142    var current_diff = element.attr('price');
1143    var original_label = element.html().replace(/<span(.*)<\/span>/i, "").replace(current_diff, "");
1144
1145    var diff = gformGetPriceDifference(current_price, price);
1146    diff = gformToNumber(diff) == 0 ? "" : " " + diff;
1147    element.attr('price', diff);
1148
1149    //don't add <span> for drop down items (not supported)
1150    var price_label = element[0].tagName.toLowerCase() == "option" ? diff : "<span class='ginput_price'>" + diff + "</span>";
1151    var label = original_label + price_label;
1152
1153    //calling hook to allow for custom option formatting
1154    if(window["gform_format_option_label"])
1155        label = gform_format_option_label(label, original_label, price_label, current_price, price, form_id, field_id);
1156
1157    return label;
1158}
1159
1160function gformGetProductIds(parent_class, element){
1161    var classes = jQuery(element).hasClass(parent_class) ? jQuery(element).attr("class").split(" ") : jQuery(element).parents("." + parent_class).attr("class").split(" ");
1162    for(var i=0; i<classes.length; i++){
1163        if(classes[i].substr(0, parent_class.length) == parent_class && classes[i] != parent_class)
1164            return {formId: classes[i].split("_")[2], productFieldId: classes[i].split("_")[3]};
1165    }
1166    return {formId:0, fieldId:0};
1167}
1168
1169function gformGetPrice(text){
1170    var val = gformParseChoiceValue( text );
1171
1172    if(val.price)
1173         return val.price;
1174
1175    return 0;
1176}
1177
1178function gformRoundPrice(price){
1179
1180	var currency = new gform.Currency(gf_global.gf_currency_config);
1181    var roundedPrice = currency.numberFormat( price, currency.currency['decimals'], '.', '' );
1182
1183    return parseFloat( roundedPrice );
1184}
1185
1186function gformRegisterPriceField(item){
1187
1188	if( ! item.formId ) {
1189		return;
1190	}
1191
1192    if(!_gformPriceFields[item.formId]) {
1193		_gformPriceFields[item.formId] = new Array();
1194	}
1195
1196    //ignore price fields that have already been registered
1197    for(var i=0; i<_gformPriceFields[item.formId].length; i++)
1198        if(_gformPriceFields[item.formId][i] == item.productFieldId)
1199            return;
1200
1201    //registering new price field
1202    _gformPriceFields[item.formId].push(item.productFieldId);
1203}
1204
1205function gformInitPriceFields(){
1206
1207	// Getting all product fields and registering them.
1208    const priceFields = gform.tools.getNodes('.gfield_price', true, document, true );
1209	priceFields.forEach( ( field ) => {
1210		const productIds = gformGetProductIds( 'gfield_price', field );
1211		gformRegisterPriceField( productIds );
1212	});
1213
1214
1215	// Getting all forms that have product fields.
1216	const formIds = Object.keys( _gformPriceFields );
1217	formIds.forEach( ( formId ) => {
1218
1219		gformCalculateTotalPrice( formId );
1220
1221		gform.state.watch( formId, ['products', 'feeds'], gformHandleProductChange );
1222		bindProductChangeEvent();
1223	} );
1224}
1225
1226function bindProductChangeEvent() {
1227	// For backwards compatibility, fire jQuery gform_price_change event.
1228	document.addEventListener( 'gform/products/product_field_changed', function( event ) {
1229		const productIds = { formId : event.detail.formId, productFieldId : event.detail.productFieldId }
1230
1231		jQuery( document ).trigger( 'gform_price_change', [ productIds, event.detail.htmlInput, this ] );
1232	} );
1233}
1234
1235
1236function gformHandleProductChange( formId, key, data ) {
1237	gformCalculateTotalPrice( formId );
1238}
1239
1240//-------------------------------------------
1241//---------- PASSWORD -----------------------
1242//-------------------------------------------
1243function gformShowPasswordStrength(fieldId){
1244    var password = document.getElementById( fieldId ).value,
1245        confirm = document.getElementById( fieldId + '_2' ) ? document.getElementById( fieldId + '_2' ).value : '';
1246
1247    var result = gformPasswordStrength( password, confirm ),
1248        text = window[ 'gf_text' ][ "password_" + result ],
1249        resultClass = result === 'unknown' ? 'blank' : result;
1250
1251    jQuery("#" + fieldId + "_strength").val(result);
1252    jQuery("#" + fieldId + "_strength_indicator").removeClass("blank mismatch short good bad strong").addClass(resultClass).html(text);
1253}
1254
1255// Password strength meter
1256function gformPasswordStrength( password1, password2 ) {
1257
1258    if ( password1.length <= 0 ) {
1259        return 'blank';
1260    }
1261
1262	var disallowedList = wp.passwordStrength.hasOwnProperty( 'userInputDisallowedList' ) ? wp.passwordStrength.userInputDisallowedList() : wp.passwordStrength.userInputBlacklist(),
1263	    strength = wp.passwordStrength.meter( password1, disallowedList, password2 );
1264
1265    switch ( strength ) {
1266
1267        case -1:
1268            return 'unknown';
1269
1270        case 2:
1271            return 'bad';
1272
1273        case 3:
1274            return 'good';
1275
1276        case 4:
1277            return 'strong';
1278
1279        case 5:
1280            return 'mismatch';
1281
1282        default:
1283            return 'short';
1284
1285    }
1286
1287}
1288
1289function gformToggleShowPassword( fieldId ) {
1290    var $password = jQuery( '#' + fieldId ),
1291        $button = $password.parent().find( 'button' ),
1292        $icon = $button.find( 'span' ),
1293        currentType = $password.attr( 'type' );
1294
1295    switch ( currentType ) {
1296        case 'password':
1297            $password.attr( 'type', 'text' );
1298            $button.attr( 'aria-label', $button.attr( 'data-label-hide' ) );
1299            $icon.removeClass( 'dashicons-hidden' ).addClass( 'dashicons-visibility' );
1300            break;
1301        case 'text':
1302            $password.attr( 'type', 'password' );
1303            $button.attr( 'aria-label', $button.attr( 'data-label-show' ) );
1304            $icon.removeClass( 'dashicons-visibility' ).addClass( 'dashicons-hidden' );
1305            break;
1306    }
1307}
1308
1309//----------------------------
1310//------ CHECKBOX FIELD ------
1311//----------------------------
1312
1313function gformToggleCheckboxes( toggleElement ) {
1314
1315	var checked,
1316        $toggleElement        = jQuery( toggleElement ),
1317        toggleElementCheckbox = $toggleElement.is( 'input[type="checkbox"]' ),
1318        $toggle               = $toggleElement.parent(),
1319	    $toggleLabel          = $toggle.find( 'label' ),
1320	    $checkboxes           = $toggle.parent().find( '.gchoice:not( .gchoice_select_all )' ),
1321	    formId         = gf_get_form_id_by_html_id( $toggle.parents( '.gfield' ).attr( 'id' ) ),
1322	    calcObj               = rgars( window, 'gf_global/gfcalc/' + formId );
1323
1324    // Determine checked state.
1325    if ( toggleElementCheckbox ) {
1326
1327        checked = toggleElement.checked;
1328
1329    } else {
1330
1331        // Get checked data.
1332        var checkedData = $toggleElement.data( 'checked' );
1333
1334        if ( typeof checkedData === 'boolean' ) {
1335            checked = !checkedData;
1336        } else {
1337            checked = !( parseInt( checkedData ) === 1 )
1338        }
1339
1340    }
1341
1342    // Set checkboxes state.
1343	$checkboxes.each( function() {
1344
1345		// Set checkbox checked state.
1346		jQuery( 'input[type="checkbox"]', this ).prop( 'checked', checked ).trigger( 'change' );
1347
1348		// Execute onclick event.
1349		if ( typeof jQuery( 'input[type="checkbox"]', this )[0].onclick === 'function' ) {
1350			jQuery( 'input[type="checkbox"]', this )[0].onclick();
1351		}
1352
1353	} );
1354
1355	// Change toggle label, checked state.
1356	gformToggleSelectAll( toggleElement, checked ? 'deselect' : 'select' );
1357
1358    // Announce change.
1359    wp.a11y.speak( checked ? gf_field_checkbox.strings.selected : gf_field_checkbox.strings.deselected );
1360
1361	if ( calcObj ) {
1362		calcObj.runCalcs( formId, calcObj.formulaFields );
1363	}
1364
1365}
1366
1367function gformToggleSelectAll( selectAllElement, action ) {
1368	var $selectAllElement = jQuery( selectAllElement ),
1369		toggleElementCheckbox = $selectAllElement.is( 'input[type="checkbox"]' ),
1370		$toggle               = toggleElementCheckbox ? $selectAllElement.parent() : $selectAllElement.prev(),
1371		$toggleLabel          = $toggle.find( 'label' );
1372
1373	if ( ! toggleElementCheckbox ) {
1374		$selectAllElement.html( action === 'deselect' ? $selectAllElement.data( 'label-deselect' ) : $selectAllElement.data( 'label-select' ) );
1375		$selectAllElement.data( 'checked', action === 'deselect' ? 1 : 0 );
1376	}
1377}
1378
1379jQuery(document).on('click', '.gfield_choice--select_all_enabled *', function() {
1380	var $select_all = jQuery( this ).closest( '.gfield_choice--select_all_enabled' ).find( '.gfield_choice_all_toggle' );
1381
1382	// if any of the checkboxes are unchecked, turn the "deselect all" button/checkbox into a "select all" button/checkbox
1383	if ( jQuery( this ).is( '.gchoice input[type="checkbox"]' ) ) {
1384		if( $select_all.is( 'input[type="checkbox"]' ) ) {
1385			if ( !jQuery( this ).prop( 'checked' ) ) {
1386				$select_all.prop( 'checked', false );
1387			}
1388		} else {
1389			gformToggleSelectAll( $select_all, 'select' );
1390		}
1391	}
1392
1393	// if all checkboxes that are not the "select all" checkbox are checked, turn the "select all" button/checkbox into a "deselect all" button/checkbox
1394	if ( jQuery( this ).is( '.gchoice input[type="checkbox"]' ) ) {
1395		var $checkboxes = jQuery( this ).closest( '.gfield_choice--select_all_enabled' ).find( '.gchoice input[type="checkbox"]:not(".gfield_choice_all_toggle")' );
1396		if ( $checkboxes.length === $checkboxes.filter( ':checked' ).length ) {
1397			if( $select_all.is( 'input[type="checkbox"]' ) ) {
1398				$select_all.prop( 'checked', true );
1399				gformToggleSelectAll( $select_all, 'deselect' );
1400			} else {
1401				gformToggleSelectAll( $select_all, 'deselect' );
1402			}
1403		}
1404	}
1405
1406});
1407
1408//----------------------------
1409//------ RADIO FIELD ------
1410//----------------------------
1411
1412function gformToggleRadioOther( radioElement ) {
1413
1414    // Get Other input element.
1415    var $other = gform.tools.getClosest( radioElement, '.ginput_container_radio' ).querySelector( 'input.gchoice_other_control' );
1416
1417    if ( $other ) {
1418        $other.disabled = radioElement.value !== 'gf_other_choice';
1419    }
1420
1421}
1422
1423//----------------------------
1424//------ LIST FIELD ----------
1425//----------------------------
1426
1427function gformAddListItem( addButton, max ) {
1428
1429    var $addButton = jQuery( addButton );
1430
1431    if( $addButton.hasClass( 'gfield_icon_disabled' ) ) {
1432        return;
1433    }
1434
1435    var $group     = $addButton.parents( '.gfield_list_group' ),
1436        $clone     = $group.clone(),
1437        $container = $group.parents( '.gfield_list_container' ),
1438        tabindex   = $clone.find( ':input:last' ).attr( 'tabindex' );
1439
1440    // reset all inputs to empty state
1441    $clone
1442        .find( 'input, select, textarea' ).attr( 'tabindex', tabindex )
1443        .not( ':checkbox, :radio' ).val( '' ).attr( 'value', '' );
1444    $clone.find( ':checkbox, :radio' ).prop( 'checked', false );
1445
1446    $clone = gform.applyFilters( 'gform_list_item_pre_add', $clone, $group );
1447
1448    $group.after( $clone );
1449
1450    gformToggleIcons( $container, max );
1451    gformAdjustClasses( $container );
1452    gformAdjustRowAttributes( $container );
1453
1454    gform.doAction( 'gform_list_post_item_add', $clone, $container );
1455
1456    wp.a11y.speak( window.gf_global.strings.newRowAdded );
1457
1458}
1459
1460function gformDeleteListItem( deleteButton, max ) {
1461
1462	var $deleteButton = jQuery( deleteButton );
1463	if ( $deleteButton.prop( 'disabled' ) ) {
1464		return;
1465	} else {
1466		$deleteButton.prop( 'disabled', true );
1467	}
1468
1469	var $group     = $deleteButton.parents( '.gfield_list_group' ),
1470		$container = $group.parents( '.gfield_list_container' );
1471
1472    $group.remove();
1473
1474    gformToggleIcons( $container, max );
1475    gformAdjustClasses( $container );
1476    gformAdjustRowAttributes( $container );
1477
1478    gform.doAction( 'gform_list_post_item_delete', $container );
1479
1480    wp.a11y.speak( window.gf_global.strings.rowRemoved );
1481
1482}
1483
1484function gformAdjustClasses( $container ) {
1485
1486    var $groups = $container.find( '.gfield_list_group' );
1487
1488    $groups.each( function( i ) {
1489
1490        var $group       = jQuery( this ),
1491            oddEvenClass = ( i + 1 ) % 2 == 0 ? 'gfield_list_row_even' : 'gfield_list_row_odd';
1492
1493        $group.removeClass( 'gfield_list_row_odd gfield_list_row_even' ).addClass( oddEvenClass );
1494
1495    } );
1496
1497}
1498
1499function gformAdjustRowAttributes( $container ) {
1500
1501    if( $container.parents( '.gform_wrapper' ).hasClass( 'gform_legacy_markup_wrapper' ) ) {
1502        return;
1503    }
1504
1505    $container.find( '.gfield_list_group' ).each( function( i ) {
1506
1507        var $input = jQuery( this ).find( 'input, select, textarea' );
1508        $input.each( function( index, input ) {
1509            var $this = jQuery( input );
1510            $this.attr( 'aria-label', $this.data( 'aria-label-template' ).gformFormat( i + 1 ) );
1511        } );
1512
1513        var $remove = jQuery( this ).find( '.delete_list_item' );
1514        $remove.attr( 'aria-label', $remove.data( 'aria-label-template' ).gformFormat( i + 1 ) );
1515
1516    } );
1517
1518}
1519
1520function gformToggleIcons( $container, max ) {
1521
1522    var groupCount  = $container.find( '.gfield_list_group' ).length,
1523        $addButtons = $container.find( '.add_list_item' ),
1524        isLegacy    =  typeof gf_legacy !== 'undefined' && gf_legacy.is_legacy;
1525
1526	if ( groupCount === 1 ) {
1527		$container.find( '.delete_list_item' ).prop( 'disabled', true ).css( 'visibility', 'hidden' );
1528	} else {
1529		$container.find( '.delete_list_item' ).prop( 'disabled', false ).css( 'visibility', 'visible' );
1530	}
1531
1532    if ( max > 0 && groupCount >= max ) {
1533
1534        // store original title in the add button
1535        $addButtons.data( 'title', $container.find( '.add_list_item' ).attr( 'title' ) );
1536        $addButtons.addClass( 'gfield_icon_disabled' ).attr( 'title', '' );
1537
1538		if ( ! isLegacy ) {
1539			$addButtons.prop( 'disabled', true );
1540		}
1541
1542    } else if( max > 0 ) {
1543
1544        $addButtons.removeClass( 'gfield_icon_disabled' );
1545
1546	    if ( ! isLegacy ) {
1547		    $addButtons.prop( 'disabled', false );
1548	    }
1549
1550        if( $addButtons.data( 'title' ) )   {
1551            $addButtons.attr( 'title', $addButtons.data( 'title' ) );
1552        }
1553
1554    }
1555}
1556
1557//-----------------------------------
1558//--------- REPEATER FIELD ----------
1559//-----------------------------------
1560
1561function gformAddRepeaterItem( addButton, max ) {
1562
1563	var $addButton = jQuery( addButton );
1564
1565	if( $addButton.hasClass( 'gfield_icon_disabled' ) ) {
1566		return;
1567	}
1568
1569	var $item     = $addButton.closest( '.gfield_repeater_item' ),
1570		$clone     = $item.clone(),
1571		$container = $item.closest( '.gfield_repeater_container' ),
1572		tabindex   = $clone.find( ':input:last' ).attr( 'tabindex' );
1573
1574	// reset all inputs to empty state
1575	$clone
1576		.find( 'input[type!="hidden"], select, textarea' ).attr( 'tabindex', tabindex )
1577		.not( ':checkbox, :radio' ).each( function( index ){
1578			// if the field has a value pre-populated, use that value in the cloned field
1579			if( jQuery( this ).attr( 'value' ) ) {
1580				jQuery( this ).val( jQuery( this ).attr( 'value' ) );
1581			} else if ( jQuery( this ).is( 'textarea' ) ) {
1582				jQuery( this ).val( this.innerHTML );
1583			} else {
1584				jQuery( this ).val( '' );
1585			}
1586	} );
1587	$clone.find( ':checkbox, :radio' ).prop( 'checked', false );
1588	$clone.find('.validation_message').remove();
1589	$clone.find('.gform-datepicker.initialized').removeClass('initialized');
1590
1591	$clone = gform.applyFilters( 'gform_repeater_item_pre_add', $clone, $item );
1592
1593	$item.after( $clone );
1594
1595	var $cells = $clone.children('.gfield_repeater_cell');
1596	$cells.each(function () {
1597		var $subContainer = jQuery(this).find('.gfield_repeater_container').first();
1598		if ($subContainer.length > 0) {
1599			resetContainerItems = function ($c) {
1600				$c.children('.gfield_repeater_items').children('.gfield_repeater_item').each(function (i) {
1601					var $children = jQuery(this).children('.gfield_repeater_cell');
1602					$children.each(function () {
1603						var $subSubContainer = jQuery(this).find('.gfield_repeater_container').first();
1604						if ($subSubContainer.length > 0) {
1605							resetContainerItems($subSubContainer);
1606						}
1607					})
1608				})
1609				$c.children('.gfield_repeater_items').children('.gfield_repeater_item').not(':first').remove();
1610			}
1611			resetContainerItems($subContainer);
1612		}
1613	})
1614
1615	gformResetRepeaterAttributes($container);
1616
1617	if ( typeof gformInitDatepicker == 'function' ) {
1618		$container.find('.ui-datepicker-trigger').remove();
1619		$container.find('.hasDatepicker').removeClass('hasDatepicker');
1620		gformInitDatepicker();
1621	}
1622
1623	gformBindFormatPricingFields();
1624
1625	gformToggleRepeaterButtons( $container, max );
1626
1627	gform.doAction('gform_repeater_post_item_add', $clone, $container);
1628
1629}
1630
1631function gformDeleteRepeaterItem(deleteButton, max) {
1632
1633	var $deleteButton = jQuery(deleteButton),
1634		$group = $deleteButton.closest('.gfield_repeater_item'),
1635		$container = $group.closest('.gfield_repeater_container');
1636
1637	$group.remove();
1638
1639	gformResetRepeaterAttributes($container);
1640	gformToggleRepeaterButtons($container, max);
1641
1642	gform.doAction('gform_repeater_post_item_delete', $container);
1643
1644}
1645
1646function gformResetRepeaterAttributes($container, depth, row) {
1647
1648	var cachedRadioSelection = null;
1649
1650	if (typeof depth === 'undefined') {
1651		depth = 0;
1652	}
1653
1654	if (typeof row === 'undefined') {
1655		row = 0;
1656	}
1657
1658	$container.children('.gfield_repeater_items').children('.gfield_repeater_item').each(function () {
1659		var $children = jQuery(this).children('.gfield_repeater_cell');
1660		$children.each(function () {
1661			var $cell = jQuery(this);
1662			var $subContainer = jQuery(this).find('.gfield_repeater_container').first();
1663
1664			if ($subContainer.length > 0) {
1665				var newDepth = depth + 1;
1666				gformResetRepeaterAttributes($subContainer, newDepth, row);
1667				return;
1668			}
1669
1670			jQuery(this).find('input, select, textarea, :checkbox, :radio').each(function () {
1671				var $this = jQuery(this);
1672				var name = $this.attr('name');
1673
1674				if ( typeof name == 'undefined' ) {
1675					return;
1676				}
1677
1678				var regEx = /^(input_[^\[]*)((\[[0-9]+\])+)/,
1679					parts = regEx.exec(name);
1680
1681				if (!parts) {
1682					return;
1683				}
1684				var inputName = parts[1],
1685					arayParts = parts[2],
1686					regExIndex = /\[([0-9]+)\]/g,
1687					indexes = [],
1688					match = regExIndex.exec(arayParts);
1689
1690				while (match != null) {
1691					indexes.push(match[1]);
1692					match = regExIndex.exec(arayParts);
1693				}
1694				var newNameIndex = parts[1];
1695				indexes = indexes.reverse();
1696				var newId = '';
1697				for (var n = indexes.length - 1; n >= 0; n--) {
1698					if (n == depth) {
1699						newNameIndex += '[' + row + ']';
1700						newId += '-' + row;
1701					} else {
1702						newNameIndex += '[' + indexes[n] + ']';
1703						newId += '-' + indexes[n];
1704					}
1705				}
1706
1707				var currentId = $this.attr('id');
1708				var $label = $cell.find("label[for='" + currentId + "']");
1709
1710				if ( currentId ) {
1711					var matches = currentId.match(/((choice|input)_[0-9|_]*)-/);
1712					if ( matches && matches[2] ) {
1713						newId = matches[1] + newId;
1714						$label.attr('for', newId);
1715						$this.attr('id', newId);
1716					}
1717				}
1718				var newName = name.replace(parts[0], newNameIndex),
1719					newNameIsChecked = jQuery('input[name="'+ newName +'"]').is(':checked');
1720
1721				if ( $this.is(':radio') && $this.is(':checked') && name !== newName && newNameIsChecked ) {
1722					if ( cachedRadioSelection !== null ) {
1723						cachedRadioSelection.prop('checked', true);
1724					}
1725
1726					$this.prop('checked', false);
1727					cachedRadioSelection = $this;
1728				}
1729
1730				$this.attr('name', newName);
1731			});
1732		});
1733		if (depth === 0) {
1734			row++;
1735		}
1736	});
1737
1738	if ( cachedRadioSelection !== null ) {
1739		cachedRadioSelection.prop('checked', true);
1740		cachedRadioSelection = null;
1741	}
1742
1743}
1744
1745function gformToggleRepeaterButtons($container) {
1746
1747	var max = $container.closest('.gfield_repeater_wrapper').data('max_items'),
1748		groupCount = $container.children('.gfield_repeater_items').children('.gfield_repeater_item').length,
1749		$buttonsContainer = $container.children('.gfield_repeater_items').children('.gfield_repeater_item').children('.gfield_repeater_buttons'),
1750		$addButtons = $buttonsContainer.children('.add_repeater_item');
1751
1752	$buttonsContainer.children('.remove_repeater_item').css('visibility', groupCount == 1 ? 'hidden' : 'visible');
1753
1754	if (max > 0 && groupCount >= max) {
1755
1756		// store original title in the add button
1757		$addButtons.data('title', $buttonsContainer.children('.add_repeater_item').attr('title'));
1758		$addButtons.addClass('gfield_icon_disabled').attr('title', '');
1759
1760	} else if (max > 0) {
1761
1762		$addButtons.removeClass('gfield_icon_disabled');
1763
1764		if ($addButtons.data('title')) {
1765			$addButtons.attr('title', $addButtons.data('title'));
1766		}
1767	}
1768
1769	$container
1770		.children('.gfield_repeater_items')
1771		.children('.gfield_repeater_item')
1772		.children( '.gfield_repeater_cell').each(function (i) {
1773			var $subContainer = jQuery(this).find('.gfield_repeater_container').first();
1774			if ($subContainer.length > 0) {
1775				gformToggleRepeaterButtons($subContainer);
1776			}
1777		});
1778}
1779
1780
1781//-----------------------------------
1782//------ CREDIT CARD FIELD ----------
1783//-----------------------------------
1784function gformMatchCard(id) {
1785
1786    var cardType = gformFindCardType(jQuery('#' + id).val());
1787    var cardContainer = jQuery('#' + id).parents('.gfield').find('.gform_card_icon_container');
1788
1789    if(!cardType) {
1790
1791        jQuery(cardContainer).find('.gform_card_icon').removeClass('gform_card_icon_selected gform_card_icon_inactive');
1792
1793    } else {
1794
1795        jQuery(cardContainer).find('.gform_card_icon').removeClass('gform_card_icon_selected').addClass('gform_card_icon_inactive');
1796        jQuery(cardContainer).find('.gform_card_icon_' + cardType).removeClass('gform_card_icon_inactive').addClass('gform_card_icon_selected');
1797    }
1798}
1799
1800function gformFindCardType(value) {
1801
1802    if(value.length < 4)
1803        return false;
1804
1805    var rules = window['gf_cc_rules'];
1806    var validCardTypes = new Array();
1807
1808    for(type in rules) {
1809
1810        //needed when implementing for in loops
1811        if(!rules.hasOwnProperty(type))
1812            continue;
1813
1814
1815        for(i in rules[type]) {
1816
1817            if(!rules[type].hasOwnProperty(i))
1818                continue;
1819
1820            if(rules[type][i].indexOf(value.substring(0, rules[type][i].length)) === 0) {
1821                validCardTypes[validCardTypes.length] = type;
1822                break;
1823            }
1824
1825        }
1826    }
1827
1828    return validCardTypes.length == 1 ? validCardTypes[0].toLowerCase() : false;
1829}
1830
1831function gformToggleCreditCard(){
1832    if(jQuery("#gform_payment_method_creditcard").is(":checked"))
1833        jQuery(".gform_card_fields_container").slideDown();
1834    else
1835        jQuery(".gform_card_fields_container").slideUp();
1836}
1837
1838
1839//----------------------------------------
1840//------ CHOSEN DROP DOWN FIELD ----------
1841//----------------------------------------
1842
1843function gformInitChosenFields( fieldList, noResultsText ) {
1844    return jQuery( fieldList ).each( function(){
1845		var element = jQuery( this );
1846	    var isConvoForm = typeof gfcf_theme_config !== 'undefined' ? ( gfcf_theme_config !== null && typeof gfcf_theme_config.data !== 'undefined' ? gfcf_theme_config.data.is_conversational_form : undefined ) : false;
1847
1848        // RTL support
1849        if( jQuery( 'html' ).attr( 'dir' ) == 'rtl' ) {
1850            element.addClass( 'chosen-rtl chzn-rtl' );
1851        }
1852
1853        // only initialize once
1854        if( ( element.is( ':visible' ) || isConvoForm ) && element.siblings( '.chosen-container' ).length == 0 ) {
1855			var chosenOptions = { no_results_text: noResultsText };
1856			if ( isConvoForm ) {
1857				chosenOptions.width = element.css( 'inline-size' );
1858			}
1859            var options = gform.applyFilters( 'gform_chosen_options', chosenOptions, element );
1860            element.chosen( options );
1861        }
1862    });
1863}
1864
1865//----------------------------------------
1866//--- CURRENCY FORMAT NUMBER FIELD -------
1867//----------------------------------------
1868
1869function gformInitCurrencyFormatFields(fieldList){
1870    jQuery(fieldList).each(function(){
1871        var $this = jQuery(this);
1872        $this.val( gformFormatMoney( jQuery(this).val() ) );
1873    }).change( function( event ) {
1874            jQuery(this).val( gformFormatMoney( jQuery(this).val() ) );
1875        });
1876}
1877
1878
1879
1880//----------------------------------------
1881//------ JS MERGE TAGS -------------------
1882//----------------------------------------
1883
1884/**
1885 * @var {Object} GFMergeTag Handles MergeTag Operations.
1886 * @remove-in 4.0
1887 * @deprecated Use gform.mergeTags instead.
1888 */
1889var GFMergeTag = function() {
1890	/**
1891     * Gets the merge tag value for the specified input Id
1892	 * @param formId  The current form Id
1893	 * @param inputId The input Id to get the merge tag from. This could be a field id (i.e. 1) or a specific input Id for multi-input fields (i.e. 1.2)
1894	 * @param modifier The merge tag modifier to be used. i.e. value, currency, price, etc...
1895	 * @returns       Returns a string containing the merge tag value for the specified input Id
1896     * @remove-in 4.0
1897	 * @deprecated Use gform.mergeTags.getFieldValue() instead.
1898	 */
1899	GFMergeTag.getMergeTagValue = function( formId, inputId, modifier ) {
1900
1901		const mergeTagInfo = gform.mergeTags.getMergeTagInfo( formId, inputId, modifier );
1902
1903		if ( ! mergeTagInfo.isVisible ) {
1904			return '';
1905		}
1906
1907		const inputForFilter = jQuery( mergeTagInfo.input );
1908		let value = window.gform.applyFilters( 'gform_value_merge_tag_' + formId + '_' + mergeTagInfo.fieldId, false, inputForFilter, mergeTagInfo.modifier );
1909		if ( value !== false ) {
1910			return value;
1911		}
1912
1913		return gform.mergeTags.getFieldValue( formId, inputId, modifier, mergeTagInfo );
1914	}
1915
1916	/**
1917     * Parses the specified text for merge tags, and replaces all of them with the appropriate merge tag values. Returns the resulting string
1918	 * @param formId    The current form Id
1919	 * @param text      The text containing merge tags
1920	 * @returns         Retuns the original "text" strings with all merge tags replaced with the appropriate merge tag values
1921     * @remove-in 4.0
1922	 * @deprecated Use gform.mergeTags.replaceMergeTags() instead.
1923	 */
1924	GFMergeTag.replaceMergeTags = function( formId, text ) {
1925		return gform.mergeTags.replaceMergeTags( formId, text );
1926	}
1927
1928	/**
1929	 * @deprecated Use gform.mergeTags.formatValue() instead.
1930     * @remove-in 4.0
1931	 */
1932	GFMergeTag.formatValue = function( value, modifier ) {
1933		return gform.mergeTags.formatValue( value, modifier );
1934	}
1935
1936	/**
1937     * Parses the merge tags in the specified text and returns an array of all the matched merge tags
1938	 *
1939	 * @param text  The text with merge tags to be parsed
1940	 * @param regEx The regular expression to be used to parse for merge tags.
1941	 *
1942	 * @returns Returns an array with all the merge tags that were matched in the original text
1943	 * @deprecated Use gform.mergeTags.parseMergeTags() instead.
1944     * @remove-in 4.0
1945	 */
1946	GFMergeTag.parseMergeTags = function( text, regEx ) {
1947		return gform.mergeTags.parseMergeTags( text, regEx );
1948	}
1949}
1950
1951new GFMergeTag();
1952
1953
1954//----------------------------------------
1955//------ CALCULATION FUNCTIONS -----------
1956//----------------------------------------
1957
1958var GFCalc = function(formId, formulaFields){
1959
1960	this.formId = formId;
1961	this.formulaFields = formulaFields;
1962
1963    this.exprPatt = /^[0-9 -/*\(\)]+$/i;
1964    this.isCalculating = {};
1965
1966    this.init = function(formId, formulaFields) {
1967
1968        var calc = this;
1969
1970        // @since 2.5.10 - namespace event to avoid multiple bindings.
1971	    jQuery(document)
1972		    .off("gform_post_conditional_logic.gfCalc_{0}".gformFormat(formId))
1973		    .on("gform_post_conditional_logic.gfCalc_{0}".gformFormat(formId), function(){
1974			    calc.runCalcs( formId, formulaFields );
1975	    } );
1976
1977        for(var i=0; i<formulaFields.length; i++) {
1978            var formulaField = jQuery.extend({}, formulaFields[i]);
1979            this.runCalc(formulaField, formId);
1980            this.bindCalcEvents(formulaField, formId);
1981        }
1982
1983    }
1984
1985    this.runCalc = function(formulaField, formId) {
1986        var calcObj      = this,
1987            field        = jQuery('#field_' + formId + '_' + formulaField.field_id),
1988            formulaInput = field.hasClass( 'gfield_price' ) ? jQuery( '#ginput_base_price_' + formId + '_' + formulaField.field_id ) : jQuery( '#input_' + formId + '_' + formulaField.field_id ),
1989            previous_val = formulaInput.val(),
1990            formula      = gform.applyFilters( 'gform_calculation_formula', formulaField.formula, formulaField, formId, calcObj ),
1991            expr         = calcObj.replaceFieldTags( formId, formula, formulaField ).replace(/(\r\n|\n|\r)/gm,""),
1992            result       = '';
1993
1994        if(calcObj.exprPatt.test(expr)) {
1995            try {
1996
1997                //run calculation
1998                result = eval(expr);
1999
2000            } catch( e ) { }
2001        } else {
2002        	return;
2003        }
2004
2005        // if result is positive infinity, negative infinity or a NaN, defaults to 0
2006        if( ! isFinite( result ) )
2007            result = 0;
2008
2009        // allow users to modify result with their own function
2010        if( window["gform_calculation_result"] ) {
2011            result = window["gform_calculation_result"](result, formulaField, formId, calcObj);
2012            if( window.console )
2013                console.log( '"gform_calculation_result" function is deprecated since version 1.8! Use "gform_calculation_result" JS hook instead.' );
2014        }
2015
2016        // allow users to modify result with their own function
2017        result = gform.applyFilters( 'gform_calculation_result', result, formulaField, formId, calcObj );
2018
2019        // allow result to be custom formatted
2020        var formattedResult = gform.applyFilters( 'gform_calculation_format_result', false, result, formulaField, formId, calcObj );
2021
2022        var numberFormat = gf_get_field_number_format(formulaField.field_id, formId);
2023
2024        //formatting number
2025        if( formattedResult !== false) {
2026            result = formattedResult;
2027        }
2028        else if( field.hasClass( 'gfield_price' ) || numberFormat == "currency") {
2029
2030            result = gformFormatMoney(result ? result : 0, true);
2031        }
2032        else {
2033
2034            var decimalSeparator = ".";
2035            var thousandSeparator = ",";
2036
2037            if(numberFormat == "decimal_comma"){
2038                decimalSeparator = ",";
2039                thousandSeparator = ".";
2040            }
2041
2042            result = gformFormatNumber(result, !gform.utils.isNumber(formulaField.rounding) ? -1 : formulaField.rounding, decimalSeparator, thousandSeparator);
2043        }
2044
2045        //If value doesn't change, abort.
2046        //This is needed to prevent an infinite loop condition with conditional logic
2047        if( result == previous_val )
2048            return;
2049
2050        // if this is a calculation product, handle differently
2051        if(field.hasClass('gfield_price')) {
2052            jQuery('#input_' + formId + '_' + formulaField.field_id).text(result);
2053
2054			// Firing jQuery change event for backwards compatibility with legacy code.
2055			formulaInput.val(result).trigger('change');
2056
2057			// Firing native change event for compatibility with new code in JS bundle.
2058			if ( formulaInput && formulaInput.length > 0 ) {
2059				window.gform.utils.trigger( { event: 'change', el: formulaInput[0], native: true } );
2060			}
2061
2062            // Announce the price change of the product only if there's no Total field.
2063            if ( jQuery( '.gfield_label_product' ).length && ! jQuery( '.ginput_total' ).length ) {
2064                result = jQuery( 'label[ for=input_' + formId + '_' + formulaField.field_id + '_1 ]' ).find( '.gfield_label_product' ).text() + ' ' + result;
2065                wp.a11y.speak( result );
2066            }
2067        } else {
2068            formulaInput.val(result).trigger('change');
2069        }
2070
2071    };
2072
2073    this.runCalcs = function( formId, formulaFields ) {
2074	    for(var i=0; i<formulaFields.length; i++) {
2075		    var formulaField = jQuery.extend({}, formulaFields[i]);
2076		    this.runCalc( formulaField, formId );
2077	    }
2078    }
2079
2080    this.bindCalcEvents = function(formulaField, formId) {
2081
2082        var calcObj = this;
2083        var formulaFieldId = formulaField.field_id;
2084        var matches = GFMergeTag.parseMergeTags( formulaField.formula );
2085
2086        calcObj.isCalculating[formulaFieldId] = false;
2087
2088        for(var i in matches) {
2089
2090            if(! matches.hasOwnProperty(i))
2091                continue;
2092
2093            var inputId = matches[i][1];
2094            var fieldId = parseInt(inputId,10);
2095            var input = jQuery('#field_' + formId + '_' + fieldId).find('input[name="input_' + inputId + '"], select[name="input_' + inputId + '"]');
2096
2097            if(input.prop('type') == 'checkbox' || input.prop('type') == 'radio') {
2098                jQuery(input).click(function(){
2099                    calcObj.bindCalcEvent(inputId, formulaField, formId, 0);
2100                });
2101                // Bind calc event to the image in an image choice field.
2102                var imageChoice = input.closest('.gfield--type-image_choice .gchoice');
2103                if ( imageChoice.length > 0 ) {
2104                    jQuery(imageChoice).click(function(){
2105                    	calcObj.bindCalcEvent(inputId, formulaField, formId, 0);
2106                    });
2107                }
2108            } else if(input.is('select') || input.prop('type') == 'hidden') {
2109                jQuery(input).change(function(){
2110                    calcObj.bindCalcEvent(inputId, formulaField, formId, 0);
2111                });
2112            } else {
2113                jQuery(input).keydown(function(){
2114                    calcObj.bindCalcEvent(inputId, formulaField, formId);
2115                }).change(function(){
2116                    calcObj.bindCalcEvent(inputId, formulaField, formId, 0);
2117                });
2118            }
2119
2120            // allow users to add custom methods for triggering calculations
2121            gform.doAction( 'gform_post_calculation_events', matches[i], formulaField, formId, calcObj );
2122
2123        }
2124
2125    }
2126
2127    this.bindCalcEvent = function(inputId, formulaField, formId, delay) {
2128
2129        var calcObj = this;
2130        var formulaFieldId = formulaField.field_id;
2131
2132        delay = delay == undefined ? 345 : delay;
2133
2134        if(calcObj.isCalculating[formulaFieldId][inputId])
2135            clearTimeout(calcObj.isCalculating[formulaFieldId][inputId]);
2136
2137        calcObj.isCalculating[formulaFieldId][inputId] = window.setTimeout(function() {
2138            calcObj.runCalc(formulaField, formId);
2139        }, delay);
2140
2141    }
2142
2143    this.replaceFieldTags = function( formId, expr, formulaField ) {
2144
2145        var matches = GFMergeTag.parseMergeTags( expr );
2146
2147        for(i in matches) {
2148
2149            if(! matches.hasOwnProperty(i))
2150                continue;
2151
2152            var inputId = matches[i][1];
2153            var fieldId = parseInt(inputId,10);
2154
2155            if ( fieldId == formulaField.field_id && fieldId == inputId ) {
2156            	continue;
2157            }
2158
2159            var modifier = 'value';
2160			if( matches[i][3] ){
2161				modifier = matches[i][3];
2162			}
2163			else {
2164				var is_product_radio =  jQuery('.gfield_price input[name=input_' + fieldId + ']').is('input[type=radio]');
2165                var is_product_dropdown = jQuery('.gfield_price select[name=input_' + fieldId + ']').length > 0;
2166                var is_option_checkbox = jQuery('.gfield_price input[name="input_' + inputId + '"]').is('input[type=checkbox]');
2167
2168                if( is_product_dropdown || is_product_radio || is_option_checkbox ) {
2169					modifier = 'price';
2170				}
2171			}
2172
2173			var isVisible = window['gf_check_field_rule'] ? gf_check_field_rule( formId, fieldId, true, '' ) == 'show' : true;
2174
2175			var value = isVisible ? GFMergeTag.getMergeTagValue( formId, inputId, modifier ) : 0;
2176
2177            // allow users to modify value with their own function
2178            value = gform.applyFilters( 'gform_merge_tag_value_pre_calculation', value, matches[i], isVisible, formulaField, formId );
2179
2180            value = this.cleanNumber( value, formId, fieldId, formulaField );
2181
2182            expr = expr.replace( matches[i][0], value );
2183        }
2184
2185        return expr;
2186    }
2187
2188	this.cleanNumber = function ( value, formId, fieldId, formulaField ) {
2189
2190		var numberFormat = gf_get_field_number_format( fieldId, formId );
2191
2192		if( ! numberFormat ) {
2193			numberFormat = gf_get_field_number_format(formulaField.field_id, formId);
2194		}
2195
2196		var decimalSeparator = gform.Currency.getDecimalSeparator(numberFormat);
2197
2198		value = gform.Currency.cleanNumber( value, '', '', decimalSeparator );
2199		if( ! value )
2200			value = 0;
2201
2202		return value;
2203	}
2204
2205    this.init(formId, formulaFields);
2206
2207
2208}
2209
2210function gformFormatNumber(number, rounding, decimalSeparator, thousandSeparator){
2211
2212    if(typeof decimalSeparator == "undefined"){
2213        if(window['gf_global']){
2214            var currency = new gform.Currency(gf_global.gf_currency_config);
2215            decimalSeparator = currency.currency["decimal_separator"];
2216        }
2217        else{
2218            decimalSeparator = ".";
2219        }
2220    }
2221
2222    if(typeof thousandSeparator == "undefined"){
2223        if(window['gf_global']){
2224            var currency = new gform.Currency(gf_global.gf_currency_config);
2225            thousandSeparator = currency.currency["thousand_separator"];
2226        }
2227        else{
2228            thousandSeparator = ",";
2229        }
2230    }
2231
2232    var currency = new gform.Currency();
2233    return currency.numberFormat(number, rounding, decimalSeparator, thousandSeparator, false)
2234}
2235
2236/**
2237 * @deprecated. Use GFMergeTags.parseMergeTag() instead
2238 * @remove-in 3.0
2239 */
2240function getMatchGroups(expr, patt) {
2241
2242	console.log('getMatchGroups() has been deprecated and will be removed in version 3.0. Use GFMergeTags.parseMergeTag() instead.');
2243
2244	var matches = new Array();
2245
2246    while(patt.test(expr)) {
2247
2248        var i = matches.length;
2249        matches[i] = patt.exec(expr)
2250        expr = expr.replace('' + matches[i][0], '');
2251
2252    }
2253
2254    return matches;
2255}
2256
2257function gf_get_field_number_format(fieldId, formId, context) {
2258
2259    var fieldNumberFormats = rgars(window, 'gf_global/number_formats/{0}/{1}'.gformFormat(formId, fieldId)),
2260        format = false;
2261
2262    if (fieldNumberFormats === '') {
2263        return format;
2264    }
2265
2266    if (typeof context == 'undefined') {
2267        format = fieldNumberFormats.price !== false ? fieldNumberFormats.price : fieldNumberFormats.value;
2268    } else {
2269        format = fieldNumberFormats[context];
2270    }
2271
2272    return format;
2273}
2274
2275//----------------------------------------
2276//------ reCAPTCHA FUNCTIONS -------------
2277//----------------------------------------
2278
2279gform.recaptcha = {
2280	/**
2281	 * Callback function on the reCAPTCAH API script.
2282	 *
2283	 * @see GF_Field_CAPTCHA::get_field_input() in /includes/fields/class-gf-field-catpcha.php
2284	 */
2285	renderRecaptcha: function() {
2286		jQuery( '.ginput_recaptcha:not(.gform-initialized)' ).each( function() {
2287			let $elem      = jQuery( this ),
2288				parameters = {
2289					'sitekey':        $elem.data( 'sitekey' ),
2290					'theme':          $elem.data( 'theme' ),
2291					'tabindex':       $elem.data( 'tabindex' ),
2292					'error-callback': () => {
2293						console.error( 'Gravity Forms: There was an error initializing reCAPTCHA v2. Please ensure your reCAPTCHA API keys are valid.' );
2294						$elem.attr( 'data-recaptcha-error', '1' );
2295					}
2296				};
2297
2298			if ( $elem.data( 'stoken' ) ) {
2299				parameters.stoken = $elem.data( 'stoken' );
2300			}
2301
2302			/**
2303			 * Allows a custom callback function to be executed when the user successfully submits the captcha.
2304			 *
2305			 * @since 2.4.x     The callback will be a function if reCAPTCHA v2 Invisible is used.
2306			 * @since 2.2.5.20
2307			 *
2308			 * @param string|false|object   The name of the callback function or the function object itself to be executed when the user successfully submits the captcha.
2309			 * @param object       $elem    The jQuery object containing the div element with the ginput_recaptcha class for the current reCaptcha field.
2310			 */
2311			const callback = gform.applyFilters( 'gform_recaptcha_callback', false, $elem );
2312			if ( callback ) {
2313				parameters.callback = callback;
2314			}
2315
2316			// Rendering recaptcha and saving the widget id as an attribute.
2317			const widgetId = grecaptcha.render( this.id, parameters );
2318			$elem[0].setAttribute( 'data-widget-id', widgetId );
2319
2320			if ( parameters.tabindex ) {
2321				$elem.find( 'iframe' ).attr( 'tabindex', parameters.tabindex );
2322			}
2323
2324			$elem.addClass( 'gform-initialized' );
2325
2326			gform.doAction( 'gform_post_recaptcha_render', $elem );
2327		} );
2328
2329		gform.recaptcha.bindRecaptchaSubmissionEvents();
2330	},
2331
2332	isSubmissionEventsInitialized: false,
2333	bindRecaptchaSubmissionEvents: function() {
2334		// If already initialized, abort.
2335		if ( gform.recaptcha.isSubmissionEventsInitialized ) {
2336			return;
2337		}
2338		// Setting initialized flag.
2339		gform.recaptcha.isSubmissionEventsInitialized = true;
2340
2341		// Subscribe to the pre_submission filter to execute invisible recaptcha when form is submitted.
2342		window.gform.utils.addAsyncFilter( 'gform/submission/pre_submission', async ( data ) => {
2343
2344			const requiresRecaptcha = data.submissionType === gform.submission.SUBMISSION_TYPE_SUBMIT || data.submissionType === gform.submission.SUBMISSION_TYPE_NEXT;
2345
2346			// Execute recaptcha if this is the right submission type and the submission hasn't been flagged to be aborted.
2347			if ( requiresRecaptcha && ! data.abort ) {
2348				await gform.recaptcha.maybeExecuteInvisibleRecaptcha( data );
2349			}
2350			return data;
2351		});
2352
2353		// Subscribe to the pre_ajax_validation filter to execute invisible recaptcha when form is validated via AJAX.
2354		window.gform.utils.addAsyncFilter( 'gform/ajax/pre_ajax_validation', gform.recaptcha.maybeExecuteInvisibleRecaptcha );
2355
2356		// Subscribe to the AJAX submission and validation events to save the recaptcha result.
2357		window.gform.utils.addFilter( 'gform/ajax/post_ajax_submission', gform.recaptcha.handleAjaxPostSubmission );
2358		window.gform.utils.addFilter( 'gform/ajax/post_ajax_validation', gform.recaptcha.handleAjaxPostValidation );
2359	},
2360
2361	/**
2362	 * @function maybeExecuteInvisibleRecaptcha
2363	 * @description Executes the invisible recaptcha and waits for the response.
2364
2365	 * @since 2.9.0
2366	 *
2367	 * @param {object} data Data passed by the pre submission filter.
2368	 * @returns {Promise<*>} Returns the pre submission data object unchanged.
2369	 */
2370	maybeExecuteInvisibleRecaptcha: async function( data ) {
2371
2372		if ( gform.recaptcha.gformIsRecaptchaPending( jQuery( data.form ) ) ) {
2373			const recaptcha = gform.utils.getNode( '.ginput_recaptcha', data.form, true );
2374
2375			await gform.recaptcha.executeRecaptcha( recaptcha.getAttribute( 'data-widget-id' ), data.form );
2376		}
2377		return data;
2378	},
2379
2380	/**
2381	 * @function executeRecaptcha
2382	 * @description Executes recaptcha and waits for the response by polling the .g-recaptcha-response field.
2383
2384	 * @since 2.9.0
2385	 *
2386	 * @param {string}          widgetId The recaptcha widgetId.
2387	 * @param {HTMLFormElement} form     The form being submitted
2388	 * @returns {Promise<string>} Returns the recaptcha response when it becomes available in the .g-recaptcha-response
2389	 */
2390	executeRecaptcha: async function( widgetId, form ) {
2391
2392		// If there was an error loading recaptcha, just abort and let the submission fail validation.
2393		const recaptcha = gform.utils.getNode( '.ginput_recaptcha', form, true );
2394		if ( recaptcha.getAttribute( 'data-recaptcha-error' ) === '1' ) {
2395			return;
2396		}
2397
2398		// Executes recaptcha.
2399		window.grecaptcha.execute( widgetId );
2400
2401		// Resolve promise when response is available.
2402		return new Promise(( resolve, reject ) => {
2403			const intervalId = setInterval(() => {
2404				const response = gform.utils.getNode( '.g-recaptcha-response', form, true );
2405
2406				if ( response && response.value ) {
2407					clearInterval( intervalId );
2408					resolve( response.value );
2409				}
2410			}, 100 );
2411		});
2412	},
2413
2414	/**
2415	 * @function handleAjaxPostValidation
2416	 * @description Saves the recaptcha response after an AJAX validation request.
2417
2418	 * @since 2.9.0
2419	 *
2420	 * @param {object} data Data passed by the ajax post validation filter.
2421	 * @returns {object}  Returns the data object unchanged.
2422	 */
2423	handleAjaxPostValidation: function( data ) {
2424		gform.recaptcha.saveRecaptchaResponse( data.validationResult.data.recaptcha_response, data.form );
2425		return data;
2426	},
2427
2428	/**
2429	 * @function handleAjaxPostSubmission
2430	 * @description Saves the recaptcha response after an AJAX submission request.
2431
2432	 * @since 2.9.0
2433	 *
2434	 * @param {object} data Data passed by the ajax post submission filter.
2435	 * @returns {object}  Returns the data object unchanged.
2436	 */
2437	handleAjaxPostSubmission: function( data ) {
2438		gform.recaptcha.saveRecaptchaResponse( data.submissionResult.data.recaptcha_response, data.form );
2439		return data;
2440	},
2441
2442	/**
2443	 * @function saveRecaptchaResponse
2444	 * @description Saves the specified recaptcha response in a hidden field.
2445
2446	 * @since 2.9.0
2447	 *
2448	 * @param {string} recaptchaResponse The recaptcha response to be saved.
2449	 * @param {form}   form              The form being submitted.
2450	 *
2451	 * @returns {void}
2452	 */
2453	saveRecaptchaResponse: function( recaptchaResponse, form ) {
2454
2455		if ( ! recaptchaResponse ) {
2456			return;
2457		}
2458
2459		let recaptchaInput = gform.tools.getNodes( 'input[name=g-recaptcha-response]', true, form, true );
2460		if ( recaptchaInput.length === 0 ) {
2461			recaptchaInput = document.createElement( 'input' );
2462			recaptchaInput.type = 'hidden';
2463			recaptchaInput.name = 'g-recaptcha-response';
2464			form.appendChild( recaptchaInput );
2465		} else {
2466			recaptchaInput = recaptchaInput[0];
2467		}
2468		recaptchaInput.value = recaptchaResponse;
2469	},
2470
2471	/**
2472	 * Helper function to determine whether a recaptcha is pending.
2473	 *
2474	 * @since 2.4.23
2475	 *
2476	 * @param {Object} form jQuery form object.
2477	 * @returns {boolean}
2478	 */
2479	gformIsRecaptchaPending: function( form ) {
2480		const recaptcha = form.find( '.ginput_recaptcha' );
2481
2482		if ( ! recaptcha.length || recaptcha.data( 'size' ) !== 'invisible' ) {
2483			return false;
2484		}
2485
2486		const recaptchaResponse = recaptcha.find( '.g-recaptcha-response' );
2487
2488		return !( recaptchaResponse.length && recaptchaResponse.val() );
2489	},
2490
2491	/**
2492	 * @function gform.recaptcha.needsRender
2493	 * @description Is there a non-rendered Recaptcha field on the page?
2494	 *
2495	 * @since 2.5.6
2496	 */
2497	needsRender: function() {
2498		return document.querySelectorAll( '.ginput_recaptcha:not(.gform-initialized)' )[ 0 ];
2499	},
2500
2501	/**
2502	 * @function gform.recaptcha.renderOnRecaptchaLoaded
2503	 * @description Render recaptcha fields once the library is available, only if non rendered elements are present.
2504	 *
2505	 * @since 2.5.6
2506	 */
2507	renderOnRecaptchaLoaded: function() {
2508		// if nothing to render, exit
2509		if ( ! gform.recaptcha.needsRender() ) {
2510			return;
2511		}
2512		var gfRecaptchaPoller = setInterval( function() {
2513			if ( ! window.grecaptcha || ! window.grecaptcha.render ) {
2514				return;
2515			}
2516			this.renderRecaptcha();
2517			clearInterval( gfRecaptchaPoller );
2518		}, 100 );
2519	}
2520};
2521
2522jQuery( document ).on( 'gform_post_render', gform.recaptcha.renderOnRecaptchaLoaded );
2523
2524window.renderRecaptcha = gform.recaptcha.renderRecaptcha;
2525window.gformIsRecaptchaPending = gform.recaptcha.gformIsRecaptchaPending;
2526
2527
2528//----------------------------------------
2529//----- SINGLE FILE UPLOAD FUNCTIONS -----
2530//----------------------------------------
2531
2532function gformValidateFileSize( field, max_file_size ) {
2533	var validation_element;
2534
2535	// Get validation message element.
2536	if ( jQuery( field ).closest( 'div' ).siblings( '.validation_message' ).length > 0 ) {
2537		validation_element = jQuery( field ).closest( 'div' ).siblings( '.validation_message' );
2538	} else {
2539		validation_element = jQuery( field ).siblings( '.validation_message' );
2540	}
2541
2542	// If file API is not supported within browser, return.
2543	if ( ! window.FileReader || ! window.File || ! window.FileList || ! window.Blob ) {
2544		return;
2545	}
2546
2547	// Get selected file.
2548	var file = field.files[0];
2549
2550	// If selected file is larger than maximum file size, set validation message and unset file selection.
2551	if ( file && file.size > max_file_size ) {
2552
2553		// Set validation message.
2554		validation_element.text(file.name + " - " + gform_gravityforms.strings.file_exceeds_limit);
2555		// Announce error.
2556		wp.a11y.speak( file.name + " - " + gform_gravityforms.strings.file_exceeds_limit );
2557
2558    } else {
2559
2560		// Reset validation message.
2561		validation_element.remove();
2562
2563	}
2564
2565}
2566
2567//----------------------------------------
2568//------ MULTIFILE UPLOAD FUNCTIONS ------
2569//----------------------------------------
2570
2571(function (gfMultiFileUploader, $) {
2572    gfMultiFileUploader.uploaders = {};
2573    var strings = typeof gform_gravityforms != 'undefined' ? gform_gravityforms.strings : {};
2574    var imagesUrl = typeof gform_gravityforms != 'undefined' ? gform_gravityforms.vars.images_url : "";
2575
2576	$(document).on('gform_post_render', function(e, formID){
2577		$( "form#gform_" + formID + " .gform_fileupload_multifile" ).each( function(){
2578			setup( this );
2579		} );
2580
2581		bindFileUploadSubmissionEvents();
2582	});
2583
2584	$(document).on("gform_post_conditional_logic", function(e,formID, fields, isInit){
2585		if(!isInit){
2586			$.each(gfMultiFileUploader.uploaders, function(i, uploader){
2587				uploader.refresh();
2588			});
2589		}
2590	});
2591
2592    $(document).ready(function () {
2593        if((typeof adminpage !== 'undefined' && adminpage === 'toplevel_page_gf_edit_forms')|| typeof plupload == 'undefined'){
2594            $(".gform_button_select_files").prop("disabled", true);
2595        } else if (typeof adminpage !== 'undefined' && adminpage.indexOf('_page_gf_entries') > -1) {
2596            $(".gform_fileupload_multifile").each(function(){
2597                setup(this);
2598            });
2599        }
2600    });
2601
2602    gfMultiFileUploader.setup = function (uploadElement){
2603        setup( uploadElement );
2604    };
2605
2606	let isInitialized = false;
2607
2608	/**
2609	 * Binds the file upload to the pre_submission event so that it can abort submission if there are pending files being uploaded.
2610	 *
2611	 * @since 2.9.0
2612	 */
2613	function bindFileUploadSubmissionEvents() {
2614
2615		// If already initialized, abort.
2616		if ( isInitialized ) {
2617			return;
2618		}
2619		isInitialized = true;
2620
2621		// Making sure there aren't any pending file uploads.
2622		window.gform.utils.addFilter( 'gform/submission/pre_submission', ( data ) => {
2623			if ( hasPendingUploads() ) {
2624				alert( strings.currently_uploading );
2625				data.abort = true;
2626			}
2627
2628			return data;
2629		}, 8);
2630	}
2631
2632	/**
2633	 * Check if there are any files currently in the process of being uploaded.
2634	 *
2635	 * @since 2.9.0
2636	 *
2637	 * @return {boolean} Returns true if there are files that haven't finished being uploaded yet. Returns false otherwise.
2638	 */
2639	function hasPendingUploads() {
2640		let pendingUploads = false;
2641		$.each( gfMultiFileUploader.uploaders, function( i, uploader ) {
2642			if( uploader.total.queued > 0 ) {
2643				pendingUploads = true;
2644				return false;
2645			}
2646		});
2647		return pendingUploads;
2648	}
2649
2650    function setup(uploadElement){
2651        var settings = $(uploadElement).data('settings');
2652
2653        var uploader = new plupload.Uploader(settings);
2654        formID = uploader.settings.multipart_params.form_id;
2655        gfMultiFileUploader.uploaders[settings.container] = uploader;
2656        var formID;
2657        var uniqueID;
2658
2659	    uploader.bind( 'Init', function( up, params ) {
2660		    if ( ! up.features.dragdrop ) {
2661			    $( ".gform_drop_instructions" ).hide();
2662		    }
2663
2664		    setFieldAccessibility( up.settings.container );
2665		    toggleLimitReached( up.settings );
2666	    } );
2667
2668	    gfMultiFileUploader.toggleDisabled = function (settings, disabled){
2669
2670            var button = typeof settings.browse_button == "string" ? $("#" + settings.browse_button) : $(settings.browse_button);
2671            button.prop("disabled", disabled);
2672        };
2673
2674	    /**
2675	     * @function setFieldAccessibility
2676	     * @description Patches accessibility issues with the plupload multi file container.
2677	     *
2678	     * @since 2.5.1
2679	     *
2680	     * @param {Node} container The generated plupload container.
2681	     */
2682
2683	    function setFieldAccessibility( container ) {
2684		    var input = container.querySelectorAll( 'input[type="file"]' )[ 0 ];
2685		    var button = container.querySelectorAll( '.gform_button_select_files' )[ 0 ];
2686		    var label = $( uploadElement ).closest( '.gfield' ).find( '.gfield_label' )[ 0 ];
2687		    if ( ! input || ! label || ! button ) {
2688			    return;
2689		    }
2690
2691		    label.setAttribute( 'for', input.id );
2692		    button.setAttribute( 'aria-label', button.innerText.toLowerCase() + ', ' + label.innerText.toLowerCase() );
2693		    input.setAttribute( 'tabindex', '-1' );
2694		    input.setAttribute( 'aria-hidden', 'true' );
2695	    }
2696
2697		function addMessage( messagesID, message) {
2698			$( "#" + messagesID ).prepend( "<li class='gfield_description gfield_validation_message'>" + htmlEncode( message ) + "</li>" );
2699			// Announce errors.
2700			setTimeout(function () {
2701				wp.a11y.speak( $( "#" + messagesID ).text() );
2702			}, 1000 );
2703		}
2704
2705	    function removeMessage(messagesID, message) {
2706		    $("#" + messagesID + " li:contains('" + message + "')").remove();
2707	    }
2708
2709	    function toggleLimitReached(settings) {
2710		    var limit = parseInt(settings.gf_vars.max_files, 10);
2711		    if (limit > 0) {
2712			    var totalCount = countFiles(settings.multipart_params.field_id),
2713				    limitReached = totalCount >= limit;
2714
2715			    gfMultiFileUploader.toggleDisabled(settings, limitReached);
2716			    if (!limitReached) {
2717				    removeMessage(settings.gf_vars.message_id, strings.max_reached);
2718			    }
2719		    }
2720	    }
2721
2722        uploader.init();
2723
2724		uploader.bind('BeforeUpload', function(up, file){
2725			up.settings.multipart_params.original_filename = file.name;
2726		});
2727
2728        uploader.bind('FilesAdded', function(up, files) {
2729            var max = parseInt(up.settings.gf_vars.max_files,10),
2730                fieldID = up.settings.multipart_params.field_id,
2731                totalCount = countFiles(fieldID),
2732                disallowed = up.settings.gf_vars.disallowed_extensions,
2733                extension;
2734
2735            if( max > 0 && totalCount >= max){
2736                $.each(files, function(i, file) {
2737                    up.removeFile(file);
2738                    return;
2739                });
2740                return;
2741            }
2742            $.each(files, function(i, file) {
2743
2744                extension = file.name.split('.').pop();
2745
2746                if($.inArray(extension, disallowed) > -1){
2747                    addMessage(up.settings.gf_vars.message_id, file.name + " - " + strings.illegal_extension);
2748                    up.removeFile(file);
2749                    return;
2750                }
2751
2752                if ((file.status == plupload.FAILED) || (max > 0 && totalCount >= max)){
2753                    up.removeFile(file);
2754                    return;
2755                }
2756
2757                var size         = typeof file.size !== 'undefined' ? plupload.formatSize(file.size) : strings.in_progress,
2758                    removeFileJs = '$this=jQuery(this); var uploader = gfMultiFileUploader.uploaders.' + up.settings.container.id + ';uploader.stop();uploader.removeFile(uploader.getFile(\'' + file.id +'\'));$this.after(\'' + strings.cancelled + '\'); uploader.start();$this.remove();',
2759                    statusMarkup = '<div id="{0}" class="ginput_preview"><span class="gfield_fileupload_filename">{1}</span><span class="gfield_fileupload_filesize">{2}</span><span class="gfield_fileupload_progress"><span class="gfield_fileupload_progressbar"><span class="gfield_fileupload_progressbar_progress"></span></span><span class="gfield_fileupload_percent"></span></span><a class="gfield_fileupload_cancel gform-theme-button gform-theme-button--simple" href="javascript:void(0)" title="{3}" onclick="{4}" onkeypress="{4}">{5}</a>';
2760
2761                /**
2762                 *  Filer the file upload markup as it is being uploaded.
2763                 *
2764                 *  @param {string}            statusMarkup Markup template used to render the status of the file being uploaded.
2765                 *  @param {plupload.File}     file         Instance of File being uploaded. See: https://www.plupload.com/docs/v2/File.
2766                 *  @param {int|string}        size         File size.
2767                 *  @param {object}            strings      Array of localized strings relating to the file upload UI.
2768                 *  @param {string}            removeFileJs JS used to remove the file when the "Cancel" link is click/pressed.
2769                 *  @param {plupload.Uploader} up           Instance of Uploader responsible for uploading current file. See: https://www.plupload.com/docs/v2/Uploader.
2770                 */
2771                statusMarkup = gform.applyFilters( 'gform_file_upload_status_markup', statusMarkup, file, size, strings, removeFileJs, up )
2772	                .gformFormat( file.id, htmlEncode( file.name ), size, strings.cancel_upload, removeFileJs, strings.cancel );
2773
2774                $( '#' + up.settings.filelist ).prepend( statusMarkup );
2775
2776                totalCount++;
2777
2778            });
2779
2780            up.refresh(); // Reposition Flash
2781
2782            var formElementID = "form#gform_" + formID;
2783            var uidElementID = "input:hidden[name='gform_unique_id']";
2784            var uidSelector = formElementID + " " + uidElementID;
2785            var $uid = $(uidSelector);
2786            if($uid.length==0){
2787                $uid = $(uidElementID);
2788            }
2789
2790            uniqueID = $uid.val();
2791            if('' === uniqueID){
2792                uniqueID = generateUniqueID();
2793                $uid.val(uniqueID);
2794            }
2795
2796
2797            if(max > 0 && totalCount >= max){
2798                gfMultiFileUploader.toggleDisabled(up.settings, true);
2799                addMessage(up.settings.gf_vars.message_id, strings.max_reached)
2800            }
2801
2802
2803            up.settings.multipart_params.gform_unique_id = uniqueID;
2804            up.start();
2805
2806        });
2807
2808        uploader.bind('UploadProgress', function(up, file) {
2809            var html = file.percent + "%";
2810            $('#' + file.id + ' span.gfield_fileupload_percent').html(html);
2811			$('#' + file.id + ' span.gfield_fileupload_progressbar_progress').css('width', file.percent + '%');
2812        });
2813
2814        uploader.bind('Error', function(up, err) {
2815            if(err.code === plupload.FILE_EXTENSION_ERROR){
2816                var extensions = typeof up.settings.filters.mime_types != 'undefined' ? up.settings.filters.mime_types[0].extensions /* plupoad 2 */ : up.settings.filters[0].extensions;
2817                addMessage(up.settings.gf_vars.message_id, err.file.name + " - " + strings.invalid_file_extension + " " + extensions);
2818            } else if (err.code === plupload.FILE_SIZE_ERROR) {
2819                addMessage(up.settings.gf_vars.message_id, err.file.name + " - " + strings.file_exceeds_limit);
2820            } else {
2821                const errorResponse = JSON.parse( err.response );
2822                const errorCode = errorResponse?.error?.code || err.code;
2823                const errorMessage = errorResponse?.error?.message || err.message;
2824                const filePart = err.file?.name ? `${ err.file.name } - ` : '';
2825                const m = `${ filePart }${ strings.error }: ${ errorCode }, ${ strings.message }: ${ errorMessage }`;
2826
2827                addMessage(up.settings.gf_vars.message_id, m);
2828            }
2829            $('#' + err.file.id ).html('');
2830            up.removeFile( err.file );
2831            up.refresh(); // Reposition Flash
2832        });
2833
2834		uploader.bind('ChunkUploaded', function(up, file, result) {
2835			var response = $.secureEvalJSON(result.response);
2836			if(response.status == "error"){
2837				up.removeFile(file);
2838				addMessage(up.settings.gf_vars.message_id, file.name + " - " + response.error.message);
2839				$('#' + file.id ).html('');
2840			} else {
2841				up.settings.multipart_params[file.target_name] = response.data;
2842			}
2843		});
2844
2845		uploader.bind('FileUploaded', function(up, file, result) {
2846			if (!up.getFile(file.id)) {
2847				// The file has been removed from the queue.
2848				return;
2849			}
2850
2851			var response = $.secureEvalJSON(result.response);
2852			if (response.status == "error") {
2853				addMessage(up.settings.gf_vars.message_id, file.name + " - " + response.error.message);
2854				$('#' + file.id).html('');
2855				toggleLimitReached(up.settings);
2856				return;
2857			}
2858
2859			var uploadedName = rgars(response, 'data/uploaded_filename');
2860			var html = '<span class="gfield_fileupload_filename">' + htmlEncode(uploadedName) + '</span><span class="gfield_fileupload_filesize">' + plupload.formatSize(file.size) + '</span>';
2861			html += '<span class="gfield_fileupload_progress gfield_fileupload_progress_complete"><span class="gfield_fileupload_progressbar"><span class="gfield_fileupload_progressbar_progress"></span></span><span class="gfield_fileupload_percent">' + file.percent + '%</span></span>';
2862			var formId = up.settings.multipart_params.form_id;
2863			var fieldId = up.settings.multipart_params.field_id;
2864
2865			if (typeof gf_legacy !== 'undefined' && gf_legacy.is_legacy) {
2866				html = "<img "
2867					+ "class='gform_delete' "
2868					+ "src='" + imagesUrl + "/delete.png' "
2869					+ "onclick='gformDeleteUploadedFile(" + formId + "," + fieldId + ", this);' "
2870					+ "onkeypress='gformDeleteUploadedFile(" + formId + "," + fieldId + ", this);' "
2871					+ "alt='" + strings.delete_file + "' "
2872					+ "title='" + strings.delete_file
2873					+ "' /> "
2874					+ html;
2875			} else {
2876				html = html + "<button class='gform_delete_file gform-theme-button gform-theme-button--simple' onclick='gformDeleteUploadedFile(" + formId + "," + fieldId + ", this);'><span class='dashicons dashicons-trash' aria-hidden='true'></span><span class='screen-reader-text'>" + strings.delete_file + ': ' + htmlEncode(uploadedName) + "</span></button>";
2877			}
2878
2879			/**
2880			 * Allows the markup for the file to be overridden.
2881			 *
2882			 * @since 1.9
2883			 * @since 2.4.23 Added the response param.
2884			 *
2885			 * @param {string} html      The HTML for the file name and delete button.
2886			 * @param {object} file      The file upload properties. See: https://www.plupload.com/docs/v2/File.
2887			 * @param {object} up        The uploader properties. See: https://www.plupload.com/docs/v2/Uploader.
2888			 * @param {object} strings   Localized strings relating to file uploads.
2889			 * @param {string} imagesURL The base URL to the Gravity Forms images directory.
2890			 * @param {object} response  The response from GFAsyncUpload.
2891			 */
2892			html = gform.applyFilters('gform_file_upload_markup', html, file, up, strings, imagesUrl, response);
2893
2894			$('#' + file.id).html(html);
2895			$('#' + file.id + ' span.gfield_fileupload_progressbar_progress').css('width', file.percent + '%');
2896
2897			if (file.percent == 100) {
2898				if (response.status && response.status == 'ok') {
2899					response.data.id = file.id;
2900					addFile(fieldId, response.data);
2901				} else {
2902					addMessage(up.settings.gf_vars.message_id, strings.unknown_error + ': ' + file.name);
2903				}
2904			}
2905
2906		});
2907
2908		uploader.bind('FilesRemoved', function (up, files) {
2909			toggleLimitReached(up.settings);
2910		});
2911
2912		function getAllFiles(){
2913			var selector = '#gform_uploaded_files_' + formID,
2914				$uploadedFiles = $(selector), files;
2915
2916			files = $uploadedFiles.val();
2917			files = (typeof files === "undefined") || files === '' ? {} : $.parseJSON(files);
2918
2919			return files;
2920		}
2921
2922        function getFiles(fieldID){
2923            var allFiles = getAllFiles();
2924            var inputName = getInputName(fieldID);
2925
2926            if(typeof allFiles[inputName] == 'undefined')
2927                allFiles[inputName] = [];
2928            return allFiles[inputName];
2929        }
2930
2931        function countFiles(fieldID){
2932            var files = getFiles(fieldID);
2933            return files.length;
2934        }
2935
2936        function addFile(fieldID, fileInfo){
2937
2938            var files = getFiles(fieldID);
2939
2940            files.unshift(fileInfo);
2941            setUploadedFiles(fieldID, files);
2942        }
2943
2944        function setUploadedFiles(fieldID, files){
2945            var allFiles = getAllFiles();
2946            var $uploadedFiles = $('#gform_uploaded_files_' + formID);
2947            var inputName = getInputName(fieldID);
2948            allFiles[inputName] = files;
2949            $uploadedFiles.val($.toJSON(allFiles));
2950        }
2951
2952        function getInputName(fieldID){
2953            return "input_" + fieldID;
2954        }
2955
2956        // fixes drag and drop in IE10
2957        $("#" + settings.drop_element).on({
2958            "dragenter": ignoreDrag,
2959            "dragover": ignoreDrag
2960        });
2961
2962        function ignoreDrag( e ) {
2963            e.preventDefault();
2964        }
2965    }
2966
2967
2968    function generateUniqueID() {
2969        return 'xxxxxxxx'.replace(/[xy]/g, function (c) {
2970            var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8;
2971            return v.toString(16);
2972        });
2973    }
2974
2975	function htmlEncode(value){
2976		return $('<div/>').text(value).html();
2977	}
2978
2979}(window.gfMultiFileUploader = window.gfMultiFileUploader || {}, jQuery));
2980
2981
2982//----------------------------------------
2983//------ GENERAL FUNCTIONS -------
2984//----------------------------------------
2985let gformIsSpinnerInitialized = false;
2986function gformInitSpinner(formId, spinnerUrl, isLegacy = true) {
2987
2988	// If already initialized, abort.
2989	if ( gformIsSpinnerInitialized ) {
2990		return;
2991	}
2992	gformIsSpinnerInitialized = true;
2993
2994	// Adding spinner on pre_submission.
2995	window.gform.utils.addFilter( 'gform/submission/pre_submission', ( data ) => {
2996
2997		gformShowSpinner( data.form.dataset.formid, spinnerUrl );
2998
2999		return data;
3000	}, 3 );
3001
3002	// Removing spinner if submission is aborted.
3003	document.addEventListener( 'gform/submission/submission_aborted', function( event ) {
3004
3005		// Removing new theme framework spinner.
3006		gformRemoveSpinner();
3007
3008		// Removing legacy spinner.
3009		jQuery( '#gform_ajax_spinner_' + event.detail.form.dataset.formid ).remove();
3010	} );
3011}
3012
3013/**
3014 * Shows the spinner.
3015 *
3016 * @since 2.9.0
3017 *
3018 * @param {int}    formId     The form id that is being submitted.
3019 * @param {string} spinnerUrl The image to use for the spinner.
3020 * @return {void}
3021 */
3022function gformShowSpinner( formId, spinnerUrl ) {
3023
3024	let filteredSpinner = gform.applyFilters('gform_spinner_url', spinnerUrl, formId);
3025	let defaultSpinner = gform.applyFilters('gform_spinner_url', gf_global.spinnerUrl, formId);
3026
3027	// Legacy spinner: this is not referring to Legacy Markup, but to the pre-2.7 spinner implementation.
3028	const isLegacy = filteredSpinner !== defaultSpinner;
3029	if ( isLegacy ) {
3030		gformAddSpinner( formId, filteredSpinner );
3031		return;
3032	}
3033
3034	let $spinnerTarget = gform.applyFilters('gform_spinner_target_elem', jQuery('#gform_submit_button_' + formId + ', #gform_wrapper_' + formId + ' .gform_next_button, #gform_send_resume_link_button_' + formId), formId);
3035
3036	gformInitializeSpinner( formId, $spinnerTarget );
3037}
3038/**
3039 * @description Initializes the theme-framework-based spinner after the provided target.
3040 *
3041 * @since 2.7
3042 *
3043 * @param {int}    formId The ID of the form within which to initialize the spinner.
3044 * @param {object} target The target element after which to inject the spinner.
3045 * @param {string} uniqId A unique ID to use for the spinner - used when removing the spinner.
3046 *
3047 * @return void
3048 */
3049function gformInitializeSpinner( formId, target, uniqId = 'gform-ajax-spinner' ) {
3050	if (jQuery('#gform_ajax_spinner_' + formId).length == 0) {
3051		var loaderHTML = '<span data-js-spinner-id="' + uniqId + '" id="gform_ajax_spinner_' + formId + '" class="gform-loader"></span>';
3052		var $spinnerTarget = target instanceof jQuery ? target : jQuery( target );
3053		$spinnerTarget.after( loaderHTML );
3054	}
3055}
3056
3057/**
3058 * @description Removes an existing theme-framework-based spinner.
3059 *
3060 * @since 2.7
3061 *
3062 * @param {string} uniqId A unique ID to use for the spinner - used when removing the spinner.
3063 *
3064 * @return void
3065 */
3066function gformRemoveSpinner( uniqId = 'gform-ajax-spinner' ) {
3067	var spinners = document.querySelectorAll( '[data-js-spinner-id="' + uniqId + '"]' );
3068
3069	if ( ! spinners ) {
3070		return;
3071	}
3072
3073	// Remove all instances of the spinner.
3074	spinners.forEach( function( spinner ) {
3075		spinner.remove();
3076	} );
3077}
3078
3079function gformAddSpinner(formId, spinnerUrl) {
3080
3081	if (typeof spinnerUrl == 'undefined' || !spinnerUrl) {
3082		spinnerUrl = gform.applyFilters('gform_spinner_url', gf_global.spinnerUrl, formId);
3083	}
3084
3085	if (jQuery('#gform_ajax_spinner_' + formId).length == 0) {
3086		/**
3087		 * Filter the element after which the AJAX spinner will be inserted.
3088		 *
3089		 * @since 2.0
3090		 *
3091		 * @param object $targetElem jQuery object containing all of the elements after which the AJAX spinner will be inserted.
3092		 * @param int    formId      ID of the current form.
3093		 */
3094		var $spinnerTarget = gform.applyFilters('gform_spinner_target_elem', jQuery('#gform_submit_button_' + formId + ', #gform_wrapper_' + formId + ' .gform_next_button, #gform_send_resume_link_button_' + formId), formId);
3095		$spinnerTarget.after('<img id="gform_ajax_spinner_' + formId + '"  class="gform_ajax_spinner" src="' + spinnerUrl + '" alt="" />');
3096	}
3097
3098}
3099
3100//----------------------------------------
3101//------ TINYMCE FUNCTIONS ---------------
3102//----------------------------------------
3103
3104/**
3105 * @function gformReInitTinymceInstance
3106 * @description Reinitializes a tinymce instance bound to a gform field if found.
3107 *
3108 * @since 2.5
3109 *
3110 * @param formId {int} Required. The form id.
3111 * @param fieldId {int} Required. The field id.
3112 */
3113
3114function gformReInitTinymceInstance( formId, fieldId ) {
3115    // check for required arguments
3116    if ( ! formId || ! fieldId ) {
3117        gform.console.error( 'gformReInitTinymceInstance requires a form and field id.' );
3118        return;
3119    }
3120    // make sure we have tinymce
3121    var tinymce = window.tinymce;
3122    if ( ! tinymce ) {
3123        gform.console.error( 'gformReInitTinymceInstance requires tinymce to be available.' );
3124        return;
3125    }
3126    // get the editor instance by form and field id and bail if not found
3127    var editor = tinymce.get( 'input_' + formId + '_' + fieldId );
3128    if ( ! editor ) {
3129        gform.console.error( 'gformReInitTinymceInstance did not find an instance for input_' + formId + '_' + fieldId + '.' );
3130        return;
3131    }
3132    // get the settings, destroy the instance and reinitialize
3133    var settings = jQuery.extend( {}, editor.settings );
3134    editor.remove();
3135    tinymce.init( settings );
3136    gform.console.log( 'gformReInitTinymceInstance reinitialized TinyMCE on input_' + formId + '_' + fieldId + '.' );
3137}
3138
3139//----------------------------------------
3140//------ EVENT FUNCTIONS -----------------
3141//----------------------------------------
3142
3143var __gf_keyup_timeout;
3144
3145jQuery( document ).on( 'change keyup', '.gfield input, .gfield select, .gfield textarea', function( event ) {
3146    gf_raw_input_change( event, this );
3147} );
3148
3149function gf_raw_input_change( event, elem ) {
3150
3151    // clear regardless of event type for maximum efficiency ;)
3152    clearTimeout( __gf_keyup_timeout );
3153
3154    var $input    = jQuery( elem ),
3155        htmlId    = $input.attr( 'id' ),
3156        fieldId   = gf_get_input_id_by_html_id( htmlId ),
3157        formId    = gf_get_form_id_by_html_id( htmlId ),
3158	    /**
3159	     * Filter the field meta generated by a raw input change.
3160	     *
3161	     * @since 2.4.1
3162	     *
3163	     * @param object fieldMeta An object containing the field ID and form ID of the triggering Gravity Forms field.
3164	     * @param object $input    The jQuery object for the triggering field element.
3165	     * @param object event     The raw JS event.
3166	     */
3167        fieldMeta = gform.applyFilters( 'gform_field_meta_raw_input_change', { fieldId: fieldId, formId: formId }, $input, event );
3168
3169    fieldId = fieldMeta.fieldId;
3170    formId = fieldMeta.formId;
3171
3172    if( ! fieldId ) {
3173        return;
3174    }
3175
3176    var isChangeElem = $input.is( ':checkbox' ) || $input.is( ':radio' ) || $input.is( 'select' ),
3177        isKeyupElem  = ! isChangeElem || $input.is( 'textarea' );
3178
3179    if( event.type == 'keyup' && ! isKeyupElem ) {
3180        return;
3181    } else if( event.type == 'change' && ! isChangeElem && ! isKeyupElem ) {
3182        return;
3183    }
3184
3185    if( event.type == 'keyup' ) {
3186        __gf_keyup_timeout = setTimeout( function() {
3187            gf_input_change( elem, formId, fieldId );
3188        }, 300 );
3189    } else {
3190        gf_input_change( elem, formId, fieldId );
3191    }
3192
3193}
3194
3195/**
3196 * Get the input id from a form element's HTML id.
3197 *
3198 * @param {string} htmlId The HTML id of a form element.
3199 *
3200 * @returns {string} inputId The input id.
3201 */
3202function gf_get_input_id_by_html_id( htmlId ) {
3203
3204    var ids = gf_get_ids_by_html_id( htmlId ),
3205        id  = ids[ ids.length - 1 ];
3206
3207    if ( ids.length == 3 ) {
3208        ids.shift();
3209        id = ids.join( '.' );
3210    }
3211
3212    return id;
3213}
3214
3215/**
3216 * Get the form id from a form element's HTML id.
3217 *
3218 * @param {string} htmlId The HTML id of a form element.
3219 *
3220 * @returns {string} formId The form id.
3221 */
3222function gf_get_form_id_by_html_id( htmlId ) {
3223    var ids = gf_get_ids_by_html_id( htmlId );
3224    return ids[0];
3225}
3226
3227/**
3228 * Get the form, field, and input id by a form elements HTML id.
3229 *
3230 * Note: Only multi-input fields will be return an input ID.
3231 *
3232 * @param {string} htmlId The HTML id of a form element.
3233 *
3234 * @returns {array} ids An array contain the form, field and input id.
3235 */
3236function gf_get_ids_by_html_id( htmlId ) {
3237    var ids = htmlId ? htmlId.split( '_' ) : [];
3238    for( var i = ids.length - 1; i >= 0; i-- ) {
3239        if ( ! gform.utils.isNumber( ids[ i ] ) ) {
3240            ids.splice( i, 1 );
3241        }
3242    }
3243    return ids;
3244}
3245
3246function gf_input_change( elem, formId, fieldId ) {
3247    gform.doAction( 'gform_input_change', elem, formId, fieldId );
3248}
3249
3250function gformExtractFieldId( inputId ) {
3251    var fieldId = parseInt( inputId.toString().split( '.' )[0],10 );
3252    return ! fieldId ? inputId : fieldId;
3253}
3254
3255function gformExtractInputIndex( inputId ) {
3256    var inputIndex = parseInt( inputId.toString().split( '.' )[1],10 );
3257    return ! inputIndex ? false : inputIndex;
3258}
3259
3260
3261
3262//----------------------------------------
3263//------ HELPER FUNCTIONS ----------------
3264//----------------------------------------
3265
3266if( ! window['rgars'] ) {
3267    function rgars( array, prop ) {
3268
3269        var props = prop.split( '/' ),
3270            value = array;
3271
3272        for( var i = 0; i < props.length; i++ ) {
3273            value = rgar( value, props[ i ] );
3274        }
3275
3276        return value;
3277    }
3278}
3279
3280if( ! window['rgar'] ) {
3281    function rgar( array, prop ) {
3282        if ( typeof array[ prop ] != 'undefined' ) {
3283            return array[ prop ];
3284        }
3285        return '';
3286    }
3287}
3288
3289if ( ! String.prototype.gformFormat ) {
3290	String.prototype.gformFormat = function() {
3291		var args = arguments;
3292		return this.replace( /{(\d+)}/g, function( match, number ) {
3293			return typeof args[ number ] != 'undefined' ? args[ number ] : match;
3294		} );
3295	};
3296}
3297
3298
3299/**
3300 * Toggle the dropdown submenus in the form editor menu bar.
3301 *
3302 * @since 2.5
3303 */
3304jQuery( document ).ready( function() {
3305	jQuery( '#gform-form-toolbar__menu' )
3306	.on( 'mouseenter focus', '> li',function() {
3307			jQuery( this ).find( '.gform-form-toolbar__submenu' ).toggleClass( 'open' );
3308			jQuery( this ).find( '.has_submenu' ).toggleClass( 'submenu-open' );
3309		} );
3310	jQuery( '#gform-form-toolbar__menu' )
3311		.on( 'mouseleave blur', '> li',function() {
3312			jQuery( '.gform-form-toolbar__submenu.open' ).removeClass( 'open' );
3313			jQuery( '.has_submenu.submenu-open' ).removeClass( 'submenu-open' );
3314		} );
3315	jQuery( '#gform-form-toolbar__menu .has_submenu' )
3316		.on( 'click', function( e ) {
3317			e.preventDefault();
3318		} );
3319} );
3320
3321/**
3322 * Add a containing class to fields with multiple inputs that we want to display inline.
3323 *
3324 * @since 2.5
3325 */
3326jQuery( document ).ready( function() {
3327	var settingsFields = jQuery( '.gform-settings-field' );
3328	settingsFields.each( function() {
3329		if ( jQuery( this ).find( '> .gform-settings-input__container' ).length > 1 ) {
3330			jQuery( this ).addClass( 'gform-settings-field--multiple-inputs' );
3331		}
3332	} );
3333} );
3334
3335jQuery( function() {
3336	gform.tools.trigger( 'gform_main_scripts_loaded' );
3337} );

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.