diff --git a/classes/controllers/FrmAppController.php b/classes/controllers/FrmAppController.php
index f466f57ff0..9621074227 100644
--- a/classes/controllers/FrmAppController.php
+++ b/classes/controllers/FrmAppController.php
@@ -1329,15 +1329,27 @@ private static function in_our_pages() {
}
/**
- * Hide all third-parties admin notices only in our admin pages.
+ * Handles actions related to the current screen.
+ *
+ * @since x.x
*
* @return void
*/
- public static function filter_admin_notices() {
+ public static function handle_current_screen() {
if ( ! self::in_our_pages() ) {
return;
}
+ self::filter_admin_notices();
+ self::remember_custom_sort();
+ }
+
+ /**
+ * Hide all third-parties admin notices only in our admin pages.
+ *
+ * @return void
+ */
+ public static function filter_admin_notices() {
$actions = array(
'admin_notices',
'network_admin_notices',
@@ -1362,6 +1374,81 @@ public static function filter_admin_notices() {
}
}
+ /**
+ * Remembers and applies user-specific sorting preferences.
+ *
+ * @return void
+ */
+ private static function remember_custom_sort() {
+ $screen = get_current_screen();
+ if ( ! $screen ) {
+ return;
+ }
+
+ if ( ! FrmAppHelper::is_admin_list_page() && ! FrmAppHelper::is_admin_list_page( 'formidable-entries' ) ) {
+ return;
+ }
+
+ $orderby = FrmAppHelper::get_param( 'orderby' );
+
+ if ( ! $orderby ) {
+ return;
+ }
+
+ $user_id = get_current_user_id();
+ $meta_key = 'frm_preferred_list_sort_' . $screen->id;
+ $order = FrmAppHelper::get_param( 'order' );
+
+ $new_sort = array(
+ 'orderby' => $orderby,
+ 'order' => $order,
+ );
+
+ $current_sort = get_user_meta( $user_id, $meta_key, true );
+
+ if ( $new_sort !== $current_sort ) {
+ update_user_meta(
+ $user_id,
+ $meta_key,
+ array(
+ 'orderby' => $orderby,
+ 'order' => $order,
+ )
+ );
+ }
+ }
+
+ /**
+ * Retrieve and apply any saved sorting preferences for the current screen.
+ *
+ * @since x.x
+ *
+ * @param string &$orderby Reference to the current 'orderby' parameter.
+ * @param string &$order Reference to the current 'order' parameter.
+ * @return void
+ */
+ public static function apply_saved_sort_preference( &$orderby, &$order ) {
+ if ( ! function_exists( 'get_current_screen' ) ) {
+ return;
+ }
+
+ $screen = get_current_screen();
+ if ( ! $screen ) {
+ return;
+ }
+
+ $user_id = get_current_user_id();
+ $preferred_list_sort = get_user_meta( $user_id, 'frm_preferred_list_sort_' . $screen->id, true );
+
+ if ( is_array( $preferred_list_sort ) && ! empty( $preferred_list_sort['orderby'] ) ) {
+ $orderby = $preferred_list_sort['orderby'];
+
+ if ( ! empty( $preferred_list_sort['order'] ) ) {
+ $order = $preferred_list_sort['order'];
+ }
+ }
+ }
+
/**
* Validate that the callback name is ours not from third-party.
*
diff --git a/classes/controllers/FrmHooksController.php b/classes/controllers/FrmHooksController.php
index ec99052bff..ee4bc506cd 100644
--- a/classes/controllers/FrmHooksController.php
+++ b/classes/controllers/FrmHooksController.php
@@ -126,7 +126,7 @@ public static function load_admin_hooks() {
add_filter( 'plugin_action_links_' . FrmAppHelper::plugin_folder() . '/formidable.php', 'FrmAppController::settings_link' );
add_filter( 'admin_footer_text', 'FrmAppController::set_footer_text' );
add_action( 'admin_footer', 'FrmAppController::add_admin_footer_links' );
- add_action( 'current_screen', 'FrmAppController::filter_admin_notices' );
+ add_action( 'current_screen', 'FrmAppController::handle_current_screen' );
// Entries Controller.
add_action( 'admin_menu', 'FrmEntriesController::menu', 12 );
diff --git a/classes/controllers/FrmUsageController.php b/classes/controllers/FrmUsageController.php
index f17550c88a..cc1e76eb5f 100644
--- a/classes/controllers/FrmUsageController.php
+++ b/classes/controllers/FrmUsageController.php
@@ -106,18 +106,7 @@ public static function load_scripts() {
* @return bool
*/
private static function is_forms_list_page() {
- if ( ! FrmAppHelper::is_admin_page() ) {
- return false;
- }
-
- // Check Trash page.
- $form_type = FrmAppHelper::simple_get( 'form_type' );
- if ( $form_type && 'published' !== $form_type ) {
- return false;
- }
-
- // Check edit or settings page.
- return ! FrmAppHelper::simple_get( 'frm_action' );
+ return FrmAppHelper::is_admin_list_page();
}
/**
diff --git a/classes/helpers/FrmAppHelper.php b/classes/helpers/FrmAppHelper.php
index 851168d7b4..ef7d3a4e01 100644
--- a/classes/helpers/FrmAppHelper.php
+++ b/classes/helpers/FrmAppHelper.php
@@ -336,6 +336,29 @@ public static function is_formidable_admin() {
return $is_formidable;
}
+ /**
+ * Checks if is a list page.
+ *
+ * @since x.x
+ *
+ * @param string $page The name of the page to check.
+ * @return bool
+ */
+ public static function is_admin_list_page( $page = 'formidable' ) {
+ if ( ! self::is_admin_page( $page ) ) {
+ return false;
+ }
+
+ // Check Trash page.
+ $form_type = self::simple_get( 'form_type' );
+ if ( $form_type && 'published' !== $form_type ) {
+ return false;
+ }
+
+ // Check edit or settings page.
+ return ! self::simple_get( 'frm_action' );
+ }
+
/**
* Check for certain page in Formidable settings
*
diff --git a/classes/helpers/FrmEntriesListHelper.php b/classes/helpers/FrmEntriesListHelper.php
index b30eea8940..fda63caa03 100644
--- a/classes/helpers/FrmEntriesListHelper.php
+++ b/classes/helpers/FrmEntriesListHelper.php
@@ -109,6 +109,8 @@ protected function get_order_by() {
)
);
+ FrmAppController::apply_saved_sort_preference( $orderby, $order );
+
return FrmDb::esc_order( $orderby . ' ' . $order );
}
diff --git a/classes/helpers/FrmFormsListHelper.php b/classes/helpers/FrmFormsListHelper.php
index 875d7bd9f2..45531739dc 100644
--- a/classes/helpers/FrmFormsListHelper.php
+++ b/classes/helpers/FrmFormsListHelper.php
@@ -46,6 +46,9 @@ public function prepare_items() {
'default' => 'ASC',
)
);
+
+ FrmAppController::apply_saved_sort_preference( $orderby, $order );
+
$start = self::get_param(
array(
'param' => 'start',
@@ -335,6 +338,7 @@ public function single_row( $item, $style = '' ) {
protected function column_shortcode( $form ) {
$val = '' . FrmAppHelper::icon_by_class( 'frmfont frm_code_icon', array( 'echo' => false ) ) . '';
$val .= $this->column_style( $form );
+ $val .= $this->column_views( $form );
$val = apply_filters( 'frm_form_list_actions', $val, array( 'form' => $form ) );
// Remove the space hard coded in Landing pages.
$val = str_replace( ' ', '', $val );
@@ -370,6 +374,26 @@ protected function column_style( $form ) {
return '' . FrmAppHelper::icon_by_class( 'frmfont frm_pallet_icon', array( 'echo' => false ) ) . '';
}
+ /**
+ * Generate the HTML for the form Views page.
+ *
+ * @since x.x
+ *
+ * @param stdClass $form Form object.
+ * @return string
+ */
+ protected function column_views( $form ) {
+ $attributes = array(
+ 'href' => admin_url( 'admin.php?page=formidable-views&form=' . absint( $form->id ) . '&show_nav=1' ),
+ 'title' => __( 'View Form', 'formidable' ),
+ 'target' => '_blank',
+ );
+
+ return '
+ ' . FrmAppHelper::icon_by_class( 'frmfont frm_eye_icon', array( 'echo' => false ) ) .
+ '';
+ }
+
/**
* @param array $actions
* @param object $item
diff --git a/classes/helpers/FrmListHelper.php b/classes/helpers/FrmListHelper.php
index d3a0a9e051..84ce4745fa 100644
--- a/classes/helpers/FrmListHelper.php
+++ b/classes/helpers/FrmListHelper.php
@@ -391,7 +391,19 @@ protected function bulk_actions( $which = '' ) {
$verify = $this->confirm_bulk_delete();
if ( $verify ) {
- echo "";
+ $confirm_delete_attributes = array(
+ 'id' => 'confirm-bulk-delete-' . $which,
+ 'class' => 'frm-hidden',
+ 'tabindex' => '-1',
+ 'aria-hidden' => 'true',
+ 'href' => 'confirm-bulk-delete',
+ 'data-loaded-from' => $this->loaded_from(),
+ 'data-frmverify' => $verify,
+ 'data-frmverify-btn' => 'frm-button-red',
+ );
+
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo '';
}
}
@@ -895,6 +907,8 @@ public function print_column_headers( $with_id = true ) {
$current_order = 'asc';
}
+ FrmAppController::apply_saved_sort_preference( $current_orderby, $current_order );
+
if ( ! empty( $columns['cb'] ) ) {
static $cb_counter = 1;
$columns['cb'] = '';
diff --git a/classes/views/frm-fields/back-end/settings.php b/classes/views/frm-fields/back-end/settings.php
index f7198a6ece..ac540e1a5f 100644
--- a/classes/views/frm-fields/back-end/settings.php
+++ b/classes/views/frm-fields/back-end/settings.php
@@ -212,6 +212,21 @@
include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/field-description.php';
}
+ /**
+ * Fires after displaying the field description in a form settings.
+ *
+ * @since x.x
+ *
+ * @param array $args {
+ * Array containing the field data.
+ *
+ * @type array $field The field settings.
+ * @type array $display The display settings for the field.
+ * @type array $values The values associated with the field.
+ * }
+ */
+ do_action( 'frm_field_options_after_description', compact( 'field', 'display', 'values' ) );
+
// Field Size
if ( $display['size'] && ! in_array( $field['type'], array( 'select', 'data', 'time' ), true ) ) {
$display_max = $display['max'];
diff --git a/css/frm_admin.css b/css/frm_admin.css
index 504ca849d6..39da3494bc 100644
--- a/css/frm_admin.css
+++ b/css/frm_admin.css
@@ -1546,7 +1546,8 @@ body.frm-hidden-overflow {
.wp-core-ui.frm-white-body .button-secondary:hover,
.wp-core-ui.frm-white-body .button-secondary:focus,
.wp-core-ui.frm-white-body .tablenav .button:hover,
-.frm_wrap .preview > .button:hover {
+.frm_wrap .preview > .button:hover,
+.frm-white-body #search-submit:hover {
border-color: var(--grey-300) !important;
color: var(--grey-800);
background: var(--grey-50) !important;
@@ -1745,14 +1746,14 @@ input.frm_insert_in_template {
margin-bottom: var(--gap-2xs) !important;
}
-.frm-mb-xs {
- margin-bottom: var(--gap-xs) !important;
-}
-
.frm-mb-sm {
margin-bottom: var(--gap-sm) !important;
}
+.frm-mb-xs {
+ margin-bottom: var(--gap-xs) !important;
+}
+
.frm-mb-md {
margin-bottom: var(--gap-md) !important;
}
@@ -1777,6 +1778,10 @@ input.frm_insert_in_template {
margin-right: auto;
}
+.frm-mr-2xs {
+ margin-right: var(--gap-2xs) !important;
+}
+
.frm-mx-0 {
margin-left: 0 !important;
margin-right: 0 !important;
@@ -1847,6 +1852,10 @@ input.frm_insert_in_template {
padding-top: var(--gap-sm) !important;
}
+.frm-pt-md {
+ padding-top: var(--gap-md) !important;
+}
+
.frm-pt-xl {
padding-top: var(--gap-xl) !important;
}
@@ -1857,6 +1866,12 @@ input.frm_insert_in_template {
.frm-pb-sm {
padding-bottom: var(--gap-sm) !important;
}
+
+.frm-px-sm {
+ padding-right: var(--gap-sm) !important;
+ padding-left: var(--gap-sm) !important;
+}
+
.frm-px-md {
padding-right: var(--gap-md) !important;
padding-left: var(--gap-md) !important;
@@ -1922,6 +1937,10 @@ input.frm_insert_in_template {
width: 100% !important;
}
+.frm-w-half {
+ width: 50% !important;
+}
+
.frm_block, /* deprecated */
.frm-block {
display: block;
@@ -1954,7 +1973,6 @@ input.frm_insert_in_template {
gap: var(--gap-sm);
}
-
.frm-flex-col {
display: flex;
flex-direction: column;
@@ -2059,6 +2077,10 @@ input.frm_insert_in_template {
cursor: pointer;
}
+.frm-align-baseline {
+ vertical-align: baseline !important;
+}
+
/* End Generic Classes, Start Forced Generic Classes */
.frm-fields p > label.frm_hidden,
@@ -6820,10 +6842,13 @@ tr.frm_options_heading td {
font-size: var(--text-md);
box-sizing: border-box;
border: 1px dashed var(--grey-300);
+ outline: 2px solid transparent;
}
.frm-over-droppable + .frm_no_fields {
border-style: solid;
+ border-color: var(--primary-500);
+ outline-color: var(--primary-500);
}
.frm_no_section_fields {
@@ -6850,7 +6875,8 @@ tr.frm_options_heading td {
visibility: hidden;
}
-.frm-has-fields .frm_no_fields {
+.frm-has-fields .frm_no_fields,
+#frm_form_editor_container:not(.frm-has-fields) #frm_drag_placeholder {
display: none;
}
diff --git a/js/formidable_admin.js b/js/formidable_admin.js
index 05ac99d18b..9d1348a0eb 100644
--- a/js/formidable_admin.js
+++ b/js/formidable_admin.js
@@ -2598,6 +2598,8 @@ function frmAdminBuildJS() {
deselectFields();
initiateMultiselect();
+ document.getElementById( 'frm-show-fields' ).classList.remove( 'frm-over-droppable' );
+
const addedEvent = new Event( 'frm_added_field', { bubbles: false });
addedEvent.frmField = field;
addedEvent.frmSection = section;
@@ -10385,7 +10387,6 @@ function frmAdminBuildJS() {
}
}
-
/**
* Initializes and manages the visibility of dependent elements based on the selected options in dropdowns with the 'frm_select_with_dependency' class.
* It sets up initial visibility at page load and updates it on each dropdown change.
@@ -10418,7 +10419,36 @@ function frmAdminBuildJS() {
// Update dependencies visibility on dropdown change
frmDom.util.documentOn( 'change', 'select.frm_select_with_dependency', ( event ) => toggleDependencyVisibility( event.target ) );
- };
+ }
+
+ /**
+ * Moves the focus to the next single option input field in the list and positions the cursor at the end of the text.
+ *
+ * @param {HTMLElement} currentInput The currently focused input element.
+ */
+ function focusNextSingleOptionInput( currentInput ) {
+ const optionsList = currentInput.closest( '.frm_single_option' ).parentElement;
+ const inputs = optionsList.querySelectorAll( '.frm_single_option input[name^="field_options[" ], .frm_single_option input[name^="rows_"]' );
+ const inputsArray = Array.from( inputs );
+
+ // Find the index of the currently focused input
+ const currentIndex = inputsArray.indexOf( currentInput );
+
+ if ( currentIndex < 0 ) {
+ return;
+ }
+
+ // Find the next visible input field
+ const nextInput = inputsArray.slice( currentIndex + 1 ).find( input => input.offsetParent !== null );
+
+ if ( nextInput ) {
+ nextInput.focus();
+
+ // Move the cursor to the end of the text in the next input field
+ const textLength = nextInput.value.length;
+ nextInput.setSelectionRange( textLength, textLength );
+ }
+ }
return {
init: function() {
@@ -10752,6 +10782,13 @@ function frmAdminBuildJS() {
frmDom.util.documentOn( 'click', '.frm-show-field-settings', clickVis );
frmDom.util.documentOn( 'change', 'select.frm_format_dropdown, select.frm_phone_type_dropdown', maybeUpdateFormatInput );
+ // Navigate to the next input field on pressing Enter in a single option field
+ $builderForm.on( 'keydown', '.frm_single_option input[name^="field_options["], .frm_single_option input[name^="rows_"]', event => {
+ if ( 'Enter' === event.key ) {
+ focusNextSingleOptionInput( event.currentTarget );
+ }
+ });
+
initBulkOptionsOverlay();
hideEmptyEle();
maybeHideQuantityProductFieldOption();