PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.0
ShopBuilder – WooCommerce Builder For Elementor v3.4.0
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.0, at app/Elementor/Widgets/Single/ProductImages.php

510 lines 17.1 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 };
115 // Size the thumbnail column (and its slots) to the main image height, the
116 // same way the frontend module does, so the editor preview isn't unbounded.
117 window.rtsbEditorSyncThumbHeight = window.rtsbEditorSyncThumbHeight || function ($slider) {
118 var $wrap = $slider.closest('.rtsb-vg-main-slider-wrapper');
119 if (!$wrap.length || !$wrap.is('.rtsb-thumbnails-position-left, .rtsb-thumbnails-position-right')) {
120 return;
121 }
122 var $thumb = $wrap.find('.rtsb-vg-thumb-slider').first();
123 if (!$thumb.length) {
124 return;
125 }
126 var mainH = Math.round($slider.outerHeight() || 0);
127 if (mainH <= 0) {
128 return;
129 }
130 var opts = $thumb.data('options') || {};
131 var perView = parseInt(opts.slidesPerView, 10) || 4;
132 var gap = parseInt(opts.spaceBetween, 10) || 0;
133 var slotH = Math.floor((mainH - gap * (perView - 1)) / perView);
134 $thumb.css('height', mainH + 'px');
135 if (slotH > 0) {
136 $thumb.find('.swiper-slide').css('height', slotH + 'px');
137 }
138 var thumbEl = $thumb.get(0);
139 if (thumbEl && thumbEl.swiper) {
140 thumbEl.swiper.update();
141 }
142 };
143 if (!'<?php echo esc_attr( Fns::is_optimization_enabled() ); ?>') {
144 setTimeout(function() {
145 window.rtsbInitGallerySliders();
146 if (typeof window.rtsbVariationGallery === 'function') {
147 window.rtsbVariationGallery();
148 }
149 }, 600);
150 } else {
151 if (typeof elementorFrontend !== 'undefined') {
152 elementorFrontend.hooks.addAction(
153 'frontend/element_ready/<?php echo esc_attr( $this->rtsb_base ); ?>.default',
154 ( $scope ) => {
155 // Init the gallery slider immediately — it only needs jQuery and
156 // the rtsb_slider plugin, not the RTSB module system, so it must
157 // not be gated behind waitForRTSB (which may not resolve here).
158 window.rtsbInitGallerySliders($scope);
159 window.waitForRTSB((RTSB) => {
160 RTSB.modules.get('variationGallery')?.refresh();
161 });
162 }
163 );
164 }
165 }
166 /*
167 * Initialize all galleries on page.
168 */
169 jQuery('.woocommerce-product-gallery').each(function () {
170 const that = this;
171
172 setTimeout(function () {
173 jQuery(this).trigger('wc-product-gallery-before-init', [this, wc_single_product_params]);
174 jQuery(that).wc_product_gallery(wc_single_product_params);
175 jQuery(that).trigger('wc-product-gallery-after-init', [this, wc_single_product_params]);
176 }, 600);
177
178 });
179 <?php if ( function_exists( 'rtwpvg' ) ) { ?>
180 setTimeout(function () {
181 jQuery('.rtwpvg-wrapper, .rtwpvg-grid-wrapper').rtWpVGallery();
182 }, 600);
183 <?php } ?>
184
185
186 // Inject the selected lightbox icon into the gallery trigger inside the builder.
187 // The front-end bundle (RTSB modules / product-image.js) is dequeued in the
188 // Elementor editor preview, so the normal runtime injection never runs here.
189 // Read the server-rendered `data-zoom-icon` and drop it into the trigger. Since
190 // WooCommerce paints its own magnifier via the trigger's ::before/::after and
191 // hides real content with text-indent — neither of which JS can clear on a
192 // pseudo-element — also register a one-time <style> that neutralises them.
193 (function () {
194 var styleId = 'rtsb-editor-zoom-icon-style';
195
196 function ensureTriggerStyle() {
197 if (document.getElementById(styleId)) {
198 return;
199 }
200 var style = document.createElement('style');
201 style.id = styleId;
202 style.textContent =
203 '.rtsb-product-images .woocommerce-product-gallery__trigger{text-indent:0 !important;display:inline-flex;align-items:center;justify-content:center;}' +
204 '.rtsb-product-images .woocommerce-product-gallery__trigger::before,' +
205 '.rtsb-product-images .woocommerce-product-gallery__trigger::after{content:none !important;}';
206 document.head.appendChild(style);
207 }
208
209 function injectZoomIcon($scope) {
210 var $root = ($scope && $scope.length) ? $scope : jQuery(document);
211 $root.find('.rtsb-product-images').each(function () {
212 var $gallery = jQuery(this);
213 var zoomIcon = $gallery.attr('data-zoom-icon');
214 var $triggers = $gallery.find('.woocommerce-product-gallery__trigger, .rtwpvg-trigger');
215 if (!zoomIcon) {
216 $triggers.remove();
217 return;
218 }
219 $triggers.html(zoomIcon);
220 });
221 }
222
223 // WooCommerce re-inits the gallery ~600ms after render (see above) and can
224 // reset the trigger, so inject after that window to win the race.
225 window.rtsbEditorInjectZoomIcon = function ($scope) {
226 ensureTriggerStyle();
227 setTimeout(function () {
228 injectZoomIcon($scope);
229 }, 700);
230 };
231
232 if (typeof elementorFrontend !== 'undefined' && elementorFrontend.hooks) {
233 elementorFrontend.hooks.addAction(
234 'frontend/element_ready/<?php echo esc_attr( $this->rtsb_base ); ?>.default',
235 function ($scope) {
236 window.rtsbEditorInjectZoomIcon($scope);
237 }
238 );
239 }
240
241 jQuery(function () {
242 window.rtsbEditorInjectZoomIcon();
243 });
244 })();
245
246 </script>
247 <?php
248 }
249
250 /**
251 * Widget Field.
252 *
253 * @param array $control Control.
254 *
255 * @return void
256 */
257 public function the_hooks( $control = [] ) {
258 if ( empty( $control ) ) {
259 return;
260 }
261 add_filter(
262 'woocommerce_product_thumbnails_columns',
263 function ( $col ) {
264 $col = 0;
265
266 return $col;
267 }
268 );
269 if ( empty( $control['show_zoom'] ) ) {
270 add_filter( 'rtwpvg_trigger_icon', '__return_false' );
271 }
272 if ( empty( $control['show_thumbnails'] ) ) {
273 add_filter( 'rtwpvg_show_product_thumbnail_slider', '__return_false' );
274 remove_action( 'woocommerce_product_thumbnails', 'woocommerce_show_product_thumbnails', 20 );
275 }
276 add_filter( 'rtwpvg_thumbnail_slider_js_options', [ $this, 'thumbnail_slider_js_options' ], 50 );
277 add_filter( 'rtwpvg_thumbnail_position', [ $this, 'thumbnail_position' ], 50 );
278 add_filter( 'rtwpvg_thumbnails_columns', [ $this, 'thumbnails_columns' ] );
279 add_filter( 'rtwpvg_sm_thumbnails_columns', [ $this, 'thumbnails_columns_sm' ] );
280 add_filter( 'rtwpvg_xs_thumbnails_columns', [ $this, 'thumbnails_columns_xs' ] );
281 // RTSB.
282 add_filter( 'rtsb/vg/thumbnails/position', [ $this, 'vg_thumbnail_position' ], 50 );
283 add_filter( 'rtsb/vg/lightbox', [ $this, 'vg_show_lightbox' ], 50 );
284 add_filter( 'rtsb/vg/lightbox/trigger/icon', [ $this, 'vg_lightbox' ], 50 );
285 add_filter( 'rtsb/vg/thumbnails/slider/options', [ $this, 'thumbnail_slider_js_options' ], 50, 2 );
286 add_filter( 'rtsb/vg/lightbox/position', [ $this, 'vg_lightbox_position' ], 50, 2 );
287 add_filter( 'rtsb/vg/gallery/columns', [ $this, 'vg_gallery_columns' ], 50, 2 );
288 }
289 /**
290 * Thumbnail Columns.
291 *
292 * @param int $col number.
293 *
294 * @return int
295 */
296 public function vg_gallery_columns( $col ) {
297 $controllers = $this->get_settings_for_display();
298 if ( ! empty( $controllers['gallery_thumbs_column']['size'] ) ) {
299 $col = absint( $controllers['gallery_thumbs_column']['size'] );
300 }
301 return $col;
302 }
303 /**
304 * Thumbnail slider js options.
305 *
306 * @param array $options Options.
307 *
308 * @return array
309 */
310 public function thumbnail_slider_js_options( $options ) {
311 $controllers = $this->get_settings_for_display();
312 if ( ! empty( $controllers['gallery_thumbs_column']['size'] ) ) {
313 $options['slidesPerView'] = absint( $controllers['gallery_thumbs_column']['size'] );
314 }
315 if ( ! empty( $controllers['gallery_thumbs_column_gap']['size'] ) ) {
316 $options['spaceBetween'] = $controllers['gallery_thumbs_column_gap']['size'];
317 }
318 return $options;
319 }
320
321 /**
322 * Thumbnails columns.
323 *
324 * @param int $column Column.
325 *
326 * @return mixed
327 */
328 public function thumbnails_columns( $column ) {
329 $controllers = $this->get_settings_for_display();
330 $column = ! empty( $controllers['gallery_thumbs_column']['size'] ) ? $controllers['gallery_thumbs_column']['size'] : $column;
331
332 return $column;
333 }
334
335 /**
336 * Thumbnails columns.
337 *
338 * @param int $column Column.
339 *
340 * @return mixed
341 */
342 public function thumbnails_columns_sm( $column ) {
343 $controllers = $this->get_settings_for_display();
344 $column = ! empty( $controllers['gallery_thumbs_column_tablet']['size'] ) ? $controllers['gallery_thumbs_column_tablet']['size'] : $column;
345
346 return $column;
347 }
348
349 /**
350 * Thumbnails columns.
351 *
352 * @param int $column Column.
353 *
354 * @return mixed
355 */
356 public function thumbnails_columns_xs( $column ) {
357 $controllers = $this->get_settings_for_display();
358 $column = ! empty( $controllers['gallery_thumbs_column_mobile']['size'] ) ? $controllers['gallery_thumbs_column_mobile']['size'] : $column;
359
360 return $column;
361 }
362 /**
363 * Thumbnail position.
364 *
365 * @param string $position Position.
366 *
367 * @return string
368 */
369 public function thumbnail_position( $position ) {
370 if ( ! function_exists( 'rtwpvg' ) || ! rtwpvg()->active_pro() || ! ( BuilderFns::is_product() || BuilderFns::is_quick_views_page() ) ) {
371 return $position;
372 }
373
374 $controllers = $this->get_settings_for_display();
375 $show_thumbnails = ! empty( $controllers['show_thumbnails'] ) ? $controllers['show_thumbnails'] : false;
376 if ( ! $show_thumbnails && 'grid' !== ( $controllers['image_layout'] ?? '' ) ) {
377 add_filter( 'rtwpvg_show_product_thumbnail_slider', '__return_false', 20 );
378 return 'bottom';
379 }
380 return ! empty( $controllers['image_layout'] ) ? $controllers['image_layout'] : 'bottom';
381 }
382 /**
383 * Thumbnail position.
384 *
385 * @param string $position Position.
386 *
387 * @return string
388 */
389 public function vg_thumbnail_position( $position ) {
390 if ( ! rtsb()->has_pro() ) {
391 return $position;
392 }
393 $controllers = $this->get_settings_for_display();
394 return ! empty( $controllers['image_layout'] ) ? $controllers['image_layout'] : 'bottom';
395 }
396
397 /**
398 * Whether the Variation Gallery lightbox trigger should render.
399 *
400 * The `rtsb/vg/lightbox` filter is a boolean gate (see the VG product-image
401 * template). It must reflect the widget's "Show Lightbox Icon?" (show_zoom)
402 * setting so disabling it actually hides the trigger — not the icon HTML, which
403 * is always truthy and would keep the trigger visible.
404 *
405 * @param bool $show Whether to show the lightbox trigger.
406 * @return bool
407 */
408 public function vg_show_lightbox( $show ) {
409 $controllers = $this->get_settings_for_display();
410 return ! empty( $controllers['show_zoom'] );
411 }
412 /**
413 * @param bool $lightbox Lightbox.
414 * @return mixed
415 */
416 public function vg_lightbox( $lightbox ) {
417 $controllers = $this->get_settings_for_display();
418 // The icons control always returns a populated array (`value`/`library`), so
419 // check the actual `value`; when it is cleared, fall back to the default SVG
420 // instead of rendering an empty icon.
421 return ! empty( $controllers['lightbox_icon']['value'] ) ? Fns::icons_manager( $controllers['lightbox_icon'] ) : $lightbox;
422 }
423 /**
424 * @param string $position position text.
425 * @return mixed
426 */
427 public function vg_lightbox_position( $position ) {
428 $controllers = $this->get_settings_for_display();
429 return ! empty( $controllers['vg_lightbox_position'] ) ? $controllers['vg_lightbox_position'] : $position;
430 }
431
432 /**
433 * Enqueue the Font Awesome library required by the selected lightbox icon.
434 *
435 * Elementor only auto-loads icon fonts for icons rendered through its own widget
436 * flow. The lightbox icon is injected into the Variation Gallery / WooCommerce
437 * gallery markup via a filter, so on the front end its font is never enqueued and
438 * the glyph renders empty — it appears in the editor only because Elementor always
439 * loads Font Awesome there. Enqueue the matching library so the icon shows on the
440 * front end. SVG icons carry no library and need no enqueue.
441 *
442 * @param array $icon Elementor icons control value.
443 * @return void
444 */
445 private function enqueue_lightbox_icon_font( $icon ) {
446 if ( empty( $icon['library'] ) ) {
447 return;
448 }
449 $handles = [
450 'fa-solid' => 'elementor-icons-fa-solid',
451 'fa-regular' => 'elementor-icons-fa-regular',
452 'fa-brands' => 'elementor-icons-fa-brands',
453 ];
454 if ( isset( $handles[ $icon['library'] ] ) ) {
455 wp_enqueue_style( $handles[ $icon['library'] ] );
456 }
457 }
458
459 /**
460 * Render Function
461 *
462 * @return void
463 */
464 protected function render() {
465 add_action( 'wp_footer', 'woocommerce_photoswipe' );
466 global $product, $post;
467 $_product = $product;
468 $product = Fns::get_product();
469 $controllers = $this->get_settings_for_display();
470 if ( empty( $controllers['show_module_badges'] ) ) {
471 add_filter( 'rtsb/module/badges/show', '__return_false', 99 );
472 }
473 // When "Show Thumbnails?" is off, hide the Variation Gallery thumbnail slider.
474 $hide_vg_thumbnails = empty( $controllers['show_thumbnails'] );
475
476 if ( ! empty( $controllers['show_zoom'] ) && ! empty( $controllers['lightbox_icon'] ) ) {
477 $this->enqueue_lightbox_icon_font( $controllers['lightbox_icon'] );
478 }
479
480 $this->theme_support();
481 $controllers['lightbox_icon'] = Fns::icons_manager( $controllers['lightbox_icon'] );
482 $data = [
483 'template' => 'elementor/single-product/product-images',
484 'controllers' => $controllers,
485 ];
486 $this->the_hooks( $controllers );
487
488 if ( $hide_vg_thumbnails ) {
489 add_filter( 'rtsb/vg/show/thumbnails', '__return_false' );
490 }
491
492 Fns::load_template( $data['template'], $data );
493
494 if ( $hide_vg_thumbnails ) {
495 remove_filter( 'rtsb/vg/show/thumbnails', '__return_false' );
496 }
497
498 if ( $this->is_builder_mode() ) {
499 $this->init_scripts();
500 }
501
502 if ( empty( $controllers['show_module_badges'] ) ) {
503 remove_filter( 'rtsb/module/badges/show', '__return_false', 99 );
504 }
505
506 $this->theme_support( 'render_reset' );
507 $product = $_product; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
508 }
509 }
510