PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Modules / Templating / Bricks / Elements / ProductsCollection.php

ProductsCollection.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Modules/Templating/Bricks/Elements/ProductsCollection.php

1,160 lines 41.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\Templating\Bricks\Elements;
4
5 use Bricks\Custom_Render_Element;
6 use Bricks\Helpers;
7 use Bricks\Query;
8 use FluentCart\App\Modules\Data\ProductDataSetup;
9 use FluentCart\App\Modules\Data\ProductQuery;
10 use FluentCart\App\Modules\Templating\AssetLoader;
11 use FluentCart\App\Modules\Templating\Bricks\BricksHelper;
12 use FluentCart\App\Services\Renderer\RenderHelper;
13 use FluentCart\App\Services\Renderer\ShopAppRenderer;
14 use FluentCart\Api\Taxonomy;
15 use FluentCart\Framework\Support\Arr;
16 use FluentCart\Framework\Support\Str;
17 use FluentCart\App\Services\Renderer\ProductFilterRender;
18
19 if (!defined('ABSPATH')) exit; // Exit if accessed directly
20
21 class ProductsCollection extends Custom_Render_Element
22 {
23 public $category = 'fluent-cart';
24 public $name = 'fct-products';
25 public $icon = 'ti-archive fluent-cart-element-icon';
26
27 protected $cssRoot = '.fct-products-wrapper-inner .fct-products-container';
28
29 public function enqueue_scripts()
30 {
31 AssetLoader::loadProductArchiveAssets();
32
33 do_action('fluent_cart/advanced_variation/enqueue_assets');
34 }
35
36 public function get_label()
37 {
38 return esc_html__('Products', 'fluent-cart');
39 }
40
41 public function set_control_groups()
42 {
43 $this->control_groups['query'] = [
44 'title' => esc_html__('Query', 'fluent-cart'),
45 'tab' => 'content',
46 ];
47
48 $this->control_groups['fields'] = [
49 'title' => esc_html__('Fields', 'fluent-cart'),
50 'tab' => 'content',
51 ];
52
53 $this->control_groups['display'] = [
54 'title' => esc_html__('Display', 'fluent-cart'),
55 'tab' => 'content',
56 ];
57
58 $this->control_groups['filter'] = [
59 'title' => esc_html__('Filter', 'fluent-cart'),
60 'tab' => 'content',
61 ];
62
63 $this->control_groups['defaultFilter'] = [
64 'title' => esc_html__('Default Filter', 'fluent-cart'),
65 'tab' => 'content',
66 ];
67
68 $this->control_groups['widgets'] = [
69 'title' => esc_html__('Widgets', 'fluent-cart'),
70 'tab' => 'widgets',
71 ];
72
73 $this->control_groups['sale_badge'] = [
74 'title' => esc_html__('Sale Badge', 'fluent-cart'),
75 'tab' => 'content',
76 ];
77
78 $this->control_groups['sale_badge_style'] = [
79 'title' => esc_html__('Sale Badge', 'fluent-cart'),
80 'tab' => 'style',
81 ];
82
83 $this->control_groups['sold_out_badge'] = [
84 'title' => esc_html__('Sold Out Badge', 'fluent-cart'),
85 'tab' => 'content',
86 ];
87
88 $this->control_groups['sold_out_badge_style'] = [
89 'title' => esc_html__('Sold Out Badge', 'fluent-cart'),
90 'tab' => 'style',
91 ];
92 }
93
94 public function set_controls()
95 {
96 // LAYOUT
97 $this->controls['viewMode'] = [
98 'tab' => 'content',
99 'group' => 'display',
100 'label' => esc_html__('View Mode', 'fluent-cart'),
101 'type' => 'select',
102 'options' => [
103 'grid' => esc_html__('Grid', 'fluent-cart'),
104 'list' => esc_html__('List', 'fluent-cart'),
105 ],
106 'placeholder' => esc_html__('Grid', 'fluent-cart'),
107 'rerender' => true,
108 ];
109
110 $this->controls['showViewSwitcher'] = [
111 'tab' => 'content',
112 'group' => 'display',
113 'label' => esc_html__('Show View Switcher', 'fluent-cart'),
114 'type' => 'checkbox',
115 'inline' => true,
116 'default' => true,
117 ];
118
119 $this->controls['paginationType'] = [
120 'tab' => 'content',
121 'group' => 'display',
122 'label' => esc_html__('Pagination Type', 'fluent-cart'),
123 'type' => 'select',
124 'options' => [
125 'scroll' => esc_html__('Scroll', 'fluent-cart'),
126 'numbers' => esc_html__('Numbers', 'fluent-cart'),
127 ],
128 'placeholder' => esc_html__('Numbers', 'fluent-cart'),
129 ];
130
131 $this->controls['columns'] = [
132 'tab' => 'content',
133 'label' => esc_html__('Columns', 'fluent-cart'),
134 'type' => 'number',
135 'min' => 1,
136 'max' => 5,
137 'breakpoints' => true,
138 'placeholder' => 4,
139 'css' => [
140 [
141 'selector' => $this->cssRoot,
142 'property' => '--grid-columns',
143 ],
144 ],
145 ];
146
147 $this->controls['gap'] = [
148 'tab' => 'content',
149 'label' => esc_html__('Gap', 'fluent-cart'),
150 'type' => 'number',
151 'units' => true,
152 'css' => [
153 [
154 'selector' => $this->cssRoot,
155 'property' => 'gap',
156 ],
157 ],
158 'placeholder' => 30,
159 ];
160
161 $this->controls['posts_per_page'] = [
162 'tab' => 'content',
163 'label' => esc_html__('Products per page', 'fluent-cart'),
164 'type' => 'number',
165 'min' => 1,
166 'max' => 100,
167 'placeholder' => 10,
168 'step' => 1,
169 ];
170
171 $this->controls['is_main_query'] = [
172 'tab' => 'content',
173 'label' => esc_html__('Is main query', 'fluent-cart'),
174 'type' => 'checkbox',
175 'inline' => true,
176 ];
177
178 // QUERY
179 $this->controls['orderby'] = [
180 'tab' => 'content',
181 'label' => esc_html__('Order by', 'fluent-cart'),
182 'type' => 'select',
183 // id|date|title|price
184 'options' => [
185 'price' => esc_html__('Price', 'fluent-cart'),
186 'title' => esc_html__('Product Name', 'fluent-cart'),
187 'date' => esc_html__('Published date', 'fluent-cart'),
188 'id' => esc_html__('Product ID', 'fluent-cart')
189 ],
190 'inline' => true,
191 'placeholder' => esc_html__('Default', 'fluent-cart'),
192 ];
193
194 $this->controls['order'] = [
195 'tab' => 'content',
196 'label' => esc_html__('Order', 'fluent-cart'),
197 'type' => 'select',
198 'options' => [
199 'ASC' => esc_html__('Ascending', 'fluent-cart'),
200 'DESC' => esc_html__('Descending', 'fluent-cart'),
201 ],
202 'inline' => true,
203 'placeholder' => esc_html__('Descending', 'fluent-cart'),
204 ];
205
206 $this->controls['main_query_info'] = [
207 'tab' => 'content',
208 'group' => 'query',
209 'type' => 'info',
210 'content' => esc_html__('The query settings will be ignored when Is main query is enabled.', 'fluent-cart'),
211 ];
212
213 $this->controls['productType'] = [
214 'tab' => 'content',
215 'group' => 'query',
216 'label' => esc_html__('Product type', 'fluent-cart'),
217 'type' => 'select',
218 // physical|digital|subscription|onetime|simple|variations
219 'options' => [
220 'simple' => esc_html__('Simple', 'fluent-cart'),
221 'physical' => esc_html__('Physical', 'fluent-cart'),
222 'digital' => esc_html__('Digital', 'fluent-cart'),
223 'variations' => esc_html__('Variations', 'fluent-cart'),
224 'ontime' => esc_html__('One-time', 'fluent-cart'),
225 'subscription' => esc_html__('Subscriptions', 'fluent-cart'),
226 ],
227 'multiple' => false,
228 'placeholder' => esc_html__('all product types', 'fluent-cart'),
229 ];
230
231 $this->controls['include'] = [
232 'tab' => 'content',
233 'group' => 'query',
234 'label' => esc_html__('Include', 'fluent-cart'),
235 'type' => 'select',
236 'optionsAjax' => [
237 'action' => 'bricks_get_posts',
238 'postType' => 'fluent-products',
239 ],
240 'multiple' => true,
241 'searchable' => true,
242 'placeholder' => esc_html__('Select products', 'fluent-cart'),
243 ];
244
245 //phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
246 $this->controls['exclude'] = [
247 'tab' => 'content',
248 'group' => 'query',
249 'label' => esc_html__('Exclude', 'fluent-cart'),
250 'type' => 'select',
251 'optionsAjax' => [
252 'action' => 'bricks_get_posts',
253 'postType' => 'fluent-products',
254 ],
255 'multiple' => true,
256 'searchable' => true,
257 'placeholder' => esc_html__('Select products', 'fluent-cart'),
258 ];
259
260 $this->controls['categories'] = [
261 'tab' => 'content',
262 'group' => 'query',
263 'label' => esc_html__('Product categories', 'fluent-cart'),
264 'type' => 'select',
265 'options' => BricksHelper::getCategoriesOptions(),
266 'multiple' => true,
267 ];
268
269 $this->controls['onSale'] = [
270 'tab' => 'content',
271 'group' => 'query',
272 'label' => esc_html__('On sale Products only', 'fluent-cart'),
273 'type' => 'checkbox',
274 ];
275
276 $this->controls['allowOutOfStock'] = [
277 'tab' => 'content',
278 'group' => 'query',
279 'label' => esc_html__('Allow Out Of Stock', 'fluent-cart'),
280 'type' => 'checkbox',
281 ];
282
283 // FILTER
284 $this->controls['enableFilter'] = [
285 'tab' => 'content',
286 'group' => 'filter',
287 'label' => esc_html__('Enable Filter', 'fluent-cart'),
288 'type' => 'checkbox',
289 'inline' => true,
290 ];
291
292 $this->controls['enableSortBy'] = [
293 'tab' => 'content',
294 'group' => 'filter',
295 'label' => esc_html__('Enable Sort By', 'fluent-cart'),
296 'type' => 'checkbox',
297 'inline' => true,
298 'default' => true,
299 'required' => ['enableFilter', '=', true],
300 ];
301
302 $this->controls['liveFilter'] = [
303 'tab' => 'content',
304 'group' => 'filter',
305 'label' => esc_html__('Live Filter', 'fluent-cart'),
306 'type' => 'checkbox',
307 'inline' => true,
308 'required' => ['enableFilter', '=', true],
309 ];
310
311 $this->controls['wildcardFilter'] = [
312 'tab' => 'content',
313 'group' => 'filter',
314 'label' => esc_html__('Wildcard Filter', 'fluent-cart'),
315 'type' => 'checkbox',
316 'inline' => true,
317 'required' => ['enableFilter', '=', true],
318 ];
319
320 $taxonomies = Taxonomy::getTaxonomies();
321 foreach ($taxonomies as $taxonomy) {
322 $controlKey = sanitize_key(str_replace('-', '_', $taxonomy));
323 $label = esc_html(Str::headline($taxonomy));
324 $taxonomyName = esc_html(str_replace('product-', '', $taxonomy));
325
326 $this->controls['taxonomy_' . $controlKey] = [
327 'tab' => 'content',
328 'group' => 'filter',
329 'label' => $label,
330 'type' => 'checkbox',
331 'inline' => true,
332 'required' => ['enableFilter', '=', true],
333 ];
334
335 $this->controls['displayName_' . $controlKey] = [
336 'tab' => 'content',
337 'group' => 'filter',
338 'label' => esc_html__('Display Name', 'fluent-cart'),
339 'type' => 'text',
340 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'),
341 'required' => [
342 ['enableFilter', '=', true],
343 ['taxonomy_' . $controlKey, '=', true],
344 ],
345 ];
346
347 /* translators: %1$s: taxonomy name (e.g. "categories", "brands") */
348 $this->controls['showEmpty_' . $controlKey] = [
349 'tab' => 'content',
350 'group' => 'filter',
351 'label' => esc_html__('Show empty', 'fluent-cart'),
352 'type' => 'checkbox',
353 'inline' => true,
354 'description' => sprintf(esc_html__('Display %1$s even if they have no products.', 'fluent-cart'), $taxonomyName),
355 'required' => [
356 ['enableFilter', '=', true],
357 ['taxonomy_' . $controlKey, '=', true],
358 ],
359 ];
360 }
361
362 $this->controls['priceRange'] = [
363 'tab' => 'content',
364 'group' => 'filter',
365 'label' => esc_html__( 'Price Range', 'fluent-cart' ),
366 'type' => 'checkbox',
367 'inline' => true,
368 'required' => ['enableFilter', '=', true],
369 ];
370
371 $this->controls['displayNamePriceRange'] = [
372 'tab' => 'content',
373 'group' => 'filter',
374 'label' => esc_html__('Display Name', 'fluent-cart'),
375 'type' => 'text',
376 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'),
377 'required' => [
378 ['enableFilter', '=', true],
379 ['priceRange', '=', true],
380 ],
381 ];
382
383 // DEFAULT FILTER
384 $this->controls['defaultFilterEnabled'] = [
385 'tab' => 'content',
386 'group' => 'defaultFilter',
387 'label' => esc_html__('Enable Default Filter', 'fluent-cart'),
388 'type' => 'checkbox',
389 'inline' => true,
390 'description' => esc_html__('Presets applied to the product query by default, independent of the visitor-facing filter above. Ignored when Is main query is enabled.', 'fluent-cart'),
391 ];
392
393 $this->controls['defaultFilterAllowOutOfStock'] = [
394 'tab' => 'content',
395 'group' => 'defaultFilter',
396 'label' => esc_html__('Allow Out Of Stock', 'fluent-cart'),
397 'type' => 'checkbox',
398 'inline' => true,
399 'required' => ['defaultFilterEnabled', '=', true],
400 ];
401
402 $this->controls['defaultFilterWildcard'] = [
403 'tab' => 'content',
404 'group' => 'defaultFilter',
405 'label' => esc_html__('Search', 'fluent-cart'),
406 'type' => 'text',
407 'placeholder' => esc_html__('Preset search term', 'fluent-cart'),
408 'required' => ['defaultFilterEnabled', '=', true],
409 ];
410
411 foreach ($taxonomies as $taxonomy) {
412 $controlKey = sanitize_key(str_replace('-', '_', $taxonomy));
413 $label = esc_html(Str::headline($taxonomy));
414
415 // Bricks' built-in terms AJAX endpoint (bounded to 100 results per
416 // request, server-side searchable) — avoids eagerly loading and
417 // flattening every term of every taxonomy on control registration.
418 $this->controls['defaultFilterTaxonomy_' . $controlKey] = [
419 'tab' => 'content',
420 'group' => 'defaultFilter',
421 'label' => $label,
422 'type' => 'select',
423 'multiple' => true,
424 'searchable' => true,
425 'optionsAjax' => [
426 'action' => 'bricks_get_terms_options',
427 'postTypes' => ['fluent-products'],
428 'taxonomy' => [$taxonomy],
429 ],
430 'placeholder' => esc_html__('Select terms', 'fluent-cart'),
431 'required' => ['defaultFilterEnabled', '=', true],
432 ];
433 }
434
435 // FIELDS
436 $fields = $this->get_post_fields();
437
438 // Remove field settings
439 unset($fields['fields']['fields']['overlay']);
440 unset($fields['fields']['fields']['dynamicPadding']);
441 unset($fields['fields']['fields']['dynamicBackground']);
442 unset($fields['fields']['fields']['dynamicBorder']);
443
444 // Set fields defaults fields set
445 $fields['fields']['default'] = [
446 [
447 'dynamicData' => '{fct_product_image:link}',
448 'id' => Helpers::generate_random_id(false),
449 ],
450 [
451 'dynamicData' => '{fct_product_title:linked}',
452 'tag' => 'h5',
453 'id' => Helpers::generate_random_id(false),
454 ],
455 [
456 'dynamicData' => '{fct_product_excerpt}',
457 'tag' => 'p',
458 'id' => Helpers::generate_random_id(false),
459 ],
460 [
461 'dynamicData' => '{fct_product_price}',
462 'id' => Helpers::generate_random_id(false),
463 ],
464 [
465 'dynamicData' => '{fct_product_button}',
466 'id' => Helpers::generate_random_id(false),
467 ]
468 ];
469
470 $this->controls = array_replace_recursive($this->controls, $fields);
471
472 $this->controls['linkProduct'] = [
473 'tab' => 'content',
474 'group' => 'fields',
475 'label' => esc_html__('Link entire product', 'fluent-cart'),
476 'type' => 'checkbox',
477 'inline' => true,
478 'description' => esc_html__('Only added if none of your product fields contains any links.', 'fluent-cart'),
479 ];
480
481 // SALE BADGE
482 $this->controls['showSaleBadge'] = [
483 'tab' => 'content',
484 'group' => 'sale_badge',
485 'type' => 'checkbox',
486 'label' => esc_html__('Show Sale Badge', 'fluent-cart'),
487 'rerender' => true,
488 ];
489
490 $this->controls['saleBadgeText'] = [
491 'tab' => 'content',
492 'group' => 'sale_badge',
493 'type' => 'text',
494 'label' => esc_html__('Badge Text', 'fluent-cart'),
495 'placeholder' => esc_html__('Sale!', 'fluent-cart'),
496 'default' => esc_html__('Sale!', 'fluent-cart'),
497 'required' => ['showSaleBadge', '=', true],
498 ];
499
500 $this->controls['saleBadgeShowPercentage'] = [
501 'tab' => 'content',
502 'group' => 'sale_badge',
503 'type' => 'checkbox',
504 'label' => esc_html__('Show discount percentage instead', 'fluent-cart'),
505 'rerender' => true,
506 'required' => ['showSaleBadge', '=', true],
507 ];
508
509 $this->controls['saleBadgePercentageText'] = [
510 'tab' => 'content',
511 'group' => 'sale_badge',
512 'type' => 'text',
513 'label' => esc_html__('Percentage Text', 'fluent-cart'),
514 'description' => esc_html__('Use {percent} as a placeholder for the discount amount, e.g. -{percent}%', 'fluent-cart'),
515 'placeholder' => '-{percent}%',
516 'default' => '-{percent}%',
517 'required' => [['showSaleBadge', '=', true], ['saleBadgeShowPercentage', '=', true]],
518 ];
519
520 $this->controls['saleBadgePriceSource'] = [
521 'tab' => 'content',
522 'group' => 'sale_badge',
523 'type' => 'select',
524 'label' => esc_html__('Price Source', 'fluent-cart'),
525 'options' => [
526 'default_variant' => esc_html__('Default Variant', 'fluent-cart'),
527 'best_discount' => esc_html__('Best Discount (All Variants)', 'fluent-cart'),
528 ],
529 'default' => 'default_variant',
530 'inline' => true,
531 'required' => ['showSaleBadge', '=', true],
532 ];
533
534 $this->controls['saleBadgeShape'] = [
535 'tab' => 'content',
536 'group' => 'sale_badge',
537 'type' => 'select',
538 'label' => esc_html__('Badge Shape', 'fluent-cart'),
539 'options' => [
540 'badge' => esc_html__('Badge', 'fluent-cart'),
541 'ribbon' => esc_html__('Ribbon', 'fluent-cart'),
542 ],
543 'default' => 'badge',
544 'inline' => true,
545 'required' => ['showSaleBadge', '=', true],
546 ];
547
548 $this->controls['saleBadgePosition'] = [
549 'tab' => 'content',
550 'group' => 'sale_badge',
551 'type' => 'select',
552 'label' => esc_html__('Position', 'fluent-cart'),
553 'options' => [
554 'top-left' => esc_html__('Top Left', 'fluent-cart'),
555 'top-right' => esc_html__('Top Right', 'fluent-cart'),
556 'bottom-left' => esc_html__('Bottom Left', 'fluent-cart'),
557 'bottom-right' => esc_html__('Bottom Right', 'fluent-cart'),
558 ],
559 'default' => 'top-left',
560 'inline' => true,
561 'required' => ['showSaleBadge', '=', true],
562 ];
563
564 $this->controls['saleBadgeTypography'] = [
565 'tab' => 'style',
566 'group' => 'sale_badge_style',
567 'type' => 'typography',
568 'label' => esc_html__('Typography', 'fluent-cart'),
569 'css' => [
570 ['property' => 'font', 'selector' => '.fct-sale-badge'],
571 ],
572 'required' => ['showSaleBadge', '=', true],
573 ];
574
575 $this->controls['saleBadgeBackgroundColor'] = [
576 'tab' => 'style',
577 'group' => 'sale_badge_style',
578 'type' => 'color',
579 'label' => esc_html__('Background Color', 'fluent-cart'),
580 'css' => [
581 ['property' => 'background-color', 'selector' => '.fct-sale-badge'],
582 ],
583 'required' => ['showSaleBadge', '=', true],
584 ];
585
586 $this->controls['saleBadgeTextColor'] = [
587 'tab' => 'style',
588 'group' => 'sale_badge_style',
589 'type' => 'color',
590 'label' => esc_html__('Text Color', 'fluent-cart'),
591 'css' => [
592 ['property' => 'color', 'selector' => '.fct-sale-badge'],
593 ],
594 'required' => ['showSaleBadge', '=', true],
595 ];
596
597 // SOLD OUT BADGE
598 $this->controls['showSoldOutBadge'] = [
599 'tab' => 'content',
600 'group' => 'sold_out_badge',
601 'type' => 'checkbox',
602 'label' => esc_html__('Show Sold Out Badge', 'fluent-cart'),
603 'rerender' => true,
604 ];
605
606 $this->controls['soldOutBadgeText'] = [
607 'tab' => 'content',
608 'group' => 'sold_out_badge',
609 'type' => 'text',
610 'label' => esc_html__('Badge Text', 'fluent-cart'),
611 'placeholder' => esc_html__('Out of Stock', 'fluent-cart'),
612 'default' => esc_html__('Out of Stock', 'fluent-cart'),
613 'required' => ['showSoldOutBadge', '=', true],
614 ];
615
616 $this->controls['soldOutBadgeShape'] = [
617 'tab' => 'content',
618 'group' => 'sold_out_badge',
619 'type' => 'select',
620 'label' => esc_html__('Badge Shape', 'fluent-cart'),
621 'options' => [
622 'badge' => esc_html__('Badge', 'fluent-cart'),
623 'ribbon' => esc_html__('Ribbon', 'fluent-cart'),
624 ],
625 'default' => 'badge',
626 'inline' => true,
627 'required' => ['showSoldOutBadge', '=', true],
628 ];
629
630 $this->controls['soldOutBadgePosition'] = [
631 'tab' => 'content',
632 'group' => 'sold_out_badge',
633 'type' => 'select',
634 'label' => esc_html__('Position', 'fluent-cart'),
635 'options' => [
636 'top-left' => esc_html__('Top Left', 'fluent-cart'),
637 'top-right' => esc_html__('Top Right', 'fluent-cart'),
638 'bottom-left' => esc_html__('Bottom Left', 'fluent-cart'),
639 'bottom-right' => esc_html__('Bottom Right', 'fluent-cart'),
640 ],
641 'default' => 'top-left',
642 'inline' => true,
643 'required' => ['showSoldOutBadge', '=', true],
644 ];
645
646 $this->controls['soldOutBadgeTypography'] = [
647 'tab' => 'style',
648 'group' => 'sold_out_badge_style',
649 'type' => 'typography',
650 'label' => esc_html__('Typography', 'fluent-cart'),
651 'css' => [
652 ['property' => 'font', 'selector' => '.fct-sold-out-badge'],
653 ],
654 'required' => ['showSoldOutBadge', '=', true],
655 ];
656
657 $this->controls['soldOutBadgeBackgroundColor'] = [
658 'tab' => 'style',
659 'group' => 'sold_out_badge_style',
660 'type' => 'color',
661 'label' => esc_html__('Background Color', 'fluent-cart'),
662 'css' => [
663 ['property' => 'background-color', 'selector' => '.fct-sold-out-badge'],
664 ],
665 'required' => ['showSoldOutBadge', '=', true],
666 ];
667
668 $this->controls['soldOutBadgeTextColor'] = [
669 'tab' => 'style',
670 'group' => 'sold_out_badge_style',
671 'type' => 'color',
672 'label' => esc_html__('Text Color', 'fluent-cart'),
673 'css' => [
674 ['property' => 'color', 'selector' => '.fct-sold-out-badge'],
675 ],
676 'required' => ['showSoldOutBadge', '=', true],
677 ];
678 }
679
680 public function render()
681 {
682 $settings = $this->settings;
683
684 $this->setBricksQuery();
685
686 $viewMode = Arr::get($settings, 'viewMode', 'grid');
687 $showViewSwitcher = !empty($settings['showViewSwitcher']);
688 $paginationType = Arr::get($settings, 'paginationType', 'numbers');
689 $isMainQuery = Arr::get($settings, 'is_main_query', false) && $this->is_frontend;
690 $perPage = (int)Arr::get($settings, 'posts_per_page', 10);
691 $enableFilter = !empty($settings['enableFilter']);
692 $priceRange = !empty($settings['priceRange']);
693 $liveFilter = !empty($settings['liveFilter']);
694
695 $this->storeSettingsTransient($settings);
696
697 if ($perPage <= 0) {
698 $perPage = 10;
699 }
700
701 if ($perPage > 100) {
702 $perPage = 100;
703 }
704
705 $args = $this->buildQueryArgs(
706 $settings,
707 $isMainQuery,
708 $perPage,
709 $viewMode
710 );
711
712 $productsQuery = (new ProductQuery($args));
713 $products = $productsQuery->get();
714 $defaultFilters = $productsQuery->getDefaultFilters();
715 $ajaxDefaultFilters = $this->getAjaxDefaultFilters(
716 $defaultFilters,
717 $settings
718 );
719
720 $filters = $this->getFilters($settings);
721
722 $wrapperClass = 'fct-products-wrapper-inner ' . ($viewMode === 'list' ? 'mode-list' : 'mode-grid') . (!$enableFilter ? ' fct-full-container-width' : '');
723
724 $wrapperAttributes = [
725 'class' => $wrapperClass,
726 'data-fluent-cart-product-wrapper-inner' => '',
727 'data-per-page' => $perPage,
728 'data-order-type' => Arr::get($defaultFilters, 'sort_type'),
729 'data-live-filter' => $liveFilter,
730 'data-paginator' => esc_attr($paginationType),
731 'data-default-filters' => wp_json_encode($ajaxDefaultFilters)
732 ];
733
734 // Persist the element's query restrictions so the AJAX round-trip
735 // (search, sort, pagination) re-applies them. Without these the
736 // Paginator sends no include/exclude/type/on-sale params and the
737 // server falls back to an unrestricted query — e.g. an excluded
738 // product reappears as soon as the visitor searches for it.
739 $includeIds = Arr::get($args, 'include_ids', []);
740 if (!empty($includeIds)) {
741 $wrapperAttributes['data-include-ids'] = wp_json_encode(array_map('intval', (array) $includeIds));
742 }
743
744 $excludeIds = Arr::get($args, 'exclude_ids', []);
745 if (!empty($excludeIds)) {
746 $wrapperAttributes['data-exclude-ids'] = wp_json_encode(array_map('intval', (array) $excludeIds));
747 }
748
749 $productTypeArg = Arr::get($args, 'product_type', '');
750 if (!empty($productTypeArg)) {
751 $wrapperAttributes['data-product-type'] = is_array($productTypeArg) ? implode(',', $productTypeArg) : $productTypeArg;
752 }
753
754 if (Arr::get($args, 'on_sale', false)) {
755 $wrapperAttributes['data-on-sale'] = '1';
756 }
757
758 $productWrapperClasses = [
759 'fct-products-wrapper',
760 'fct-brick-products-wrapper',
761 ];
762
763 if (!$this->is_frontend) {
764 $productWrapperClasses[] = 'fct-bricks-editor-mode';
765 }
766
767 $rendererConfig = [
768 'view_mode' => $viewMode,
769 'enable_wildcard_filter' => !empty($settings['wildcardFilter']),
770 'custom_filters' => [
771 'enabled' => $enableFilter,
772 'price_range' => $priceRange,
773 'live_filter' => $liveFilter
774 ],
775 ];
776
777 // Only override enable_sort_by if explicitly set in settings (key exists)
778 if (array_key_exists('enableSortBy', $settings)) {
779 $rendererConfig['enable_sort_by'] = !empty($settings['enableSortBy']);
780 }
781
782 $shopAppRenderer = new ShopAppRenderer($products, $rendererConfig);
783
784 ?>
785 <div
786 <?php
787 //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_attributes() handles escaping internally
788 echo $this->render_attributes('_root');
789 ?>
790 >
791 <div
792 <?php
793 //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_attributes() handles escaping internally
794 echo $this->render_attributes('wrapper');
795 ?>
796 >
797 <div
798 class="<?php echo esc_attr(implode(' ', $productWrapperClasses)); ?>"
799 data-fluent-cart-shop-app
800 data-fluent-cart-product-wrapper
801 >
802 <!-- View switcher -->
803 <?php
804 if ($showViewSwitcher) {
805 $shopAppRenderer->renderViewSwitcher();
806 }
807 ?>
808
809 <!-- Products Container -->
810 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
811 <!-- Filter render here -->
812 <?php
813 if ($enableFilter) {
814 $productFilterRenderer = new ProductFilterRender($filters);
815 $shopAppRenderer->renderFilter($productFilterRenderer);
816 }
817 ?>
818
819 <!-- Products -->
820 <div
821 data-fluent-cart-shop-app-product-list
822 class="fct-products-container"
823 >
824 <?php $this->renderProducts($products); ?>
825 </div>
826
827 <div class="fluent-cart-product-loader loader-hidden" data-fluent-cart-product-loader>
828 <div class="fluent-cart-product-spinner"></div>
829 </div>
830
831 </div>
832
833 <!-- Pagination -->
834 <?php
835 if ($paginationType !== 'scroll') {
836 $this->renderPagination(
837 $products,
838 $defaultFilters,
839 $paginationType,
840 $perPage,
841 $viewMode
842 );
843 }
844 ?>
845
846
847 </div>
848 </div>
849 </div>
850
851 <?php
852 }
853
854 /**
855 * Store settings in transient.
856 */
857 private function storeSettingsTransient($settings)
858 {
859 $uuid = 'fc_bx_collection_' . $this->uid;
860 if (!get_transient($uuid)) {
861 // save the settings as transient
862 set_transient($uuid, $settings, 48 * HOUR_IN_SECONDS);
863 }
864 }
865
866 /**
867 * Build query args.
868 */
869 private function buildQueryArgs($settings, $isMainQuery, $perPage, $viewMode) {
870 $args = array_filter([
871 'paginate' => 'simple',
872 'is_main_query' => $isMainQuery,
873 'sort_by' => Arr::get($settings, 'orderby', 'date'),
874 'sort_type' => Arr::get($settings, 'order', 'desc'),
875 'per_page' => $perPage,
876 'view_mode' => $viewMode,
877 ]);
878
879 if (!$isMainQuery) {
880 $includeIds = Arr::get($settings, 'include', []);
881 if ($includeIds) {
882 $args['include_ids'] = $includeIds;
883 }
884
885 $excludeIds = Arr::get($settings, 'exclude', []);
886 if ($excludeIds) {
887 $args['exclude_ids'] = $excludeIds;
888 }
889
890 $productType = Arr::get($settings, 'productType', []);
891 if ($productType) {
892 $args['product_type'] = $productType;
893 }
894
895 $onSale = Arr::get($settings, 'onSale', false);
896
897 if ($onSale) {
898 $args['on_sale'] = true;
899 }
900
901 $allowOutOfStock = Arr::get($settings, 'allowOutOfStock', false);
902 if ($allowOutOfStock) {
903 $args['allow_out_of_stock'] = true;
904 }
905
906 $categories = Arr::get($settings, 'categories', []);
907 if ($categories) {
908 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
909 $args['tax_query'] = [
910 'product-categories' => $categories,
911 ];
912 }
913
914 if (!empty($settings['defaultFilterEnabled'])) {
915 if (!empty($settings['defaultFilterAllowOutOfStock'])) {
916 $args['allow_out_of_stock'] = true;
917 }
918
919 $wildcard = trim((string) Arr::get($settings, 'defaultFilterWildcard', ''));
920 if ($wildcard !== '') {
921 $args['search'] = sanitize_text_field($wildcard);
922 }
923
924 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
925 $taxQuery = Arr::get($args, 'tax_query', []);
926 foreach (Taxonomy::getTaxonomies() as $taxonomy) {
927 $controlKey = sanitize_key(str_replace('-', '_', $taxonomy));
928 $selectedTerms = (array) Arr::get($settings, 'defaultFilterTaxonomy_' . $controlKey, []);
929 if (empty($selectedTerms)) {
930 continue;
931 }
932
933 // bricks_get_terms_options values are shaped "{taxonomy}::{term_id}",
934 // not plain term IDs — strip the prefix before use.
935 $termIds = array_values(array_filter(array_map(function ($value) {
936 $parts = explode('::', (string) $value);
937 return sanitize_key(end($parts));
938 }, $selectedTerms)));
939
940 if (!empty($termIds)) {
941 $existing = Arr::get($taxQuery, $taxonomy, []);
942 $taxQuery[$taxonomy] = array_values(array_unique(array_merge((array) $existing, $termIds)));
943 }
944 }
945 if (!empty($taxQuery)) {
946 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
947 $args['tax_query'] = $taxQuery;
948 }
949 }
950 }
951
952 return $args;
953 }
954
955 /**
956 * Render product loop.
957 */
958
959 private function renderProducts($products) {
960 ProductDataSetup::setProductsCache($products);
961
962 $postIndex = 1;
963
964 foreach ($products as $product) {
965 $post = get_post($product->ID);
966
967 setup_postdata($post);
968
969 $this->set_loop_object($post);
970
971 $this->render_fields($post, $postIndex);
972
973 $this->next_iteration();
974
975 $postIndex++;
976 }
977
978 wp_reset_postdata();
979
980 $this->end_iteration();
981 }
982
983 /**
984 * Render pagination.
985 */
986 private function renderPagination(
987 $products,
988 $defaultFilters,
989 $paginationType,
990 $perPage,
991 $viewMode
992 ) {
993 $renderer = new ShopAppRenderer(
994 [
995 'products' => $products,
996 'total' => $products->total(),
997 ],
998 [
999 'default_filters' => $defaultFilters,
1000 'pagination_type' => $paginationType,
1001 'per_page' => $perPage,
1002 'view_mode' => $viewMode,
1003 ]
1004 );
1005
1006 $renderer->renderPaginator();
1007 }
1008
1009 /**
1010 * Get available product filters.
1011 */
1012 private function getFilters(array $settings): array
1013 {
1014 $filters = [];
1015 $taxonomies = Taxonomy::getTaxonomies();
1016
1017 foreach ($taxonomies as $taxonomy) {
1018 $controlKey = sanitize_key(str_replace('-', '_', $taxonomy));
1019 $defaultLabel = Str::headline($taxonomy);
1020
1021 $enabledKey = 'taxonomy_' . $controlKey;
1022 $enabled = array_key_exists($enabledKey, $settings)
1023 ? !empty($settings[$enabledKey])
1024 : !empty($settings[$this->legacyControlKey($taxonomy)]);
1025
1026 $labelKey = 'displayName_' . $controlKey;
1027 $label = array_key_exists($labelKey, $settings)
1028 ? ($settings[$labelKey] !== '' ? $settings[$labelKey] : $defaultLabel)
1029 : (!empty($settings[$this->legacyDisplayNameKey($taxonomy)])
1030 ? $settings[$this->legacyDisplayNameKey($taxonomy)]
1031 : $defaultLabel);
1032
1033 $showEmptyKey = 'showEmpty_' . $controlKey;
1034 $showEmpty = array_key_exists($showEmptyKey, $settings)
1035 ? !empty($settings[$showEmptyKey])
1036 : !empty($settings[$this->legacyShowEmptyKey($taxonomy)]);
1037
1038 $filters[$taxonomy] = [
1039 'filter_type' => 'options',
1040 'is_meta' => true,
1041 'label' => $label,
1042 'enabled' => $enabled,
1043 'multiple' => false,
1044 'show_empty' => $showEmpty,
1045 ];
1046 }
1047
1048 $priceRangeLabel = !empty($settings['displayNamePriceRange']) ? $settings['displayNamePriceRange'] : __('Price', 'fluent-cart');
1049 $filters['price_range'] = [
1050 'filter_type' => 'range',
1051 'is_meta' => false,
1052 'label' => $priceRangeLabel,
1053 'enabled' => !empty($settings['priceRange']),
1054 ];
1055
1056 return $filters;
1057 }
1058
1059 private function legacyControlKey($taxonomy)
1060 {
1061 $map = [
1062 'product-categories' => 'productCategories',
1063 'product-brands' => 'productBrands',
1064 ];
1065 return Arr::get($map, $taxonomy, '');
1066 }
1067
1068 private function legacyDisplayNameKey($taxonomy)
1069 {
1070 $map = [
1071 'product-categories' => 'displayNameCategories',
1072 'product-brands' => 'displayNameBrands',
1073 ];
1074 return Arr::get($map, $taxonomy, '');
1075 }
1076
1077 private function legacyShowEmptyKey($taxonomy)
1078 {
1079 $map = [
1080 'product-categories' => 'showEmptyCategories',
1081 'product-brands' => 'showEmptyBrands',
1082 ];
1083 return Arr::get($map, $taxonomy, '');
1084 }
1085
1086 private function setBricksQuery()
1087 {
1088 $query_object = new Query(
1089 [
1090 'id' => $this->id,
1091 'name' => $this->name,
1092 'settings' => $this->settings,
1093 ]
1094 );
1095
1096 // Set $bricks_query (@since 1.10.2)
1097 $this->set_bricks_query($query_object);
1098 $this->start_iteration();
1099 }
1100
1101 public function render_fields($post, $post_index)
1102 {
1103 BricksHelper::renderCollectionCard($this->settings, $post, $post_index, $this->uid);
1104 }
1105
1106 public function renderAjaxContents($products, $settings)
1107 {
1108
1109 $this->settings = $settings;
1110
1111 $this->setBricksQuery();
1112
1113 ProductDataSetup::setProductsCache($products);
1114 $postIndex = 1;
1115 foreach ($products as $product) {
1116 $post = get_post($product->ID);
1117 setup_postdata($post);
1118 $this->set_loop_object($post);
1119 $this->render_fields($post, $postIndex);
1120 $this->next_iteration();
1121 $postIndex++;
1122 }
1123 wp_reset_postdata();
1124
1125 $this->end_iteration();
1126 }
1127
1128 private function getAjaxDefaultFilters(array $defaultFilters, array $settings): array
1129 {
1130 $filters = [];
1131 $taxQuery = Arr::get($defaultFilters, 'tax_query', []);
1132
1133 if (!empty($taxQuery)) {
1134 foreach ($taxQuery as $taxonomy => $termIds) {
1135 $filters[$taxonomy] = $termIds;
1136 }
1137 }
1138
1139 $wildcard = Arr::get($defaultFilters, 'search', '');
1140 if ($wildcard !== '') {
1141 $filters['wildcard'] = $wildcard;
1142 }
1143
1144 $allowOutOfStock = !empty($settings['allowOutOfStock']) || (
1145 !empty($settings['defaultFilterEnabled']) &&
1146 !empty($settings['defaultFilterAllowOutOfStock'])
1147 );
1148 if ($allowOutOfStock) {
1149 $filters['allow_out_of_stock'] = true;
1150 }
1151
1152 if (!empty($filters)) {
1153 $filters['enabled'] = true;
1154 }
1155
1156 return $filters;
1157 }
1158
1159 }
1160