(function($) {

	'use strict';

	// Set is_admin
	$.WS_Form.prototype.set_is_admin = function() { return false; }

	// One time init for admin page
	$.WS_Form.prototype.init = function() {

		// Build data cache
		this.data_cache_build();

		// Set global variables once for performance
		this.set_globals();
	}

	// Continue initialization after submit data retrieved
	$.WS_Form.prototype.init_after_get_submit = function(submit_retrieved) {

		// Debug
		if($.WS_Form.debug_rendered) {

			// Form data source debug
			if(this.submit_auto_populate !== false) { this.log('debug_action_get', this.submit_auto_populate.action_label); }
			if(this.submit !== false) { this.log('debug_submit_loaded'); }
		}

		// Build form
		this.form_build();
	}

	// Set global variables once for performance
	$.WS_Form.prototype.set_globals = function() {

		// Get framework ID
		this.framework_id = $.WS_Form.settings_plugin.framework;

		// Get framework settings
		this.framework = $.WS_Form.frameworks.types[this.framework_id];

		// Update debug interface
		if($.WS_Form.debug_rendered) {

			this.debug_info('debug_info_framework', this.framework.name);
		}

		// Get current framework
		this.framework_fields = this.framework.fields.public;

		// Custom action URL
		if(typeof this.form_obj.attr('action') !== 'undefined') {

			var action_form = this.form_obj.attr('action');

			// Search string for permalinks configured as rewrites
			var action_ajax = ws_form_settings.url_ajax_namespace + '/submit';

			// Search string for permalinks configured to use query string
			var action_rest_route = 'rest_route=' + encodeURIComponent('/' + ws_form_settings.url_ajax_namespace + '/submit');

			// Check if action is custom
			this.form_action_custom = (action_form.indexOf(action_ajax) === -1) && (action_form.indexOf(action_rest_route) === -1);

		} else {

			this.form_action_custom = true;
		}

		// Get validated class
		var class_validated_array = (typeof this.framework.fields.public.class_form_validated !== 'undefined') ? this.framework.fields.public.class_form_validated : [];
		this.class_validated = class_validated_array.join(' ');
		this.selector_validated = '.' + class_validated_array.join(',.');

		// Hash
		if(
			ws_form_settings.wsf_hash &&
			(typeof ws_form_settings.wsf_hash === 'object')
		) {

			// Set hash from query string
			for(var hash_index in ws_form_settings.wsf_hash) {

				if(!ws_form_settings.wsf_hash.hasOwnProperty(hash_index)) { continue; }

				var wsf_hash = ws_form_settings.wsf_hash[hash_index];

				if(
					(typeof wsf_hash.id !== 'undefined') &&
					(typeof wsf_hash.hash !== 'undefined') &&
					(typeof wsf_hash.token !== 'undefined') &&
					(Number(wsf_hash.id) === Number(this.form_id))
				) {

					this.hash_set(wsf_hash.hash, wsf_hash.token, true);
				}
			}

		} else {

			// Set hash from cookie
			this.hash_set(this.cookie_get('hash', ''), false, true);
		}

		// Visual editor?
		this.visual_editor = (typeof this.form_canvas_obj.attr('data-visual-builder') !== 'undefined');

		// Read submission data if hash is defined
		var ws_this = this;
		if(this.hash) {

			var url = 'submit/hash/' + this.hash + '/';
			if(this.token) { url += this.token + '/'; }

			// Call AJAX request
			ws_this.api_call(url, 'GET', false, function(response) {

				if(typeof response.data !== 'undefined') {

					// Save the submissions data
					ws_this.submit = response.data;
				}

				// Initialize after getting submit
				ws_this.init_after_get_submit(true);

				// Finished with submit data
				ws_this.submit = false;

			}, function(response) {

				// Read auto populate data instead
				ws_this.read_json_populate();

				// Initialize after getting submit
				ws_this.init_after_get_submit(false);
			});

		} else {

			// Read auto populate data
			this.read_json_populate();

			// Initialize after getting submit
			this.init_after_get_submit(false);
		}
	}

	// Read auto populate data
	$.WS_Form.prototype.read_json_populate = function() {

		if(typeof wsf_form_json_populate !== 'undefined') {

			if(typeof wsf_form_json_populate[this.form_id] !== 'undefined') {

				this.submit_auto_populate = wsf_form_json_populate[this.form_id];
			}
		}
	}

	// Render a log message
	$.WS_Form.prototype.log = function(language_id, variable, log_class) {

		if(typeof variable === 'undefined') { variable = ''; }
		if(typeof log_class === 'undefined') { log_class = ''; }

		return ($.WS_Form.debug_rendered) ? this.debug_audit_add('log', this.language(language_id, variable, false).replace(/%s/g, variable), log_class) : false;
	}

	// Render an error message
	$.WS_Form.prototype.error = function(language_id, variable, error_class) {

		if(typeof variable === 'undefined') { variable = ''; }
		if(typeof error_class === 'undefined') { error_class = ''; }

		// Build error message
		var error_message = this.language(language_id, variable, false).replace(/%s/g, variable);

		if($.WS_Form.debug_rendered) {

			this.debug_audit_add('error', error_message, error_class);
		}

		return error_message;
	}

	// Render any interface elements that rely on the form object
	$.WS_Form.prototype.form_render = function() {

		// Form timer
		this.form_timer();

		// Calculation log
		if(typeof this.form_calc_log === 'function') { this.form_calc_log(); }

		// Get conditional actions
		if(
			(typeof this.form.meta.action !== 'undefined')
		) {

			for(var key in this.form.meta.action) {

				if(!this.form.meta.action.hasOwnProperty(key)) { continue; }

				// Get action
				var action = this.form.meta.action[key];

				// Get action ID
				var action_id = action.id.toString();

				// Actions - All
				this.conditional_actions_run_action.push(action_id);

				// Actions - Save
				if(action.save) {

					this.conditional_actions_run_save.push(action_id);
				}

				// Actions - Submit
				if(action.submit) {

					this.conditional_actions_run_submit.push(action_id);
				}

				// Google analytics
				if(action.conversion_type === 'google') {

					this.action_ga = true;
				}
			}
		}

		// Reset arrays
		this.conditional_actions_changed = false;
		this.signatures = [];
		this.signatures_by_name = [];
		// Captchas
		this.recaptchas = [];
		this.recaptchas_v2_default = [];
		this.recaptchas_v2_invisible = [];
		this.recaptchas_v3_default = [];
		this.recaptchas_conditions = [];
		this.hcaptchas = [];
		this.hcaptchas_default = [];
		this.hcaptchas_invisible = [];
		this.hcpatchas_conditions = [];
		this.turnstiles = [];
		this.turnstiles_default = [];
		this.turnstiles_conditions = [];
		this.captchafoxes = [];
		this.captchafoxes_default = [];
		this.captchafoxes_invisible = [];
		this.captchafoxes_conditions = [];

		// Set style ID if not set (Used by site builders that don't have form object available when rendering)
		if(this.form.meta.style_id && typeof this.form_obj.attr('data-wsf-style-id') === 'undefined') {

			this.form_obj.attr('data-wsf-style-id', this.form.meta.style_id);
		}

		// Initialize framework
		this.form_framework();

		// Form preview
		this.form_preview();

		// Groups - Tabs - Initialize
		if(typeof this.form_tab === 'function') { this.form_tab(); }

		// Repeatable sections
		if(typeof this.form_section_repeatable === 'function') { this.form_section_repeatable(); }

		// Navigation
		this.form_navigation();

		// Analytics
		if(typeof this.form_analytics === 'function') { this.form_analytics(); }

		// Client side form validation
		this.form_validation();

		// Select
		if(typeof this.form_select === 'function') { this.form_select(); }

		// Select min max
		if(typeof this.form_select_min_max === 'function') { this.form_select_min_max(); }

		// Select2
		if(typeof this.form_select2 === 'function') { this.form_select2(); }

		// Dedupe value scope
		if(typeof this.form_dedupe_value_scope === 'function') { this.form_dedupe_value_scope(); }

		// Checkbox min max
		if(typeof this.form_checkbox_min_max === 'function') { this.form_checkbox_min_max(); }

		// Checkbox select all
		if(typeof this.form_checkbox_select_all === 'function') { this.form_checkbox_select_all(); }

		// Radio validation
		if(typeof this.form_radio_validation === 'function') { this.form_radio_validation(); }

		// Text input and textarea character and word count
		this.form_character_word_count();

		// Telephone
		if(typeof this.form_tel === 'function') { this.form_tel(); }

		// Dates
		if(typeof this.form_date === 'function') { this.form_date(); }

		// Colors
		if(typeof this.form_color === 'function') { this.form_color(); }

		// Signatures
		if(typeof this.form_signature === 'function') { this.form_signature(); }

		// SSNs
		if(typeof this.form_ssn === 'function') { this.form_ssn(); }

		// Honeypot
		this.form_honeypot();

		// reCAPTCHA
		if(typeof this.form_recaptcha === 'function') { this.form_recaptcha(); }

		// hCAPTCHA
		if(typeof this.form_hcaptcha === 'function') { this.form_hcaptcha(); }

		// Turnstile
		if(typeof this.form_turnstile === 'function') { this.form_turnstile(); }

		// CaptchaFox
		if(typeof this.form_captchafox === 'function') { this.form_captchafox(); }

		// Email
		if(typeof this.form_email === 'function') { this.form_email(); }

		// Label
		this.form_label();

		// CSS vars
		this.form_css_var();
		// Legal
		if(typeof this.form_legal === 'function') { this.form_legal(); }

		// Rating
		if(typeof this.form_rating === 'function') { this.form_rating(); }

		// Google Map
		if(typeof this.form_google_map === 'function') { this.form_google_map(); }

		// Google Address
		if(typeof this.form_google_address === 'function') { this.form_google_address(); }

		// Google Route
		if(typeof this.form_google_route === 'function') { this.form_google_route(); }

		// E-Commerce
		if(typeof this.form_ecommerce === 'function') { this.form_ecommerce(); }

		// File inputs
		if(typeof this.form_file === 'function') { this.form_file(); }

		// Media capture
		if(typeof this.form_media_capture === 'function') { this.form_media_capture(); }

		// Client side form conditions (Initial run = true)
		if(typeof this.form_conditional === 'function') { this.form_conditional(true); }

		// Conversational
		if(typeof this.form_conversational === 'function') { this.form_conversational(); }

		// Progress
		if(typeof this.form_progress === 'function') { this.form_progress(); }

		// Range sliders
		this.form_help_value();

		// Text areas
		if(typeof this.form_textarea === 'function') { this.form_textarea(); }

		// Consent
		if(typeof this.form_consent === 'function') { this.form_consent(); }

		// Required
		this.form_required();

		// Input masks
		this.form_inputmask();

		// Tracking
		if(typeof this.form_tracking === 'function') { this.form_tracking(); }

		// Password visibility toggle
		if(typeof this.form_password_visibility_toggle === 'function') { this.form_password_visibility_toggle(); }

		// Password generate
		if(typeof this.form_password_generate === 'function') { this.form_password_generate(); }

		// Password strength meter
		if(typeof this.form_password_strength_meter === 'function') { this.form_password_strength_meter(); }

		// Credit Card
		this.form_credit_card();
		// Transform
		this.form_transform();

		// Bypass
		this.form_bypass_enabled = true;
		this.form_bypass(false);

		// Form validation - Real time
		this.form_validate_real_time();

		// Form validation - Field
		if(typeof this.form_validate_field === 'function') { this.form_validate_field('init'); }

		// Calculations
		if(typeof this.form_calc === 'function') { this.form_calc(); }

		// Form stats
		this.form_stat();

		// Select Cascading
		if(typeof this.form_cascade === 'function') { this.form_cascade(); }

		// Geo
		if(typeof this.form_geo === 'function') { this.form_geo(); }
		// Tab validation
		if(typeof this.form_tab_validation === 'function') { this.form_tab_validation(); }

		// Accessibility
		this.form_accessibility();

		// Section repeatable hidden field (This triggers repeatable section events also)
		if(typeof this.section_repeatable_hidden_field === 'function') { this.section_repeatable_hidden_field(); }

		// Log
		this.log('debug_form_rendered');

		// Loader
		if(typeof this.form_loader_hide === 'function') {

			this.form_loader_hide(false);
		}

		// Trigger rendered event
		this.trigger('rendered');

		// Set data-wsf-rendered attribute
		this.form_obj.attr('data-wsf-rendered', '');

		// Styler (Runs here to ensure this.conversational is set)
		if(typeof this.styler === 'function') { this.styler(); }

		// Styler scheme (Runs here to ensure this.conversational is set)
		if(typeof this.styler_scheme === 'function') { this.styler_scheme(!this.visual_editor, true); }
	}

	// Form timer
	$.WS_Form.prototype.form_timer = function() {

		// Check for date start cookie
		this.date_start = this.cookie_get('date_start', false);

		if((this.date_start === false) || isNaN(this.date_start) || (this.date_start === '')) {

			this.date_start = new Date().getTime();

			// Set cookie if duration track enabled
			if(this.get_object_meta_value(this.form, 'tracking_duration', '') === 'on') {

				this.cookie_set('date_start', this.date_start, false);
			}
		}
	}

	// Form timer - Reset
	$.WS_Form.prototype.form_timer_reset = function() {

		this.date_start = false;

		if(this.get_object_meta_value(this.form, 'tracking_duration', '') === 'on') {

			this.cookie_set('date_start', '', false);
		}
	}

	// Trigger events
	$.WS_Form.prototype.trigger = function(slug) {

		// New method
		var action_type = 'wsf-' + slug;
		$(document).trigger(action_type, [this.form, this.form_id, this.form_instance_id, this.form_obj, this.form_canvas_obj, this.group_index]);
		this.log('log_trigger', action_type, 'event');

		// Legacy method - Instance
		var trigger_instance = 'wsf-' + slug + '-instance-' + this.form_instance_id;
		$(window).trigger(trigger_instance);

		// Legacy method - Form
		var trigger_form = 'wsf-' + slug + '-form-' + this.form_id;
		$(window).trigger(trigger_form);
	}

	// Initialize JS
	$.WS_Form.prototype.form_framework = function() {

		// Add framework form attributes
		if(
			(typeof this.framework.form.public !== 'undefined') &&
			(typeof this.framework.form.public.attributes === 'object')
		) {

			for(var attribute in this.framework.form.public.attributes) {

				var attribute_value = this.framework.form.public.attributes[attribute];

				this.form_obj.attr(attribute, attribute_value);
			}
		}

		// Check framework init_js
		if(typeof this.framework.init_js !== 'undefined') {

			// Framework init JS values
			var framework_init_js_values = {'form_canvas_selector': '#' + this.form_obj_id};
			var framework_init_js = this.mask_parse(this.framework.init_js, framework_init_js_values);

			try {

				$.globalEval("(function($) {\n" + framework_init_js + "\n})(jQuery);");

			} catch(e) {

				this.error('error_js', framework_init_js);
			}
		}
	}

	// Form - Reset
	$.WS_Form.prototype.form_reset = function(e) {

		var ws_this = this;

		// Trigger
		this.trigger('reset-before');

		// Unmark as validated
		this.form_canvas_obj.removeClass(this.class_validated);

		// HTML form reset
		this.form_obj[0].reset();
		// Reset special fields
		for(var key in this.field_data_cache) {

			if(!this.field_data_cache.hasOwnProperty(key)) { continue; }

			var field = this.field_data_cache[key];

			var field_id = field.id;
			var field_name = this.field_name_prefix + field_id;

			var field_type_config = $.WS_Form.field_type_cache[field.type];
			var trigger = (typeof field_type_config.trigger !== 'undefined') ? field_type_config.trigger : 'change';

			var field_obj = $('[name="' + this.esc_selector(field_name) + '"], [name^="' + this.esc_selector(field_name) + '["]', this.form_canvas_obj);

			switch(field.type) {

				case 'textarea' :
					
					field_obj.each(function() {

						if(typeof ws_this.textarea_set_value === 'function') { ws_this.textarea_set_value($(this), $(this).val()); }

						$(this).trigger(trigger);
					});

					break;

				case 'color' :

					field_obj.each(function() {

						$(this).trigger(trigger);

						if(typeof Coloris !== 'undefined') {

							$(this)[0].dispatchEvent(new Event('input', { bubbles: true }));
						}
					});

					break;

				case 'file' :

					// Regular file uploads
					field_obj.each(function() {

						$(this).trigger(trigger);
					});

					// Dropzone file uploads
					if(
						(typeof ws_this.form_file_dropzonejs_populate === 'function') &&
						(typeof Dropzone !== 'undefined')
					) {

						$('[name="' + this.esc_selector(field_name) + '"][data-file-type="dropzonejs"], [name^="' + this.esc_selector(field_name) + '["][data-file-type="dropzonejs"]', this.form_canvas_obj).each(function() {

							ws_this.form_file_dropzonejs_populate($(this), false);
						});
					}

					break;

				default :

					field_obj.each(function() {

						$(this).trigger(trigger);
					});
			}
		}

		// Clear signatures
		if(typeof this.signatures_clear === 'function') { this.signatures_clear(); }
		// Reset reCAPTCHA
		if(typeof this.recaptcha_reset === 'function') { this.recaptcha_reset(); }

		// Reset hCaptcha
		if(typeof this.hcaptcha_reset === 'function') { this.hcaptcha_reset(); }

		// Reset Turnstile
		if(typeof this.turnstile_reset === 'function') { this.turnstile_reset(); }

		// Reset CaptchaFox
		if(typeof this.captchafox_reset === 'function') { this.captchafox_reset(); }
		// Form conditional=
		if(typeof this.form_conditional === 'function') { this.form_conditional(); }
		// Trigger
		this.trigger('reset-complete');
	}

	// Form - Clear
	$.WS_Form.prototype.form_clear = function() {

		var ws_this = this;

		// Trigger
		this.trigger('clear-before');

		// Unmark as validated
		this.form_canvas_obj.removeClass(this.class_validated);

		// Clear fields
		for(var key in this.field_data_cache) {

			if(!this.field_data_cache.hasOwnProperty(key)) { continue; }

			var field = this.field_data_cache[key];

			var field_id = field.id;
			var field_name = this.field_name_prefix + field_id;

			var field_type_config = $.WS_Form.field_type_cache[field.type];
			var trigger = (typeof field_type_config.trigger !== 'undefined') ? field_type_config.trigger : 'change';

			var field_obj = $('[name="' + this.esc_selector(field_name) + '"], [name^="' + this.esc_selector(field_name) + '["]', this.form_canvas_obj);

			// Clear value
			switch(field.type) {

				case 'checkbox' :
				case 'price_checkbox' :
				case 'radio' :
				case 'price_radio' :

					field_obj.each(function() {

						if($(this).is(':checked')) {
	
							$(this).prop('checked', false).trigger(trigger);
						}
					});

					break;

				case 'select' :
				case 'price_select' :

					$('[name="' + this.esc_selector(field_name) + '"], [name^="' + this.esc_selector(field_name) + '["] option', this.form_canvas_obj).each(function() {

						if($(this).is(':selected')) {
	
							$(this).prop('selected', false);
							$(this).closest('select').trigger(trigger);
						}
					});

					break;

				case 'textarea' :

					field_obj.each(function() {

						if($(this).val() !== '') {

							$(this).val('').trigger(trigger);

							if(typeof ws_this.textarea_set_value === 'function') { ws_this.textarea_set_value($(this), ''); }
						}
					});

					break;

				case 'color' :

					field_obj.each(function() {

						if($(this).val() !== '') {

							$(this).val('').trigger(trigger);

							if(typeof Coloris !== 'undefined') {

								$(this)[0].dispatchEvent(new Event('input', { bubbles: true }));
							}
						}
					});

					break;

				case 'file' :

					// Regular file uploads
					field_obj.each(function() {

						if($(this).val() !== '') {

							$(this).val('').trigger(trigger);
						}
					});

					// Dropzone file uploads
					if(
						(typeof ws_this.form_file_dropzonejs_populate === 'function') &&
						(typeof Dropzone !== 'undefined')
					) {

						$('[name="' + this.esc_selector(field_name) + '"][data-file-type="dropzonejs"], [name^="' + this.esc_selector(field_name) + '["][data-file-type="dropzonejs"]', this.form_canvas_obj).each(function() {

							ws_this.form_file_dropzonejs_populate($(this), true);
						});
					}

					break;

				default:

					field_obj.each(function() {

						if($(this).val() !== '') {

							$(this).val('').trigger(trigger);
						}
					});
			}
		}

		// Clear signatures
		if(typeof this.signatures_clear === 'function') { this.signatures_clear(); }
		// Reset reCAPTCHA
		if(typeof this.recaptcha_reset === 'function') { this.recaptcha_reset(); }

		// Reset hCaptcha
		if(typeof this.hcaptcha_reset === 'function') { this.hcaptcha_reset(); }

		// Reset Turnstile
		if(typeof this.turnstile_reset === 'function') { this.turnstile_reset(); }

		// Reset CaptchaFox
		if(typeof this.captchafox_reset === 'function') { this.captchafox_reset(); }
		// Form conditional
		if(typeof this.form_conditional === 'function') { this.form_conditional(); }
		// Trigger
		this.trigger('clear-complete');
	}

	// Form reload
	$.WS_Form.prototype.form_reload = function() {

		// Clear events
		for(var form_events_reset_index in this.form_events_reset) {

			if(!this.form_events_reset.hasOwnProperty(form_events_reset_index)) { continue; }

			var form_event = this.form_events_reset[form_events_reset_index];

			form_event.obj.off(form_event.event);
		}
		this.form_obj.off();
		this.form_canvas_obj.off();
		this.form_events_reset = [];

		// Clear calcs
		this.calc = [];

		// Read submission data if hash is defined
		var ws_this = this;
		if(this.hash !== '') {

			var hash_url = 'submit/hash/' + this.hash + '/';
			if(this.token) { hash_url += this.token + '/'; }

			// Call AJAX request
			ws_this.api_call(hash_url, 'GET', false, function(response) {

				// Save the submissions data
				ws_this.submit = response.data;

				ws_this.form_reload_after_get_submit(true);

				// Finished with submit data
				ws_this.submit = false;

			}, function(response) {

				ws_this.form_reload_after_get_submit(false);
			});

		} else {

			// Reset submit
			this.submit = false;

			this.form_reload_after_get_submit(false);
		}
	}

	// Form reload - After get submit
	$.WS_Form.prototype.form_reload_after_get_submit = function(submit_retrieved) {

		// Clear any messages
		$('[data-wsf-message][data-wsf-instance-id="' + this.form_instance_id + '"]').remove();

		// Show the form
		this.form_canvas_obj.show();

		// Reset form tag
		this.form_canvas_obj.removeClass(this.class_validated)

		// Clear ecommerce real time validation hooks
		this.form_validation_real_time_hooks = [];

		// Empty form object
		this.form_canvas_obj.empty();

		// Initialize loader
		if(typeof this.form_loader === 'function') { this.form_loader(); }

		// Show loader
		if(typeof this.form_loader_show === 'function') { this.form_loader_show('render'); }

		// Build form
		this.form_build();
	}

	// Form - Hash reset
	$.WS_Form.prototype.form_hash_clear = function() {

		// Clear hash variable
		this.hash = '';

		// Clear hash cookie
		this.cookie_clear('hash');

		// Update debug interface
		if($.WS_Form.debug_rendered) {

			this.debug_info('debug_info_hash', this.language('debug_hash_empty'));
			this.debug_info('debug_info_submit_count', 0);
		}

		// Log
		this.log('log_hash_clear');
	}

	// Form - Transform
	$.WS_Form.prototype.form_transform = function() {

		var ws_this = this;

		$('[data-wsf-transform]:not([data-wsf-transform-init])', this.form_canvas_obj).each(function() {

			// Mark so it is not initialized again
			$(this).attr('data-wsf-transform-init', '');

			// Get transform method
			var transform_method = $(this).attr('data-wsf-transform');

			// Event handler
			$(this).on('change input', function() {

				ws_this.form_transform_process($(this), transform_method);
			})

			// Initial transform
			ws_this.form_transform_process($(this), transform_method);
		});
	}

	// Form - Transform - Process uppercase
	$.WS_Form.prototype.form_transform_process = function(obj, transform_method) {

		var input_value = obj.val();

		switch(obj.attr('type')) {

			// Does not support setSelectionRange
			case 'email' :

				var set_selection_range = false;
				break;

			default :

				var set_selection_range = true;
		}

		if(input_value && (typeof input_value === 'string')) {

			// Remember cursor position
			if(set_selection_range) {

				var pos_start = obj[0].selectionStart;
				var pos_end = obj[0].selectionEnd;
			}

			// Set value
			switch(transform_method) {

				case 'uc' :

					obj.val(input_value.toUpperCase());
					break;

				case 'lc' :

					obj.val(input_value.toLowerCase());
					break;

				case 'capitalize' :

					obj.val(this.ucwords(input_value.toLowerCase()));
					break;

				case 'sentence' :

					obj.val(this.ucfirst(input_value.toLowerCase()));
					break;
			}

			// Recover cursor position
			if(set_selection_range) {

				obj[0].setSelectionRange(pos_start, pos_end);
			}
		}
	}

	// Form credit card
	$.WS_Form.prototype.form_credit_card = function() {

		var ws_this = this;

		$('input[data-credit-card]:not([data-init-credit-card])', this.form_canvas_obj).each(function() {

			// Set attribute so it doesn't initialize again
			$(this).attr('data-init-credit-card', '');

			// Set event
			$(this).on("input", function() {

				if(typeof $(this).inputmask === 'undefined') { return; }

				var value = $(this).val().replace(/\D/g, '');
				var mask;

				if ((/^3[47]\d{0,13}$/).test(value)) {

					// American Express
					mask = '9999 999999 99999'

				} else if ((/^3(?:0[0-5]|[68]\d)\d{0,11}$/).test(value)) { 

					// Diner's Club
					mask = '9999 999999 9999'

				} else if ((/^\d{0,16}$/).test(value)) {

					// Other Credit Cards
					mask = '9999 9999 9999 9999'

				} else {

					// Unknown
					mask = 'remove'
				}

				if($(this).attr('data-currentmask') !== mask) {

					$(this).attr('data-currentmask', mask)
					$(this).inputmask(mask, { placeholder: '', clearMaskOnLostFocus: false });
				}
			});
		});
	}

	// Form email
	$.WS_Form.prototype.form_email = function() {

		var ws_this = this;

		// Regular email fields
		$('input[type="email"]:not([data-init-email])', this.form_canvas_obj).each(function() {

			// Get field data
			var field = ws_this.get_field($(this));

			// Check for allow / deny
			var field_allow_deny = ws_this.get_object_meta_value(field, 'allow_deny', '');
			var field_allow_deny_values = ws_this.get_object_meta_value(field, 'allow_deny_values', []);
			if(
				(field_allow_deny !== '') &&
				(['allow', 'deny'].indexOf(field_allow_deny) !== -1) &&
				(typeof field_allow_deny_values === 'object')
			) {

				// Initial check
				ws_this.form_email_allow_deny($(this));

				// Event handler
				$(this).on('change', function() {

					ws_this.form_email_allow_deny($(this));
				});
			}

			// Set attribute so this field is not initialized again
			$(this).attr('data-init-email', '');
		});
	}

	$.WS_Form.prototype.form_email_allow_deny = function(obj) {

		// Get field value
		var field_value = obj.val();
		if(field_value === '') { return; }

		// Get field data
		var field = this.get_field(obj);

		// Check for allow / deny
		var field_allow_deny = this.get_object_meta_value(field, 'allow_deny', '');

		// Values
		var field_allow_deny_values = this.get_object_meta_value(field, 'allow_deny_values', []);

		// Default value
		var field_value_allowed = (field_allow_deny === 'deny');

		// Execute hooks and pass form_valid to them
		for(var field_allow_deny_values_index in field_allow_deny_values) {

			if(!field_allow_deny_values.hasOwnProperty(field_allow_deny_values_index)) { continue; }

			var row = field_allow_deny_values[field_allow_deny_values_index];

			var field_allow_deny_value = row.allow_deny_value;
			var field_allow_deny_regex = new RegExp(field_allow_deny_value.replace('*', '.*') + '$');
			if (field_allow_deny_regex.test(field_value)) {

				field_value_allowed = (field_allow_deny === 'allow');
			}
		}

		if(!field_value_allowed) {

			// Get message
			var allow_deny_message = this.get_object_meta_value(field, 'allow_deny_message', '');
			if(!allow_deny_message) { allow_deny_message = this.language('error_email_allow_deny_message'); }

			// Set invalid feedback
			this.set_invalid_feedback(obj, allow_deny_message);

		} else {

			// Reset invalid feedback
			this.set_invalid_feedback(obj, '');
		}
	}

	// Form navigation
	$.WS_Form.prototype.form_navigation = function() {

		var ws_this = this;

		var group_count = this.get_group_count();

		// Buttons - Next
		$('[data-action="wsf-tab_next"]', this.form_canvas_obj).each(function() {

			// Remove existing click event
			$(this).off('click');

			// Get next group
			var group_next = $(this).closest('[data-group-index]').nextAll(':not([data-wsf-group-hidden])').first();

			// If there are no tabs, or no next tab, disable the next button
			if(
				(group_count <= 1) ||
				(!group_next.length)
			) {
				$(this).prop('disabled', true).attr('data-wsf-disabled', '');

			} else {

				if(typeof $(this).attr('data-wsf-disabled') !== 'undefined') { $(this).prop('disabled', false).removeAttr('data-wsf-disabled'); }
			}

			// If button is disabled, then don't initialize
			if(typeof $(this).attr('disabled') !== 'undefined') { return; }

			// Add click event
			$(this).on('click', function() {

				var tab_validation = ws_this.get_object_meta_value(ws_this.form, 'tab_validation');
				var tab_validation_show = ws_this.get_object_meta_value(ws_this.form, 'tab_validation_show');

				if(tab_validation && tab_validation_show) {

					var group_obj = ws_this.get_group($(this));

					if(
						group_obj &&
						(typeof group_obj.attr('data-wsf-validated') === 'undefined')
					) {
						// Add validated class
						group_obj.addClass(ws_this.class_validated);

						// Process accessibility
						ws_this.form_accessibility();

						// Focus object
						if(ws_this.get_object_meta_value(ws_this.form, 'invalid_field_focus', true)) {

							// Find first invalid field and focus on it (We do it this way to overcome with jQuery bug that causes a syntax error for :invalid in certain browsers)
							ws_this.get_field_elements(group_obj).each(function() {

								if(ws_this.is_invalid($(this))) {

									// Focus invalid field
									$(this).focus().trigger('focus');

									// Return false to exit the loop
									return false;
								}
							});
						}

					} else {

						// Remove validated class
						group_obj.removeClass(this.class_validated);

						// Progress to the next tab
						if(typeof ws_this.form_tab_group_index_new === 'function') {

							ws_this.form_tab_group_index_new($(this), group_next.attr('data-group-index'));
						}
					}

				} else {

					// Progress to the next tab
					if(typeof ws_this.form_tab_group_index_new === 'function') {

						ws_this.form_tab_group_index_new($(this), group_next.attr('data-group-index'));
					}
				}
			});
		});

		// Buttons - Previous
		$('[data-action="wsf-tab_previous"]', this.form_canvas_obj).each(function() {

			// Remove existing click event
			$(this).off('click');

			// Get previous group
			var group_previous = $(this).closest('[data-group-index]').prevAll(':not([data-wsf-group-hidden])').first();

			// If there are no tabs, or no previous tab, disable the previous button
			if(
				(group_count <= 1) ||
				(!group_previous.length)
			) {
				$(this).prop('disabled', true).attr('data-wsf-disabled', '');

			} else {

				if(typeof $(this).attr('data-wsf-disabled') !== 'undefined') { $(this).prop('disabled', false).removeAttr('data-wsf-disabled'); }
			}

			// If button is disabled, then don't initialize
			if(typeof $(this).attr('disabled') !== 'undefined') { return; }

			// Add click event
			if(typeof ws_this.form_tab_group_index_new === 'function') {

				$(this).on('click', function() {

					ws_this.form_tab_group_index_new($(this), group_previous.attr('data-group-index'));
				});
			}
		});

		// Buttons - Save
		this.form_canvas_obj.off('click', '[data-action="wsf-save"]').on('click', '[data-action="wsf-save"]', function() {

			// Get field
			var field = ws_this.get_field($(this));

			if(typeof field !== 'undefined') {

				var validate_form = ws_this.get_object_meta_value(field, 'validate_form', '');

				if(validate_form) {

					ws_this.form_post_if_validated('save');

				} else {

					ws_this.form_post('save');
				}
			}
		});

		// Buttons - Reset
		this.form_canvas_obj.off('click', '[data-action="wsf-reset"]').on('click', '[data-action="wsf-reset"]', function(e) {

			// Prevent default
			e.preventDefault();

			ws_this.form_reset();
		});

		// Buttons - Clear
		this.form_canvas_obj.off('click', '[data-action="wsf-clear"]').on('click', '[data-action="wsf-clear"]', function() {

			ws_this.form_clear();
		});
	}

	// Get tab index object resides in
	$.WS_Form.prototype.get_group_index = function(obj) {

		// Get group
		var group_single = this.get_group(obj);
		if(group_single === false) { return false; }

		// Get group index
		var group_index = group_single.first().attr('data-group-index');
		if(group_index === undefined) { return false; }

		return parseInt(group_index, 10);
	}

	// Get group count
	$.WS_Form.prototype.get_group_count = function() {

		var group_count = $('.wsf-group-tabs', this.form_canvas_obj).children(':not([data-wsf-group-hidden])').length;
	}

	// Get group object resides in
	$.WS_Form.prototype.get_group = function(obj) {

		// Check that tabs exist
		if(this.get_group_count() <= 1) { return false; }

		// Get group
		var group_single = obj.closest('[data-group-index]');
		if(group_single.length === 0) { return false; }

		return group_single;
	}

	// Get group id from object
	$.WS_Form.prototype.get_group_id = function(obj) {

		var group_id = obj.closest('[data-id]').attr('data-id');

		return (typeof group_id !== 'undefined') ? parseInt(group_id, 10) : false;
	}

	// Get section object resides in
	$.WS_Form.prototype.get_section = function(obj) {

		// Get section
		var section_single = obj.closest('fieldset');
		if(section_single.length === 0) { return false; }

		return section_single;
	}

	// Get section id from object
	$.WS_Form.prototype.get_section_id = function(obj) {

		var section_id = obj.closest('[id^="' + this.form_id_prefix + 'section-"]').attr('data-id');

		return (typeof section_id !== 'undefined') ? parseInt(section_id, 10) : false;
	}

	// Get section repeatable index from object
	$.WS_Form.prototype.get_section_repeatable_index = function(obj) {

		var section_repeatable_index = obj.closest('[id^="' + this.form_id_prefix + 'section-"]').attr('data-repeatable-index');

		return (section_repeatable_index > 0) ? parseInt(section_repeatable_index, 10) : 0;
	}

	// Get section repeatable suffix from object
	$.WS_Form.prototype.get_section_repeatable_suffix = function(obj) {

		var section_repeatable_index = this.get_section_repeatable_index(obj);

		return section_repeatable_index ? '-repeat-' + section_repeatable_index : '';
	}

	// Get all field elements within obj
	$.WS_Form.prototype.get_field_elements = function(obj) {

		return $('input,select,textarea', obj).filter(':not([data-hidden],[data-hidden-section],[data-hidden-group],[disabled],[type="hidden"])');
	}

	// Get field from obj
	$.WS_Form.prototype.get_field = function(obj) {

		var field_id = this.get_field_id(obj);

		return field_id ? this.field_data_cache[field_id] : false;
	}

	// Get field wrapper from object
	$.WS_Form.prototype.get_field_wrapper = function(obj) {

		return obj.closest('[data-id]')
	}

	// Get field id from object
	$.WS_Form.prototype.get_field_id = function(obj) {

		// Get field ID
		var field_id = obj.closest('[data-type][data-id]').attr('data-id');
		if(field_id) { return parseInt(field_id, 10); }

		// Check for hidden field
		var field_id = obj.attr('data-id-hidden');
		if(field_id) { return parseInt(field_id, 10); }

		return false;
	}

	// Get field type from object
	$.WS_Form.prototype.get_field_type = function(obj) {

		var field_type = obj.closest('[data-type]').attr('data-type');

		return (typeof field_type !== 'undefined') ? field_type : false;
	}

	// Get object row id from object
	$.WS_Form.prototype.get_field_object_row_id = function(obj) {

		// Get row wrapper
		var object_row_wrapper = obj.closest('[data-row-radio][data-row-id],[data-row-checkbox][data-row-id]');
		if(object_row_wrapper.length === 0) { return false; }

		// Get row ID
		var object_row_id = object_row_wrapper.attr('data-row-id');

		return (typeof object_row_id !== 'undefined') ? parseInt(object_row_id, 10) : false;
	}

	// Get label object
	$.WS_Form.prototype.get_label_obj = function(obj) {

		var field_id = this.get_field_id(obj);
		var section_repeatable_suffix = this.get_section_repeatable_suffix(obj);

		return $('#' + this.form_id_prefix + 'label-' + field_id + section_repeatable_suffix, this.form_canvas_obj);
	}

	// Get min/max object
	$.WS_Form.prototype.get_checkbox_min_max_obj = function(obj) {

		var field_id = this.get_field_id(obj);
		var section_repeatable_suffix = this.get_section_repeatable_suffix(obj);

		return $('#' + this.form_id_prefix + 'checkbox-min-max-' + field_id + section_repeatable_suffix, this.form_canvas_obj);
	}

	// Get checkbox row count
	$.WS_Form.prototype.get_checkbox_row_count = function(obj) {

		// Get field wrapper
		var field_wrapper = this.get_field_wrapper(obj);

		// Get checkboxes
		var checkbox_objs = $('[data-row-checkbox]:not([style*="display: none"]) input:not([data-hidden],[data-hidden-section],[data-hidden-group])', field_wrapper);

		return checkbox_objs ? checkbox_objs.length : 0;
	}

	// Get radio row count
	$.WS_Form.prototype.get_radio_row_count = function(obj) {

		// Get field wrapper
		var field_wrapper = this.get_field_wrapper(obj);

		// Get radios
		var radio_objs = $('[data-row-radio]:not([style*="display: none"]) input:not([data-hidden],[data-hidden-section],[data-hidden-group])', field_wrapper);

		return radio_objs ? radio_objs.length : 0;
	}

	// Get help from object
	$.WS_Form.prototype.get_help_obj = function(obj) {

		var field_id = this.get_field_id(obj);
		var section_repeatable_suffix = this.get_section_repeatable_suffix(obj);

		return $('#' + this.form_id_prefix + 'help-' + field_id + section_repeatable_suffix, this.form_canvas_obj);
	}

	// Get invalid feedback object
	$.WS_Form.prototype.get_invalid_feedback_obj = function(obj, object_row_id) {

		return $('#' + this.get_invalid_feedback_id(obj, object_row_id));
	}

	// Get invalid feedback ID
	$.WS_Form.prototype.get_invalid_feedback_id = function(obj, object_row_id) {

		var field_id = this.get_field_id(obj);
		var section_repeatable_suffix = this.get_section_repeatable_suffix(obj);
		var row_suffix = (object_row_id ? '-row-' + object_row_id : '');

		return this.form_id_prefix + 'invalid-feedback-' + field_id + row_suffix + section_repeatable_suffix;
	}

	// Get invalid feedback from object
	$.WS_Form.prototype.get_invalid_feedback = function(obj) {

		if(
			!obj.length ||
			(typeof obj[0].validationMessage === 'undefined')
		) {
			return false;
		}

		return obj[0].validationMessage;
	}

	// Set invalid feedback on object
	$.WS_Form.prototype.set_invalid_feedback = function(obj, message, object_row_id) {

		// Check for object row ID
		if(this.is_not_number(object_row_id)) { object_row_id = 0; }

		// Check if object_row_id is an object
		if(
			(typeof object_row_id === 'object') &&
			(typeof object_row_id[0] !== 'undefined')
		) {
			object_row_id = object_row_id[0];
		}

		// Get invalid feedback obj
		var invalid_feedback_obj = this.get_invalid_feedback_obj(obj, object_row_id);

		// Get section ID
		var section_id = this.get_section_id(obj);

		// Get section repeatable index
		var section_repeatable_index = this.get_section_repeatable_index(obj);

		// Get field ID
		var field_id = this.get_field_id(obj);

		// Check for false message
		if(message === false) { message = invalid_feedback_obj.html(); }

		// HTML 5 custom validity
		if(obj.length && obj[0].willValidate) {

			if(message !== '') {

				// Store message
				if(typeof this.validation_message_cache[section_id] === 'undefined') { this.validation_message_cache[section_id] = []; }
				if(typeof this.validation_message_cache[section_id][section_repeatable_index] === 'undefined') { this.validation_message_cache[section_id][section_repeatable_index] = []; }
				if(typeof this.validation_message_cache[section_id][section_repeatable_index][field_id] === 'undefined') { this.validation_message_cache[section_id][section_repeatable_index][field_id] = []; }

				this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id] = message;

			} else {

				// Recall message
				if(
					(typeof this.validation_message_cache[section_id] !== 'undefined') &&
					(typeof this.validation_message_cache[section_id][section_repeatable_index] !== 'undefined') &&
					(typeof this.validation_message_cache[section_id][section_repeatable_index][field_id] !== 'undefined') &&
					(typeof this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id] !== 'undefined')
				) {

					delete this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id];
				}
			}

			// Set custom validity
			obj[0].setCustomValidity(message);

			// Run validate real time processing
			this.form_validate_real_time_process(false, false);
		}

		// Invalid feedback text
		if(invalid_feedback_obj.length) {

			if(message !== '') {

				// Store invalid feedback
				if(typeof this.invalid_feedback_cache[section_id] === 'undefined') { this.invalid_feedback_cache[section_id] = []; }
				if(typeof this.invalid_feedback_cache[section_id][section_repeatable_index] === 'undefined') { this.invalid_feedback_cache[section_id][section_repeatable_index] = []; }
				if(typeof this.invalid_feedback_cache[section_id][section_repeatable_index][field_id] === 'undefined') { this.invalid_feedback_cache[section_id][section_repeatable_index][field_id] = []; }

				if(typeof this.invalid_feedback_cache[section_id][section_repeatable_index][field_id][object_row_id] === 'undefined') {

					this.invalid_feedback_cache[section_id][section_repeatable_index][field_id][object_row_id] = invalid_feedback_obj.html();
				}

				// Set invalid feedback
				invalid_feedback_obj.html(message);

			} else {

				// Recall invalid feedback
				if(
					(typeof this.invalid_feedback_cache[section_id] !== 'undefined') &&
					(typeof this.invalid_feedback_cache[section_id][section_repeatable_index] !== 'undefined') &&
					(typeof this.invalid_feedback_cache[section_id][section_repeatable_index][field_id] !== 'undefined') &&
					(typeof this.invalid_feedback_cache[section_id][section_repeatable_index][field_id][object_row_id] !== 'undefined')
				) {

					invalid_feedback_obj.html(this.invalid_feedback_cache[section_id][section_repeatable_index][field_id][object_row_id]);

					delete this.invalid_feedback_cache[section_id][section_repeatable_index][field_id][object_row_id];
				}
			}
		}
	}

	// Form preview
	$.WS_Form.prototype.form_preview = function() {

		if(this.form_canvas_obj[0].hasAttribute('data-preview')) {

			this.form_add_hidden_input('wsf_preview', 'true');
		}
	}

	// Honeypot
	$.WS_Form.prototype.form_honeypot = function() {

		// Honeypot
		var honeypot = this.get_object_meta_value(this.form, 'honeypot', false);

		if(honeypot) {

			// Add honeypot field
			var honeypot_hash = (this.form.published_checksum !== '') ? this.form.published_checksum : ('honeypot_unpublished_' + this.form_id);

			// Build honeypot input
			var framework_type = $.WS_Form.settings_plugin.framework;
			var framework = $.WS_Form.frameworks.types[framework_type];
			var fields = this.framework.fields.public;
			var honeypot_attributes = (typeof fields.honeypot_attributes !== 'undefined') ? ' ' + fields.honeypot_attributes.join(' ') : '';

			// Add to form
			var honeypot_html = '<label for="field_' + honeypot_hash + '" aria-hidden="true" style="position: absolute !important; ' + (ws_form_settings.rtl ? 'right' : 'left') + ': -9999em !important; height: 0 !important; margin: 0 !important; padding: 0 !important;">' + this.esc_html(honeypot_hash) + '<input type="text" id="field_' + this.esc_attr(honeypot_hash) + '" name="field_' + this.esc_attr(honeypot_hash) + '" value="" autocomplete="off" tabindex="-1" style="display: none !important;"' + honeypot_attributes + '></label>';
			this.form_canvas_obj.append(honeypot_html);

			// Debug
			this.log('log_honeypot', '', 'spam-protect');
		}
	}

	// Get CSS var
	$.WS_Form.prototype.get_css_var = function(css_var, default_value) {

		if(typeof default_value === 'undefined') { default_value = ''; }

		var computed_style = getComputedStyle(this.form_obj[0]);
		if(!computed_style) { return default_value; }

		var property_value = computed_style.getPropertyValue(css_var);
		if(typeof property_value === 'undefined') { return default_value; }

		return property_value.trim();
	}

	// Addition styling for field borders
	$.WS_Form.prototype.form_css_var = function() {

		// Field label inside mode
		switch(this.get_css_var('--wsf-field-label-inside-mode', 'move')) {

			case 'hide' :

				this.form_obj.addClass('wsf-label-position-inside-hide');
				break;
		}

		// Field border placement
		switch(this.get_css_var('--wsf-field-border-placement', 'all')) {

			case 'bottom' :

				this.form_obj.addClass('wsf-field-border-placement-bottom');
				break;
		}
	}

	// Addition styling for labels
	$.WS_Form.prototype.form_label = function(obj) {

		if(typeof obj === 'undefined') { obj = this.form_canvas_obj; }

		var ws_this = this;

		// Find all fields with inside label positioning
		$('.wsf-label-position-inside:not([wsf-label-position-inside-init]):visible', obj).each(function() {

			// Get prefix object
			var prefix_obj = $('.wsf-input-group-prepend', $(this));

			// Check if ITI enabled on field
			var iti_obj = $('.iti', $(this));

			// Check if a prefix exists and it is not an ITI enabled field (ITI changes the DOM structure)
			if(prefix_obj.length && !iti_obj.length) {

				// Get label object
				var label_obj = $('label', $(this));

				// Get existing left offset
				var left_offset_old = parseFloat(label_obj.css('left').replace('px', ''));

				// Get width of prefix
				var prefix_width = prefix_obj.outerWidth();

				// Calculate new left offset
				var left_offset_new = left_offset_old + prefix_width;

				// Set label object left CSS attribute
				label_obj.css('left', left_offset_new + 'px');
			}

			// Get help ovject
			var help_obj = ws_this.get_help_obj($(this));

			if(help_obj.length) {

				// Get field ID
				var field_id = ws_this.get_field_id($(this));

				// Get field config
				var field = ws_this.field_data_cache[field_id];

				// Check help position
				if(ws_this.get_help_position(field) === 'top') {

					// Get height of help object
					var help_obj_height = parseFloat(help_obj.outerHeight());

					// Get bottom margin of help object
					var help_obj_margin_bottom = parseFloat(help_obj.css('margin-bottom'));

					// Get label object
					var label_obj = ws_this.get_label_obj($(this));

					// Get top of label
					var label_top = parseFloat(label_obj.css('top'));

					// Calculate new top of label
					label_top += help_obj_height + help_obj_margin_bottom;

					// Set new top of label
					label_obj.css('top', label_top + 'px');
				}
			}

			// Set as initialized
			$(this).attr('wsf-label-position-inside-init', '');
		});
	}

	// Adds required string (if found in framework config) to all labels
	$.WS_Form.prototype.form_required = function() {

		var ws_this = this;

		// Get required label HTML
		var label_required = this.get_object_meta_value(this.form, 'label_required', false);
		if(!label_required) { return false; }

		var label_mask_required = this.get_object_meta_value(this.form, 'label_mask_required', '', true, true);
		if(label_mask_required === '') {

			// Use framework mask_required_label
			var framework_type = $.WS_Form.settings_plugin.framework;
			var framework = $.WS_Form.frameworks.types[framework_type];
			var fields = this.framework.fields.public;

			if(typeof fields.mask_required_label === 'undefined') { return false; }
			var label_mask_required = fields.mask_required_label;
			if(label_mask_required === '') { return false; }
		}

		// Get all labels in this form
		$('label', this.form_canvas_obj).each(function() {

			// Get 'for' attribute of label
			var id = $(this).attr('for');
			if(typeof id !== 'undefined') {

				// Get field related to 'for'
				var field_obj = $('[id="' + ws_this.esc_selector(id) + '"]', ws_this.form_canvas_obj);
				if(!field_obj.length) { return; }

				// Check if field should be processed
				if(typeof field_obj.attr('data-init-required') !== 'undefined') { return; }

				// Check if field is required
				var field_required = (typeof field_obj.attr('data-required') !== 'undefined');

			} else {

				// Is this a checkbox with min / max set?
				var field_obj = ws_this.get_checkbox_min_max_obj($(this));
				if(!field_obj.length) { return; }

				// Check for min attribute
				var field_required = (typeof field_obj.attr('min') !== 'undefined') && (parseInt(field_obj.attr('min'), 10) >  0);
			}

			// Check if the require string should be added to the parent label (e.g. for radios)
			var label_required_id = $(this).attr('data-label-required-id');
			if((typeof label_required_id !== 'undefined') && (label_required_id !== false)) {

				var label_obj = $('#' + label_required_id, ws_this.form_canvas_obj);

			} else {

				var label_obj = $(this);
			}

			// Check if wsf-required-wrapper span exists, if not, create it (You can manually insert it in config using #required)
			var required_wrapper = $('.wsf-required-wrapper', label_obj);
			if(!required_wrapper.length && field_required) {

				var required_wrapper_html = '<span class="wsf-required-wrapper"></span>';

				// If field is wrapped in label, find the first the first element to inject the required wrapper before
				var first_child = label_obj.children('div,[name]').first();

				// Add at appropriate place
				if(first_child.length) {

					first_child.before(required_wrapper_html);

				} else {

					label_obj.append(required_wrapper_html);
				}

				required_wrapper = $('.wsf-required-wrapper', label_obj);
			}

			if(field_required) {

				// Add it
				required_wrapper.html(label_mask_required);
				field_obj.attr('data-init-required', '');

			} else {

				// Remove it
				required_wrapper.html('');
				field_obj.removeAttr('data-init-required');
			}
		});
	}

	// Field required bypass
	$.WS_Form.prototype.form_bypass = function(conditional_initiated) {

		if(!this.form_bypass_enabled) { return false; }

		var ws_this = this;

		// Look for number fields and add step="1" if none present
		// This ensures that if a number field is hidden that the data-step-bypass is added with step="any" so that the form submits
		$('input[type="number"]:not([step]):not([data-step-bypass])', this.form_canvas_obj).attr('step', 1);

		// Process attributes that should be bypassed if a field is hidden
		var attributes = this.form_bypass_attributes();

		for(var attribute_source in attributes) {

			if(!attributes.hasOwnProperty(attribute_source)) { continue; }

			var attribute_config = attributes[attribute_source];

			var attribute_bypass = attribute_config.bypass;
			var attribute_not = attribute_config.not;
			var attribute_replace = (typeof attribute_config.replace !== 'undefined') ? attribute_config.replace : false;

			// Groups

			// If a group is visible, and contains fields that have a data bypass attribute, reset that attribute
			if($('[' + attribute_bypass + '-group]', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'group-"]:not([data-wsf-group-hidden]) [' + attribute_bypass + '-group]:not(' + attribute_not + ')', this.form_canvas_obj).attr(attribute_source, function() { return $(this).attr(attribute_bypass + '-group'); }).removeAttr(attribute_bypass + '-group');
			}

			// If a group is not visible, and contains validation attributes, add bypass attributes
			if($('[' + attribute_source + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'group-"][data-wsf-group-hidden] [' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '-group]), [id^="' + this.form_id_prefix + 'group-"][data-wsf-group-hidden] [' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '-group])').attr(attribute_bypass + '-group', function() { return ws_this.form_bypass_hidden($(this), attribute_source, attribute_replace); });
			}

			// If a hidden field is in a hidden group, convert bypass address to group level
			if($('[' + attribute_bypass + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'group-"][data-wsf-group-hidden] [' + attribute_bypass + ']:not(' + attribute_not + '), [id^="' + this.form_id_prefix + 'group-"][data-wsf-group-hidden] [' + attribute_bypass + ']:not(' + attribute_not + ')').attr(attribute_bypass + '-group', function() { return ws_this.form_bypass_visible($(this), attribute_bypass); }).removeAttr(attribute_bypass);
			}

			// Sections

			// If a section is visible, and contains fields that have a data bypass attribute, reset that attribute
			if($('[' + attribute_bypass + '-section]', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'section-"]:not([style*="display:none"],[style*="display: none"]) [' + attribute_bypass + '-section]:not(' + attribute_not + ')', this.form_canvas_obj).attr(attribute_source, function() { return $(this).attr(attribute_bypass + '-section'); }).removeAttr(attribute_bypass + '-section');
			}

			// If a section is not visible, and contains validation attributes, add bypass attributes
			if($('[' + attribute_source + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'section-"][style*="display:none"] [' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '-section]), [id^="' + this.form_id_prefix + 'section-"][style*="display: none"] [' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '-section])').attr(attribute_bypass + '-section', function() { return ws_this.form_bypass_hidden($(this), attribute_source, attribute_replace); });
			}

			// If a hidden field is in a hidden section, convert bypass address to section level
			if($('[' + attribute_bypass + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'section-"][style*="display:none"] [' + attribute_bypass + ']:not(' + attribute_not + '), [id^="' + this.form_id_prefix + 'section-"][style*="display: none"] [' + attribute_bypass + ']:not(' + attribute_not + ')').attr(attribute_bypass + '-section', function() { return ws_this.form_bypass_visible($(this), attribute_bypass); }).removeAttr(attribute_bypass);
			}

			// Fields

			// If field is visible, add validation attributes back that have a bypass data tag
			if($('[' + attribute_bypass + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'field-wrapper-"]:not([style*="display:none"],[style*="display: none"]) [' + attribute_bypass + ']:not(' + attribute_not + ')', this.form_canvas_obj).attr(attribute_source, function() { return ws_this.form_bypass_visible($(this), attribute_bypass); }).removeAttr(attribute_bypass);
			}

			// If field is not visible, add contain validation attributes, add bypass attributes
			if($('[' + attribute_source + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'field-wrapper-"][style*="display:none"] [' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + ']), [id^="' + this.form_id_prefix + 'field-wrapper-"][style*="display: none"] [' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '])', this.form_canvas_obj).attr(attribute_bypass, function() { return ws_this.form_bypass_hidden($(this), attribute_source, attribute_replace); });
			}

			// Rows - Checkbox

			// If field is visible, add validation attributes back that have a bypass data tag
			if($('[' + attribute_bypass + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'field-wrapper-"]:not([style*="display:none"],[style*="display: none"]) [data-row-checkbox]:not([style*="display:none"],[style*="display: none"]) > input[type="checkbox"] [' + attribute_bypass + ']:not(' + attribute_not + ')', this.form_canvas_obj).attr(attribute_source, function() { return ws_this.form_bypass_visible($(this), attribute_bypass); }).removeAttr(attribute_bypass);
			}

			// If field is not visible, add contain validation attributes, add bypass attributes
			if($('[' + attribute_source + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-checkbox][style*="display:none"] > input[type="checkbox"][' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + ']), [id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-checkbox][style*="display: none"] > input[type="checkbox"][' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '])', this.form_canvas_obj).attr(attribute_bypass, function() { return ws_this.form_bypass_hidden($(this), attribute_source, attribute_replace); });
			}

			// Rows - Radio

			// If field is visible, add validation attributes back that have a bypass data tag
			if($('[' + attribute_bypass + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'field-wrapper-"]:not([style*="display:none"],[style*="display: none"]) [data-row-radio]:not([style*="display:none"],[style*="display: none"]) > input[type="radio"] [' + attribute_bypass + ']:not(' + attribute_not + ')', this.form_canvas_obj).attr(attribute_source, function() { return ws_this.form_bypass_visible($(this), attribute_bypass); }).removeAttr(attribute_bypass);
			}

			// If field is not visible, add contain validation attributes, add bypass attributes
			if($('[' + attribute_source + ']', this.form_canvas_obj).length) {

				$('[id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-radio][style*="display:none"] > input[type="radio"][' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + ']), [id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-radio][style*="display: none"] > input[type="radio"][' + attribute_source + ']:not(' + attribute_not + ',[' + attribute_bypass + '])', this.form_canvas_obj).attr(attribute_bypass, function() { return ws_this.form_bypass_hidden($(this), attribute_source, attribute_replace); });
			}
		}

		// Process custom validity messages - Groups
		$('[id^="' + this.form_id_prefix + 'group-"]:not([data-wsf-group-hidden])', this.form_canvas_obj).find('[name]:not([type="hidden"]),[data-static],[data-recaptcha],[data-hcaptcha],[data-turnstile],[data-captchafox]').each(function() {

			ws_this.form_bypass_process($(this), '-group', false);
		});

		$('[id^="' + this.form_id_prefix + 'group-"][data-wsf-group-hidden]', this.form_canvas_obj).find('[name]:not([type="hidden"]),[data-static],[data-recaptcha],[data-hcaptcha],[data-turnstile],[data-captchafox]').each(function() {

			ws_this.form_bypass_process($(this), '-group', true);
		});

		// Process custom validity messages - Sections
		$('[id^="' + this.form_id_prefix + 'section-"]:not([style*="display:none"],[style*="display: none"])', this.form_canvas_obj).find('[name]:not([type="hidden"],[data-hidden-group]),[data-static],[data-recaptcha],[data-hcaptcha],[data-turnstile],[data-captchafox]').each(function() {

			ws_this.form_bypass_process($(this), '-section', false);
		});

		$('[id^="' + this.form_id_prefix + 'section-"][style*="display:none"], [id^="' + this.form_id_prefix + 'section-"][style*="display: none"]').find('[name]:not([type="hidden"]),[data-static],[data-recaptcha],[data-hcaptcha],[data-turnstile],[data-captchafox]').each(function() {

			ws_this.form_bypass_process($(this), '-section', true);
		});

		// Process custom validity messages - Fields
		$('[id^="' + this.form_id_prefix + 'field-wrapper-"]:not([style*="display:none"],[style*="display: none"])', this.form_canvas_obj).find('[name]:not([type="hidden"],[data-hidden-section],[data-hidden-group]),[data-static],[data-recaptcha],[data-hcaptcha],[data-turnstile],[data-captchafox]').each(function() {

			ws_this.form_bypass_process($(this), '', false);
		});

		$('[id^="' + this.form_id_prefix + 'field-wrapper-"][style*="display:none"], [id^="' + this.form_id_prefix + 'field-wrapper-"][style*="display: none"]', this.form_canvas_obj).find('[name]:not([type="hidden"]),[data-static],[data-recaptcha],[data-hcaptcha],[data-turnstile],[data-captchafox]').each(function() {

			ws_this.form_bypass_process($(this), '', true);
		});

		// Process custom validity messages - Rows - Checkbox
		$('[id^="' + this.form_id_prefix + 'field-wrapper-"]:not([style*="display:none"],[style*="display: none"]) [data-row-checkbox]:not([style*="display:none"],[style*="display: none"]) input[type="checkbox"]', this.form_canvas_obj).each(function() {

			ws_this.form_bypass_process($(this), '', false);
		});

		$('[id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-checkbox][style*="display:none"] input[type="checkbox"], [id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-checkbox][style*="display: none"] input[type="checkbox"]', this.form_canvas_obj).each(function() {

			ws_this.form_bypass_process($(this), '', true);
		});

		// Process custom validity messages - Rows - Radio
		$('[id^="' + this.form_id_prefix + 'field-wrapper-"]:not([style*="display:none"],[style*="display: none"]) [data-row-radio]:not([style*="display:none"],[style*="display: none"]) input[type="radio"]', this.form_canvas_obj).each(function() {

			ws_this.form_bypass_process($(this), '', false);
		});

		$('[id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-radio][style*="display:none"] input[type="radio"], [id^="' + this.form_id_prefix + 'field-wrapper-"] [data-row-radio][style*="display: none"] input[type="radio"]', this.form_canvas_obj).each(function() {

			ws_this.form_bypass_process($(this), '', true);
		});

		// Process form progress
		if(typeof this.form_progress_process === 'function') { this.form_progress_process(); }

		// Recalculate e-commerce
		if(
			this.has_ecommerce &&
			(typeof this.form_ecommerce_calculate === 'function')
		) {
			this.form_ecommerce_calculate();
		}

		// Fire real time form validation
		this.form_validate_real_time_process(conditional_initiated, false);

		return true;
	}

	// Form bypass - Attributes
	$.WS_Form.prototype.form_bypass_attributes = function() {

		return {
			'required':						{'bypass': 'data-required-bypass', 'not': '[type="hidden"]'},
			'aria-required':				{'bypass': 'data-aria-required-bypass', 'not': '[type="hidden"]'},
			'min':							{'bypass': 'data-min-bypass', 'not': '[type="hidden"],[type="range"]'},
			'max':							{'bypass': 'data-max-bypass', 'not': '[type="hidden"],[type="range"]'},
			'minlength':					{'bypass': 'data-minlength-bypass', 'not': '[type="hidden"]'},
			'maxlength':					{'bypass': 'data-maxlength-bypass', 'not': '[type="hidden"]'},
			'pattern':						{'bypass': 'data-pattern-bypass', 'not': '[type="hidden"]'},
			'step':							{'bypass': 'data-step-bypass', 'not': '[type="hidden"],[type="range"]', 'replace': 'any'},
			'data-ecommerce-price':			{'bypass': 'data-ecommerce-price-bypass', 'not': '[data-ecommerce-persist]'},
			'data-ecommerce-cart-price':	{'bypass': 'data-ecommerce-cart-price-bypass', 'not': '[data-ecommerce-persist]'}
		};
	}

	// Form bypass - Reset on object back to its original state ready for form_bypass to be reprocessed on it
	$.WS_Form.prototype.form_bypass_obj_reset = function(obj) {

		var attributes = this.form_bypass_attributes();

		for(var attribute_source in attributes) {

			if(!attributes.hasOwnProperty(attribute_source)) { continue; }

			var attribute_config = attributes[attribute_source];

			var attribute_bypass = attribute_config.bypass;

			if(obj.attr(attribute_bypass + '-group')) {

				obj.attr(attribute_source, obj.attr(attribute_bypass + '-group')).removeAttr(attribute_bypass + '-group');;
			}

			if(obj.attr(attribute_bypass + '-section')) {

				obj.attr(attribute_source, obj.attr(attribute_bypass + '-section')).removeAttr(attribute_bypass + '-section');
			}

			if(obj.attr(attribute_bypass)) {

				obj.attr(attribute_source, obj.attr(attribute_bypass)).removeAttr(attribute_bypass);
			}
		}
	}

	// Form bypass - Hidden
	$.WS_Form.prototype.form_bypass_hidden = function(obj, attribute_source, attribute_replace) {

		var attribute_source_value = obj.attr(attribute_source);

		if(attribute_replace) {

			obj.attr(attribute_source, attribute_replace);

		} else {

			obj.removeAttr(attribute_source);
		}

		return attribute_source_value;
	}

	// Form bypass - Visible
	$.WS_Form.prototype.form_bypass_visible = function(obj, attribute_bypass) {

		return obj.attr(attribute_bypass);
	}

	// Form bypass process
	$.WS_Form.prototype.form_bypass_process = function(obj, attr_suffix, set_hidden) {

		// Get section ID
		var section_id = this.get_section_id(obj);

		// Get section repeatable index
		var section_repeatable_index = this.get_section_repeatable_index(obj);

		// Get field ID
		var field_id = this.get_field_id(obj);

		// Get object row ID (0 if not found)
		var object_row_id = this.get_field_object_row_id(obj);
		if(!object_row_id) { object_row_id = 0; }

		// Get object element
		var obj_el = obj[0];

		if(set_hidden) {

			// Cache custom validation message if set
			if(
				obj_el.willValidate &&
				obj_el.validity &&
				obj_el.validity.customError &&
				(obj_el.validationMessage !== '')
			) {
				if(typeof this.validation_message_cache[section_id] === 'undefined') { this.validation_message_cache[section_id] = []; }
				if(typeof this.validation_message_cache[section_id][section_repeatable_index] === 'undefined') { this.validation_message_cache[section_id][section_repeatable_index] = []; }
				if(typeof this.validation_message_cache[section_id][section_repeatable_index][field_id] === 'undefined') { this.validation_message_cache[section_id][section_repeatable_index][field_id] = []; }

				this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id] = obj_el.validationMessage;

				// Set custom validation message to blank
				obj_el.setCustomValidity('');
			}

			// Add data-hidden attribute
			obj.attr('data-hidden' + attr_suffix, '');

		} else {

			// Recall custom validation message if cached
			if(
				obj_el.willValidate &&
				(typeof this.validation_message_cache[section_id] !== 'undefined') &&
				(typeof this.validation_message_cache[section_id][section_repeatable_index] !== 'undefined') &&
				(typeof this.validation_message_cache[section_id][section_repeatable_index][field_id] !== 'undefined') &&
				(typeof this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id] !== 'undefined')
			) {

				// Recall custom validation message from cache
				obj_el.setCustomValidity(this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id]);

				// Delete from cache
				delete this.validation_message_cache[section_id][section_repeatable_index][field_id][object_row_id];
			}

			// Remove data-hidden attribute
			obj.removeAttr('data-hidden' + attr_suffix);
		}
	}

	// Form - Input mask
	$.WS_Form.prototype.form_inputmask = function() {

		var ws_this = this;

		$('[data-inputmask]', this.form_canvas_obj).each(function () {

			// Ensure inputmask is loaded
			if(typeof $(this).inputmask !== 'undefined') {

				// Initialize inputmask on field. Remove invalid event handler to avoid blur bug.
				$(this).inputmask().off('invalid');

				// Check for input mask validation
				if(typeof $(this).attr('data-inputmask-validate') !== 'undefined') {

					// Validate on change
					$(this).on('change input', function() {

						ws_this.form_inputmask_validate($(this));
					});

					// Initial validation
					ws_this.form_inputmask_validate($(this));
				}
			}
		});
	}

	// Form - Input mask - Validate
	$.WS_Form.prototype.form_inputmask_validate = function(obj) {

		if(obj.inputmask('isComplete')) {

			this.set_invalid_feedback(obj, '');

			// Input mask runs after date validity on change; clearing feedback here would drop min/max
			// (and other date rules). Re-run date checks when the mask is satisfied.
			if(
				(typeof this.field_date_validity_process === 'function') &&
				obj.is('input[type="text"][data-date-type]')
			) {

				this.field_date_validity_process(obj);
			}

		} else {

			this.set_invalid_feedback(obj);
		}
	}

	// Form - Client side validation
	$.WS_Form.prototype.form_validation = function() {

		// WS Form forms are set with novalidate attribute so we can manage that ourselves
		var ws_this = this;

		// Disable submit on enter
		if(!this.get_object_meta_value(this.form, 'submit_on_enter', false)) {

			this.form_obj.on('keydown', ':input:not(textarea)', function(e) {

				if(e.keyCode === 13) {

					e.preventDefault();
					return false;
				}
			});

			// Add to form events reset array
			this.form_events_reset.push({

				obj: this.form_obj,
				event: 'keydown'
			});
		}

		// On submit
		this.form_obj.on('submit', function(e) {

			e.preventDefault();
			e.stopPropagation();

			// Post if form validates
			ws_this.form_post_if_validated('submit');
		});

		// Add to form events reset array
		this.form_events_reset.push({

			obj: this.form_obj,
			event: 'submit'
		});
	}

	// Form - Post if validated
	$.WS_Form.prototype.form_post_if_validated = function(post_mode) {

		// Trigger
		this.trigger(post_mode + '-before');

		// If form post is locked, return
		if(this.form_post_locked) { return; }

		// Recalculate e-commerce
		if(
			this.has_ecommerce &&
			(typeof this.form_ecommerce_calculate === 'function')
		) {
			this.form_ecommerce_calculate();
		}

		// Mark form as validated
		this.form_canvas_obj.addClass(this.class_validated);

		// Check validity of form
		if(this.form_validate(this.form_obj)) {

			// Trigger
			this.trigger(post_mode + '-validate-success');

			// reCAPTCHA V2 invisible
			if(this.recaptchas_v2_invisible.length > 0) {

				// Execute (Once reCAPTCHA executes, it calls form_submit)
				this.recaptcha_v2_invisible_execute();

			// reCAPTCHA V3
			} else if(this.recaptchas_v3_default.length > 0) {

				// Execute (Once reCAPTCHA executes, it calls form_submit)
				this.recaptcha_v3_default_execute();

			// hCaptcha
			} else if(this.hcaptchas_invisible.length > 0) {

				// Execute (Once hCaptcha executes, it calls form_submit)
				this.hcaptcha_invisible_execute();

			// CaptchaFox
			} else if(this.captchafoxes_invisible.length > 0) {

				// Execute (Once CaptchaFox executes, it calls form_submit)
				this.captchafox_invisible_execute();

			} else {

				// Submit form
				this.form_post(post_mode);
			}

		} else {

			// Form validation - Field
			if(typeof this.form_validate_field === 'function') { this.form_validate_field('submit'); }
			// Trigger
			this.trigger(post_mode + '-validate-fail');
		}
	}

	// Form - Validate
	$.WS_Form.prototype.form_validate = function(form) {

		if(typeof form === 'undefined') { form = this.form_obj; }

		// Trigger rendered event
		this.trigger('validate-before');

		// Tab focussing
		var group_index_focus = false;
		var object_focus = false;

		// Get form as element
		var form_el = form[0];

		// Execute browser validation
		var form_validated = form_el.checkValidity();

		if(!form_validated) {

			var ws_this = this;

			// Get all invalid fields
			this.get_field_elements(form).each(function() {

				if(ws_this.is_invalid($(this))) {

					// Found invalid field to focus on
					object_focus = $(this);

					// Return false to exit the loop
					return false;
				}
			});

			if(object_focus !== false) {

				// Get group index
				group_index_focus = this.get_group_index(object_focus);
			}
		}

		if(typeof this.form_validate_captcha === 'function') {

			// reCAPTCHA validation
			var captcha_validate_return = this.form_validate_captcha(this.recaptchas_v2_default, 'recaptcha', form);
			if(typeof captcha_validate_return === 'object') {

				form_validated = false;
				if(object_focus === false) { object_focus = captcha_validate_return.object_focus; }
				if(group_index_focus === false) { group_index_focus = captcha_validate_return.group_index_focus; }
			}

			// hCaptcha validation
			var captcha_validate_return = this.form_validate_captcha(this.hcaptchas_default, 'hcaptcha', form);
			if(typeof captcha_validate_return === 'object') {

				form_validated = false;
				if(object_focus === false) { object_focus = captcha_validate_return.object_focus; }
				if(group_index_focus === false) { group_index_focus = captcha_validate_return.group_index_focus; }
			}

			// Turnstile validation
			var captcha_validate_return = this.form_validate_captcha(this.turnstiles_default, 'turnstile', form);
			if(typeof captcha_validate_return === 'object') {

				form_validated = false;
				if(object_focus === false) { object_focus = captcha_validate_return.object_focus; }
				if(group_index_focus === false) { group_index_focus = captcha_validate_return.group_index_focus; }
			}

			// CaptchaFox validation
			var captcha_validate_return = this.form_validate_captcha(this.captchafoxes_default, 'captchafox', form);
			if(typeof captcha_validate_return === 'object') {

				form_validated = false;
				if(object_focus === false) { object_focus = captcha_validate_return.object_focus; }
				if(group_index_focus === false) { group_index_focus = captcha_validate_return.group_index_focus; }
			}
		}

		// Process accessibility
		this.form_accessibility();

		// Focus
		if(!form_validated) {

			if(object_focus !== false) {

				// Focus object
				if(this.get_object_meta_value(this.form, 'invalid_field_focus', true)) {

					if(group_index_focus !== false) { 

						this.object_focus = object_focus;

					} else {

						object_focus.trigger('focus');
					}
				}
			}

			// Focus tab
			if(
				(typeof this.form_tab_group_index_set === 'function') &&
				(group_index_focus !== false)
			) {
				this.form_tab_group_index_set(group_index_focus);
			}
		}

		// Trigger rendered event
		this.trigger('validate-after');

		return form_validated;
	}

	// Form - Validate - Real time
	$.WS_Form.prototype.form_validate_real_time = function() {

		var ws_this = this;

		// Set up form validation events
		for(var field_index in this.field_data_cache) {

			if(!this.field_data_cache.hasOwnProperty(field_index)) { continue; }

			var field_type = this.field_data_cache[field_index].type;
			var field_type_config = $.WS_Form.field_type_cache[field_type];

			// Get events
			if(typeof field_type_config.events === 'undefined') { continue; }
			var form_validate_event = field_type_config.events.event;

			// Get field ID
			var field_id = this.field_data_cache[field_index].id;

			// Check to see if this field is submitted as an array
			var submit_array = (typeof field_type_config.submit_array !== 'undefined') ? field_type_config.submit_array : false;

			// Check to see if field is in a repeatable section
			var field_wrapper = $('div[data-type][data-id="' + this.esc_selector(field_id) + '"],input[type="hidden"][data-id-hidden="' + this.esc_selector(field_id) + '"]', this.form_canvas_obj);

			// Run through each wrapper found (there might be repeatables)
			field_wrapper.each(function() {

				// Determine section repeatable suffix
				var section_repeatable_index = $(this).attr('data-repeatable-index');
				var section_repeatable_suffix = (section_repeatable_index > 0) ? '[' + section_repeatable_index + ']' : '';

				// Build field selector
				var field_selector = ((field_type === 'hidden') ? 'input[type="hidden"]' : '') + '[name="' + ws_this.esc_selector(ws_form_settings.field_prefix + field_id + section_repeatable_suffix) + (submit_array ? '[]' : '') + '"]:not([data-init-validate-real-time])';

				// Get field object
				var field_obj = (field_type === 'hidden') ? $(field_selector, ws_this.form_canvas_obj) : $(field_selector, $(this));

				if(field_obj.length) {

					// Flag so it only initializes once
					field_obj.attr('data-init-validate-real-time', '');

					// Check if field should be bypassed
					var event_validate_bypass = (typeof field_type_config.event_validate_bypass !== 'undefined') ? field_type_config.event_validate_bypass : false;

					// Create event (Also run on blur, this prevents the mask component from causing false validation results)
					field_obj.on(form_validate_event + ' blur', function(e) {

						// Form validation
						if(!event_validate_bypass) {

							// Run validate real time processing
							ws_this.form_validate_real_time_process(false, false);
						}

						// Run calculation
						if(e.type !== 'blur') {

							var field_obj_id = $(this).attr('id');
							if(field_obj_id) {

								if(typeof ws_this.form_calc === 'function') { ws_this.form_calc(field_obj_id); }
							}
						}
					});
				}
			});
		}

		// Inline validation on change - Sections
		$('[data-wsf-section-validated-class]:not([data-wsf-section-validated-class-init])', this.form_canvas_obj).each(function() {

			// Mark as inititalized so it doesn't initialize again
			$(this).attr('data-wsf-section-validated-class-init', '');

			// On change event
			$('input:not([type="hidden"]),textarea,select', $(this)).on('change blur', function(e) {

				// Get section wrapper
				var section_wrapper = $(this).closest('[data-wsf-section-validated-class]');

				// Get field wrapper
				var field_wrapper = ws_this.get_field_wrapper($(this));

				// If field wrapper found, add validated class
				if(field_wrapper.length) {

					var validated_class = section_wrapper.attr('data-wsf-section-validated-class');

					field_wrapper.addClass(validated_class).off(e);
				}
			});
		});

		// Inline validation on change - Fields
		$('[data-wsf-field-validated-class]:not([data-wsf-field-validated-class-init])', this.form_canvas_obj).each(function() {

			// Mark as inititalized so it doesn't initialize again
			$(this).attr('data-wsf-field-validated-class-init', '');

			// On change event
			$('input:not([type="hidden"]),textarea,select', $(this)).on('change blur', function(e) {

				// Get field wrapper
				var field_wrapper = ws_this.get_field_wrapper($(this));

				// If field wrapper found, add validated class and remove redundant attributes
				if(field_wrapper.length) {

					var validated_class = field_wrapper.attr('data-wsf-field-validated-class');

					field_wrapper.addClass(validated_class).removeAttr('data-wsf-field-validated-class data-wsf-field-validated-class-init').off(e);
				}
			});
		});

		// Initial validation fire
		// wsf-validate triggering is forced because initial field set-up (e.g. date fields) could have called this previously
		this.form_validate_real_time_process(false, true);
	}

	$.WS_Form.prototype.form_validate_real_time_process = function(conditional_initiated, wsf_validate_force) {

		// Validate
		this.form_valid = this.form_validate_silent(this.form_obj);

		// Check for form validation changes
		if(
			wsf_validate_force ||
			(
				((this.form_valid_old === null) || (this.form_valid_old !== this.form_valid)) &&
				!conditional_initiated
			)
		) {
			// Run conditional logic
			this.form_canvas_obj.trigger('wsf-validate');
		}

		// Run conditional logic
		if(!conditional_initiated) { this.form_canvas_obj.trigger('wsf-validate-silent'); }

		// Remember state
		this.form_valid_old = this.form_valid;

		// Execute hooks and pass form_valid to them
		for(var hook_index in this.form_validation_real_time_hooks) {

			if(!this.form_validation_real_time_hooks.hasOwnProperty(hook_index)) { continue; }

			var hook = this.form_validation_real_time_hooks[hook_index];

			if(typeof hook === 'undefined') {

				delete(this.form_validation_real_time_hooks[hook_index]);

			} else {

				hook(this.form_valid, this.form, this.form_id, this.form_instance_id, this.form_obj, this.form_canvas_obj);
			}
		}

		// Form validation - Field
		if(typeof this.form_validate_field === 'function') { this.form_validate_field('real_time'); }
		return this.form_valid;
	}

	$.WS_Form.prototype.form_validate_real_time_register_hook = function(hook) {

		this.form_validation_real_time_hooks.push(hook);
	}

	// Form - Validate - Silent
	$.WS_Form.prototype.form_validate_silent = function(form) {

		// Get form as element
		var form_el = form[0];

		// Accessibility
		this.form_accessibility(form);

		// Execute browser validation
		var form_validated = form_el.checkValidity();
		if(!form_validated) { return false; }

		if(typeof this.form_validate_silent_captchas === 'function') {

			// Validate reCAPTCHAs
			if(!this.form_validate_silent_captchas(this.recaptchas_v2_default, 'recaptcha', form)) {

				return false;
			}

			// Validate hCaptchas
			if(!this.form_validate_silent_captchas(this.hcaptchas_default, 'hcaptcha', form)) {

				return false;
			}

			// Validate Turnstiles
			if(!this.form_validate_silent_captchas(this.turnstiles_default, 'turnstile', form)) {

				return false;
			}

			// Validate CaptchaFoxes
			if(!this.form_validate_silent_captchas(this.captchafoxes_default, 'captchafox', form)) {

				return false;
			}
		}

		return true;
	}

	// Form accessibility
	$.WS_Form.prototype.form_accessibility = function(obj) {

		var ws_this = this;

		if(typeof obj === 'undefined') { obj = this.form_canvas_obj; }

		// Process ARIA attributes

		// aria-invalid should only be shown when fields are validated
		// Validated fields have a parent element with the framework validated class

		// If the form is validated, include all fields
		// Otherwise only include fields within validated groups, sections or fields
		var selector_prefix = (obj.is(this.selector_validated) ? '' : (this.selector_validated + ' '));
		var selector = selector_prefix + 'input,' + selector_prefix + 'select,' + selector_prefix + 'textarea';
		var objs_validated = $(selector, obj).filter(':not([data-hidden],[data-hidden-section],[data-hidden-group],[disabled],[type="hidden"])');

		// We have to use filters to overcome a jQuery bug that causes a syntax error for :invalid in certain browsers)
		objs_validated.filter('[aria-invalid="true"]').each(function() {

			if(ws_this.is_valid($(this))) {

				// aria-invalid - Remove
				$(this).removeAttr('aria-invalid');

				// aria-describedby - Remove invalid feedback ID
				ws_this.attribute_remove_item($(this), 'aria-describedby', ws_this.get_invalid_feedback_id($(this)));

				// aria-hidden - Add to invalid feedback ID
				var invalid_feedback_obj = ws_this.get_invalid_feedback_obj($(this));
				invalid_feedback_obj.attr('aria-hidden', 'true');
			}
		});

		objs_validated.filter(':not([aria-invalid="true"])').each(function() {

			if(ws_this.is_invalid($(this))) {

				// aria-invalid - Add
				$(this).attr('aria-invalid', 'true');

				// aria-describedby - Add invalid feedback ID
				ws_this.attribute_add_item($(this), 'aria-describedby', ws_this.get_invalid_feedback_id($(this)));

				// aria-hidden - Remove from invalid feedback ID
				var invalid_feedback_obj = ws_this.get_invalid_feedback_obj($(this));
				invalid_feedback_obj.removeAttr('aria-hidden');
			}
		});	
	}

	// Validate any form object
	$.WS_Form.prototype.object_validate = function(obj) {

		var radio_field_processed = [];		// This ensures correct progress numbers of radios

		if(typeof obj === 'undefined') { return false; }

		var ws_this = this;

		var valid = true;

		// Get fields
		this.get_field_elements(obj).each(function() {

			// Get field
			var field = ws_this.get_field($(this));
			var field_type = field.type;

			// Get repeatable suffix
			var section_repeatable_index = ws_this.get_section_repeatable_index($(this));
			var section_repeatable_suffix = (section_repeatable_index > 0) ? '[' + section_repeatable_index + ']' : '';

			// Build field name
			var field_name = ws_form_settings.field_prefix + ws_this.get_field_id($(this)) + section_repeatable_suffix;

			// Determine field validity based on field type
			var validity = false;
			switch(field_type) {

				case 'radio' :
				case 'price_radio' :

					if(typeof radio_field_processed[field_name] === 'undefined') { 

						validity = $(this)[0].checkValidity();

					} else {

						return;
					}
					break;

				default :

					validity = $(this)[0].checkValidity();
			}

			radio_field_processed[field_name] = true;

			if(!validity) { valid = false; return false; }
		});

		return valid;
	}

	// Convert hex color to RGB values
	$.WS_Form.prototype.hex_to_hsl = function(color) {

		// Get RGB of hex color
		var rgb = this.hex_to_rgb(color);
		if(rgb === false) { return false; }

		// Get HSL of RGB
		var hsl = this.rgb_to_hsl(rgb);

		return hsl;
	}

	// Convert hex color to RGB values
	$.WS_Form.prototype.hex_to_rgb = function(color) {

		// If empty, return false
		if(color === '') { return false; }

		// Does color have a hash?
		var color_has_hash = (color[0] === '#');

		// Check
		if(color_has_hash && (color.length !== 7)) { return false; }
		if(!color_has_hash && (color.length !== 6)) { return false; }

		// Strip hash
		var color = color_has_hash ? color.slice(1) : color;

		// Get RGB values
		var r = parseInt(color.substring(0, 2), 16);
		var g = parseInt(color.substring(2, 4), 16);
		var b = parseInt(color.substring(4, 6), 16);

		return {'r': r, 'g': g, 'b': b};
	}

	// Convert RGB to HSL
	$.WS_Form.prototype.rgb_to_hsl = function(rgb) {

		if(typeof rgb.r === 'undefined') { return false; }
		if(typeof rgb.g === 'undefined') { return false; }
		if(typeof rgb.b === 'undefined') { return false; }

		var r = rgb.r;
		var g = rgb.g;
		var b = rgb.b;

		r /= 255, g /= 255, b /= 255;

		var max = Math.max(r, g, b), min = Math.min(r, g, b);
		var h, s, l = (max + min) / 2;

		if(max === min){
	
			h = s = 0;
	
		} else {
	
			var d = max - min;
			s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

			switch(max){
				case r: h = (g - b) / d + (g < b ? 6 : 0); break;
				case g: h = (b - r) / d + 2; break;
				case b: h = (r - g) / d + 4; break;
			}

			h /= 6;
		}

		return {'h': h, 's': s, 'l': l};
	}

	$.WS_Form.prototype.group_fields_reset = function(group_id, field_clear) {

		if(typeof this.group_data_cache[group_id] === 'undefined') { return false; }

		// Get group
		var group = this.group_data_cache[group_id];
		if(typeof group.sections === 'undefined') { return false; }

		// Get all fields in group
		var sections = group.sections;

		for(var section_index in sections) {

			if(!sections.hasOwnProperty(section_index)) { continue; }

			var section = sections[section_index];

			this.section_fields_reset(section.id, field_clear, false);
		}
	}

	$.WS_Form.prototype.section_fields_reset = function(section_id, field_clear, section_repeatable_index) {

		if(typeof this.section_data_cache[section_id] === 'undefined') { return false; }

		// Get section
		var section = this.section_data_cache[section_id];
		if(typeof section.fields === 'undefined') { return false; }

		// Get all fields in section
		var fields = section.fields;

		for(var field_index in fields) {

			if(!fields.hasOwnProperty(field_index)) { continue; }

			var field = fields[field_index];
			var field_id = field.id;

			if(section_repeatable_index === false) {

				var object_selector_wrapper = '[id^="' + this.esc_selector(this.form_id_prefix + 'field-wrapper-' + field_id) + '"][data-id="' + this.esc_selector(field.id) + '"]';

			} else {

				var object_selector_wrapper = '#' + this.form_id_prefix + 'field-wrapper-' + field_id + '-repeat-' + section_repeatable_index;
			}

			var obj_wrapper = $(object_selector_wrapper, this.form_canvas_obj);

			this.field_reset(field_id, field_clear, obj_wrapper);
		}
	}

	$.WS_Form.prototype.field_reset = function(field_id, field_clear, obj_wrapper) {

		var ws_this = this;

		if(typeof obj_wrapper === 'undefined') { obj_wrapper = false; }

		if(typeof this.field_data_cache[field_id] === 'undefined') { return; }

		var field = this.field_data_cache[field_id];

		var field_type_config = $.WS_Form.field_type_cache[field.type];
		var trigger_action = (typeof field_type_config.trigger !== 'undefined') ? field_type_config.trigger : 'change';

		switch(field.type) {

			case 'select' :
			case 'price_select' :

				$('option', obj_wrapper).each(function() {

					var selected_new = field_clear ? false : $(this).prop('defaultSelected');
					var trigger = $(this).prop('selected') !== selected_new;
					$(this).prop('selected', selected_new);
					if(trigger) { $(this).trigger(trigger_action); }
				});
				break;

			case 'checkbox' :
			case 'price_checkbox' :

				$('input[type="checkbox"]', obj_wrapper).each(function() {

					var checked_new = field_clear ? false : $(this).prop('defaultChecked');
					var trigger = $(this).prop('checked') !== checked_new;
					$(this).prop('checked', checked_new);
					if(trigger) { $(this).trigger(trigger_action); }
				});
				break;

			case 'radio' :
			case 'price_radio' :

				$('input[type="radio"]', obj_wrapper).each(function() {

					var checked_new = field_clear ? false : $(this).prop('defaultChecked');
					var trigger = $(this).prop('checked') !== checked_new;
					$(this).prop('checked', checked_new);
					if(trigger) { $(this).trigger(trigger_action); }
				});
				break;

			case 'textarea' :

				$('textarea', obj_wrapper).each(function() {

					var val_new = field_clear ? '' : $(this).prop('defaultValue');
					var trigger = $(this).val() !== val_new;
					$(this).val(val_new);
					if(typeof ws_this.textarea_set_value === 'function') { ws_this.textarea_set_value($(this), val_new); }
					if(trigger) { $(this).trigger('change'); }
				});
				break;

			case 'color' :

				$('input', obj_wrapper).each(function() {

					var val_new = field_clear ? '' : $(this).prop('defaultValue');
					var trigger = $(this).val() !== val_new;
					$(this).val(val_new);
					if(trigger) {
						$(this).trigger('change');

						if(typeof Coloris !== 'undefined') {

							$(this)[0].dispatchEvent(new Event('input', { bubbles: true }));
						}
					}
				});
				break;

			case 'hidden' :

				// Hidden fields don't have a wrapper so the obj_wrapper is the field. You cannot use the defaultValue property on hidden fields as it gets update when val() is used, so we use data-default-value attribute instead.
				var val_new = field_clear ? '' : obj_wrapper.attr('data-default-value');
				var trigger = obj_wrapper.val() !== val_new;
				obj_wrapper.val(val_new);
				if(trigger) { obj_wrapper.trigger(trigger_action); }
				break;

			case 'googlemap' :

				$('input', obj_wrapper).each(function() {

					var val_new = field_clear ? '' : $(this).attr('data-default-value');
					var trigger = $(this).val() !== val_new;
					$(this).val(val_new);
					if(trigger) { $(this).trigger(trigger_action); }
				});
				break;

			case 'file' :

				// Regular file uploads
				$('input[type="file"]', obj_wrapper).each(function() {

					var trigger = $(this).val() !== '';
					$(this).val('');
					if(trigger) { $(this).trigger(trigger_action); }
				});

				if(
					(typeof ws_this.form_file_dropzonejs_populate === 'function') &&
					(typeof Dropzone !== 'undefined')
				) {


					$('input[data-file-type="dropzonejs"]', obj_wrapper).each(function() {

						var val_old = $(this).val();

						ws_this.form_file_dropzonejs_populate($(this), field_clear);

						if($(this).val() !== val_old) { $(this).trigger(trigger_action); }
					});
				}

				break;

			default :

				$('input', obj_wrapper).each(function() {

					var val_new = field_clear ? '' : $(this).prop('defaultValue');
					var trigger = $(this).val() !== val_new;
					$(this).val(val_new);
					if(trigger) { $(this).trigger(trigger_action); }
				});
		}
	}

	// Form - Post
	$.WS_Form.prototype.form_post = function(post_mode, row_id_filter) {

		if(typeof post_mode === 'undefined') { post_mode = 'save'; }
		if(typeof row_id_filter === 'undefined') { row_id_filter = 0; }

		// Determine if this is a submit
		var submit = (post_mode === 'submit');

		// Trigger post mode event
		this.trigger(post_mode);

		var ws_this = this;

		// Build form data
		this.form_add_hidden_input('wsf_form_id', this.form_id);
		this.form_add_hidden_input('wsf_hash', this.hash);
		if(ws_form_settings.wsf_nonce) {

			this.form_add_hidden_input(ws_form_settings.wsf_nonce_field_name, ws_form_settings.wsf_nonce);
		}

		// Tracking - Duration (If disabled, cookie and hidden field are not set to respect privacy settings)
		var duration_tracking = (this.get_object_meta_value(this.form, 'tracking_duration', '') === 'on');
		if(duration_tracking) {

			this.form_add_hidden_input('wsf_duration', Math.round((new Date().getTime() - this.date_start) / 1000));
		}

		// Repeatable section hidden field
		if(typeof this.section_repeatable_hidden_field === 'function') { this.section_repeatable_hidden_field(); }

		// Reset date start
		if(post_mode === 'submit') {

			this.form_timer_reset();

			this.form_timer();
		}

		if((typeof ws_form_settings.post_id !== 'undefined') && (ws_form_settings.post_id > 0)) {

			this.form_add_hidden_input('wsf_post_id', ws_form_settings.post_id);
		}

		// Post mode
		this.form_add_hidden_input('wsf_post_mode', post_mode);

		// Work out which fields are hidden
		var hidden_array = $('[data-hidden],[data-hidden-section],[data-hidden-group]', this.form_canvas_obj).filter(':not([data-hidden-bypass])').map(function() {

			// Get name
			var name = $(this).attr('name');
			if(typeof name === 'undefined') {

				var name = $(this).attr('data-name');
				if(typeof name === 'undefined') {

					return '';
				}
			}

			// Strip brackets (For select, radio and checkboxes)
			name = name.replace('[]', '');

			// For radio and checkboxes, we should only mark it as hidden if all the rows are hidden
			switch(ws_this.get_field_type($(this))) {

				case 'checkbox' :
				case 'price_checkbox' :

					if(ws_this.get_checkbox_row_count($(this))) { return ''; }
					break;

				case 'radio' :
				case 'price_radio' :

					if(ws_this.get_radio_row_count($(this))) { return ''; }
					break;
			}

			return name;

		}).get();
		hidden_array = hidden_array.filter(function(value, index, self) { 

			return (

				(self.indexOf(value) === index) &&
				(value !== '')
			);
		});
		var hidden = hidden_array.join();
		this.form_add_hidden_input('wsf_hidden', hidden);

		// Work out which required fields to bypass (because they are hidden) or no longer required because of conditional logic
		var bypass_required_array = $('[data-required-bypass],[data-required-bypass-section],[data-required-bypass-group],[data-conditional-logic-bypass]', this.form_canvas_obj).map(function() {

			// Get name
			var name = $(this).attr('name');

			// Strip brackets (For select, radio and checkboxes)
			name = name.replace('[]', '');

			return name;

		}).get();
		bypass_required_array = bypass_required_array.filter(function(value, index, self) { 

			return self.indexOf(value) === index;
		});
		var bypass_required = bypass_required_array.join();
		this.form_add_hidden_input('wsf_bypass_required', bypass_required);

		// Submit hidden fields (from wsf_submit_hidden_fields hook)
		if(this.form.submit_hidden_fields && (typeof this.form.submit_hidden_fields === 'object')) {

			for(var submit_hidden_field_index in this.form.submit_hidden_fields) {

				if(!this.form.submit_hidden_fields.hasOwnProperty(submit_hidden_field_index)) { continue; }

				var submit_hidden_field = this.form.submit_hidden_fields[submit_hidden_field_index];

				// Check for minimum requirements
				if(
					(typeof submit_hidden_field.name === 'undefined') ||
					(typeof submit_hidden_field.value === 'undefined') ||
					(typeof submit_hidden_field.type === 'undefined')
				) {
					continue;
				}

				// Process by type
				switch(submit_hidden_field.type) {

					case 'local_storage' :

						submit_hidden_field.value = this.local_storage_get_raw(submit_hidden_field.value);
						break;
				}

				// Check for ID
				if(typeof submit_hidden_field.id === 'undefined') {

					submit_hidden_field.id = false;
				}

				// Check for attributes
				if(typeof submit_hidden_field.attributes === 'undefined') {

					submit_hidden_field.attributes = false;
				}

				// Add hidden field
				this.form_add_hidden_input(

					submit_hidden_field.name,
					submit_hidden_field.value,
					submit_hidden_field.id,
					submit_hidden_field.attributes
				);
			}
		}

		// Signatures
		if(typeof this.signature_form_post === 'function') { this.signature_form_post(); }

		// Clear action processing
		$('input[type="hidden"][name="wsf_actions_run[]"]', this.form_canvas_obj).remove();

		// Action processing (Conditional)
		var actions_run = [];
		if(this.conditional_actions_changed) {

			if(!submit && (this.conditional_actions_run_save.length > 0)) { actions_run = this.conditional_actions_run_save; }
			if(submit && (this.conditional_actions_run_submit.length > 0)) { actions_run = this.conditional_actions_run_submit; }

			for(var actions_run_index in actions_run) {

				if(!actions_run.hasOwnProperty(actions_run_index)) { continue; }

				var action_run_id = actions_run[actions_run_index];
				// for...in keys are strings; indexOf returns a number — compare numeric index (do not use === against the raw string key)
				if(actions_run.indexOf(action_run_id) === parseInt(actions_run_index, 10)) { this.form_add_hidden_input('wsf_actions_run[]', action_run_id); }
			}

			// Run no actions
			if($('input[type="hidden"][name="wsf_actions_run[]"]', this.form_canvas_obj).length === 0) {

				this.form_add_hidden_input('wsf_actions_run[]', -1);
			}
		}

		// POST form
		this.log(submit ? 'log_form_submit' : 'log_form_save');

		// Do not run AJAX
		if(
			submit &&
			(row_id_filter === 0) &&
			(this.form_ajax === false)
		) {

			// We're done!
			this.form_hash_clear();
			this.trigger(post_mode + '-complete');
			this.trigger('complete');
			return;
		}

		// Lock form
		this.form_post_lock('progress', false, false, true);

		// Show loader
		if(typeof this.form_loader_show === 'function') { this.form_loader_show(post_mode); }

		// Trigger
		ws_this.trigger(post_mode + '-before-ajax');

		// Build form data
		var form_data = new FormData(this.form_obj[0]);

		// Row ID filter (Inject into form_data so that it doesn't stay on the form)
		if(row_id_filter > 0) {

			form_data.append('wsf_row_id_filter', row_id_filter);
		}

		// If this is not a submit, there are some form data elements we should remove to avoid conflicting with WooCommerce
		if(post_mode !== 'submit') {

			// WooCommerce
			form_data.delete('quantity');
			form_data.delete('add-to-cart');
			form_data.delete('product_id');
			form_data.delete('variation_id');
		}

		// ITI tel field processing
		if(typeof this.form_tel_post === 'function') { this.form_tel_post(form_data); }

		// Media capture
		if(typeof this.form_media_capture_post === 'function') { this.form_media_capture_post(form_data); }

		// Call API
		this.api_call('submit', 'POST', form_data, function(response) {

			// Reset captchas on submit (don't do this on save as it can break immediate resubmits such as Stripe)
			if(submit) {

				// Reset reCAPTCHA
				if(typeof ws_this.recaptcha_reset === 'function') { ws_this.recaptcha_reset(); }

				// Reset hCaptcha
				if(typeof ws_this.hcaptcha_reset === 'function') { ws_this.hcaptcha_reset(); }

				// Reset Turnstile
				if(typeof ws_this.turnstile_reset === 'function') { ws_this.turnstile_reset(); }

				// Reset CaptchaFox
				if(typeof ws_this.captchafox_reset === 'function') { ws_this.captchafox_reset(); }
			}

			// Check for validation errors
			var error_validation = (typeof response.error_validation !== 'undefined') && response.error_validation;

			// Check for errors
			var errors = (

				(typeof response.data !== 'undefined') &&
				(typeof response.data.errors !== 'undefined') &&
				response.data.errors.length
			);

			var action_redirect_found = false;

			// Is a redirect action about to run?
			if(
				(typeof response.data === 'object') &&
				(typeof response.data.js === 'object')
			) {

				var js_actions = response.data.js;

				for(var js_actions_index in js_actions) {

					if(!js_actions.hasOwnProperty(js_actions_index)) { continue; }

					var js_action = js_actions[js_actions_index];

					var action = ws_this.js_action_get_parameter(js_action, 'action');

					if(action === 'redirect') {

						action_redirect_found = true;
						break;
					}
				}
			}

			if(!action_redirect_found) {

				// If response is invalid or form is being saved, force unlock it
				var form_post_unlock_force = (

					(typeof response.data === 'undefined') ||
					(post_mode === 'save') ||
					error_validation ||
					errors
				);

				// Unlock form
				ws_this.form_post_unlock('progress', !form_post_unlock_force, form_post_unlock_force, true);

				// Hide loader
				if(typeof ws_this.form_loader_hide === 'function') { ws_this.form_loader_hide(!form_post_unlock_force); }
			}

			// Trigger error event
			if(errors || error_validation) {

				// Trigger error
				ws_this.trigger(post_mode + '-error');
				ws_this.trigger('error');

			} else {

				// Trigger success
				ws_this.trigger(post_mode + '-success');
				ws_this.trigger('success');
			}

			// Check for form reload on submit
			if(
				submit &&
				!error_validation &&
				!errors
			) {

				// Clear hash
				ws_this.form_hash_clear();

				if(
					ws_this.get_object_meta_value(ws_this.form, 'submit_reload', true) &&
					!action_redirect_found
				) {

					// Reload
					ws_this.form_reload();
				}
			}

			// Show error messages
			if(errors && ws_this.get_object_meta_value(ws_this.form, 'submit_show_errors', true)) {

				for(var error_index in response.data.errors) {

					if(!response.data.errors.hasOwnProperty(error_index)) { continue; }

					var error_message = response.data.errors[error_index];
					ws_this.action_message(error_message);
				}
			}

			ws_this.trigger(post_mode + '-complete');
			ws_this.trigger('complete');

			return !errors;

		}, function(response) {

			// Error

			// Unlock form
			ws_this.form_post_unlock('progress', true, true, true);

			// Hide loader
			if(typeof ws_this.form_loader_hide === 'function') { ws_this.form_loader_hide(true); }

			// Reset reCAPTCHA
			if(typeof ws_this.recaptcha_reset === 'function') { ws_this.recaptcha_reset(); }

			// Reset hCaptcha
			if(typeof ws_this.hcaptcha_reset === 'function') { ws_this.hcaptcha_reset(); }

			// Reset Turnstile
			if(typeof ws_this.turnstile_reset === 'function') { ws_this.turnstile_reset(); }

			// Reset CaptchaFox
			if(typeof ws_this.captchafox_reset === 'function') { ws_this.captchafox_reset(); }

			// Show error message
			if(typeof response.error_message !== 'undefined') {

				ws_this.action_message(response.error_message);
			}

			// Trigger post most complete event
			ws_this.trigger(post_mode + '-error');
			ws_this.trigger('error');

		}, (row_id_filter > 0) || !submit);
	}

	// Form lock
	$.WS_Form.prototype.form_post_lock = function(cursor, force, ecommerce_calculate_disable, button_selector) {

		if(typeof cursor === 'undefined') { cursor = 'progress'; }
		if(typeof force === 'undefined') { force = false; }
		if(typeof ecommerce_calculate_disable === 'undefined') { ecommerce_calculate_disable = false; }
		if(typeof button_selector === 'undefined') { button_selector = false; }

		// Start lock timer
		this.form_post_lock_start = new Date();

		// Get lock class
		var class_lock = this.get_form_post_lock_class(button_selector);

		// If already locked, skip
		if(this.form_obj.hasClass(class_lock)) { return; }

		if(force || this.get_object_meta_value(this.form, 'submit_lock', false)) {

			// Stop further calculations
			if(ecommerce_calculate_disable) {

				this.form_ecommerce_calculate_enabled = false;
			}

			// Get buttons
			var button_objs = this.get_form_post_lock_button_objs(button_selector);

			// Check if already disabled
			button_objs.each(function() {

				if(typeof $(this).attr('disabled') !== 'undefined') {

					$(this).attr('data-form-lock-disabled-bypass', '');

				} else {

					$(this).prop('disabled', true);
				}
			})

			// Add locked class to form
			this.form_obj.addClass(class_lock + (cursor ? ' wsf-form-post-lock-' + cursor : ''));

			// Lock form
			this.form_post_locked = true;

			// Trigger lock event
			this.trigger('lock');

			// Add to log
			this.log('log_submit_lock', '', 'duplicate-protect');
		}
	}

	// Form unlock
	$.WS_Form.prototype.form_post_unlock = function(cursor, timeout, force, button_selector) {

		if(typeof cursor === 'undefined') { cursor = 'progress'; }
		if(typeof timeout === 'undefined') { timeout = true; }
		if(typeof force === 'undefined') { force = false; }
		if(typeof button_selector === 'undefined') { button_selector = false; }

		var class_lock = this.get_form_post_lock_class(button_selector);

		if(!this.form_obj.hasClass(class_lock)) { return; }

		var ws_this = this;

		var unlock_fn = function() {

			// Re-enable cart calculations
			ws_this.form_ecommerce_calculate_enabled = true;

			// Remove locked class from form
			ws_this.form_obj.removeClass(class_lock + (cursor ? ' wsf-form-post-lock-' + cursor : ''));

			// Get buttons
			var button_objs = ws_this.get_form_post_lock_button_objs(button_selector);

			// Enable buttons
			button_objs.each(function() {

				if(typeof $(this).attr('data-form-lock-disabled-bypass') !== 'undefined') {

					$(this).removeAttr('data-form-lock-disabled-bypass');

				} else {

					$(this).prop('disabled', false);
				}
			});

			// Unlock form
			ws_this.form_post_locked = false;

			// Reset post upload progress indicators
			if(typeof ws_this.form_progress_api_call_reset === 'function') { ws_this.form_progress_api_call_reset(); }

			// Trigger unlock event
			ws_this.trigger('unlock');

			// Add to log
			ws_this.log('log_submit_unlock', '', 'duplicate-protect');

			// Run form validation
			ws_this.form_validate_real_time_process(false, false);
		}

		if(force || this.get_object_meta_value(this.form, 'submit_unlock', false)) {

			// Calculate timeout. Form will not be locked longer than form_post_lock_duration_max.
			var form_post_lock_duration = new Date() - this.form_post_lock_start;
			var timeout_duration = Math.max(this.form_post_lock_duration_max - form_post_lock_duration, 0);

			// Enable post buttons
			timeout ? setTimeout(function() { unlock_fn(); }, timeout_duration) : unlock_fn();
		}
	}

	// Get button objects
	$.WS_Form.prototype.get_form_post_lock_button_objs = function(button_selector) {

		// Custom button selector
		if(typeof button_selector === 'string') {

			return $(button_selector, this.form_canvas_obj);
		}

		// Build button selector
		var button_selector = 'button[type="submit"].wsf-button, input[type="submit"].wsf-button, button[data-action="wsf-save"].wsf-button, button[data-ecommerce-payment].wsf-button, [data-post-lock]' + (button_selector ? ', button[type="button"].wsf-button' : '');

		// If button selector is not a string, include custom buttons and navigation if button_selector === true
		return $(button_selector, this.form_canvas_obj);
	}

	// Get lock class
	$.WS_Form.prototype.get_form_post_lock_class = function(button_selector) {

		var class_lock = 'wsf-form-post-lock';

		if(typeof button_selector === 'string') { class_lock += '-custom-selector'; }

		return class_lock;
	}

	// API Call
	$.WS_Form.prototype.api_call = function(ajax_path, method, params, success_callback, error_callback, force_ajax_path) {

		// Defaults
		if(typeof method === 'undefined') { method = 'POST'; }
		if(!params) { params = new FormData(); }
		if(typeof force_ajax_path === 'undefined') { force_ajax_path = false; }

		var ws_this = this;

		// Timer - Start
		var timer_start = new Date();

		// Check permalinks
		if(ws_form_settings.use_rest_route) {

			var ajax_path_parts = ajax_path.split('?');
			ajax_path = encodeURIComponent(ajax_path_parts[0]) + (ajax_path_parts[1] ? '&' + ajax_path_parts[1] : '');
		}

		// Make AJAX request
		var url = force_ajax_path ? (ws_form_settings.url_ajax + ajax_path) : ((ajax_path === 'submit') ? this.form_obj.attr('action') : (ws_form_settings.url_ajax + ajax_path));

		// Check for custom action URL
		if(
			!force_ajax_path &&
			this.form_action_custom &&
			(ajax_path === 'submit')
		) {

			// Custom action submit
			this.form_obj.off('submit');
			this.form_obj.trigger('submit');
			return true;
		}

		// NONCE
		if(
			(
				(typeof params.get === 'undefined') || // Do it anyway for IE 11
				(params.get(ws_form_settings.wsf_nonce_field_name) === null)
			) &&
			(ws_form_settings.wsf_nonce)
		) {
			params.append(ws_form_settings.wsf_nonce_field_name, ws_form_settings.wsf_nonce);
		}

		// Convert FormData to object if making GET request (IE11 friendly code so not that elegant)
		if(method === 'GET') {

			var params_object = {};

			var form_data_entries = params.entries();
			var form_data_entry = form_data_entries.next();

			while (!form_data_entry.done) {

				var pair = form_data_entry.value;
				params_object[pair[0]] = pair[1];
				form_data_entry = form_data_entries.next();
			}

			params = params_object;
		}

		// Process validation focus
		this.action_js_process_validation_focus = true;

		// Call AJAX
		var ajax_request = {

			method: method,
			url: url,
			contentType: false,
			processData: (method === 'GET'),

			beforeSend: function(xhr) {

				// Nonce (X-WP-Nonce)
				if(ws_form_settings.x_wp_nonce) {

					xhr.setRequestHeader('X-WP-Nonce', ws_form_settings.x_wp_nonce);
				}
			},

			success: function(response) {

				ws_this.api_call_handler_success(response, success_callback, timer_start);
			},

			error: function(jq_xhr, text_status, error_thrown) {

				ws_this.api_call_handler_error(jq_xhr, text_status, error_thrown, success_callback, error_callback, timer_start, url);
			}
		};

		// Data
		if(params !== false) { ajax_request.data = params; }

		// Progress
		var progress_objs = $('[data-source="post_progress"]', this.form_canvas_obj);
		if(progress_objs.length) {

			ajax_request.xhr = function() {

				var xhr = new window.XMLHttpRequest();
				xhr.upload.addEventListener("progress", function(e) { ws_this.form_progress_api_call(progress_objs, e); }, false);
				xhr.addEventListener("progress", function(e) { ws_this.form_progress_api_call(progress_objs, e); }, false);
				return xhr;
			}
		}

		return $.ajax(ajax_request);
	}

	// API call - Success
	$.WS_Form.prototype.api_call_handler_success = function(response, success_callback, timer_start) {

		// Handle hash response
		var hash_ok = this.api_call_hash(response);

		// Debug
		if(hash_ok && $.WS_Form.debug_rendered) {

			// Timer - Duration
			var timer_duration = new Date() - timer_start;

			this.debug_info('debug_info_submit_count', response.data.count);
			this.debug_info('debug_info_submit_duration_user', this.get_nice_duration(response.data.submit_duration_user));
			this.debug_info('debug_info_submit_duration_client', timer_duration + ' ms');
			this.debug_info('debug_info_submit_duration_server', response.data.submit_duration_server + ' ms');
		}

		// Check for action logs and errors (These are returned from the action system to tell the debugger to log the message)
		if($.WS_Form.debug_rendered) {

			if(typeof response.data !== 'undefined') {

				if(typeof response.data.logs === 'object') {

					for(var logs_index in response.data.logs) {

						if(!response.data.logs.hasOwnProperty(logs_index)) { continue; }

						this.log('log_action', response.data.logs[logs_index], 'action');
					}
				}

				if(typeof response.data.errors === 'object') {

					for(var errors_index in response.data.errors) {

						if(!response.data.errors.hasOwnProperty(errors_index)) { continue; }

						// Get error message
						var error_message = response.data.errors[errors_index];

						// Check error message and log error
						if(error_message) {

							this.error('error_action', error_message, 'action');

						} else {

							this.error('error_action_no_message', '', 'action');
						}
					}
				}
			}
		}
		// Check for new nonce values
		if(typeof response.x_wp_nonce !== 'undefined') { ws_form_settings.x_wp_nonce = response.x_wp_nonce; }
		if(typeof response.wsf_nonce !== 'undefined') { ws_form_settings.wsf_nonce = response.wsf_nonce; }

		// Call success function
		var success_callback_result = (typeof success_callback === 'function') ? success_callback(response) : true;

		// Check for data to process
		if(
			(typeof response.data !== 'undefined') &&
			success_callback_result
		) {

			// Check for action_js (These are returned from the action system to tell the browser to do something)
			if(typeof response.data.js === 'object') { this.action_js_init(response.data.js); }
		}

		return true;
	}

	// API call - Process AJAX error
	$.WS_Form.prototype.api_call_handler_error = function(jq_xhr, text_status, error_thrown, success_callback, error_callback, timer_start, url) {

		// Get response JSON
		var response_json = (typeof jq_xhr.responseJSON !== 'undefined') ? jq_xhr.responseJSON : false;

		// Get status
		var status = jq_xhr.status;

		// Error message
		var error_message = this.language('error_api_call_unknown');

		// Process by status code
		switch(status) {

			case 200:

				// Process error thrown
				if(typeof error_thrown === 'string') {

					error_message = this.error('error_api_call_response_error', error_thrown);

				} else if(error_thrown instanceof Error) {

					error_message = this.error('error_api_call_response_error', error_thrown.message);

				} else {

					try {

						error_message = this.error('error_api_call_response_error', JSON.stringify(error_thrown));

					} catch (e) {

						error_message = this.error('error_api_call_response_error', this.language('error_api_call_unknown'));
					}
				}

				// Log response text
				this.error('error_api_call_response_text', this.esc_html(jq_xhr.responseText));

				// Build response
				response_json = {

					error: true,
					error_message: error_message
				};

				break;

			case 400:
			case 401:
			case 403:
			case 404:
			case 500:

				// Process WS Form API error message
				if(response_json && response_json.error) {

					if(response_json.error_message) {

						error_message = this.error('error_api_call_' + status, response_json.error_message);

					} else {

						error_message = this.error('error_api_call_' + status, url);
					}

				} else {

					// Fallback
					error_message = this.error('error_api_call_' + status, url);

					// Build response
					response_json = {

						error: true,
						error_message: error_message
					};
				}

				break;
		}

		// Call error callback
		if(typeof error_callback === 'function') {

			// Run error callback
			error_callback(response_json);
		}
	}

	// API Call
	$.WS_Form.prototype.api_call_hash = function(response) {

		// Check if hash exists in response
		if(typeof response.hash !== 'string') { return false; }

		// Get hash
		var hash = response.hash;

		// Check for a hash clear that is returned if a hash cannot be read from the DB
		if(hash === 'clear') {

			// Add to log
			this.log('log_hash_not_found');
			// Clear hash
			this.form_hash_clear();

			return false;
		}

		// Check hash length
		if(hash.length !== 32) { return false; }

		// Check hash with regex
		var hash_regex = /^[a-fA-F0-9]{32}$/gi;
		if(!hash_regex.test(hash)) { return false; }

		// Set hash
		this.hash_set(hash);

		return true;
	}

	// Hash - Set
	$.WS_Form.prototype.hash_set = function(hash, token, cookie_set) {

		if(typeof token === 'undefined') { token = false; }
		if(typeof cookie_set === 'undefined') { cookie_set = false; }

		if(hash !== this.hash) {

			// Set hash
			this.hash = hash;

			// Set hash cookie
			cookie_set = true;

			// Add to log
			this.log('log_hash_set', this.hash);

			// Debug
			if($.WS_Form.debug_rendered) { this.debug_info('debug_info_hash', this.hash, 'clear_hash'); }
		}

		if(token) {

			// Set token
			this.token = token;

			// Add to log
			this.log('log_token_set', this.token);
		}

		if(cookie_set) {

			var cookie_hash = this.get_object_value($.WS_Form.settings_plugin, 'cookie_hash');

			if(cookie_hash) {

				this.cookie_set('hash', this.hash);
			}
		}
	}

	// JS Actions - Init
	$.WS_Form.prototype.action_js_init = function(action_js) {

		// Trigger actions start event
		this.trigger('actions-start');

		this.action_js = action_js;

		this.action_js_process_next();
	}

	$.WS_Form.prototype.action_js_process_next = function() {

		if(this.action_js.length === 0) {

			// Trigger actions finish event
			this.trigger('actions-finish');

			return false;
		}

		var js_action = this.action_js.shift();

		var action = this.js_action_get_parameter(js_action, 'action');

		switch(action) {

			// Redirect
			case 'redirect' :

				var url = this.js_action_get_parameter(js_action, 'url');
				if(url !== false) { location.href = js_action.url; }

				// Actions end at this point because of the redirect
				return true;

			// Message
			case 'message' :

				var message = this.js_action_get_parameter(js_action, 'message');
				var type = this.js_action_get_parameter(js_action, 'type');
				var method = this.js_action_get_parameter(js_action, 'method');
				var duration = this.js_action_get_parameter(js_action, 'duration');
				var form_hide = this.js_action_get_parameter(js_action, 'form_hide');
				var clear = this.js_action_get_parameter(js_action, 'clear');
				var scroll_top = this.js_action_get_parameter(js_action, 'scroll_top');
				var scroll_top_offset = this.js_action_get_parameter(js_action, 'scroll_top_offset');
				var scroll_top_duration = this.js_action_get_parameter(js_action, 'scroll_top_duration');
				var form_show = this.js_action_get_parameter(js_action, 'form_show');
				var message_hide = this.js_action_get_parameter(js_action, 'message_hide');

				this.action_message(message, type, method, duration, form_hide, clear, scroll_top, scroll_top_offset, scroll_top_duration, form_show, message_hide);

				break;
			// Conversion
			case 'conversion' :

				if(typeof this.action_conversion === 'function') {

					var type = this.js_action_get_parameter(js_action, 'type');
					var parse_values = this.js_action_get_parameter(js_action, 'parse_values');
					var data_layer_reset = this.js_action_get_parameter(js_action, 'data_layer_reset');

					this.action_conversion(type, parse_values, data_layer_reset);
				}

				break;

			// JavaScript
			case 'javascript' :

				var javascript = this.js_action_get_parameter(js_action, 'javascript');

				this.action_javascript(javascript);

				break;
			// Field invalid feedback
			case 'field_invalid_feedback' :

				var field_id = parseInt(this.js_action_get_parameter(js_action, 'field_id'), 10);
				var section_repeatable_index = parseInt(this.js_action_get_parameter(js_action, 'section_repeatable_index'), 10);
				var section_repeatable_suffix = section_repeatable_index ? '-repeat-' + section_repeatable_index : '';
				var message = this.js_action_get_parameter(js_action, 'message');

				// Field object
				var field_obj = $('#' + this.form_id_prefix + 'field-' + field_id + section_repeatable_suffix, this.form_canvas_obj);

				// Set invalid feedback
				this.set_invalid_feedback(field_obj, message);

				// Log event
				this.log('error_invalid_feedback', field_id + ' (' + this.esc_html(message) + ')');
				var ws_this = this;

				// Reset if field modified
				field_obj.one('change input', function() {

					// Reset invalid feedback
					ws_this.set_invalid_feedback($(this), '');
				});

				// Process focus?
				if(
					this.get_object_meta_value(this.form, 'invalid_field_focus', true) &&
					this.action_js_process_validation_focus
				) {

					// Get group index
					var group_index_focus = this.get_group_index(field_obj);

					// Focus object
					if(group_index_focus !== false) { 

						this.object_focus = field_obj;

					} else {

						field_obj.trigger('focus');
					}

					// Focus tab
					if(
						(typeof this.form_tab_group_index_set === 'function') &&
						(group_index_focus !== false)
					) {
						this.form_tab_group_index_set(group_index_focus);
					}

					// Prevent further focus
					this.action_js_process_validation_focus = false;
				}

				// Mark form as validated
				this.form_canvas_obj.addClass(this.class_validated);

				// Process accessibility
				this.form_accessibility();

				// Process next action
				this.action_js_process_next();

				// Form validation - Field
				if(typeof this.form_validate_field === 'function') { this.form_validate_field('field_invalid_feedback'); }
				break;

			// Field value
			case 'field_value' :

				var field_id = parseInt(this.js_action_get_parameter(js_action, 'field_id'), 10);
				var section_repeatable_index = parseInt(this.js_action_get_parameter(js_action, 'section_repeatable_index'), 10);
				var section_repeatable_suffix = section_repeatable_index ? '-repeat-' + section_repeatable_index : '';
				var value = this.js_action_get_parameter(js_action, 'value');
				var check = this.js_action_get_parameter(js_action, 'check', true);
				var append = this.js_action_get_parameter(js_action, 'append');
				var prepend = this.js_action_get_parameter(js_action, 'prepend');

				// Field wrapper object
				var obj_wrapper = $('#' + this.form_id_prefix + 'field-wrapper-' + field_id + section_repeatable_suffix, this.form_canvas_obj);

				// Field object
				var obj = $('#' + this.form_id_prefix + 'field-' + field_id + section_repeatable_suffix, this.form_canvas_obj);

				// Set value
				this.field_value_set(obj_wrapper, obj, value, check, append, prepend);

				// Log event
				this.log('log_field_value', field_id, 'field_value, append, prepend');
				// Process next action
				this.action_js_process_next();

				break;

			// Populate file field with dropzonejs
			case 'field_dropzonejs_file_objects' :

				var field_id = parseInt(this.js_action_get_parameter(js_action, 'field_id'), 10);
				var section_repeatable_index = parseInt(this.js_action_get_parameter(js_action, 'section_repeatable_index'), 10);
				var section_repeatable_suffix = section_repeatable_index ? '-repeat-' + section_repeatable_index : '';
				var file_objects = this.js_action_get_parameter(js_action, 'file_objects');

				// Field object
				var field_obj = $('#' + this.form_id_prefix + 'field-' + field_id + section_repeatable_suffix, this.form_canvas_obj);

				field_obj.attr('data-default-value', JSON.stringify(file_objects));

				// Populate DropzoneJS field
				this.form_file_dropzonejs_populate(field_obj);

				// Log event
				this.log('log_field_dropzonejs_file_objects', field_id);
				// Process next action
				this.action_js_process_next();

				break;

			case 'trigger' :

				var event = this.js_action_get_parameter(js_action, 'event');
				var params = this.js_action_get_parameter(js_action, 'params');

				$(document).trigger(event, params);

				this.action_js_process_next();

				break;
		}
	}

	// Field value set
	$.WS_Form.prototype.field_value_set = function(obj_wrapper, obj, value, check, append, prepend) {

		// Check field wrapper exists
		if(!obj_wrapper.length) {

			// Check for fields without a wrapper
			if(obj.length) {

				switch(obj.attr('type')) {

					case 'hidden' :

						var field_type = 'hidden';
						break;

					default :

						return;
				}

			} else {

				return;
			}

		} else {

			// Get object wrapper field type (We use this as the primary way of determining a field type because some wrapper don't contain child elements, e.g. Text Editor and HTML)
			var field_type = obj_wrapper.attr('data-type');
		}

		// Check inputs
		if(typeof check === 'undefined') { check = true; }
		if(typeof append === 'undefined') { append = false; }
		if(typeof prepend === 'undefined') { prepend = false; }

		// Get object node name
		var field_node_name = ((obj.length) ? obj[0].nodeName : false);

		// Process by node name
		if(typeof field_node_name === 'string') {

			switch(field_node_name.toLowerCase()) {

				case 'button' :

					// This is done for all buttons to ensure add-ons that copy buttons are included in this function
					field_type = 'button';
					break;
			}
		}

		// Process by field wrapper type
		switch(field_type) {

			case 'select' :
			case 'price_select' :

				var trigger = ($('option[value="' + this.esc_selector(value) + '"]', obj).prop('selected') !== check);
				$('option[value="' + this.esc_selector(value) + '"]', obj).prop('selected', check);
				if(trigger) { obj.trigger('change'); }

				break;

			case 'checkbox' :
			case 'price_checkbox' :
			case 'radio' :
			case 'price_radio' :

				var trigger = ($('input[value="' + this.esc_selector(value) + '"]', obj_wrapper).prop('checked') !== check);
				$('input[value="' + this.esc_selector(value) + '"]', obj_wrapper).prop('checked', check);
				if(trigger) { $('input[value="' + this.esc_selector(value) + '"]', obj_wrapper).trigger('change'); }

				break;

			case 'button' :

				obj.html(value);
				break;

			case 'html' :
			case 'texteditor' :
			case 'message' :

				// Append
				if(append) {

					$('[data-html],[data-text-editor]', obj_wrapper).append(value);

				// Prepend
				} else if(prepend) { 

					$('[data-html],[data-text-editor]', obj_wrapper).prepend(value);

				} else {

					$('[data-html],[data-text-editor]', obj_wrapper).html(value);
				}

				break;

			case 'color' :

				obj.attr('data-value-old', function() { return $(this).val(); }).val(value).filter(function() { return $(this).val() !== $(this).attr('data-value-old') }).trigger('change').removeAttr('data-value-old');

				if(typeof Coloris !== 'undefined') {

					obj[0].dispatchEvent(new Event('input', { bubbles: true }));
				}

				break;

			case 'price' :
			case 'cart_price' :

				// Price formatting (Ensure correctly foratted price is injected into price fields for the currency input mask)

				// Check for blank values
				if(value !== '') {

					// Check if value is a number, if not, try to convert it using current website currency format
					if(isNaN(value)) { 

						var value = this.get_number(value);
					}

					// We set the value as follows to ensure the input mask currency is set correctly
					// - No currency symbol
					// - Decimal and thousand separator characters are correct
					value = this.get_price(value, this.get_currency(), false);
				}

				// Set value
				obj.attr('data-value-old', function() { return $(this).val(); }).val(value).filter(function() { return $(this).val() !== $(this).attr('data-value-old') }).trigger('change').removeAttr('data-value-old');

				break;

			default :

				// Append
				if(append) {

					value = obj.val() + value;

				// Prepend
				} else if(prepend) { 

					value = value + obj.val();
				}

				// Set value
				obj.attr('data-value-old', function() { return $(this).val(); }).val(value).filter(function() { return $(this).val() !== $(this).attr('data-value-old') }).trigger('change').removeAttr('data-value-old');

				// If textareaa then set value (provides support for CodeMirror and TinyMCE)
				if(
					(field_type === 'textarea') &&
					(typeof this.textarea_set_value === 'function')
				) {

					this.textarea_set_value(obj, value);
				}
		}
	}

	// JS Actions - Get js_action config parameter from AJAX return
	$.WS_Form.prototype.js_action_get_parameter = function(js_action_parameters, meta_key, default_value = false) {

		return (typeof js_action_parameters[meta_key] !== 'undefined') ? js_action_parameters[meta_key] : default_value;
	}

	// JS Actions - Get framework config value
	$.WS_Form.prototype.get_framework_config_value = function(object, meta_key) {

		if(typeof this.framework[object] === 'undefined') {
			this.error('error_api_call_framework_invalid');
			return false;
		}
		if(typeof this.framework[object].public === 'undefined') {
			this.error('error_api_call_framework_invalid');
			return false;
		}
		if(typeof this.framework[object].public[meta_key] === 'undefined') { return false; }

		return this.framework[object].public[meta_key];
	}

	// JS Action - Message
	$.WS_Form.prototype.action_message = function(message, type, method, duration, form_hide, clear, scroll_top, scroll_top_offset, scroll_top_duration, form_show, message_hide) {

		// Check error message
		if(!message) { return; }

		// Error message setting defaults
		if(typeof type === 'undefined') { type = this.get_object_meta_value(this.form, 'error_type', 'danger'); }
		if(typeof method === 'undefined') { method = this.get_object_meta_value(this.form, 'error_method', 'after'); }
		if(typeof duration === 'undefined') { duration = parseInt(this.get_object_meta_value(this.form, 'error_duration', '4000'), 10); }
		if(typeof form_hide === 'undefined') { form_hide = (this.get_object_meta_value(this.form, 'error_form_hide', '') === 'on'); }
		if(typeof clear === 'undefined') { clear = (this.get_object_meta_value(this.form, 'error_clear', '') === 'on'); }
		if(typeof scroll_top === 'undefined') { scroll_top = (this.get_object_meta_value(this.form, 'error_scroll_top', '') === 'on'); }
		if(typeof scroll_top_offset === 'undefined') { scroll_top_offset = parseInt(this.get_object_meta_value(this.form, 'error_scroll_top_offset', '0'), 10); }
		scroll_top_offset = (scroll_top_offset === '') ? 0 : parseInt(scroll_top_offset, 10);
		if(typeof scroll_top_duration === 'undefined') { scroll_top_duration = parseInt(this.get_object_meta_value(this.form, 'error_scroll_top_duration', '400'), 10); }
		if(typeof form_show === 'undefined') { form_show = (this.get_object_meta_value(this.form, 'error_form_show', '') === 'on'); }
		if(typeof message_hide === 'undefined') { message_hide = (this.get_object_meta_value(this.form, 'error_message_hide', 'on') === 'on'); }

		var scroll_position = this.form_canvas_obj.offset().top - scroll_top_offset;

		// Parse duration
		duration = parseInt(duration, 10);
		if(duration < 0) { duration = 0; }

		// Get config
		var mask_wrapper = this.get_framework_config_value('message', 'mask_wrapper');
		var types = this.get_framework_config_value('message', 'types');

		var type = (typeof types[type] !== 'undefined') ? types[type] : false;
		var mask_wrapper_class = (typeof type.mask_wrapper_class !== 'undefined') ? type.mask_wrapper_class : '';

		// Clear other messages
		if(clear) {

			$('[data-wsf-message][data-wsf-instance-id="' + this.form_instance_id + '"]').remove();
		}

		// Scroll top
		switch(scroll_top) {

			case 'instant' :
			case 'on' :			// Legacy

				$('html,body').scrollTop(scroll_position);

				break;

			// Smooth
			case 'smooth' :

				scroll_top_duration = (scroll_top_duration === '') ? 0 : parseInt(scroll_top_duration, 10);

				$('html,body').animate({

					scrollTop: scroll_position

				}, scroll_top_duration);

				break;
		}

		var mask_wrapper_values = {

			'message':            message,
			'mask_wrapper_class': mask_wrapper_class
		};

		// Add style ID
		var style_id = (typeof this.form_canvas_obj.attr('data-wsf-style-id') !== 'undefined') ? this.form_canvas_obj.attr('data-wsf-style-id') : false;

		if(style_id) {

			mask_wrapper_values.style_id = style_id;
		}

		var message_div = $('<div/>', { html: this.mask_parse(mask_wrapper, mask_wrapper_values) });
		message_div.attr('role', 'alert');
		message_div.attr('data-wsf-message', '');
		message_div.attr('data-wsf-instance-id', this.form_instance_id);

		// Add style ID
		if(style_id) {

			// Check if styler is present
			if($('#wsf-styler').length) {

				// Inherit modified styles
				message_div.attr('style', this.form_canvas_obj.attr('style'));
			}
		}

		// Hide form?
		if(form_hide) {

			// Hide form
			this.form_obj.hide();

			// Clear duration timer
			this.form_timer_reset();
		}

		// Render message
		switch(method) {

			// Before
			case 'before' :

				message_div.insertBefore(this.form_obj);
				break;

			// After
			default :

				message_div.insertAfter(this.form_obj);
				break;
		}

		// Conversational adjustment
		if(this.conversational && $('.wsf-form-conversational-nav').length) {

			message_div.css('bottom', $('.wsf-form-conversational-nav').outerHeight() + 'px');
		}

		// Process next action
		var ws_this = this;

		duration = parseInt(duration, 10);

		if(duration > 0) {

			setTimeout(function() {

				// Should this message be removed?
				if(message_hide) { message_div.remove(); }

				// Should the form be shown?
				if(form_show) {

					// Show form
					ws_this.form_obj.show();
					// Start duration timer
					ws_this.form_timer();
				}

				// Process next js_action
				ws_this.action_js_process_next();

			}, duration);

		} else {

			// Process next js_action
			ws_this.action_js_process_next();
		}
	}
	// JS Action - Run JavaScript
	$.WS_Form.prototype.action_javascript = function(action_javascript) {

		try {

			// Run JavaScript
			$.globalEval("(function($) {\n" + action_javascript + "\n})(jQuery);");

			// Log event
			this.log('log_javascript', 'action');

		} catch(e) {

			this.error('error_js', action_javascript);
		}

		// Process next js_action
		this.action_js_process_next();
	}
	// Text input and textarea character and word count
	$.WS_Form.prototype.form_character_word_count = function(obj) {

		var ws_this = this;
		if(typeof obj === 'undefined') { obj = this.form_canvas_obj; }

		// Run through each input that accepts text
		for(var field_id in this.field_data_cache) {

			if(!this.field_data_cache.hasOwnProperty(field_id)) { continue; }

			var field = this.field_data_cache[field_id];

			// Process help?
			var help = this.get_object_meta_value(field, 'help', '', false, true);
			var process_help = (

				(help.indexOf('#character_') !== -1) ||
				(help.indexOf('#word_') !== -1)
			);

			// Process min or max?
			var process_min_max = (

				this.has_object_meta_key(field, 'min_length') ||
				this.has_object_meta_key(field, 'max_length') ||
				this.has_object_meta_key(field, 'min_length_words') ||
				this.has_object_meta_key(field, 'max_length_words')
			);

			if(process_min_max || process_help) {

				// Process count functionality on field
				var field_obj = $('#' + this.form_id_prefix + 'field-' + field_id, obj);
				if(!field_obj.length) { field_obj = $('[id^="' + this.form_id_prefix + 'field-' + field_id + '-"]:not([data-init-char-word-count]):not(iframe)', obj); }

				field_obj.each(function() {

					// Flag so it only initializes once
					$(this).attr('data-init-char-word-count', '');

					if(ws_this.form_character_word_count_process($(this))) {

						$(this).on('change input', function() { ws_this.form_character_word_count_process($(this)); });
					}
				});
			}
		}
	}

	// Text input and textarea character and word count - Process
	$.WS_Form.prototype.form_character_word_count_process = function(obj) {

		// Get minimum and maximum character count
		var field = this.get_field(obj);

		var min_length = this.get_object_meta_value(field, 'min_length', '');
		min_length = (parseInt(min_length, 10) > 0) ? parseInt(min_length, 10) : false;

		var max_length = this.get_object_meta_value(field, 'max_length', '');
		max_length = (parseInt(max_length, 10) > 0) ? parseInt(max_length, 10) : false;

		// Get minimum and maximum word length
		var min_length_words = this.get_object_meta_value(field, 'min_length_words', '');
		min_length_words = (parseInt(min_length_words, 10) > 0) ? parseInt(min_length_words, 10) : false;

		var max_length_words = this.get_object_meta_value(field, 'max_length_words', '');
		max_length_words = (parseInt(max_length_words, 10) > 0) ? parseInt(max_length_words, 10) : false;

		// Calculate sizes
		var val = obj.val();

		// Check value is a string
		if(typeof val !== 'string') { return; }

		var character_count = val.length;
		var character_remaining = (max_length !== false) ? max_length - character_count : false;
		if(character_remaining < 0) { character_remaining = 0; }

		var word_count = this.get_word_count(val);
		var word_remaining = (max_length_words !== false) ? max_length_words - word_count : false;
		if(word_remaining < 0) { word_remaining = 0; }

		// Check minimum and maximums counts
		var count_invalid = false;
		var count_invalid_message_array = [];

		if((min_length !== false) && (character_count < min_length)) {

			count_invalid_message_array.push(this.language('error_min_length', min_length));
			count_invalid = true;
		}
		if((max_length !== false) && (character_count > max_length)) {

			count_invalid_message_array.push(this.language('error_max_length', max_length));
			count_invalid = true;
		}
		if((min_length_words !== false) && (word_count < min_length_words)) {

			count_invalid_message_array.push(this.language('error_min_length_words', min_length_words));
			count_invalid = true;
		}
		if((max_length_words !== false) && (word_count > max_length_words)) {

			count_invalid_message_array.push(this.language('error_max_length_words', max_length_words));
			count_invalid = true;
		}

		// Check if required
		if(
			(typeof obj.attr('required') !== 'undefined') ||
			(val.length > 0)
		) {

			// Check if count_invalid
			if(count_invalid) {

				// Set invalid feedback
				this.set_invalid_feedback(obj, count_invalid_message_array.join(' / '));

			} else {

				// Reset invalid feedback
				this.set_invalid_feedback(obj, '');
			}

		} else {

			// Reset invalid feedback
			this.set_invalid_feedback(obj, '');
		}

		// Process form bypass
		this.form_bypass();

		// Process help
		var help = this.get_object_meta_value(field, 'help', '', false, true);

		// If #character_ and #word_ not present, don't bother processing
		if(
			(help.indexOf('#character_') === -1) &&
			(help.indexOf('#word_') === -1)
		) {
			return true;
		}

		// Get language
		var character_singular = this.language('character_singular');
		var character_plural = this.language('character_plural');
		var word_singular = this.language('word_singular');
		var word_plural = this.language('word_plural');

		// Set mask values
		var mask_values_help = {

			// Characters
			'character_count':				character_count,
			'character_count_label':		(character_count === 1 ? character_singular : character_plural),
			'character_remaining':			(character_remaining !== false) ? character_remaining : '',
			'character_remaining_label':	(character_remaining === 1 ? character_singular : character_plural),
			'character_min':				(min_length !== false) ? min_length : '',
			'character_min_label':			(min_length !== false) ? (min_length === 1 ? character_singular : character_plural) : '',
			'character_max':				(max_length !== false) ? max_length : '',
			'character_max_label':			(max_length !== false) ? (max_length === 1 ? character_singular : character_plural) : '',

			// Words
			'word_count':			word_count,
			'word_count_label':		(word_count === 1 ? word_singular : word_plural),
			'word_remaining':		(word_remaining !== false) ? word_remaining : '',
			'word_remaining_label': (word_remaining === 1 ? word_singular : word_plural),
			'word_min':				(min_length_words !== false) ? min_length_words : '',
			'word_min_label':		(min_length_words !== false) ? (min_length_words === 1 ? word_singular : word_plural) : '',
			'word_max':				(max_length_words !== false) ? max_length_words : '',
			'word_max_label':		(max_length_words !== false) ? (max_length_words === 1 ? word_singular : word_plural) : ''
		};

		// Parse help mask
		var help_parsed = this.mask_parse(help, mask_values_help);

		// Update help HTML
		var help_obj = this.get_help_obj(obj);
		help_obj.html(help_parsed);

		return true;
	}

	// Get word count of a string
	$.WS_Form.prototype.get_word_count = function(input_string) {

		// Trim input string
		input_string = input_string.trim();

		// If string is empty, return 0
		if(input_string.length === 0) { return 0; }

		// Return word count
		return input_string.trim().replace(/\s+/gi, ' ').split(' ').length;
	}

	// Form - Statistics
	$.WS_Form.prototype.form_stat = function() {

		// Add view
		if(ws_form_settings.stat) { this.form_stat_add_view(); }
	}

	// Add view statistic
	$.WS_Form.prototype.form_stat_add_view = function() {

		if(!this.form_stat_add_lock) {

			// Call AJAX
			$.ajax({ method: 'POST', url: ws_form_settings.add_view_url, data: { wsffid: this.form_id } });

			// Prevent further calls
			this.form_stat_add_lock = true;
		}
	}

	// Google Maps API JS - Await
	$.WS_Form.prototype.form_google_maps_js_api_await = function(field_type) {

		var ws_this = this;

		var total_ms_start = new Date().getTime();

		return new Promise(function(resolve) {

			function check() {

				// Timeout check
				if((new Date().getTime() - total_ms_start) > ws_this.timeout_google_maps) {

					ws_this.error('error_timeout_google_maps_api_js', field_type);

					return resolve(false);
				}

				// Base check
				var not_loaded = (!window.google || !window.google.maps);

				switch($.WS_Form.settings_plugin.google_maps_js_api_version) {

					case '2' :

						// Only do base check, library loading done after this
						break;

					default :

						switch(field_type) {

							case 'googleaddress' :

								not_loaded = not_loaded || (

									!window.google.maps.Geocoder ||
									!window.google.maps.places ||
									!window.google.maps.places.Autocomplete
								);

								break;

							case 'googlemap' :

								not_loaded = not_loaded || (

									!window.google ||
									!window.google.maps ||
									!window.google.maps.Geocoder ||
									!window.google.maps.marker ||
									!window.google.maps.marker.AdvancedMarkerElement ||
									!window.google.maps.places ||
									!window.google.maps.places.SearchBox
								);

								break;

							case 'googleroute' :

								// Routes API (New) loads via importLibrary
								if($.WS_Form.settings_plugin.google_maps_routes_api_version !== '2') {

									not_loaded = not_loaded || (

										!window.google ||
										!window.google.maps ||
										!window.google.maps.DirectionsRenderer ||
										!window.google.maps.DirectionsService
									);
								}

								break;
						}
				}

				if(not_loaded) {

					// Not loaded
					setTimeout(check, ws_this.timeout_interval);

					return;
				}

				// Loaded
				ws_this.log('log_google_maps_api_js_init_complete', field_type, 'google-maps-js-api');

				return resolve(true);
			}

			check();
		});

	}

	$.WS_Form.prototype.form_google_maps_js_api_import_libraries = function(field_type) {

		var ws_this = this;

		return (async function() {

			ws_this.log('log_google_maps_api_js_init', field_type, 'google-maps-js-api');

			switch($.WS_Form.settings_plugin.google_maps_js_api_version) {

				case '2' :

					switch(field_type) {

						case 'googleaddress':

							await google.maps.importLibrary('geocoding');
							await google.maps.importLibrary('places');

							break;

						case 'googlemap':

							await google.maps.importLibrary('geocoding');
							await google.maps.importLibrary('maps');
							await google.maps.importLibrary('marker');
							await google.maps.importLibrary('places');

							break;

						case 'googleroute':

							await google.maps.importLibrary('routes');

							break;
					}

					ws_this.log('log_google_maps_api_js_library_import', field_type, 'google-maps-js-api');

					break;

				default :

					// Routes API (New) with Places API (Legacy) still needs the routes library
					if(
						(field_type === 'googleroute') &&
						($.WS_Form.settings_plugin.google_maps_routes_api_version === '2')
					) {

						await google.maps.importLibrary('routes');

						ws_this.log('log_google_maps_api_js_library_import', field_type, 'google-maps-js-api');
					}

					break;
			}

		})();
	}

	// Initialize forms function
	window.wsf_form_instances = [];

	window.wsf_form_init = function(force_reload, reset_events, container) {

		if(typeof force_reload === 'undefined') { force_reload = false; }
		if(typeof reset_events === 'undefined') { reset_events = false; }
		if(typeof container === 'undefined') {

			var forms = $('.wsf-form');

		} else {

			var forms = $('.wsf-form', container);
		}

		if(!forms.length) { return; }

		// Get highest instance ID
		var set_instance_id = 0;
		var instance_id_array = [];

		$('.wsf-form').each(function() {

			if(typeof $(this).attr('data-instance-id') === 'undefined') { return; }

			// Get instance ID
			var instance_id_single = parseInt($(this).attr('data-instance-id'), 10);

			// Check for duplicate instance ID
			if(instance_id_array.indexOf(instance_id_single) !== -1) {

				// If duplicate, remove the data-instance-id so it is reset
				$(this).removeAttr('data-instance-id');

			} else {

				// Check if this is the highest instance ID
				if(instance_id_single > set_instance_id) { set_instance_id = instance_id_single; }
			}

			instance_id_array.push(instance_id_single);
		});

		// Increment to next instance ID
		set_instance_id++;

		// Render each form
		forms.each(function() {

			// Skip forms already initialized
			if(!force_reload && (typeof $(this).attr('data-wsf-rendered') !== 'undefined')) { return; }

			// Reset events
			if(reset_events) { $(this).off(); }

			// Set instance ID
			if(typeof $(this).attr('data-instance-id') === 'undefined') {

				// Set ID (Only if custom ID not set)
				if(typeof $(this).attr('data-wsf-custom-id') === 'undefined') {

					$(this).attr('id', 'ws-form-' + set_instance_id);
				}

				// Set instance ID
				$(this).attr('data-instance-id', set_instance_id);

				set_instance_id++;
			}

			// Get attributes
			var id = $(this).attr('id');
			var form_id = $(this).attr('data-id');
			var instance_id = $(this).attr('data-instance-id');

			if(id && form_id && instance_id) {

				// Initiate new WS Form object
				var ws_form = new $.WS_Form();

				// Save to wsf_form_instances array
				window.wsf_form_instances[instance_id] = ws_form;

				// Render
				ws_form.render({

					'obj' :			$(this),
					'form_id':		form_id
				});
			}
		});
	}

	// On load
	$(function() { wsf_form_init(); });

})(jQuery);
