1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
if ( ! defined( 'ABSPATH' ) ) { |
4
|
|
|
exit; |
5
|
|
|
} |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Layered Navigation Widget. |
9
|
|
|
* |
10
|
|
|
* @author WooThemes |
11
|
|
|
* @category Widgets |
12
|
|
|
* @package WooCommerce/Widgets |
13
|
|
|
* @version 2.6.0 |
14
|
|
|
* @extends WC_Widget |
15
|
|
|
*/ |
16
|
|
|
class WC_Widget_Layered_Nav extends WC_Widget { |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Constructor. |
20
|
|
|
*/ |
21
|
|
|
public function __construct() { |
22
|
|
|
$this->widget_cssclass = 'woocommerce widget_layered_nav'; |
23
|
|
|
$this->widget_description = __( 'Shows a custom attribute in a widget which lets you narrow down the list of products when viewing product categories.', 'woocommerce' ); |
24
|
|
|
$this->widget_id = 'woocommerce_layered_nav'; |
25
|
|
|
$this->widget_name = __( 'WooCommerce Layered Nav', 'woocommerce' ); |
26
|
|
|
parent::__construct(); |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Updates a particular instance of a widget. |
31
|
|
|
* |
32
|
|
|
* @see WP_Widget->update |
33
|
|
|
* |
34
|
|
|
* @param array $new_instance |
35
|
|
|
* @param array $old_instance |
36
|
|
|
* |
37
|
|
|
* @return array |
38
|
|
|
*/ |
39
|
|
|
public function update( $new_instance, $old_instance ) { |
40
|
|
|
$this->init_settings(); |
41
|
|
|
return parent::update( $new_instance, $old_instance ); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Outputs the settings update form. |
46
|
|
|
* |
47
|
|
|
* @see WP_Widget->form |
48
|
|
|
* |
49
|
|
|
* @param array $instance |
50
|
|
|
*/ |
51
|
|
|
public function form( $instance ) { |
52
|
|
|
$this->init_settings(); |
53
|
|
|
parent::form( $instance ); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Init settings after post types are registered. |
58
|
|
|
*/ |
59
|
|
|
public function init_settings() { |
60
|
|
|
$attribute_array = array(); |
61
|
|
|
$attribute_taxonomies = wc_get_attribute_taxonomies(); |
62
|
|
|
|
63
|
|
|
if ( $attribute_taxonomies ) { |
|
|
|
|
64
|
|
|
foreach ( $attribute_taxonomies as $tax ) { |
65
|
|
|
if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { |
66
|
|
|
$attribute_array[ $tax->attribute_name ] = $tax->attribute_name; |
67
|
|
|
} |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
$this->settings = array( |
72
|
|
|
'title' => array( |
73
|
|
|
'type' => 'text', |
74
|
|
|
'std' => __( 'Filter by', 'woocommerce' ), |
75
|
|
|
'label' => __( 'Title', 'woocommerce' ) |
76
|
|
|
), |
77
|
|
|
'attribute' => array( |
78
|
|
|
'type' => 'select', |
79
|
|
|
'std' => '', |
80
|
|
|
'label' => __( 'Attribute', 'woocommerce' ), |
81
|
|
|
'options' => $attribute_array |
82
|
|
|
), |
83
|
|
|
'display_type' => array( |
84
|
|
|
'type' => 'select', |
85
|
|
|
'std' => 'list', |
86
|
|
|
'label' => __( 'Display type', 'woocommerce' ), |
87
|
|
|
'options' => array( |
88
|
|
|
'list' => __( 'List', 'woocommerce' ), |
89
|
|
|
'dropdown' => __( 'Dropdown', 'woocommerce' ) |
90
|
|
|
) |
91
|
|
|
), |
92
|
|
|
'query_type' => array( |
93
|
|
|
'type' => 'select', |
94
|
|
|
'std' => 'and', |
95
|
|
|
'label' => __( 'Query type', 'woocommerce' ), |
96
|
|
|
'options' => array( |
97
|
|
|
'and' => __( 'AND', 'woocommerce' ), |
98
|
|
|
'or' => __( 'OR', 'woocommerce' ) |
99
|
|
|
) |
100
|
|
|
), |
101
|
|
|
); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Output widget. |
106
|
|
|
* |
107
|
|
|
* @see WP_Widget |
108
|
|
|
* |
109
|
|
|
* @param array $args |
110
|
|
|
* @param array $instance |
111
|
|
|
*/ |
112
|
|
|
public function widget( $args, $instance ) { |
113
|
|
|
if ( ! is_post_type_archive( 'product' ) && ! is_tax( get_object_taxonomies( 'product' ) ) ) { |
114
|
|
|
return; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); |
118
|
|
|
$taxonomy = isset( $instance['attribute'] ) ? wc_attribute_taxonomy_name( $instance['attribute'] ) : $this->settings['attribute']['std']; |
119
|
|
|
$query_type = isset( $instance['query_type'] ) ? $instance['query_type'] : $this->settings['query_type']['std']; |
120
|
|
|
$display_type = isset( $instance['display_type'] ) ? $instance['display_type'] : $this->settings['display_type']['std']; |
121
|
|
|
|
122
|
|
|
if ( ! taxonomy_exists( $taxonomy ) ) { |
123
|
|
|
return; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$get_terms_args = array( 'hide_empty' => '1' ); |
127
|
|
|
|
128
|
|
|
$orderby = wc_attribute_orderby( $taxonomy ); |
129
|
|
|
|
130
|
|
View Code Duplication |
switch ( $orderby ) { |
|
|
|
|
131
|
|
|
case 'name' : |
132
|
|
|
$get_terms_args['orderby'] = 'name'; |
133
|
|
|
$get_terms_args['menu_order'] = false; |
134
|
|
|
break; |
135
|
|
|
case 'id' : |
136
|
|
|
$get_terms_args['orderby'] = 'id'; |
137
|
|
|
$get_terms_args['order'] = 'ASC'; |
138
|
|
|
$get_terms_args['menu_order'] = false; |
139
|
|
|
break; |
140
|
|
|
case 'menu_order' : |
141
|
|
|
$get_terms_args['menu_order'] = 'ASC'; |
142
|
|
|
break; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
$terms = get_terms( $taxonomy, $get_terms_args ); |
146
|
|
|
|
147
|
|
|
if ( 0 === sizeof( $terms ) ) { |
148
|
|
|
return; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
ob_start(); |
152
|
|
|
|
153
|
|
|
$this->widget_start( $args, $instance ); |
154
|
|
|
|
155
|
|
|
if ( 'dropdown' === $display_type ) { |
156
|
|
|
$found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type ); |
157
|
|
|
} else { |
158
|
|
|
$found = $this->layered_nav_list( $terms, $taxonomy, $query_type ); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
$this->widget_end( $args ); |
162
|
|
|
|
163
|
|
|
// Force found when option is selected - do not force found on taxonomy attributes |
164
|
|
|
if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) { |
165
|
|
|
$found = true; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
if ( ! $found ) { |
169
|
|
|
ob_end_clean(); |
170
|
|
|
} else { |
171
|
|
|
echo ob_get_clean(); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Return the currently viewed taxonomy name. |
177
|
|
|
* @return string |
178
|
|
|
*/ |
179
|
|
|
protected function get_current_taxonomy() { |
180
|
|
|
return is_tax() ? get_queried_object()->taxonomy : ''; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Return the currently viewed term ID. |
185
|
|
|
* @return int |
186
|
|
|
*/ |
187
|
|
|
protected function get_current_term_id() { |
188
|
|
|
return absint( is_tax() ? get_queried_object()->term_id : 0 ); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Return the currently viewed term slug. |
193
|
|
|
* @return int |
194
|
|
|
*/ |
195
|
|
|
protected function get_current_term_slug() { |
196
|
|
|
return absint( is_tax() ? get_queried_object()->slug : 0 ); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Show dropdown layered nav. |
201
|
|
|
* @param array $terms |
202
|
|
|
* @param string $taxonomy |
203
|
|
|
* @param string $query_type |
204
|
|
|
* @return bool Will nav display? |
205
|
|
|
*/ |
206
|
|
|
protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) { |
207
|
|
|
$found = false; |
208
|
|
|
|
209
|
|
|
if ( $taxonomy !== $this->get_current_taxonomy() ) { |
210
|
|
|
$term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); |
211
|
|
|
$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); |
212
|
|
|
$taxonomy_filter_name = str_replace( 'pa_', '', $taxonomy ); |
213
|
|
|
|
214
|
|
|
echo '<select class="dropdown_layered_nav_' . esc_attr( $taxonomy_filter_name ) . '">'; |
215
|
|
|
echo '<option value="">' . sprintf( __( 'Any %s', 'woocommerce' ), wc_attribute_label( $taxonomy ) ) . '</option>'; |
216
|
|
|
|
217
|
|
|
foreach ( $terms as $term ) { |
218
|
|
|
|
219
|
|
|
// If on a term page, skip that term in widget list |
220
|
|
|
if ( $term->term_id === $this->get_current_term_id() ) { |
221
|
|
|
continue; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
// Get count based on current view |
225
|
|
|
$current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); |
226
|
|
|
$option_is_set = in_array( $term->slug, $current_values ); |
227
|
|
|
$count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; |
228
|
|
|
|
229
|
|
|
// Only show options with count > 0 |
230
|
|
View Code Duplication |
if ( 0 < $count ) { |
|
|
|
|
231
|
|
|
$found = true; |
232
|
|
|
} elseif ( 'and' === $query_type && 0 === $count && ! $option_is_set ) { |
233
|
|
|
continue; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
echo '<option value="' . esc_attr( $term->slug ) . '" ' . selected( $option_is_set, true, false ) . '>' . esc_html( $term->name ) . '</option>'; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
echo '</select>'; |
240
|
|
|
|
241
|
|
|
wc_enqueue_js( " |
242
|
|
|
jQuery( '.dropdown_layered_nav_". esc_js( $taxonomy_filter_name ) . "' ).change( function() { |
243
|
|
|
var slug = jQuery( this ).val(); |
244
|
|
|
location.href = '" . preg_replace( '%\/page\/[0-9]+%', '', str_replace( array( '&', '%2C' ), array( '&', ',' ), esc_js( add_query_arg( 'filtering', '1', remove_query_arg( array( 'page', 'filter_' . $taxonomy_filter_name ) ) ) ) ) ) . "&filter_". esc_js( $taxonomy_filter_name ) . "=' + slug; |
245
|
|
|
}); |
246
|
|
|
" ); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
return $found; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Get current page URL for layered nav items. |
254
|
|
|
* @return string |
255
|
|
|
*/ |
256
|
|
|
protected function get_page_base_url( $taxonomy ) { |
257
|
|
View Code Duplication |
if ( defined( 'SHOP_IS_ON_FRONT' ) ) { |
|
|
|
|
258
|
|
|
$link = home_url(); |
259
|
|
|
} elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id('shop') ) ) { |
260
|
|
|
$link = get_post_type_archive_link( 'product' ); |
261
|
|
|
} else { |
262
|
|
|
$link = get_term_link( get_query_var('term'), get_query_var('taxonomy') ); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
// Min/Max |
266
|
|
|
if ( isset( $_GET['min_price'] ) ) { |
267
|
|
|
$link = add_query_arg( 'min_price', wc_clean( $_GET['min_price'] ), $link ); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
if ( isset( $_GET['max_price'] ) ) { |
271
|
|
|
$link = add_query_arg( 'max_price', wc_clean( $_GET['max_price'] ), $link ); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
// Orderby |
275
|
|
View Code Duplication |
if ( isset( $_GET['orderby'] ) ) { |
|
|
|
|
276
|
|
|
$link = add_query_arg( 'orderby', wc_clean( $_GET['orderby'] ), $link ); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
// Search Arg |
280
|
|
|
if ( get_search_query() ) { |
281
|
|
|
$link = add_query_arg( 's', get_search_query(), $link ); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
// Post Type Arg |
285
|
|
View Code Duplication |
if ( isset( $_GET['post_type'] ) ) { |
|
|
|
|
286
|
|
|
$link = add_query_arg( 'post_type', wc_clean( $_GET['post_type'] ), $link ); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
// Min Rating Arg |
290
|
|
|
if ( isset( $_GET['min_rating'] ) ) { |
291
|
|
|
$link = add_query_arg( 'min_rating', wc_clean( $_GET['min_rating'] ), $link ); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
// All current filters |
295
|
|
View Code Duplication |
if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) { |
|
|
|
|
296
|
|
|
foreach ( $_chosen_attributes as $name => $data ) { |
297
|
|
|
if ( $name === $taxonomy ) { |
298
|
|
|
continue; |
299
|
|
|
} |
300
|
|
|
$filter_name = sanitize_title( str_replace( 'pa_', '', $name ) ); |
301
|
|
|
if ( ! empty( $data['terms'] ) ) { |
302
|
|
|
$link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link ); |
303
|
|
|
} |
304
|
|
|
if ( 'or' == $data['query_type'] ) { |
305
|
|
|
$link = add_query_arg( 'query_type_' . $filter_name, 'or', $link ); |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $link; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Count products within certain terms, taking the main WP query into consideration. |
315
|
|
|
* @param array $term_ids |
316
|
|
|
* @param string $taxonomy |
317
|
|
|
* @param string $query_type |
318
|
|
|
* @return array |
319
|
|
|
*/ |
320
|
|
|
protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { |
321
|
|
|
global $wpdb; |
322
|
|
|
|
323
|
|
|
$tax_query = WC_Query::get_main_tax_query(); |
324
|
|
|
$meta_query = WC_Query::get_main_meta_query(); |
325
|
|
|
|
326
|
|
|
if ( 'or' === $query_type ) { |
327
|
|
|
foreach ( $tax_query as $key => $query ) { |
328
|
|
|
if ( $taxonomy === $query['taxonomy'] ) { |
329
|
|
|
unset( $tax_query[ $key ] ); |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$meta_query = new WP_Meta_Query( $meta_query ); |
335
|
|
|
$tax_query = new WP_Tax_Query( $tax_query ); |
336
|
|
|
$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); |
337
|
|
|
$tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); |
338
|
|
|
|
339
|
|
|
$sql = " |
340
|
|
|
SELECT COUNT( {$wpdb->posts}.ID ) as term_count, term_count_relationships.term_taxonomy_id as term_count_id FROM {$wpdb->posts} |
341
|
|
|
INNER JOIN {$wpdb->term_relationships} AS term_count_relationships ON ({$wpdb->posts}.ID = term_count_relationships.object_id) |
342
|
|
|
" . $tax_query_sql['join'] . $meta_query_sql['join'] . " |
343
|
|
|
WHERE {$wpdb->posts}.post_type = 'product' AND {$wpdb->posts}.post_status = 'publish' |
344
|
|
|
" . $tax_query_sql['where'] . $meta_query_sql['where'] . " |
345
|
|
|
AND term_count_relationships.term_taxonomy_id IN (" . implode( ',', array_map( 'absint', $term_ids ) ) . ") |
346
|
|
|
GROUP BY term_count_relationships.term_taxonomy_id; |
347
|
|
|
"; |
348
|
|
|
|
349
|
|
|
$results = $wpdb->get_results( $sql ); |
350
|
|
|
|
351
|
|
|
return wp_list_pluck( $results, 'term_count', 'term_count_id' ); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Show list based layered nav. |
356
|
|
|
* @param array $terms |
357
|
|
|
* @param string $taxonomy |
358
|
|
|
* @param string $query_type |
359
|
|
|
* @return bool Will nav display? |
360
|
|
|
*/ |
361
|
|
|
protected function layered_nav_list( $terms, $taxonomy, $query_type ) { |
362
|
|
|
// List display |
363
|
|
|
echo '<ul>'; |
364
|
|
|
|
365
|
|
|
$term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); |
366
|
|
|
$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); |
367
|
|
|
$found = false; |
368
|
|
|
|
369
|
|
|
foreach ( $terms as $term ) { |
370
|
|
|
$current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); |
371
|
|
|
$option_is_set = in_array( $term->slug, $current_values ); |
372
|
|
|
$count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; |
373
|
|
|
|
374
|
|
|
// skip the term for the current archive |
375
|
|
|
if ( $this->get_current_term_id() === $term->term_id ) { |
376
|
|
|
continue; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
// Only show options with count > 0 |
380
|
|
View Code Duplication |
if ( 0 < $count ) { |
|
|
|
|
381
|
|
|
$found = true; |
382
|
|
|
} elseif ( 'and' === $query_type && 0 === $count && ! $option_is_set ) { |
383
|
|
|
continue; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
$filter_name = 'filter_' . sanitize_title( str_replace( 'pa_', '', $taxonomy ) ); |
387
|
|
|
$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( $_GET[ $filter_name ] ) ) : array(); |
388
|
|
|
$current_filter = array_map( 'sanitize_title', $current_filter ); |
389
|
|
|
|
390
|
|
|
if ( ! in_array( $term->slug, $current_filter ) ) { |
391
|
|
|
$current_filter[] = $term->slug; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
$link = $this->get_page_base_url( $taxonomy ); |
395
|
|
|
|
396
|
|
|
// Add current filters to URL. |
397
|
|
|
foreach ( $current_filter as $key => $value ) { |
398
|
|
|
// Exclude query arg for current term archive term |
399
|
|
|
if ( $value === $this->get_current_term_slug() ) { |
400
|
|
|
unset( $current_filter[ $key ] ); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
// Exclude self so filter can be unset on click. |
404
|
|
|
if ( $option_is_set && $value === $term->slug ) { |
405
|
|
|
unset( $current_filter[ $key ] ); |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
if ( ! empty( $current_filter ) ) { |
410
|
|
|
$link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link ); |
411
|
|
|
|
412
|
|
|
// Add Query type Arg to URL |
413
|
|
|
if ( $query_type === 'or' && ! ( 1 === sizeof( $current_filter ) && $option_is_set ) ) { |
414
|
|
|
$link = add_query_arg( 'query_type_' . sanitize_title( str_replace( 'pa_', '', $taxonomy ) ), 'or', $link ); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
echo '<li class="wc-layered-nav-term ' . ( $option_is_set ? 'chosen' : '' ) . '">'; |
419
|
|
|
|
420
|
|
|
echo ( $count > 0 || $option_is_set ) ? '<a href="' . esc_url( apply_filters( 'woocommerce_layered_nav_link', $link ) ) . '">' : '<span>'; |
421
|
|
|
|
422
|
|
|
echo esc_html( $term->name ); |
423
|
|
|
|
424
|
|
|
echo ( $count > 0 || $option_is_set ) ? '</a>' : '</span>'; |
425
|
|
|
|
426
|
|
|
echo ' <span class="count">(' . absint( $count ) . ')</span></li>'; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
echo '</ul>'; |
430
|
|
|
|
431
|
|
|
return $found; |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.