PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/woocommerce/includes/widgets/class-wc-widget-layered-nav.php

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