PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / trunk
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler vtrunk
1.7.0 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 All 50 releases
fluent-cart / app / Hooks / Handlers / BlockEditors / MediaCarousel / InnerBlocks / InnerBlocks.php

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

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