PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.25
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.25
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
fluent-cart / app / Hooks / Handlers / BlockEditors / MediaCarousel / InnerBlocks / InnerBlocks.php

InnerBlocks.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.25, at app/Hooks/Handlers/BlockEditors/MediaCarousel/InnerBlocks/InnerBlocks.php

509 lines 16.9 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\Hooks\Handlers\BlockEditors\MediaCarousel\InnerBlocks;
4
5 use FluentCart\Api\Contracts\CanEnqueue;
6 use FluentCart\Api\Resource\ProductResource;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\App\App;
9 use FluentCart\App\Helpers\Helper;
10 use FluentCart\App\Models\Product;
11 use FluentCart\App\Services\TemplateService;
12 use FluentCart\App\Services\Translations\TransStrings;
13 use FluentCart\Framework\Support\Arr;
14 use FluentCart\App\Vite;
15
16 class InnerBlocks
17 {
18 use CanEnqueue;
19
20 public static $parentBlock = 'fluent-cart/media-carousel';
21
22 public array $blocks = [];
23
24 public static function register()
25 {
26 $self = new self();
27 $blocks = $self->getInnerBlocks();
28
29 foreach ($blocks as $block) {
30 $args = [
31 'apiVersion' => 3,
32 'api_version' => 3,
33 'version' => 3,
34 'title' => $block['title'],
35 'category' => 'fluent-cart',
36 'parent' => array_merge($block['parent'] ?? [], [static::$parentBlock]),
37 'supports' => Arr::get($block, 'supports', []),
38 'attributes' => Arr::get($block, 'attributes', []),
39 'uses_context' => Arr::get($block, 'uses_context', []),
40 ];
41
42 // Only add render_callback if it exists
43 if (!empty($block['callback']) && is_callable($block['callback'])) {
44 $args['render_callback'] = $block['callback'];
45 }
46
47 register_block_type($block['slug'], $args);
48
49 }
50
51 add_action('enqueue_block_editor_assets', function () use ($self) {
52 $self->enqueueScripts();
53 });
54 }
55
56 public function getInnerBlocks(): array
57 {
58 return [
59 [
60 'title' => __('Media Carousel Loop', 'fluent-cart'),
61 'slug' => 'fluent-cart/media-carousel-loop',
62 'callback' => [$this, 'renderMediaCarouselLoop'],
63 'component' => 'MediaCarouselLoopBlock',
64 'icon' => 'slides',
65 'parent' => [
66 'fluent-cart/media-carousel',
67 'fluent-cart/shopapp-product-loop',
68 ],
69 'supports' => [
70 'align' => ['wide', 'full'],
71 'html' => false,
72 ],
73 'attributes' => array_merge(
74 \WP_Block_Type_Registry::get_instance()->get_registered('core/group')->attributes,
75 [
76 'customAttr' => ['type' => 'string'],
77 ]
78 ),
79 'uses_context' => [
80 'fluent-cart/query_type',
81 'fluent-cart/carousel_settings',
82 'fluent-cart/product_id',
83 'fluent-cart/variation_ids',
84 'fluent-cart/has_controls',
85 'fluent-cart/has_pagination',
86 ],
87 ],
88 [
89 'title' => __('Product Image', 'fluent-cart'),
90 'slug' => 'fluent-cart/media-carousel-product-image',
91 'parent' => [
92 'fluent-cart/media-carousel-loop',
93 'fluent-cart/shopapp-product-loop',
94 ],
95 'callback' => [$this, 'renderImage'],
96 'component' => 'ProductImageBlockEditor',
97 'icon' => 'format-image',
98 'supports' => [
99 'html' => false,
100 'align' => ['left', 'center', 'right', 'wide', 'full'],
101 '__experimentalBorder' => [
102 'color' => true,
103 'radius' => true,
104 'style' => true,
105 'width' => true,
106 ],
107 'spacing' => [
108 'margin' => true,
109 'padding' => true,
110 ],
111 'shadow' => true,
112 '__experimentalFilter' => [
113 'duotone' => true,
114 ],
115 ],
116 'uses_context' => [
117 'fluent-cart/query_type',
118 'fluent-cart/current_image',
119 'fluent-cart/image_index',
120 ],
121 ],
122 [
123 'title' => __('Carousel Controls', 'fluent-cart'),
124 'slug' => 'fluent-cart/media-carousel-controls',
125 'component' => 'CarouselControlsBlock',
126 'icon' => 'controls-repeat',
127 'parent' => [
128 'fluent-cart/media-carousel',
129 'fluent-cart/shopapp-product-loop',
130 ],
131 'supports' => ['html' => false],
132 'uses_context' => [
133 'fluent-cart/carousel_settings',
134 ],
135 ],
136 [
137 'title' => __('Carousel Pagination', 'fluent-cart'),
138 'slug' => 'fluent-cart/media-carousel-pagination',
139 'component' => 'CarouselPaginationBlock',
140 'icon' => 'ellipsis',
141 'parent' => [
142 'fluent-cart/media-carousel',
143 'fluent-cart/shopapp-product-loop',
144 ],
145 'supports' => ['html' => false],
146 'uses_context' => [
147 'fluent-cart/carousel_settings',
148 ],
149 ],
150
151 ];
152 }
153
154 public function renderImage($attributes, $content, $block): string
155 {
156 // Gutenberg frontend supports
157 $wrapper_attributes = get_block_wrapper_attributes([
158 'class' => 'fct-media-carousel-product-image',
159 ]);
160
161 // Image from carousel loop context
162 $image = Arr::get($block->context, 'fluent-cart/current_image');
163
164 // Fallback: product image
165 if (!$image) {
166 $product = $this->getProductFromBlockContext($block);
167 if (!$product) {
168 return '';
169 }
170
171 $image = [
172 'url' => $product->thumbnail ?: Vite::getAssetUrl('images/placeholder.svg'),
173 'title' => $product->post_title,
174 'link' => $product->view_url ?? '',
175 ];
176 }
177
178 $alt = $image['title'] ?? '';
179
180 ob_start();
181 ?>
182 <div <?php echo $wrapper_attributes; ?>>
183 <a
184 class="fct-product-image-link"
185 href="<?php echo esc_url($image['link'] ?? '#'); ?>"
186 aria-label="<?php echo esc_attr($alt); ?>"
187 >
188 <img
189 class="fct-product-image"
190 src="<?php echo esc_url($image['url']); ?>"
191 alt="<?php echo esc_attr($alt); ?>"
192 loading="lazy"
193 />
194 </a>
195 </div>
196 <?php
197
198 return ob_get_clean();
199 }
200
201 public function getProductFromBlockContext($block)
202 {
203 // Editor preview
204 if (!empty($block->context['fluent_cart_current_product'])) {
205 return $block->context['fluent_cart_current_product'];
206 }
207
208 // Frontend fallback
209 return fluent_cart_get_current_product();
210 }
211
212 protected static function getDefaultCarouselSettings(): array
213 {
214 return [
215 // Core
216 'slidesToShow' => 3,
217 'spaceBetween' => 4,
218 'infinite' => 'no',
219
220 // Autoplay
221 'autoplay' => 'yes', // 'no' | 'yes' | 'hover'
222 'autoplayDelay' => 3000,
223
224 // Controls
225 'arrows' => 'yes',
226 'arrowsSize' => 'md', // sm | md | lg
227
228 // Pagination
229 'pagination' => 'yes',
230 'paginationType' => 'bullets', // bullets | fraction | progress | segmented
231 ];
232 }
233
234 protected function getCarouselSettingsFromContext($block): array
235 {
236 $contextSettings = Arr::get($block->context, 'fluent-cart/carousel_settings', []);
237
238 return array_merge(
239 static::getDefaultCarouselSettings(),
240 is_array($contextSettings) ? $contextSettings : []
241 );
242 }
243
244 protected function isSlideBlock($block): bool
245 {
246 return !in_array($block->name, [
247 'fluent-cart/media-carousel-controls',
248 'fluent-cart/media-carousel-pagination',
249 ], true);
250 }
251
252 public function renderMediaCarouselLoop($attributes, $content, $block): string
253 {
254 $carouselSettings = $this->getCarouselSettingsFromContext($block);
255 $productId = Arr::get($block->context, 'fluent-cart/product_id');
256 $variationIds = Arr::get($block->context, 'fluent-cart/variation_ids', []);
257 $queryType = Arr::get($block->context, 'fluent-cart/query_type', 'default');
258
259 $hasControls = Arr::get($block->context, 'fluent-cart/has_controls', 'yes');
260 $hasPagination = Arr::get($block->context, 'fluent-cart/has_pagination', 'yes');
261 if (!$hasControls) {
262 $carouselSettings['arrows'] = 'no';
263 }
264
265 if (!$hasPagination) {
266 $carouselSettings['pagination'] = 'no';
267 }
268
269 $product = null;
270 if ($queryType === 'default') {
271 $product = fluent_cart_get_current_product();
272 }
273 else {
274 $product = ProductResource::findByProductAndVariants([
275 'product_id' => $productId,
276 'variant_ids' => $variationIds,
277 ]);
278 }
279
280 if (empty($product)) {
281 return '';
282 }
283
284 $product = $product->toArray();
285
286 /**
287 * ---------------------------------------------------------
288 * Build IMAGE LIST
289 * ---------------------------------------------------------
290 */
291 $images = [];
292
293 // Product gallery images
294 $gallery = Arr::get($product, 'detail.gallery_image.meta_value', []);
295
296 foreach ($gallery as $img) {
297 if (Arr::get($img, 'url')) {
298 $images[] = $img;
299 }
300 }
301
302 // Variant images
303 $variants = Arr::get($product, 'variants', []);
304
305 foreach ($variants as $variant) {
306 $variantImages = Arr::get($variant, 'media.meta_value', []);
307
308 foreach ($variantImages as $img) {
309 if (Arr::get($img, 'url')) {
310 $images[] = $img;
311 }
312 }
313 }
314
315 if (empty($images)) {
316 $placeholder = Vite::getAssetUrl('images/placeholder.svg');
317
318 $images[] = [
319 'url' => $placeholder,
320 'title' => sprintf(
321 /* translators: %s: product title */
322 __('Placeholder image for %s', 'fluent-cart'),
323 Arr::get($product, 'post_title', '')
324 ),
325 'is_placeholder' => true,
326 ];
327 }
328
329 /**
330 * Hide pagination and arrows if there's only one image or only placeholder
331 */
332 $isOnlyPlaceholder = count($images) === 1 && Arr::get($images[0], 'is_placeholder');
333 $isSingleImage = count($images) === 1;
334
335 if ($isOnlyPlaceholder || $isSingleImage) {
336 $hasControls = 'no';
337 $hasPagination = 'no';
338 $carouselSettings['arrows'] = 'no';
339 $carouselSettings['pagination'] = 'no';
340 }
341
342 /**
343 * ---------------------------------------------------------
344 * Render Swiper (IMAGE-LEVEL, INNER BLOCK DRIVEN)
345 * ---------------------------------------------------------
346 */
347 $blockContext = array_merge(
348 $block->context,
349 [
350 'fluent-cart/product_id' => Arr::get($block->context, 'fluent-cart/product_id'),
351 'fluent-cart/variation_ids' => Arr::get($block->context, 'fluent-cart/variation_ids', []),
352 ]
353 );
354
355 $html = '<div class="fct-product-carousel-wrapper">';
356 $html .= '<div class="swiper fct-product-carousel"
357 data-fluent-cart-product-carousel
358 data-carousel-settings="' . esc_attr(wp_json_encode($carouselSettings)) . '">';
359
360 $html .= '<div class="swiper-wrapper">';
361
362 foreach ($images as $index => $image) {
363
364 $html .= '<div class="swiper-slide"
365 data-image-index="' . esc_attr($index) . '">';
366
367 // Inject CURRENT IMAGE into context
368 $slideContext = array_merge($blockContext, [
369 'fluent-cart/current_image' => $image,
370 'fluent-cart/image_index' => $index,
371 ]);
372
373 // Render INNER BLOCKS (Product Image block, overlays, etc.)
374 foreach ($block->inner_blocks as $inner_block) {
375
376 if (
377 !isset($inner_block->parsed_block) ||
378 !$this->isSlideBlock($inner_block)
379 ) {
380 continue;
381 }
382
383 $instance = new \WP_Block(
384 $inner_block->parsed_block,
385 $slideContext
386 );
387
388 $html .= $instance->render();
389 }
390
391 $html .= '</div>';
392 }
393
394 $html .= '</div>';
395
396 // Controls & pagination
397 if ($hasControls === 'yes') {
398 $html .= $this->renderCarouselControls([], '', $block);
399 }
400
401 if ($hasPagination === 'yes') {
402 $html .= $this->renderCarouselPagination([], '', $block);
403 }
404
405 $html .= '</div></div>';
406
407 return $html;
408 }
409
410 public function renderCarouselControls($attributes, $content, $block): string
411 {
412 $settings = $this->getCarouselSettingsFromContext($block);
413
414 if (Arr::get($settings, 'arrows') !== 'yes') {
415 return '';
416 }
417
418 $size = Arr::get($settings, 'arrowsSize');
419
420 // Navigation arrows (INSIDE swiper, OUTSIDE wrapper)
421 return sprintf(
422 '<div class="fct-carousel-controls fct-arrows-%s">
423 <div class="swiper-button-prev" aria-label="%s">
424 <svg width="12" height="12" viewBox="0 0 24 24" fill="none"
425 stroke="currentColor" stroke-width="2"
426 stroke-linecap="round" stroke-linejoin="round">
427 <polyline points="15 18 9 12 15 6"></polyline>
428 </svg>
429 </div>
430 <div class="swiper-button-next" aria-label="%s">
431 <svg width="12" height="12" viewBox="0 0 24 24" fill="none"
432 stroke="currentColor" stroke-width="2"
433 stroke-linecap="round" stroke-linejoin="round">
434 <polyline points="9 18 15 12 9 6"></polyline>
435 </svg>
436 </div>
437 </div>',
438 esc_attr($size),
439 esc_attr__('Previous slide', 'fluent-cart'),
440 esc_attr__('Next slide', 'fluent-cart')
441 );
442 }
443
444 public function renderCarouselPagination($attributes, $content, $block): string
445 {
446 $settings = $this->getCarouselSettingsFromContext($block);
447
448 if (Arr::get($settings, 'pagination') !== 'yes') {
449 return '';
450 }
451
452 $type = Arr::get($settings, 'paginationType');
453
454 return sprintf(
455 '<div class="fct-carousel-pagination fct-pagination-%s">
456 <div class="swiper-pagination"></div>
457 </div>',
458 esc_attr($type)
459 );
460 }
461
462 protected function getLocalizationKey(): string
463 {
464 return 'fluent_cart_media_carousel_inner_blocks';
465 }
466
467 public function localizeData(): array
468 {
469 return [
470 $this->getLocalizationKey() => [
471 'blocks' => Arr::except($this->getInnerBlocks(), ['callback']),
472 ],
473 'fluentcart_single_product_vars' => [
474 'trans' => TransStrings::singleProductPageString(),
475 'enable_image_zoom' => (new StoreSettings())->get('enable_image_zoom_in_single_product'),
476 'enable_image_zoom_in_modal' => (new StoreSettings())->get('enable_image_zoom_in_modal')
477 ]
478 ];
479 }
480
481 public function getStyles(): array
482 {
483 return [
484 'public/single-product/single-product.scss',
485 ];
486 }
487
488 public function getScripts(): array
489 {
490 $scripts = [
491 [
492 'source' => 'admin/BlockEditor/ReactSupport.js',
493 'dependencies' => ['wp-blocks', 'wp-components']
494 ],
495 [
496 'source' => 'admin/BlockEditor/MediaCarousel/InnerBlocks/InnerBlocks.jsx',
497 'dependencies' => ['wp-blocks', 'wp-components', 'wp-block-editor']
498 ],
499 ];
500
501 return $scripts;
502 }
503
504 protected function generateEnqueueSlug(): string
505 {
506 return 'fluent_cart_media_carousel_inner_blocks';
507 }
508 }
509