WC_Widget_Layered_Nav

Layered Navigation Widget.

Defined (1)

The class is defined in the following location(s).

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