widget_name() . '/section_layout/before_section_end',
array( $this, 'register_provider_controls' )
);
add_action(
'elementor/query/query_results',
array( $this, 'add_no_result_text' ),
10, 2
);
if ( ! jet_smart_filters()->query->is_ajax_filter() ) {
/**
* First of all you need to store default provider query and required attributes to allow
* JetSmartFilters attach this data to AJAX request.
*/
add_filter( 'elementor/widget/before_render_content', array( $this, 'store_default_settings' ), 0, 3 );
/**
* Store default query args
*/
add_action( 'elementor/query/query_results', array( $this, 'store_default_query' ), 10, 2 );
}
}
/**
* Register provider-specific controls for the widget
*/
public function register_provider_controls( $widget ) {
$widget->add_control(
'_jsf_filterable',
array(
'label' => esc_html__( 'Is Filterable', 'elementor-pro' ),
'description' => esc_html__( 'Enable this if you want to filter current loop with JetSmartFilters', 'jet-samrt-filters' ),
'type' => 'switcher',
'separator' => 'before',
)
);
$widget->add_control(
'_jsf_no_result_text',
array(
'label' => esc_html__( 'No Result Text', 'elementor-pro' ),
'label_block' => true,
'description' => esc_html__( 'Text that will be displayed if there are no results after filtering', 'jet-samrt-filters' ),
'type' => 'text',
'default' => __( 'Sorry, nothing to see here...', 'elementor-pro' ),
'condition' => array(
'_jsf_filterable' => 'yes'
),
)
);
}
public function add_no_result_text( $query, $widget ) {
if ( ! $this->is_filterable_widget( $widget ) ) {
return;
}
$total = $query->found_posts;
if ( $total == 0 ) {
$no_result_text = $widget->get_settings_for_display()['_jsf_no_result_text'];
if ( $no_result_text ) {
remove_action(
'elementor/query/query_results',
array( $this, 'add_no_result_text' ),
10, 2
);
$classes = apply_filters( 'jet-smart-filters/providers/' . $this->widget_name() . '/no-result-classes', array(
'container' => 'elementor-loop-container',
'text' => 'jet-smart-filters-elementor-loop-no-result'
));
echo '
';
}
}
}
public function is_filterable_widget( $widget ) {
if ( $this->widget_name() !== $widget->get_name() ) {
return false;
}
$settings = $widget->get_settings_for_display();
$is_filterable = isset( $settings['_jsf_filterable'] ) ? $settings['_jsf_filterable'] : false;
$is_filterable = filter_var( $is_filterable, FILTER_VALIDATE_BOOLEAN );
if ( ! $is_filterable ) {
return false;
}
return true;
}
public function get_query_id( $widget ) {
$settings = $widget->get_settings_for_display();
return ! empty( $settings['_element_id'] ) ? $settings['_element_id'] : 'default';
}
/**
* Store default block attributes to add them to filters AJAX request
*/
public function store_default_settings( $widget ) {
if ( ! $this->is_filterable_widget( $widget ) ) {
return;
}
$settings = $widget->get_settings_for_display();
$query_id = $this->get_query_id( $widget );
$current_document = \Elementor\Plugin::$instance->documents->get_current();
if ( ! $current_document ) {
$post_id = get_the_ID();
} else {
$post_id = $current_document->get_main_id();
}
/**
* We'll parse required block settings from page content.
* In this case such approach used because we need inner content anyway.
* If your block content defined only with attributes - here you can set array of these attributes
* and store it with jet_smart_filters()->providers->add_provider_settings(), than filter add these attributes
* to request and you'll can create new instane of required block without content parsing
*/
$attrs = array(
'widget_id' => $widget->get_id(),
'filtered_post_id' => $post_id,
);
jet_smart_filters()->providers->add_provider_settings( $this->get_id(), $attrs, $query_id );
/**
* Store default query args from widget query settings
*/
// regular query args filter
add_filter( 'elementor/query/query_args', array( $this, 'store_default_widget_query_settings' ), 20, 2 );
// current query args filter
add_filter( 'elementor/query/get_query_args/current_query', function( $current_query_args ) use ( $widget ) {
return $this->store_default_widget_query_settings(
// adding 'post_status' to current query arguments to hide draft posts
array_merge(
array(
'post_status' => array( 'publish', 'private' )
),
$current_query_args
),
$widget
);
} );
}
/**
* Save default query
*/
public function store_default_query( $wp_query, $widget ) {
if ( ! $this->is_filterable_widget( $widget ) ) {
return;
}
$settings = $widget->get_settings_for_display();
$query_id = $this->get_query_id( $widget );
$wp_query->set( 'jet_smart_filters', $this->get_id() . '/' . $query_id );
jet_smart_filters()->query->set_props(
$this->get_id(),
array(
'found_posts' => $wp_query->found_posts,
'max_num_pages' => $wp_query->max_num_pages,
'page' => $wp_query->get( 'paged' ),
),
$query_id
);
}
/**
* Save default query from widget query settings
*/
public function store_default_widget_query_settings( $query_args, $widget ) {
if ( ! $this->is_filterable_widget( $widget ) ) {
return $query_args;
}
$settings = $widget->get_settings_for_display();
$query_id = $this->get_query_id( $widget );
$default_args = $query_args;
/*
* these parameters break:
- the alternate template "Static item position" option
- Query source - Sales/Featured/Cross-Sells
*/
foreach ( array( 'paged', 'posts_per_page', 'ignore_sticky_posts', ) as $propKey ) {
unset( $default_args[$propKey] );
}
jet_smart_filters()->query->store_provider_default_query( $this->get_id(), $default_args, $query_id );
return $query_args;
}
/**
* Returns Elementor Pro apropriate widget name
*/
public function widget_name() {
return 'loop-grid';
}
/**
* Get provider name
*/
public function get_name() {
return __( 'Elementor Pro Loop Grid', 'jet-samrt-filters' );
}
/**
* Get provider ID
*/
public function get_id() {
return 'epro-loop-builder';
}
/**
* Get provider wrapper selector
* Its CSS selector of HTML element with provider content.
*/
public function get_wrapper_selector() {
return '.elementor-loop-container';
}
/**
* Set prefix for unique ID selector. Mostly is default '#' sign, but sometimes class '.' sign needed.
* For example for Query Loop block we don't have HTML/CSS ID attribute, so we need to use class as unique identifier.
*/
public function id_prefix() {
return '#';
}
/**
* Action for wrapper selector - 'insert' into it or 'replace'
*/
public function get_wrapper_action() {
return 'replace';
}
/**
* If added unique ID this paramter will determine - search selector inside this ID, or is the same element
*/
public function in_depth() {
return true;
}
/**
* Get filtered provider content.
*/
public function ajax_get_content() {
$settings = ! empty( $_REQUEST['settings'] ) ? $_REQUEST['settings'] : [];
$post_id = ! empty( $settings['filtered_post_id'] ) ? absint( $settings['filtered_post_id'] ) : false;
$widget_id = ! empty( $settings['widget_id'] ) ? $settings['widget_id'] : false;
if ( ! $post_id || ! $widget_id ) {
_e( 'Error. Incomplete request', 'jet-smart-filters' );
return;
}
$widget = $this->get_filtered_widget( $post_id, $widget_id );
if ( $widget ) {
// regular query args filter
add_filter( 'elementor/query/query_args', array( $this, 'add_query_args' ), 30, 2 );
// current query args filter
add_filter( 'elementor/query/get_query_args/current_query', function( $current_query_vars ) use ( $widget ) {
return $this->add_query_args( $current_query_vars, $widget );
} );
// render content
ob_start();
$skin = $widget->get_current_skin();
if ( $skin ) {
$skin->set_parent( $widget );
$skin->render_by_mode();
} else {
$widget->render_by_mode();
}
$content = ob_get_clean();
if ( $content ) {
echo $content;
} else {
echo '';
}
} else {
echo 'Widget not found';
}
}
/**
* Apply filters on page reload
* Filter arguments in this case pased with $_GET request
*/
public function apply_filters_in_request() {
$args = jet_smart_filters()->query->get_query_args();
if ( ! $args ) {
return;
}
// regular query args filter
add_filter( 'elementor/query/query_args', array( $this, 'add_query_args' ), 30, 2 );
// current query args filter
add_filter( 'elementor/widget/before_render_content', function( $widget ) {
if ( $this->is_filterable_widget( $widget ) ) {
add_filter( 'elementor/query/get_query_args/current_query', function( $current_query_vars ) use ( $widget ) {
return $this->add_query_args( $current_query_vars, $widget );
} );
}
} );
}
/**
* Find filtered widget inside given page content
*/
public function get_filtered_widget( $post_id, $widget_id ) {
$elementor = \Elementor\Plugin::instance();
$document = $elementor->documents->get( $post_id );
if ( $document ) {
$widget = $this->find_widget_recursive( $document->get_elements_data(), $widget_id );
if ( $widget ) {
$widget_instance = $elementor->elements_manager->create_element_instance( $widget );
}
}
return $widget_instance;
}
/**
* Find required widget in given widgets stack
*/
public function find_widget_recursive( $widgets, $widget_id ) {
foreach ( $widgets as $widget ) {
if ( $widget_id === $widget['id'] ) {
return $widget;
}
if ( ! empty( $widget['elements'] ) ) {
$widget = $this->find_widget_recursive( $widget['elements'], $widget_id );
if ( $widget ) {
return $widget;
}
}
}
return false;
}
/**
* Check if is currently filtered widget
*/
public function is_currently_filtered_widget( $widget, $query_id = 'default' ) {
$settings = $widget->get_settings_for_display();
$is_filterable = isset( $settings['_jsf_filterable'] ) ? $settings['_jsf_filterable'] : false;
$is_filterable = filter_var( $is_filterable, FILTER_VALIDATE_BOOLEAN );
if ( ! $is_filterable ) {
return false;
}
$widget_query_id = ! empty( $settings['_element_id'] ) ? $settings['_element_id'] : 'default';
return $query_id === $widget_query_id;
}
/**
* Add custom query arguments
* This methos used by both - AJAX and page reload filters to add filter request data to query.
* You need to check - should it be applied or not before hooking on 'pre_get_posts'
*/
public function add_query_args( $query_args, $widget ) {
/**
* With this method we can get prepared query arguments from filters request.
* This method returns only filtered query argumnets, not whole query.
* Arguments returned in the format prepared for WP_Query usage. If you need to use it in some other way -
* you need to manually parse this arguments into required format.
*
* All custom query variables will be gathered under 'meta_query'
*/
$args = jet_smart_filters()->query->get_query_args();
if ( empty( $args ) ) {
return $query_args;
}
$provider = jet_smart_filters()->query->get_current_provider();
if ( empty( $provider ) || $this->get_id() !== $provider['provider'] ) {
return $query_args;
}
if ( ! $this->is_currently_filtered_widget( $widget, $provider['query_id'] ) ) {
return $query_args;
}
foreach ( $args as $query_var => $value ) {
if ( in_array( $query_var, array( 'tax_query', 'meta_query' ) ) ) {
$current = isset( $query_args[ $query_var ] ) ? $query_args[ $query_var ] : [];
if ( ! empty( $current ) ) {
$value = array_merge( $current, $value );
}
$query_args[ $query_var ] = $value;
} else {
$query_args[ $query_var ] = $value;
}
}
// query args type conversion from string to boolean
$boolean_args = apply_filters(
'jet-smart-filters/widgets/loop-grid/boolean-query-args',
array( 'nopaging', 'no_found_rows', 'ignore_sticky_posts')
);
foreach ( $boolean_args as $arg ) {
if ( isset( $query_args[$arg] ) ) {
$query_args[$arg] = filter_var( $query_args[$arg], FILTER_VALIDATE_BOOLEAN );
}
}
// Remove offset parameter when pagination is used, because it brakes the pagination
if ( ! empty( $query_args['paged'] ) && isset( $query_args['offset'] ) ) {
unset( $query_args['offset'] );
}
return $query_args;
}
}