PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
shopbuilder / app / Elementor / Widgets / Single / ProductImages.php

ProductImages.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Elementor/Widgets/Single/ProductImages.php

576 lines 20.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main ProductImages class.
4 *
5 * @package RadiusTheme\SB
6 */
7
8 namespace RadiusTheme\SB\Elementor\Widgets\Single;
9
10 use RadiusTheme\SB\Helpers\BuilderFns;
11 use RadiusTheme\SB\Helpers\ElementorDataMap;
12 use RadiusTheme\SB\Helpers\Fns;
13 use RadiusTheme\SB\Abstracts\ElementorWidgetBase;
14 use RadiusTheme\SB\Elementor\Widgets\Controls\ProductImagesSettings;
15 use RadiusTheme\SBPRO\Helpers\BuilderFunPro;
16
17 // Do not allow directly accessing this file.
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit( 'This script cannot be accessed directly.' );
20 }
21
22 /**
23 * Product Images class
24 */
25 class ProductImages extends ElementorWidgetBase {
26 /**
27 * Construct function
28 *
29 * @param array $data default array.
30 * @param mixed $args default arg.
31 */
32 public function __construct( $data = [], $args = null ) {
33 $this->rtsb_name = esc_html__( 'Product Images', 'shopbuilder' );
34 $this->rtsb_base = 'rtsb-product-image';
35 parent::__construct( $data, $args );
36 $this->the_hooks();
37 }
38 /**
39 * Widget Field
40 *
41 * @return array
42 */
43 public function widget_fields() {
44 return $this->modify_widget_fields();
45 }
46 /**
47 * Widget Field
48 *
49 * @return array
50 */
51 public function modify_widget_fields() {
52 $fields = ProductImagesSettings::widget_fields( $this );
53 $extra_controls['image_border'] = [
54 'mode' => 'group',
55 'type' => 'border',
56 'selector' => $this->selectors['image_border'],
57 'size_units' => [ 'px' ],
58 'fields_options' => [
59 'border' => [
60 'label' => esc_html__( 'Border', 'shopbuilder' ),
61 'label_block' => true,
62 ],
63 'color' => [
64 'label' => esc_html__( 'Border Color', 'shopbuilder' ),
65 ],
66 ],
67 ];
68 $fields = Fns::insert_controls( 'image_width', $fields, $extra_controls, true );
69 return $fields;
70 }
71
72 /**
73 * Widget Field.
74 *
75 * @return void
76 */
77 public function init_scripts() {
78 ?>
79 <script type="text/javascript">
80 // Initialize the gallery's Swiper sliders. In the Elementor editor the
81 // widget renders/re-renders after the slider script's one-time init has
82 // already run, so the main + thumbnail Swipers need to be (re)initialized
83 // here — otherwise the thumbnails stack and the gallery looks broken.
84 window.rtsbInitGallerySliders = window.rtsbInitGallerySliders || function ($scope) {
85 var $ctx = ($scope && $scope.length) ? $scope : jQuery(document);
86 // Elementor's AJAX re-render can run outside edit-mode detection, which
87 // leaves the gallery wrapper in the hidden `rtsb-content-loading` (opacity:0)
88 // state. Reveal it so the editor never shows a blank/hidden gallery.
89 $ctx.find('.rtsb-content-loading').removeClass('rtsb-content-loading');
90 // Hide the thumbnail column when the current view has no thumbnails (the
91 // runtime variation JS that normally does this doesn't run in the editor).
92 $ctx.find('.rtsb-vg-main-slider-wrapper').each(function () {
93 var $wrap = jQuery(this);
94 if ($wrap.find('.rtsb-vg-thumb-slider .swiper-slide').length < 1) {
95 $wrap.addClass('rtsb-vg-thumbs-hidden');
96 }
97 });
98 $ctx.find('.rtsb-carousel-slider').each(function () {
99 var $slider = jQuery(this);
100 if (typeof $slider.rtsb_slider !== 'function') {
101 return;
102 }
103 // The runtime height-sync (syncThumbHeight) only runs on WooCommerce's
104 // wc_variation_form event, which doesn't fire in the editor — so the
105 // vertical thumbnail Swiper has no definite height and grows unbounded.
106 // Replicate the sync here once the slider has loaded.
107 $slider.off('rtsb_slider_loaded.rtsbEditor').on('rtsb_slider_loaded.rtsbEditor', function () {
108 setTimeout(function () {
109 window.rtsbEditorSyncThumbHeight($slider);
110 }, 400);
111 });
112 $slider.rtsb_slider();
113 });
114 // The sync writes a fixed px height onto the thumbnail column and its
115 // slots. Without re-running it on viewport change that value goes stale
116 // the moment the layout reflows — switching to a responsive/device width
117 // leaves a tall thumbnail column beside a much smaller main image. The
118 // frontend module rebinds on resize for exactly this reason; the editor
119 // preview needs the same.
120 window.rtsbEditorBindResize($ctx);
121 };
122 // Re-run the height sync (debounced) whenever the viewport changes, so the
123 // side-thumbnail layouts stay correct in Elementor's responsive modes and in
124 // the browser's device toolbar.
125 window.rtsbEditorBindResize = window.rtsbEditorBindResize || function ($ctx) {
126 var resizeTimer;
127 jQuery(window).off('resize.rtsbEditorVg').on('resize.rtsbEditorVg', function () {
128 clearTimeout(resizeTimer);
129 resizeTimer = setTimeout(function () {
130 var $scope = ($ctx && $ctx.length) ? $ctx : jQuery(document);
131 $scope.find('.rtsb-carousel-slider').each(function () {
132 window.rtsbEditorSyncThumbHeight(jQuery(this));
133 });
134 }, 150);
135 });
136 };
137 // Size the thumbnail column (and its slots) to the main image height, the
138 // same way the frontend module does, so the editor preview isn't unbounded.
139 window.rtsbEditorSyncThumbHeight = window.rtsbEditorSyncThumbHeight || function ($slider) {
140 var $wrap = $slider.closest('.rtsb-vg-main-slider-wrapper');
141 if (!$wrap.length || !$wrap.is('.rtsb-thumbnails-position-left, .rtsb-thumbnails-position-right')) {
142 return;
143 }
144 var $thumb = $wrap.find('.rtsb-vg-thumb-slider').first();
145 if (!$thumb.length) {
146 return;
147 }
148 var mainH = Math.round($slider.outerHeight() || 0);
149 if (mainH <= 0) {
150 return;
151 }
152 var opts = $thumb.data('options') || {};
153 var perView = parseInt(opts.slidesPerView, 10) || 4;
154 var gap = parseInt(opts.spaceBetween, 10) || 0;
155 var slotH = Math.floor((mainH - gap * (perView - 1)) / perView);
156 $thumb.css('height', mainH + 'px');
157 if (slotH > 0) {
158 $thumb.find('.swiper-slide').css('height', slotH + 'px');
159 }
160 var thumbEl = $thumb.get(0);
161 if (thumbEl && thumbEl.swiper) {
162 thumbEl.swiper.update();
163 }
164 };
165 // Re-scan for gallery wrappers, whichever way the module happens to be
166 // available. The module registers itself with the RTSB registry only when
167 // RTSB was already initialised at bundle-eval time; otherwise it just
168 // exposes window.rtsbVariationGallery. Asking the registry alone therefore
169 // silently no-ops in the Elementor template-builder preview, so the module
170 // never runs and `rtsb-content-loading` (opacity:0) is never removed.
171 window.rtsbRefreshVariationGallery = window.rtsbRefreshVariationGallery || function () {
172 var modules = window.RTSB && window.RTSB.modules;
173 var mod = (modules && typeof modules.get === 'function') ? modules.get('variationGallery') : null;
174 if (mod && typeof mod.refresh === 'function') {
175 mod.refresh();
176 return;
177 }
178 if (typeof window.rtsbVariationGallery === 'function') {
179 window.rtsbVariationGallery();
180 return;
181 }
182 if (typeof window.waitForRTSB === 'function') {
183 window.waitForRTSB(function (RTSB) {
184 RTSB.modules.get('variationGallery')?.refresh();
185 });
186 }
187 };
188 // Last-resort reveal: the wrapper ships with `rtsb-content-loading`
189 // (opacity:0) and every init path above is responsible for dropping it. If
190 // they all fail the gallery is invisible with no way back, so uncover it
191 // rather than leave a blank box.
192 window.rtsbRevealGalleryFallback = window.rtsbRevealGalleryFallback || function () {
193 setTimeout(function () {
194 jQuery('.rtsb-vg-main-slider-wrapper.rtsb-content-loading').removeClass('rtsb-content-loading');
195 }, 3000);
196 };
197 if (!'<?php echo esc_attr( Fns::is_optimization_enabled() ); ?>') {
198 setTimeout(function() {
199 window.rtsbInitGallerySliders();
200 window.rtsbRefreshVariationGallery();
201 }, 600);
202 } else {
203 if (typeof elementorFrontend !== 'undefined') {
204 elementorFrontend.hooks.addAction(
205 'frontend/element_ready/<?php echo esc_attr( $this->rtsb_base ); ?>.default',
206 ( $scope ) => {
207 // Init the gallery slider immediately — it only needs jQuery and
208 // the rtsb_slider plugin, not the RTSB module system, so it must
209 // not be gated behind waitForRTSB (which may not resolve here).
210 window.rtsbInitGallerySliders($scope);
211 window.rtsbRefreshVariationGallery();
212 }
213 );
214 } else {
215 // No Elementor frontend on this request (some preview/standalone
216 // renders): nothing would ever initialise the module otherwise.
217 jQuery(function () {
218 window.rtsbInitGallerySliders();
219 window.rtsbRefreshVariationGallery();
220 });
221 }
222 }
223 window.rtsbRevealGalleryFallback();
224 /*
225 * Initialize all galleries on page.
226 */
227 jQuery('.woocommerce-product-gallery').each(function () {
228 const that = this;
229
230 setTimeout(function () {
231 // `wc_single_product_params` is localised on WooCommerce's
232 // `wc-single-product` script, which is absent in some contexts —
233 // Elementor template previews most commonly. Referencing it there
234 // throws a ReferenceError that aborts gallery initialisation, so
235 // bail out quietly instead.
236 if (typeof wc_single_product_params === 'undefined' || typeof jQuery(that).wc_product_gallery !== 'function') {
237 return;
238 }
239 jQuery(that).trigger('wc-product-gallery-before-init', [that, wc_single_product_params]);
240 jQuery(that).wc_product_gallery(wc_single_product_params);
241 jQuery(that).trigger('wc-product-gallery-after-init', [that, wc_single_product_params]);
242 }, 600);
243
244 });
245 <?php if ( function_exists( 'rtwpvg' ) ) { ?>
246 setTimeout(function () {
247 jQuery('.rtwpvg-wrapper, .rtwpvg-grid-wrapper').rtWpVGallery();
248 }, 600);
249 <?php } ?>
250
251
252 // Inject the selected lightbox icon into the gallery trigger inside the builder.
253 // The front-end bundle (RTSB modules / product-image.js) is dequeued in the
254 // Elementor editor preview, so the normal runtime injection never runs here.
255 // Read the server-rendered `data-zoom-icon` and drop it into the trigger. Since
256 // WooCommerce paints its own magnifier via the trigger's ::before/::after and
257 // hides real content with text-indent — neither of which JS can clear on a
258 // pseudo-element — also register a one-time <style> that neutralises them.
259 (function () {
260 var styleId = 'rtsb-editor-zoom-icon-style';
261
262 function ensureTriggerStyle() {
263 if (document.getElementById(styleId)) {
264 return;
265 }
266 var style = document.createElement('style');
267 style.id = styleId;
268 style.textContent =
269 '.rtsb-product-images .woocommerce-product-gallery__trigger{text-indent:0 !important;display:inline-flex;align-items:center;justify-content:center;}' +
270 '.rtsb-product-images .woocommerce-product-gallery__trigger::before,' +
271 '.rtsb-product-images .woocommerce-product-gallery__trigger::after{content:none !important;}';
272 document.head.appendChild(style);
273 }
274
275 function injectZoomIcon($scope) {
276 var $root = ($scope && $scope.length) ? $scope : jQuery(document);
277 $root.find('.rtsb-product-images').each(function () {
278 var $gallery = jQuery(this);
279 var zoomIcon = $gallery.attr('data-zoom-icon');
280 var $triggers = $gallery.find('.woocommerce-product-gallery__trigger, .rtwpvg-trigger');
281 if (!zoomIcon) {
282 $triggers.remove();
283 return;
284 }
285 $triggers.html(zoomIcon);
286 });
287 }
288
289 // WooCommerce re-inits the gallery ~600ms after render (see above) and can
290 // reset the trigger, so inject after that window to win the race.
291 window.rtsbEditorInjectZoomIcon = function ($scope) {
292 ensureTriggerStyle();
293 setTimeout(function () {
294 injectZoomIcon($scope);
295 }, 700);
296 };
297
298 if (typeof elementorFrontend !== 'undefined' && elementorFrontend.hooks) {
299 elementorFrontend.hooks.addAction(
300 'frontend/element_ready/<?php echo esc_attr( $this->rtsb_base ); ?>.default',
301 function ($scope) {
302 window.rtsbEditorInjectZoomIcon($scope);
303 }
304 );
305 }
306
307 jQuery(function () {
308 window.rtsbEditorInjectZoomIcon();
309 });
310 })();
311
312 </script>
313 <?php
314 }
315
316 /**
317 * Widget Field.
318 *
319 * @param array $control Control.
320 *
321 * @return void
322 */
323 public function the_hooks( $control = [] ) {
324 if ( empty( $control ) ) {
325 return;
326 }
327 add_filter(
328 'woocommerce_product_thumbnails_columns',
329 function ( $col ) {
330 $col = 0;
331
332 return $col;
333 }
334 );
335 if ( empty( $control['show_zoom'] ) ) {
336 add_filter( 'rtwpvg_trigger_icon', '__return_false' );
337 }
338 if ( empty( $control['show_thumbnails'] ) ) {
339 add_filter( 'rtwpvg_show_product_thumbnail_slider', '__return_false' );
340 remove_action( 'woocommerce_product_thumbnails', 'woocommerce_show_product_thumbnails', 20 );
341 }
342 add_filter( 'rtwpvg_thumbnail_slider_js_options', [ $this, 'thumbnail_slider_js_options' ], 50 );
343 add_filter( 'rtwpvg_thumbnail_position', [ $this, 'thumbnail_position' ], 50 );
344 add_filter( 'rtwpvg_thumbnails_columns', [ $this, 'thumbnails_columns' ] );
345 add_filter( 'rtwpvg_sm_thumbnails_columns', [ $this, 'thumbnails_columns_sm' ] );
346 add_filter( 'rtwpvg_xs_thumbnails_columns', [ $this, 'thumbnails_columns_xs' ] );
347 // RTSB.
348 add_filter( 'rtsb/vg/thumbnails/position', [ $this, 'vg_thumbnail_position' ], 50 );
349 add_filter( 'rtsb/vg/lightbox', [ $this, 'vg_show_lightbox' ], 50 );
350 add_filter( 'rtsb/vg/lightbox/trigger/icon', [ $this, 'vg_lightbox' ], 50 );
351 add_filter( 'rtsb/vg/thumbnails/slider/options', [ $this, 'thumbnail_slider_js_options' ], 50, 2 );
352 add_filter( 'rtsb/vg/lightbox/position', [ $this, 'vg_lightbox_position' ], 50, 2 );
353 add_filter( 'rtsb/vg/gallery/columns', [ $this, 'vg_gallery_columns' ], 50, 2 );
354 }
355 /**
356 * Thumbnail Columns.
357 *
358 * @param int $col number.
359 *
360 * @return int
361 */
362 public function vg_gallery_columns( $col ) {
363 $controllers = $this->get_settings_for_display();
364 if ( ! empty( $controllers['gallery_thumbs_column']['size'] ) ) {
365 $col = absint( $controllers['gallery_thumbs_column']['size'] );
366 }
367 return $col;
368 }
369 /**
370 * Thumbnail slider js options.
371 *
372 * @param array $options Options.
373 *
374 * @return array
375 */
376 public function thumbnail_slider_js_options( $options ) {
377 $controllers = $this->get_settings_for_display();
378 if ( ! empty( $controllers['gallery_thumbs_column']['size'] ) ) {
379 $options['slidesPerView'] = absint( $controllers['gallery_thumbs_column']['size'] );
380 }
381 if ( ! empty( $controllers['gallery_thumbs_column_gap']['size'] ) ) {
382 $options['spaceBetween'] = $controllers['gallery_thumbs_column_gap']['size'];
383 }
384 return $options;
385 }
386
387 /**
388 * Thumbnails columns.
389 *
390 * @param int $column Column.
391 *
392 * @return mixed
393 */
394 public function thumbnails_columns( $column ) {
395 $controllers = $this->get_settings_for_display();
396 $column = ! empty( $controllers['gallery_thumbs_column']['size'] ) ? $controllers['gallery_thumbs_column']['size'] : $column;
397
398 return $column;
399 }
400
401 /**
402 * Thumbnails columns.
403 *
404 * @param int $column Column.
405 *
406 * @return mixed
407 */
408 public function thumbnails_columns_sm( $column ) {
409 $controllers = $this->get_settings_for_display();
410 $column = ! empty( $controllers['gallery_thumbs_column_tablet']['size'] ) ? $controllers['gallery_thumbs_column_tablet']['size'] : $column;
411
412 return $column;
413 }
414
415 /**
416 * Thumbnails columns.
417 *
418 * @param int $column Column.
419 *
420 * @return mixed
421 */
422 public function thumbnails_columns_xs( $column ) {
423 $controllers = $this->get_settings_for_display();
424 $column = ! empty( $controllers['gallery_thumbs_column_mobile']['size'] ) ? $controllers['gallery_thumbs_column_mobile']['size'] : $column;
425
426 return $column;
427 }
428 /**
429 * Thumbnail position.
430 *
431 * @param string $position Position.
432 *
433 * @return string
434 */
435 public function thumbnail_position( $position ) {
436 if ( ! function_exists( 'rtwpvg' ) || ! rtwpvg()->active_pro() || ! ( BuilderFns::is_product() || BuilderFns::is_quick_views_page() ) ) {
437 return $position;
438 }
439
440 $controllers = $this->get_settings_for_display();
441 $show_thumbnails = ! empty( $controllers['show_thumbnails'] ) ? $controllers['show_thumbnails'] : false;
442 if ( ! $show_thumbnails && 'grid' !== ( $controllers['image_layout'] ?? '' ) ) {
443 add_filter( 'rtwpvg_show_product_thumbnail_slider', '__return_false', 20 );
444 return 'bottom';
445 }
446 return ! empty( $controllers['image_layout'] ) ? $controllers['image_layout'] : 'bottom';
447 }
448 /**
449 * Thumbnail position.
450 *
451 * @param string $position Position.
452 *
453 * @return string
454 */
455 public function vg_thumbnail_position( $position ) {
456 if ( ! rtsb()->has_pro() ) {
457 return $position;
458 }
459 $controllers = $this->get_settings_for_display();
460 return ! empty( $controllers['image_layout'] ) ? $controllers['image_layout'] : 'bottom';
461 }
462
463 /**
464 * Whether the Variation Gallery lightbox trigger should render.
465 *
466 * The `rtsb/vg/lightbox` filter is a boolean gate (see the VG product-image
467 * template). It must reflect the widget's "Show Lightbox Icon?" (show_zoom)
468 * setting so disabling it actually hides the trigger — not the icon HTML, which
469 * is always truthy and would keep the trigger visible.
470 *
471 * @param bool $show Whether to show the lightbox trigger.
472 * @return bool
473 */
474 public function vg_show_lightbox( $show ) {
475 $controllers = $this->get_settings_for_display();
476 return ! empty( $controllers['show_zoom'] );
477 }
478 /**
479 * @param bool $lightbox Lightbox.
480 * @return mixed
481 */
482 public function vg_lightbox( $lightbox ) {
483 $controllers = $this->get_settings_for_display();
484 // The icons control always returns a populated array (`value`/`library`), so
485 // check the actual `value`; when it is cleared, fall back to the default SVG
486 // instead of rendering an empty icon.
487 return ! empty( $controllers['lightbox_icon']['value'] ) ? Fns::icons_manager( $controllers['lightbox_icon'] ) : $lightbox;
488 }
489 /**
490 * @param string $position position text.
491 * @return mixed
492 */
493 public function vg_lightbox_position( $position ) {
494 $controllers = $this->get_settings_for_display();
495 return ! empty( $controllers['vg_lightbox_position'] ) ? $controllers['vg_lightbox_position'] : $position;
496 }
497
498 /**
499 * Enqueue the Font Awesome library required by the selected lightbox icon.
500 *
501 * Elementor only auto-loads icon fonts for icons rendered through its own widget
502 * flow. The lightbox icon is injected into the Variation Gallery / WooCommerce
503 * gallery markup via a filter, so on the front end its font is never enqueued and
504 * the glyph renders empty — it appears in the editor only because Elementor always
505 * loads Font Awesome there. Enqueue the matching library so the icon shows on the
506 * front end. SVG icons carry no library and need no enqueue.
507 *
508 * @param array $icon Elementor icons control value.
509 * @return void
510 */
511 private function enqueue_lightbox_icon_font( $icon ) {
512 if ( empty( $icon['library'] ) ) {
513 return;
514 }
515 $handles = [
516 'fa-solid' => 'elementor-icons-fa-solid',
517 'fa-regular' => 'elementor-icons-fa-regular',
518 'fa-brands' => 'elementor-icons-fa-brands',
519 ];
520 if ( isset( $handles[ $icon['library'] ] ) ) {
521 wp_enqueue_style( $handles[ $icon['library'] ] );
522 }
523 }
524
525 /**
526 * Render Function
527 *
528 * @return void
529 */
530 protected function render() {
531 add_action( 'wp_footer', 'woocommerce_photoswipe' );
532 global $product, $post;
533 $_product = $product;
534 $product = Fns::get_product();
535 $controllers = $this->get_settings_for_display();
536 if ( empty( $controllers['show_module_badges'] ) ) {
537 add_filter( 'rtsb/module/badges/show', '__return_false', 99 );
538 }
539 // When "Show Thumbnails?" is off, hide the Variation Gallery thumbnail slider.
540 $hide_vg_thumbnails = empty( $controllers['show_thumbnails'] );
541
542 if ( ! empty( $controllers['show_zoom'] ) && ! empty( $controllers['lightbox_icon'] ) ) {
543 $this->enqueue_lightbox_icon_font( $controllers['lightbox_icon'] );
544 }
545
546 $this->theme_support();
547 $controllers['lightbox_icon'] = Fns::icons_manager( $controllers['lightbox_icon'] );
548 $data = [
549 'template' => 'elementor/single-product/product-images',
550 'controllers' => $controllers,
551 ];
552 $this->the_hooks( $controllers );
553
554 if ( $hide_vg_thumbnails ) {
555 add_filter( 'rtsb/vg/show/thumbnails', '__return_false' );
556 }
557
558 Fns::load_template( $data['template'], $data );
559
560 if ( $hide_vg_thumbnails ) {
561 remove_filter( 'rtsb/vg/show/thumbnails', '__return_false' );
562 }
563
564 if ( $this->is_builder_mode() ) {
565 $this->init_scripts();
566 }
567
568 if ( empty( $controllers['show_module_badges'] ) ) {
569 remove_filter( 'rtsb/module/badges/show', '__return_false', 99 );
570 }
571
572 $this->theme_support( 'render_reset' );
573 $product = $_product; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
574 }
575 }
576