PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 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 All 48 releases
← All changes | app/Services/Renderer/ProductCardRender.php +159 -29 1.3.21 → 1.6.5 View file →
@@ -36,8 +36,21 @@
36 36
37 37 public function render()
38 38 {
39 39 AssetLoader::loadSingleProductAssets();
40 + // Lets extensions (e.g. Pro's advanced-variation selector) enqueue the
41 + // CSS/JS a product purchase UI needs. Backs the card surfaces that go
42 + // through this renderer's render() — Shop, Product List, the product
43 + // shortcode, and the quick-view modal. Surfaces that render card parts
44 + // via the individual render* methods instead of render() (Product
45 + // Carousel, Related Products) fire this same hook from their own block
46 + // render. Guarded with a static flag so a grid of N cards fires it once
47 + // per request, not once per card. No payload — assets are uniform.
48 + static $assetsHookFired = false;
49 + if (!$assetsHookFired) {
50 + $assetsHookFired = true;
51 + do_action('fluent_cart/advanced_variation/enqueue_assets');
52 + }
40 53 $cursor = '';
41 54 if (!empty($this->config['cursor'])) {
42 55 $cursor = 'data-fluent-cart-cursor="' . esc_attr($this->config['cursor']) . '"';
43 56 }
@@ -49,11 +62,24 @@
49 62 $cardWidth = 'style="width: ' . esc_attr($widthValue) . ';"';
50 63 }
51 64
52 65
66 + // Card wrapper classes are filterable so an integration can tag cards
67 + // (sale, low-stock, layout variants) without wrapping or re-rendering
68 + // the whole card. Sanitised in RenderGate, so listeners may return a
69 + // plain array of class names.
70 + $cardClasses = RenderGate::cardClasses(['fct-product-card'], [
71 + 'product' => $this->product,
72 + 'scope' => RenderGate::SCOPE_CARD,
73 + ]);
74 +
75 + do_action('fluent_cart/product/group/before_card', RenderContext::decorate([
76 + 'product' => $this->product,
77 + 'scope' => RenderGate::SCOPE_CARD,
78 + ]));
53 79 ?>
54 80 <article data-fluent-cart-shop-app-single-product data-fct-product-card=""
55 - class="fct-product-card"
81 + class="<?php echo esc_attr($cardClasses); ?>"
56 82 <?php echo $cursor; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped ?>
57 83 <?php echo $cardWidth; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped ?>
58 84 aria-label="<?php echo esc_attr(sprintf(
59 85 /* translators: %s: product title */
@@ -60,15 +86,56 @@
60 86 __('%s product card', 'fluent-cart'), $this->product->post_title));
61 87 ?>">
62 88 <?php $this->renderProductImage(); ?>
63 89 <?php $this->renderTitle(); ?>
64 - <?php $this->renderExcerpt(); ?>
90 + <?php if (!Arr::get($this->config, 'hide_excerpt', false)) { $this->renderExcerpt(); } ?>
65 91 <?php $this->renderPrices(); ?>
66 92 <?php $this->showBuyButton(); ?>
67 93 </article>
68 94 <?php
95 + do_action('fluent_cart/product/group/after_card', RenderContext::decorate([
96 + 'product' => $this->product,
97 + 'scope' => RenderGate::SCOPE_CARD,
98 + ]));
69 99 }
70 100
101 + /**
102 + * @deprecated Use PackageDescriptionRenderer::buildPackageInfoFromOtherInfo() directly.
103 + * Kept as a compatibility shim for external callers (themes, extensions).
104 + *
105 + * @param array $otherInfo Order item's other_info array
106 + * @return string e.g. "Gift (Box) · 30 × 20 × 15 cm · Wt: 2 kg · Shipping: 2.5 kg"
107 + */
108 + public static function buildPackageInfoFromOtherInfo($otherInfo)
109 + {
110 + return PackageDescriptionRenderer::buildPackageInfoFromOtherInfo($otherInfo);
111 + }
112 +
113 + /**
114 + * @deprecated Use PackageDescriptionRenderer::renderPackageDescription() directly.
115 + * Kept as a compatibility shim for external callers (themes, extensions) —
116 + * package-description rendering now lives in PackageDescriptionRenderer.
117 + */
118 + public function renderPackageDescription(
119 + $wrapper_attributes = '',
120 + $showName = true,
121 + $showDimensions = true,
122 + $showProductWeight = true,
123 + $showTotalWeight = true,
124 + $variant = null,
125 + $defaultVariant = null
126 + ) {
127 + (new PackageDescriptionRenderer($this->product))->renderPackageDescription(
128 + $wrapper_attributes,
129 + $showName,
130 + $showDimensions,
131 + $showProductWeight,
132 + $showTotalWeight,
133 + $variant,
134 + $defaultVariant
135 + );
136 + }
137 +
71 138 public function renderExcerpt($atts = '')
72 139 {
73 140 if (empty($this->product->post_excerpt)) {
74 141 return;
@@ -73,8 +140,16 @@
73 140 if (empty($this->product->post_excerpt)) {
74 141 return;
75 142 }
76 143
144 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
145 +
146 + if (!RenderGate::shouldRender('excerpt', $gateContext)) {
147 + return;
148 + }
149 +
150 + do_action('fluent_cart/product/group/before_excerpt_block', $gateContext);
151 +
77 152 echo sprintf(
78 153 '<p %1$s class="fct-product-card-excerpt">
79 154 %2$s
80 155 </p>',
@@ -80,12 +155,22 @@
80 155 </p>',
81 156 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
82 157 wp_kses_post($this->product->post_excerpt),
83 158 );
159 +
160 + do_action('fluent_cart/product/group/after_excerpt_block', $gateContext);
84 161 }
85 162
86 163 public function renderTitle($atts = '', $config = [])
87 164 {
165 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
166 +
167 + if (!RenderGate::shouldRender('title', $gateContext)) {
168 + return;
169 + }
170 +
171 + do_action('fluent_cart/product/group/before_title_block', $gateContext);
172 +
88 173 $link = Arr::get($config, 'isLink', true);
89 174 $target = Arr::get($config, 'target', '_self');
90 175
91 176 $titleText = esc_html($this->product->post_title);
@@ -115,12 +200,18 @@
115 200 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
116 201 esc_html($titleText)
117 202 );
118 203 }
204 +
205 + do_action('fluent_cart/product/group/after_title_block', $gateContext);
119 206 }
120 207
121 208 public function renderProductImage()
122 209 {
210 + if (!RenderGate::shouldRender('image', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
211 + return;
212 + }
213 +
123 214 $image = $this->product->thumbnail;
124 215 $isPlaceholder = false;
125 216
126 217 if (!$image) {
@@ -133,12 +224,12 @@
133 224 /* translators: %s: product title */
134 225 __('Placeholder image for %s', 'fluent-cart'), $this->product->post_title)
135 226 : $this->product->post_title;
136 227
137 - do_action('fluent_cart/product/group/before_image_block', [
228 + do_action('fluent_cart/product/group/before_image_block', RenderContext::decorate([
138 229 'product' => $this->product,
139 230 'scope' => 'product_card'
140 - ]);
231 + ]));
141 232 ?>
142 233 <a class="fct-product-card-image-wrap"
143 234 href="<?php echo esc_url($this->viewUrl); ?>"
144 235 style="display: block;"
@@ -154,21 +245,26 @@
154 245 height="300"/>
155 246 </a>
156 247 <?php
157 248
158 - do_action('fluent_cart/product/group/after_image_block', [
249 + do_action('fluent_cart/product/group/after_image_block', RenderContext::decorate([
159 250 'product' => $this->product,
160 251 'scope' => 'product_card'
161 - ]);
252 + ]));
162 253 }
163 254
164 255 public function renderPrices($wrapper_attributes = '')
165 256 {
257 + if (!RenderGate::shouldRender('price', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
258 + return;
259 + }
260 +
166 261 $priceFormat = Arr::get($this->config, 'price_format', 'starts_from');
167 262 $isSimple = $this->product->detail->variation_type === 'simple';
168 263 $minPrice = $this->product->detail->min_price;
169 264 $maxPrice = $this->product->detail->max_price;
170 265 $comparePrice = 0;
266 + $firstVariant = null;
171 267
172 268 if ($isSimple) {
173 269 $firstVariant = $this->product->variants->first();
174 270 if ($firstVariant) {
@@ -187,13 +283,13 @@
187 283 $formattedMinPrice = Helper::toDecimal($minPrice);
188 284 $formattedMaxPrice = Helper::toDecimal($maxPrice);
189 285 $formattedComparePrice = Helper::toDecimal($comparePrice);
190 286
191 - do_action('fluent_cart/product/group/before_price_block', [
287 + do_action('fluent_cart/product/group/before_price_block', RenderContext::decorate([
192 288 'product' => $this->product,
193 289 'current_price' => $minPrice,
194 290 'scope' => 'product_card'
195 - ]);
291 + ]));
196 292 ?>
197 293 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
198 294 class="fct-product-card-prices"
199 295 role="region"
@@ -198,12 +294,11 @@
198 294 class="fct-product-card-prices"
199 295 role="region"
200 296 aria-label="<?php echo esc_attr__('Product pricing', 'fluent-cart'); ?>">
201 297 <?php if ($comparePrice): ?>
202 - <span class="fct-compare-price" aria-label="<?php echo esc_attr(sprintf(
203 - /* translators: %s: product price */
204 - __('Original price: %s', 'fluent-cart'), $formattedComparePrice)); ?>">
205 - <del aria-hidden="true"><?php echo esc_html($formattedComparePrice); ?></del>
298 + <span class="fct-compare-price">
299 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
300 + <del><?php echo esc_html($formattedComparePrice); ?></del>
206 301 </span>
207 302 <?php endif; ?>
208 303
209 304 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
@@ -208,49 +303,68 @@
208 303
209 304 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
210 305 <!-- Case 2: price range -->
211 306 <?php if ($priceFormat === 'range'): ?>
212 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
213 - /* translators: %1$s: min price, %2$s: max price */
214 - __('Price range from %1$s to %2$s', 'fluent-cart'), $formattedMinPrice, $formattedMaxPrice)); ?>">
215 - <span aria-hidden="true"><?php echo esc_html($formattedMinPrice); ?> - <?php echo esc_html($formattedMaxPrice); ?></span>
307 + <span class="fct-item-price">
308 + <span class="fct-sr-only"><?php echo esc_html__('Price range:', 'fluent-cart'); ?></span>
309 + <?php echo esc_html($formattedMinPrice); ?>
310 + <span aria-hidden="true">-</span>
311 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
312 + <?php echo esc_html($formattedMaxPrice); ?>
216 313 </span>
217 314 <?php else: ?>
218 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
219 - /* translators: %s: min price */
220 - __('Starting from %s', 'fluent-cart'), $formattedMinPrice)); ?>">
221 - <span aria-hidden="true"><?php
315 + <span class="fct-item-price"><?php
222 316 /* translators: %s is the minimum price */
223 317 printf(esc_html__('From %s', 'fluent-cart'), esc_html($formattedMinPrice));
224 318 ?></span>
225 - </span>
226 319 <?php endif; ?>
227 320
228 321 <?php else: ?>
229 322 <!-- Case 3: Simple or single price -->
230 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
231 - /* translators: %s: product price */
232 - __('Price: %s', 'fluent-cart'), $formattedMinPrice)); ?>">
233 - <span aria-hidden="true"><?php echo esc_html($formattedMinPrice); ?></span>
323 + <span class="fct-item-price">
324 + <span class="fct-sr-only"><?php echo esc_html__('Price:', 'fluent-cart'); ?></span>
325 + <?php echo esc_html($formattedMinPrice); ?>
234 326 </span>
235 327 <?php endif; ?>
236 328
237 - <?php do_action('fluent_cart/product/after_price', [
329 + <?php
330 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
238 331 'product' => $this->product,
332 + 'variant' => $firstVariant,
239 333 'current_price' => $minPrice,
240 334 'scope' => 'product_card'
241 - ]); ?>
335 + ]));
336 + RenderHelper::renderPriceSuffix($this->product, $firstVariant, 'product_card');
337 + ?>
242 338 </div>
243 339 <?php
244 - do_action('fluent_cart/product/group/after_price_block', [
340 + do_action('fluent_cart/product/group/after_price_block', RenderContext::decorate([
245 341 'product' => $this->product,
246 342 'current_price' => $minPrice,
247 343 'scope' => 'product_card'
248 - ]);
344 + ]));
249 345 }
250 346
251 347 public function showBuyButton($atts = '')
252 348 {
349 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
350 +
351 + // 'actions' gates the whole affordance slot, including the disabled
352 + // out-of-stock placeholder — hiding the row should not leave a stray
353 + // "Not Available" button behind.
354 + if (!RenderGate::shouldRender('actions', $gateContext)) {
355 + return;
356 + }
357 +
358 + do_action('fluent_cart/product/group/before_actions_block', $gateContext);
359 +
360 + $this->renderBuyButtonMarkup($atts, $gateContext);
361 +
362 + do_action('fluent_cart/product/group/after_actions_block', $gateContext);
363 + }
364 +
365 + protected function renderBuyButtonMarkup($atts, array $gateContext)
366 + {
253 367 $isOutOfStock = ModuleSettings::isActive('stock_management') && !$this->product->isStock();
254 368
255 369 if ($isOutOfStock) {
256 370 $soldOutText = __('Not Available', 'fluent-cart');
@@ -296,8 +410,24 @@
296 410 $buttonText = __('Add To Cart', 'fluent-cart');
297 411 $ariaLabel = sprintf(
298 412 /* translators: %s: product title */
299 413 __('Add %s to cart', 'fluent-cart'), $this->product->post_title);
414 + }
415 + }
416 +
417 + // The card fills this slot with one of three things: an instant-checkout
418 + // Buy Now anchor (simple + subscription), an Add to Cart button (simple),
419 + // or a "View Options" link through to the product page (variable). The
420 + // first two are purchase affordances and answer to their own gates.
421 + // "View Options" is navigation, so it stays under 'actions' alone — a
422 + // catalog-mode listener hides the buying, not the browsing.
423 + if ($isInstantCheckout) {
424 + if (!RenderGate::shouldRender('buy_now_button', $gateContext)) {
425 + return;
426 + }
427 + } elseif ($firstVariant) {
428 + if (!RenderGate::shouldRender('add_to_cart_button', $gateContext)) {
429 + return;
300 430 }
301 431 }
302 432
303 433 $buttonAttributes = [