'jetpack-filters widget_search', 'description' => __( 'Instant search and filtering to help visitors quickly find relevant answers and explore your site.', 'jetpack' ), ) ); if ( Helper::is_active_widget( $this->id ) && ! $this->is_search_active() ) { $this->activate_search(); } if ( is_admin() ) { add_action( 'sidebar_admin_setup', array( $this, 'widget_admin_setup' ) ); } else { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); } add_action( 'jetpack_search_render_filters_widget_title', array( 'Automattic\Jetpack\Search\Template_Tags', 'render_widget_title' ), 10, 3 ); if ( Options::is_instant_enabled() ) { add_action( 'jetpack_search_render_filters', array( 'Automattic\Jetpack\Search\Template_Tags', 'render_instant_filters' ), 10, 2 ); } else { add_action( 'jetpack_search_render_filters', array( 'Automattic\Jetpack\Search\Template_Tags', 'render_available_filters' ), 10, 2 ); } } /** * Check whether search is currently active * * @since 6.3 */ public function is_search_active() { return Jetpack::is_module_active( 'search' ); } /** * Activate search * * @since 6.3 */ public function activate_search() { Jetpack::activate_module( 'search', false, false ); } /** * Enqueues the scripts and styles needed for the customizer. * * @since 5.7.0 */ public function widget_admin_setup() { wp_enqueue_style( 'widget-jetpack-search-filters', plugins_url( 'search/css/search-widget-admin-ui.css', __FILE__ ), array(), JETPACK__VERSION ); // Register jp-tracks and jp-tracks-functions. Tracking::register_tracks_functions_scripts(); wp_register_script( 'jetpack-search-widget-admin', plugins_url( 'search/js/search-widget-admin.js', __FILE__ ), array( 'jquery', 'jquery-ui-sortable', 'jp-tracks-functions' ), JETPACK__VERSION, false ); wp_localize_script( 'jetpack-search-widget-admin', 'jetpack_search_filter_admin', array( 'defaultFilterCount' => self::DEFAULT_FILTER_COUNT, 'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(), 'tracksEventData' => array( 'is_customizer' => (int) is_customize_preview(), ), 'i18n' => array( 'month' => Helper::get_date_filter_type_name( 'month', false ), 'year' => Helper::get_date_filter_type_name( 'year', false ), 'monthUpdated' => Helper::get_date_filter_type_name( 'month', true ), 'yearUpdated' => Helper::get_date_filter_type_name( 'year', true ), ), ) ); wp_enqueue_script( 'jetpack-search-widget-admin' ); } /** * Enqueue scripts and styles for the frontend. * * @since 5.8.0 */ public function enqueue_frontend_scripts() { if ( ! is_active_widget( false, false, $this->id_base, true ) || Options::is_instant_enabled() ) { return; } wp_enqueue_script( 'jetpack-search-widget', plugins_url( 'search/js/search-widget.js', __FILE__ ), array(), JETPACK__VERSION, true ); wp_enqueue_style( 'jetpack-search-widget', plugins_url( 'search/css/search-widget-frontend.css', __FILE__ ), array(), JETPACK__VERSION ); } /** * Get the list of valid sort types/orders. * * @since 5.8.0 * * @return array The sort orders. */ private function get_sort_types() { return array( 'relevance|DESC' => is_admin() ? esc_html__( 'Relevance (recommended)', 'jetpack' ) : esc_html__( 'Relevance', 'jetpack' ), 'date|DESC' => esc_html__( 'Newest first', 'jetpack' ), 'date|ASC' => esc_html__( 'Oldest first', 'jetpack' ), ); } /** * Callback for an array_filter() call in order to only get filters for the current widget. * * @see Jetpack_Search_Widget::widget() * * @since 5.7.0 * * @param array $item Filter item. * * @return bool Whether the current filter item is for the current widget. */ public function is_for_current_widget( $item ) { return isset( $item['widget_id'] ) && $this->id == $item['widget_id']; // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison } /** * This method returns a boolean for whether the widget should show site-wide filters for the site. * * This is meant to provide backwards-compatibility for VIP, and other professional plan users, that manually * configured filters via `Jetpack_Search::set_filters()`. * * @since 5.7.0 * * @return bool Whether the widget should display site-wide filters or not. */ public function should_display_sitewide_filters() { $filter_widgets = get_option( 'widget_jetpack-search-filters' ); // This shouldn't be empty, but just for sanity. if ( empty( $filter_widgets ) ) { return false; } // If any widget has any filters, return false. foreach ( $filter_widgets as $number => $widget ) { $widget_id = sprintf( '%s-%d', $this->id_base, $number ); if ( ! empty( $widget['filters'] ) && is_active_widget( false, $widget_id, $this->id_base ) ) { return false; } } return true; } /** * Widget defaults. * * @param array $instance Previously saved values from database. */ public function jetpack_search_populate_defaults( $instance ) { $instance = wp_parse_args( (array) $instance, array( 'title' => '', 'search_box_enabled' => true, 'user_sort_enabled' => true, 'sort' => self::DEFAULT_SORT, 'filters' => array( array() ), 'post_types' => array(), ) ); return $instance; } /** * Populates the instance array with appropriate default values. * * @since 8.6.0 * @param array $instance Previously saved values from database. * @return array Instance array with default values approprate for instant search */ public function populate_defaults_for_instant_search( $instance ) { return wp_parse_args( (array) $instance, array( 'title' => '', 'filters' => array(), ) ); } /** * Responsible for rendering the widget on the frontend. * * @since 5.0.0 * * @param array $args Widgets args supplied by the theme. * @param array $instance The current widget instance. */ public function widget( $args, $instance ) { $instance = $this->jetpack_search_populate_defaults( $instance ); if ( ( new Status() )->is_offline_mode() ) { echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
widget_empty_instant( $args, $instance ); } else { $this->widget_instant( $args, $instance ); } } else { $this->widget_non_instant( $args, $instance ); } } /** * Render the non-instant frontend widget. * * @since 8.3.0 * * @param array $args Widgets args supplied by the theme. * @param array $instance The current widget instance. */ public function widget_non_instant( $args, $instance ) { $display_filters = false; if ( is_search() ) { if ( Helper::should_rerun_search_in_customizer_preview() ) { Jetpack_Search::instance()->update_search_results_aggregations(); } $filters = Jetpack_Search::instance()->get_filters(); if ( ! Helper::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) { $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) ); } if ( ! empty( $filters ) ) { $display_filters = true; } } if ( ! $display_filters && empty( $instance['search_box_enabled'] ) && empty( $instance['user_sort_enabled'] ) ) { return; } $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; /** This filter is documented in core/src/wp-includes/default-widgets.php */ $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
sorting_to_wp_query_param( $default_sort ); $current_sort = "{$orderby}|{$order}"; // we need to dynamically inject the sort field into the search box when the search box is enabled, and display // it separately when it's not. if ( ! empty( $instance['search_box_enabled'] ) ) { Automattic\Jetpack\Search\Template_Tags::render_widget_search_form( $instance['post_types'], $orderby, $order ); } if ( ! empty( $instance['search_box_enabled'] ) && ! empty( $instance['user_sort_enabled'] ) ) : ?>
maybe_render_sort_javascript( $instance, $order, $orderby ); echo '
'; echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Render the instant frontend widget. * * @since 8.3.0 * * @param array $args Widgets args supplied by the theme. * @param array $instance The current widget instance. */ public function widget_instant( $args, $instance ) { if ( Helper::should_rerun_search_in_customizer_preview() ) { Jetpack_Search::instance()->update_search_results_aggregations(); } $filters = Jetpack_Search::instance()->get_filters(); if ( ! Helper::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) { $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) ); } $display_filters = ! empty( $filters ); $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; /** This filter is documented in core/src/wp-includes/default-widgets.php */ $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
'; echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Render the instant widget for the overlay. * * @since 8.3.0 * * @param array $args Widgets args supplied by the theme. * @param array $instance The current widget instance. */ public function widget_empty_instant( $args, $instance ) { $title = isset( $instance['title'] ) ? $instance['title'] : ''; if ( empty( $title ) ) { $title = ''; } /** This filter is documented in core/src/wp-includes/default-widgets.php */ $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
'; echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders JavaScript for the sorting controls on the frontend. * * This JS is a bit complicated, but here's what it's trying to do: * - find the search form * - find the orderby/order fields and set default values * - detect changes to the sort field, if it exists, and use it to set the order field values * * @since 5.8.0 * * @param array $instance The current widget instance. * @param string $order The order to initialize the select with. * @param string $orderby The orderby to initialize the select with. */ private function maybe_render_sort_javascript( $instance, $order, $orderby ) { if ( Options::is_instant_enabled() ) { return; } if ( ! empty( $instance['user_sort_enabled'] ) ) : ?> maybe_reformat_widget( $new_instance ); $instance = array(); $instance['title'] = sanitize_text_field( $new_instance['title'] ); $instance['search_box_enabled'] = empty( $new_instance['search_box_enabled'] ) ? '0' : '1'; $instance['user_sort_enabled'] = empty( $new_instance['user_sort_enabled'] ) ? '0' : '1'; $instance['sort'] = $new_instance['sort']; $instance['post_types'] = empty( $new_instance['post_types'] ) || empty( $instance['search_box_enabled'] ) ? array() : array_map( 'sanitize_key', $new_instance['post_types'] ); $filters = array(); if ( isset( $new_instance['filter_type'] ) ) { foreach ( (array) $new_instance['filter_type'] as $index => $type ) { $count = (int) $new_instance['num_filters'][ $index ]; $count = min( 50, $count ); // Set max boundary at 50. $count = max( 1, $count ); // Set min boundary at 1. switch ( $type ) { case 'taxonomy': $filters[] = array( 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ), 'type' => 'taxonomy', 'taxonomy' => sanitize_key( $new_instance['taxonomy_type'][ $index ] ), 'count' => $count, ); break; case 'post_type': $filters[] = array( 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ), 'type' => 'post_type', 'count' => $count, ); break; case 'date_histogram': $filters[] = array( 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ), 'type' => 'date_histogram', 'count' => $count, 'field' => sanitize_key( $new_instance['date_histogram_field'][ $index ] ), 'interval' => sanitize_key( $new_instance['date_histogram_interval'][ $index ] ), ); break; } } } if ( ! empty( $filters ) ) { $instance['filters'] = $filters; } return $instance; } /** * Reformats the widget instance array to one that is recognized by the `update` function. * This is only necessary when handling changes from the block-based widget editor. * * @param array $widget_instance - Jetpack Search widget instance. * * @return array - Potentially reformatted instance compatible with the save function. */ protected function maybe_reformat_widget( $widget_instance ) { if ( isset( $widget_instance['filter_type'] ) || ! isset( $widget_instance['filters'] ) || ! is_array( $widget_instance['filters'] ) ) { return $widget_instance; } $instance = $widget_instance; foreach ( $widget_instance['filters'] as $filter ) { $instance['filter_type'][] = isset( $filter['type'] ) ? $filter['type'] : ''; $instance['taxonomy_type'][] = isset( $filter['taxonomy'] ) ? $filter['taxonomy'] : ''; $instance['filter_name'][] = isset( $filter['name'] ) ? $filter['name'] : ''; $instance['num_filters'][] = isset( $filter['count'] ) ? $filter['count'] : 5; $instance['date_histogram_field'][] = isset( $filter['field'] ) ? $filter['field'] : ''; $instance['date_histogram_interval'][] = isset( $filter['interval'] ) ? $filter['interval'] : ''; } unset( $instance['filters'] ); return $instance; } /** * Outputs the settings update form. * * @since 5.0.0 * * @param array $instance Previously saved values from database. */ public function form( $instance ) { if ( Options::is_instant_enabled() ) { return $this->form_for_instant_search( $instance ); } $instance = $this->jetpack_search_populate_defaults( $instance ); $title = wp_strip_all_tags( $instance['title'] ); $hide_filters = Helper::are_filters_by_widget_disabled(); $classes = sprintf( 'jetpack-search-filters-widget %s %s %s', $hide_filters ? 'hide-filters' : '', $instance['search_box_enabled'] ? '' : 'hide-post-types', $this->id ); ?>

false ), 'objects' ) as $post_type ) : ?>

render_widget_edit_filter( $filter ); ?>

populate_defaults_for_instant_search( $instance ); $classes = sprintf( 'jetpack-search-filters-widget %s', $this->id ); ?>

render_widget_edit_filter( $filter ); ?>

" : esc_attr( $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * We need to render HTML in two formats: an Underscore template (client-size) * and native PHP (server-side). This helper function allows for easy rendering * of the "selected" attribute in both formats. * * @since 5.8.0 * * @param string $name Attribute name. * @param string $value Attribute value. * @param string $compare Value to compare to the attribute value to decide if it should be selected. * @param bool $is_template Whether this is for an Underscore template or not. */ private function render_widget_option_selected( $name, $value, $compare, $is_template ) { $compare_js = rawurlencode( $compare ); echo $is_template ? "<%= decodeURIComponent( '$compare_js' ) === $name ? 'selected=\"selected\"' : '' %>" : selected( $value, $compare ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Responsible for rendering a single filter in the customizer or the widget administration screen in wp-admin. * * We use this method for two purposes - rendering the fields server-side, and also rendering a script template for Underscore. * * @since 5.7.0 * * @param array $filter The filter to render. * @param bool $is_template Whether this is for an Underscore template or not. */ public function render_widget_edit_filter( $filter, $is_template = false ) { $args = wp_parse_args( $filter, array( 'name' => '', 'type' => 'taxonomy', 'taxonomy' => '', 'post_type' => '', 'field' => '', 'interval' => '', 'count' => self::DEFAULT_FILTER_COUNT, ) ); $args['name_placeholder'] = Helper::generate_widget_filter_name( $args ); ?>