control_groups['query'] = [ 'title' => esc_html__('Query', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['fields'] = [ 'title' => esc_html__('Fields', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['display'] = [ 'title' => esc_html__('Display', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['filter'] = [ 'title' => esc_html__('Filter', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['defaultFilter'] = [ 'title' => esc_html__('Default Filter', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['widgets'] = [ 'title' => esc_html__('Widgets', 'fluent-cart'), 'tab' => 'widgets', ]; $this->control_groups['sale_badge'] = [ 'title' => esc_html__('Sale Badge', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['sale_badge_style'] = [ 'title' => esc_html__('Sale Badge', 'fluent-cart'), 'tab' => 'style', ]; $this->control_groups['sold_out_badge'] = [ 'title' => esc_html__('Sold Out Badge', 'fluent-cart'), 'tab' => 'content', ]; $this->control_groups['sold_out_badge_style'] = [ 'title' => esc_html__('Sold Out Badge', 'fluent-cart'), 'tab' => 'style', ]; } public function set_controls() { // LAYOUT $this->controls['viewMode'] = [ 'tab' => 'content', 'group' => 'display', 'label' => esc_html__('View Mode', 'fluent-cart'), 'type' => 'select', 'options' => [ 'grid' => esc_html__('Grid', 'fluent-cart'), 'list' => esc_html__('List', 'fluent-cart'), ], 'placeholder' => esc_html__('Grid', 'fluent-cart'), 'rerender' => true, ]; $this->controls['showViewSwitcher'] = [ 'tab' => 'content', 'group' => 'display', 'label' => esc_html__('Show View Switcher', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'default' => true, ]; $this->controls['paginationType'] = [ 'tab' => 'content', 'group' => 'display', 'label' => esc_html__('Pagination Type', 'fluent-cart'), 'type' => 'select', 'options' => [ 'scroll' => esc_html__('Scroll', 'fluent-cart'), 'numbers' => esc_html__('Numbers', 'fluent-cart'), ], 'placeholder' => esc_html__('Numbers', 'fluent-cart'), ]; $this->controls['columns'] = [ 'tab' => 'content', 'label' => esc_html__('Columns', 'fluent-cart'), 'type' => 'number', 'min' => 1, 'max' => 5, 'breakpoints' => true, 'placeholder' => 4, 'css' => [ [ 'selector' => $this->cssRoot, 'property' => '--grid-columns', ], ], ]; $this->controls['gap'] = [ 'tab' => 'content', 'label' => esc_html__('Gap', 'fluent-cart'), 'type' => 'number', 'units' => true, 'css' => [ [ 'selector' => $this->cssRoot, 'property' => 'gap', ], ], 'placeholder' => 30, ]; $this->controls['posts_per_page'] = [ 'tab' => 'content', 'label' => esc_html__('Products per page', 'fluent-cart'), 'type' => 'number', 'min' => 1, 'max' => 100, 'placeholder' => 10, 'step' => 1, ]; $this->controls['is_main_query'] = [ 'tab' => 'content', 'label' => esc_html__('Is main query', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, ]; // QUERY $this->controls['orderby'] = [ 'tab' => 'content', 'label' => esc_html__('Order by', 'fluent-cart'), 'type' => 'select', // id|date|title|price 'options' => [ 'price' => esc_html__('Price', 'fluent-cart'), 'title' => esc_html__('Product Name', 'fluent-cart'), 'date' => esc_html__('Published date', 'fluent-cart'), 'id' => esc_html__('Product ID', 'fluent-cart') ], 'inline' => true, 'placeholder' => esc_html__('Default', 'fluent-cart'), ]; $this->controls['order'] = [ 'tab' => 'content', 'label' => esc_html__('Order', 'fluent-cart'), 'type' => 'select', 'options' => [ 'ASC' => esc_html__('Ascending', 'fluent-cart'), 'DESC' => esc_html__('Descending', 'fluent-cart'), ], 'inline' => true, 'placeholder' => esc_html__('Descending', 'fluent-cart'), ]; $this->controls['main_query_info'] = [ 'tab' => 'content', 'group' => 'query', 'type' => 'info', 'content' => esc_html__('The query settings will be ignored when Is main query is enabled.', 'fluent-cart'), ]; $this->controls['productType'] = [ 'tab' => 'content', 'group' => 'query', 'label' => esc_html__('Product type', 'fluent-cart'), 'type' => 'select', // physical|digital|subscription|onetime|simple|variations 'options' => [ 'simple' => esc_html__('Simple', 'fluent-cart'), 'physical' => esc_html__('Physical', 'fluent-cart'), 'digital' => esc_html__('Digital', 'fluent-cart'), 'variations' => esc_html__('Variations', 'fluent-cart'), 'ontime' => esc_html__('One-time', 'fluent-cart'), 'subscription' => esc_html__('Subscriptions', 'fluent-cart'), ], 'multiple' => false, 'placeholder' => esc_html__('all product types', 'fluent-cart'), ]; $this->controls['include'] = [ 'tab' => 'content', 'group' => 'query', 'label' => esc_html__('Include', 'fluent-cart'), 'type' => 'select', 'optionsAjax' => [ 'action' => 'bricks_get_posts', 'postType' => 'fluent-products', ], 'multiple' => true, 'searchable' => true, 'placeholder' => esc_html__('Select products', 'fluent-cart'), ]; //phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude $this->controls['exclude'] = [ 'tab' => 'content', 'group' => 'query', 'label' => esc_html__('Exclude', 'fluent-cart'), 'type' => 'select', 'optionsAjax' => [ 'action' => 'bricks_get_posts', 'postType' => 'fluent-products', ], 'multiple' => true, 'searchable' => true, 'placeholder' => esc_html__('Select products', 'fluent-cart'), ]; $this->controls['categories'] = [ 'tab' => 'content', 'group' => 'query', 'label' => esc_html__('Product categories', 'fluent-cart'), 'type' => 'select', 'options' => BricksHelper::getCategoriesOptions(), 'multiple' => true, ]; $this->controls['onSale'] = [ 'tab' => 'content', 'group' => 'query', 'label' => esc_html__('On sale Products only', 'fluent-cart'), 'type' => 'checkbox', ]; $this->controls['allowOutOfStock'] = [ 'tab' => 'content', 'group' => 'query', 'label' => esc_html__('Allow Out Of Stock', 'fluent-cart'), 'type' => 'checkbox', ]; // FILTER $this->controls['enableFilter'] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Enable Filter', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, ]; $this->controls['enableSortBy'] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Enable Sort By', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'default' => true, 'required' => ['enableFilter', '=', true], ]; $this->controls['liveFilter'] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Live Filter', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'required' => ['enableFilter', '=', true], ]; $this->controls['wildcardFilter'] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Wildcard Filter', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'required' => ['enableFilter', '=', true], ]; $taxonomies = Taxonomy::getTaxonomies(); foreach ($taxonomies as $taxonomy) { $controlKey = sanitize_key(str_replace('-', '_', $taxonomy)); $label = esc_html(Str::headline($taxonomy)); $taxonomyName = esc_html(str_replace('product-', '', $taxonomy)); $this->controls['taxonomy_' . $controlKey] = [ 'tab' => 'content', 'group' => 'filter', 'label' => $label, 'type' => 'checkbox', 'inline' => true, 'required' => ['enableFilter', '=', true], ]; $this->controls['displayName_' . $controlKey] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Display Name', 'fluent-cart'), 'type' => 'text', 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'), 'required' => [ ['enableFilter', '=', true], ['taxonomy_' . $controlKey, '=', true], ], ]; /* translators: %1$s: taxonomy name (e.g. "categories", "brands") */ $this->controls['showEmpty_' . $controlKey] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Show empty', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'description' => sprintf(esc_html__('Display %1$s even if they have no products.', 'fluent-cart'), $taxonomyName), 'required' => [ ['enableFilter', '=', true], ['taxonomy_' . $controlKey, '=', true], ], ]; } $this->controls['priceRange'] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__( 'Price Range', 'fluent-cart' ), 'type' => 'checkbox', 'inline' => true, 'required' => ['enableFilter', '=', true], ]; $this->controls['displayNamePriceRange'] = [ 'tab' => 'content', 'group' => 'filter', 'label' => esc_html__('Display Name', 'fluent-cart'), 'type' => 'text', 'placeholder' => esc_html__('Custom filter label', 'fluent-cart'), 'required' => [ ['enableFilter', '=', true], ['priceRange', '=', true], ], ]; // DEFAULT FILTER $this->controls['defaultFilterEnabled'] = [ 'tab' => 'content', 'group' => 'defaultFilter', 'label' => esc_html__('Enable Default Filter', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, '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'), ]; $this->controls['defaultFilterAllowOutOfStock'] = [ 'tab' => 'content', 'group' => 'defaultFilter', 'label' => esc_html__('Allow Out Of Stock', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'required' => ['defaultFilterEnabled', '=', true], ]; $this->controls['defaultFilterWildcard'] = [ 'tab' => 'content', 'group' => 'defaultFilter', 'label' => esc_html__('Search', 'fluent-cart'), 'type' => 'text', 'placeholder' => esc_html__('Preset search term', 'fluent-cart'), 'required' => ['defaultFilterEnabled', '=', true], ]; foreach ($taxonomies as $taxonomy) { $controlKey = sanitize_key(str_replace('-', '_', $taxonomy)); $label = esc_html(Str::headline($taxonomy)); // Bricks' built-in terms AJAX endpoint (bounded to 100 results per // request, server-side searchable) — avoids eagerly loading and // flattening every term of every taxonomy on control registration. $this->controls['defaultFilterTaxonomy_' . $controlKey] = [ 'tab' => 'content', 'group' => 'defaultFilter', 'label' => $label, 'type' => 'select', 'multiple' => true, 'searchable' => true, 'optionsAjax' => [ 'action' => 'bricks_get_terms_options', 'postTypes' => ['fluent-products'], 'taxonomy' => [$taxonomy], ], 'placeholder' => esc_html__('Select terms', 'fluent-cart'), 'required' => ['defaultFilterEnabled', '=', true], ]; } // FIELDS $fields = $this->get_post_fields(); // Remove field settings unset($fields['fields']['fields']['overlay']); unset($fields['fields']['fields']['dynamicPadding']); unset($fields['fields']['fields']['dynamicBackground']); unset($fields['fields']['fields']['dynamicBorder']); // Set fields defaults fields set $fields['fields']['default'] = [ [ 'dynamicData' => '{fct_product_image:link}', 'id' => Helpers::generate_random_id(false), ], [ 'dynamicData' => '{fct_product_title:linked}', 'tag' => 'h5', 'id' => Helpers::generate_random_id(false), ], [ 'dynamicData' => '{fct_product_excerpt}', 'tag' => 'p', 'id' => Helpers::generate_random_id(false), ], [ 'dynamicData' => '{fct_product_price}', 'id' => Helpers::generate_random_id(false), ], [ 'dynamicData' => '{fct_product_button}', 'id' => Helpers::generate_random_id(false), ] ]; $this->controls = array_replace_recursive($this->controls, $fields); $this->controls['linkProduct'] = [ 'tab' => 'content', 'group' => 'fields', 'label' => esc_html__('Link entire product', 'fluent-cart'), 'type' => 'checkbox', 'inline' => true, 'description' => esc_html__('Only added if none of your product fields contains any links.', 'fluent-cart'), ]; // SALE BADGE $this->controls['showSaleBadge'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'checkbox', 'label' => esc_html__('Show Sale Badge', 'fluent-cart'), 'rerender' => true, ]; $this->controls['saleBadgeText'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'text', 'label' => esc_html__('Badge Text', 'fluent-cart'), 'placeholder' => esc_html__('Sale!', 'fluent-cart'), 'default' => esc_html__('Sale!', 'fluent-cart'), 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgeShowPercentage'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'checkbox', 'label' => esc_html__('Show discount percentage instead', 'fluent-cart'), 'rerender' => true, 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgePercentageText'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'text', 'label' => esc_html__('Percentage Text', 'fluent-cart'), 'description' => esc_html__('Use {percent} as a placeholder for the discount amount, e.g. -{percent}%', 'fluent-cart'), 'placeholder' => '-{percent}%', 'default' => '-{percent}%', 'required' => [['showSaleBadge', '=', true], ['saleBadgeShowPercentage', '=', true]], ]; $this->controls['saleBadgePriceSource'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'select', 'label' => esc_html__('Price Source', 'fluent-cart'), 'options' => [ 'default_variant' => esc_html__('Default Variant', 'fluent-cart'), 'best_discount' => esc_html__('Best Discount (All Variants)', 'fluent-cart'), ], 'default' => 'default_variant', 'inline' => true, 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgeShape'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'select', 'label' => esc_html__('Badge Shape', 'fluent-cart'), 'options' => [ 'badge' => esc_html__('Badge', 'fluent-cart'), 'ribbon' => esc_html__('Ribbon', 'fluent-cart'), ], 'default' => 'badge', 'inline' => true, 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgePosition'] = [ 'tab' => 'content', 'group' => 'sale_badge', 'type' => 'select', 'label' => esc_html__('Position', 'fluent-cart'), 'options' => [ 'top-left' => esc_html__('Top Left', 'fluent-cart'), 'top-right' => esc_html__('Top Right', 'fluent-cart'), 'bottom-left' => esc_html__('Bottom Left', 'fluent-cart'), 'bottom-right' => esc_html__('Bottom Right', 'fluent-cart'), ], 'default' => 'top-left', 'inline' => true, 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgeTypography'] = [ 'tab' => 'style', 'group' => 'sale_badge_style', 'type' => 'typography', 'label' => esc_html__('Typography', 'fluent-cart'), 'css' => [ ['property' => 'font', 'selector' => '.fct-sale-badge'], ], 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgeBackgroundColor'] = [ 'tab' => 'style', 'group' => 'sale_badge_style', 'type' => 'color', 'label' => esc_html__('Background Color', 'fluent-cart'), 'css' => [ ['property' => 'background-color', 'selector' => '.fct-sale-badge'], ], 'required' => ['showSaleBadge', '=', true], ]; $this->controls['saleBadgeTextColor'] = [ 'tab' => 'style', 'group' => 'sale_badge_style', 'type' => 'color', 'label' => esc_html__('Text Color', 'fluent-cart'), 'css' => [ ['property' => 'color', 'selector' => '.fct-sale-badge'], ], 'required' => ['showSaleBadge', '=', true], ]; // SOLD OUT BADGE $this->controls['showSoldOutBadge'] = [ 'tab' => 'content', 'group' => 'sold_out_badge', 'type' => 'checkbox', 'label' => esc_html__('Show Sold Out Badge', 'fluent-cart'), 'rerender' => true, ]; $this->controls['soldOutBadgeText'] = [ 'tab' => 'content', 'group' => 'sold_out_badge', 'type' => 'text', 'label' => esc_html__('Badge Text', 'fluent-cart'), 'placeholder' => esc_html__('Out of Stock', 'fluent-cart'), 'default' => esc_html__('Out of Stock', 'fluent-cart'), 'required' => ['showSoldOutBadge', '=', true], ]; $this->controls['soldOutBadgeShape'] = [ 'tab' => 'content', 'group' => 'sold_out_badge', 'type' => 'select', 'label' => esc_html__('Badge Shape', 'fluent-cart'), 'options' => [ 'badge' => esc_html__('Badge', 'fluent-cart'), 'ribbon' => esc_html__('Ribbon', 'fluent-cart'), ], 'default' => 'badge', 'inline' => true, 'required' => ['showSoldOutBadge', '=', true], ]; $this->controls['soldOutBadgePosition'] = [ 'tab' => 'content', 'group' => 'sold_out_badge', 'type' => 'select', 'label' => esc_html__('Position', 'fluent-cart'), 'options' => [ 'top-left' => esc_html__('Top Left', 'fluent-cart'), 'top-right' => esc_html__('Top Right', 'fluent-cart'), 'bottom-left' => esc_html__('Bottom Left', 'fluent-cart'), 'bottom-right' => esc_html__('Bottom Right', 'fluent-cart'), ], 'default' => 'top-left', 'inline' => true, 'required' => ['showSoldOutBadge', '=', true], ]; $this->controls['soldOutBadgeTypography'] = [ 'tab' => 'style', 'group' => 'sold_out_badge_style', 'type' => 'typography', 'label' => esc_html__('Typography', 'fluent-cart'), 'css' => [ ['property' => 'font', 'selector' => '.fct-sold-out-badge'], ], 'required' => ['showSoldOutBadge', '=', true], ]; $this->controls['soldOutBadgeBackgroundColor'] = [ 'tab' => 'style', 'group' => 'sold_out_badge_style', 'type' => 'color', 'label' => esc_html__('Background Color', 'fluent-cart'), 'css' => [ ['property' => 'background-color', 'selector' => '.fct-sold-out-badge'], ], 'required' => ['showSoldOutBadge', '=', true], ]; $this->controls['soldOutBadgeTextColor'] = [ 'tab' => 'style', 'group' => 'sold_out_badge_style', 'type' => 'color', 'label' => esc_html__('Text Color', 'fluent-cart'), 'css' => [ ['property' => 'color', 'selector' => '.fct-sold-out-badge'], ], 'required' => ['showSoldOutBadge', '=', true], ]; } public function render() { $settings = $this->settings; $this->setBricksQuery(); $viewMode = Arr::get($settings, 'viewMode', 'grid'); $showViewSwitcher = !empty($settings['showViewSwitcher']); $paginationType = Arr::get($settings, 'paginationType', 'numbers'); $isMainQuery = Arr::get($settings, 'is_main_query', false) && $this->is_frontend; $perPage = (int)Arr::get($settings, 'posts_per_page', 10); $enableFilter = !empty($settings['enableFilter']); $priceRange = !empty($settings['priceRange']); $liveFilter = !empty($settings['liveFilter']); $this->storeSettingsTransient($settings); if ($perPage <= 0) { $perPage = 10; } if ($perPage > 100) { $perPage = 100; } $args = $this->buildQueryArgs( $settings, $isMainQuery, $perPage, $viewMode ); $productsQuery = (new ProductQuery($args)); $products = $productsQuery->get(); $defaultFilters = $productsQuery->getDefaultFilters(); $ajaxDefaultFilters = $this->getAjaxDefaultFilters( $defaultFilters, $settings ); $filters = $this->getFilters($settings); $wrapperClass = 'fct-products-wrapper-inner ' . ($viewMode === 'list' ? 'mode-list' : 'mode-grid') . (!$enableFilter ? ' fct-full-container-width' : ''); $wrapperAttributes = [ 'class' => $wrapperClass, 'data-fluent-cart-product-wrapper-inner' => '', 'data-per-page' => $perPage, 'data-order-type' => Arr::get($defaultFilters, 'sort_type'), 'data-live-filter' => $liveFilter, 'data-paginator' => esc_attr($paginationType), 'data-default-filters' => wp_json_encode($ajaxDefaultFilters) ]; // Persist the element's query restrictions so the AJAX round-trip // (search, sort, pagination) re-applies them. Without these the // Paginator sends no include/exclude/type/on-sale params and the // server falls back to an unrestricted query — e.g. an excluded // product reappears as soon as the visitor searches for it. $includeIds = Arr::get($args, 'include_ids', []); if (!empty($includeIds)) { $wrapperAttributes['data-include-ids'] = wp_json_encode(array_map('intval', (array) $includeIds)); } $excludeIds = Arr::get($args, 'exclude_ids', []); if (!empty($excludeIds)) { $wrapperAttributes['data-exclude-ids'] = wp_json_encode(array_map('intval', (array) $excludeIds)); } $productTypeArg = Arr::get($args, 'product_type', ''); if (!empty($productTypeArg)) { $wrapperAttributes['data-product-type'] = is_array($productTypeArg) ? implode(',', $productTypeArg) : $productTypeArg; } if (Arr::get($args, 'on_sale', false)) { $wrapperAttributes['data-on-sale'] = '1'; } $productWrapperClasses = [ 'fct-products-wrapper', 'fct-brick-products-wrapper', ]; if (!$this->is_frontend) { $productWrapperClasses[] = 'fct-bricks-editor-mode'; } $rendererConfig = [ 'view_mode' => $viewMode, 'enable_wildcard_filter' => !empty($settings['wildcardFilter']), 'custom_filters' => [ 'enabled' => $enableFilter, 'price_range' => $priceRange, 'live_filter' => $liveFilter ], ]; // Only override enable_sort_by if explicitly set in settings (key exists) if (array_key_exists('enableSortBy', $settings)) { $rendererConfig['enable_sort_by'] = !empty($settings['enableSortBy']); } $shopAppRenderer = new ShopAppRenderer($products, $rendererConfig); ?>