feat: implement WooCommerce AJAX product filtering
This commit is contained in:
@@ -23,12 +23,10 @@ function xshop_enqueue_assets(): void {
|
||||
// Theme JS is tiny; feature modules conditionally enqueued elsewhere.
|
||||
wp_enqueue_script('xshop', XSHOP_THEME_URI . '/assets/js/theme.js', [], $ver, true);
|
||||
|
||||
// Search JS — only where ajax-search component is rendered (header has it globally, so enqueue on front-end when search UI exists)
|
||||
// We check for header search presence via has_action or simply enqueue on all front-end where header is rendered (is not admin/customizer preview irrelevant)
|
||||
// To satisfy "only where useful" while keeping header simple, enqueue on non-admin front-end (header renders on all front-end pages)
|
||||
if (!is_admin() && !wp_is_json_request()) {
|
||||
// Only enqueue if theme header will render (which is all front-end). We further guard via DOM marker in JS (init guard).
|
||||
wp_enqueue_script('xshop-search', XSHOP_THEME_URI . '/assets/js/search.js', [], $ver, true);
|
||||
// Filter JS — header search exists globally, filters exist on shop archives. Enqueue front-end globally (8KB) with DOM guard (init only if [data-xshop-filters] present).
|
||||
wp_enqueue_script('xshop-filter', XSHOP_THEME_URI . '/assets/js/filter.js', [], $ver, true);
|
||||
}
|
||||
|
||||
// Localized data for REST/AJAX (nonces, i18n hints).
|
||||
|
||||
@@ -33,7 +33,87 @@ function xshop_woo_setup(): void {
|
||||
|
||||
// Related/upsells: ensure they use product-card grid (filter columns)
|
||||
add_filter('woocommerce_output_related_products_args', 'xshop_wc_related_args');
|
||||
add_filter('woocommerce_upsells_total', '__return_false'); // keep? actually we keep upsells - no filter, just ensure columns via CSS grid
|
||||
add_filter('woocommerce_upsells_total', '__return_false');
|
||||
|
||||
// M4: No-JS fallback — apply filters to main query on shop/category archive
|
||||
add_action('pre_get_posts', 'xshop_wc_filter_main_query', 20);
|
||||
}
|
||||
|
||||
function xshop_wc_filter_main_query(\WP_Query $q): void {
|
||||
if (\is_admin() || !$q->is_main_query()) { return; }
|
||||
if (!\class_exists('WooCommerce')) { return; }
|
||||
// Only on product archive (shop or product_taxonomy)
|
||||
if (!(\is_shop() || \is_product_taxonomy())) { return; }
|
||||
// Don't interfere with REST
|
||||
if (\defined('REST_REQUEST') && \REST_REQUEST) { return; }
|
||||
|
||||
// If no filter params, leave default Woo query
|
||||
$hasFilter = isset($_GET['min_price']) || isset($_GET['max_price']) || isset($_GET['stock']) || isset($_GET['on_sale']) || isset($_GET['sale']) || isset($_GET['rating']) || isset($_GET['category']) || isset($_GET['orderby']) || isset($_GET['s']);
|
||||
// Also check pa_* attributes
|
||||
foreach ($_GET as $k => $v) {
|
||||
if (\str_starts_with((string) $k, 'pa_') || \str_starts_with((string) $k, 'filter_') || \str_starts_with((string) $k, 'attribute_')) { $hasFilter = true; break; }
|
||||
}
|
||||
if (!$hasFilter) { return; }
|
||||
|
||||
// Build params from $_GET (same canonical as REST)
|
||||
$params = [
|
||||
'search' => isset($_GET['s']) ? \sanitize_text_field(\wp_unslash($_GET['s'])) : '',
|
||||
'category' => isset($_GET['category']) ? \sanitize_text_field(\wp_unslash($_GET['category'])) : '',
|
||||
'attributes' => [],
|
||||
'min_price' => isset($_GET['min_price']) ? \sanitize_text_field(\wp_unslash($_GET['min_price'])) : null,
|
||||
'max_price' => isset($_GET['max_price']) ? \sanitize_text_field(\wp_unslash($_GET['max_price'])) : null,
|
||||
'stock' => isset($_GET['stock']) ? \sanitize_key(\wp_unslash($_GET['stock'])) : 'all',
|
||||
'on_sale' => (isset($_GET['on_sale']) && $_GET['on_sale'] === '1') || (isset($_GET['sale']) && $_GET['sale'] === '1'),
|
||||
'rating' => isset($_GET['rating']) ? \absint($_GET['rating']) : 0,
|
||||
'orderby' => isset($_GET['orderby']) ? \sanitize_key(\wp_unslash($_GET['orderby'])) : 'default',
|
||||
'page' => isset($_GET['page']) ? \absint($_GET['page']) : (isset($_GET['paged']) ? \absint($_GET['paged']) : 1),
|
||||
'per_page' => 12,
|
||||
];
|
||||
// Collect attributes from pa_* etc.
|
||||
foreach ($_GET as $k => $v) {
|
||||
$kClean = \sanitize_key((string) $k);
|
||||
if (\str_starts_with($kClean, 'pa_') || \str_starts_with($kClean, 'attribute_')) {
|
||||
$tax = $kClean;
|
||||
if (\str_starts_with($tax, 'attribute_')) { $tax = 'pa_' . \substr($tax, 10); }
|
||||
$terms = [];
|
||||
if (\is_array($v)) { foreach ($v as $t) { $t = \sanitize_title((string) $t); if ($t !== '') { $terms[] = $t; } } }
|
||||
else { foreach (\explode(',', (string) $v) as $t) { $t = \sanitize_title(\trim($t)); if ($t !== '') { $terms[] = $t; } } }
|
||||
if (!empty($terms) && \taxonomy_exists($tax)) { $params['attributes'][$tax] = $terms; }
|
||||
}
|
||||
if (\str_starts_with($kClean, 'filter_')) {
|
||||
$tax = 'pa_' . \substr($kClean, 7);
|
||||
if (\taxonomy_exists($tax)) {
|
||||
$terms = [];
|
||||
foreach (\explode(',', (string) $v) as $t) { $t = \sanitize_title(\trim($t)); if ($t !== '') { $terms[] = $t; } }
|
||||
if (!empty($terms)) { $params['attributes'][$tax] = $terms; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// If on category archive and category param empty, preserve queried category
|
||||
if (\is_product_category() && empty($params['category'])) {
|
||||
$term = \get_queried_object();
|
||||
if ($term instanceof \WP_Term) { $params['category'] = $term->slug; }
|
||||
}
|
||||
|
||||
// Use shared ProductQuery to build args, then merge into main query
|
||||
if (!\class_exists('XShop\Core\Query\ProductQuery')) {
|
||||
// Fallback if plugin not loaded yet (should be loaded via xshop-core)
|
||||
return;
|
||||
}
|
||||
// ProductQuery class is functions-based, not class; use function directly if available
|
||||
if (\function_exists('XShop\Core\Query\build_product_query_args')) {
|
||||
$wpArgs = \XShop\Core\Query\build_product_query_args($params);
|
||||
// Merge into main query (only tax_query/meta_query/orderby etc, not post_type/status which already correct)
|
||||
if (!empty($wpArgs['tax_query'])) { $q->set('tax_query', $wpArgs['tax_query']); }
|
||||
if (!empty($wpArgs['meta_query'])) { $q->set('meta_query', $wpArgs['meta_query']); }
|
||||
if (isset($wpArgs['meta_key'])) { $q->set('meta_key', $wpArgs['meta_key']); }
|
||||
if (isset($wpArgs['orderby'])) { $q->set('orderby', $wpArgs['orderby']); }
|
||||
if (isset($wpArgs['order'])) { $q->set('order', $wpArgs['order']); }
|
||||
if (isset($wpArgs['s'])) { $q->set('s', $wpArgs['s']); }
|
||||
// Ensure per_page/page respects
|
||||
$q->set('posts_per_page', $wpArgs['posts_per_page']);
|
||||
$q->set('paged', $wpArgs['paged']);
|
||||
}
|
||||
}
|
||||
|
||||
function xshop_wc_remove_default_gallery(): void {
|
||||
|
||||
Reference in New Issue
Block a user