customFilters = $customFilters; $enableFilter = false; if (is_string($customFilters)) { $customFilters = json_decode($customFilters, true); $this->customFilters = $customFilters; } if (!empty($customFilters)) { $enableFilter = true; if (Arr::has($customFilters, 'enabled')) { $enableFilter = Arr::get($customFilters, 'enabled'); } } $this->config = $config; $this->viewMode = !empty($config['view_mode']) ? $config['view_mode'] : 'grid'; $this->isFilterEnabled = $enableFilter; $this->per_page = Arr::get($config, 'per_page', Arr::get($defaultFilters, 'per_page', 10)); $this->order_type = Arr::get($defaultFilters, 'sort_type', 'DESC'); $this->order_by = Arr::get($defaultFilters, 'sort_by', 'id'); $this->liveFilter = Arr::get($this->customFilters, 'live_filter', true); $this->priceFormat = $config['price_format'] ?? 'starts_from'; $this->paginator = Arr::get($config, 'pagination_type', false); if ($this->paginator === 'simple') { $this->paginator = 'numbers'; } else if ($this->paginator === 'cursor') { $this->paginator = 'scroll'; } $this->productBoxGridSize = $config['product_box_grid_size'] ?? 4; $this->isWildcardFilterEnabled = Arr::get($config, 'enable_wildcard_filter', false); $this->enableSortBy = Arr::get($config, 'enable_sort_by', true); if (Arr::get($products, 'products', [])) { $this->products = Arr::get($products, 'products', []); } else { $this->products = $products; } if($this->products instanceof CursorPaginator){ $this->total = 0; }else{ $this->total = Arr::get($products, 'total', 0); if (!$this->total && $this->products instanceof LengthAwarePaginator) { // A raw LengthAwarePaginator's ArrayAccess proxies to its item // collection, not its own properties, so Arr::get($products, 'total') // above always misses when the caller passes the paginator directly // instead of the normalized ['products' => ..., 'total' => ...] shape. $this->total = $this->products->total(); } } //$this->total = $products['total']; $this->defaultFilters = array_merge($this->defaultFilters, Arr::get($defaultFilters, 'tax_query', [])); // Merge shortcode taxonomy_filters into defaultFilters so they persist in AJAX pagination $taxonomyFilters = Arr::get($config, 'taxonomy_filters', []); foreach ($taxonomyFilters as $taxonomy => $termIds) { if (!empty($termIds)) { if (!is_array($termIds)) { $termIds = [$termIds]; } $existing = Arr::get($this->defaultFilters, $taxonomy, []); if (is_string($existing) && $existing !== '') { $existing = array_map('trim', explode(',', $existing)); } elseif (!is_array($existing)) { $existing = []; } $this->defaultFilters[$taxonomy] = array_unique(array_merge($existing, $termIds)); } } if (!empty($this->defaultFilters)) { $this->defaultFilters['enabled'] = true; } // Merge allow_out_of_stock from config into defaultFilters if (Arr::get($config, 'allow_out_of_stock')) { $this->defaultFilters['allow_out_of_stock'] = true; } // Merge wildcard search preset into defaultFilters so it persists in AJAX re-fetches $wildcard = Arr::get($defaultFilters, 'wildcard', ''); if ($wildcard !== '') { $this->defaultFilters['wildcard'] = $wildcard; if (empty($this->defaultFilters['enabled'])) { $this->defaultFilters['enabled'] = true; } } // Merge sort_by from config filters into defaultFilters for AJAX persistence $configFilters = Arr::get($config, 'filters', []); if (is_string($configFilters)) { $configFilters = json_decode($configFilters, true) ?: []; } $shortcodeSortBy = Arr::get($configFilters, 'sort_by', ''); if ($shortcodeSortBy) { $this->defaultFilters['sort_by'] = $shortcodeSortBy; if (empty($this->defaultFilters['enabled'])) { $this->defaultFilters['enabled'] = true; } } $priceRange = Arr::get($this->customFilters, 'price_range', false); if ($priceRange) { // Accepts true (default "Price" label) or ['label' => '...'] so // integrations can name the price filter the same way the // taxonomies config names taxonomy filters. $this->filters['price_range'] = [ "filter_type" => "range", "is_meta" => false, "label" => is_array($priceRange) && !empty($priceRange['label']) ? $priceRange['label'] : "Price", "enabled" => true, ]; } if (isset($this->customFilters['taxonomies'])) { // Convert string to array if needed $taxonomies = $this->customFilters['taxonomies']; if (!is_array($taxonomies)) { $taxonomies = array_map('trim', explode(',', $taxonomies)); } foreach ($taxonomies as $key => $taxonomy) { if (is_array($taxonomy)) { foreach ($taxonomy as $taxonomyKey => $tax) { $this->filters[$taxonomyKey] = [ 'enabled' => true, 'filter_type' => 'options', 'is_meta' => true, 'label' => Arr::get($tax, 'label'), 'multiple' => false, 'options' => [] ]; foreach ($tax['options'] as $option) { $this->filters[$taxonomyKey]['options'][] = [ 'value' => $option['term_id'], 'label' => $option['name'], 'parent' => $option['parent'], 'children' => [] ]; } } } else { $this->filters[$taxonomy] = [ "filter_type" => "options", "is_meta" => true, "label" => ucfirst(str_replace('-', ' ', $taxonomy)), "enabled" => true, "multiple" => false, ]; } } } // Example of $this->filters // $this->filters = [ // "product-categories" => [ // "filter_type" => "options", // "is_meta" => true, // "label" => "Product Categories", // "enabled" => true, // "multiple" => false // ], // "product-types" => [ // "filter_type" => "options", // "is_meta" => true, // "label" => "Product Types", // "enabled" => true, // "multiple" => false // ] // ]; } public function render() { AssetLoader::loadProductArchiveAssets(); $isFullWidth = !$this->isFilterEnabled ? ' fct-full-container-width ' : ''; $renderer = new \FluentCart\App\Services\Renderer\ProductFilterRender($this->filters); $wrapperAttributes = [ 'class' => 'fct-products-wrapper-inner mode-' . $this->viewMode . $isFullWidth, 'data-fluent-cart-product-wrapper-inner' => '', 'data-per-page' => $this->per_page, 'data-order-type' => $this->order_type, 'data-live-filter' => $this->liveFilter, 'data-paginator' => $this->paginator, 'data-default-filters' => wp_json_encode($this->defaultFilters) ]; // Shortcode filter data attributes for AJAX persistence $includeIds = Arr::get($this->config, 'include_ids', []); $excludeIds = Arr::get($this->config, 'exclude_ids', []); $productType = Arr::get($this->config, 'product_type', ''); $onSale = Arr::get($this->config, 'on_sale', false); if (!empty($includeIds)) { $wrapperAttributes['data-include-ids'] = wp_json_encode($includeIds); } if (!empty($excludeIds)) { $wrapperAttributes['data-exclude-ids'] = wp_json_encode($excludeIds); } if (!empty($productType)) { $wrapperAttributes['data-product-type'] = esc_attr($productType); } if ($onSale) { $wrapperAttributes['data-on-sale'] = '1'; } // Persist hide_excerpt so AJAX pagination/filtering renders cards the // same way as the initial response (Paginator.js round-trips it). if (Arr::get($this->config, 'hide_excerpt', false)) { $wrapperAttributes['data-hide-excerpt'] = '1'; } ?>