diff --git a/classes/controllers/FrmFieldsController.php b/classes/controllers/FrmFieldsController.php index 95d358eb47..e4a82b9362 100644 --- a/classes/controllers/FrmFieldsController.php +++ b/classes/controllers/FrmFieldsController.php @@ -68,10 +68,11 @@ public static function create() { $field_type = FrmAppHelper::get_post_param( 'field_type', '', 'sanitize_text_field' ); $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' ); $field_options = FrmAppHelper::get_post_param( 'field_options', array(), 'wp_kses_post' ); + $field_order = FrmAppHelper::get_post_param( 'field_order', 0, 'absint' ); do_action( 'frm_before_create_field', $field_type, $form_id ); - $field = self::include_new_field( $field_type, $form_id, $field_options ); + $field = self::include_new_field( $field_type, $form_id, $field_options, $field_order ); // This hook will allow for multiple fields to be added at once do_action( 'frm_after_field_created', $field, $form_id ); @@ -82,14 +83,18 @@ public static function create() { /** * Set up and create a new field * + * @since x.x The $field_order parameter was added. + * * @param string $field_type * @param int $form_id * @param array $field_options + * @param int $field_order The field order reserved by the form builder. + * Pass 0 to allocate one from the database instead. * * @return array|false */ - public static function include_new_field( $field_type, $form_id, $field_options = array() ) { - $field_values = FrmFieldsHelper::setup_new_vars( $field_type, $form_id ); + public static function include_new_field( $field_type, $form_id, $field_options = array(), $field_order = 0 ) { + $field_values = FrmFieldsHelper::setup_new_vars( $field_type, $form_id, $field_order ); if ( $field_options ) { $field_values['field_options'] = array_merge( $field_values['field_options'], $field_options ); diff --git a/classes/helpers/FrmFieldsHelper.php b/classes/helpers/FrmFieldsHelper.php index da0f13e7ac..4c9ad63068 100644 --- a/classes/helpers/FrmFieldsHelper.php +++ b/classes/helpers/FrmFieldsHelper.php @@ -13,12 +13,16 @@ class FrmFieldsHelper { private static $context_is_safe_to_load_field_options_from_request_data; /** + * @since x.x The $field_order parameter was added. + * * @param string $type * @param int|string $form_id + * @param int $field_order The field order reserved by the form builder. + * Pass 0 to allocate one from the database instead. * * @return array */ - public static function setup_new_vars( $type = '', $form_id = '' ) { + public static function setup_new_vars( $type = '', $form_id = '', $field_order = 0 ) { if ( str_contains( $type, '|' ) ) { list( $type, $setting ) = explode( '|', $type ); } @@ -33,9 +37,24 @@ public static function setup_new_vars( $type = '', $form_id = '' ) { array( 'order_by' => 'field_order DESC' ) ); + $field_count = (int) $field_count; + + /* + * A reserved order is used exactly as it was given. The form builder reserves + * one for every field it adds, because the count above is read in a query of + * its own, so requests that overlap, which is what happens when fields are + * dragged in faster than the requests come back, all read the same count and + * would share an order. Fields sorted on an ambiguous field_order then swap + * places between page loads. Reservations are already unique, so falling back + * to the count for the ones that arrive out of order would only put two of + * them back on the same value. Nothing is reserved when other code creates a + * field, which is what the fallback is for. + */ + $field_order = $field_order > 0 ? $field_order : $field_count + 1; + $values['field_key'] = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_fields', 'field_key' ); $values['form_id'] = $form_id; - $values['field_order'] = $field_count + 1; + $values['field_order'] = $field_order; $values['field_options']['custom_html'] = self::get_default_html( $type ); @@ -47,12 +66,44 @@ public static function setup_new_vars( $type = '', $form_id = '' ) { } } - // Increase the field order of submit field and fields in the same row. - FrmSubmitHelper::update_last_row_fields_order_when_adding_field( $field_count ); + /* + * Increase the field order of submit field and fields in the same row. The + * highest order wins so that the submit row stays after every field, even + * when this request reserved an order below one that is still in flight. + */ + FrmSubmitHelper::update_last_row_fields_order_when_adding_field( max( $field_order, $field_count ) ); return $values; } + /** + * Get the field order the form builder should count up from. + * + * The builder reserves an order for every field it adds so that overlapping + * insert requests cannot share one. This is the value it starts from. Child + * forms are included because a single counter covers every form the builder + * can add a field to, and that includes the child form behind a repeater. + * + * @since x.x + * + * @param int|string $form_id The ID of the form being edited. + * + * @return int + */ + public static function get_next_field_order( $form_id ) { + $form_ids = FrmDb::get_col( 'frm_forms', array( 'parent_form_id' => $form_id ), 'id' ); + $form_ids[] = $form_id; + + $highest_order = FrmDb::get_var( + 'frm_fields', + array( 'form_id' => $form_ids ), + 'field_order', + array( 'order_by' => 'field_order DESC' ) + ); + + return (int) $highest_order; + } + /** * @param array $field * @param string $plus diff --git a/classes/views/frm-forms/form.php b/classes/views/frm-forms/form.php index cf84a6706f..98e2dc9278 100644 --- a/classes/views/frm-forms/form.php +++ b/classes/views/frm-forms/form.php @@ -46,7 +46,7 @@ ?> -