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();