<?php
/**
 * Custom block that overwrites the core query block.
 * 
 * It enables features like custom pagination, arrows, animations, live search...
 */
namespace Greyd\Query;

// depends on greyd\blocks\helper
// ::implode_html_attributes
// ::compose_css
use \greyd\blocks\helper as Blocks_Helper;

use Greyd\Helper as Helper;
use Greyd\Settings as Settings;

if ( ! defined( 'ABSPATH' ) ) exit;

class Query_Block {

	/**
	 * The Block ID. Defaults to post-results-[i].
	 * 
	 * @var string
	 */
	public $ID;

	/**
	 * The full block, including name and attributes.
	 * 
	 * @var array
	 */
	public $block;

	/**
	 * The block content about to be appended.
	 * 
	 * @var array
	 */
	public $block_content;

	/**
	 * The block without the attributes.
	 * 
	 * @var array
	 */
	public $inner_block;

	/**
	 * Block attributes.
	 * 
	 * @var array
	 */
	public $atts;

	/**
	 * Whether to inherit WP main query.
	 * 
	 * @var bool
	 */
	public $inherit = false;

	/**
	 * Holds the query.
	 * 
	 * @var WP_Query
	 */
	public $query;

	/**
	 * Whether we found any posts.
	 * 
	 * @var bool
	 */
	public $found_posts = 0;

	/**
	 * Holds the advanced search settings.
	 * 
	 * @var array
	 */
	public $search_settings;

	/**
	 * Whether this displays the live search query results.
	 * 
	 * @var bool
	 */
	public $is_live_search = false;

	/**
	 * Whether a live filter query is enabled.
	 * 
	 * @var bool
	 */
	public $is_live_filter = false;

	/**
	 * Wrapper attributes (class, data...)
	 * 
	 * @var array
	 */
	public $wrapper_atts = array(
		'class' => array(
			'greyd-posts-slider',
			'wp-block-post-template',
			// 'posts_wrapper' /** @deprecated since 2.0 */
		),
		'data' => array(
			'currentpage' => '1'
		),
		'style' => array()
	);

	/**
	 * Constructor
	 * 
	 * @param array  $block           The full block, including name and attributes.
	 * @param string $block_content   Already rendered content.
	 * 
	 * @return string HTML wrapper.
	 */
	public function __construct( $block, $block_content="" ) {

		// basic class vars
		$this->block_content    = strval( $block_content );
		$this->block            = $block;
		$this->atts             = isset($block['attrs']) ? $block['attrs'] : array( 'query' => array() );
		$this->inherit          = isset($this->atts['query']['query']['inherit']) && $this->atts['query']['query']['inherit'];
		$this->search_settings  = Settings::get_setting(array('site', 'advanced_search'));
		$this->is_live_search   = $this->inherit && is_search() && $this->search_settings && $this->search_settings['live_search'] === 'true';
		$this->is_live_filter   = ! $this->inherit;

		// live filter dropdown is enabled
		if ( isset($this->atts['filter']['enable']) && $this->atts['filter']['enable'] ) {
			$this->is_live_filter == true;
		}
		// change items count is enabled
		else if ( isset($this->atts['query']['displayLayout']) && isset($this->atts['query']['displayLayout']['responsive']) ) {
			$responsive_layout = $this->atts['query']['displayLayout']['responsive'];
			if (
				( isset($responsive_layout['lg']) && isset($responsive_layout['lg']['items']) )
				|| ( isset($responsive_layout['md']) && isset($responsive_layout['md']['items']) )
				|| ( isset($responsive_layout['md']) && isset($responsive_layout['md']['items']) )
			) {
				$this->is_live_filter == true;
			}
		}

		// create an inner_block that has no attributes
		$this->inner_block = $block;
		if ( isset($this->inner_block['attrs']) ) {
			$this->inner_block['attrs'] = array();
		}

		// ID
		if ( isset($this->atts['query']['anchor']) && !empty($this->atts['query']['anchor']) ) {
			$this->ID = $this->atts['query']['anchor'];
		} else {
			$this->ID = wp_unique_id( 'post-results-' );
		}

		// save unique IDs for elements that are re-rendered with live-filter
		if (isset($this->atts['pagination']['greydStyles']) && !isset($this->atts['pagination']['greydClass'])) {
			$this->atts['pagination']['greydClass'] = wp_unique_id('query_');
		}
		if (isset($this->atts['arrows']['greydStyles']) && !isset($this->atts['arrows']['greydClass'])) {
			$this->atts['arrows']['greydClass'] = wp_unique_id('query_');
		}
		if (isset($this->atts['sorting']['greydStyles']) && !isset($this->atts['sorting']['greydClass'])) {
			$this->atts['sorting']['greydClass'] = wp_unique_id('query_');
		}
		if (!isset($this->atts['query']['anchor'])) {
			$this->atts['query']['anchor'] = $this->ID;
		}
	}

	/**
	 * =================================================================
	 *                          Render
	 * =================================================================
	 */

	/**
	 * Render the query block.
	 * 
	 * @param array $options    Optional options object.
	 * 
	 * @return string Rendered block output.
	 */
	public function render( $options=array() ) {

		/**
		 * We only wan't to re-render this block if the block content
		 * holds the original query results or is empty
		 */
		if ( '<ul' === substr($this->block_content, 0, 3) || empty($this->block_content) ) {
			$options = wp_parse_args(
				$options,
				array(
					'dynamic' => true
				)
			);
		} else {
			// otherwise we just return the already rendered content.
			return $this->block_content;
		}

		// don't render this block dynamically.
		if ( ! $options['dynamic'] ) {
			return $this->block_content;
		}

		// decide whether we need to rebuild the query
		$rebuild_query = $this->inherit;
		if ( !$this->inherit && !empty($this->block_content) ) {
			// re-build the query only if we had some results in the original query.
			$rebuild_query = true;
		}

		if ( $rebuild_query  ) {

			$query_args  = $this->get_query_args();

			if ( is_search() && $this->inherit ) {
				$query_args[ 'is_livesearch' ] = true;
			}

			$this->query = new \WP_Query( $query_args );

			$this->found_posts = $this->query->found_posts;
		}

		/**
		 * If we haven't found any posts, the only case in which we're
		 * still rendering the wrapper and everything, is when this is
		 * a live search results wrapper.
		 * Because in this case we need the whole div structure to 
		 * render results when the search term is changed.
		 * 
		 * @since 1.3.0 also render when live_filter is enabled
		 */
		if ( $this->found_posts === 0 && !$this->is_live_search && !$this->is_live_filter ) {
			return $this->no_results_message();
		}

		ob_start();

		$this->render_filter_select( $options, 'top' );

		// wrapper
		echo "<div id='$this->ID' ".Blocks_Helper::implode_html_attributes( $this->get_wrapper_atts() ).">";

		$this->render_sorting_dropdown( 'top' );

		$this->render_pagination( 'top' );

		$this->render_arrows( 'left' );

		// pages wrapper
		echo "<div class='query-pages-wrapper' data-ppp='{$this->atts['query']['query']['perPage']}'>";

		// page
		$page_num = 1;
		echo "<div class='query-page is-current' data-page='{$page_num}'>";

		if ( $this->found_posts ) {

			// enable highlighting of the search term in titles & excerpts
			if ( isset($query_args['s']) && !empty($query_args['s']) && strlen($query_args['s']) > 3 ) {
				global $query_search_term;
				$query_search_term = $query_args['s'];
			}

			$i = 0;
			while ($this->query->have_posts()) {
	
				// set the post
				$this->query->the_post();
				$post_id = get_the_ID();
				
				/**
				 * really exclude excluded posts.
				 * wpml does not properly exclude translations of sticky posts.
				 */
				if (
					isset($this->query->query['post__not_in']) && 
					in_array($post_id, $this->query->query['post__not_in'])
				) continue;
	
				// pagebreak
				if ( !$this->inherit && $i == ($this->atts['query']['query']['perPage'] * $page_num)) {
					$page_num++;
					// end page
					echo "</div>";
					// new page
					echo "<div class='query-page' data-page='{$page_num}'>";
				}
	
				// render the tile
				echo sprintf(
					"<article class='query-post %s' %s>",
					esc_attr( implode( ' ', get_post_class('wp-block-post') ) ),
					implode( array(
						"data-post='{$post_id}'",
						"data-title='".preg_replace('/[^a-z0-9\s]/', '', strtolower(get_the_title()))."'",
						"data-date='".get_post()->post_date."'",
						"data-postviews='".intval(get_post_meta($post_id, 'postviews_count', true))."'"
					) )
				);

				global $current_post_index;
				$current_post_index = $i + 1;
	
				// render the block
				$data = array(
					'postType' => get_post_type(),
					'postId'   => $post_id,
				);
				echo (new \WP_Block($this->block, $data))->render(array( 'dynamic' => false ));
	
				echo "</article>";
	
				$i++;
			}

			// reset
			wp_reset_postdata();
			if ( isset($query_search_term) ) {
				$query_search_term = null;
			}
		}
		else if ( $this->is_live_filter ) {
			// render the no result message
			echo $this->no_results_message();

		}
	
		// close page and pages wrapper
		echo "</div></div>";

		$this->render_arrows( 'right' );

		$this->render_sorting_dropdown( 'bottom' );

		$this->render_pagination( 'bottom' );

		// close wrapper
		echo "</div>";

		echo $this->loading_spinner();

		$this->render_live_search_elements();
		
		$this->enqueue_responsive_columns_stylesheet();

		$this->render_filter_select( $options, 'bottom' );

		$content = ob_get_contents();
		ob_end_clean();

		return ltrim( $content );
	}

	/**
	 * Render the live filter multiselect.
	 * 
	 * @param array	 $opts	render options.
	 * @param string $pos (top|bottom)
	 * 
	 * @return string HTML
	 */
	public function render_filter_select( $opts, $pos ) {

		if ( !$this->is_live_filter || isset($opts['update']) ) return;

		$atts = isset($this->atts['filter']) ? $this->atts['filter'] : array();
		$enabled = isset($atts['enable']) ? $atts['enable'] == true : false;
		$position = isset($atts['position']) ? $atts['position'] : 'top';

		// render if enabled position matches
		if ( !$enabled || $position !== $pos ) return;

		// styles
		// debug($atts);
		if ( isset($atts['inputStyle']) && $atts['inputStyle'] === 'sec') $atts['inputStyle'] = 'is-style-sec';
		$greydClass   = isset($atts['greydClass']) ? $atts['greydClass'] : wp_unique_id('filter_');
		$greydStyles  = isset($atts['greydStyles']) ? (array) $atts['greydStyles'] : array();
		$customStyles = isset($atts['custom']) && $atts['custom'] && isset($atts['customStyles']) ? (array) $atts['customStyles'] : array();

		// get filter options
		$options = array();
		$selected = array();
		$taxes = Helper::get_all_taxonomies($this->atts['query']['query']['postType']);
		$showTitles = isset($atts['showTaxTitle']) && $atts['showTaxTitle'] == true ? true : false;

		/**
		 * Filter the taxonomies that are used for the filter.
		 * 
		 * @since 1.3.0
		 * 
		 * @param array $taxes              The taxonomies.
		 * @param Query_Block $query_class  Reference of this class
		 * 
		 * @return array
		 */
		$taxes = apply_filters('greyd_query_live_filter_taxonomies', $taxes, $this);

		// debug($taxes);
		foreach ($taxes as $i => $tax) {
			$terms = Helper::get_all_terms( $tax->slug, array(
				'hide_empty' => true
			) );
			
			// debug($terms);
			foreach ($terms as $j => $term) {
				if ($showTitles) $options[$tax->slug.'|'.$term->id] = $tax->title.': '.$term->title;
				else $options[$tax->slug.'|'.$term->id] = $term->title;
				if (isset($this->atts['query']['query']['taxQuery'][$tax->slug]) && 
					in_array($term->id, $this->atts['query']['query']['taxQuery'][$tax->slug])) {
					array_push($selected, $tax->slug.'|'.$term->id);
				}
			}
		}

		// render multiselect
		// if ($position == 'bottom') echo $this->loading_spinner();
		echo "<div class='pgn filter ".$greydClass."'>";
			echo Helper::render_multiselect(
				$this->ID."-taxselect",
				$options,
				array( 
					'value' => implode(',', $selected),
					'class' => isset($atts['inputStyle']) ? $atts['inputStyle'] : '',
					'placeholder' => isset($atts['empty']) && !empty($atts['empty']) ? $atts['empty'] : __("Select filter", 'greyd_hub')
				)
			);
		echo "</div>";
		// if ($position == 'top') echo $this->loading_spinner();
		
		// enqueue styles
		self::enqueue_css(
			".{$greydClass}.filter.pgn",
			array( 'display' => 'flex', 'justify-content' => isset($atts['align']) ? $atts['align'] : 'end' )
		);
		self::enqueue_css(
			".{$greydClass}.filter.pgn .greyd_multiselect",
			$greydStyles
		);
		if ( !empty($customStyles) ) {
			self::enqueue_css(
				".{$greydClass}.filter.pgn .input, .{$greydClass}.filter.pgn .dropdown",
				$customStyles
			);
		}
	}

	/**
	 * Render the sorting dropdown.
	 * 
	 * @param string $pos (top|bottom)
	 * 
	 * @return string HTML
	 */
	public function render_sorting_dropdown( $pos ) {

		$atts    = isset($this->atts['sorting']) ? $this->atts['sorting'] : array();
		$enabled = isset($atts['enable']) ? $atts['enable'] == true : false;

		if ( !$enabled ) return;

		// render if position matches
		$position = isset($atts['position']) ? $atts['position'] : 'top';
		if ( $position !== $pos ) return;

		// styles
		// debug($atts);
		if ( isset($atts['inputStyle']) && $atts['inputStyle'] === 'sec') $atts['inputStyle'] = 'is-style-sec';
		$greydClass   = isset($atts['greydClass']) ? $atts['greydClass'] : wp_unique_id('sorting_');
		$greydStyles  = isset($atts['greydStyles']) ? (array) $atts['greydStyles'] : array();
		if (isset($greydStyles['width']) && $greydStyles['width'] == "") unset($greydStyles['width']);
		$customStyles = isset($atts['custom']) && $atts['custom'] && isset($atts['customStyles']) ? (array) $atts['customStyles'] : array();
		if (isset($customStyles['background'])) $customStyles['box-shadow'] = 'none';

		// build options
		$selected_option = "";
		$options = array(
			'date_DESC'		=> __("chronological (newest first)", 'greyd_hub'),
			'date_ASC'		=> __("chronological (oldest first)", 'greyd_hub'),
			'title_ASC' 	=> __("alphabetical (ascending)", 'greyd_hub'),
			'title_DESC' 	=> __("alphabetical (descending)", 'greyd_hub')
		);
		if ( $this->search_settings && $this->search_settings['postviews_counter'] === 'true' ) {
			$options['views_DESC'] = __("most read", 'greyd_hub');
		}
		if ( $this->search_settings && $this->search_settings['relevance'] === 'true' && is_search() ) {
			$options = array_merge(
				array( 'relevance_DESC' => __("Relevance", 'greyd_hub') ),
				$options
			);
		}

		// get current query atts
		$query_vars = $this->atts['query']['query'];
		$order   = isset($query_vars['order']) ? strtoupper($query_vars['order']) : 'DESC';
		$orderby = isset($query_vars['orderby']) ? strtolower($query_vars['orderby']) : ($this->search_settings && $this->search_settings['relevance'] === 'true' ? "relevance" : 'date');
		
		// selected option
		$selected_option = $orderby."_".$order;

		// adjust options when query is inherited
		if ( $this->inherit ) {

			global $wp;
			$query_vars = $wp->query_vars;
		
			if ( isset($query_vars['paged']) ) unset( $query_vars['paged'] );

			// get current query atts
			$order   = isset($query_vars['order']) ? strtoupper($query_vars['order']) : 'DESC';
			$orderby = isset($query_vars['orderby']) ? strtolower($query_vars['orderby']) : ($this->search_settings && $this->search_settings['relevance'] === 'true' ? "relevance" : 'date');
			
			// selected option inherits the query
			$selected_option = $orderby."_".$order;

			// in archive & search templates we redirect when selecting a sorting option
			if ( ( is_archive() || is_search() ) && !$this->is_live_search ) {
			
				$url = add_query_arg( $query_vars, preg_replace( "/page\/\d+/", "", home_url($wp->request) ) );
				$url = remove_query_arg(array('meta_query', 'orderby', 'order'), $url);

				foreach ($options as $value => $label) {
					if ( empty($value) ) continue;

					list($orderby, $order) = !empty($value) ? explode('_', $value) : array("","");

					$option_url = add_query_arg(array('orderby' => $orderby, 'order'   => $order), $url);

					if ( $value === "views_DESC" ) {
						$option_url = esc_url(add_query_arg(array(
							"orderby"  => 'meta_value_num',
							"order"    => 'DESC',
							'meta_query' => array(
								'relation' => 'OR',
								array(
									'key' => 'postviews_count', 
									'compare' => 'EXISTS',
								),
								array(
									'key' => 'postviews_count', 
									'compare' => 'NOT EXISTS',
								)
							),
						), $url));
					}

					// add the url to the option
					$options[ $value ] = array(
						'label' => $label,
						'value' => $option_url
					);
				}
			}
		}

		// wrapper
		echo sprintf(
			'<div %s >',
			Blocks_Helper::implode_html_attributes( array(
				'class' => array( 'pgn sorting', $greydClass, $position ),
				'style' => 'justify-content:' . ( isset($atts['align']) ? $atts['align'] : 'end' )
			) )
		);

		// select
		echo '<div class="custom-select '.(isset($atts['inputStyle']) ? $atts['inputStyle'] : '').'"><select autocomplete="off">';

		// options
		foreach( $options as $key => $option ) {

			if ( is_array($option) ) {
				$label = $option[ 'label' ];
				$value = $option[ 'value' ];
			} else {
				$label = $option;
				$value = str_replace('_', ' ', $key);
			}

			// use user input value
			if ( isset($atts['options'][$key]) && !empty($atts['options'][$key]) ) {
				$label = esc_attr( $atts['options'][$key] );
			}

			echo sprintf(
				'<option value="%s" %s>%s</option>',
				$value,
				$selected_option == $key ? 'selected="selected"' : '',
				$label
			);
		}

		echo '</select></div>';

		echo '</div>';
		
		// enqueue styles
		self::enqueue_css(
			".{$greydClass}.sorting.pgn > .custom-select",
			array_merge( array( 'width' => 'auto', 'margin' => array( 'bottom' => 'var(--FRMmargin)' ) ), $greydStyles )
		);
		self::enqueue_css(
			".{$greydClass}.sorting.pgn > .custom-select > div",
			$customStyles
		);
	}

	/**
	 * Render pagination arrows on the side.
	 * 
	 * @param string $pos (left|right)
	 * 
	 * @return string HTML
	 */
	public function render_arrows( $pos ) {

		if ( $this->is_live_search ) return;

		$atts    = isset($this->atts['arrows']) ? $this->atts['arrows'] : array();
		$enabled = isset($atts['enable']) ? $atts['enable'] == true : false;

		if ( !$enabled ) return;

		// only render when multiple pages
		if ( $this->get_query_tags()['page-count'] < 2 ) return;

		// styles
		$greydClass   = isset($atts['greydClass']) ? $atts['greydClass'] : wp_unique_id('sorting_');
		$greydStyles  = isset($atts['greydStyles']) ? (array) $atts['greydStyles'] : array();

		// html atts
		$wrapper_atts = array(
			'class' => array($greydClass, 'pgn_arrows pgn arrows animate_fast', $pos)
		);

		// setup overlap
		if ( isset($atts['overlap']) && $atts['overlap'] ) {
			$wrapper_atts['class'][] = "overlap";
		}

		echo "<div ".Blocks_Helper::implode_html_attributes( $wrapper_atts ).">";
		
		$this->render_pagination_arrow( $pos, $atts );
		
		echo "</div>";

		/**
		 * Fix color inhertiation of custom pagination arrow colors.
		 * 
		 * @since 1.3.3
		 */
		if ( isset($greydStyles['color']) || isset($greydStyles['backgroundColor']) || isset($greydStyles['hover']) ) {

			$link_styles = array();

			if ( isset($greydStyles['color']) ) {
				$link_styles['color'] = $greydStyles['color'];
				unset( $greydStyles['color'] );
			}
			if ( isset($greydStyles['backgroundColor']) ) {
				$link_styles['backgroundColor'] = $greydStyles['backgroundColor'];
				unset( $greydStyles['backgroundColor'] );
			}
			if ( isset($greydStyles['hover']) ) {
				$link_styles['hover'] = $greydStyles['hover'];
				unset( $greydStyles['hover'] );
			}

			self::enqueue_css(
				'.pgn.arrows.'.$greydClass.' > a',
				$link_styles
			);
		}

		self::enqueue_css(
			'.pgn.arrows.'.$greydClass,
			$greydStyles
		);
	}

	/**
	 * Render the pagination bar.
	 * 
	 * @param string $pos (top|bottom)
	 * 
	 * @return string HTML
	 */
	public function render_pagination( $pos ) {

		if ( $this->is_live_search ) return;

		$atts    = isset($this->atts['pagination']) ? $this->atts['pagination'] : array();
		$enabled = isset($atts['enable']) ? $atts['enable'] == true : true;

		if ( !$enabled ) return;

		// render if position matches
		$position = isset($atts['position']) ? $atts['position'] : 'bottom';
		if ( $position !== $pos ) return;

		// only render when multiple pages
		if ( $this->get_query_tags()['page-count'] < 2 ) return;

		// styles
		$greydClass   = isset($atts['greydClass']) ? $atts['greydClass'] : wp_unique_id('sorting_');
		$greydStyles  = isset($atts['greydStyles']) ? (array) $atts['greydStyles'] : array();

		$wrapper_atts = array(
			'class' => array($greydClass, 'pagination pgn numbers', $position)
		);

		// overlap
		if ( isset($atts['overlap']) && $atts['overlap'] ) {
			$wrapper_atts['class'][] = "overlap";
		}


		echo "<div ".Blocks_Helper::implode_html_attributes( $wrapper_atts ).">";

		$this->render_pagination_arrow( 'left', $atts );

		$this->render_pagination_numbers( $atts );

		$this->render_pagination_arrow( 'right', $atts );
		
		echo "</div>";

		// enqueue styles
		if ( !empty($greydStyles) ) {
			$filter_keys = array(
				'color' => '',
				'opacity' => '',
				'hover' => '',
				'active' => '',
			);
			// wrapper styles
			self::enqueue_css(
				'.pgn.numbers.'.$greydClass,
				array_merge(
					array_diff_key(
						$greydStyles,
						$filter_keys // except these keys
					),
					array(
						'--pgn-numbers-gutter' => isset($greydStyles['gutter']) ? $greydStyles['gutter'] : '',
					)
				)
			);
			// link styles
			self::enqueue_css(
				'.pgn.numbers.'.$greydClass.' .pgn_number',
				array_intersect_key(
					$greydStyles,
					$filter_keys // only those keys
				),
				array( 'pseudo_active' => '.pgn_current' )
			);
		}
	}

	/**
	 * Render pagination numbers inside the bar.
	 * 
	 * @param array $atts        $block['attrs']['pagination']
	 */
	public function render_pagination_numbers( $atts ) {

		$pgn_type = isset($atts['type']) ? esc_attr( $atts['type'] ) : 'icon';

		if ( empty($pgn_type) ) return;

		$numbers_atts = array();

		// get icons
		if ($pgn_type == 'icon') {
			$icon_normal = isset($atts['icon_normal']) ? $atts['icon_normal'] : 'icon_circle-empty';
			$icon_active = isset($atts['icon_active']) ? $atts['icon_active'] : 'icon_circle-slelected';
			$numbers_atts = array(
				'data' => array(
					'iconnormal' => $icon_normal,
					'iconactive' => $icon_active,
				)
			);
		}
		// get images
		else if ($pgn_type == 'image') {
			$src_normal = isset($atts['img_normal']) ? wp_get_attachment_url($atts['img_normal']) : false;
			$src_active = isset($atts['img_active']) ? wp_get_attachment_url($atts['img_active']) : false;
			$numbers_atts = array(
				'data' => array(
					'imgnormal' => $src_normal,
					'imgactive' => $src_active,
				)
			);
		}

		$numbers = array();
		$queryTags = $this->get_query_tags();
		$page_keys = $this->inherit ? $queryTags['page-keys'] : array();


		if ( $this->inherit ) {
			foreach( $queryTags['page-keys'] as $i => $page ) {
	
				$text       = strval($i);
				$num_atts   = array();
				$icon_class = 'pgn_number'.( $queryTags['page-num'] == $i ? ' pgn_current' : '' );
	
				if ( $pgn_type === 'text' ) {
					if ( isset($atts['text_type']) ) {
						$text = self::make_pagination_number_style($i, $atts['text_type']);
					}
				}
				else if ( $pgn_type == 'icon' ) {
					if (!isset($atts['icon_type']) || $atts['icon_type'] == 'icon') {
						if ( $queryTags['page-num'] == $i ) $icon_class .= ' '.$icon_active;
						else $icon_class .= ' '.$icon_normal;
						$text = "";
					}
					else if ($atts['icon_type'] == 'dots') {
						$text = "●";
						$numbers_atts = array();
					}
					else if ($atts['icon_type'] == 'blocks') {
						$text = "■";
						$numbers_atts = array();
					}
				}
				else if ( $pgn_type == 'image' ) {
					if ( $src_normal ) $text = "<img aria-hidden='true' src='{$src_normal}'>";
					if ( $queryTags['page-num'] == $i && $src_active ) $text = "<img aria-hidden='true' src='{$src_active}'>";
				}
	
				// add href if set
				if ( !empty($page) ) {
					$num_atts['href'] = $page;
					$num_atts['role'] = 'link';
				}
				// empty element means that's the current page or the '…' filler.
				else {
					$num_atts['aria-hidden'] = 'true';
					$num_atts['style']       = 'pointer-events:none;';
					
					if ( !empty($i) && !is_numeric($i) ) {
						$icon_class = '';
						$text = $i;
					}
				}
				
				$numbers[] = "<a ".Blocks_Helper::implode_html_attributes( $num_atts )."><span class='{$icon_class}'>{$text}</span></a>";
			}
		}
		else {
			for ( $i = 1; $i <= $queryTags['page-count']; $i++ ) {
	
				$text     = strval($i);
				$num_atts = array(
					'tabindex'      => '0',
					'role'          => 'button',
					'href'          => 'javascript:void(0)',
					'data-pagelink' => $text,
					'aria-label'    => sprintf(__("go to page %s", "greyd_hub"), $text ),
				);
				$icon_class = 'pgn_number'.( $queryTags['page-num'] == $i ? ' pgn_current' : '' );
	
				if ( $pgn_type === 'text' ) {
					if ( isset($atts['text_type']) ) {
						$text = self::make_pagination_number_style($i, $atts['text_type']);
					}
				}
				else if ( $pgn_type == 'icon' ) {
					if (!isset($atts['icon_type']) || $atts['icon_type'] == 'icon') {
						if ( $queryTags['page-num'] == $i ) $icon_class .= ' '.$icon_active;
						else $icon_class .= ' '.$icon_normal;
						$text = "";
					}
					else if ($atts['icon_type'] == 'dots') {
						$text = "●";
						$numbers_atts = array();
					}
					else if ($atts['icon_type'] == 'blocks') {
						$text = "■";
						$numbers_atts = array();
					}
				}
				else if ( $pgn_type == 'image' ) {
					if ( $src_normal ) $text = "<img aria-hidden='true' src='{$src_normal}'>";
					if ( $queryTags['page-num'] == $i && $src_active ) $text = "<img aria-hidden='true' src='{$src_active}'>";
				}
				
				$numbers[] = "<a ".Blocks_Helper::implode_html_attributes( $num_atts )."><span class='{$icon_class}'>{$text}</span></a>";
			}
		}

		$numbers = apply_filters( 'greyd_pagination_numbers', $numbers, $this->query, $this->block );

		if ( count( $numbers ) ) {
			echo "<span class='pgn_numbers' ".Blocks_Helper::implode_html_attributes( $numbers_atts ).">".implode('', $numbers)."</span>";
		}
	}
	
	/**
	 * Render a pagination number.
	 * @param int $number
	 * @param string $numbes_style	('A' 'a' '1.' '01' '01.')
	 * @return string
	 */
	public static function make_pagination_number_style($number, $numbers_style) {
		if ($numbers_style === 'A' || $numbers_style === 'a') {
			$result = '';
			for ($j = 1; $number >= 0 && $j < 10; $j++) {
				$result = chr(0x40 + ($number % pow(26, $j) / pow(26, $j - 1))) . $result;
				$number -= pow(26, $j);
			}
			if ($numbers_style == 'a') $result = strtolower($result);
		}
		else {
			$result = $number;
			if (($numbers_style === '01' || $numbers_style === '01.') && $result < 10) $result = '0'.$result;
			if ($numbers_style === '1.' || $numbers_style === '01.') $result .= '.';
		}
		return $result;
	}

	/**
	 * Render a single pagination arrow.
	 * 
	 * @param string $direction  (previous|next)
	 * @param array $atts        $block['attrs']['pagination']
	 */
	public function render_pagination_arrow( $pos, $atts ) {

		$arrow_type = 'icon';

		// classic pagination arrows use the attribute 'arrows_type'
		if ( isset($atts['arrows_type']) ) {
			$arrow_type = esc_attr( $atts['arrows_type'] );
		}
		// side arrows always have 'enable' set & use the attribute 'type'
		else if ( isset($atts['enable']) && isset($atts['type']) ) {
			$arrow_type = esc_attr( $atts['type'] );
		}

		if ( empty($arrow_type) ) return;

		$direction  = $pos === 'right' ? 'next' : 'previous';
		$queryTags  = $this->get_query_tags();
		$inner_html = "";
		$arrow_atts = array(
			'tabindex'      => '0',
			'role'          => 'button',
			'href'          => 'javascript:void(0)',
			'class'         => array( 'pgn_number pgn_'.$direction ),
			'data-pagelink' => substr($direction, 0, 4),
			'aria-label'    => $direction === 'next' ? __("go to next page", "greyd_hub") : __("go to previous page", "greyd_hub")
		);

		// add href tag if in query
		if ( $this->inherit ) {
			$href = isset($queryTags[ 'page-'.$direction ]) ? $queryTags[ 'page-'.$direction ] : null;
			if ( !empty($href) ) {
				$arrow_atts['href'] = $href;
				$arrow_atts['role'] = 'link';
			}
		}

		// render as images
		if ( $arrow_type == 'image' ) {
			$src = isset($atts[ 'img_'.$direction ]) ? wp_get_attachment_url($atts[ 'img_'.$direction ]) : false;
			$inner_html = $src ? "<img aria-hidden='true' src='{$src}'>" : "";
		}
		// render as icons
		else {
			$arrow_icon_class = isset($atts[ 'icon_'.$direction ]) ? $atts[ 'icon_'.$direction ] : 'arrow_'.$pos;
			$arrow_atts['class'][] = $arrow_icon_class;
		}

		// hide if on current page
		if (
			( $pos === 'left' && $queryTags['page-num'] < 2 ) ||
			( $pos === 'right' && $queryTags['page-num'] === $queryTags['page-count'] ) ||
			( isset($href) && empty($href) ) // hide the arrow when the 'next|prev' link is empty
		) {
			$arrow_atts['class'][] = 'pgn_current';

			// hide completely if on archive page
			if ( $this->inherit ) {
				$arrow_atts = array();
			}
		}
		
		echo "<a ".Blocks_Helper::implode_html_attributes($arrow_atts).">{$inner_html}</a>";
	}

	/**
	 * Render the loader, spinner & load more button.
	 * 
	 * @return string HTML
	 */
	public function render_live_search_elements() {

		if ( ! $this->is_live_search ) return;

		// render the no result message
		echo $this->no_results_message();

		$loader_atts    = isset($this->atts['loader']) ? $this->atts['loader'] : null;
		$wrapper_class  = "load_more_wrapper";
		$button_class   = "button";

		if ( $loader_atts ) {
			if ( isset($loader_atts['style']) && !empty($loader_atts['style']) ) {
				$button_class = $loader_atts['style'];
			}
			if ( isset($loader_atts['size']) && !empty($loader_atts['size']) ) {
				$button_class .= " " . $loader_atts['size'];
			}
			if ( isset($loader_atts['greydClass']) && !empty($loader_atts['greydClass']) ) {
				$wrapper_class .= " " . $loader_atts['greydClass'];

				if ( isset($loader_atts['greydStyles']) && !empty($loader_atts['greydStyles']) ) {
					self::enqueue_css( ".load_more_wrapper.".$loader_atts['greydClass'], $loader_atts['greydStyles'] );
				}
			}
		}

		echo "<div class='$wrapper_class' ".( $this->found_posts > $this->get_query_tags()['posts-per-page'] ? "" : "style='display:none'" ).">
			<div class='load_more $button_class'>".__("load more", "greyd_hub")."</div>
			{$this->loading_spinner()}
		</div>";
	}

	/**
	 * Make the columns responsive and adding the stylesheet.
	 */
	public function enqueue_responsive_columns_stylesheet() {
		
		if (
			! isset($this->atts['query']['displayLayout']) ||
			! isset($this->atts['query']['displayLayout']['type']) ||
			$this->atts['query']['displayLayout']['type'] != 'flex'
		) {
			return;
		}

		$layout = $this->atts['query']['displayLayout'];

		// vars
		$style_lg = $style_md = $style_sm = "";
		$style = "grid-template-columns: repeat({$layout['columns']}, minmax(0, 1fr));";

		// responsive
		if ( isset($layout['responsive']) ) {
			// lg
			if (isset($layout['responsive']['lg']['columns'])) {
				$style_lg = "grid-template-columns: repeat({$layout['responsive']['lg']['columns']}, minmax(0, 1fr));";
			}
			// md
			if (isset($layout['responsive']['md']['columns'])) {
				$style_md = "grid-template-columns: repeat({$layout['responsive']['md']['columns']}, minmax(0, 1fr));";
			}
			// sm
			if (isset($layout['responsive']['sm']['columns'])) {
				$style_sm = "grid-template-columns: repeat({$layout['responsive']['sm']['columns']}, minmax(0, 1fr));";
			}
		}
		
		// build css
		$selector = "#".$this->ID.".greyd-posts-slider .query-page";
		$css = "$selector { ".$style." } ";
		if ($style_lg != "") $css .= "@media (max-width: 1200px) { $selector { ".$style_lg." } } ";
		if ($style_md != "") $css .= "@media (max-width: 992px) { $selector { ".$style_md." } } ";
		if ($style_sm != "") $css .= "@media (max-width: 576px) { $selector { ".$style_sm." } } ";

		// enqueue
		Helper::add_custom_style( $css );
	}

	/**
	 * =================================================================
	 *                          Utils
	 * =================================================================
	 */

	/**
	 * Compose & enqueue styles.
	 * 
	 * @param string $selector          CSS class or ID, usually a greydClass (including prefix).
	 * @param array $styles             All default, hover & responsive styles.
	 * @param array $atts               Additional attributes (deprecated: @param bool $important)
	 */
	public static function enqueue_css($selector, $styles, $atts=array()) {
		$finalCSS = Blocks_Helper::compose_css( array( '' => $styles ), $selector, $atts );
		Helper::add_custom_style($finalCSS);
	}

	/**
	 * Get query arguments
	 * 
	 * @return array
	 */
	public function get_query_args() {

		$paged = 1;
		if ( $this->inherit ) {
			global $wp_query;

			$page_key = isset($this->atts['query']['queryId']) ? 'query-'.$this->atts['query']['queryId'].'-page' : 'query-page';
			$paged    = isset($_GET[$page_key]) && !empty($_GET[$page_key]) ? intval($_GET[$page_key]) : null;
			if ( !$paged ) {
				$paged = $wp_query->paged;
			}
		}

		// build new query
		if ( class_exists( '\Greyd\Dynamic\Dynamic_Helper' ) ) {
			$query_args = \Greyd\Dynamic\Dynamic_Helper::build_query( $this->atts['query'], $paged );
		}
		else {
			$query_args = array(
				'post_type'    => 'post',
				'order'        => 'DESC',
				'orderby'      => 'date',
				'post__not_in' => array(),
				'post_status'       => 'publish',
				'suppress_filters'  => false,
			);
		}

		// fix offset (-1 ignores the offset param)
		if ( !$this->inherit ) {
			$query_args['posts_per_page'] = wp_count_posts($query_args['post_type'])->publish;
			if (isset($this->atts['query']['query']) && $this->atts['query']['query']['pages'] != 0 && isset($this->atts['query']['query']['perPage'])) {
				$query_args['posts_per_page'] = intval($this->atts['query']['query']['perPage']) * intval($this->atts['query']['query']['pages']);
			}
		}
		
		// inherit query args
		if ( $this->inherit ) {
			
			if ( $wp_query && isset($wp_query->query_vars) && is_array($wp_query->query_vars) ) {
				
				// Unset `offset` because if is set, $wp_query overrides/ignores the paged parameter and breaks pagination.
				unset($query_args['offset']);
				$query_args = wp_parse_args($wp_query->query_vars, $query_args);
				// $query_args['paged'] = $paged;
	
				if (empty($query_args['post_type']) && is_singular()) {
					$query_args['post_type'] = get_post_type( get_the_ID() );
				}
				// debug($query_args);
			}
		}

		if ( empty($query_args['post_type']) ) {
			$query_args['post_type'] = 'any';
		}

		/**
		 * Filter query arguments.
		 * 
		 * @filter greyd_query_args
		 * 
		 * @param array       $query_args   Current query arguments
		 * @param Query_Block $query_class  Instance of this class.
		 * 
		 * @return array $query_args
		 */
		return apply_filters( 'greyd_query_args', $query_args, $this );
	}

	/**
	 * Get queryTags from $this->atts and fix if necessary.
	 * 
	 * @var array $queryTags
	 *      @var string query          The search term.
	 *      ...
	 *      @var int post-count        Number of total posts found.
	 *      @var int posts-per-page    Number of posts per page.
	 *      @var int page-count        Total number of pages.
	 *      @var int page-num          Current page number.
	 *      @var string page-next      URL to the next page. Empty if last page.
	 *      @var string page-previous  URL to the previous page. Empty if first page.
	 *      @var array page-keys       URLs to the different pages, keyed by page-num.
	 */
	public function get_query_tags() {

		// use cache
		if ( isset($this->atts['queryTags']['cached']) ) {
			return $this->atts['queryTags'];
		}

		// queryTags from block attributes
		if ( ! $this->inherit ) {
			
			// /**
			//  * Fix page & post count.
			//  * 
			//  * @since 1.7.6
			//  */
			// if ( $this->found_posts ) {
			// 	$this->atts['queryTags']['post-count'] = $this->found_posts;
			// 	$this->atts['query']['query']['pages'] = ceil( $this->atts['queryTags']['post-count'] / $this->atts['queryTags']['posts-per-page'] );
			// }

			/**
			 * Fix offset.
			 * 
			 * @since 1.3.3
			 */
			if ( isset($this->atts['query']['query']['offset']) && $this->atts['query']['query']['offset'] > 0 ) {

				$post_count = intval(strip_tags($this->atts['queryTags']['post-count']));
				$offset     = intval(strip_tags($this->atts['query']['query']['offset']));
				$per_page   = intval(strip_tags($this->atts['queryTags']['posts-per-page']));

				$this->atts['queryTags']['post-count'] = max( 0, $post_count - $offset );
				$this->atts['queryTags']['page-count'] = ceil( $this->atts['queryTags']['post-count'] / $per_page );
			}

			/**
			 * Fix page-count inside queryTags.
			 * 
			 * @since 1.3.1
			 */
			if ( isset($this->atts['query']['query']['pages']) && $this->atts['query']['query']['pages'] ) {
				$this->atts['queryTags']['page-count'] = min( $this->atts['queryTags']['page-count'], $this->atts['query']['query']['pages'] );
			}
		}

		// queryTags from global wp_query
		if ( $this->inherit ) {

			$overwrites = array();
	
			// inherit overwrites from global query
			global $wp_query;
			if ( $wp_query->paged ) {
				$overwrites['page-num'] = $wp_query->paged;
			}
			if ( $wp_query->posts_per_page ) {
				$overwrites['posts-per-page'] = $wp_query->posts_per_page;
			}
			if ( $wp_query->max_num_pages ) {
				$overwrites['page-count'] = $wp_query->max_num_pages;
			}
	
			// set query tags with overwrites
			$queryTags  = wp_parse_args( $overwrites, $this->atts['queryTags'] );
	
			$page_num   = $queryTags['page-num'];
			$page_count = $queryTags['page-count'];
	
			// get page keys (array of urls to the pagination pages)
			$page_keys  = isset($queryTags['page-keys']) ? $queryTags['page-keys'] : array();
			$page_href  = isset($page_keys[$page_num]) ? $page_keys[$page_num] : '';
	
			// If the link to the current page is not empty,
			// we need to fix the page links...
			if ( ! empty( $page_href ) ) {
	
				// override corrupt page links
				$queryTags['page-next']     = '';
				$queryTags['page-previous'] = '';
				$queryTags['page-keys']     = array_fill( 1, $page_count, '' );
	
				// re-generate page links
				$paginate_links = paginate_links( array(
					'type'      => 'array',
					'current'   => $page_num,
					'total'     => $page_count,
					'prev_text' => '{{previous}}',
					'next_text' => '{{next}}',
					'before_page_number' => '{{',
					'after_page_number'  => '}}'
				) );
				// debug( array_map('esc_attr', $paginate_links) );
	
				if ( is_array($paginate_links) ) {
					foreach( $paginate_links as $i => $link ) {
		
						// get href
						$href = Helper::get_string_between( $link, 'href="', '"' );
						if ( empty($href) ) continue;
						
						// get the page key
						$page_key = Helper::get_string_between( $link, '{{', '}}' );
		
						if ( is_numeric( $page_key ) ) {
							// this is a page number
							$page_key = intval( $page_key );
							$queryTags['page-keys'][ $page_key ] = $href;
						} else {
							// this is a prev/next page link
							$queryTags[ 'page-'.$page_key ] = $href;
						}
					}
				}
			}
			
			// set cache
			$this->atts['queryTags'] = $queryTags;
		}

		// enable cache
		$this->atts['queryTags']['cached'] = true;

		/**
		 * Filter query tags.
		 * 
		 * @filter greyd_query_tags
		 * 
		 * @param array       $query_tags   Current query tags.
		 *      @var string query           The search term.
		 *      ...
		 *      @var int post-count         Number of total posts found.
		 *      @var int posts-per-page     Number of posts per page.
		 *      @var int page-count         Total number of pages.
		 *      @var int page-num           Current page number.
		 *      @var string page-next       URL to the next page. Empty if last page.
		 *      @var string page-previous   URL to the previous page. Empty if first page.
		 *      @var array page-keys        URLs to the different pages, keyed by page-num.
		 * @param Query_Block $query_class  Instance of this class.
		 * 
		 * @return array $query_args
		 */
		return apply_filters( 'greyd_query_tags', $this->atts['queryTags'], $this );
	}

	/**
	 * Get the no results message.
	 * 
	 * @return string
	 */
	public function no_results_message() {

		$message = "";

		if ( is_archive() ) {
			$message = __("Unfortunately, no posts could be found.", "greyd_hub");
		}
		else if ( is_search() ) {
			$message = sprintf(
				__("Sorry, no search results could be found for \"%s\". Please try a different term.", "greyd_hub"),
				"<strong class='query--search-query'>" . get_search_query() . "</strong>"
			);
		}

		/**
		 * Filter no results message.
		 * 
		 * @filter greyd_query_no_results_message
		 * 
		 * @param string      $message      Current message.
		 * @param Query_Block $query_class  Instance of this class.
		 * 
		 * @return array $query_args
		 */
		$message = apply_filters( 'greyd_query_no_results_message', $message, $this );

		if ( empty($message) ) return "";
		
		/**
		 * Filter empty message html.
		 * 
		 * @filter greyd_query_no_results_message_html
		 * 
		 * @param string      $message      Current message.
		 * @param Query_Block $query_class  Instance of this class.
		 * 
		 * @return array $query_args
		 */
		return apply_filters( 'greyd_query_no_results_message_html', sprintf(
			"<p class='message info no_result' %s>%s</p>",
			$this->found_posts === 0 ? "" : "style='display:none'",
			$message
		), $this );
	}

	/**
	 * Get the loading spinner HTML.
	 */
	public function loading_spinner() {
		return "<div class='loading_spinner_wrapper' style='display:none;'>
			<div class='loading_spinner'>
				<div></div> <div></div> <div></div> <div></div>
			</div>
		</div>";
	}

	/**
	 * Get all the wrapper arguments.
	 * 
	 * @return array
	 */
	public function get_wrapper_atts() {
		
		$this->prepare_slider_wrapper_atts();
		$this->prepare_live_search_wrapper_atts();
		$this->prepare_live_filter_wrapper_atts();

		/**
		 * Filter query wrapper HTML attributes.
		 * 
		 * @filter greyd_query_wrapper_atts
		 * 
		 * @param array       $wrapper_atts Current wrapper HTML attributes.
		 * @param Query_Block $query_class  Instance of this class.
		 * 
		 * @return array $query_args
		 */
		return apply_filters( 'greyd_query_wrapper_atts', $this->wrapper_atts, $this );
	}

	/**
	 * Add the wrapper atts to make this a JS slider.
	 */
	public function prepare_slider_wrapper_atts() {

		if ( $this->inherit || $this->is_live_search ) return;

		
		$this->wrapper_atts['class'][] = "js";

		// get the attributes
		$anim_atts = null;
		if ( isset($this->atts['animation']) ) {
			$anim_atts = $this->atts['animation'];
		}
		else if ( isset($this->atts['query']['animation']) ) {
			$anim_atts = $this->atts['query']['animation'];
		}
		
		// static wrapper
		if ( empty( $anim_atts ) || !is_array( $anim_atts ) ) {
			$this->wrapper_atts['data']['height'] = 'max';
			return;
		}

		// animation type
		if ( isset($anim_atts['anim']) ) {
			$this->wrapper_atts['data']['animation'] = esc_attr( $anim_atts['anim'] );
		}

		// autoplay
		if ( isset($anim_atts['autoplay']) && $anim_atts['autoplay'] == true ) {
			$this->wrapper_atts['class'][] = "autoplay";
			$interval = isset($anim_atts['interval']) ? esc_attr($anim_atts['interval']) : "5";
			$this->wrapper_atts['data']['interval'] = $interval;
		}

		// loop
		if ( isset($anim_atts['loop']) && $anim_atts['loop'] == true ) {
			$this->wrapper_atts['class'][] = "loop";
		}

		// scroll to top
		if ( isset($anim_atts['scroll_top']) && $anim_atts['scroll_top'] == true ) {
			$this->wrapper_atts['class'][] = "slider_scroll_top";
		}

		// height
		$height = isset($anim_atts['height']) ? esc_attr($anim_atts['height']) : "max";
		if ( $height == 'custom' ) {
			$height = isset($anim_atts['height_custom']) ? esc_attr($anim_atts['height_custom']) : "500px";
			$this->wrapper_atts['style'][] = "height: ".$height.";";
		}
		$this->wrapper_atts['data']['height'] = $height;
	}

	/**
	 * Add the wrapper atts to make this a live search wrapper.
	 */
	public function prepare_live_search_wrapper_atts() {

		if ( ! $this->is_live_search ) return;

		$this->wrapper_atts['live-search'] = 'true';

		$this->wrapper_atts['data']['block-data'] = htmlentities(
			json_encode($this->inner_block),
			JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE
		);
	}

	/**
	 * Add the wrapper atts to make this a live filter wrapper.
	 */
	public function prepare_live_filter_wrapper_atts() {

		if ( ! $this->is_live_filter ) return;

		// enqueue the script
		if ( ! wp_style_is( 'greyd-query-livefilter-script', 'enqueued') ) {
			wp_enqueue_script( 'greyd-query-livefilter-script' );
		}

		$this->wrapper_atts['live-query'] = 'true';

		// save global post id for advanced filter
		if ( isset($this->atts['query']['advancedFilter'])) {
			global $post;
			$this->wrapper_atts['data']['post-id'] = $post->ID;
		}
		// save wp_query for conditional content
		global $wp_query;
		if ( isset($wp_query->query) ) {
			$this->wrapper_atts['data']['wp-query'] = htmlentities(json_encode($wp_query->query));
		}

		// apply atts to block
		$this->block['attrs'] = $this->atts;
		// save block data
		$this->wrapper_atts['data']['block-data'] = htmlentities(
			json_encode($this->block),
			JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE
		);
		
		// set responsive items per page
		if ( isset($this->atts['query']['displayLayout']) && isset($this->atts['query']['displayLayout']['responsive']) ) {
			$displayLayout = $this->atts['query']['displayLayout'];
			$responsive_layout = $displayLayout['responsive'];
			$perPage = array();

			foreach ( array( 'sm', 'md', 'lg' ) as $bp ) {
				if ( isset($responsive_layout[$bp]['items']) ) {
					$perPage[$bp] = $responsive_layout[$bp]['items'];
				}
			}
			if ( !empty($perPage) ) {
				// set xl (default)
				if ( isset($displayLayout['items']) ) {
					$perPage['xl'] = $displayLayout['items'];
				}
			}

			// save perPage data
			$this->wrapper_atts['data']['perPage'] = htmlentities( json_encode( array(
				'current' => $this->atts['query']['query']['perPage'],
				'items' => $perPage,
				'breakpoints' => array(
					// dynamic grid
					'sm' => get_theme_mod( 'grid_sm', 576 ), 
					'md' => get_theme_mod( 'grid_md', 992 ), 
					'lg' => get_theme_mod( 'grid_lg', 1200 ), 
					'xl' => get_theme_mod( 'grid_xl', 1621 ),
				)
			) ) );
		}

	}

	

}