feat: implement WooCommerce AJAX product filtering

This commit is contained in:
XShop
2026-09-13 20:31:20 +03:30
parent f5c283af25
commit b78e332966
15 changed files with 1543 additions and 150 deletions
+16 -17
View File
@@ -15,24 +15,22 @@ Two installable artifacts: theme (`xshop`) + plugin (`xshop-core`). Theme never
## 2. Theme – `xshop-theme`
### 2.1 File map (M3)
### 2.1 File map (M4)
```
xshop-theme/
style.css / functions.php / rtl.css / header.php / footer.php / index.php etc
inc/
setup/assets.php — tokens→base→layout→components→woo→utilities→style.css + theme.js + search.js (front-end only where data-xshop-search exists, documented)
woocommerce/setup.php — loop_start/end, toolbar, gallery remove, related 4/4, product.js conditional
assets/
css/woo.css — + search dropdown (.xshop-search, _dropdown, _results, _item, responsive fixed @640)
js/search.js — debounce 250ms, AbortController, stale seq, keyboard, a11y
template-parts/components/
ajax-search.php — reusable combobox (role=combobox, aria-expanded/controls, listbox, live status)
+ previous M2 components
woocommerce/ — 4 overrides as M2
inc/setup/assets.php — tokens→…→woo→utilities + theme.js + search.js + filter.js (front-end, DOM guard, documented)
inc/woocommerce/setup.php — loop, toolbar, gallery, related, product.js, pre_get_posts no-JS fallback
assets/css/woo.css — + search + filters (.xshop-shop, _filters, _chip, _drawer, responsive)
assets/js/search.js — debounce 250ms, AbortController, stale seq
assets/js/filter.js — URL sync, chips, debounce 300ms for price, AbortController, drawer a11y
template-parts/components/ajax-search.php + product-filters.php (sidebar+drawer, no-JS fallback)
woocommerce/archive-product.php — shop/category with filters layout + chips + AJAX grid
xshop-core/
includes/REST/Search.php — xshop/v1/search endpoint
includes/Core/Plugin.php — loads REST/Search.php
includes/Query/ProductQuery.php — shared sanitize, map_orderby, build_product_query_args, format_product, execute_query
includes/REST/Search.php — refactored to use Query
includes/REST/Products.php — xshop/v1/products (category, pa_*, price, stock, sale, rating, orderby, pagination, search)
includes/Core/Plugin.php — loads Query + both REST
```
### 2.2 Bootstrap
@@ -107,10 +105,11 @@ Tokens: `tokens.css` unchanged — adds WC tokens `--xshop-wc-badge-sale/--xshop
- Localized `xshopData` on `xshop` handle. No jQuery for theme code; Woo's jQuery remains for variations/add-to-cart.
- Build: `tools/build-release.ps1` now includes top-level folder `xshop/` wrapper (fixed in M1.1).
## 7. AJAX / REST (M3: Search)
## 7. AJAX / REST (M4)
- **Search**: `GET /wp-json/xshop/v1/search?search=&limit=` via `xshop-core/includes/REST/Search.php:1` (`register_rest_route xshop/v1/search`). Public read-only (`permission_callback __return_true`), sanitized `search` (trim/strip_tags/sanitize_text_field/max 100) + `limit` 1..20, min length 2 returns empty without DB, `WP_Query` s + `_sku` LIKE, `post_status publish`, `no_found_rows`, `fields ids` deduped to `limit` (default 6), response `{items:[{id,title,url,image,price_html,type,in_stock}], total}` + `Cache-Control: public, max-age=60`. No private data, no N+1, no draft, no customer/order, `wc_get_product` only for returned IDs.
- Prefer REST for search/filter/Q&A, fallback `admin-ajax` for cart where WC expects.
- **Search** (M3): `GET /wp-json/xshop/v1/search?search=&limit=` — public, `search` max 100 + `limit` 1..20, min 2, `WP_Query s` + `_sku LIKE`, `publish` only, `no_found_rows` `fields ids`, `{items:[id,title,url,image,price_html,type,in_stock],total}` + `Cache-Control: public, max-age=60`, shared `Query\ProductQuery`.
- **Products** (M4): `GET /wp-json/xshop/v1/products?search=&category=&pa_*=&min_price=&max_price=&stock=&on_sale=&rating=&orderby=&page=&per_page=` — public, sanitized `search` + `category` slug/ID + dynamic `pa_*` (only `taxonomy_exists`), `min/max` numeric 0..99M + `min<=max` 400, `stock all/instock/outofstock`, `on_sale 0/1` via meta OR, `rating 0..5` via `_wc_average_rating`, `orderby` allow-list `default/latest/price_asc/desc/popularity/rating/date` → `map_orderby`, `page 1..100` `per_page 1..24` default 12, shared `ProductQuery::build_product_query_args` → `WP_Query` `tax_query` + `meta_query` + `orderby` + `post__in` for sale, `execute_query` → `{items, pagination:{page,per_page,total,total_pages}}` + `Cache-Control: public, max-age=30`, no private data, no N+1.
- Prefer REST for search/filter, fallback `admin-ajax` for cart.
## 8. Security Model
+8 -9
View File
@@ -1,15 +1,15 @@
# XShop – Compatibility
## 1. Tested Up To (M2: 2026-09-13)
## 1. Tested Up To (M4: 2026-09-13)
| Dependency | Minimum | Tested |
|------------|---------|--------|
| WordPress | 6.4 | 7.1 (local XAMPP) |
| WordPress | 6.4 | 7.1 |
| WooCommerce| 8.0 | 11.1.0 |
| PHP | 8.2 | 8.2.12 (XAMPP), 8.3 recommended per spec |
| PHP | 8.2 | 8.2.12, 8.3 recommended |
| Elementor | 3.15 (optional) | 3.2x |
| Gutenberg | WP bundled | — |
| Browsers | last 2 versions: Chrome, Firefox, Edge, Safari | Layout checked 360/768/1024/1280/1440 via CSS grid |
| Browsers | last 2 versions | 360/390/768/1024/1280/1440 via Playwright + CSS |
## 2. WordPress APIs Used
@@ -19,12 +19,11 @@
- Customizer: `customize_register` for preview (typography/colours).
- REST: `register_rest_route` (namespace `xshop/v1`).
## 3. WooCommerce Integration (M2)
## 3. WooCommerce Integration (M4)
- Declared `add_theme_support('woocommerce')` + gallery features (`wc-product-gallery-zoom/lightbox/slider` now disabled via `remove_action` for custom gallery; can re-enable per ADR).
- Wrapper via `woocommerce_before_main_content` / `after_main_content` (`.xshop-main xshop-container .xshop-woo-layout`).
- Hooks: `woocommerce_product_loop_start/end` → `xshop-products`, `woocommerce_before_shop_loop` 9/35 toolbar, `init` remove gallery, `related_args` 4/4, `wp` enqueue `product.js` only on `is_product()`. Styles dequeued (`woocommerce_enqueue_styles → __return_empty_array`), replaced by `woo.css` (global, documented).
- Overrides (4, minimal, versioned): `content-product.php 3.6.0`, `archive-product.php 3.4.0`, `content-single-product.php 3.6.0`, `single-product.php 1.6.4`. See `ARCHITECTURE.md:11` for why/risks. Monitor `WC()->version` and template `Version:` header.
- `add_theme_support('woocommerce')` + wrappers, loop `xshop-products`, toolbar, `product.js` conditional, `search.js`+`filter.js` front-end (DOM guard).
- REST: `xshop/v1/search` + `xshop/v1/products` (shared `ProductQuery`), HPOS via `wc_get_products`/`WP_Query`, no raw SQL.
- Overrides (4): `content-product.php 3.6.0`, `archive-product.php 3.4.0` (now with `xshop-shop` layout + `product-filters` sidebar/drawer + `pre_get_posts` no-JS fallback), `content-single-product.php 3.6.0`, `single-product.php 1.6.4`.
## 4. HPOS
+13
View File
@@ -63,6 +63,19 @@
| RTL/LTR/dark/mobile: logical `woo.css` search dropdown (`fixed @640`), dark tokens, no overflow | ✅ Pass |
| No `console.log` / `php -l` clean / `debug.log` clean | ✅ Pass |
### M4 Gate (AJAX Filter) — manual QA 2026-09-13 (WP 7.1 / WC 11.1.0)
| Check | Result |
|-------|--------|
| `GET /wp-json/xshop/v1/products?per_page=12` no filter → 4 items | ✅ Pass |
| `category=uncategorized` →4, `min_price 100k max 200k` →1, `stock instock 3` `outofstock 1`, `on_sale 1` →1, `rating 4` →0, `orderby price_asc/desc` → sorted, `page 2 per_page 2` →2, `search Test`+category+sale →1, `min>max`→400, no private data | ✅ Pass |
| UI: `product-filters.php` `data-xshop-filters` `chips` `price debounce` `sale/stock/rating` `orderby` `Apply/Clear` `drawer dialog` `aria-modal` | ✅ Pass |
| JS: `filter.js` URL sync `history.pushState` `popstate` back/forward, `AbortController` stale `seq`, `debounce 300` price, `click outside` `Escape` focus trap, `aria-busy` `aria-live` | ✅ Pass |
| No-JS fallback `GET /shop/?stock=instock` → server `xshop-products` **PASS**, `?on_sale=1` **PASS** | ✅ Pass |
| Browser E2E (Playwright): category→ chips → remove → clear, sale→ grid, price, stock, sorting, pagination, rapid, back/forward, drawer open/close, search+filter coexist, no duplicate, no stale, no overflow 360/390/768/1024/1280 | ✅ Pass (duplicate form fixed) |
| RTL `fa_IR` `dir=rtl` `xshop--rtl` + dark `data-theme` + mobile 390 drawer | ✅ Pass |
| No `console.log` / `php -l` `filter.js` / `debug.log` clean | ✅ Pass |
## 3. Browser
Chromium, Firefox, Edge, Safari (where available). Responsive: 360/768/1024/1280.
+7 -5
View File
@@ -41,10 +41,11 @@ CSS layering (M2): `tokens→base→layout→components→woo→utilities→styl
## 3. Components
Reusable in `template-parts/components/` — **M1**: `site-header`, `site-footer`, `breadcrumbs`, `pagination`, `search-form`, `post-card`, `empty-state`, `loading`, `section-heading`, `hero`, `promo-banner`. **M2**: `product-card`/`product-grid`/`product-gallery`/`mini-cart`. **M3 adds**: `ajax-search.php:1` (reusable `data-xshop-search`, `role=combobox` `aria-expanded/controls/autocomplete`, `listbox` `aria-live`, `data-xshop-search-input/dropdown/results/loading/empty/error/viewall`, mobile fixed dropdown).
Reusable in `template-parts/components/` — **M1**: `site-header`, `site-footer`, `breadcrumbs`, `pagination`, `search-form`, `post-card`, `empty-state`, `loading`, `section-heading`, `hero`, `promo-banner`. **M2**: `product-card`/`product-grid`/`product-gallery`/`mini-cart`. **M3**: `ajax-search.php:1`. **M4 adds**: `product-filters.php:1` (`data-xshop-filters` `chips` `form method=get` no-JS fallback, `category radio` `pa_* checkbox` `price number` `stock radio` `sale checkbox` `rating radio` `orderby select`, `data-filter` + `data-tax`, drawer `role=dialog` `aria-modal`).
Implemented details:
- `ajax-search.php` — `data-min-length 2` `data-limit 6`, combobox pattern, `aria-expanded`/`aria-controls`, `aria-activedescendant` managed via JS, live status, view-all `?s=&post_type=product`, hidden `post_type` input when Woo active, RTL via logical props, dark via tokens, max-width 520px
- `ajax-search.php` — as M3
- `product-filters.php` — `chips` `aria-live`, `price debounce`, `stock/sale/rating` radios, `orderby`, `Apply/Clear all`, hidden `s` preserve, category context, RTL logical, dark tokens
- `product-card` etc — as M2
States: default/hover/focus-visible/active/disabled/loading. Focus ring via `:focus-visible` (token `--xshop-focus`).
@@ -71,10 +72,11 @@ SVG sprite or inline SVG, stroke 1.5–2. No icon font dependency. RTL flip via
`[data-theme="dark"]` covers all Woo elements via tokens: shop cards, gallery, price (`--xshop-wc-price-sale`), sale badge, tabs, tables, forms, notices. Verified dark tokens in `tokens.css:1` (`--xshop-bg 0b1220` etc). No second dark system.
## 8. Accessibility (M3)
## 8. Accessibility (M4)
- Search: `role=search`, `label.sr-only`, `role=combobox` + `aria-expanded` + `aria-controls` + `aria-activedescendant` + `listbox` `aria-live polite`, ArrowUp/Down `aria-selected`, Enter to select/fallback to form, Escape to close, click-outside, focus stays on input, loading/empty/error `aria-live`.
- General: semantic landmarks, skip link, trap focus for mobile nav, visible `:focus-visible`, contrast AA, `prefers-reduced-motion`.
- Search (M3): `role=search` `combobox` + `aria-expanded/controls/activedescendant` + `listbox` `aria-live`, ArrowUp/Down `aria-selected`, Enter/Escape/click-outside.
- Filters (M4): `fieldset/legend`, `label` for all inputs, `aria-expanded/controls` for drawer `role=dialog` `aria-modal`, `Escape` focus return to toggle, `aria-busy` on grid, `aria-live` chips + result count, keyboard Tab/Enter/Space.
- General: landmarks, skip link, trap focus, `:focus-visible`, contrast AA, `prefers-reduced-motion`.
## 9. RTL/LTR