feat: implement WooCommerce AJAX product filtering
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Filters — reusable sidebar (desktop) + drawer (mobile) + no-JS fallback.
|
||||
*
|
||||
* Expects context via $args or current queried object (shop/category).
|
||||
* Uses same canonical query params as REST /xshop/v1/products and URL sync.
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!xshop_has_woocommerce()) { return; }
|
||||
|
||||
// Current state from URL (canonical)
|
||||
$currentCategory = isset($_GET['category']) ? sanitize_text_field(wp_unslash($_GET['category'])) : '';
|
||||
if (is_product_category() && $currentCategory === '') {
|
||||
$term = get_queried_object();
|
||||
if ($term instanceof WP_Term) { $currentCategory = $term->slug; }
|
||||
}
|
||||
$currentMinPrice = isset($_GET['min_price']) ? sanitize_text_field(wp_unslash($_GET['min_price'])) : '';
|
||||
$currentMaxPrice = isset($_GET['max_price']) ? sanitize_text_field(wp_unslash($_GET['max_price'])) : '';
|
||||
$currentStock = isset($_GET['stock']) ? sanitize_key(wp_unslash($_GET['stock'])) : 'all';
|
||||
$currentOnSale = isset($_GET['on_sale']) ? '1' : (isset($_GET['sale']) ? '1' : '0');
|
||||
if (isset($_GET['on_sale'])) { $currentOnSale = $_GET['on_sale'] === '1' ? '1' : '0'; }
|
||||
if (isset($_GET['sale']) && $_GET['sale'] === '1') { $currentOnSale = '1'; }
|
||||
$currentRating = isset($_GET['rating']) ? absint($_GET['rating']) : 0;
|
||||
$currentOrderby = isset($_GET['orderby']) ? sanitize_key(wp_unslash($_GET['orderby'])) : 'default';
|
||||
|
||||
// Categories
|
||||
$categories = get_terms(['taxonomy'=>'product_cat','hide_empty'=>true,'number'=>20]);
|
||||
if (is_wp_error($categories)) { $categories = []; }
|
||||
|
||||
// Attributes — discover public Woo attribute taxonomies pa_*
|
||||
$attributeTaxonomies = [];
|
||||
if (function_exists('wc_get_attribute_taxonomies')) {
|
||||
$taxes = wc_get_attribute_taxonomies();
|
||||
foreach ($taxes as $tax) {
|
||||
$name = 'pa_' . $tax->attribute_name;
|
||||
if (taxonomy_exists($name)) { $attributeTaxonomies[] = $name; }
|
||||
}
|
||||
}
|
||||
// Also allow fallback to any pa_* that exists and has terms
|
||||
if (empty($attributeTaxonomies)) {
|
||||
$allTax = get_taxonomies(['public'=>false]);
|
||||
foreach ($allTax as $t) { if (str_starts_with($t,'pa_') && taxonomy_exists($t)) { $attributeTaxonomies[] = $t; } }
|
||||
}
|
||||
|
||||
// For no-JS fallback, we render a <form method="get" action="shop/category URL"> with same params
|
||||
$shopUrl = wc_get_page_permalink('shop');
|
||||
if (is_product_category()) {
|
||||
$term = get_queried_object();
|
||||
if ($term instanceof WP_Term) { $shopUrl = get_term_link($term); }
|
||||
}
|
||||
if (is_wp_error($shopUrl) || !$shopUrl) { $shopUrl = home_url('/shop/'); }
|
||||
|
||||
// Active chips data for JS
|
||||
?>
|
||||
<div class="xshop-filters" data-xshop-filters data-shop-url="<?php echo esc_url($shopUrl); ?>" data-category-context="<?php echo esc_attr($currentCategory); ?>">
|
||||
<!-- Active chips -->
|
||||
<div class="xshop-filters__chips" data-xshop-chips aria-live="polite"></div>
|
||||
|
||||
<form method="get" action="<?php echo esc_url($shopUrl); ?>" class="xshop-filters__form" data-xshop-filters-form>
|
||||
<?php
|
||||
// Preserve search keyword if present (from M3)
|
||||
if (isset($_GET['s'])) {
|
||||
echo '<input type="hidden" name="s" value="' . esc_attr(sanitize_text_field(wp_unslash($_GET['s']))) . '" />';
|
||||
}
|
||||
// Preserve category context as hidden if on category archive (so clearing filters doesn't drop category unless user clears chip)
|
||||
if (is_product_category() && $currentCategory !== '') {
|
||||
echo '<input type="hidden" name="category" value="' . esc_attr($currentCategory) . '" data-xshop-category-context />';
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Categories (if not already on category context? Show anyway with active state) -->
|
||||
<?php if (!empty($categories)): ?>
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php esc_html_e('Categories', 'xshop'); ?></legend>
|
||||
<div class="xshop-filter__options">
|
||||
<?php foreach ($categories as $cat):
|
||||
$isActive = $currentCategory === $cat->slug;
|
||||
?>
|
||||
<label class="xshop-filter__option">
|
||||
<input type="radio" name="category" value="<?php echo esc_attr($cat->slug); ?>" <?php checked($isActive); ?> data-filter="category" />
|
||||
<span><?php echo esc_html($cat->name); ?></span>
|
||||
<span class="xshop-filter__count"><?php echo esc_html(number_format_i18n((int)$cat->count)); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<label class="xshop-filter__option">
|
||||
<input type="radio" name="category" value="" <?php checked($currentCategory === ''); ?> data-filter="category" />
|
||||
<span><?php esc_html_e('All categories', 'xshop'); ?></span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Attributes -->
|
||||
<?php foreach ($attributeTaxonomies as $tax):
|
||||
$terms = get_terms(['taxonomy'=>$tax,'hide_empty'=>true,'number'=>30]);
|
||||
if (is_wp_error($terms) || empty($terms)) { continue; }
|
||||
$taxLabel = wc_attribute_label($tax);
|
||||
$activeTerms = [];
|
||||
if (isset($_GET[$tax])) {
|
||||
$raw = wp_unslash($_GET[$tax]);
|
||||
if (is_string($raw)) {
|
||||
foreach (explode(',', $raw) as $t) { $t = sanitize_title($t); if ($t !== '') { $activeTerms[] = $t; } }
|
||||
}
|
||||
}
|
||||
// Also support filter_ prefix legacy
|
||||
$filterKey = 'filter_' . substr($tax, 3);
|
||||
if (isset($_GET[$filterKey])) {
|
||||
$raw = wp_unslash($_GET[$filterKey]);
|
||||
foreach (explode(',', (string)$raw) as $t) { $t = sanitize_title($t); if ($t !== '') { $activeTerms[] = $t; } }
|
||||
}
|
||||
?>
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php echo esc_html($taxLabel); ?></legend>
|
||||
<div class="xshop-filter__options">
|
||||
<?php foreach ($terms as $term): $isActive = in_array($term->slug, $activeTerms, true); ?>
|
||||
<label class="xshop-filter__option">
|
||||
<input type="checkbox" name="<?php echo esc_attr($tax); ?>[]" value="<?php echo esc_attr($term->slug); ?>" <?php checked($isActive); ?> data-filter="attribute" data-tax="<?php echo esc_attr($tax); ?>" />
|
||||
<span><?php echo esc_html($term->name); ?></span>
|
||||
<span class="xshop-filter__count"><?php echo esc_html(number_format_i18n((int)$term->count)); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Price -->
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php esc_html_e('Price', 'xshop'); ?></legend>
|
||||
<div class="xshop-filter__price">
|
||||
<label>
|
||||
<span class="xshop-sr-only"><?php esc_html_e('Min price', 'xshop'); ?></span>
|
||||
<input type="number" name="min_price" placeholder="<?php esc_attr_e('Min', 'xshop'); ?>" value="<?php echo esc_attr($currentMinPrice); ?>" min="0" step="1" data-filter="price" />
|
||||
</label>
|
||||
<span aria-hidden="true">—</span>
|
||||
<label>
|
||||
<span class="xshop-sr-only"><?php esc_html_e('Max price', 'xshop'); ?></span>
|
||||
<input type="number" name="max_price" placeholder="<?php esc_attr_e('Max', 'xshop'); ?>" value="<?php echo esc_attr($currentMaxPrice); ?>" min="0" step="1" data-filter="price" />
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Stock -->
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php esc_html_e('Stock', 'xshop'); ?></legend>
|
||||
<div class="xshop-filter__options">
|
||||
<label class="xshop-filter__option"><input type="radio" name="stock" value="all" <?php checked($currentStock, 'all'); ?> data-filter="stock" /> <span><?php esc_html_e('All', 'xshop'); ?></span></label>
|
||||
<label class="xshop-filter__option"><input type="radio" name="stock" value="instock" <?php checked($currentStock, 'instock'); ?> data-filter="stock" /> <span><?php esc_html_e('In stock', 'xshop'); ?></span></label>
|
||||
<label class="xshop-filter__option"><input type="radio" name="stock" value="outofstock" <?php checked($currentStock, 'outofstock'); ?> data-filter="stock" /> <span><?php esc_html_e('Out of stock', 'xshop'); ?></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Sale -->
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php esc_html_e('Sale', 'xshop'); ?></legend>
|
||||
<label class="xshop-filter__option"><input type="checkbox" name="on_sale" value="1" <?php checked($currentOnSale, '1'); ?> data-filter="sale" /> <span><?php esc_html_e('On sale', 'xshop'); ?></span></label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Rating -->
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php esc_html_e('Rating', 'xshop'); ?></legend>
|
||||
<div class="xshop-filter__options">
|
||||
<?php for ($r=5;$r>=1;$r--): $isActive = $currentRating === $r; ?>
|
||||
<label class="xshop-filter__option"><input type="radio" name="rating" value="<?php echo esc_attr((string)$r); ?>" <?php checked($isActive); ?> data-filter="rating" /> <span><?php echo esc_html(sprintf(esc_html__('%d+ stars', 'xshop'), $r)); ?></span></label>
|
||||
<?php endfor; ?>
|
||||
<label class="xshop-filter__option"><input type="radio" name="rating" value="0" <?php checked($currentRating, 0); ?> data-filter="rating" /> <span><?php esc_html_e('Any rating', 'xshop'); ?></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Sorting (also in toolbar, but keep here for no-JS) -->
|
||||
<fieldset class="xshop-filter">
|
||||
<legend class="xshop-filter__title"><?php esc_html_e('Sort', 'xshop'); ?></legend>
|
||||
<select name="orderby" data-filter="orderby" aria-label="<?php esc_attr_e('Sort', 'xshop'); ?>">
|
||||
<option value="default" <?php selected($currentOrderby, 'default'); ?>><?php esc_html_e('Default', 'xshop'); ?></option>
|
||||
<option value="latest" <?php selected($currentOrderby, 'latest'); ?>><?php esc_html_e('Latest', 'xshop'); ?></option>
|
||||
<option value="price_asc" <?php selected($currentOrderby, 'price_asc'); ?>><?php esc_html_e('Price: low to high', 'xshop'); ?></option>
|
||||
<option value="price_desc" <?php selected($currentOrderby, 'price_desc'); ?>><?php esc_html_e('Price: high to low', 'xshop'); ?></option>
|
||||
<option value="popularity" <?php selected($currentOrderby, 'popularity'); ?>><?php esc_html_e('Popular', 'xshop'); ?></option>
|
||||
<option value="rating" <?php selected($currentOrderby, 'rating'); ?>><?php esc_html_e('Rated', 'xshop'); ?></option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<div class="xshop-filters__actions">
|
||||
<button type="submit" class="xshop-btn xshop-btn--primary"><?php esc_html_e('Apply', 'xshop'); ?></button>
|
||||
<a href="<?php echo esc_url($shopUrl); ?>" class="xshop-btn xshop-btn--ghost" data-xshop-clear-all><?php esc_html_e('Clear all', 'xshop'); ?></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user