/* global ajaxurl,gvGlobals,console,alert,form,gfMergeTagsObj,jQuery */
/**
 * Custom js script at Add New / Edit Views screen
 *
 * @package   GravityView
 * @license   GPL2+
 * @author    GravityKit <hello@gravitykit.com>
 * @link      http://www.gravitykit.com
 * @copyright Copyright 2014, Katz Web Services, Inc.
 *
 * @since 1.0.0
 *
 * @typedef {{
*   passed_form_id: bool,
*   has_merge_tag_listener: bool,
*   label_cancel: string,
*   label_continue: string,
*   loading_text: string,
*   nonce: string,
*   label_close: string,
*   field_loaderror: string,
*   cookiepath: string,
*   label_viewname: string,
*   label_publisherror: string,
*   label_expand_dialog: string,
*   label_contract_dialog: string,
* }} gvGlobals
*
* @typedef {{
*  target: element,
*  relatedTarget: element,
*  which: number,
*  pageX: number,
*  pageY: number,
*  metaKey: string,
*  altKey: boolean,
*  cancelable: bool,
*  char: string,
*  charCode: number,
*  clientX: number,
*  clientY: number,
*  ctrlKey: bool,
*  currentTarget: element,
*  data: object,
*  keyCode: number,
*  namespace: string,
*  result: object,
*  type: string,
*  preventDefault: function,
*  stopImmediatePropagation: function
* }} jQueryEvent
*/

(function( $ ) {
   // Alias jQuery UI's tooltip() function to gvTooltip() to prevent a conflict with Bootstrap that also registers a global tooltip() function.
   $.widget.bridge( 'gvTooltip', $.ui.tooltip );

   var viewConfiguration, viewGeneralSettings;

	// Ensures gform.applyFilters exists (needed for merge tags compatibility and fix conflict with Yoast)
	window.gform = window.gform || {};
	window.gform.applyFilters = window.gform.applyFilters || function (_a, v, ..._args) { return v; };

   const $spinner = $( '<svg class="loading" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z" fill="currentColor"></path></svg>' );

   const EXPAND_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M2.2 7.2c.4 0 .7-.3.7-.7V6l-.2-2.1 1.6 1.7 2 2c.1.2.3.2.5.2.5 0 .8-.3.8-.8 0-.2 0-.4-.2-.5l-2-2-1.7-1.6h2.2c0 .1.5.1.5.1.4 0 .7-.3.7-.7 0-.4-.3-.7-.7-.7H2.6c-.8 0-1.2.5-1.2 1.2v3.7c0 .4.3.7.8.7zm7.4 7.5h3.8c.8 0 1.2-.4 1.2-1.2V9.8c0-.4-.3-.7-.8-.7-.4 0-.7.3-.7.7v2.6c.1 0-1.5-1.7-1.5-1.7l-2-2c-.1-.1-.3-.2-.5-.2-.5 0-.8.3-.8.8s0 .4.2.5l2 2 1.7 1.6H10c0-.1-.5-.1-.5-.1-.4 0-.7.3-.7.7 0 .4.3.7.7.7z"/></svg>';
   const CONTRACT_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M6.9 2c-.4 0-.7.3-.7.7v.5l.2 2.1-1.6-1.7-2-2c-.1-.2-.3-.2-.5-.2-.5 0-.8.3-.8.8s0 .4.2.5l2 2 1.7 1.6H3.2c0-.1-.5-.1-.5-.1-.4 0-.7.3-.7.7s.3.7.7.7h3.8c.8 0 1.2-.5 1.2-1.2V2.7c0-.4-.3-.7-.8-.7zm6.5 6.4H9.6c-.8 0-1.2.5-1.2 1.2v3.7c0 .4.3.7.8.7s.7-.3.7-.7v-2.6c-.1 0 1.5 1.7 1.5 1.7l2 2c.1.2.3.2.5.2.5 0 .8-.3.8-.8s0-.4-.2-.5l-2-2-1.7-1.6H13c0 .1.5.1.5.1.4 0 .7-.3.7-.7s-.3-.7-.7-.7z"/></svg>';

   viewConfiguration = {

	   // Checks if the execution is on a Start Fresh context
	   startFreshStatus: false,

	   /**
		* @since 2.10
		* @type {bool} Whether to show "Are you sure you want to leave this page?" warning
		*/
	   hasUnsavedChanges: false,

	   /**
		* @since 1.17.3
		* @type {bool} Whether the alt (modifier) key is currently being clicked
		*/
	   altKey: false,

	   /**
		* @since 1.14
		* @type {int} The width of the modal dialogs to use for field and widget settings
		*/
	   dialogWidth: 750,

	   /**
		* @since 2.10
		* @type {bool} Whether an AJAX action is being performed
		*/
	   performingAjaxAction: false,

	   /**
	    * @since 2.42
	    * @type {number} The maximum width of the modal dialogs to use for field and widget settings.
	    */
	   maxDialogWidth: 0.95,

	   /**
		* @since 2.42
		* @type {boolean} Whether to ignore the `escape` key event.
		*/
	   ignoreEscapeEvent: false,

	   /**
		* @since 2.48.2
		* @type {number} Delay in milliseconds to wait before moving focus after smooth scrolling.
		*/
	   smoothScrollFocusDelay: 300,

	   /**
		* @since $ver$
		* @type {object} Per-template serialized field configurations, stored when switching away.
		*/
	   templateSnapshots: {},

	   /**
		* Selector matching the field configuration inputs, as serialized into `gv_fields`.
		*
		* @since $ver$
		* @type {string}
		*/
	   fieldsInputsSelector: ':input[name^="fields["]',

	   init: function () {

		   // short tag
		   var vcfg = viewConfiguration;

		   // Safari-specific save-performance fix.
		   //
		   // Submitting the View edit form is pathologically slow in Safari
		   // (~9 s) along *both* paths the browser exposes:
		   //
		   //   • Clicking Publish/Update/Save Draft → handled in serializeForm
		   //     by replacing $.trigger('click') with native form.submit().
		   //   • Pressing Enter inside an input (implicit submission) → the
		   //     browser runs its native submit() algorithm, which does enough
		   //     pre-submit work in Safari that `novalidate` alone doesn't
		   //     help. So we intercept the Enter keydown before the browser
		   //     starts that work and route through our fast submit path.
		   //
		   // novalidate is set as defense-in-depth for any other path that
		   // might still flow through HTML5 constraint validation.
		   $( '#post' ).attr( 'novalidate', 'novalidate' );

		   // Listen at the document level in capture phase so we run BEFORE any
		   // inner widget that might stopPropagation/preventDefault on keydown
		   // (Selectwoo, autocomplete, jQuery UI, etc.). Bubble-phase listeners
		   // on the form are silently swallowed if any descendant calls
		   // stopPropagation, which is why a form-level handler didn't help.
		   document.addEventListener( 'keydown', function ( e ) {
			   if ( e.key !== 'Enter' ) {
				   return;
			   }
			   if ( e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.isComposing ) {
				   return;
			   }

			   var target = e.target;
			   var form   = document.getElementById( 'post' );
			   if ( ! target || ! form || ! form.contains( target ) ) {
				   return;
			   }
			   if ( target.tagName !== 'INPUT' ) {
				   return;
			   }

			   // Skip controls whose Enter behavior shouldn't be hijacked.
			   var type = ( target.type || 'text' ).toLowerCase();
			   if ( [ 'checkbox', 'radio', 'submit', 'reset', 'button', 'image', 'file', 'range' ].indexOf( type ) > -1 ) {
				   return;
			   }

			   // Don't interfere with autocomplete/menu widgets that handle Enter themselves.
			   if ( target.closest && target.closest( '.ui-autocomplete, .ui-menu, .select2-search__field, .selectwoo-search__field, [role="combobox"], [aria-autocomplete]' ) ) {
				   return;
			   }

			   // Cancel the browser's slow implicit-submission path entirely.
			   e.preventDefault();
			   e.stopImmediatePropagation();

			   // Route through the submit handler chain. Use a native SubmitEvent so
			   // jQuery-bound submit listeners (including our own processFormSubmit)
			   // still fire. serializeForm will then call native form.submit().
			   var submitEvent;
			   try {
				   submitEvent = new SubmitEvent( 'submit', { bubbles: true, cancelable: true } );
			   } catch ( err ) {
				   submitEvent = new Event( 'submit', { bubbles: true, cancelable: true } );
			   }
			   if ( form.dispatchEvent( submitEvent ) ) {
				   form.submit();
			   }
		   }, true );

		   //select form dropdown
		   vcfg.gvSelectForm = $( '#gravityview_form_id' );

		   vcfg.gvSwitchView = $('#gv_switch_view_button');

		   //current form/template selection
		   vcfg.currentFormId = vcfg.gvSelectForm.val();
		   vcfg.currentDirectoryTemplate = $("#gravityview_directory_template").val();
		   vcfg.currentSingletemplate = $("#gravityview_single_template").val();

		   vcfg.directAccessSelect = $( '#gv-direct-access-select' );

		   // Start by showing/hiding on load
		   vcfg.toggleInitialVisibility( vcfg );

		   // Start bind to $( document.body )
		   $( document.body )

			   // Track modifier keys being clicked
			   .on( 'keydown keyup', vcfg.altKeyListener )

			   // select form
			   .on( 'change', '#gravityview_form_id', vcfg.formChange )

			   // start fresh button
			   .on( 'click', 'a[href="#gv_start_fresh"]', vcfg.startFresh )

			   // when saving the View, try to create form before proceeding
			   .on( 'click', '#publish, #save-post', vcfg.processFormSubmit )

			   // when saving the View, try to create form before proceeding
			   .on( 'submit', '#post', vcfg.processFormSubmit )

			   // Hover overlay show/hide
			   .on( 'click', ".gv-view-types-hover", vcfg.selectTemplateHover )

			   // Convert rel="external" to target="_blank" for accessibility
			   .on( 'click', 'a[rel*=external]', vcfg.openExternalLinks )

			   // Scroll to settings without adding anchor to URL
			   .on( 'click', '#gk-settings-link', vcfg.scrollToSettings )

			   // close all tooltips if user clicks outside the tooltip
			   .on( 'click mouseup keyup', vcfg.closeTooltips )

			   // close all tooltips if user clicks outside the tooltip
			   .on( 'click', '.gv-field-filter-form span[role="button"]', vcfg.switchTooltipLayout )

			   // switch View (for existing forms)
			   .on( 'click', '#gv_switch_view_button', vcfg.switchView )

			   .on( 'click', '.clear-all-fields', vcfg.removeAllFields )

			   // select template
			   .on( 'click', '.gv_select_template', vcfg.selectTemplate )
			   .on( 'change', 'select[data-view-dropdown]', vcfg.selectTemplate )

			   // bind Add Field fields to the addField method
			   .on( 'click', '.ui-tooltip-content .gv-fields', vcfg.startAddField )

			   // Bind Add fields popup to button between fields.
			   .on( 'click', '.gv-add-field-before', function () {
				   $( this )
					   .closest( '.active-drop-container' )
					   .find( 'a.gv-add-field' )
					   .trigger( 'click', { before: $( this ).closest( '.gv-fields' ) } );
			   } )

			   // Clicks can land before init_tooltips() binds the field picker; lazily bind
			   // this element and re-dispatch so the first click always works. See GVIEW-228.
			   .on( 'click', '.gv-add-field', function ( e, data ) {
				   if ( $( this ).is( ':ui-tooltip' ) ) {
					   return;
				   }

				   e.preventDefault();

				   viewConfiguration.init_tooltips( this );

				   // Forward the event data so the `before` payload set by .gv-add-field-before is preserved.
				   $( this ).trigger( 'click', data );
			   } )

			   // Bind duplicate field action to duplication button.
			   .on( 'click', '.gv-field-duplicate', vcfg.duplicateField )

			   // Show the direct access options and hide the toggle button when opened.
			   .on( 'click', "#gv-direct-access .edit-direct-access", vcfg.editDirectAccess )

			   // Cancel direct access selection area and hide it from view.
			   .on( 'click', "#gv-direct-access-select .cancel-direct-access", vcfg.cancelDirectAccess )

			   // Set the selected direct access setting as current.
			   .on( 'click', "#gv-direct-access-select .save-direct-access", vcfg.updateDirectAccess )

			   // When changing forms, update the form info helper links
			   .on( 'gravityview_form_change', vcfg.updateFormLinks )

			   // When changing forms, update the widget form_ids
			   .on( 'gravityview_form_change', vcfg.updateWidgetFormIds )

			   // Show fields that are being used as links to single entry
			   .on( 'change', ".gv-dialog-options input[name*=show_as_link]", vcfg.toggleShowAsEntry )

			   .on( 'change', '.gv-dialog-options input[name*=only_loggedin]', vcfg.toggleCustomVisibility )

			   .on( 'change', '.gv-dialog-options [name*=allow_edit_cap]', vcfg.toggleCustomVisibility )

			   .on( 'change', '.gv-dialog-options [name*=input_type]', vcfg.toggleInputHidden )

			   // show field buttons: Settings & Remove
			   .on( 'click', ".gv-field-controls .gv-remove-field", vcfg.removeField )

			   // Clicking a settings link opens settings
			   .on( 'click', ".gv-field-controls .gv-field-settings", vcfg.openFieldSettings )

			   // Double-clicking a field/widget label opens settings
			   .on( 'dblclick', ".gv-fields:not(.gv-nonexistent-form-field):not(.gv-bulk-actions-ghost-field) h5", vcfg.openFieldSettings )

			   // Clicking the Bulk Actions ghost field settings control opens the widget settings.
			   .on( 'click', '.gv-bulk-actions-ghost-settings', vcfg.openBulkActionsGhostFieldSettings )

			   .on( 'change', "#gravityview_settings", vcfg.changedSettingsAction )

			   .on( 'click', 'div[data-js="gform-simplebar"]', vcfg.changedSettingsAction )

			   .on( 'click', '.gv-field-details--toggle', function( _e ) {

				   var $dialog = $( this ).parents('.ui-dialog');

				   var was_closed = $( '.gv-field-details', $dialog ).hasClass('gv-field-details--closed');

				   viewConfiguration.toggleFieldDetails( $dialog, was_closed );

				   // When toggled, set a new cookie
				   $.cookie( 'gv-field-details-expanded', was_closed, { path: gvGlobals.admin_cookiepath } );

				   return false;
			   })

			   .on( 'search keydown keyup', '.gv-field-filter-form input:visible', vcfg.setupFieldFilters )

			   /**
				* When dismissing tab configuration warnings, don't show to the user again
				*/
			   .on( 'click', '.gv-section .is-dismissible .notice-dismiss', function( _e ) {

				   var warning_name = $( this ).parents( '.gv-section' ).attr( 'id' ) + '-' + $( '#post_ID' ).val();

				   $.cookie( 'warning-dismissed-' + warning_name, 1, { path: gvGlobals.admin_cookiepath } );

				   $( document.body ).trigger( 'gravityview/tabs-ready' );
			   })

			   .on( 'gravityview/loaded gravityview/tabs-ready gravityview/field-added gravityview/field-removed gravityview/all-fields-removed gravityview/show-as-entry gravityview/view-config-updated', vcfg.toggleTabConfigurationWarnings )

			   .on( 'gravityview/loaded gravityview/tabs-ready gravityview/field-added gravityview/field-removed gravityview/all-fields-removed gravityview/show-as-entry gravityview/view-config-updated', vcfg.toggleRemoveAllFields )

			   .on( 'gravityview/loaded gravityview/field-added gravityview/field-removed gravityview/all-fields-removed', vcfg.updateBulkActionsActionSelects )

			   .on( 'gravityview/loaded gravityview/tabs-ready gravityview/field-added gravityview/field-removed gravityview/all-fields-removed gravityview/view-config-updated', vcfg.updateBulkActionsGhostField )

			   .on( 'change', '[data-fieldid="bulk_actions"] [name$="[bulk_actions_checkbox_position]"]', vcfg.updateBulkActionsGhostField )

			   .on( 'search keydown keyup', '.gv-field-filter-form input:visible', vcfg.setupFieldFilters )

			   // Only start tracking changes after the View is loaded to prevent this from being run multiple times.
			   .on( 'gravityview/loaded', function() {
				   $( '.gv-setting-list, #gravityview_settings' ).on( 'change', vcfg.toggleCheckboxes ).trigger( 'change' );
			   })

			   .on( 'change', ".gv-dialog-options", vcfg.toggleCheckboxes )

			   .on( 'focus', '.gv-add-field', function( _e ) {
				   $( this ).parent('.gv-fields').addClass( 'trigger--hover' );
			   })

			   .on( 'blur', '.gv-add-field', function( _e ) {
				   $( this ).parent('.gv-fields').removeClass( 'trigger--hover' );
			   })

			   .on( 'keydown', '.gv-add-field', function( e ) {
				   if ( 'Enter' !== e.key && ' ' !== e.key ) {
					   return true;
				   }
				   $( this ).parent( '.gv-fields' ).addClass( 'trigger--active' );
			   })

			   .on( 'keyup', '.gv-add-field', function( e ) {
				   if ( 'Enter' !== e.key && ' ' !== e.key ) {
					   return true;
				   }
				   $( this ).parent( '.gv-fields' ).removeClass( 'trigger--active' );
			   })

			   .on( 'gravityview/dropdown/activate gravityview/dropdown/install', vcfg.enableLockedTemplate )

			   .on( 'gravityview/dialog-opened', function( _e, dialog ) {
					const $parent = $( dialog ).parent();

					// Skip warning dialogs, such as when changing the View type.
					if ( $parent.find('.gv-dialog-warning').length ) {
						return;
					}

					// Only add the expand button if it doesn't exist and dialog width is less than the maximum dialog width.
					if ( $parent.find('.gv-dialog-expand').length === 0 && vcfg.dialogWidth < $( window ).width() * vcfg.maxDialogWidth ) {
						const $expandButton = $('<button>', {
							class: 'gv-dialog-expand ui-button',
							'aria-label': gvGlobals.label_expand_dialog,
							'aria-expanded': 'false',
							'aria-controls': $parent.find('.ui-dialog-content').attr('id') || 'dialog-content',
							title: gvGlobals.label_expand_dialog,
							html: '<span class="screen-reader-text">' + gvGlobals.label_expand_dialog + '</span>' + EXPAND_ICON
						});
						$parent.find('.ui-dialog-titlebar').append( $expandButton );
					}

					vcfg.initTomSelectFields( dialog );
				} )

				// jQuery UI's dialogopen fires on every open (gravityview/dialog-opened only fires once per
				// dialog init), so the TomSelect openOnFocus suppression must hook this event for the
				// subsequent-open case to also be covered.
				.on( 'dialogopen', function ( e ) {
					const dialog = e.target;

					$( dialog ).find( '.gv-tom-select.tomselected' ).each( function () {
						const ts = this.tomselect;

						if ( ! ts ) {
							return;
						}

						const originalOpenOnFocus = ts.settings.openOnFocus;
						ts.settings.openOnFocus = false;

						if ( ts.isOpen ) {
							ts.close();
						}

						window.setTimeout( function () { ts.settings.openOnFocus = originalOpenOnFocus; }, 400 );
					} );
				} )

				.on( 'click', '.gv-dialog-expand', function(e) {
					// Only handle click or Enter/Space key
					if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') {
						return;
					}

					e.preventDefault();
					const $dialog = $(this).closest('.ui-dialog');
					const $button = $(this);
					const isExpanded = $dialog.hasClass('gv-dialog-expanded');

					$dialog.toggleClass('gv-dialog-expanded');
					$button.attr({
						'aria-expanded': !isExpanded,
						'aria-label': isExpanded ? gvGlobals.label_contract_dialog : gvGlobals.label_expand_dialog,
						title: isExpanded ? gvGlobals.label_contract_dialog : gvGlobals.label_expand_dialog
					})
						.find('.screen-reader-text').text(isExpanded ? gvGlobals.label_contract_dialog : gvGlobals.label_expand_dialog).end()
						.find('svg').replaceWith(isExpanded ? EXPAND_ICON : CONTRACT_ICON);

					// Trigger window resize to recalculate dialog position.
					$( window ).trigger( 'resize' );
				})
			   .on( 'tooltipopen', vcfg.onTooltipOpen )
		   ;
		   // End bind to $( document.body )

		   $( window ).on( 'resize', function () {
			   const $openDialog = $( ".ui-dialog:visible" ).find( '.ui-dialog-content' );

			   $openDialog.dialog( 'option', 'position', {
				   my: 'center',
				   at: 'center',
				   collision: 'fit',
				   of: window,
				   within: $( '#wpwrap' ),
			   } );

				$openDialog.dialog( 'option', 'width', vcfg.getDialogWidth( $openDialog ) );
		   });

		   // Make sure the user intends to leave the page before leaving.
		   window.addEventListener('beforeunload', ( event) => {
			   if ( vcfg.hasUnsavedChanges ) {
				   event.preventDefault();
			   }
		   } );

		   if( gvGlobals.passed_form_id ) {
			   vcfg.gvSelectForm.trigger( 'change' );
		   }

		   // Enable inserting GF merge tags into WP's CodeMirror
		   var _sendToEditor = window.send_to_editor;

		   window.send_to_editor = function ( val ) {
			   var $el = $( '#' + window.wpActiveEditor );

			   if ( !$el.hasClass( 'codemirror' ) && _sendToEditor ) {
				   return _sendToEditor( val );
			   }

			   var codeMirror = $el.next( '.CodeMirror' )[ 0 ].CodeMirror;
			   var cursorPosition = codeMirror.getCursor();
			   codeMirror.replaceRange( val, window.wp.CodeMirror.Pos( cursorPosition.line, cursorPosition.ch ) );
		   };

		   $( 'div .gform-dropdown__trigger' ).on( 'click.gravityforms', vcfg.sendMergeTagValueToCodemirrorEditor );
	   },

		/**
		 * Returns the width of the dialog, based on the window size and field type.
		 *
		 * @since 2.42
		 *
		 * @param $dialog
		 * @returns {number|int}
		 */
		getDialogWidth: function( $dialog ) {

			var dialog_width = viewConfiguration.dialogWidth;

			// If the dialog is for the Search Bar widget, make it wider.
			if ( $dialog.parents( '[data-fieldid="search_bar"]' ).length > 0 ) {
				dialog_width = 1200;
			}

			// If dialog width is greater than the maximum dialog width, set to the maximum dialog width.
			let calcMaxDialogWidth = $( window ).width() * viewConfiguration.maxDialogWidth;
			if ( dialog_width > calcMaxDialogWidth ) {
				return calcMaxDialogWidth;
			}

			return dialog_width;
		},

	   getCookieVal: function ( cookie ) {
		   if ( ! cookie || cookie === 'undefined' || 'false' === cookie ) {
			   return false;
		   }

		   return cookie;
	   },

	   /**
		* Show or hide tab warning icons
		*
		* @since 2.10
		* @param e
		*/
	   toggleTabConfigurationWarnings: function ( _e ) {

		   const tabs = {
			   single: {
				   configured: ( $( '.gv-dialog-options input[name*=show_as_link]:checked', '#directory-active-fields' ).length || $( '[data-fieldid="entry_link"]', '#directory-active-fields' ).length ),
				   icon: 'dashicons-media-default',
			   },
			   edit: {
				   configured: $( '.gv-fields .field-key[value="edit_link"]' ).length,
				   icon: 'dashicons-welcome-write-blog',
			   }
		   };

		   $.each( tabs,  function ( index, value ) {

			   const warning_name = index + '-fields' + '-' + $( '#post_ID' ).val();
			   const dismissed_warning = viewConfiguration.getCookieVal( $.cookie( 'warning-dismissed-' + warning_name ) );

			   const $fields_section = $( '#' + index + '-fields' );
			   const fields_count = $fields_section.find('.active-drop .gv-fields').length;
			   const show_warning = ! dismissed_warning && value.configured === 0 && fields_count > 0;

			   $fields_section.find( '.notice-no-link' ).toggle( show_warning );

			   $( 'li[aria-controls="' + index + '-view"]' )
				   .toggleClass( 'tab-not-configured', show_warning )
				   .find( '.tab-icon' )
				   .toggleClass( 'dashicons-warning', show_warning )
				   .toggleClass( value.icon, ! show_warning );
		   });
	   },

	   /**
		* Listen for whether the altKey is being held down. If so, we modify some behavior.
		*
		* This is necessary here because clicking on <select> doesn't register the altKey properly
		*
		* @since 1.17.3
		*
		* @param {jQuery} e
		*/
	   altKeyListener: function( e ) {
		   viewConfiguration.altKey = e.altKey;
	   },

	   /**
		* Manages all actions required when the settings are updated.
		*
		* @since 2.33
		*
		* @param {Event} event
		*/
	   changedSettingsAction: function (_event) {
		   // Revalidate all current tab fields, as new fields may appear when settings are changed.
		   var $tabFields = viewGeneralSettings.metaboxObj.find( '[name^=template_settings]:visible' );
		   $tabFields.each( function () {
			   viewConfiguration.validateField( $( this ) );
		   } );

		   // Recalculate zebra stripes.
		   viewConfiguration.zebraStripeSettings();
	   },

	   /**
		* Validates the field when its value changes.
		*
		* @since 2.33
		*
		* @param {jQuery} $field
		*/
	   validateField: function ( $field ) {
		   var rules = $field.data( 'rules' );
		   if ( ! rules ) {
			   return;
		   }

		   var error = viewConfiguration.validateValue( $field.val(), rules );

		   $field.toggleClass( 'gv-error', !! error );

		   $field.parent().find( '.gv-error-message' ).remove();
		   if ( error ) {
			   $field.parent().append(
				   $( '<div>', { class: 'gv-error-message', text: error } )
			   );
		   }
	   },

	   /**
		* Validates a value against a set of rules.
		*
		* @since 2.33
		*
		* @param {any} value - The value to validate.
		* @param {Array} rules - The rules to validate against.
		* @returns {String|undefined} - Error message. Empty if valid.
		*/
	   validateValue( value, rules ) {
		   if ( ! rules ) {
			   return;
		   }

		   var validators = viewConfiguration.getValidators();
		   for ( var i in rules ) {
			   if ( ! rules.hasOwnProperty( i ) ) {
				   continue;
			   }
			   var ruleset = rules[ i ],
				   rule = ruleset.rule,
				   message = ruleset.message,
				   param = '',
				   isValid = true;

			   // Split the rule to get the rule parameter. Example: max:5, rule - max, param - 5.
			   if ( rule.includes( ':' ) ) {
				   var parts = rule.split( /:(.+)/ ); // Split only on the first ":"
				   rule = parts[ 0 ];
				   param = parts[ 1 ];
			   }
			   if ( validators[ rule ] ) {
				   isValid = validators[ rule ]( value, param );
			   }
			   if ( ! isValid ) {
				   return message;
			   }
		   }
	   },

	   /**
		* Gets a list of validation callbacks.
		*
		* @since 2.33
		*
		* @returns {Object} - An object containing validation callbacks.
		*/
	   getValidators: function () {
		   return {
			   required: function ( value ) {
				   return value !== null && value !== undefined && value.toString().trim() !== '';
			   },
			   max: function ( value, max ) {
				   return value !== null && value !== undefined && Number( value ) <= max;
			   },
			   min: function ( value, min ) {
				   return value !== null && value !== undefined && Number( value ) >= min;
			   },
			   email: function ( value ) {
				   return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value );
			   },
			   integer: function ( value ) {
				   return Number.isInteger ? Number.isInteger( Number( value ) ) : Number( value ) % 1 === 0;
			   },
			   matches: function ( value, pattern ) {
				   return new RegExp( pattern ).test( value );
			   },
		   };
	   },

	   /**
		* Update zebra striping when settings are changed
		* This prevents two gray rows next to each other.
		* @since 1.19
		*/
	   zebraStripeSettings: function () {
		   setTimeout( function () {
			   viewGeneralSettings.metaboxObj.find( 'table' ).each( function () {
				   var $trs = $( this ).find( 'tr' ).filter( ':visible' );

				   $trs.removeClass( 'alternate' );

				   $trs.filter( ':even' ).addClass( 'alternate' );
			   } );
		   }, 50 );
	   },

	   /**
		* Show/hide checkboxes that have visibility conditionals
		* @see GravityView_FieldType_checkboxes
		* @param  {jQuery} e
		*/
	   toggleCheckboxes: function (  e ) {

		   var target = e.currentTarget ? e.currentTarget : e;

		   viewConfiguration.toggleRequired( target, 'requires', false );
		   viewConfiguration.toggleRequired( target, 'requires-not', true );

		   var $parent = $( target ).is( '.gv-fields' ) ? $( target ) : $( target ).parents( '.gv-fields' );

		   if ( ! $parent.length ) {
			   return;
		   }

		   // "Link to Post" should hide when "Link to single entry" is checked
		   viewConfiguration.toggleDisabled( $( 'input[type=checkbox][name*=link_to_]', $parent ), $( 'input[type=checkbox][name*=show_as_link]', $parent ) );

		   // "Make Phone Number Clickable" should hide when "Link to single entry" is checked
		   viewConfiguration.toggleDisabled( $( 'input[type=checkbox][name*=link_phone]', $parent ), $( 'input[type=checkbox][name*=show_as_link]', $parent ) );
	   },

	   /**
		* If one setting is enabled, disable the other. Requires the input support `:checked` attribute.
		*
		* @since 2.10
		*
		* @param {jQuery} $one
		* @param {jQuery} $two
		*/
	   toggleDisabled: function ( $one, $two ) {

		   if ( $one.length === 0 || $two.length === 0 ) {
			   return;
		   }

		   if ( $one.is( ':checked' ) ) {
			   $two.prop( 'disabled', true );
			   return;
		   }

		   if ( $two.is(':checked') ) {
			   $one.prop( 'disabled', true );
		   }
	   },

	   /**
		* Process conditional show/hide logic
		*
		* @since 2.3
		*
		* @param {jQueryEvent} currentTarget
		* @param {string} data_attr The attribute to find in the target, like `requires` or `requires-not`
		* @param {boolean} reverse_logic If true, find items that do not match the attribute value. True = `requires-not`; false = `requires`
		*/
	   toggleRequired: function( currentTarget, data_attr, reverse_logic ) {
		   const $root = $( currentTarget, '#post' );

		   $root
			   .find( '[data-' + data_attr + ']' )
			   .each( function ()  {
				   const $this = $( this );
				   const requires = String( $this.data( data_attr ) || '' );
				   const conditions = requires.split( ',' ).map( function ( condition ) {
					   return condition.trim();
				   } ).filter( Boolean );

				   // Scope to the closest per-action panel first so each panel's
				   // controlling checkbox only gates its own dependents. Fall back to
				   // the dialog (handles nested dialogs), then to the original root.
				   const $actionPanel = $this.closest('.gv-bulk-action-settings-panel');
				   const $options = $this.closest('.gv-dialog-options');
				   const $parent = $actionPanel.length > 0
					   ? $actionPanel
					   : ( $options.length > 0 ? $options : $root );

				   const shouldShow = conditions.every( function ( condition ) {
					   // Each condition supports comparison operators (=, !=, >, <, >=, <=);
					   // a bare "name" or "name=value" falls through to equality.
					   const operatorMatch = condition.match( /^([a-zA-Z0-9_-]+)\s*(>=|<=|!=|>|<|=)\s*(.+)$/ );
					   let requiresName;
					   let requiresValue;
					   let operator = '=';

					   if ( operatorMatch ) {
						   requiresName  = operatorMatch[1];
						   operator      = operatorMatch[2];
						   requiresValue = operatorMatch[3];
					   } else {
						   const separatorIndex = condition.indexOf( '=' );
						   requiresName  = separatorIndex > -1 ? condition.substring( 0, separatorIndex ) : condition;
						   requiresValue = separatorIndex > -1 ? condition.substring( separatorIndex + 1 ) : undefined;
					   }

					   const $input = $parent.find('[name$="[' + requiresName + ']"]').filter(':input');

					   // The input this condition depends on isn't in scope, so there's nothing to
					   // evaluate against. A missing dependency input must not hide an otherwise-valid
					   // setting, so leave the row alone instead of gating on a control that isn't there.
					   if ( ! $input.length ) {
						   return true;
					   }

					   const $checkboxes = $input.filter('[type=checkbox]');
					   const $radios = $input.filter('[type=radio]');
					   let matches = false;

					   if ( $checkboxes.length ) {
						   matches = $checkboxes.is(':checked');
					   } else if ( $radios.length ) {
						   const radioOn = requiresValue === undefined ? $radios.is(':checked') : $radios.filter('[value="' + requiresValue + '"]').is(':checked');
						   matches = operator === '!=' ? ! radioOn : radioOn;
					   } else if ( requiresValue !== undefined ) {
						   const numVal    = parseFloat( $input.val() );
						   const numTarget = parseFloat( requiresValue );

						   if ( operator !== '=' && operator !== '!=' && ! isNaN( numVal ) && ! isNaN( numTarget ) ) {
							   switch ( operator ) {
								   case '>':  matches = numVal >  numTarget; break;
								   case '<':  matches = numVal <  numTarget; break;
								   case '>=': matches = numVal >= numTarget; break;
								   case '<=': matches = numVal <= numTarget; break;
							   }
						   } else if ( operator === '!=' ) {
							   matches = $input.val() !== requiresValue;
						   } else {
							   matches = $input.val() === requiresValue;
						   }
					   }

					   return reverse_logic ? ! matches : matches;
				   } );

				   $this.toggle( shouldShow );
			   });

	   },

	   /**
		* When clicking the field picker layout, change the tooltip class
		*
		* @param  {jQueryEvent} e [description]
		* @return {bool}   [description]
		*/
	   switchTooltipLayout: function ( _e ) {

		   var layout = $( this ).data( 'value' );

		   viewConfiguration.setTooltipLayout( layout );
	   },

	   setTooltipLayout: function ( layout ) {

		   $( '.gv-items-picker--' + layout ).addClass( 'active' );

		   $( '.gv-items-picker' ).not( '.gv-items-picker--' + layout ).removeClass( 'active' );

		   $( '.gv-items-picker-container' ).attr( 'data-layout', layout );

		   // When choice is made, set a new cookie
		   $.cookie( 'gv-items-picker-layout', layout, { path: gvGlobals.admin_cookiepath } );
	   },

	   /**
		* Close all tooltips if user clicks outside the tooltip or presses escape key
		* @param  {jQueryEvent} e [description]
		* @return {bool}   [description]
		*/
	   closeTooltips: function ( e ) {
		   var activeTooltips = $( "[data-tooltip='active']" );

		   var close = false;
		   var return_false = false;
		   switch ( e.type ) {

			   case 'keyup':

				   // Escape key was pressed
				   if ( e.keyCode === 27 ) {
					   if ( $( '.ui-autocomplete' ).is( ':visible' ) ) {
						   return;
					   }

					   if ( viewConfiguration.ignoreEscapeEvent ) {
						   viewConfiguration.ignoreEscapeEvent = false;
						   return;
					   }

					   close = $( '.gv-field-filter-form input[data-has-search]:focus' ).length === 0;
					   return_false = close;

					   // The Beacon escape key behavior is flaky. Make it work better.
					   if ( window.Beacon  ) {
						   window.Beacon('close');
					   }
				   }

				   // The click was on the close link
				   if ( ( 13 === e.keyCode || 32 === e.keyCode ) && $( e.target ).is( '.close' ) || $( e.target ).is('.dashicons-dismiss') ) {
					   close = true;
				   }

				   break;

			   case 'mouseup':

				   if ( $( e.target ).closest( '.ui-tooltip' ).length) {
					   // If clicked inside a tooltip.
					   close = false;
				   } else if ( $( e.target ).closest( '.ui-dialog' ).length) {
					   // If clicked inside a dialog.
					   if (activeTooltips.length > 0) {
						   // And there are tooltips active, close only those.
						   close = 'tooltips';
					   }
				   }

				   // For tooltips, clicking on anything outside the tooltip
				   // should close it. Not for dialogs.
				   else if ( activeTooltips.length > 0 ) {
					   close = true;
				   }

				   // The click was on the close link
				   if ( $( e.target ).parents( '.close' ).length ) {
					   close = true;
				   }

				   break; // End mouseup switch

			   // Run on click instead of mouseup so that when selecting a form using the
			   // select, it doesn't close the dialog right away
			   case 'click':

				   // They clicked the overlay
				   if ( $( e.target ).is( '.gv-overlay' ) ) {
					   close = true;
					   return_false = true;

					   // Always remove the overlay
					   $( e.target ).remove();
				   }

				   break;

		   }


		   if ( close ) {

			   // Close all open tooltips
			   activeTooltips.gvTooltip( 'close' );

			   // Close all open dialogs
			   if ( close !== 'tooltips' ) {
				   $( '.ui-dialog:visible' ).find( '.ui-dialog-content' ).dialog( 'close' );
			   }

			   // Prevent scrolling window on click close
			   if ( return_false ) {
				   return false;
			   }
		   }
	   },

	   /**
		* Toggle the dashicon link representing whether the field is being used as a link to the single entry
		* @param  {jQueryEvent} e jQuery event object
		* @return {void}
		*/
	   toggleShowAsEntry: function ( e ) {

		   var parent = $( e.target ).parents( '.gv-fields' );

		   parent.toggleClass( 'has-single-entry-link', $( e.target ).is( ':checked' ) );

		   parent.find( '.gv-field-controls .dashicons-media-default' ).toggleClass( 'hide-if-js', $( e.target ).not( ':checked' ) );

		   $( document.body ).trigger( 'gravityview/show-as-entry', $( e.target ).is( ':checked' ) );
	   },

	   /**
		* Toggle the dashicon link representing whether the field has custom visibility settings
		* @param  {jQueryEvent} e jQuery event object
		* @return {void}
		*/
	   toggleCustomVisibility: function ( e ) {

		   var custom_visibility;

		   if ( $( e.target ).is( 'select' ) ) {
			   custom_visibility = 'read' !== $( e.target ).val();
		   } else {
			   custom_visibility = $( e.target ).is( ':checked' );
		   }

		   const $settings = $( e.target ).closest( '.gv-dialog-options' );
		   let $field;

		   if ( $settings.data( 'field' ) ) {
			   $field = $settings.data( 'field' );
		   } else {
			   const $parent = $( e.target ).closest( '.gv-fields' );
			   $parent.toggleClass( 'has-custom-visibility', custom_visibility );
			   $field = $parent.find( '.gv-field-controls' );
		   }

		   if ( $field ) {
			   $field.find( '.icon-custom-visibility' ).toggleClass( 'hide-if-js', !custom_visibility );
		   }
	   },

	   /**
		* Toggle the dashicon representing whether the field has an input type hidden.
		*
		* @since 2.42
		*
		* @param  {jQueryEvent} e jQuery event object.
		*/
	   toggleInputHidden: function ( e ) {

		   const is_hidden = 'hidden' === $( e.target ).val();

		   const $settings = $( e.target ).closest( '.gv-dialog-options' );
		   let $field;

		   if ( $settings.data( 'field' ) ) {
			   $field = $settings.data( 'field' );
		   } else {
			   const $parent = $( e.target ).closest( '.gv-fields' );
			   $parent.toggleClass( 'has-input-hidden', is_hidden );
			   $field = $parent.find( '.gv-field-controls' );
		   }

		   if ( $field ) {
			   $field.find( '.icon-hidden' ).toggleClass( 'hide-if-js', !is_hidden );
		   }
	   },

	   /**
		* Select the text of an input field on click
		* @param  {jQueryEvent}    e     [description]
		* @return {[type]}          [description]
		*/
	   selectText: function ( e ) {
		   e.preventDefault();

		   $( this ).trigger('focus').trigger('select');

		   return false;
	   },

	   /**
		* @param  {jQueryEvent} e jQuery event object.
		* @since TODO
		* @return {void}
		*/
	   editDirectAccess: function ( e ) {
		   var vcfg = viewConfiguration;

		   e.preventDefault();

		   if ( vcfg.directAccessSelect.is( ':visible' ) ) {
			   return;
		   }

		   vcfg.directAccessSelect.slideDown( 'fast', function () {
			   vcfg.directAccessSelect.find( 'input[type="radio"]' ).first().trigger( 'focus' );
		   } );

		   $( this ).hide();
	   },

	   /**
		* Cancel direct access selection area and hide it from view.
		*
		* @param  {jQueryEvent} e jQuery event object.
		* @since TODO
		* @return {void}
		*/
	   cancelDirectAccess: function ( e ) {
		   viewConfiguration.directAccessSelect.slideUp( 'fast' );

		   $( '#gv-direct-access-display strong' ).text( function () {
			   return $( this ).data( 'initial-label' );
		   } );

		   $( '#gv-direct-access .edit-direct-access' ).show().trigger( 'focus' );

		   e.preventDefault();
	   },

	   /**
		* Set the selected direct access setting as current.
		* @since TODO
		* @param {jQueryEvent} e jQuery event object.
		*/
	   updateDirectAccess: function ( e ) {
		   let checked = false,
			   selectedDirectAccess = viewConfiguration.directAccessSelect.find( 'input:radio:checked' );

		   viewConfiguration.directAccessSelect.slideUp('fast');

		   $('#gv-direct-access .edit-direct-access').show().trigger( 'focus' );

		   checked = 'embed' === selectedDirectAccess.val();

		   // Update the _actual_ setting in the Permissions tab.
		   $( '#gravityview_se_embed_only' ).prop( 'checked', checked );

		   // Update the display label.
		   $('#gv-direct-access-display strong').text( selectedDirectAccess.data( 'display-label' ) );

		   // Update the class on the container to reflect the current setting.
		   $('#gv-direct-access').toggleClass('embed-only', checked );

		   e.preventDefault();
	   },

	   /**
		* @param  {jQueryEvent} e jQuery event object.
		* @param {viewConfiguration} vcfg
		*/
	   toggleInitialVisibility: function ( vcfg ) {

		   // There are no Gravity Forms forms
		   if ( vcfg.gvSelectForm.length === 0 ) {
			   return;
		   }

		   // check if there's a form selected
		   if ( '' === vcfg.currentFormId ) {
			   // if no form is selected, hide all the configs
			   vcfg.hideView();
		   } else {
			   // if both form and template were selected, show View Layout config
			   if ( $( '#gravityview_directory_template' ).length && $( '#gravityview_directory_template' ).val().length > 0 ) {
				   $( '#gravityview_select_template' ).slideUp( 150 );
				   vcfg.showViewConfig();
			   } else {
				   // else show the template picker
				   vcfg.templateFilter( 'custom' );
				   vcfg.showViewTypeMetabox();
			   }
		   }

		   if ( vcfg.currentFormId && !vcfg.currentTemplateId ) {
			   vcfg.gvSwitchView.hide();
		   }

		   vcfg.togglePreviewButton();

		   vcfg.zebraStripeSettings( true );

	   },

	   /**
		* Only show the Preview button if a form is selected.
		* Otherwise, gravityview_get_entries() doesn't work.
		*/
	   togglePreviewButton: function() {

		   var preview_button = $('#preview-action').find('.button');

		   if( '' === viewConfiguration.gvSelectForm.val() ) {
			   preview_button.hide();
		   } else {
			   preview_button.show();
		   }

	   },

	   // hides template picker metabox and view config metabox
	   hideView: function () {
		   var vcfg = viewConfiguration;

		   vcfg.currentFormId = '';
		   vcfg.togglePreviewButton();
		   $( "#gravityview_view_config, #gravityview_select_template, #gravityview_sort_filter, .gv-form-links" ).hide();
		   viewGeneralSettings.metaboxObj.hide();

	   },

	   /**
		* Update the Data Source links to the selected form
		* @return {void}
		*/
	   updateFormLinks: function () {
		   var vcfg = viewConfiguration;

		   $( '.gv-form-links a' ).each( function () {

			   var new_url = $( this ).attr( 'href' ).replace( /id=([0-9]+)/gm, 'id=' + vcfg.gvSelectForm.val() );

			   $( this ).attr( 'href', new_url );

		   } );
	   },

	   /**
		* Update Widget form IDs to the selected form
		* @return {void}
		*/
	   updateWidgetFormIds: function() {
		   var vcfg = viewConfiguration;

		   $( '.field-form-id' ).each( function() {
			   $( this ).val( vcfg.gvSelectForm.val() );
		   } );
	   },

	   /**
		* Show/Hide
		*/
	   toggleViewTypeMetabox: function () {
		   var $templates = $( "#gravityview_select_template" );
		   var vcfg = viewConfiguration;

		   if ( $templates.is( ':visible' ) ) {
			   vcfg.gvSwitchView.text( function () {
				   return $( this ).attr( 'data-text-backup' );
			   } );

			   if ( vcfg.currentDirectoryTemplate ) {
				   $templates.slideUp( 150 );
			   }
		   } else {
			   if ( vcfg.currentDirectoryTemplate ) {
				   vcfg.gvSwitchView.attr( 'data-text-backup', function () {
					   return $( this ).text();
				   } ).text( gvGlobals.label_cancel );
			   } else {
				   vcfg.gvSwitchView.hide();
			   }

			   $templates.slideDown( 150 );
		   }
	   },

	   showViewTypeMetabox: function () {
		   $( "#gravityview_select_template" ).slideDown( 150 );
	   },

	   /**
		* Triggered when the Start Fresh button has been clicked
		* @param {jQueryEvent} e
		*/
	   startFresh: function ( e ) {
		   e.preventDefault();
		   var vcfg = viewConfiguration;

		   vcfg.startFreshStatus = true;

		   // If fields are configured (either via a form or preset selection), warn against making changes
		   if ( vcfg.getConfiguredFields().length ) {
			   vcfg.showDialog( '#gravityview_select_preset_dialog' );
		   } else {
			   vcfg.startFreshContinue();
		   }
	   },

	   startFreshContinue: function () {
		   var vcfg = viewConfiguration;

		   // start fresh on save trigger
		   $( '#gravityview_form_id_start_fresh' ).val( '1' );

		   // Reset the selected form value
		   $( '#gravityview_form_id' ).val( '' );

		   vcfg.currentFormId = '';
		   vcfg.currentTemplateId = '';
		   vcfg.gvSwitchView.hide();

		   // show templates
		   vcfg.templateFilter( 'preset' );
		   vcfg.showViewTypeMetabox();

		   // hide config metabox
		   vcfg.hideViewConfig();

		   vcfg.togglePreviewButton();
	   },

	   /**
		* The Data Source dropdown has been changed. Show alert dialog or process.
		* @param {jQueryEvent} e
		* @return void
		*/
	   formChange: function ( e ) {
		   e.preventDefault();

		   var vcfg = viewConfiguration;

		   // Holding down on the alt key while switching forms allows you to change forms without resetting configurations
		   if( vcfg.altKey ) {
			   return;
		   }

		   vcfg.startFreshStatus = false;

		   if ( vcfg.getConfiguredFields().length ) {
			   vcfg.showDialog( '#gravityview_change_form_dialog' );
		   } else {
			   vcfg.formChangeContinue();
		   }

		   vcfg.togglePreviewButton();
	   },

	   formChangeContinue: function () {
		   var vcfg = viewConfiguration;

		   if ( ! vcfg.gvSelectForm.val() ) {
			   vcfg.getConfiguredFields().remove();
			   vcfg.hideView();
			   vcfg.gvSwitchView.fadeOut( 150 );
		   } else {
			   vcfg.templateFilter( 'custom' );

			   Promise.all( [
				   vcfg.getAvailableFields(),
				   vcfg.getSortableFields()
			   ] ).then( function () {
				   if ( !vcfg.currentFormId && !vcfg.currentDirectoryTemplate ) {
					   vcfg.showViewTypeMetabox();
					   vcfg.gvSwitchView.fadeOut( 150 );
				   } else {
					   if (vcfg.currentDirectoryTemplate && vcfg.currentDirectoryTemplate) {
						   vcfg.gvSwitchView.show();
					   }
					   vcfg.gvSwitchView.click();
				   }
			   } );
		   }

		   vcfg.currentTemplateId = '';
		   vcfg.currentFormId = vcfg.gvSelectForm.val();
		   // Snapshots belong to the previous form; restoring them would bring back stale fields.
		   vcfg.templateSnapshots = {};
		   vcfg.setUnsavedChanges( true );
		   $( document.body ).trigger( 'gravityview_form_change' ).addClass( 'gv-form-changed' );
	   },

	   showDialog: function ( dialogSelector, buttons ) {

		   var vcfg = viewConfiguration;

		   var thisDialog = $( dialogSelector );

		   var cancel_button = {
			   text: gvGlobals.label_cancel,
			   click: function () {
				   if ( thisDialog.is( '#gravityview_change_form_dialog' ) ) {
					   vcfg.startFreshStatus = false;
					   vcfg.gvSelectForm.val( vcfg.currentFormId );
				   }
				   // "Changing the View Type will reset your field configuration. Changes will be permanent once you save the View."
				   else if ( thisDialog.is( '#gravityview_switch_template_dialog' ) ) {
					   if ( !vcfg._isViewDropDown() ) {
						   vcfg.toggleViewTypeMetabox();
					   }
					   vcfg.showViewConfig();
				   }

				   thisDialog.dialog( 'close' );
			   }
		   };

		   var continue_button = {
			   text: gvGlobals.label_continue,
			   click: function() {
				   if ( thisDialog.is( '#gravityview_change_form_dialog' ) || thisDialog.is( '#gravityview_select_preset_dialog' ) ) {
					   if ( vcfg.startFreshStatus ) {
						   vcfg.startFreshContinue();
					   } else {
						   vcfg.formChangeContinue();
					   }
				   }
				   // "Changing the View Type will reset your field configuration. Changes will be permanent once you save the View."
				   else if ( thisDialog.is( '#gravityview_switch_template_dialog' ) ) {
					   vcfg.selectTemplateContinue( false );
					   if ( !vcfg._isViewDropDown() ) {
						   vcfg.toggleViewTypeMetabox();
					   }
				   }

				   vcfg._storeValue();
				   thisDialog.dialog( 'close' );
			   },
		   };

		   var default_buttons = [ cancel_button, continue_button ];

		   // If the buttons var isn't passed, use the defaults instead.
		   buttons = buttons || default_buttons;

		   thisDialog.dialog( {
			   dialogClass: 'wp-dialog gv-dialog',
			   appendTo: thisDialog.parent(),
			   draggable: false,
			   resizable: false,
			   width: vcfg.getDialogWidth( thisDialog ),
			   open: function () {
				   $( '<div class="gv-overlay" />' ).prependTo( '#wpwrap' );

				   $( document.body ).trigger( 'gravityview/dialog-opened', thisDialog );

				   vcfg.toggleCheckboxes( thisDialog );
				   vcfg.setupFieldDetails( thisDialog );

				   vcfg.refresh_merge_tags( thisDialog, function() {
					   // Configure CodeMirror after merge tags are refreshed (300ms following the DOMContentLoaded event).
					   vcfg.setupCodeMirror( thisDialog );
				   } );

				   $sortableEls = $( '.ui-widget-content[aria-hidden="false"]' ).find( '.active-drop-widget, .active-drop-field' );

				   if ( $sortableEls.length ) {
					   $sortableEls.each( ( _i, el ) => {
						   if ( !$( el ).hasClass( 'ui-sortable' ) ) {
							   return;
						   }

						   $( el ).sortable( 'disable' );
					   } );
				   }

				   return true;
			   },
			   close: function ( e ) {
				   e.preventDefault();

				   $( 'textarea.code', thisDialog ).each( function () {

					   $CodeMirror = $( this ).next( '.CodeMirror' );

					   if ( 0 === $CodeMirror.length || ! $CodeMirror[0].hasOwnProperty('CodeMirror') ) {
						   return;
					   }

					   $CodeMirror[0].CodeMirror.toTextArea();
				   } );

				   thisDialog.find( '.merge-tag-support' ).removeClass( 'merge-tag-support' ).addClass( 'gv-merge-tag-support' );

				   $( '.gv-field-settings.active', '#gravityview_view_config' ).removeClass( 'active' );

				   vcfg.setCustomLabel( thisDialog );

				   $( '#wpwrap').find('> .gv-overlay' ).fadeOut( 'fast', function () {
					   $( this ).remove();
				   } );

				   $sortableEls = $( '.ui-widget-content[aria-hidden="false"]' ).find( '.active-drop-widget, .active-drop-field' );

				   if ( $sortableEls.length ) {
					   $sortableEls.each( ( _i, el ) => {
						   if ( !$( el ).hasClass( 'ui-sortable' ) ) {
							   return;
						   }

						   $( el ).sortable( 'enable' );
					   } );
				   }

				   vcfg._restoreValue(); // Restore value if not persisted.

				   $( document.body ).trigger( 'gravityview/dialog-closed', thisDialog );
			   },
			   closeOnEscape: true,
			   buttons: buttons
		   } );

	   },

	   /**
		* When opening a dialog, convert textareas with CSS class ".code" to codeMirror instances
		*
		* @since 2.10
		* @param {jQuery} dialog
		*/
	   setupCodeMirror: function ( dialog ) {
		   var vcfg = viewConfiguration;

		   $( 'textarea.code:visible', dialog ).each( function () {

			   // Define a default configuration
			   const codemirrorConfig = $.extend( true, {}, wp.codeEditor.defaultSettings );

			   let attributeValue = $( this ).data( 'codemirror' );
			   if ( attributeValue ) {
				   codemirrorConfig.codemirror = $.extend( {}, codemirrorConfig.codemirror, attributeValue );
			   }

			   // And then instantiate CodeMirror using those settings, which will then extend the WP defaults.
			   let editor = wp.codeEditor.initialize( $( this ), codemirrorConfig );

			   // If Merge Tags aren't enabled, don't continue.
			   if ( ! $( this ).hasClass( 'merge-tag-support' ) && ! $( this ).hasClass( 'gv-merge-tag-support' ) ) {
				   return;
			   }

			   // Leave room for Merge Tags icon.
			   editor.codemirror.setSize( '95%' );

			   var $textarea = $( this );
			   var editorId = $textarea.attr( 'id' );
			   var mergeTags = window.gfMergeTags.getAutoCompleteMergeTags( $textarea );
			   var mergeTag = '';
			   var initialEditorCursorPos = editor.codemirror.getCursor();

			   // Move merge tag before before CodeMirror in DOM to fix floating issue
			   $textarea.parent().find( '.all-merge-tags' ).detach().insertBefore( $textarea );

			   $textarea.parent().find( 'div .gform-dropdown__trigger' ).on( 'click.gravityforms', vcfg.sendMergeTagValueToCodemirrorEditor );

			   // Set up Merge Tag autocomplete
			   $textarea.autocomplete( {
				   appendTo: $textarea.parent(),
				   minLength: 1,
				   position: {
					   my: 'center top',
					   at: 'center bottom',
					   collision: 'none'
				   },
				   source: mergeTags,
				   select: function ( _event, ui ) {
					   // insert the merge tag value without curly braces
					   var val = ui.item.value.replace( /^{|}$/gm, '' );
					   var currentEditorCursorPos = editor.codemirror.getCursor();

					   editor.codemirror.replaceRange( val, initialEditorCursorPos, window.wp.CodeMirror.Pos( currentEditorCursorPos.line, currentEditorCursorPos.ch ) );
					   editor.codemirror.focus();
					   editor.codemirror.setCursor( window.wp.CodeMirror.Pos( currentEditorCursorPos.line, currentEditorCursorPos.ch + val.length + 1 ) );
				   },
			   } );

			   var $autocompleteEl = $textarea.parent().find( 'ul.ui-autocomplete' );

			   var closeAutocompletion = function () {
				   $( '#' + editorId ).autocomplete( 'close' );
			   };

			   $( document.body ).on( 'keyup', function ( e ) {
				   if ( $autocompleteEl.is( ':visible' ) && 27 === e.which ) {
					   e.preventDefault();
					   closeAutocompletion();
					   $textarea.trigger( 'focus' );
				   }
			   } );

			   editor.codemirror.on( 'mousedown', function () {
				   closeAutocompletion();
			   } );

			   editor.codemirror.on( 'keydown', function ( _el, e ) {
				   if ( !$autocompleteEl.is( ':visible' ) ) {
					   return;
				   }

				   if ( 38 === e.which || 40 === e.which || 13 === e.which ) {
					   if ( $autocompleteEl.not( ':focus' ) ) {
						   $autocompleteEl.trigger( 'focus' );
					   }

					   e.preventDefault();
				   }
			   } );

			   editor.codemirror.on( 'change', function ( _e, obj ) {
				   // detect curly braces and update the cursor position
				   if ( obj.text[ 0 ] === '{}' ) {
					   initialEditorCursorPos = editor.codemirror.getCursor();
				   }

				   // select everything between the initial and current cursor positions
				   var currentEditorCursorPos = editor.codemirror.getCursor();
				   mergeTag = editor.codemirror.getRange( {
					   ch: initialEditorCursorPos.ch - 1,
					   line: initialEditorCursorPos.line
				   }, currentEditorCursorPos );

				   // if the value starts with a curly braces, initiate autocompletion
				   if ( mergeTag[ 0 ] === '{' ) {
					   $( '#' + editorId ).autocomplete( 'search', mergeTag );

					   return;
				   }

				   closeAutocompletion();
			   } );
		   } );
	   },

	   /**
		* Event handler that inserts the merge tag value (data-value property) to WP's CodeMirror
		*
		* @since 2.14.4
		* @param {jQueryEvent} e
		*/
	   sendMergeTagValueToCodemirrorEditor: function ( e ) {
		   // Always make sure the active editor is set.
		   // This can also be overridden by other plugins (like Members), so make a backup.
		   var _activeEditorBackup = window.wpActiveEditor;

		   window.wpActiveEditor = $( e.currentTarget ).parentsUntil( '.gv-setting-container' ).find( 'textarea' ).attr( 'id' );

		   if ( window.wpActiveEditor ) {
			   window.send_to_editor( $( this ).data( 'value' ) );
		   }

		   // Restore prior active editor
		   window.wpActiveEditor = _activeEditorBackup;
	   },

	   /**
		* When opening a dialog, restore the Field Details visibility based on cookie
		* @since 2.10
		* @param {jQuery} dialog
		*/
	   setupFieldDetails: function ( dialog ) {

			// Don't show field details in the search bar dialog.
			if ( dialog.parents( "[data-fieldid=\"search_bar\"]" ).length ) {
				return;
			}

		   // Add the details to the title bar
		   $( '.gv-field-details--container', dialog ).insertAfter( '.ui-dialog-title:visible' );

		   // When the dialog opens, read the cookie
		   // Otherwise, check for cookies
		   var show_details_cookie = $.cookie( 'gv-field-details-expanded' );

		   var show_details = viewConfiguration.getCookieVal( show_details_cookie );

		   viewConfiguration.toggleFieldDetails( dialog, show_details );

		   viewConfiguration.migrateSurveyScore( dialog );
	   },

	   /**
		* Migrate Likert fields with [score] to [choice_display]
		* @since 2.11
		* @param {jQuery} $dialog
		*/
	   migrateSurveyScore: function ( $dialog ) {

		   // Only process on Survey fields
		   if ( 0 === $dialog.parents('[data-inputtype="survey"]').length ) {
			   return;
		   }

		   var $score = $dialog.find( '.gv-setting-container-score input' );

		   if ( ! $score ) {
			   return;
		   }

		   if ( 0 === $score.val() * 1 ) {
			   return;
		   }

		   $dialog
			   .find( '.gv-setting-container-choice_display input[value="score"]' )
			   .trigger('click') // Update the choice
			   .trigger('focus') // Highlight the selected choice
		   ;
	   },

	   /**
		* Toggle visibility for field details
		* @since 2.10
		* @param {jQuery}  $dialog The open dialog
		* @param {boolean|string} show_details Whether to show the field details or not
		*/
	   toggleFieldDetails: function ( $dialog, show_details ) {

		   $parent = $dialog.parent();

		   $parent
			   .find( '.gv-field-details' ).toggleClass( 'gv-field-details--closed', ! show_details ).end()
			   .find( '.gv-field-details--toggle .dashicons' )
			   .toggleClass( 'dashicons-arrow-down', !! show_details )
			   .toggleClass( 'dashicons-arrow-right', ! show_details ).end();
	   },

	   /**
		* Update the field display to show the custom label while editing
		* @param {jQuery} dialog The dialog object
		*/
	   setCustomLabel: function ( dialog ) {

		   // Does the field have a custom label?
		   var $admin_label = $( '[name*=admin_label]', dialog );
		   var $custom_label_field;

		   if ( ! $admin_label.length || ! $admin_label.val() ) {
			   $custom_label_field = $( '[name*=custom_label]', dialog );
		   } else {
			   $custom_label_field = $admin_label; // We have an administrative label for this field
		   }

		   // Prevent updating the custom label from a nested search field.
		   if ( $( dialog ).closest('[data-search-fields]').length !== $custom_label_field.closest( '[data-search-fields]' ).length ) {
			   // This works because the regular search widget does not have a custom title field.
			   return;
		   }

		   var $label = dialog.closest( '.gv-fields' ).find( '.gv-field-label-text-container' ).first();

		   // If there's a custom title, use it for the label.
		   if ( $custom_label_field.length ) {

			   var custom_label_text = $custom_label_field.val().trim();

			   // Make sure the custom label isn't empty
			   if( custom_label_text.length > 0 ) {
				   $label.html( custom_label_text );
			   } else {
				   // If there's no custom title, then use the original
				   // @see GravityView_Admin_View_Item::getOutput()
				   $label.html( $label.parent( '.gv-field-label' ).data( 'original-title' ) );
			   }

		   }
	   },

	   /**
		* Ignores the escape button for the next call.
		* @since 2.42
		*/
	   ignoreEscape: function () {
		   viewConfiguration.ignoreEscapeEvent = true;
	   },

	   /**
		* @todo Combine with the embed shortcode dropdown
		* @param  {string} context Context (multiple, single, edit)
		* @param  {string} id      Template ID
		* @return {void}
		*/
	   getSortableFields: function ( context, id ) {
		   return new Promise((resolve, _reject) => {
			   var vcfg = viewConfiguration;

			   // While it's loading, disable the field, remove previous options, and add loading message.
			   $( ".gravityview_sort_field" ).prop( 'disabled', 'disabled' ).empty().append( '<option>' + gvGlobals.loading_text + '</option>' );

			   var data = {
				   action: 'gv_sortable_fields_form',
				   nonce: gvGlobals.nonce
			   };

			   if ( context !== undefined && 'preset' === context ) {
				   data.template_id = id;
			   } else {
				   data.form_id = vcfg.gvSelectForm.val(); // TODO: Update for Joins
			   }

			   $.post( ajaxurl, data, function ( response ) {
				   if ( response !== 'false' && response !== '0' ) {
					   $( ".gravityview_sort_field" ).empty().append( response ).prop( 'disabled', null );
				   }

				   resolve();
			   } );
		   });
	   },

	   /**
		* Hide metaboxes related to view configuration.
		* @return {void}
		*/
	   hideViewConfig: function () {
		   $( "#gravityview_view_config" ).slideUp( 150 );

		   $( document ).trigger( 'gv_admin_views_hideViewConfig' );
	   },

	   /**
		* Show metaboxes related to view configuration.
		* @return {void}
		*/
	   showViewConfig: function () {
		   $( '#gravityview_view_config' ).slideDown( 150 );

		   viewGeneralSettings.metaboxObj.show();
		   viewConfiguration.toggleDropMessage();
		   viewConfiguration.init_tooltips();

		   $( document ).trigger( 'gv_admin_views_showViewConfig' );
	   },

	   /**
		* @param {jQueryEvent} e
		*/
	   switchView: function ( e ) {
		   e.preventDefault();
		   e.stopImmediatePropagation();

		   var vcfg = viewConfiguration;

		   vcfg.templateFilter( 'custom' );
		   vcfg.toggleViewTypeMetabox();
	   },

	   /**
		* Change which filters to show, depending on whether the form is Start Fresh or pre-existing forms
		* @param  {string} templateType Checks against the `data-filter` attribute of the HTML
		* @return {[type]}              [description]
		*/
	   templateFilter: function ( templateType ) {
		   $( ".gv-view-types-module" ).each( function () {
			   if ( $( this ).attr( 'data-filter' ) === templateType ) {
				   $( this ).parent().show();
			   } else {
				   $( this ).parent().hide();
			   }
		   } );
	   },

	   /**
		* Whether the current template selector is a `data-view-dropdown`.
		* @since 2.24
		* @return {boolean}
		* @private
		*/
	   _isViewDropDown: function () {
		   return viewConfiguration.wantedTemplate
			   && 'undefined' !== typeof viewConfiguration.wantedTemplate.data( 'view-data' );
	   },

	   /**
		* Returns the current template ID, based on the selector.
		*
		* If a section was provided (i.e. it uses a view drop down) it will return the template for that section.
		* If no section was provided, it will fall back to the directory template for backward compatibility.
		*
		* @since 2.24
		* @return {string} The template id.
		* @private
		*/
	   _getCurrentTemplateId() {
		   const section = this._getTemplateSection();

		   if ( section === null || section === 'directory') {
			   return this.currentDirectoryTemplate;
		   }

		   if ( section === 'single' ) {
			   return this.currentSingletemplate;
		   }

		   return '';
	   },

	   /**
		* Sets the current template ID based on the active section.
		* @since 2.24
		* @param {string} template_id The template ID.
		* @private
		*/
	   _setCurrentTemplateId( template_id ) {
		   const section = this._getTemplateSection();
		   if ( section === null || section === 'directory' ) {
			   this.currentDirectoryTemplate = template_id;
		   }
		   if ( section === null || section === 'single' ) {
			   this.currentSingletemplate = template_id;
		   }
	   },

	   /**
		* Returns the template ID from the selector.
		* @since 2.24
		* @param {boolean} use_base_template Whether to use the base template of a preset.
		* @return {string} The template ID on the selector.
		* @private
		*/
	   _getTemplateId: function ( use_base_template = false ) {
		   const $template = viewConfiguration.wantedTemplate;
		   if ( !$template ) {
			   return '';
		   }

		   let template_id = $template.data( use_base_template ? 'base-template' : 'templateid' );
		   if ( viewConfiguration._isViewDropDown() ) {
			   template_id = viewConfiguration.wantedTemplate.val();
			   template_id = String( $template.val() );
		   }

		   return template_id;
	   },

	   /**
		* Returns the section from the selector.
		*
		* Only a view dropdown has a section, so this method helps ease the difference with the "old" selector.
		*
		* @since 2.24
		* @return {null|string} The section.
		* @private
		*/
	   _getTemplateSection: function() {
		   let section = null;
		   if ( viewConfiguration._isViewDropDown() ) {
			   section = viewConfiguration.wantedTemplate.data( 'section' );
		   }

		   return section;
	   },

	   /**
		* Restores the value on the selector.
		*
		* This is only relevant for view dropdowns.
		*
		* @since 2.24
		* @private
		*/
	   _restoreValue: function() {
		   if ( viewConfiguration._isViewDropDown() ) {
			   viewConfiguration.wantedTemplate.data( 'view-data' ).restoreValue();
		   }
	   },

	   /**
		* Stores the value on the selector.
		*
		* If the current selector is not a view dropdown, we store all view dropdowns, since all are updated.
		*
		* @since 2.24
		* @private
		*/
	   _storeValue: function() {
		   if ( !viewConfiguration.wantedTemplate ) {
			   return;
		   }

		   if ( viewConfiguration._isViewDropDown() ) {
			   viewConfiguration.wantedTemplate.data( 'view-data' ).storeValue();
		   } else {
			   // Persist all current values for dropdowns.
			   $( 'select[data-view-dropdown]' ).each( function () {
				   $( this ).data( 'view-data' ).storeValue();
			   } );
		   }

		   this._setCurrentTemplateId( this._getTemplateId() );
	   },

	   /**
		* @param {jQueryEvent} e
		*/
	   selectTemplate: function( e, data ) {
		   var vcfg = viewConfiguration;

		   // Prevent recursive selection.
		   if (data !== undefined && data.section === null) {
			   return;
		   }

		   e.preventDefault();
		   e.stopImmediatePropagation();

		   // get selected template
		   vcfg.wantedTemplate = $( this );
		   var selectedTemplateId = vcfg._getTemplateId();
		   var regexMatch = /(.*?)_(.*?)$/i;
		   var currentTemplate = vcfg._getCurrentTemplateId();
		   var currTemplateIdSlug = currentTemplate.replace( regexMatch, '$2' );
		   var selectedTemplateIdSlug = selectedTemplateId.replace( regexMatch, '$2' );
		   var slugmatch = ( selectedTemplateIdSlug === currTemplateIdSlug );

		   // check if template is being changed
		   if ( ! currentTemplate || slugmatch || ! vcfg.getConfiguredFields().length ) {
			   $( '#gravityview_select_template' ).slideUp( 150 );
			   vcfg.selectTemplateContinue( slugmatch );
			   vcfg._storeValue();
		   } else if ( currentTemplate !== selectedTemplateId ) {
			   // warn if fields are configured
			   if ( vcfg.getConfiguredFields().length ) {
				   // Tell the user what will actually happen: a restore of their
				   // previous configuration, or a migration into the new layout.
				   const willRestore = !! vcfg._getRestorableSnapshot( selectedTemplateId );

				   $( '#gravityview_switch_template_dialog' )
					   .find( '.gv-switch-template--migrate' ).toggle( ! willRestore ).end()
					   .find( '.gv-switch-template--restore' ).toggle( willRestore );

				   vcfg.showDialog( '#gravityview_switch_template_dialog' );
			   } else {
				   vcfg.toggleViewTypeMetabox();
				   vcfg.selectTemplateContinue( slugmatch );
			   }
		   } else {
			   // revert back to how things were before clicking "use a form preset"
			   vcfg.toggleViewTypeMetabox();
			   vcfg.showViewConfig();
		   }
	   },

	   selectTemplateContinue: function ( slugmatch ) {

		   var vcfg = viewConfiguration,
			   selectedTemplateId = vcfg._getTemplateId(),
			   selectedFormId = vcfg.gvSelectForm.val(),
			   changeAllSection = !vcfg._getTemplateSection();

		   if ( changeAllSection ) {
			   var base_template = vcfg._getTemplateId();
			   $( "#gravityview_directory_template" ).val( base_template ).trigger( 'change', { section: null } );
			   $( "#gravityview_single_template" ).val( base_template ).trigger( 'change', { section: null } );
		   }

		   //add Selected class
		   var $parent = vcfg.wantedTemplate.parents( ".gv-view-types-module" );
		   $parent.parents( ".gv-grid" ).find( ".gv-view-types-module" ).removeClass( 'gv-selected' );
		   $parent.addClass( 'gv-selected' );

		   vcfg.waiting('start');

		   // check for start fresh context
		   if ( vcfg.startFreshStatus ) {
			   Promise.all( [
				   // fetch preset form fields
				   vcfg.getAvailableFields( 'preset', selectedTemplateId ),
				   // fetch present View fields
				   vcfg.getPresetFields( selectedTemplateId ),
				   // fetch sortable fields
				   vcfg.getSortableFields( 'preset', selectedTemplateId ) ]
			   ).then( function () {
				   $( '.ui-tabs-panel' ).each( function () {
					   vcfg.init_droppables( this );
				   } );
			   } );
		   } else {

			   if( ! slugmatch || changeAllSection ) {
				   // Keep the outgoing configuration around so switching back can restore it.
				   vcfg.snapshotCurrentTemplate();

				   //change view configuration active areas
				   vcfg.updateActiveAreas( selectedTemplateId, ( selectedFormId * 1 ) );
			   } else {
				   vcfg.waiting('stop');
			   }

			   if ( changeAllSection ) {
				   vcfg.gvSwitchView.fadeIn( 150 );
				   vcfg.toggleViewTypeMetabox();
			   }
		   }

		   vcfg.currentTemplateId = selectedTemplateId;
		   vcfg.setUnsavedChanges( true );
	   },

	   server_request: ( ajaxRoute, payload ) => {
		   const defer = $.Deferred();

		   viewConfiguration.performingAjaxAction = true;

		   $( '.gv-view-template-notice' ).hide();

		   const { _wpNonce: nonce, _wpAjaxAction: action, _wpAjaxUrl: url, ajaxRouter, frontendFoundationVersion } = gvGlobals.foundation_licenses_router;

		   const request = {
			   nonce,
			   action,
			   ajaxRouter,
			   ajaxRoute,
			   frontendFoundationVersion,
			   payload
		   };

		   $.post( url, request )
			   .fail( (response) => {
				   viewConfiguration.performingAjaxAction = false;

				   defer.reject( response.responseText );
			   } )
			   .done( (response) => {
				   if ( !response.success ) {
					   viewConfiguration.performingAjaxAction = false;

					   defer.reject( response.data );

					   return;
				   }

				   viewConfiguration.performingAjaxAction = false;

				   defer.resolve( response );
			   } );

		   return defer.promise();
	   },

	   /**
		* When clicking the hover overlay, select the template by clicking the #gv_select_template button
		* @param  {jQueryEvent}    e     jQuery event object
		*/
	   selectTemplateHover: function ( e ) {
		   const vcfg = viewConfiguration;
		   const $link = $( e.target );
		   const $parent = $link.parents( '.gv-view-types-module' );
		   const $select = $( this ).find( '.gv_select_template' );

		   // If we're internally linking
		   if ( $link.is( '[rel=internal]' ) && ( !$link.hasClass( 'gv-layout-activate' ) && !$link.hasClass( 'gv-layout-install' ) ) ) {
			   return true;
		   }

		   e.preventDefault();
		   e.stopImmediatePropagation();

		   const on_fail = ( error ) => {
			   $( '.gv-view-template-notice' ).show().find( 'p' ).html( error );

			   document.querySelector( '.gv-view-template-notice' ).scrollIntoView( {
				   behavior: 'smooth'
			   } );
		   };

		   const do_always = () => {
			   vcfg.performingAjaxAction = false;
			   $link.removeClass( 'disabled' );
			   $parent.removeClass( 'active' );
		   };

		   const on_success = () => {
			   $parent.find( '.gv-view-types-hover > div:eq(0)' ).hide();
			   $parent.find( '.gv-view-types-hover > div:eq(1)' ).removeClass( 'hidden' );
			   $parent.removeClass( 'gv-view-template-placeholder' );
			   $parent.find( 'a.gv_select_template' ).attr( 'data-templateid', $link.data( 'templateid' ) ).trigger( 'click' );
			   vcfg.activateViewSelection( $link.data( 'templateid' ) );
			   $select.trigger( 'click' );
		   };

		   // Activate layout
		   if ( $link.hasClass( 'gv-layout-activate' ) ) {
			   if ( vcfg.performingAjaxAction ) {
				   return;
			   }

			   $parent.addClass( 'active' );
			   $link.addClass( 'disabled' ).attr( 'disabled', true );
			   $link.html( $spinner );

			   $.when( vcfg.server_request( 'activate_product', {
					   text_domain: $link.attr( 'data-template-text-domain' ),
				   } ) )
				   .then( on_success )
				   .always( do_always )
				   .fail( on_fail );

			   return;
		   }

		   // Install layout
		   if ( $link.hasClass( 'gv-layout-install' ) ) {
			   if ( vcfg.performingAjaxAction ) {
				   return;
			   }

			   $parent.addClass( 'active' );
			   $link.addClass( 'disabled' ).attr( 'disabled', true );
			   $link.html( $spinner );

			   $.when( vcfg.server_request( 'install_product', {
					   id: $link.attr( 'data-download-id' ),
					   text_domain: $link.attr( 'data-template-text-domain' ),
					   activate: true,
				   } ) )
				   .then( on_success )
				   .always( do_always )
				   .fail( on_fail );

			   return;
		   }
	   },

	   enableLockedTemplate: function ( e, data ) {
		   const $option = $( data?.option ) || null;
		   const action = data?.action || null;
		   const payload = {
			   text_domain: $option.data( 'template-text-domain' ),
			   activate: true,
		   };

		   if ( JSON.stringify( payload ) !== '{}' ) {
			   const $pill = $( e.target );
			   const $item = $pill.closest( '.view-dropdown-list-item' );

			   $pill.addClass( 'is-idle' ).html( $spinner );
			   $item.addClass( 'is-idle' );

			   $.when( viewConfiguration.server_request( action + '_product', payload ) )
				   .then( () => {
					   $pill.removeClass( 'has-failed' );

					   viewConfiguration.activateViewSelection( $option.data('template-id') );

					   data?.dropdown?.focusActive();
				   } )
				   .fail( ( error ) => {
					   $pill.addClass( 'has-failed' ).text( 'Error' );
					   console.log( error );
				   } )
				   .always( () => {
					   $pill.removeClass( 'is-idle' );
					   $item.removeClass( 'is-idle' );
				   } );
		   }
	   },

	   /**
		* Activates a selection on all view selectors after installation or activation.
		*
		* @since $ver$
		*
		* @param {String} template_id The template ID.
		*/
	   activateViewSelection: function ( template_id ) {
		   // We need to update all view selectors on the page.
		   const $view_selectors = $( '[data-view-dropdown]' );
		   const $options = $view_selectors.find( 'option[data-template-id="' + template_id + '"]' );

		   $options.attr( 'disabled', false );
		   $options.val( template_id );

		   // Refresh the selectors with the updated values.
		   $view_selectors.each( ( _, el ) => {
			   const dropdown = $( el ).viewDropdown();
			   dropdown.renderOptions();
		   } );

		   viewConfiguration.updateSettingsArea();
	   },

	   openExternalLinks: function () {

		   if ( !! window.Beacon && ( $( this ).is( '[data-beacon-article]' ) || $( this ).is( '[data-beacon-article-modal]' ) || $( this ).is( '[data-beacon-article-sidebar]' ) || $( this ).is( '[data-beacon-article-inline]' ) ) ) {
			   return false;
		   }

		   window.open( this.href );
		   return false;
	   },

	   /**
		* Scroll to settings section without adding anchor to URL.
		*
		* Respects user's reduced motion preferences and moves focus to the settings section
		* for keyboard and screen reader users.
		*
		* @param {Event} e jQuery event object
		* @return {boolean} false to prevent default anchor behavior
		*/
		scrollToSettings: function( e ) {
			e.preventDefault();

			var $settings = $( '#gravityview_settings' );
			if ( ! $settings.length ) {
				return false;
			}

			var prefersReducedMotion = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
			var behavior = prefersReducedMotion ? 'instant' : 'smooth';

			// Scroll into view
			$settings[0].scrollIntoView( { behavior: behavior, block: 'start' } );

			// Make sure it's focusable, then focus it
			if ( ! $settings.is( ':focus' ) ) {
				$settings.attr( 'tabindex', '-1' ).trigger( 'focus' );
			}

			return false;
	   },

	   /**
		* Display a screenshot of the current template. Not currently in use.
		*
		* @todo REMOVE ?
		* @param  {jQueryEvent}    e     jQuery event object
		* @return void
		*/
	   previewTemplate: function ( e ) {
		   e.preventDefault();
		   e.stopImmediatePropagation();
		   var parent = $( e.currentTarget ).parents( ".gv-view-types-module" );
		   parent.find( ".gv-template-preview" ).dialog( {
			   dialogClass: 'wp-dialog gv-dialog',
			   appendTo: $( "#gravityview_select_template" ),
			   width: viewConfiguration.dialogWidth,
			   open: function () {
				   $( '<div class="gv-overlay" />' ).prependTo( '#wpwrap' );
			   },
			   close: function () {
				   $( this ).dialog( "option", "appendTo", parent );
				   $( '#wpwrap' ).find('> .gv-overlay' ).fadeOut( 'fast', function () {
					   $( this ).remove();
				   } );
			   },
			   closeOnEscape: true,
			   buttons: [
				   {
					   text: gvGlobals.label_close,
					   click: function () {
						   $( this ).dialog( 'close' );
					   }
				   }
			   ]
		   } );

	   },

	   /**
		* @param {string} template The selected template ID.
		* @param {int} form_id The selected form ID.
		*/
	   /**
		* Re-renders the zone configuration for a template.
		*
		* When switching back to a template whose field set is unchanged since we
		* stored it, the stored configuration is re-rendered as-is. Otherwise, the
		* outgoing template and live configuration are sent so the server migrates
		* the fields into the new layout's zones.
		*
		* @param {string} template The selected template ID.
		* @param {int} form_id The selected form ID.
		* @return {Promise} Resolves when the zones have been updated (or recovery ran).
		*/
	   updateActiveAreas: function ( template, form_id ) {
		   var vcfg = viewConfiguration;

		   const snapshot = vcfg._getRestorableSnapshot( template );

		   var data = {
			   action: 'gv_get_active_areas',
			   template_id: template,
			   form_id: form_id,
			   current_template_id: snapshot ? template : vcfg._getCurrentTemplateId(),
			   gv_fields: snapshot ? snapshot.gv_fields : vcfg._serializeFields(),
			   nonce: gvGlobals.nonce
		   };

		   return vcfg.updateViewConfig( data );
	   },

	   /**
		* Renders the current page again, but with possible changes; and replaces the settings section.
		*
		* @since $ver$
		*/
	   updateSettingsArea: function () {
		   const $settings_content = $('#gravityview_settings .inside');
		   $settings_content.html('');

		   $.get( document.URL, function ( response ) {
			   if ( response ) {
				   const $document = $(response);
				   $settings_content.html( $document.find( '#gravityview_settings .inside' ).html() );

				   viewGeneralSettings.refresh();
				   // Some plugins rely on this event to show or hide settings.
				   $( '#gravityview_directory_template' ).trigger( 'change' );
			   }
		   } );
	   },

	   /**
		* @param {string} template The template ID
		*/
	   getPresetFields: function ( template ) {
		   var vcfg = viewConfiguration;

		   $( "#directory-active-fields, #single-active-fields" ).children().remove();

		   var data = {
			   action: 'gv_get_preset_fields',
			   template_id: template,
			   nonce: gvGlobals.nonce
		   };

		   return vcfg.updateViewConfig( data );
	   },

	   /**
		* POST to AJAX and insert the returned field HTML into zone DOM
		*
		* @since 1.17.2
		* @param {object} data `action`, `template_id` and `nonce` keys
		*/
	   updateViewConfig: function ( data ) {
		   return new Promise( ( resolve, _reject ) => {
			   const vcfg = viewConfiguration;
			   const section = vcfg._getTemplateSection();
			   const update_directory = ( section === 'directory' || section === null );
			   const update_single = ( section === 'single' || section === null );

			   if ( update_directory ) {
				   $( "#directory-active-fields" ).children().remove();
			   }
			   if ( update_single ) {
				   $( "#single-active-fields" ).children().remove();
			   }

			   $.post( ajaxurl, data, function ( response ) {
				   var content = null;

				   if ( response ) {
					   if ( typeof response === 'object' ) {
						   content = response;
					   } else {
						   try {
							   content = JSON.parse( response );
						   } catch ( error ) {
							   console.error( 'GravityView: could not parse the View configuration response.', error, response );
						   }
					   }
				   }

				   // The zones were emptied above; saving now would lose the configuration.
				   if ( ! content ) {
					   vcfg.handleViewConfigError();
					   resolve();

					   return;
				   }

				   if ( update_directory ) {
					   // gv_get_active_areas responses have no header/footer keys; jQuery's
					   // .html( undefined ) is a no-op getter, so widgets survive a template
					   // switch on purpose. Preset responses do carry both keys.
					   $( '#directory-header-widgets' ).html( content.header );
					   $( '#directory-footer-widgets' ).html( content.footer );
					   $( '#directory-active-fields' ).append( content.directory );

					   // Update the template and form ID for all [data-templateid] buttons.
					   $( '#directory-header-widgets a[data-templateid], #directory-footer-widgets a[data-templateid]' )
						   .attr( 'data-templateid', data.template_id )
						   .attr( 'data-formid', data.form_id );
				   }

				   if ( update_single ) {
					   $( '#single-active-fields' ).append( content.single );
				   }

				   // The replaced zone markup has no sortable instances yet; reinitialize
				   // so fields are immediately draggable. Untouched panels are skipped by
				   // init_droppables' per-panel guard.
				   $( '.ui-tabs-panel' ).each( function () {
					   vcfg.init_droppables( this );
				   } );

				   vcfg.showViewConfig();
				   vcfg.waiting( 'stop' );

				   /**
					* Triggers after the AJAX is loaded for the zone
					* @since 2.10
					* @param {object} JSON response with `header` `footer` (widgets) `directory` and `single` (contexts) properties
					*/
				   $( document.body ).trigger( 'gravityview/view-config-updated', content, section );

				   resolve();
			   } ).fail( function () {
				   vcfg.handleViewConfigError();
				   resolve();
			   } );

			   vcfg.setUnsavedChanges( true );
		   });
	   },

	   /**
		* Recovers the editor UI when a zone configuration request fails.
		*
		* The zones are emptied before the request is sent, so saving in this state
		* would lose the configuration. Warn the user to reload without saving.
		*
		* @since $ver$
		*/
	   handleViewConfigError: function () {
		   viewConfiguration.waiting( 'stop' );
		   viewConfiguration.showViewConfig();

		   /**
			* Triggers when a zone configuration request failed and recovery ran.
			*
			* @since $ver$
			*/
		   $( document.body ).trigger( 'gravityview/view-config-error' );

		   window.alert( gvGlobals.config_load_error );
	   },

	   /**
		* Toggle the "loading" indicator
		* @since 1.16.5
		* @param {string} action "start" or "stop"
		*/
	   waiting: function( action ) {

		   $containers = $( '#wpwrap,.gv-fields' );

		   if( 'start' === action ) {
			   $containers.addClass('gv-wait');
		   } else {
			   $containers.removeClass('gv-wait');
		   }
	   },


	   // tooltips
	   remove_tooltips: function ( el ) {
		   if ( $( el || '.gv-add-field' ).is( ':ui-tooltip' ) ) {
			   $( '.gv-add-field' ).gvTooltip( 'destroy' ).off( 'click' );
		   }
	   },

	   init_tooltips: function (el) {

		   // Already initialized.
		   if ( 0 === $( el || '.gv-add-field', '#post' ).not( ':ui-tooltip' ).length ) {
			   return;
		   }

		   $( el || ".gv-add-field", '#post' ).gvTooltip( {
			   show:    150,
			   hide:    200,
			   content: function () {
				   // Is the field picker in single or directory mode?
				   var context = $( this ).attr( 'data-context' );
				   var formId = $( this ).attr( 'data-formid' ) || $( '#gravityview_form_id' ).val();
				   var templateId = $( '#gravityview_directory_template' ).val();

				   switch ( $( this ).attr( 'data-objecttype' ) ) {
					   case 'search':
					   case 'field':
						   // Show available fields according to the selected context (single or directory, general or advanced).
						   return $( '#' + context + '-available-fields-' + ( formId || templateId ) ).html();
					   case 'widget':
						   return viewConfiguration.filterWidgetsByTemplate( $( "#directory-available-widgets" ).html(), templateId );
				   }
			   },
			   close: function () {
				   $( this ).attr( 'data-tooltip', null );
			   },
			   open: function( _event, tooltip ) {
				   $( this )
					   .attr( 'data-tooltip', 'active' )
					   .attr( 'data-tooltip-id', $( this ).attr( 'aria-describedby' ) );

				   let $focus_item = $( 'input[type=search]', tooltip.tooltip );

				   // Widgets don't have a search field; select the first "Add Widget" button instead
				   if ( ! $focus_item.length) {
					   $focus_item = $( tooltip.tooltip ).find( '.close' ).first();
				   }

				   var activate_layout = 'list';

				   // If layout is coded in HTML, use it.
				   if ( $( tooltip ).find('.gv-items-picker-container[data-layout]').length ) {
					   activate_layout = $( tooltip ).find( '.gv-items-picker-container[data-layout]' ).attr( 'data-layout' );
				   } else {

					   // Otherwise, check for cookies
					   const layout_cookie = $.cookie( 'gv-items-picker-layout' );

					   if ( viewConfiguration.getCookieVal( layout_cookie ) ) {
						   activate_layout = layout_cookie;
					   }
				   }

				   viewConfiguration.setTooltipLayout( activate_layout );

				   // Smooth scroll to focus.
				   $focus_item[ 0 ].focus( { preventScroll: true } );
				   const box = $focus_item[ 0 ].getBoundingClientRect();

				   if ( box.y < 0 || box.y > document.body.getBoundingClientRect().height ) {
					   window.scrollTo(
						   {
							   left: window.scrollX,
							   top: window.scrollY + box.y + ( box.y < 0 ? -60 : 60 ),
							   behavior: 'smooth'
						   }
					   );
				   }
			   },
			   disabled: true, // Don't open on hover
			   position: {
				   my: "center bottom",
				   at: "center top-12"
			   },
			   tooltipClass: 'gravityview-item-picker-tooltip top'
		   } )
			   // add title attribute so the tooltip can continue to work (jquery ui bug?)
			   .attr( 'title', function() {
				   return $( this ).attr( 'title' ) || $( this ).data( 'title' ) || '';
			   } )
			   .on( 'mouseout focusout', e => e.stopImmediatePropagation() )
			   .on( 'click', function ( e, data ) {
				   // add title attribute so the tooltip can continue to work (jquery ui bug?)
				   $( this ).attr( "title", "" );

				   $( this ).data( 'before', null ); // reset "before" element.
				   if ( data?.before ) {
					   $( this ).data( 'before', data.before );
				   }

				   e.preventDefault();
				   //e.stopImmediatePropagation();

				   $( this ).gvTooltip( "open" );

			   } );

	   },

	   /**
		* Filters visible fields in the field picker tooltip when the value of the field filter search input changes (or is cleared)
		*
		* {@since 2.0.11}
		*
		* {@returns void}
		*/
	   setupFieldFilters: function( e ) {

		   /**
		    * Normalize string to remove accents/diacritics for comparison
		    * @param {string} str The string to normalize
		    * @return {string} The normalized string
		    */
		   var normalizeString = function( str ) {
			   if ( typeof str !== 'string' ) {
				   return '';
			   }
			   // Normalize to NFD (decomposed form) then remove combining diacritical marks
			   return str.normalize( 'NFD' ).replace( /[\u0300-\u036f]/g, '' ).toLowerCase();
		   };

		   var input = $( this ).val().trim(),
			   $tooltip = $( this ).parents( '.ui-tooltip-content' ),
			   $resultsNotFound = $tooltip.find( '.gv-no-results' );

		   // Allow closeTooltips to know whether to close the tooltip on escape
		   if( 'keydown' === e.type ) {
			   $( this ).attr( 'data-has-search', ( input.length > 0 ) ? input.length : null );
			   return; // Only process the filtering on keyup
		   }

		   $tooltip.find( '.gv-fields' ).show().filter( function () {

			   // Normalize the search input for accent-insensitive comparison
			   var normalizedInput = normalizeString( input );

			   // Get and normalize the field values
			   var fieldTitle = $( this ).find( '.gv-field-label' ).attr( 'data-original-title' ) || '';
			   var fieldId = $( this ).attr( 'data-fieldid' ) || '';
			   var parentLabel = $( this ).attr( 'data-parent-label' ) || '';

			   // Perform accent-insensitive matching
			   var match_title = normalizedInput === '' || normalizeString( fieldTitle ).indexOf( normalizedInput ) !== -1;
			   var match_id = normalizedInput === '' || normalizeString( fieldId ).indexOf( normalizedInput ) !== -1;
			   var match_parent = normalizedInput === '' || ( parentLabel && normalizeString( parentLabel ).indexOf( normalizedInput ) !== -1 );

			   return !( match_title || match_id || match_parent );
		   } ).hide();

		   if ( ! $tooltip.find( '.gv-fields:visible' ).length ) {
			   $resultsNotFound.show();
		   } else {
			   $resultsNotFound.hide();
		   }
	   },

	   /**
		* Refresh Gravity Forms tooltips (the real help tooltips)
		*/
	   refreshGFtooltips: function () {
		   $( ".gf_tooltip" ).gvTooltip( {
			   show: 500,
			   hide: 1000,
			   content: function () {
				   return $( this ).prop( 'title' );
			   }
		   } );
	   },

	   /**
		* Initializes Tom Select controls inside a View editor dialog.
		*
		* @since 3.0.0
		*
		* @param {Element} context Dialog element.
		* @return {void}
		*/
	   initTomSelectFields: function ( context ) {
		   if ( typeof window.TomSelect !== 'function' ) {
			   return;
		   }

		   $( context ).find( '.gv-tom-select:not(.tomselected)' ).each( function () {
			   const frameworkOptions = {
				   plugins: {
					   remove_button: {
						   title: gvGlobals.label_remove || 'Remove'
					   },
					   drag_drop: {}
				   },
				   maxOptions: null,
				   placeholder: $( this ).data( 'placeholder' ) || ''
			   };
			   const pickerConfig = viewConfiguration.getTomSelectPickerConfig( this );
			   const tomSelect = new window.TomSelect( this, viewConfiguration.mergeTomSelectOptions( frameworkOptions, pickerConfig ) );

			   tomSelect.on( 'change', function () {
				   viewConfiguration.syncTomSelectOptionOrder( tomSelect );
				   viewConfiguration.updateBulkActionsWidgetSummary( tomSelect );
			   } );

			   $( this ).closest( 'form' ).on( 'submit', function () {
				   viewConfiguration.syncTomSelectOptionOrder( tomSelect );
			   } );

			   viewConfiguration.initBulkActionsActionSelect( this, tomSelect );
			   viewConfiguration.initBulkActionDependentSetting( this, tomSelect );
			   viewConfiguration.updateBulkActionsWidgetSummary( tomSelect );
		   } );
	   },

	   /**
		* Returns picker config overrides from a select element.
		*
		* @since TBD
		*
		* @param {HTMLSelectElement} select Select element.
		* @return {Object}
		*/
	   getTomSelectPickerConfig: function ( select ) {
		   const configJson = select.getAttribute( 'data-picker-config' );

		   if ( ! configJson ) {
			   return {};
		   }

		   try {
			   return JSON.parse( configJson ) || {};
		   } catch ( e ) {
			   console.warn( 'Invalid GravityView picker_config JSON.', e );

			   return {};
		   }
	   },

	   /**
		* Merges framework TomSelect options with author picker config.
		*
		* @since TBD
		*
		* @param {Object} frameworkOptions Framework defaults.
		* @param {Object} pickerConfig     Author overrides.
		* @return {Object}
		*/
	   mergeTomSelectOptions: function ( frameworkOptions, pickerConfig ) {
		   const options = Object.assign( {}, frameworkOptions, pickerConfig );

		   if ( pickerConfig.plugins && typeof pickerConfig.plugins === 'object' && ! Array.isArray( pickerConfig.plugins ) ) {
			   options.plugins = Object.assign( {}, frameworkOptions.plugins || {}, pickerConfig.plugins );
		   }

		   return options;
	   },

	   /**
		* Initializes live option refresh for a dependent bulk action setting.
		*
		* @since TBD
		*
		* @param {HTMLSelectElement} select    Select element.
		* @param {Object}            tomSelect TomSelect instance.
		* @return {void}
		*/
	   initBulkActionDependentSetting: function ( select, tomSelect ) {
		   const dependsJson = select.getAttribute( 'data-depends-on' );

		   if ( ! dependsJson ) {
			   return;
		   }

		   let dependsOn = [];

		   try {
			   dependsOn = JSON.parse( dependsJson ) || [];
		   } catch ( e ) {
			   console.warn( 'Invalid GravityView depends_on JSON.', e );
			   return;
		   }

		   if ( ! Array.isArray( dependsOn ) || ! dependsOn.length ) {
			   return;
		   }

		   const $panel = $( select ).closest( '[data-bulk-action-settings-panel]' );
		   const actionKey = select.getAttribute( 'data-bulk-action-key' ) || $panel.attr( 'data-bulk-action-settings-panel' ) || '';
		   const settingKey = select.getAttribute( 'data-bulk-action-setting-key' ) || '';
		   const refresh = viewConfiguration.debounce( function () {
			   viewConfiguration.refreshBulkActionSettingOptions( $panel, actionKey, settingKey, tomSelect );
		   }, 250 );

		   $panel.on( 'change', ':input[name]', function () {
			   const changedKey = viewConfiguration.getBulkActionSettingKeyFromName( this.name, actionKey );

			   if ( changedKey && dependsOn.indexOf( changedKey ) !== -1 ) {
				   refresh();
			   }
		   } );
	   },

	   /**
		* Refreshes one bulk action setting's options from the server.
		*
		* @since TBD
		*
		* @param {jQuery} $panel     Action settings panel.
		* @param {string} actionKey  Action key.
		* @param {string} settingKey Setting key.
		* @param {Object} tomSelect  TomSelect instance.
		* @return {void}
		*/
	   refreshBulkActionSettingOptions: function ( $panel, actionKey, settingKey, tomSelect ) {
		   if ( ! actionKey || ! settingKey ) {
			   return;
		   }

		   tomSelect.disable();

		   $.ajax( {
			   type: 'POST',
			   url: window.ajaxurl || ajaxurl,
			   dataType: 'json',
			   data: {
				   action: 'gv_bulk_action_setting_options',
				   nonce: gvGlobals.nonce,
				   view_id: parseInt( $( '#post_ID' ).val(), 10 ) || 0,
				   action_key: actionKey,
				   setting_key: settingKey,
				   current_drawer_state: viewConfiguration.collectBulkActionDrawerState( $panel, actionKey )
			   }
		   } ).done( function ( response ) {
			   if ( ! response || ! response.success ) {
				   console.warn( 'GravityView bulk action setting options could not be refreshed.', response );
				   return;
			   }

			   viewConfiguration.replaceTomSelectOptions( tomSelect, response.data && response.data.options ? response.data.options : {} );
		   } ).fail( function ( jqXHR ) {
			   console.warn( 'GravityView bulk action setting options request failed.', jqXHR );
		   } ).always( function () {
			   tomSelect.enable();
		   } );
	   },

	   /**
		* Collects current setting values from an action settings drawer.
		*
		* @since TBD
		*
		* @param {jQuery} $panel    Action settings panel.
		* @param {string} actionKey Action key.
		* @return {Object}
		*/
	   collectBulkActionDrawerState: function ( $panel, actionKey ) {
		   const state = {};

		   $panel.find( ':input[name]' ).each( function () {
			   const settingKey = viewConfiguration.getBulkActionSettingKeyFromName( this.name, actionKey );

			   if ( ! settingKey || this.disabled ) {
				   return;
			   }

			   const isArray = /\[\]$/.test( this.name );
			   let value = $( this ).val();

			   if ( this.type === 'checkbox' && ! this.checked ) {
				   return;
			   }

			   if ( isArray ) {
				   if ( ! Array.isArray( state[ settingKey ] ) ) {
					   state[ settingKey ] = [];
				   }

				   if ( Array.isArray( value ) ) {
					   state[ settingKey ] = state[ settingKey ].concat( value );
				   } else if ( value !== null && value !== '' ) {
					   state[ settingKey ].push( value );
				   }

				   return;
			   }

			   state[ settingKey ] = value;
		   } );

		   return state;
	   },

	   /**
		* Returns a bulk action setting key parsed from an input name.
		*
		* @since TBD
		*
		* @param {string} name      Input name.
		* @param {string} actionKey Action key.
		* @return {string}
		*/
	   getBulkActionSettingKeyFromName: function ( name, actionKey ) {
		   const marker = '[' + actionKey + '][';
		   const markerIndex = name.indexOf( marker );

		   if ( markerIndex === -1 ) {
			   return '';
		   }

		   const afterMarker = name.substring( markerIndex + marker.length );
		   const endIndex = afterMarker.indexOf( ']' );

		   return endIndex === -1 ? '' : afterMarker.substring( 0, endIndex );
	   },

	   /**
		* Replaces TomSelect options while preserving still-valid selections.
		*
		* @since TBD
		*
		* @param {Object} tomSelect TomSelect instance.
		* @param {Object} options   Options keyed by value.
		* @return {void}
		*/
	   replaceTomSelectOptions: function ( tomSelect, options ) {
		   const currentValue = tomSelect.getValue();
		   const currentValues = Array.isArray( currentValue ) ? currentValue : [ currentValue ];
		   const optionKeys = Object.keys( options );
		   const allowed = {};

		   optionKeys.forEach( function ( key ) {
			   allowed[ key ] = true;
		   } );

		   tomSelect.clear( true );
		   tomSelect.clearOptions();

		   optionKeys.forEach( function ( key ) {
			   tomSelect.addOption( {
				   value: key,
				   text: String( options[ key ] )
			   } );
		   } );

		   const nextValues = currentValues.filter( function ( value ) {
			   return value && allowed[ value ];
		   } );

		   if ( tomSelect.settings.maxItems === 1 ) {
			   tomSelect.setValue( nextValues[ 0 ] || '', true );
		   } else {
			   tomSelect.setValue( nextValues, true );
		   }

		   tomSelect.refreshOptions( false );
	   },

	   /**
		* Debounces a function.
		*
		* @since TBD
		*
		* @param {Function} callback Callback.
		* @param {number}   wait     Wait time in milliseconds.
		* @return {Function}
		*/
	   debounce: function ( callback, wait ) {
		   let timeout = null;

		   return function () {
			   const args = arguments;
			   const context = this;

			   window.clearTimeout( timeout );
			   timeout = window.setTimeout( function () {
				   callback.apply( context, args );
			   }, wait );
		   };
	   },

	   /**
		* Initializes a Bulk Actions action selector whose options depend on configured View fields.
		*
		* @since 3.0.0
		*
		* @param {HTMLSelectElement} select    Original select element.
		* @param {Object}            tomSelect TomSelect instance.
		* @return {void}
		*/
	   initBulkActionsActionSelect: function ( select, tomSelect ) {
		   const requirementsJson = select.getAttribute( 'data-bulk-actions-field-requirements' );

		   if ( ! requirementsJson ) {
			   return;
		   }

		   let requirements = {};

		   try {
			   requirements = JSON.parse( requirementsJson ) || {};
		   } catch ( e ) {
			   requirements = {};
		   }

		   const options = {};

		   Object.keys( tomSelect.options ).forEach( function ( key ) {
			   options[ key ] = Object.assign( {}, tomSelect.options[ key ] );
		   } );

		   const state = {
			   tomSelect: tomSelect,
			   requirements: requirements,
			   options: options,
			   initialItems: tomSelect.items.slice( 0 ),
			   manuallyRemoved: {},
			   syncing: false
		   };

		   tomSelect.on( 'item_remove', function ( value ) {
			   if ( ! state.syncing ) {
				   state.manuallyRemoved[ value ] = true;
			   }
		   } );

		   $( select ).data( 'gvBulkActionsSelect', state );
		   this.updateBulkActionsActionSelect( state );
	   },

	   /**
		* Updates all initialized Bulk Actions action selectors.
		*
		* @since 3.0.0
		*
		* @return {void}
		*/
	   updateBulkActionsActionSelects: function () {
		   $( '.gv-tom-select[data-bulk-actions-field-requirements]' ).each( function () {
			   const state = $( this ).data( 'gvBulkActionsSelect' );

			   if ( state ) {
				   viewConfiguration.updateBulkActionsActionSelect( state );
			   }
		   } );
	   },

	   /**
		* Updates one Bulk Actions action selector.
		*
		* @since 3.0.0
		*
		* @param {Object} state Bulk Actions select state.
		* @return {void}
		*/
	   updateBulkActionsActionSelect: function ( state ) {
		   const configuredFields = this.getBulkActionsConfiguredFieldIds();
		   const tomSelect = state.tomSelect;

		   Object.keys( state.requirements ).forEach( function ( actionKey ) {
			   const requiredFields = Array.isArray( state.requirements[ actionKey ] ) ? state.requirements[ actionKey ].map( String ) : [];
			   const isAvailable = requiredFields.every( function ( fieldId ) {
				   return configuredFields.indexOf( fieldId ) !== -1;
			   } );

			   if ( ! isAvailable ) {
				   state.syncing = true;
				   tomSelect.removeItem( actionKey, true );
				   state.syncing = false;
				   tomSelect.removeOption( actionKey, true );

				   return;
			   }

			   if ( ! tomSelect.options[ actionKey ] && state.options[ actionKey ] ) {
				   tomSelect.addOption( state.options[ actionKey ] );
			   }

			   if (
				   state.initialItems.indexOf( actionKey ) !== -1
				   && ! state.manuallyRemoved[ actionKey ]
				   && tomSelect.items.indexOf( actionKey ) === -1
			   ) {
				   state.syncing = true;
				   tomSelect.addItem( actionKey, true );
				   state.syncing = false;
			   }
		   } );

		   tomSelect.refreshOptions( false );
		   tomSelect.refreshItems();
		   tomSelect.updateOriginalInput( { silent: true } );
		   this.syncTomSelectOptionOrder( tomSelect );
		   this.updateBulkActionsWidgetSummary( tomSelect );
	   },

	   /**
		* Updates the Bulk Actions widget summary in the View editor.
		*
		* @since 3.0.0
		*
		* @param {Object} tomSelect TomSelect instance.
		* @return {void}
		*/
	   updateBulkActionsWidgetSummary: function ( tomSelect ) {
		   const select = tomSelect && tomSelect.input;

		   if ( ! select || ! select.getAttribute( 'data-bulk-actions-widget-summary' ) ) {
			   return;
		   }

		   const labels = tomSelect.items.map( function ( value ) {
			   return tomSelect.options[ value ] ? tomSelect.options[ value ].text : '';
		   } ).filter( Boolean );
		   const summary = this.formatBulkActionsWidgetSummary( labels, select );
		   const $widget = $( select ).closest( '[data-fieldid="bulk_actions"]' );

		   if ( ! $widget.length ) {
			   return;
		   }

		   let $summary = $widget.find( '.gv-bulk-actions-widget-summary' );

		   if ( ! $summary.length ) {
			   let $fieldInfo = $widget.find( '.gv-field-info' );

			   if ( ! $fieldInfo.length ) {
				   $widget.find( 'h5' ).append( '<span class="gv-field-info"></span>' );
				   $fieldInfo = $widget.find( '.gv-field-info' );
			   }

			   $fieldInfo.html( '<span class="gv-bulk-actions-widget-summary"></span>' );
			   $summary = $widget.find( '.gv-bulk-actions-widget-summary' );
		   }

		   $summary.text( summary ).attr( 'aria-label', summary );
	   },

	   /**
		* Formats the Bulk Actions widget summary.
		*
		* @since 3.0.0
		*
		* @param {string[]}          labels Selected action labels.
		* @param {HTMLSelectElement} select Original select element.
		* @return {string}
		*/
	   formatBulkActionsWidgetSummary: function ( labels, select ) {
		   if ( ! labels.length ) {
			   return $( select ).data( 'bulkActionsSummaryEmpty' ) || 'No actions selected';
		   }

		   const visible = labels.slice( 0, 3 );
		   const remaining = labels.length - visible.length;

		   if ( remaining > 0 ) {
			   const template = 1 === remaining
				   ? $( select ).data( 'bulkActionsSummaryMoreOne' )
				   : $( select ).data( 'bulkActionsSummaryMoreMany' );

			   visible.push( ( template || '+[count] actions' ).replace( '[count]', remaining.toLocaleString() ) );
		   }

		   return visible.join( ', ' );
	   },

	   /**
		* Returns field IDs configured in the multiple-entry layout.
		*
		* @since 3.0.0
		*
		* @return {string[]} Configured field IDs.
		*/
	   getBulkActionsConfiguredFieldIds: function () {
		   const fields = [];

		   $( '#directory-active-fields .active-drop-field .gv-fields:not(.gv-bulk-actions-ghost-field)[data-fieldid]' ).each( function () {
			   const fieldId = String( $( this ).attr( 'data-fieldid' ) || '' );

			   if ( fieldId && fields.indexOf( fieldId ) === -1 ) {
				   fields.push( fieldId );
			   }
		   } );

		   return fields;
	   },

	   /**
		* Returns the active Bulk Actions widget row.
		*
		* @since 3.0.0
		*
		* @return {jQuery} Bulk Actions widget row.
		*/
	   getBulkActionsWidget: function () {
		   return $( '#directory-header-widgets [data-fieldid="bulk_actions"], #directory-footer-widgets [data-fieldid="bulk_actions"]' ).first();
	   },

	   /**
		* Returns where the Bulk Actions selection checkbox should be shown.
		*
		* @since 3.0.0
		*
		* @param {jQuery} $widget Bulk Actions widget row.
		* @return {string} first|last
		*/
	   getBulkActionsCheckboxPosition: function ( $widget ) {
		   const value = $widget.find( '[name$="[bulk_actions_checkbox_position]"]' ).val();

		   return 'last' === value ? 'last' : 'first';
	   },

	   /**
		* Creates the visual-only Bulk Actions selection checkbox row.
		*
		* @since 3.0.0
		*
		* @return {jQuery} Ghost field row.
		*/
	   createBulkActionsGhostField: function () {
		   const title = gvGlobals.label_bulk_actions_configure || 'Configure Bulk Actions';
		   const label = gvGlobals.label_bulk_actions_selection_checkbox || 'Selection Checkbox';
		   const summary = gvGlobals.label_bulk_actions_ghost_summary || 'Configured in the Bulk Actions widget';
		   const $field = $( '<div/>', {
			   class: 'gv-fields gv-bulk-actions-ghost-field',
			   title: title
		   } );
		   const $heading = $( '<h5/>', {
			   class: 'selectable gfield field-id-bulk-actions-selection-checkbox'
		   } );
		   const $controls = $( '<span/>', {
			   class: 'gv-field-controls'
		   } );
		   const $settingsButton = $( '<button/>', {
			   type: 'button',
			   class: 'gv-bulk-actions-ghost-settings',
			   title: title,
			   'aria-label': title
		   } ).append(
			   $( '<span/>', {
				   class: 'dashicons dashicons-admin-generic',
				   'aria-hidden': 'true'
			   } )
		   );
		   const $label = $( '<span/>', {
			   class: 'gv-field-label',
			   title: title
		   } ).append(
			   $( '<i/>', {
				   class: 'dashicons dashicons-list-view',
				   'aria-hidden': 'true'
			   } ),
			   $( '<span/>', {
				   class: 'gv-field-label-text-container'
			   } ).text( label )
		   );
		   const $info = $( '<span/>', {
			   class: 'gv-field-info'
		   } ).append(
			   $( '<span/>', {
				   class: 'gv-bulk-actions-ghost-field-summary'
			   } ).text( summary )
		   );

		   $controls.append( $settingsButton );
		   $heading.append( $controls, $label, $info );
		   $field.append( $heading );

		   return $field;
	   },

	   /**
		* Adds, removes, or moves the Bulk Actions ghost field row.
		*
		* @since 3.0.0
		*
		* @return {void}
		*/
	   updateBulkActionsGhostField: function () {
		   const $tableColumns = $( '#directory-active-fields .active-drop-field[data-areaid="directory_table-columns"]' ).first();
		   const $widget = viewConfiguration.getBulkActionsWidget();
		   const $ghostFields = $( '.gv-bulk-actions-ghost-field' );
		   const shouldDisplay = $tableColumns.length && $widget.length;

		   if ( ! shouldDisplay ) {
			   $ghostFields.remove();
			   viewConfiguration.toggleDropMessage();
			   viewConfiguration.toggleRemoveAllFields();

			   return;
		   }

		   let $ghostField = $ghostFields.first();

		   $ghostFields.not( $ghostField ).remove();

		   if ( ! $ghostField.length ) {
			   $ghostField = viewConfiguration.createBulkActionsGhostField();
		   }

		   const position = viewConfiguration.getBulkActionsCheckboxPosition( $widget );
		   const $realFields = $tableColumns.children( '.gv-fields:not(.gv-bulk-actions-ghost-field)' );

		   $ghostField.attr( 'data-bulk-actions-checkbox-position', position );

		   if ( 'last' === position || ! $realFields.length ) {
			   $ghostField.appendTo( $tableColumns );
		   } else {
			   $ghostField.insertBefore( $realFields.first() );
		   }

		   viewConfiguration.toggleDropMessage();
		   viewConfiguration.toggleRemoveAllFields();
	   },

	   /**
		* Opens the Bulk Actions widget settings from the ghost field row.
		*
		* @since 3.0.0
		*
		* @param {jQueryEvent} e Click or keydown event.
		* @return {void}
		*/
	   openBulkActionsGhostFieldSettings: function ( e ) {
		   if ( 'keydown' === e.type && 13 !== e.keyCode && 32 !== e.keyCode ) {
			   return;
		   }

		   e.preventDefault();
		   e.stopImmediatePropagation();

		   const $widget = viewConfiguration.getBulkActionsWidget();
		   const $settingsButton = $widget.find( '.gv-field-settings' ).first();

		   if ( $settingsButton.length ) {
			   $settingsButton.trigger( 'click' );
		   }
	   },

	   /**
		* Keeps the original multiselect option order aligned with TomSelect item order.
		*
		* @since 3.0.0
		*
		* @param {Object} tomSelect TomSelect instance.
		* @return {void}
		*/
	   syncTomSelectOptionOrder: function ( tomSelect ) {
		   const select = tomSelect && tomSelect.input;

		   if ( ! select || ! select.multiple ) {
			   return;
		   }

		   const options = Array.prototype.slice.call( select.options );
		   const findOption = function ( value ) {
			   return options.filter( function ( option ) {
				   return option.value === value;
			   } )[ 0 ] || null;
		   };

		   tomSelect.items.forEach( function ( value ) {
			   const option = findOption( value );

			   if ( option ) {
				   select.appendChild( option );
			   }
		   } );
	   },


	   /**
		* Get fields configured in each context
		*
		* @return array
		*/
	   getConfiguredFields: function () {
		   const section = viewConfiguration._getTemplateSection();
		   const selectors = {
			   'directory': '#directory-active-fields',
			   'single': '#single-active-fields',
		   };

		   const selector = selectors.hasOwnProperty( section )
			   ? selectors[ section ]
			   : '#directory-active-fields, #single-active-fields, #edit-active-fields';

		   return $( selector ).find( '.gv-fields:not(.gv-bulk-actions-ghost-field)' );
	   },

	   /**
		* Returns a stable fingerprint of the configured fields (sorted unique IDs).
		*
		* Reads every `fields[<zone>][<uid>][...]` input inside the whole #post form —
		* the same scope `_serializeFields()` snapshots — so adding or removing a field
		* in ANY zone (including Edit Entry) invalidates stored snapshots. Migration
		* preserves the unique ID keys, so an unchanged fingerprint means no fields
		* were added or removed since a snapshot was taken. Zones are deliberately
		* excluded from the hash: the same fields in different zones is still "no change".
		*
		* @since $ver$
		* @return {string} The fingerprint.
		*/
	   getFieldsFingerprint: function () {
		   const ids = {};

		   $( '#post' ).find( viewConfiguration.fieldsInputsSelector ).each( function () {
			   const match = ( this.name || '' ).match( /^fields\[[^\]]+\]\[([^\]]+)\]/ );

			   if ( match ) {
				   ids[ match[ 1 ] ] = true;
			   }
		   } );

		   return Object.keys( ids ).sort().join( ',' );
	   },

	   /**
		* Returns the snapshot storage key for a template, scoped by the active section.
		*
		* @since $ver$
		* @param {string} template The template ID.
		* @return {string} The storage key.
		*/
	   _snapshotKey: function ( template ) {
		   return ( viewConfiguration._getTemplateSection() || 'all' ) + '::' + template;
	   },

	   /**
		* Serializes the current field configuration, in the same format the Save flow posts.
		*
		* Deliberately scoped to the whole #post form, not just the directory/single
		* containers: the Edit Entry zone inputs must ride along so they survive the
		* migration round trip unchanged.
		*
		* @since $ver$
		* @return {string} The serialized `fields[...]` inputs.
		*/
	   _serializeFields: function () {
		   return $( '#post' ).find( viewConfiguration.fieldsInputsSelector ).serialize();
	   },

	   /**
		* Returns the stored configuration for a template when restoring it is safe.
		*
		* Restoring is safe when no fields were added or removed since the snapshot
		* was taken: migration preserves field unique IDs, so an unchanged
		* fingerprint guarantees the same field set.
		*
		* @since $ver$
		* @param {string} template The template ID being switched to.
		* @return {object|null} The snapshot, or null when none is restorable.
		*/
	   _getRestorableSnapshot: function ( template ) {
		   const vcfg = viewConfiguration;
		   const snapshot = vcfg.templateSnapshots[ vcfg._snapshotKey( template ) ];

		   if ( snapshot && snapshot.fingerprint === vcfg.getFieldsFingerprint() ) {
			   return snapshot;
		   }

		   return null;
	   },

	   /**
		* Stores the current field configuration for the active template.
		*
		* When switching back to this template later, the stored configuration is
		* re-rendered by the server instead of migrating the then-current fields,
		* so the exact arrangement is restored.
		*
		* @since $ver$
		*/
	   snapshotCurrentTemplate: function () {
		   const vcfg = viewConfiguration;
		   const template = vcfg._getCurrentTemplateId();

		   if ( ! template ) {
			   return;
		   }

		   vcfg.templateSnapshots[ vcfg._snapshotKey( template ) ] = {
			   gv_fields: vcfg._serializeFields(),
			   fingerprint: vcfg.getFieldsFingerprint()
		   };
	   },

	   /**
		* Fetch the Available Fields for a given Form ID or Preset Template ID
		* @param  {null|string}    preset
		* @param  {string}    templateid      The "slug" of the View template
		* @return void
		*/
	   getAvailableFields: function( preset, templateid ) {
		   return new Promise( ( resolve, _reject ) => {
			   var vcfg = viewConfiguration;

			   vcfg.toggleDropMessage();

			   vcfg.getConfiguredFields().remove();

			   var data = {
				   action: 'gv_available_fields',
				   nonce: gvGlobals.nonce,
			   };

			   if ( preset !== undefined && 'preset' === preset ) {
				   data.form_preset_ids = [ templateid ];
			   } else {
				   /**
					* TODO: Update to support multiple fields in Joins
					* @see GravityView_Ajax::gv_available_fields()
					* */
				   data.form_preset_ids = [ vcfg.gvSelectForm.val() ];
			   }

			   // Do not fetch fields if we already have them for the given form or template
			   if ( $( '#directory-available-fields-' + data.form_preset_ids[ 0 ] ).length ) {
				   return;
			   }

			   $.post( ajaxurl, data, function ( response ) {
				   if ( !response.success && !response.data ) {
					   resolve();
				   }

				   $.each( response.data, function ( context, markup ) {
					   $( '#' + context + '-fields' ).append( markup );
				   } );

				   resolve();
			   } );
		   } );
	   },

	   /**
		* When a field is clicked in the field picker, add the field or add all fields
		* @param  {jQueryEvent} e [description]
		* @return {void}
		*/
	   startAddField: function ( e ) {

		   // If the clicked field div contains the all fields label,
		   // we're dealing with an all fields click!
		   if ( $( this ).has( '.field-id-all-fields' ).length ) {
			   viewConfiguration.addAllFields( $( this ) );
		   } else {
			   // Add a single field.
			   viewConfiguration.addField( $( this ), e );
		   }
	   },

	   /**
		* Add all the fields available at once. Bam!
		* @param  {object}    clicked jQuery object of the clicked "+ Add All Fields" link
		*/
	   addAllFields: function ( clicked ) {
		   const fields = clicked.siblings( '.gv-fields' ).filter( function () {
			   const field_id = $( this ).data( 'fieldid' );

			   return ( +field_id === parseInt( field_id, 10 ) );
		   } );

		   const triggerClick = ( el ) => {
			   return new Promise( ( resolve, _reject ) => {
				   $( document.body ).one( 'gravityview/field-added', function () {
					   resolve();
				   } );
				   $( el ).trigger( 'click' );
			   } );
		   };

		   ( async function () {
			   for ( let i = 0; i < fields.length; i++ ) {
				   await triggerClick( fields[ i ] );
			   }

			   $( 'a.gv-add-field[data-tooltip=\'active\']' ).gvTooltip( 'close' );
		   } )();
	   },

	   /**
		* Drop selected field in the active area
		* @param  {object} clicked jQuery DOM object of the clicked Add Field button
		* @param  {jQueryEvent}    e     jQuery Event object
		*/
	   addField: function ( clicked, e ) {
		   e.preventDefault();

		   const tooltipId = clicked.closest( '.ui-tooltip' ).attr( 'id' );
		   const $addButton = $( '.gv-add-field[data-tooltip-id="' + tooltipId + '"]' );
		   const $before = $addButton.data( 'before' );

		   viewConfiguration.placeField(
			   clicked,
			   $addButton,
			   $before,
			   !!$before
		   );

		   viewConfiguration.toggleAllowedOnceField( clicked );
	   },

	   /**
		* Add (a copy of the) selected field in the active area.
		* @param  {object} $field jQuery DOM object of the clicked field to place.
		* @param  {object} $addButton jQuery DOM object of the add-button that belongs to the field.
		* @param  {object|undefined} $anchor (optional) jQuery DOM object to place the new field after.
		* @param  {Boolean} add_before_anchor Whether to place field before the anchor field. Will be `after` for `false`.
		*/
	   placeField: function ( $field, $addButton, $anchor, add_before_anchor = false ) {
		   const vcfg = viewConfiguration;
		   const $newField = $field.clone().hide();
		   const templateId = $addButton.attr( 'data-templateid' ) ?? $addButton.parents( '.gv-section' ).find( '.view-template-select select' ).val() ?? $( "#gravityview_directory_template" ).val();

		   const data = {
			   action: 'gv_field_options',
			   template: templateId,
			   area: $addButton.attr( 'data-areaid' ),
			   context: $addButton.attr( 'data-context' ),
			   field_id: $newField.attr( 'data-fieldid' ),
			   field_label: $newField.find( '.gv-field-label' ).attr( 'data-original-title' ),
			   field_type: $addButton.attr( 'data-objecttype' ),
			   input_type: $newField.attr( 'data-inputtype' ),
			   form_id: parseInt( $field.attr( 'data-formid' ), 10 ) || vcfg.currentFormId,
			   view_id: parseInt( $( '#post_ID' ).val(), 10 ) || 0,
			   nonce: gvGlobals.nonce
		   };

		   // Get the HTML for the Options <div>
		   // - If there are no options, response will NULL
		   // - If response is false, it means the request was invalid.
		   $.ajax( {
			   type: "POST",
			   url: ajaxurl,
			   data: data,
			   async: true,
			   beforeSend: function () {
				   // Don't allow saving until this is done.
				   vcfg.disable_publish();
			   },
			   complete: function () {
				   // Enable saving after it's done
				   vcfg.enable_publish();
			   }
		   } ).done( function ( response ) {
			   // Retrieve the <HASH> ID from an input like: widgets[header_top][<HASH>][id]
			   const regex = /[^\[]+\[[^\]]+\]\[([^\]]+)\].*/i;

			   if ( $field.find( 'input.field-key' ).length > 0 ) {
				   $newField.find( '.gv-dialog, .gv-dialog-options' ).remove();

				   const oldId = $field.find( 'input.field-key' ).attr( 'name' ).replace( regex, '$1' );
				   const newId = response.match( regex, '$1' )[ 1 ] ?? null;

				   // Make the response a jQuery object.
				   response = $(response);

				   $field.find( '.gv-dialog-options :input' ).each( function ( _i, el ) {
					   if ( !$( el ).attr( 'name' ) ) {
						   return;
					   }

					   const $fields = response.find( '[name="' + $( el ).attr( 'name' ).replaceAll( '' + oldId, '' + newId ) + '"]' );

					   if ( $fields.length === 1 ) {
						   $fields.val( $( el ).val() );
					   } else if ($fields.length === 2) {
						   // Possible checkbox.
						   if ( $( el ).is( ':checked' ) ) {
							   $fields.prop( 'checked', true );
						   }
					   }
				   } );

			   }

			   // Add in the Options <div>
			   $newField.append( response );

			   $( '.ui-tabs-panel' ).each( function () {
				   vcfg.init_droppables( this );
			   } );

			   // If there are field options, show the settings gear.
			   if ( $( '.gv-dialog-options', $newField ).length > 0 ) {
				   $( '.gv-field-settings', $newField ).removeClass( 'hide-if-js' );
			   }

			   if ( $anchor ) {
				   // Append the new field after this element.
				   const insert_method = add_before_anchor ? 'insertBefore' : 'insertAfter';
				   $newField[ insert_method ]( $anchor );
			   } else {
				   // append the new field to the active drop
				   $addButton
					   .closest( '.gv-droppable-area' )
					   .find( '.active-drop' )
					   .first()
					   .append( $newField );
			   }

			   $( document.body ).trigger( 'gravityview/field-added', $newField );

			   // Show the new field
			   $newField.fadeIn( 100 );

			   // refresh the little help tooltips
			   vcfg.refreshGFtooltips();

		   } ).fail( function ( jqXHR ) {

			   // Enable publish on error
			   vcfg.enable_publish();

			   // Something went wrong
			   alert( gvGlobals.field_loaderror );

			   console.log( jqXHR );

		   } ).always( function () {

			   vcfg.toggleDropMessage();
			   vcfg.setUnsavedChanges( true );

		   } );
	   },
	   /**
		* Duplicate a field and add it under the duplicated field.
		* @since 2.22
		* @param  {jQueryEvent} e jQuery Event object
		*/
	   duplicateField: function ( e ) {
		   e.preventDefault();
		   const $field = $( this ).closest( '.gv-fields' );

		   viewConfiguration.placeField(
			   $field,
			   $( this ).closest( '.active-drop-container' ).find( 'a.gv-add-field' ),
			   $field
		   );
	   },

	   onTooltipOpen: function ( e, tooltip ) {
		   viewConfiguration.filterFields( e.target, tooltip.tooltip[0] );
	   },

	   /**
		* Toggles the allowed once class on a field.
		*
		* @since 2.42
		*
		* @param {Element} target The field element (or child there of).
		* @param {Boolean} toggle Whether to add or remove the class.
		*/
	   toggleAllowedOnceField: function ( target, toggle = true ) {
		   if ( !$( target ).is( '[data-allowed-once="true"]' ) ) {
			   target = $( target ).closest( '[data-allowed-once="true"]' );
		   }

		   $( target ).toggleClass( 'gv-field--is-added', toggle );
	   },

	   /**
		* Updates the field lists to
		* @param source
		* @param wrapper
		*/
	   filterFields: function ( source, wrapper ) {
		   const objectType = $( source ).attr( 'data-objecttype' ) || 'field';
		   const activeDropSelector = '.active-drop-' + objectType + ' [data-fieldid]';
		   const fields = $( source )
			   .closest( '.gv-section' )
			   .find( activeDropSelector )
			   .map(
				   ( _, el ) => $( el ).data( 'fieldid' )
			   )
			   .toArray();

		   let content = $( wrapper ).html();

		   if ( !content ) {
			   return;
		   }

		   const $content = $( '<div>' ).html( content );
		   $content
			   .find( '[data-allowed-once="true"]' )
			   .each( ( _, el ) => {
				   const is_added = fields.indexOf( $( el ).data( 'fieldid' ) ) > -1;
				   viewConfiguration.toggleAllowedOnceField( el, is_added );
			   } );

		   content = $content.html();
		   $( wrapper ).html( content );
	   },

	   /**
		* Filters widget picker content by the current directory template.
		*
		* @since 3.0.0
		*
		* @param {string} content Widget picker HTML.
		* @param {string} templateId Current directory template ID.
		* @return {string} Filtered widget picker HTML.
		*/
	   filterWidgetsByTemplate: function ( content, templateId ) {
		   if ( !content || !templateId ) {
			   return content;
		   }

		   const $content = $( '<div>' ).html( content );

		   $content.find( '[data-show-in-template]' ).each( function () {
			   const templates = ( $( this ).attr( 'data-show-in-template' ) || '' ).split( /\s+/ );

			   if ( templates.indexOf( templateId ) === -1 ) {
				   $( this ).remove();
			   }
		   } );

		   return $content.html();
	   },
	   /**
		* Re-initialize Merge Tags
		*
		* @since 1.22.1
		*/
	   refresh_merge_tags: function( $source, onRefresh ) {
		   let $merge_tag_supported = $source ? $( '.gv-merge-tag-support,.merge-tag-support', $source ) : $( '.gv-merge-tag-support:visible' );

		   $merge_tag_supported
			   .removeClass( 'gv-merge-tag-support mt-initialized' )
			   .addClass( 'merge-tag-support' );

		   // GF 2.6+
		   if ( window.gform?.instances?.mergeTags ) {
			   // Remove existing merge tags, since otherwise GF will add another
			   $( '.all-merge-tags', $source ).remove();

			   document.dispatchEvent( new Event( 'DOMContentLoaded' ) );

			   // Restore the namespaced classnames.
			   setTimeout( function() {
				   $merge_tag_supported
					   .removeClass( 'merge-tag-support' )
					   .addClass( 'gv-merge-tag-support' );

				   if ( onRefresh ) {
					   onRefresh();
				   }
			   }, 300 ); // This needs to be longer than the time it takes to perform the DOMContentLoaded event.

			   return;
		   }

		   // Only init merge tags if the View has been saved and the form hasn't been changed.
		   if ( 'undefined' !== typeof( form ) && $( document.body ).not( '.gv-form-changed' ) && $merge_tag_supported.length >= 0 ) {

			   if ( window.gfMergeTags ) {

				   // Remove existing merge tags, since otherwise GF will add another
				   $( '.all-merge-tags:visible' ).remove();

				   if ( gfMergeTags.hasOwnProperty( 'destroy' ) ) {

					   // 2.3 re-init
					   $merge_tag_supported.each( function () {
						   new gfMergeTagsObj( form, $( this ) );
					   } );

				   } else {

					   // Re-init merge tag dropdowns, pre-2.3
					   window.gfMergeTags = new gfMergeTagsObj( form );

				   }
			   }

			   $merge_tag_supported
				   .removeClass( 'merge-tag-support' )
				   .addClass( 'gv-merge-tag-support' );

			   if ( onRefresh ) {
				   onRefresh();
			   }
		   }
	   },

	   /**
		* Enable the publish input; enable saving a View
		* @return {void}
		*/
	   enable_publish: function () {

		   /**
			* Added in ~ WP 3.8
			* @see https://github.com/WordPress/WordPress/blob/master/wp-admin/js/post.js#L365-L367
			*/
		   $( document ).trigger( 'autosave-enable-buttons.edit-post' );

		   // Restore saving after settings are generated
		   $( '#publishing-action').find('#publish' ).prop( 'disabled', null ).removeClass( 'button-primary-disabled' );
	   },

	   /**
		* Disable the publish input; prevent saving a View
		* @return {void}
		*/
	   disable_publish: function () {

		   /**
			* Added in ~ WP 3.8
			* @see https://github.com/WordPress/WordPress/blob/master/wp-admin/js/post.js#L363-L364
			*/
		   $( document ).trigger( 'autosave-disable-buttons.edit-post' );

		   $( '#publishing-action').find('#publish' ).prop( 'disabled', 'disabled' ).addClass( 'button-primary-disabled' );
	   },

	   // Sortables and droppables
	   init_droppables: function ( panel ) {

		   // Already initialized.
		   if( $( panel ).find( ".active-drop-field" ).sortable( 'instance' ) ) {
			   return;
		   }

		   var vcfg = viewConfiguration;

		   // widgets
		   $( panel ).find( ".active-drop-widget" ).sortable( {
			   placeholder: "fields-placeholder",
			   items: '> .gv-fields',
			   distance: 2,
			   revert: 75,
			   connectWith: ".active-drop-widget",
			   start: function( _event, _ui ) {
				   $( '#directory-fields, #single-fields' ).find( ".active-drop-container-widget" ).addClass('is-receivable');
			   },
			   stop: function( _event, _ui ) {
				   $( '#directory-fields, #single-fields' ).find( ".active-drop-container-widget" ).removeClass('is-receivable');
			   },
			   change: function( _event, _ui ) {
				   vcfg.setUnsavedChanges( true );
			   },
			   receive: function( _event, ui ) {
				   // Check if field comes from another active area and if so, update name attributes.

				   var sender_area = ui.sender.attr( 'data-areaid' ), receiver_area = $( this ).attr( 'data-areaid' );

				   ui.item.find( '[name^="widgets[' + sender_area + ']"]' ).each( function () {
					   var name = $( this ).attr( 'name' );
					   $( this ).attr( 'name', name.replace( sender_area, receiver_area ) );
				   } );

				   vcfg.toggleDropMessage();
			   }
		   } );

		   //fields
		   $( panel ).find( ".active-drop-field" ).sortable( {
			   placeholder: "fields-placeholder",
			   items: '> .gv-fields:not(.gv-bulk-actions-ghost-field)',
			   distance: 2,
			   revert: 75,
			   connectWith: ".active-drop-field",
			   start: function( _event, _ui ) {
				   $( document.body ).find( ".active-drop-container-field" ).addClass('is-receivable');
			   },
			   stop: function( _event, _ui ) {
				   $( document.body ).find( ".active-drop-container-field" ).removeClass('is-receivable');
				   vcfg.updateBulkActionsGhostField();
			   },
			   change: function( _event, _ui ) {
				   vcfg.setUnsavedChanges( true );
			   },
			   receive: function( _event, ui ) {
				   // Check if field comes from another active area and if so, update name attributes.
				   if ( ui.item.find( ".gv-dialog-options" ).length > 0 ) {

					   var sender_area = ui.sender.attr( 'data-areaid' ), receiver_area = $( this ).attr( 'data-areaid' );

					   ui.item.find( '[name^="fields[' + sender_area + ']"]' ).each( function () {
						   var name = $( this ).attr( 'name' );
						   $( this ).attr( 'name', name.replace( sender_area, receiver_area ) );
					   } );

				   }

				   vcfg.toggleDropMessage();
				   vcfg.updateBulkActionsGhostField();
			   }
		   } );

		   // Search fields.
		   $( panel ).find( ".active-drop-search" ).sortable( {
			   placeholder: "fields-placeholder",
			   items: '> .gv-fields',
			   distance: 2,
			   revert: 75,
			   connectWith: ".active-drop-search",
			   start: function ( _, ui ) {
				   const allowedSections = ui.item.data( 'allowed-sections' );
				   let $containers = $( document.body ).find( '.active-drop-container-search' );

				   // Only add `is-receivable` if the field is allowed in the current section.
				   if ( allowedSections ) {
					   const sections = allowedSections.split( ',' );
					   $containers = $containers.filter( function () {
						   return sections.indexOf( $( this ).closest( '[data-grid-context]' ).data( 'grid-context' ) ) > -1;
					   } );
				   }

				   $containers.addClass( 'is-receivable' );
			   },
			   stop: function( _, _ui ) {
				   $( document.body ).find( ".active-drop-container-search" ).removeClass( 'is-receivable' );
			   },
			   change: function( ) {
				   vcfg.setUnsavedChanges( true );
			   },
			   receive: function ( _, ui ) {
				   // Check if field comes from another active area and if so, update name attributes.
				   if ( ui.item.find( ".gv-dialog-options" ).length > 0 ) {

					   const sender_area = ui.sender.attr( 'data-areaid' );
					   const receiver_area = $( this ).attr( 'data-areaid' );

					   ui.item.find( '[name*="[' + sender_area + ']"]' ).each( function () {
						   const name = $( this ).attr( 'name' );
						   $( this ).attr( 'name', name.replace( sender_area, receiver_area ) );
					   } );
				   }

				   const allowedSections = ui.item.data( 'allowed-sections' );
				   if ( allowedSections ) {
					   const $targetList = $( this ).closest( '[data-grid-context]' );
					   const targetContext = $targetList.data( 'grid-context' );
					   const allowedArray = allowedSections.split( ',' );

					   if ( !allowedArray.includes( targetContext ) ) {
						   // Prevent insertion.
						   $( ui.sender ).sortable( 'cancel' );
					   }
				   }

				   vcfg.toggleDropMessage();
			   },
			   over: function ( _, ui ) {
				   const $targetList = $( this ).closest( '[data-grid-context]' );
				   const targetContext = $targetList.data( 'grid-context' );

				   const allowedSections = ui.item.data( 'allowed-sections' );
				   if ( allowedSections ) {
					   const allowedArray = allowedSections.split( ',' );

					   if ( !allowedArray.includes( targetContext ) ) {
						   // // Disallow placeholder even showing.
						   ui.item.addClass( 'gv-field--not-allowed' );
					   }
				   }
			   },
			   out: function ( _, ui ) {
				   ui.item.removeClass( 'gv-field--not-allowed' );
			   }
		   } );
	   },

	   toggleDropMessage: function () {

		   $( '.active-drop' ).each( function () {
			   if ( $( this ).find( ".gv-fields" ).length > 0 ) {
				   $( this ).find( ".drop-message" ).hide();
			   } else {
				   $( this ).find( ".drop-message" ).fadeIn( 100 );
			   }
		   } );

	   },

	   /**
		* Event handler to remove Fields from active areas
		* @param {jQueryEvent} e
		*/
	   removeField: function ( e ) {

		   e.preventDefault();

		   var vcfg = viewConfiguration;
		   var area = $( e.currentTarget ).parentsUntil( ".active-drop" ).parent();

		   vcfg.setUnsavedChanges( true );

		   // Nice little easter egg: when holding down control, get rid of all fields in the zone at once.
		   if ( e.altKey && $( area ).find( '.gv-fields:not(.gv-bulk-actions-ghost-field)' ).length > 1 ) {
			   vcfg.removeAllFields( e, area );

			   return;
		   }

			$( e.currentTarget ).parentsUntil( '.gv-fields' ).parent().fadeOut( 'fast', function () {

			   $( this ).remove();

			   $( document.body ).trigger( 'gravityview/field-removed', $( this ) );

			   vcfg.toggleDropMessage();
		   } );

	   },

	   /**
		* Remove all fields from an area
		*
		* @param e
		* @param area If passed, the jQuery DOM object where .gv-fields are that should be removed.
		*/
	   removeAllFields: function ( e, area ) {

		   e.preventDefault();

		   // Show a confirm dialog
		   var remove_all = window.confirm( gvGlobals.remove_all_fields );

		   // If yes, remove all, otherwise don't do anything
		   if ( ! remove_all ) {
			   return;
		   }

		   area = area || null;

		   // If the area name hasn;
		   if ( ! area ) {
			   area_id = $( e.originalEvent.target ).data( 'areaid' );
			   area = $( e.originalEvent.target ).parents( 'div[data-areaid="' + area_id + '"]' )[ 0 ];
		   }

		   $( area ).find( '.gv-fields:not(.gv-bulk-actions-ghost-field)' ).remove();

		   $( document.body ).trigger( 'gravityview/all-fields-removed' );

		   viewConfiguration.toggleDropMessage();
	   },

	   toggleRemoveAllFields: function ( ) {
		   let has_fields = false;

		   $( ".active-drop:visible" ).each( function ( _index, item ) {
			   has_fields = ( $( this ).find( '.gv-fields:not(.gv-bulk-actions-ghost-field)' ).length > 1 );
			   $( '.clear-all-fields', $( item ).parents( '.gv-droppable-area' ) ).toggleClass( 'gv-hide', ! has_fields );
		   } );
	   },

	   /**
		* Event handler to open dialog with Field Settings
		* @param {jQueryEvent} e
		*/
	   openFieldSettings: function ( e ) {
		   e.preventDefault();

		   // Don't open a dialog for search field settings.
		   const $field = $( e.target ).closest( '[data-fieldid="search_bar"]' );
		   if ( $field.length && $( e.target ).closest( $field.find( '.gv-dialog-options' ) ).length ) {
			   return;
		   }

		   var parent, vcfg = viewConfiguration;

		   if ( $( e.currentTarget ).is( '.gv-fields' ) ) {
			   parent = $( e.currentTarget );
		   } else {
			   parent = $( e.currentTarget ).parents( '.gv-fields' );
		   }

		   $( '.gv-field-settings', parent ).first().addClass( 'active' );

		   vcfg.updateVisibilitySettings( e, true );

		   // Toggle checkbox when changing field visibility
		   $( document.body ).on( 'change', '.gv-fields input[type=checkbox]', vcfg.updateVisibilitySettings );

		   var buttons = [
			   {
				   text: gvGlobals.label_close,
				   class: 'button button-link',
				   click: function () {
					   $( this ).dialog( 'close' );
				   }
			   }
		   ];

		   // Just the first one in case of nested options.
		   const dialogOptions = parent.find( '.gv-dialog-options' ).first();

		   // Set up a one-time event handler to detect changes in this dialog
		   dialogOptions.find(':input').one( 'change', function() {
			   vcfg.setUnsavedChanges( true );
		   } );

		   vcfg.showDialog( dialogOptions, buttons );
	   },

	   /**
		* @param {jQueryEvent} e Check the "only visible to..." checkbox if the capability isn't public
		* @param {bool} first_run Is this the first run (on load)?
		*/
	   updateVisibilitySettings: function ( e, first_run ) {

		   var vcfg = viewConfiguration;

		   // Is this coming from the window opening?
		   first_run = first_run || false;

		   // If coming from the openFieldSettings method, we need a different parent
		   var $parent = $( e.currentTarget ).is( '.gv-fields' ) ? $( e.currentTarget ) : $( e.currentTarget ).parents( '.gv-fields' );

		   $( ".gv-setting-list", $parent ).trigger( 'change' );

		   $( 'input[type=checkbox]', $parent ).attr( 'disabled', null );

		   // Only mark as having unsaved changes if this is not the initial dialog opening
		   if ( ! first_run ) {
		       vcfg.setUnsavedChanges( true );
		   }
	   },

	   /**
		* Show/Hide Visibility of an input's container list item based on the value of a checkbox
		*
		* @param  {jQuery} $checkbox The checkbox to use when determining show/hide. Checked: show; unchecked: hide
		* @param  {jQuery} $toggled  The field whose container to show/hide
		* @param  {boolean} first_run Is this the first run (on load)? If so, show/hide immediately
		* @param  {boolean} inverse   Should the logic be flipped (unchecked = show)?
		* @return {void}
		*/
	   toggleVisibility: function ( $checkbox, $toggled, _first_run, inverse ) {

		   var speed = 0;

		   var checked = $checkbox.is( ':checked' );

		   checked = inverse ? ! checked : checked;

		   if ( checked ) {
			   $toggled.parents( '.gv-setting-container' ).fadeIn( speed );
		   } else {
			   $toggled.parents( '.gv-setting-container' ).fadeOut( speed );
		   }

	   },

	   /**
		* When the Publish/Update form is submitted
		*
		* - Make sure there is a GF Form selected. If doing Start Fresh, calls `createPresetForm()` to create the GF form for the template ID.
		* - Serializes the field data so that the request isn't too large
		*
		* @param  {jQueryEvent} e [description]
		* @return {boolean}   True: success; False: stuff didn't work out.
		*/
	   processFormSubmit: function ( e ) {
		   var vcfg = viewConfiguration;
		   var templateId = vcfg._getTemplateId();

		   if ( ! vcfg.validateSettingFields( e ) ) {
			   return false;
		   }

		   // Create the form if we're starting fresh.
		   // On success, this also sets the vcfg.startFreshStatus to false.
		   if ( vcfg.startFreshStatus ) {
			   vcfg.createPresetForm( e, templateId );
			   return false;
		   }

		   // If the View isn't a Start Fresh view, we just return true
		   // so that the click on the Publish button can process.
		   if ( !vcfg.startFreshStatus || templateId === '' ) {
			   vcfg.setUnsavedChanges( false );

			   // Serialize the inputs so that `max_input_vars`
			   return vcfg.serializeForm( e );
		   }

		   return false;

	   },

	   /**
		* @since {2.16}
		* @param {boolean} has_changes
		*/
	   setUnsavedChanges( has_changes ) {
		   viewConfiguration.hasUnsavedChanges = has_changes;
	   },

	   /**
		* Serialize all GV field data and submit it all as one field value
		*
		* To fix issues where there are too many array items, causing PHP max_input_vars threshold to be met
		*
		* @param  {jQueryEvent} e jQuery event object
		*
		* @return {boolean}
		*/
	   serializeForm: function ( e ) {

		   var $post = $('#post');
		   var serialized_data, $fields;

		   if ( $post.data( 'gv-valid' ) ) {
			   return true;
		   }

		   e.stopImmediatePropagation();

		   $post.data( 'gv-valid', false );

		   if ( $post.data( 'gv-serialized' ) ) {
			   // Guard against double serialization/remove attempts
			   serialized_data = $post.data( 'gv-serialized' );
		   } else {
			   // Get all the fields where the `name` attribute start with `fields`
			   $fields = $post.find( '[name^=fields]' ).filter(':input');

			   // Serialize the data
			   serialized_data = $fields.serialize();

			   // Don't include the fields in the $_POSTed data
			   $fields.prop( 'disabled', true );

			   $post.data( 'gv-serialized', serialized_data );
		   }

		   // Also exclude these fields from $_POST...
		   $post.find( '[name=gv_fields]' ).filter(':input').prop( 'disabled', true );

		   // ...instead, add a single field to the form that contains all the data.
		   $post.append( $( '<input/>', {
			   'name': 'gv_fields',
			   'value': serialized_data,
			   'type': 'hidden'
		   } ) );

		   $post.data( 'gv-valid', true );

		   // Submit the form natively. We avoid both jQuery's trigger('click')
		   // (pathologically slow in Safari with this many delegated handlers)
		   // and form.requestSubmit() (whose internal HTML5 constraint-validation
		   // walk takes ~9s in Safari with this many form controls, even when no
		   // required/pattern rules exist).
		   //
		   // Instead: fire a 'submit' event ourselves so any submit-time handlers
		   // (WP's updateText/timestamp validation, our own, etc.) still run, then
		   // call native form.submit() which is instant because it skips both the
		   // synthetic-click chain and constraint validation.
		   var form      = $post[ 0 ];
		   var submitter = ( 'click' === e.type && e.target ) ? e.target : null;

		   // Native form.submit() omits the submit button's name=value from the
		   // form data, so WordPress can't tell whether the user clicked Publish
		   // vs. Save Draft (post status won't transition). Re-inject it as a
		   // hidden input with a marker so we can clean it up on resubmit.
		   //
		   // i18n: WordPress's server-side post-status logic checks the submit
		   // button's HTML *name* attribute (e.g. "publish", "save"), which is
		   // not localized. submitter.value is localized ("Publish" / "Publicar"
		   // / "公開") but is irrelevant to the status-transition decision.
		   $post.find( 'input[type=hidden][data-gv-submitter]' ).remove();
		   if ( submitter && submitter.name ) {
			   var $hidden = $( '<input/>', {
				   type: 'hidden',
				   name: submitter.name,
				   value: submitter.value
			   } ).attr( 'data-gv-submitter', '1' );
			   $post.append( $hidden );
		   }

		   var submitEvent;
		   try {
			   // SubmitEvent supported in Safari 15+, all modern browsers.
			   submitEvent = new SubmitEvent( 'submit', { bubbles: true, cancelable: true, submitter: submitter } );
		   } catch ( err ) {
			   submitEvent = new Event( 'submit', { bubbles: true, cancelable: true } );
		   }

		   if ( form.dispatchEvent( submitEvent ) ) {
			   form.submit();
		   }

		   return false;
	   },

	   validateSettingFields: function ( e ) {

		   var $metabox = viewGeneralSettings.metaboxObj;

		   var $invalidFields = $metabox
			   .find( '[name^=template_settings].gv-error' )
			   .filter( function () {
				   // Get only active fields, i.e., those whose parent <tr> is not "display: none".
				   return $( this ).closest( 'tr.alternate' ).css( 'display' ) !== 'none';
			   } );

		   // If no invalid fields are found, return.
		   if ( ! $invalidFields.length ) {
			   return true;
		   }

		   // Prevent form submission.
		   e.stopImmediatePropagation();
		   e.preventDefault();

		   // Open the tab containing the invalid field.
		   var tabPanelId = $invalidFields.first().closest( 'div[role=tabpanel]' ).prop( 'id' );
		   var $tabLink = $metabox.find( '.ui-tab[aria-controls=' + tabPanelId + '] a.nav-tab' );
		   $tabLink.trigger( 'click' );

		   // Scroll to the Settings section.
		   window.scrollTo(
			   {
				   top: $metabox.offset().top,
				   behavior: 'smooth',
			   },
		   );

		   return false;
	   },

	   /**
		* Create a Gravity Forms form using a preset defined by the View Template selected during Start Fresh
		*
		* This is done just before the Publish click is registered.
		*
		* @see GravityView_Admin_Views::create_preset_form()
		*
		* @param {jQueryEvent} e
		* @param {string} templateId Template ID
		*
		* @return boolean|void
		*/
	   createPresetForm: function ( e, templateId ) {
		   var vcfg = viewConfiguration;
		   var $target = $( e.target );

		   e.stopPropagation();

		   // Try to create preset form in Gravity Forms. On success assign it to post before saving
		   var data = {
			   action: 'gv_set_preset_form',
			   template_id: templateId,
			   nonce: gvGlobals.nonce
		   };


		   $.ajax( {
			   type: "POST",
			   url: ajaxurl,
			   data: data,
			   async: false, // Allows returning the value. Important!

			   success: function ( response ) {

				   if ( response !== 'false' && response !== '0' ) {

					   vcfg.startFreshStatus = false;

					   //set the form id
					   vcfg.gvSelectForm.find( "option:selected" ).removeAttr( "selected" ).end().append( response );

					   // Continue submitting the form, since we preventDefault() above
					   if ( 'click' === e.type ) {
						   $target.trigger( 'click' );
					   } else {
						   $('#post').trigger('submit');
					   }

				   } else {

					   $target.before( '<div id="message" class="error below-h2"><p>' + gvGlobals.label_publisherror + '</p></div>' );

				   }

			   }
		   } );

		   return false;
	   }

   }; // end viewConfiguration object


   /**
	* Manages the General View Settings
	*
	* @since 1.7
	*
	* @type {{templateId: null, init: Function, updateSettingsDisplay: Function, toggleSetting: Function}}
	*/
   viewGeneralSettings = {

	   /**
		* Holds the current view type id (template)
		*/
	   templateId: null,

	   /**
		* Holds the tabbed Settings metabox container
		*/
	   metaboxObj: null,

	   /**
		* Init method
		*/
	   init: function() {

		   viewGeneralSettings.metaboxObj = $( '#gravityview_settings' );

		   // Init general settings tabs
		   viewGeneralSettings.initTabs();

		   // Conditional display general settings & trigger display settings if template changes
		   $('#gravityview_directory_template')
			   .on('change', viewGeneralSettings.updateSettingsDisplay );

		   $( document.body )
			   // Enable a setting tab (since 1.8)
			   .on('gravityview/settings/tab/enable', viewGeneralSettings.enableSettingTab )

			   // Disable a setting tab (since 1.8)
			   .on('gravityview/settings/tab/disable', viewGeneralSettings.disableSettingTab );

	   },

	   /**
		* Refreshes the tabs after HTML was replaced.
		*
		* @since $ver$
		*/
	   refresh: function () {
		   viewGeneralSettings.metaboxObj.trigger( 'change' );

		   viewGeneralSettings.metaboxObj.tabs( 'destroy' );

		   viewGeneralSettings.initTabs();
	   },

	   /**
		* Callback method to show/hide settings if template changes and settings have a specific template attribute
		*/
	   updateSettingsDisplay: function () {

		   viewGeneralSettings.templateId = $( this ).val();

		   $( '[data-show-if]' ).each( viewGeneralSettings.toggleSetting );

	   },

	   /**
		* Show/Hides setting based on the template
		*/
	   toggleSetting: function () {
		   var row = $( this ), templates = row.attr( 'data-show-if' );

		   // if setting field attribute is empty, leave..
		   if ( templates.length < 1 || ! viewGeneralSettings.templateId ) {
			   return;
		   }


		   if ( viewGeneralSettings.templateId.length > 0 && templates.indexOf( viewGeneralSettings.templateId ) > -1 ) {
			   row.show();
		   } else {
			   row.hide();
		   }

	   },

	   /**
		* Set up the settings metabox vertical tabs
		*
		* @since 1.8
		* @return {void}
		*/
	   initTabs: function() {

		   // Save the state on a per-post basis
		   let cookie_key = 'gv-active-setting-tab-' + $( '#post_ID' ).val();

		   // The default tab is the first (0)
		   let active_settings_tab = $.cookie( cookie_key );

		   if ( false === viewConfiguration.getCookieVal( active_settings_tab ) ) {
			   active_settings_tab = 0;
		   }

		   viewGeneralSettings.metaboxObj
			   // Force the sort metabox to be directly under the view configuration. Damn 3rd party metaboxes!
			   .insertAfter( $('#gravityview_view_config') )

			   // Make tabs
			   .tabs( {
				   active: active_settings_tab,
				   create: function( _event, ui ) {
					   // When the Custom Code tab is active on-load, we need a small amount of
					   // time before instantiating CodeMirror.
					   setTimeout( function() {
						   viewConfiguration.setupCodeMirror( ui.panel );
					   }, 50 );
				   },
				   activate: function( _event, ui ) {
					   // When the tab is activated, set a new cookie
					   $.cookie( cookie_key, ui.newTab.index(), {
						   path: gvGlobals.admin_cookiepath
					   } );

					   viewConfiguration.setupCodeMirror( ui.newPanel );
					   viewConfiguration.zebraStripeSettings();
				   }
			   } )
			   .addClass( "ui-tabs-vertical ui-helper-clearfix" )
			   .find('li')
			   .removeClass( "ui-corner-top" );

	   },

	   /**
		* After creating the Tabs we need to do a few tweaks to make it look good
		*
		* @since 1.8
		*
		* @param {jQueryEvent} event jQuery Event
		* @param {Object} ui jQuery UI Tab element, with: `ui.tab` and `ui.panel`
		*
		* @return {void}
		*/
	   tabsCreate: function( _event, _ui ){
		   var $container = $( this ),
			   $panels = $container.find( '.ui-tabs-panel' ),
			   max = [];

		   $panels.each( function(){
			   max.push( $( this ).outerHeight( true ) );
		   } ).css( { 'min-height': _.max( max ) } );
	   },

	   /**
		* Enable a tab in the settings metabox
		*
		* Useful for when switching View types that support a type of setting (DataTables)
		*
		* @since 1.8
		*
		* @param {jQueryEvent} e jQuery Event
		* @param {jQuery} tab DOM of tab to enable
		*
		* @return {void}
		*/
	   enableSettingTab: function( _e, tab ) {

		   viewGeneralSettings.metaboxObj
			   .tabs('enable', $( tab ).attr('id') );

	   },

	   /**
		* Disable a tab in the settings metabox
		*
		* Useful for when switching View types that may not support a type of setting (DataTables)
		*
		* @since 1.8
		*
		* @param {jQueryEvent} e jQuery Event
		* @param {jQuery} tab DOM of tab to enable
		*
		* @return {void}
		*/
	   disableSettingTab: function( _e, tab ) {

		   viewGeneralSettings.metaboxObj
			   .tabs('disable', $( tab ).attr('id') );

	   }

   };  // end viewGeneralSettings object


   /**
	* This nested function is necessary, otherwise, View editor tabs aren't properly initialized.
	*
	* I tried moving the code out of this function; it didn't work.
	*
	* I tried setTimeout; it didn't work.
	*
	* I tried moving window.gvAdminActions and $( document.body ).trigger( 'gravityview/loaded' );
	* outside of this function; it didn't work.
	*
	* This probably needs to stay here until we rewrite this beast.
	*
	* - Zack
	*/
   jQuery( function ( $ ) {

	   // title placeholder
	   $( '#title-prompt-text' ).text( gvGlobals.label_viewname );

	   // start the general view settings magic
	   viewGeneralSettings.init();

	   // start the View Configuration magic
	   viewConfiguration.init();

	   // Attach calendar popover to date fields (allows freetext like relative dates).
	   if ( jQuery.fn.attachCalendarPopover ) {
		   $( '.gv-datepicker' ).attachCalendarPopover( { showTodayButton: true } );
	   }

	   // Save the state on a per-post basis
	   var cookie_key = 'gv-active-tab-' + $( '#post_ID' ).val();

	   // The default tab is the first (0)
	   var activate_tab = $.cookie( cookie_key );

	   if ( false === viewConfiguration.getCookieVal( activate_tab ) ) {
		   activate_tab = 0;
	   }

	   if ( location.hash && $( location.hash ).length ) {
		   activate_tab = $( location.hash ).index() - 1;
	   }

	   // View Configuration - Tabs (and persist after refresh)
	   $( "#gv-view-configuration-tabs" ).tabs( {
		   active: activate_tab,
		   hide: false,
		   show: false,
		   create: function( _event, ui ) {
			   viewConfiguration.init_droppables( ui.panel );

			   /** @since 2.14.1 */
			   $( document.body )
				   .trigger( 'gravityview/tab-ready', ui.panel )
				   .trigger( 'gravityview/tabs-ready' );
		   },
		   activate: function( _event, ui ) {
			   // When the tab is activated, set a new cookie
			   $.cookie( cookie_key, ui.newTab.index(), { path: gvGlobals.cookiepath } );

			   viewConfiguration.init_droppables( ui.newPanel );

			   /** @since 2.14.1 */
			   $( document.body ).trigger( 'gravityview/tab-ready', ui.newPanel );
		   }
	   } );

	   const $embedShortcodeEl = $( '#gv-embed-shortcode' );
	   $( '#gravityview_se_is_secure' ).on( 'change', function () {
		   let embedShortcode = $embedShortcodeEl.val();
		   if ( !embedShortcode ) {
			   return;
		   }

		   if ( $( this ).is( ':checked' ) ) {
			   embedShortcode = embedShortcode.replace( /]$/, ` secret="${ $embedShortcodeEl.data( 'secret' ) }"]` );

		   } else {
			   embedShortcode = embedShortcode.replace( / secret="[^"]+"/, '' );
		   }

		   $embedShortcodeEl.val( embedShortcode );
	   } );

	   // Expose globally methods to initialize/destroy tooltips and to display dialog window
	   window.gvAdminActions = {
		   initTooltips: viewConfiguration.init_tooltips,
		   removeTooltips: viewConfiguration.remove_tooltips,
		   showDialog: viewConfiguration.showDialog,
		   initDroppables: viewConfiguration.init_droppables,
		   setCustomLabel: viewConfiguration.setCustomLabel,
		   ignoreEscape: viewConfiguration.ignoreEscape,
	   };

	   $( document.body ).trigger( 'gravityview/loaded' );
   } );

   /**
	* Handles CSV widget classes.
	* @since 2.21
	*/
   $( function () {
	   const $csv_enable = $( '#gravityview_se_csv_enable' );
	   const update_csv_widget_classes = function () {
		   $( '[data-fieldid="export_link"]' )
			   .toggleClass( 'csv-disabled', !$csv_enable.is( ':checked' ) )
			   .attr( 'aria-disabled', $csv_enable.is( ':checked' ) ? 'false' : 'true' )
		   ;
	   };

	   $csv_enable.on( 'change', update_csv_widget_classes );
	   update_csv_widget_classes();
   } );

   /**
	* Upgrade plugins support.
	*
	* @since 2.26
	*/
   $( function () {
	   const $spinner = $( '<svg class="loading" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z" fill="currentColor"></path></svg>' );

	   $( document ).on( 'click', '.gk-gravityview-placeholder-actions [data-action]', function ( e ) {
		   e.preventDefault();

		   if ( viewConfiguration.hasUnsavedChanges && !window.confirm( gvGlobals.discard_unsaved_changes ) ) {
			   return;
		   }

		   if ( $( this ).hasClass( 'is-idle' ) ) {
			   return;
		   }

		   $( this ).addClass( 'is-idle' ).html( $spinner );

		   const action = $( this ).data( 'action' ) + '_product';

		   const payload = {
			   text_domain: $( this ).data( 'text-domain' ),
			   activate: true,
		   };

		   const on_fail = () => $(this).removeClass( 'is-idle' ).addClass( 'is-error' ).text( 'Try again' );

		   $.when( viewConfiguration.server_request( action, payload ) )
			   .then( ( response ) => {
				   if ( ! response.success ) {
					   throw new Error();
				   }

				   // Refresh page on success.
				   document.location = document.location;
			   } )
			   .fail( on_fail );
	   } );
   } );
}(jQuery));
