Source: includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php

<?php
/**
 * Class responsible for rendering the filters.
 *
 * @since 4.2.0
 * @package elasticpress
 */

namespace ElasticPress\Feature\Facets\Types\Taxonomy;

use ElasticPress\Features;
use ElasticPress\Utils;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Facets render class
 */
class Renderer extends \ElasticPress\Feature\Facets\Renderer {
	/**
	 * Whether the term count should be displayed or not.
	 *
	 * @var bool
	 */
	protected $display_count = false;

	/**
	 * Output the widget or block HTML.
	 *
	 * @param array $args     Widget args
	 * @param array $instance Instance settings
	 */
	public function render( $args, $instance ) {
		global $wp_query;

		$args     = wp_parse_args(
			$args,
			[
				'before_widget' => '',
				'before_title'  => '',
				'after_title'   => '',
				'after_widget'  => '',
			]
		);
		$instance = wp_parse_args(
			$instance,
			[
				'title'        => '',
				'displayCount' => false,
			]
		);

		$feature = Features::factory()->get_registered_feature( 'facets' );

		if ( $wp_query->get( 'ep_facet', false ) && ! $feature->is_facetable( $wp_query ) ) {
			return false;
		}

		$es_success = ( ! empty( $wp_query->elasticsearch_success ) ) ? true : false;

		if ( ! $es_success ) {
			return;
		}

		if ( empty( $instance['facet'] ) ) {
			return;
		}

		$taxonomy            = $instance['facet'];
		$this->display_count = $instance['displayCount'];

		if ( ! is_search() ) {

			if ( is_tax() ) {
				$post_type = get_taxonomy( get_queried_object()->taxonomy )->object_type;
			} else {
				$post_type = $wp_query->get( 'post_type' ) ? $wp_query->get( 'post_type' ) : 'post';
			}

			if ( ! is_object_in_taxonomy( $post_type, $taxonomy ) ) {
				return;
			}
		}

		$selected_filters = $feature->get_selected();

		/**
		 * Get all the terms so we know if we should output the widget
		 */
		$terms = get_terms(
			/**
			 * Filter arguments passed to get_terms() while getting all possible terms for the facet widget.
			 *
			 * @since  3.5.0
			 * @hook ep_facet_search_get_terms_args
			 * @param  {array} $terms_args Array of arguments passed to get_terms()
			 * @param  {array} $args Widget args
			 * @param  {array} $instance Instance settings
			 * @return  {array} New terms args
			 */
			apply_filters(
				'ep_facet_search_get_terms_args',
				[
					'taxonomy'               => $taxonomy,
					'hide_empty'             => true,
					'update_term_meta_cache' => false,
				],
				$args,
				$instance
			)
		);

		/**
		 * Terms validity check
		 */
		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return;
		}

		$terms_by_slug = array();

		foreach ( $terms as $term ) {
			$terms_by_slug[ $term->slug ] = $term;

			if ( ! empty( $GLOBALS['ep_facet_aggs'][ $taxonomy ][ $term->slug ] ) ) {
				$term->count = $GLOBALS['ep_facet_aggs'][ $taxonomy ][ $term->slug ];
			} else {
				$term->count = 0;
			}
		}

		/**
		 * Filter the taxonomy facet terms.
		 *
		 * Example of usage, to hide unavailable category terms:
		 * ```
		 * add_filter(
		 *     'ep_facet_taxonomy_terms',
		 *     function ( $terms, $taxonomy ) {
		 *         if ( 'category' !== $taxonomy ) {
		 *             return $terms;
		 *         }
		 *
		 *         return array_filter(
		 *              $terms,
		 *              function ( $term ) {
		 *                  return $term->count > 0;
		 *              }
		 *         );
		 *      },
		 *      10,
		 *      2
		 * );
		 * ```
		 *
		 * @since 4.3.1
		 * @hook ep_facet_taxonomy_terms
		 * @param {array} $terms Terms
		 * @param {string} $taxonomy Taxonomy name
		 * @return {array} New terms
		 */
		$terms_by_slug = apply_filters( 'ep_facet_taxonomy_terms', $terms_by_slug, $taxonomy );

		if ( empty( $terms_by_slug ) ) {
			return;
		}

		/**
		 * Check to make sure all terms exist before proceeding
		 */
		if ( ! empty( $selected_filters['taxonomies'][ $taxonomy ] ) && ! empty( $selected_filters['taxonomies'][ $taxonomy ]['terms'] ) ) {
			foreach ( $selected_filters['taxonomies'][ $taxonomy ]['terms'] as $term_slug => $nothing ) {
				if ( empty( $terms_by_slug[ $term_slug ] ) ) {
					/**
					 * Term does not exist!
					 */
					return;
				}
			}
		}

		$orderby = isset( $instance['orderby'] ) ? $instance['orderby'] : 'count';
		$order   = isset( $instance['order'] ) ? $instance['order'] : 'count';

		$terms = Utils\get_term_tree( $terms_by_slug, $orderby, $order, true );

		$outputted_terms = array();

		echo wp_kses_post( $args['before_widget'] );

		if ( ! empty( $instance['title'] ) ) {
			echo wp_kses_post( $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'] );
		}

		$taxonomy_object = get_taxonomy( $taxonomy );

		/**
		 * Filter facet search threshold
		 *
		 * @hook ep_facet_search_threshold
		 * @param  {int} $search_threshold Search threshold
		 * @param  {string} $taxonomy Current taxonomy
		 * @param  {string} $context Hint about where the value will be used
		 * @return  {int} New threshold
		 */
		$search_threshold = apply_filters( 'ep_facet_search_threshold', 15, $taxonomy, 'taxonomy' );
		?>

		<div class="terms <?php if ( count( $terms_by_slug ) > $search_threshold ) : ?>searchable<?php endif; ?>">
			<?php if ( count( $terms_by_slug ) > $search_threshold ) : ?>
				<?php // translators: Taxonomy Name ?>
				<input class="facet-search" type="search" placeholder="<?php printf( esc_html__( 'Search %s', 'elasticpress' ), esc_attr( $taxonomy_object->labels->name ) ); ?>">
				<?php
			endif;
			ob_start();
			?>

			<div class="inner">
				<?php if ( ! empty( $selected_filters['taxonomies'][ $taxonomy ] ) ) : ?>
					<?php
					foreach ( $selected_filters['taxonomies'][ $taxonomy ]['terms'] as $term_slug => $value ) :
						if ( ! empty( $outputted_terms[ $term_slug ] ) ) {
							continue;
						}

						$term = $terms_by_slug[ $term_slug ];

						if ( empty( $term->parent ) && empty( $term->children ) ) {
							$outputted_terms[ $term_slug ] = $term;
							$new_filters                   = $selected_filters;

							if ( ! empty( $new_filters['taxonomies'][ $taxonomy ] ) && ! empty( $new_filters['taxonomies'][ $taxonomy ]['terms'][ $term_slug ] ) ) {
								unset( $new_filters['taxonomies'][ $taxonomy ]['terms'][ $term_slug ] );
							}

							$term->is_selected = true;
							// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
							echo $this->get_facet_item_value_html(
								$term,
								$feature->build_query_url( $new_filters )
							);
							// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
						} else {
							/**
							 * This code is so that when we encounter a selected child/parent term, we push it's whole branch
							 * to the top. Very very painful.
							 */
							$top_of_tree = $term;
							$i           = 0;

							/**
							 * Get top of tree
							 */
							while ( true && $i < 10 ) {
								if ( ! empty( $term->parent_term->slug ) ) {
									$top_of_tree = $terms_by_slug[ $term->parent_term->slug ];
								} else {
									break;
								}

								++$i;
							}

							$flat_ordered_terms = array();

							$flat_ordered_terms[] = $top_of_tree;

							$to_process = $this->order_by_selected( $top_of_tree->children, $selected_filters['taxonomies'][ $taxonomy ]['terms'], $order, $orderby );

							while ( ! empty( $to_process ) ) {
								$term = array_shift( $to_process );

								$flat_ordered_terms[] = $term;

								if ( ! empty( $term->children ) ) {
									$to_process = array_merge( $this->order_by_selected( $term->children, $selected_filters['taxonomies'][ $taxonomy ]['terms'], $order, $orderby ), $to_process );
								}
							}

							foreach ( $flat_ordered_terms as $term ) {
								$selected                       = ! empty( $selected_filters['taxonomies'][ $taxonomy ]['terms'][ $term->slug ] );
								$outputted_terms[ $term->slug ] = $term;
								$new_filters                    = $selected_filters;

								if ( $selected ) {
									if ( ! empty( $new_filters['taxonomies'][ $taxonomy ] ) && ! empty( $new_filters['taxonomies'][ $taxonomy ]['terms'][ $term->slug ] ) ) {
										unset( $new_filters['taxonomies'][ $taxonomy ]['terms'][ $term->slug ] );
									}
								} else {
									if ( empty( $new_filters['taxonomies'][ $taxonomy ] ) ) {
										$new_filters['taxonomies'][ $taxonomy ] = array(
											'terms' => array(),
										);
									}

									$new_filters['taxonomies'][ $taxonomy ]['terms'][ $term->slug ] = true;
								}

								$term->is_selected = $selected;
								// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
								echo $this->get_facet_item_value_html(
									$term,
									$feature->build_query_url( $new_filters )
								);
								// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
							}
						}
					endforeach;
					?>
				<?php endif; ?>

				<?php
				foreach ( $terms as $term ) :
					if ( ! empty( $outputted_terms[ $term->slug ] ) ) {
						continue;
					}

					$new_filters = $selected_filters;

					if ( empty( $new_filters['taxonomies'][ $taxonomy ] ) ) {
						$new_filters['taxonomies'][ $taxonomy ] = array(
							'terms' => array(),
						);
					}

					$new_filters['taxonomies'][ $taxonomy ]['terms'][ $term->slug ] = true;

					$term->is_selected = false;

					// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
					echo $this->get_facet_item_value_html(
						$term,
						$feature->build_query_url( $new_filters )
					);
					// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
				endforeach;
				?>
			</div>
			<?php $facet_html = ob_get_clean(); ?>

			<?php
			// phpcs:disable
			/**
			 * Filter facet search widget HTML
			 *
			 * @hook ep_facet_search_widget
			 * @param  {string} $facet_html Widget HTML
			 * @param  {array} $selected_filters Selected filters
			 * @param  {array} $terms_by_slug Terms by slug
			 * @param  {array} $outputted_terms Outputted $terms
			 * @param  {string} $title Widget title
			 * @return  {string} New HTML
			 */
			echo apply_filters( 'ep_facet_search_widget', $facet_html, $selected_filters, $terms_by_slug, $outputted_terms, $instance['title'] );
			// phpcs:enable
			?>
		</div>
		<?php

		// Enqueue Script & Styles
		wp_enqueue_script( 'elasticpress-facets' );
		wp_enqueue_style( 'elasticpress-facets' );

		echo wp_kses_post( $args['after_widget'] );
	}

	/**
	 * Get the markup for an individual facet item.
	 *
	 * @param WP_Term $term     Term object.
	 * @param string  $url      Filter URL.
	 * @param boolean $selected Whether the term is currently selected.
	 * @since 4.2.0, 4.7.0 deprecated in favor of a method in the abstract renderer class.
	 * @return string HTML for an individual facet term.
	 */
	public function get_facet_term_html( $term, $url, $selected = false ) {
		$term->is_selected = $selected;
		_deprecated_function( __FUNCTION__, '4.7.0', '$this->renderer->get_facet_item_value_html()' );

		return $this->get_facet_item_value_html( $term, $url );
	}

	/**
	 * Get the markup for an individual facet item.
	 *
	 * @param WP_Term $item     Facet item Term object.
	 * @param string  $url      Filter URL.
	 * @return string HTML for an individual facet term.
	 */
	public function get_facet_item_value_html( $item, $url ) {
		$href = sprintf(
			'href="%s"',
			esc_url( $url )
		);

		$label = $item->name;
		if ( $this->display_count ) {
			$label .= ' <span>(' . $item->count . ')</span>';
		}

		/**
		 * Filter the label for an individual facet term.
		 *
		 * @since 3.6.3
		 * @hook ep_facet_widget_term_label
		 * @param {string} $label Facet term label.
		 * @param {WP_Term} $item Term object.
		 * @param {boolean} $selected Whether the term is selected.
		 * @return {string} Individual facet term label.
		 */
		$label = apply_filters( 'ep_facet_widget_term_label', $label, $item, $item->is_selected );

		/**
		 * Filter the accessible label for an individual facet term link.
		 *
		 * Used as the aria-label attribute for filter links. The accessible
		 * label should include additional context around what action will be
		 * performed by visiting the link, such as whether the filter will be
		 * added or removed.
		 *
		 * @since 4.0.0
		 * @hook ep_facet_widget_term_accessible_label
		 * @param {string} $label Facet term accessible label.
		 * @param {WP_Term} $item Term object.
		 * @param {boolean} $selected Whether the term is selected.
		 * @return {string} Individual facet term accessible label.
		 */
		$accessible_label = apply_filters(
			'ep_facet_widget_term_accessible_label',
			$item->is_selected
				/* translators: %s: Filter term name. */
				? sprintf( __( 'Remove filter: %s', 'elasticpress' ), $item->name )
				/* translators: %s: Filter term name. */
				: sprintf( __( 'Apply filter: %s', 'elasticpress' ), $item->name ),
			$item,
			$item->is_selected
		);

		$link = sprintf(
			'<a aria-label="%1$s" %2$s rel="nofollow"><div class="ep-checkbox %3$s" role="presentation"></div>%4$s</a>',
			esc_attr( $accessible_label ),
			$item->count ? $href : 'aria-role="link" aria-disabled="true"',
			$item->is_selected ? 'checked' : '',
			wp_kses_post( $label )
		);

		$html = sprintf(
			'<div class="term level-%1$d %2$s %3$s" data-term-name="%4$s" data-term-slug="%5$s">%6$s</div>',
			absint( $item->level ),
			$item->is_selected ? 'selected' : '',
			! $item->count ? 'empty-term' : '',
			esc_attr( strtolower( $item->name ) ),
			esc_attr( strtolower( $item->slug ) ),
			$link
		);

		/**
		 * Filter the HTML for an individual facet term.
		 *
		 * For term search to work correctly the outermost wrapper of the term
		 * HTML must have data-term-name and data-term-slug attributes set to
		 * lowercase versions of the term name and slug respectively.
		 *
		 * Kept for retro compatibility.
		 *
		 * @since 3.6.3
		 * @deprecated 4.7.0
		 * @hook ep_facet_widget_term_html
		 * @param {string} $html Facet term HTML.
		 * @param {WP_Term} $term Term object.
		 * @param {string} $url Filter URL.
		 * @param {boolean} $selected Whether the term is selected.
		 * @return {string} Individual facet term HTML.
		 */
		$html = apply_filters_deprecated( 'ep_facet_widget_term_html', array( $html, $item, $url, $item->is_selected ), '4.7.0', 'ep_facet_taxonomy_value_html' );

		/**
		 * Filter the HTML for an individual facet post-type value.
		 *
		 * For term search to work correctly the outermost wrapper of the term
		 * HTML must have data-term-name and data-term-slug attributes set to
		 * lowercase versions of the term name and slug respectively.
		 *
		 * @since 4.7.0
		 * @hook ep_facet_taxonomy_value_html
		 * @param {string} $html  Facet post-type value HTML.
		 * @param {array}  $item Value array. It contains `value`, `name`, `count`, and `is_selected`.
		 * @param {string} $url   Filter URL.
		 * @return {string} Individual facet taxonomy value HTML.
		 */
		return apply_filters( 'ep_facet_taxonomy_value_html', $html, $item, $url );
	}

	/**
	 * Order terms putting selected at the top
	 *
	 * @param  array  $terms Array of terms
	 * @param  array  $selected_terms Selected terms
	 * @param  string $order The order to sort from. Desc or Asc.
	 * @param  string $orderby The orderby to sort items from.
	 * @return array
	 */
	protected function order_by_selected( $terms, $selected_terms, $order = false, $orderby = false ) {
		$ordered_terms = [];
		$terms_by_slug = [];

		foreach ( $terms as $term ) {
			$terms_by_slug[ $term->slug ] = $term;
		}

		foreach ( $selected_terms as $term_slug ) {
			if ( ! empty( $terms_by_slug[ $term_slug ] ) ) {
				$ordered_terms[ $term_slug ] = $terms_by_slug[ $term_slug ];
			}
		}

		foreach ( $terms_by_slug as $term_slug => $term ) {
			if ( empty( $ordered_terms[ $term_slug ] ) ) {
				$ordered_terms[ $term_slug ] = $terms_by_slug[ $term_slug ];
			}
		}

		if ( 'count' === $orderby ) {
			if ( 'asc' === $order ) {
				uasort(
					$ordered_terms,
					function ( $a, $b ) {
						return $a->count <=> $b->count;
					}
				);
			} else {
				uasort(
					$ordered_terms,
					function ( $a, $b ) {
						return $b->count <=> $a->count;
					}
				);
			}
		} elseif ( 'asc' === $order ) {
				ksort( $ordered_terms );
		} else {
			krsort( $ordered_terms );
		}

		return array_values( $ordered_terms );
	}
}