PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
1.6.6 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 All 49 releases
← All changes | app/Services/Renderer/ProductCardRender.php +103 -28 1.6.1 → 1.6.6 View file →
@@ -62,11 +62,24 @@
62 62 $cardWidth = 'style="width: ' . esc_attr($widthValue) . ';"';
63 63 }
64 64
65 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 + ]));
66 79 ?>
67 80 <article data-fluent-cart-shop-app-single-product data-fct-product-card=""
68 - class="fct-product-card"
81 + class="<?php echo esc_attr($cardClasses); ?>"
69 82 <?php echo $cursor; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped ?>
70 83 <?php echo $cardWidth; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped ?>
71 84 aria-label="<?php echo esc_attr(sprintf(
72 85 /* translators: %s: product title */
@@ -78,8 +91,12 @@
78 91 <?php $this->renderPrices(); ?>
79 92 <?php $this->showBuyButton(); ?>
80 93 </article>
81 94 <?php
95 + do_action('fluent_cart/product/group/after_card', RenderContext::decorate([
96 + 'product' => $this->product,
97 + 'scope' => RenderGate::SCOPE_CARD,
98 + ]));
82 99 }
83 100
84 101 /**
85 102 * @deprecated Use PackageDescriptionRenderer::buildPackageInfoFromOtherInfo() directly.
@@ -123,8 +140,16 @@
123 140 if (empty($this->product->post_excerpt)) {
124 141 return;
125 142 }
126 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 +
127 152 echo sprintf(
128 153 '<p %1$s class="fct-product-card-excerpt">
129 154 %2$s
130 155 </p>',
@@ -130,12 +155,22 @@
130 155 </p>',
131 156 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
132 157 wp_kses_post($this->product->post_excerpt),
133 158 );
159 +
160 + do_action('fluent_cart/product/group/after_excerpt_block', $gateContext);
134 161 }
135 162
136 163 public function renderTitle($atts = '', $config = [])
137 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 +
138 173 $link = Arr::get($config, 'isLink', true);
139 174 $target = Arr::get($config, 'target', '_self');
140 175
141 176 $titleText = esc_html($this->product->post_title);
@@ -165,12 +200,18 @@
165 200 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
166 201 esc_html($titleText)
167 202 );
168 203 }
204 +
205 + do_action('fluent_cart/product/group/after_title_block', $gateContext);
169 206 }
170 207
171 208 public function renderProductImage()
172 209 {
210 + if (!RenderGate::shouldRender('image', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
211 + return;
212 + }
213 +
173 214 $image = $this->product->thumbnail;
174 215 $isPlaceholder = false;
175 216
176 217 if (!$image) {
@@ -183,12 +224,12 @@
183 224 /* translators: %s: product title */
184 225 __('Placeholder image for %s', 'fluent-cart'), $this->product->post_title)
185 226 : $this->product->post_title;
186 227
187 - do_action('fluent_cart/product/group/before_image_block', [
228 + do_action('fluent_cart/product/group/before_image_block', RenderContext::decorate([
188 229 'product' => $this->product,
189 230 'scope' => 'product_card'
190 - ]);
231 + ]));
191 232 ?>
192 233 <a class="fct-product-card-image-wrap"
193 234 href="<?php echo esc_url($this->viewUrl); ?>"
194 235 style="display: block;"
@@ -204,16 +245,20 @@
204 245 height="300"/>
205 246 </a>
206 247 <?php
207 248
208 - do_action('fluent_cart/product/group/after_image_block', [
249 + do_action('fluent_cart/product/group/after_image_block', RenderContext::decorate([
209 250 'product' => $this->product,
210 251 'scope' => 'product_card'
211 - ]);
252 + ]));
212 253 }
213 254
214 255 public function renderPrices($wrapper_attributes = '')
215 256 {
257 + if (!RenderGate::shouldRender('price', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
258 + return;
259 + }
260 +
216 261 $priceFormat = Arr::get($this->config, 'price_format', 'starts_from');
217 262 $isSimple = $this->product->detail->variation_type === 'simple';
218 263 $minPrice = $this->product->detail->min_price;
219 264 $maxPrice = $this->product->detail->max_price;
@@ -238,13 +283,13 @@
238 283 $formattedMinPrice = Helper::toDecimal($minPrice);
239 284 $formattedMaxPrice = Helper::toDecimal($maxPrice);
240 285 $formattedComparePrice = Helper::toDecimal($comparePrice);
241 286
242 - do_action('fluent_cart/product/group/before_price_block', [
287 + do_action('fluent_cart/product/group/before_price_block', RenderContext::decorate([
243 288 'product' => $this->product,
244 289 'current_price' => $minPrice,
245 290 'scope' => 'product_card'
246 - ]);
291 + ]));
247 292 ?>
248 293 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
249 294 class="fct-product-card-prices"
250 295 role="region"
@@ -249,12 +294,11 @@
249 294 class="fct-product-card-prices"
250 295 role="region"
251 296 aria-label="<?php echo esc_attr__('Product pricing', 'fluent-cart'); ?>">
252 297 <?php if ($comparePrice): ?>
253 - <span class="fct-compare-price" aria-label="<?php echo esc_attr(sprintf(
254 - /* translators: %s: product price */
255 - __('Original price: %s', 'fluent-cart'), $formattedComparePrice)); ?>">
256 - <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>
257 301 </span>
258 302 <?php endif; ?>
259 303
260 304 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
@@ -259,53 +303,68 @@
259 303
260 304 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
261 305 <!-- Case 2: price range -->
262 306 <?php if ($priceFormat === 'range'): ?>
263 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
264 - /* translators: %1$s: min price, %2$s: max price */
265 - __('Price range from %1$s to %2$s', 'fluent-cart'), $formattedMinPrice, $formattedMaxPrice)); ?>">
266 - <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); ?>
267 313 </span>
268 314 <?php else: ?>
269 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
270 - /* translators: %s: min price */
271 - __('Starting from %s', 'fluent-cart'), $formattedMinPrice)); ?>">
272 - <span aria-hidden="true"><?php
315 + <span class="fct-item-price"><?php
273 316 /* translators: %s is the minimum price */
274 317 printf(esc_html__('From %s', 'fluent-cart'), esc_html($formattedMinPrice));
275 318 ?></span>
276 - </span>
277 319 <?php endif; ?>
278 320
279 321 <?php else: ?>
280 322 <!-- Case 3: Simple or single price -->
281 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
282 - /* translators: %s: product price */
283 - __('Price: %s', 'fluent-cart'), $formattedMinPrice)); ?>">
284 - <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); ?>
285 326 </span>
286 327 <?php endif; ?>
287 328
288 329 <?php
289 - do_action('fluent_cart/product/after_price', [
330 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
290 331 'product' => $this->product,
291 332 'variant' => $firstVariant,
292 333 'current_price' => $minPrice,
293 334 'scope' => 'product_card'
294 - ]);
335 + ]));
295 336 RenderHelper::renderPriceSuffix($this->product, $firstVariant, 'product_card');
296 337 ?>
297 338 </div>
298 339 <?php
299 - do_action('fluent_cart/product/group/after_price_block', [
340 + do_action('fluent_cart/product/group/after_price_block', RenderContext::decorate([
300 341 'product' => $this->product,
301 342 'current_price' => $minPrice,
302 343 'scope' => 'product_card'
303 - ]);
344 + ]));
304 345 }
305 346
306 347 public function showBuyButton($atts = '')
307 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 + {
308 367 $isOutOfStock = ModuleSettings::isActive('stock_management') && !$this->product->isStock();
309 368
310 369 if ($isOutOfStock) {
311 370 $soldOutText = __('Not Available', 'fluent-cart');
@@ -351,8 +410,24 @@
351 410 $buttonText = __('Add To Cart', 'fluent-cart');
352 411 $ariaLabel = sprintf(
353 412 /* translators: %s: product title */
354 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;
355 430 }
356 431 }
357 432
358 433 $buttonAttributes = [