( function ( $ ) {

	/**
	 * Get the group ID of the targeted element.
	 *
	 * @param {string} groupId The ID of the group to be set on the targeted element.
	 *
	 * @returns {jQuery}
	 */
	$.fn.setGroupId = function ( groupId ) {

		this.attr( 'data-groupId', groupId );

		this.each( function () {
			var field = getFieldByElement( $( this ) );
			if ( field ) {
				field.layoutGroupId = groupId;
			}
		} );

		return this;
	};

	/**
	 * Set the grid column span CSS property of the targeted element.
	 *
	 * @param {number} span The number of columns the targeted element should span.
	 *
	 * @returns {jQuery}
	 */
	$.fn.setGridColumnSpan = function ( span ) {

		if ( span === null ) {
			this.css( 'grid-column', 'auto / auto' );
			return this;
		}

		var field;

		this.css( 'grid-column', 'span {0}'.format( span ) );

		this.each( function () {
			// Spacer fields are pseudo-fields; they are generated when the last field in the group is resized and are
			// rendered based on that field's layoutSpacerGridColumnSpan property.
			if ( $( this ).hasClass( 'spacer' ) ) {
				var $prev = $( this ).prev( '.gfield' );
				field = getFieldByElement( $prev );
				field.layoutSpacerGridColumnSpan = span;
			} else {
				field = getFieldByElement( $( this ) );
				if ( field ) {
					field.layoutGridColumnSpan = span;
				}
			}
		} );

		return this;
	};

	/**
	 * Get the grid column span CSS property of the targeted element.
	 *
	 * @returns {number}
	 */
	$.fn.getGridColumnSpan = function () {
		// Use 'gridColumnStart' instead of 'grid-column' as Firefox returns null for the latter.
		var span = parseInt( this.css( 'gridColumnStart' ).split( ' ' )[ 1 ] );
		if ( isNaN( span ) && typeof columnCount !== 'undefined' ) {
			span = columnCount;
		}
		return span;
	};

	/**
	 * Replace placeholders in the targeted string with passed values.
	 *
	 * @returns {string}
	 */
	String.prototype.format = function () {
		var args = arguments;
		return this.replace( /{(\d+)}/g, function ( match, number ) {
			return typeof args[ number ] != 'undefined' ? args[ number ] : match;
		} );
	};

	var $editorContainer = $( '#form_editor_fields_container' ),
		$editor = $( '.gform_editor' ),
		$container = $( '#gform_fields' ),
		$noFields = $( '#no-fields' ),
		$noFieldsDropzone = $( '#no-fields-drop' ),
		$sidebar = $( '.editor-sidebar' ),
		$button = $( '.gfield-field-action' ),
		$fields = $elements(),
		$elem = null,
		fieldButtonsSelector = '.add-buttons button';

	/**
	 * The max column count determined by the fields container's grid CSS.
	 * @type {number}
	 */
	var columnCount = getComputedStyle( $container[ 0 ] )[ 'grid-template-columns' ].split( ' ' ).length,
		/**
		 * The minimum number of columns a field can span.
		 * @type {number}
		 */
		min = columnCount / 4,
		/**
		 * The maximum number of columns a field can span.
		 * @type {number}
		 */
		max = null,
		/**
		 * A flag to determine if the field was dropped in droparea that appears when the form has no fields.
		 * @type {boolean}
		 */
		isNoFieldsDrop = false,
		/**
		 * The group ID of the last deleted field. This is used to resize the remaining fields in that group once the field has been removed from the DOM.
		 * @type {boolean}
		 */
		deletedFieldGroupId;

	// Initialize fields for layout editor.
	initElement( $fields );

	// Parse and maybe patch group ids
	validateGroupIds();

	// Initialize field buttons.
	initFieldButtons( $( fieldButtonsSelector ) );

	// Initialize the No Fields droparea.
	$noFields.droppable( {
		accept: fieldButtonsSelector,
		activate: function ( event, ui ) {
			$( this ).addClass( 'ready' )
		},
		over: function () {
			$( this ).addClass( 'hovering' );
			$noFieldsDropzone.addClass( 'hovering' );
		},
		out: function () {
			$( this ).removeClass( 'hovering' );
			$noFieldsDropzone.removeClass( 'hovering' );
		},
		drop: function () {
			isNoFieldsDrop = true;
			$( this ).removeClass( 'hovering' );
			$noFieldsDropzone.removeClass( 'hovering' );
		},
		deactivate: function () {
			$( this ).removeClass( 'ready' );
		}
	} );

	// Clear field selection when clicking off of any field.
	$editorContainer.on( 'click', function () {
		clearFieldSelection();
	} );

	// Handle adding a new field.
	$( document ).on( 'gform_field_added', function ( event, form, field ) {

		var $field = $( '#field_' + field.id );

		// This field was added by clicking.
		if ( $elem === null ) {

			$field.setGroupId( getGroupId() );

		}
		// This field was added by dragging into the editor.
		else {

			moveByTarget( $field, $indicator().data( 'target' ), $indicator().data( 'where' ) );

			$elem.remove();
			$elem = null;

		}

		$indicator().remove();

		initElement( $field );

	} );

	// Save the group ID of the deleted field.
	$( document ).on( 'gform_field_deleted', function ( event, form, fieldId ) {
		deletedFieldGroupId = getGroupId( $( '#field_' + fieldId ) );
	} );

	// Handle resizing the group after the deleted field has been fully removed from the DOM.
	gform.addAction( 'gform_after_field_removed', function ( form, fieldId ) {
		resizeGroup( deletedFieldGroupId );
	} );

	// Handle duplicating a field.
	gform.addAction( 'gform_field_duplicated', function ( form, field, $field, sourceFieldId ) {

		var $source      = $( '#field_' + sourceFieldId );
		var $sourceGroup = getGroup( getGroupId( $source ) );

		// Add duplicated fields *after* the last field in its group so that it will always appear on a new row.
		$sourceGroup.last().after( $field );

		$field
			.setGridColumnSpan( columnCount )
			.setGroupId( getGroupId() );

		initElement( $field );

	} );

	// Re-initialize the field after it's markup is refreshed (e.g. after the description is updated).
	gform.addAction( 'gform_after_refresh_field_preview', function( fieldId ) {
		initElement( $( '#field_' + fieldId ) );
	} );

	gform.addAction( 'gform_before_get_field_markup', function( form, field, index ) {
		addFieldPlaceholder( field, index );
	} );

	gform.addAction( 'gform_after_get_field_markup', function( form, field, index ) {
		removeFieldPlaceholder();
	} );

	gform.addAction( 'gform_before_field_duplicated', function( sourcefieldId ) {
		var $source = $( '#field_' + sourcefieldId );
		var $index  = $container.children().index( $source );

		addFieldPlaceholder( null, $index + 1 );
	} );

	gform.addAction( 'gform_field_duplicated', function() {
		removeFieldPlaceholder();
	} );

	gform.addAction( 'gform_before_refresh_field_preview', function( field_id ) {
		addFieldUpdateIndicator( field_id );
	} );

	gform.addAction( 'gform_after_refresh_field_preview', function( field_id ) {
		removeFieldUpdateIndicator( field_id );
	} );

	function addFieldPlaceholder( field, index ) {

		var fieldString = '<li data-js-field-loading-placeholder><div class="dropzone__loader">' +
			'<div class="dropzone__loader-item dropzone__loader-label"></div>' +
			'<div class="dropzone__loader-item dropzone__loader-content"></div>' +
			'</div></li>';

		//sets up DOM for new field
		if ( typeof index != 'undefined' ) {
			if ( index === 0 ) {
				$( '#gform_fields' ).prepend( fieldString );
			} else {
				$( '#gform_fields' ).children().eq( index - 1 ).after( fieldString );
			}
		} else {
			$( '#gform_fields' ).append( fieldString );
		}

		$( '[data-js-field-loading-placeholder]' ).setGridColumnSpan( columnCount );

		$( '#form_editor_fields_container' ).addClass( 'dropzone-loader-visible' );

		moveByTarget( $( '[data-js-field-loading-placeholder]' ), $indicator( false ).data( 'target' ), $indicator( false ).data( 'where' ) );
	}

	function removeFieldPlaceholder() {
		$( '#form_editor_fields_container' ).removeClass( 'dropzone-loader-visible' );
		$( '[data-js-field-loading-placeholder]' ).remove();
	}

	function addFieldUpdateIndicator( field_id ) {
		jQuery( "#field_" + field_id ).addClass( 'loading' );
	}

	function removeFieldUpdateIndicator( field_id ) {
		jQuery( "#field_" + field_id ).removeClass( 'loading' );
	}

	/**
	 * Initialize a form field so that it can be dragged and resized.
	 *
	 * @param {jQuery} $element The element(s) to be initialized.
	 */
	function initElement( $element ) {

		if ( $element.hasClass( 'ui-draggable' ) ) {
			$element
				.draggable( 'destroy' )
				.resizable( 'destroy' );
		}

		$element
			.draggable( {
				helper: 'clone',
				zIndex: 999,
				handle: '.gfield-drag',
				create: function( event, ui ) {
					if ( isSpacer( $( this ) ) ) {
						return;
					}

					var groupId,
						fieldId = $( this ).attr( 'id' ).replace( 'field_', '' ),
						field = fieldId ? GetFieldById( fieldId ) : false;

					if ( field && field.layoutGroupId && ! $editor.hasClass( 'gform_legacy_markup' ) ) {
						groupId = field.layoutGroupId;
					}
					// This applies when initializing a newly added field.
					else if ( ! getGroupId( $( this ), false ) ) {
						groupId = getGroupId();
					}

					$( this ).setGroupId( groupId );
				},
				start: function( event, ui ) {
					$container.addClass( 'dragging' );
					$editorContainer.addClass( 'droppable' );
					$elem = $( this );
					$elem.addClass( 'placeholder' );
				},
				drag: function( event, ui ) {
					// Match the helper to the current elements size.
					ui.helper
						.width( $elem.width() )
						.height( $elem.height() )
						// Firefox has trouble positioning the dragged element when it still has it's grid-column property set.
						.setGridColumnSpan( null );

					if ( ! gform.tools.isRtl() ) {
						helperLeft = ui.position.left;
					} else {
						helperLeft = ui.position.left + ( ui.helper.outerWidth() );
					}

					handleDrag( event, ui, ui.position.top, helperLeft );
				},
				stop: function( event, ui ) {
					$container.removeClass( 'dragging' );
					$editorContainer.removeClass( 'droppable' );
					$elem.removeClass( 'placeholder' );
					$elements().removeClass( 'hovering' );

					if ( $indicator().data( 'target' ) ) {
						moveByTarget( $elem, $indicator().data( 'target' ), $indicator().data( 'where' ) );
					}

					$indicator().remove();

					ui.helper.remove();
				},
			} )
			.resizable( {
				handles: 'e, w',
				start: function( event, ui ) {
					if ( gf_legacy.is_legacy === '1' ) {
						$element.resizable( 'option', 'minWidth', ui.size.width );
						$element.resizable( 'option', 'maxWidth', ui.size.width );
						alert( gf_vars.alertLegacyMode );
						return;
					}
					max = null;
					$container.addClass( 'resizing' );
				},
				resize: function( event, ui ) {
					if ( gf_legacy.is_legacy === '1' ) {
						return;
					}
					var columnWidth = $container.outerWidth() / columnCount,
						$item = ui.element,
						width = $item.outerWidth(),
						span = Math.max( min, Math.round( width / columnWidth ) ),
						prevSpan = $item.getGridColumnSpan(),
						$group = getGroup( getGroupId( $item ) ),
						lastInGroup = isLastInGroup( $item, $group ),
						$spacer = $group.filter( '.spacer' ),
						$sibling = lastInGroup && ! $spacer.length ? null : $item.next(),
						siblingSpan;

					/**
					 * Calculate the max on the first move of a resize and then rely on the set max until a new resize is initialized.
					 * Attempting to recalculate the max on each move results in some odd calculations...
					 */
					if ( max === null ) {
						if ( $group.length > 1 ) {
							siblingSpan = $sibling ? getGroupGridColumnSpan( $sibling ) : 0;
							max = prevSpan + siblingSpan;
						} else {
							max = columnCount;
						}
					}

					/**
					 * We've calculated the desired span based on the physical size of the field. Now let's adjust it to
					 * make sure it's not too big or too small.
					 *
					 * If the field is in a group, we will deduct the minimum span from the max to always save room for
					 * the field to it's right. If it the last field, we do not have to save this room.
					 */
					span = getAdjustedGridColumnSpan( span, min, max - ( $group.length > 1 && ! lastInGroup ? min : 0 ) );

					$().add( ui.helper ).add( ui.element )
						// Resizable will set a width with each increment, we have to deliberately override this.
						.css( 'width', 'auto' ).css( 'left', 'auto' )
						.setGridColumnSpan( span );

					if ( $sibling ) {
						siblingSpan = max - span;
						$sibling
							.css( 'width', 'auto' )
							.setGridColumnSpan( siblingSpan );
					}

					// If resizing a field to it's max allowable span, remove the spacer.
					if ( span == columnCount || span == max ) {
						removeSpacer( $spacer );
					}
					// Insert spacer when resizing a field with no field to its right.
					else if ( lastInGroup && ! $spacer.length && getGroupGridColumnSpan( $group ) < columnCount ) {
						addSpacer( $item, getGroupId( $item ), 1 );
					}
				},
				stop: function() {
					if ( gf_legacy.is_legacy === '1' ) {
						return;
					}
					$container.removeClass( 'resizing' );
				},
			} );
	}

	/**
	 * @function getFieldsAsRows
	 * @description Return an array of elements plus group ids grouped into rows as sub arrays.
	 *
	 * @since 2.5.1
	 *
	 * @returns {*[]}
	 */

	function getFieldsAsRows() {
		var rows = [];
		var row = [];
		var previousOffset = $fields[ 0 ].offsetTop;

		$fields.each( function() {
			// this element is on the same row as previous
			if ( previousOffset === this.offsetTop ) {
				row.push( {
					el     : this,
					groupId: this.dataset.groupid,
				} );
			} else {
				// we are on a new row, push previously stored row and start a new store
				if ( row.length ) {
					rows.push( row );
					row = [];
				}
				// push the current item into the new store
				row.push( {
					el     : this,
					groupId: this.dataset.groupid,
				} );
			}
			previousOffset = this.offsetTop;
		} );

		return rows;
	}

	/**
	 * @function setUniqueGroupIdForRow
	 * @description Get a new unique groupId and apply it to a row of fields.
	 *
	 * @since 2.5.1
	 *
	 * @param {Array} row An array of objects that each contain a field element and its groupId.
	 */

	function setUniqueGroupIdForRow( row ) {
		var groupId = getGroupId();
		row.forEach( function( entry ) {
			$( entry.el ).setGroupId( groupId );
		} );
	}

	/**
	 * @function validateGroupIds
	 * @description Iterate over all fields and patch any duplicate group id's, or rows that have mismatched group id's.
	 *
	 * @since 2.5.1
	 */

	function validateGroupIds() {
		// no need to run in legacy mode or if no fields
		if ( window.gf_legacy.is_legacy === '1' || ! $fields.length ) {
			return;
		}
		var rows = getFieldsAsRows();
		var ids = [];

		rows.forEach( function( currentRow ) {
			var rowIds = [];
			var duplicateFound = false;

			currentRow.forEach( function( entry ) {
				if ( ids.indexOf( entry.groupId ) !== - 1 ) {
					// this id has already been used in a previous field row
					duplicateFound = true;
				}
				rowIds.push( entry.groupId );
			} );

			// test if all ids for the row match
			var groupIdsMatchForRow = rowIds.every( function( val, i, arr ) {
				return val === arr[ 0 ];
			} );
			// if the row has mismatched id's, or contains an id used before, scrub and set fresh group id for the row
			if ( ! groupIdsMatchForRow || duplicateFound ) {
				setUniqueGroupIdForRow( currentRow );
			}
			// store the id for duplicate check in subsequent iterations
			ids.push( currentRow[ 0 ].groupId );
		} );
	}

	/**
	 * Initialize the field buttons so they can be dragged over the layout editor.
	 *
	 * @param {jQuery} $buttons All field buttons.
	 */
	function initFieldButtons( $buttons ) {
		$buttons
			.on( 'mousedown touchstart', function() {
				// hides the tooltip during drag, stop method sets it back using the data-description
				// start was too late to execute this with, the tooltip would persist in some browsers
				$( this ).attr( 'title', '' );
			} )
			.draggable( {
				helper: 'clone',
				revert: function () {
					// @todo Return true when field will not be added. This is low priority polish.
					return false;
				},
				cancel: false,
				appendTo: $container,
				containment: 'document',
				start: function( event, ui ) {
					clearFieldSelection();

					$editorContainer.addClass( 'droppable' );

					if ( gf_vars[ 'currentlyAddingField' ] == true ) {
						return false;
					}

					// Match the helper to the current elements size.
					ui.helper
						.width( $( this ).width() )
						.height( $( this ).height() );

					$container.addClass( 'dragging' );
					$elem = $( this ).clone();
					$elem.addClass( 'placeholder' );

					$( this ).addClass( 'fieldPlaceholder' );
				},
				drag: function( event, ui ) {
					// When form has no fields, there is only one place the field can be dragged...
					if ( ! form.fields.length ) {
						return;
					}

					/**
					 * New field buttons are dragged relative to #wpbody so their position needs to be adjusted to work the
					 * the same way as dragging an existing field (which is relative to #gform_fields).
					 */
					var helperTop = ui.position.top - 0 + ( ui.helper.outerHeight() / 2 ),
						helperLeft = ui.position.left - 0 + ( ui.helper.outerWidth() / 2 );

					handleDrag( event, ui, helperTop, helperLeft );

				},
				stop: function( event, ui ) {
					$( this ).removeClass( 'fieldPlaceholder' );
					$editorContainer.removeClass( 'droppable' );
					$container.removeClass( 'dragging' );

					var isAddingField = false;

					// Make sure the *entire* button has been dragged into the fields area before we add a field.
					if ( ! form.fields.length && isNoFieldsDrop ) {
						isNoFieldsDrop = false;
						isAddingField = addField( ui.helper.data( 'type' ) );
					} else if ( form.fields.length && $indicator( false ).data( 'target' ) ) {
						isAddingField = addField( ui.helper.data( 'type' ) );
					}

					// If we're not adding a new field, remove our placeholder element.
					if ( ! isAddingField ) {
						$indicator( false ).remove();
						$elem.remove();
						$elem = null;
					}

					$( this ).attr( 'title', $( this ).attr( 'data-description' ) );
				}
			} )
			.on( 'click keypress', function () {
				$elem = null;
			} );
	}

	/**
	 * Handle placing the indicator when a field is dragged over the layout editor.
	 *
	 * @param {Event}  event
	 * @param {object} ui         jQuery UI helper object which manages the current state.
	 * @param {number} helperTop  The top position of the element being dragged.
	 * @param {number} helperLeft The left position of the element being dragged.
	 */
	function handleDrag( event, ui, helperTop, helperLeft ) {

		$elements().removeClass( 'hovering' );

		if ( ! isInEditorArea( helperLeft, helperTop ) ) {
			$indicator( false ).remove();
			return;
		}

		// Check if field is dragged *above* all other fields.
		if ( helperTop < 0 ) {
			$indicator()
				.css( {
					top: -30,
					left: 0,
					height: '4px',
					width: $container.outerWidth()
				} )
				.data( {
					where: 'top',
					target: $elements().first()
				} );
			return;
		}
		// Check if field is dragged *below* all other fields.
		else if ( helperTop > $container.outerHeight() ) {
			$indicator()
				.css( {
					top: $container.outerHeight() - 14,
					left: 0,
					height: '4px',
					width: $container.outerWidth()
				} )
				.data( {
					where: 'bottom',
					target: $elements().last()
				} );
			return;
		}

		$elements()
			.not( ui.helper )
			.not( this )
			.each( function() {

				var $target = $( this ),
					sibPos = $target.position(),
					sibArea = {
						top: sibPos.top,
						right: sibPos.left + $target.outerWidth(),
						bottom: sibPos.top + $target.outerHeight(),
						left: sibPos.left
					};

				if ( ! isInArea( helperLeft, helperTop, sibArea ) ) {
					return;
				}

				$target.addClass( 'hovering' );

				if ( isSpacer( $target ) ) {
					$target = $target.prev();
					sibPos = $target.position();
					where = 'right';
				}

				var where = whichArea( helperLeft, helperTop, sibArea, $target.outerWidth(), $target.outerHeight() ),
					targetGroupId = getGroupId( $target ),
					$targetGroup = getGroup( targetGroupId, false );

				var isGroupMaxed = $targetGroup.length >= ( columnCount / min );

				if ( getGroupId( $target ) === getGroupId( ui.helper ) ) {
					isGroupMaxed = false;
				}

				var available = isSpaceAvailable( ui, $target );

				if ( where === 'left' || where === 'right' ) {
					// Columns are not supported in Legacy markup or with Page or Section fields.
					if ( ! areColumnsEnabled( $target, $elem ) ) {
						return;
					} else if ( isGroupMaxed || ( available === false ) ) {
						return;
					}
				}

				$indicator().data( {
					where: where,
					target: $target
				} );

				// Where on the child field has the helper been dragged?
				switch ( where ) {
					case 'left':

						$indicator()
							.css( {
								top: sibPos.top,
								left: sibPos.left - 10,
								height: $target.outerHeight(),
								width: '4px'
							} );

						return false;
					case 'right':

						$indicator().css( {
							top: sibPos.top,
							left: sibPos.left + $target.outerWidth() + 6,
							right: 'auto',
							height: $target.outerHeight(),
							width: '4px'
						} );

						return false;
					case 'bottom':

						$indicator().css( {
							top: sibPos.top + $target.outerHeight() + 26,
							left: 0,
							height: '4px',
							width: '100%',
						} );

						return false;
					case 'top':

						$indicator().css( {
							top: sibPos.top - 30,
							left: 0,
							height: '4px',
							width: '100%'
						} );

						return false;
				}

			} );

	}

	/**
	 * Determine whether columns are enabled based on the current element and the target over which it is being dragged.
	 *
	 * @param {jQuery} $target The element over which the dragged element is currently positioned.
	 * @param {jQuery} $elem   The element that is being dragged.
	 *
	 * @returns {boolean}
	 */
	function areColumnsEnabled( $target, $elem ) {

		if ( $editor.hasClass( 'gform_legacy_markup' ) ) {
			return false;
		}

		if ( $target.hasClass( 'gpage' ) || $target.hasClass( 'gsection' ) || $target.hasClass( 'gform_hidden' ) ) {
			return false;
		}

		if ( $elem.hasClass( 'gpage' ) || $elem.hasClass( 'gsection' ) || $elem.hasClass( 'gform_hidden' ) || $elem.data( 'type' ) === 'hidden' ) {
			return false;
		}

		if ( $elem.is( 'button' ) && ( $.inArray( $elem.val().toLowerCase(), [ 'page', 'section' ] ) !== -1 ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine whether the given coordinates are in the specified area.
	 *
	 * @param {number} x    The left position of the coordinate.
	 * @param {number} y    The top position of the coordinate.
	 * @param {object} area An object of top, right, bottom and left positions.
	 *
	 * @returns {boolean}
	 */
	function isInArea( x, y, area ) {
		return y < area.bottom && y > area.top && x < area.right && x > area.left;
	}

	/**
	 * Determine which portion of a specified area the given coordinates are in.
	 *
	 * @param {number} x      The left position of the coordinate.
	 * @param {number} y      The top position of the coordinate.
	 * @param {object} area   An object of top, right, bottom and left positions.
	 * @param {number} width  The width of the given area.
	 * @param {number} height The height of the given area.
	 *
	 * @returns {string}
	 */
	function whichArea( x, y, area, width, height ) {

		var thresholdLeft = area.left + ( width / 2 ),
			thresholdRight = area.right - ( width / 2 ),
			thresholdTop = area.top + ( height / 5 ),
			thresholdBottom = area.bottom - ( height / 5 );

		if ( y > area.top && y < thresholdTop ) {
			return 'top';
		} else if ( y < area.bottom && y > thresholdBottom ) {
			return 'bottom';
		} else if ( x > area.left && x < thresholdLeft ) {
			return 'left';
		} else if ( x < area.right && x > thresholdRight ) {
			return 'right';
		}

		return 'center';
	}

	/**
	 * Determine whether the given coordinates are in the area of the layout editor.
	 *
	 * @param {number} x The left position of the coordinate.
	 * @param {number} y The top position of the coordinate.
	 *
	 * @returns {boolean}
	 */
	function isInEditorArea( x, y ) {

		if ( ! gform.tools.isRtl() ) {
			var editorOffsetLeft = $editorContainer.offset().left;
		} else {
			var editorOffsetLeft = $container.offset().left;
		}
		var containerOffset = $container.offset(),
			offsetTop = containerOffset.top - $editorContainer.offset().top,
			offsetLeft = containerOffset.left - editorOffsetLeft,
			buttonWidth = $button.outerWidth() || null,
			editorArea = {
				top: -offsetTop + buttonWidth,
				right: -offsetLeft + $editorContainer.outerWidth() - $sidebar.outerWidth() - buttonWidth,
				bottom: -offsetTop + $editorContainer.outerHeight(),
				left: -offsetLeft,
			};

		return y > editorArea.top && y < editorArea.bottom && x > editorArea.left && x < editorArea.right;
	}

	/**
	 * Check if a group has room to accommodate an additional field.
	 *
	 * @param {object} ui      jQuery UI helper object which manages the current state.
	 * @param {jQuery} $target The element over which the dragged element was last positioned.
	 */
	function isSpaceAvailable( ui, $target ) {
		var targetSpan, splitSpan, $targetGroup, groupId, $spacer, helperGroupId;

		groupId = getGroupId( $target );
		helperGroupId = getGroupId( ui.helper );
		$targetGroup = getGroup( groupId );

		if ( groupId === helperGroupId ) {
			return true;
		}

		// Figure out if we're dropping a field onto a spacer or next to a spacer.
		if ( isSpacer( $target ) ) {
			$spacer = $target;
			$target = $target.prev();
		} else if ( isSpacer( $target.next() ) && $targetGroup.index( $target.next() ) !== false ) {
			$spacer = $target.next();
		}

		// If we're dropping onto or next to a spacer, set the target span to the spacer span.
		targetSpan = $spacer ? $spacer.getGridColumnSpan() : null;

		// Determine the span of the field we're dropping in.
		if ( targetSpan ) {
			splitSpan = targetSpan;
		} else if ( isEvenSplit( $targetGroup ) ) {
			splitSpan = columnCount / ( $targetGroup.length + 1 ); // +1 for the element about to be added to this group.
		} else {
			targetSpan = $target.getGridColumnSpan();
			splitSpan = targetSpan / 2;
		}

		// If the span of the field we're dropping in calculates to less than 3, no space available.
		if ( parseInt( splitSpan ) < 3 ) {
			return false;
		}
	}

	/**
	 * Move the given element based on the specified target and location.
	 *
	 * @param {jQuery} $elem   The element to be moved.
	 * @param {jQuery} $target The element over which the dragged element was last positioned.
	 * @param {string} where   The area of the target element over which the element was last positioned.
	 */
	function moveByTarget( $elem, $target, where ) {

		if ( ! $target ) {
			return;
		}

		var targetSpan,
			splitSpan,
			$targetGroup,
			$resizeGroup,
			groupId,
			sourceGroupId,
			movingIntoTargetGroup,
			$spacer;

		sourceGroupId = getGroupId( $elem );
		groupId = getGroupId( $target );
		$targetGroup = getGroup( groupId );

		if ( isSpacer( $target ) ) {
			$spacer = $target;
			$target = $target.prev();
		} else if ( ( isSpacer( $target.next() ) || isPlaceholder( $target.next() ) ) && $targetGroup.index( $target.next() ) !== false ) {
			$spacer = $target.next();
		}

		movingIntoTargetGroup = where === 'left' || where === 'right';

		if ( $spacer && movingIntoTargetGroup ) {
			targetSpan = $spacer.getGridColumnSpan();
			removeSpacer( $spacer );
			$targetGroup = getGroup( groupId );
		}

		if ( where == 'top' ) {
			$target = $targetGroup.first();
		} else if ( where == 'bottom' ) {
			$target = $targetGroup.last();
		}

		var direction = gform.tools.isRtl() ? 'right' : 'left';

		if ( where == 'top' || where == direction ) {
			$elem.insertBefore( $target );
		} else {
			$elem.insertAfter( $target );
		}

		if ( ! movingIntoTargetGroup ) {

			groupId = getGroupId();
			$elem.setGridColumnSpan( columnCount );

		} else {

			if ( targetSpan ) {
				$resizeGroup = $elem;
				splitSpan = targetSpan;
			} else if ( isEvenSplit( $targetGroup ) ) {
				splitSpan = columnCount / ( $targetGroup.length + 1 ); // +1 for the element about to be added to this group.
				$resizeGroup = $targetGroup.add( $elem );
			} else {
				targetSpan = $target.getGridColumnSpan();
				splitSpan = targetSpan / 2;
				$resizeGroup = $target.add( $elem );
			}

			if ( parseInt( splitSpan ) == splitSpan ) {
				$resizeGroup.setGridColumnSpan( splitSpan );
			}
			// Handle non-even spans by making one smaller than the other. Should only happen in non-even splits.
			else {
				var floor = Math.floor( splitSpan ),
					ceil = Math.ceil( splitSpan );
				$elem.setGridColumnSpan( floor );
				$target.setGridColumnSpan( ceil );
			}

		}

		$elem.setGroupId( groupId );

		// Reset sizes on the group the element has been removed from.
		resizeGroup( sourceGroupId );

	}

	/**
	 * Get the group ID of the given element or generate a new group ID if none exists.
	 *
	 * @param {jQuery}  $elem        The element for which we are getting the group ID.
	 * @param {boolean} autoGenerate Whether or not a group ID should be auto-generated if no group ID exists.
	 *
	 * @returns {string}
	 */
	function getGroupId( $elem, autoGenerate ) {
		var groupId;
		if ( typeof $elem !== 'undefined' ) {
			groupId = $elem.attr( 'data-groupId' );
		}
		if ( ! groupId && ( autoGenerate || typeof autoGenerate === 'undefined' ) ) {
			groupId = 'xxxxxxxx'.replace( /[xy]/g, function ( c ) {
				var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8;
				return v.toString( 16 );
			} );
		}
		return groupId;
	}

	/**
	 * Get a group of field elements by the given group ID.
	 *
	 * @param {string} groupId The ID of the group to be set on the targeted element.
	 *
	 * @returns {jQuery}
	 */
	function getGroup( groupId, spacers ) {
		if ( spacers || 'undefined' === typeof( spacers ) ) {
			return $elements()
				.filter( '[data-groupId="{0}"]'.format( groupId ) )
				.not( '.ui-draggable-dragging' );
		} else {
			return $elements()
				.filter( '[data-groupId="{0}"]'.format( groupId ) )
				.not( '.ui-draggable-dragging' )
				.not( '.spacer' );
		}
	}

	/**
	 * Get the grid column span value adjusted by a specified min and max value.
	 *
	 * @param {number} span The desired number columns to be spanned.
	 * @param {number} min  The minimum number of columns that must be spanned.
	 * @param {number} max  The maximum number of columns that can be spanned.
	 *
	 * @returns {number}
	 */
	function getAdjustedGridColumnSpan( span, min, max ) {
		return Math.max( min, Math.min( max, span ) );
	}

	/**
	 * Get the combined grid column span value of the given group.
	 *
	 * @param {jQuery} $group A group of field elements making up a row.
	 *
	 * @returns {number}
	 */
	function getGroupGridColumnSpan( $group ) {
		var span = 0;
		$group.each( function () {
			span += $( this ).getGridColumnSpan();
		} );
		return span;
	}

	/**
	 * Determine whether the grid column span for the given group is the same for all elements in the group.
	 *
	 * @param {jQuery} $group A group of field elements making up a row.
	 *
	 * @returns {boolean}
	 */
	function isEvenSplit( $group ) {

		var baseSpan = $group.first().getGridColumnSpan(),
			isEvenSplit = true;

		$group.each( function () {
			var span = $( this ).getGridColumnSpan();
			if ( span !== baseSpan ) {
				isEvenSplit = false;
				return false;
			}
		} );

		return isEvenSplit;
	}

	/**
	 * Resize the elements in a group based on the provided group ID.
	 *
	 * @param {string} groupId The ID of the group to be set on the targeted element.
	 */
	function resizeGroup( groupId ) {

		var $group = getGroup( groupId ),
			splitSpan = columnCount / ( $group.length ),
			$spacer = $group.filter( '.spacer' );

		// If the only field in a group is a spacer, remove the spacer.
		if ( $group[0] === $spacer[0] && $group.length > 0 ) {
			removeSpacer( $spacer );
		}

		$group.setGridColumnSpan( splitSpan );

	}

	/**
	 * Determine whether the given element is the last element in the specified group.
	 *
	 * @param {jQuery} $elem  The element to check if it is the last in the specified group.
	 * @param {jQuery} $group The group of field elements to which the given element belongs.
	 *
	 * @returns {boolean}
	 */
	function isLastInGroup( $elem, $group ) {
		$group = $group.not( '.spacer' );
		return $group.length === 1 || $group.last()[ 0 ] === $elem[ 0 ];
	}

	/**
	 * Insert a Spacer field after the given field element.
	 *
	 * @param {jQuery} $field  The field element after which the Spacer should be inserted.
	 * @param {string} groupId The ID of the group to be set on the targeted element.
	 * @param {number} span    The number of columns the Spacer should span.
	 *
	 * @returns {jQuery}
	 */
	function addSpacer( $field, groupId, span ) {

		var $spacer = $( '<div class="spacer gfield"></div>' )
			.setGroupId( groupId )
			.setGridColumnSpan( span );

		$field.after( $spacer );

		return $spacer;
	}

	/**
	 * Remove the given Spacer field from the DOM.
	 *
	 * @param {jQuery} $spacer A field element representing a Spacer field.
	 */
	function removeSpacer( $spacer ) {
		$spacer
			.setGridColumnSpan( 0 )
			.remove();
	}

	/**
	 * Determine whether the given element is a Spacer field.
	 *
	 * @param {jQuery} $elem The element for which to determine if it is a Spacer field.
	 *
	 * @returns {boolean}
	 */
	function isSpacer( $elem ) {
		return $elem.filter( '.spacer' ).length > 0;
	}

	/**
	 * Determine whether the given element is a Placeholder.
	 *
	 * @since 2.5
	 *
	 * @param {jQuery} $elem The element for which to determine if it is a placeholder.
	 *
	 * @returns {boolean}
	 */
	function isPlaceholder( $elem ) {
		return $elem.filter( '[data-js-field-loading-placeholder]' ).length > 0;
	}

	/**
	 * Get the Gravity Forms field object based on the given element.
	 *
	 * @param {jQuery} $elem The element to be used to fetch the field object.
	 *
	 * @returns {object|boolean}
	 */
	function getFieldByElement( $elem ) {
		var id = $elem.attr( 'id' );
		var fieldId = id && id.indexOf( 'field_' ) !== -1 ? String( id ).replace( 'field_', '' ) : false;
		return fieldId ? GetFieldById( fieldId ) : false;
	}

	/**
	 * Add a new field of the specified type to the form.
	 *
	 * @param {string} type The field type to add to the form.
	 *
	 * @returns {boolean}
	 */
	function addField( type ) {
		return StartAddField( type, Math.max( 0, $container.children().index( $elem ) ) );
	}

	/**
	 * Deselect the currently selected field.
	 */
	function clearFieldSelection() {
		$elements().removeClass( 'field_selected' );
		$( '.sidebar' ).tabs( 'option', 'active', 0 );
		HideSettings();
	}

	/**
	 * Get all field elements in current form.
	 *
	 * @returns {jQuery|[]}
	 */
	function $elements() {
		return $container.find( '.gfield' );
	}

	/**
	 * Create or return the current Indicator. The Indicator indicates where the currently dragged field will be placed when dropped.
	 *
	 * @param {boolean} create Whether or not an indicator should be created if it does not exist.
	 *
	 * @returns {jQuery}
	 */
	function $indicator( create ) {

		create = typeof create === 'undefined';

		var $indicator = $( '#indicator' );

		if ( ! $indicator.length && create ) {
			$indicator = $( '<div id="indicator"></div>' );
			$container.append( $indicator );
		}

		return $indicator;
	}

} )( jQuery );
