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 / Controllers / Hooks / BuilderHooks.php

BuilderHooks.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Controllers/Hooks/BuilderHooks.php

403 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main initialization class.
4 *
5 * @package RadiusTheme\SB
6 */
7
8 namespace RadiusTheme\SB\Controllers\Hooks;
9
10 defined( 'ABSPATH' ) || exit();
11
12 use RadiusTheme\SB\Helpers\Fns;
13 use RadiusTheme\SB\Helpers\BuilderFns;
14 use RadiusTheme\SB\Helpers\ElementorDataMap;
15 use RadiusTheme\SB\Models\TemplateSettings;
16 use RadiusTheme\SB\Traits\SingletonTrait;
17
18 /**
19 * Builder Frontend.
20 */
21 class BuilderHooks {
22 /**
23 * Builder page id.
24 *
25 * @var integer
26 */
27 private $builder_page_id = 0;
28
29 /**
30 * Page Edit By.
31 *
32 * @var string
33 */
34 private $page_edit_with = '';
35
36 /**
37 * Internal cache.
38 *
39 * @var array
40 */
41 private static $cache = [];
42
43 /**
44 * Singleton.
45 */
46 use SingletonTrait;
47
48 /**
49 * Final constructor.
50 */
51 private function __construct() {
52 // Template Redirect hook run after the global query.
53 add_action( 'template_redirect', [ $this, 'frontend_init' ] );
54 add_filter( 'rtsb/builder/set/current/page/type', [ $this, 'builder_page_type' ] );
55 add_filter( 'body_class', [ $this, 'product_page_body_class' ] );
56 add_action( 'pre_get_posts', [ $this, 'override_archive_posts_per_page' ] );
57 }
58
59 /**
60 * Override posts_per_page for shop/archive pages based on widget setting.
61 *
62 * @param \WP_Query $query The WP_Query instance.
63 *
64 * @return void
65 */
66 public function override_archive_posts_per_page( $query ) {
67 if ( is_admin() || ! $query->is_main_query() ) {
68 return;
69 }
70
71 if ( ! ( is_shop() || is_product_taxonomy() ) ) {
72 return;
73 }
74
75 $type = is_shop() ? 'shop' : 'archive';
76 $option_name = BuilderFns::option_name( $type );
77 $builder_page_id = TemplateSettings::instance()->get_option( $option_name );
78
79 if ( empty( $builder_page_id ) ) {
80 $option_name = BuilderFns::option_name( $type, true );
81 $builder_page_id = TemplateSettings::instance()->get_option( $option_name );
82 }
83
84 // Fallback: use shop template for archive pages when no archive template exists.
85 if ( empty( $builder_page_id ) && 'archive' === $type ) {
86 $option_name = BuilderFns::option_name( 'shop' );
87 $builder_page_id = TemplateSettings::instance()->get_option( $option_name );
88
89 if ( empty( $builder_page_id ) ) {
90 $option_name = BuilderFns::option_name( 'shop', true );
91 $builder_page_id = TemplateSettings::instance()->get_option( $option_name );
92 }
93 }
94
95 if ( empty( $builder_page_id ) ) {
96 return;
97 }
98
99 $per_page = $this->get_archive_widget_per_page( $builder_page_id );
100
101 if ( $per_page ) {
102 $query->set( 'posts_per_page', $per_page );
103 }
104 }
105
106 /**
107 * Get posts_per_page from the ProductsArchive widget in Elementor data.
108 *
109 * @param int $post_id Builder page ID.
110 *
111 * @return int|false The per page value or false if not set.
112 */
113 private function get_archive_widget_per_page( $post_id ) {
114 $cache_key = 'rtsb_archive_per_page_' . $post_id;
115
116 if ( isset( self::$cache[ $cache_key ] ) ) {
117 return self::$cache[ $cache_key ];
118 }
119
120 // Check default layout widget first.
121 $widgets = ElementorDataMap::instance()->get_widget_data( 'rtsb-products-archive', null, $post_id );
122
123 if ( ! empty( $widgets ) ) {
124 $widget = reset( $widgets );
125 $per_page = isset( $widget['settings']['posts_per_page'] ) && '' !== $widget['settings']['posts_per_page']
126 ? absint( $widget['settings']['posts_per_page'] )
127 : false;
128
129 self::$cache[ $cache_key ] = $per_page;
130
131 return $per_page;
132 }
133
134 // Check custom layout widget.
135 $widgets = ElementorDataMap::instance()->get_widget_data( 'rtsb-products-archive-custom', null, $post_id );
136
137 if ( ! empty( $widgets ) ) {
138 $widget = reset( $widgets );
139 $per_page = isset( $widget['settings']['posts_per_page'] ) && '' !== $widget['settings']['posts_per_page']
140 ? absint( $widget['settings']['posts_per_page'] )
141 : false;
142
143 self::$cache[ $cache_key ] = $per_page;
144
145 return $per_page;
146 }
147
148 self::$cache[ $cache_key ] = false;
149
150 return false;
151 }
152
153 /**
154 * Builder page Body class.
155 *
156 * @param array $classes class list.
157 * @return array.
158 */
159 public function product_page_body_class( $classes ) {
160 $classes[] = 'rtsb-shopbuilder-plugin rtsb-shopbuilder-v-' . RTSB_VERSION . ' rtsb_theme_' . rtsb()->current_theme;
161
162 if ( defined( 'RTSBPRO_VERSION' ) ) {
163 $classes[] = 'rtsb-shopbuilder-pro-v-' . RTSBPRO_VERSION;
164 }
165
166 if ( defined( 'ELEMENTOR_PRO_VERSION' ) ) {
167 $classes[] = 'rtsb-elementor-pro-v-' . ELEMENTOR_PRO_VERSION;
168 }
169 if ( Fns::is_woocommerce() || BuilderFns::is_builder_preview() ) {
170 $classes[] = 'woocommerce-page';
171 }
172
173 if ( BuilderFns::is_product() ) {
174 $classes[] = 'single-product';
175 }
176
177 if ( BuilderFns::is_shop() ) {
178 $classes[] = 'woocommerce-shop';
179 }
180
181 if ( BuilderFns::is_cart() ) {
182 $classes[] = 'woocommerce-cart';
183 } else {
184 $classes[] = 'woocommerce';
185 }
186
187 if ( BuilderFns::is_checkout() ) {
188 $classes[] = 'woocommerce-checkout ';
189 }
190
191 if ( rtsb()->has_pro() ) {
192 $disable_cart = Fns::is_guest_feature_disabled( 'hide_cart_page', '' );
193 $disable_checkout = Fns::is_guest_feature_disabled( 'hide_checkout_page', '' );
194
195 if ( $disable_cart || $disable_checkout ) {
196 $classes[] = 'rtsb-not-visible';
197 }
198 }
199
200 $hide_notification = Fns::get_option( 'general', 'notification', 'hide_notification', '' );
201
202 if ( 'on' === $hide_notification ) {
203 $classes[] = 'rtsb-notifications-hidden';
204 }
205
206 return $classes;
207 }
208 /**
209 * Undocumented function
210 *
211 * @return void
212 */
213 public function frontend_init() {
214 $this->builder_page_id = BuilderFns::builder_page_id_by_page();
215 $this->page_edit_with = Fns::page_edit_with( $this->builder_page_id );
216 if ( $this->builder_page_id ) {
217 if ( did_action( 'elementor/loaded' ) && 'elementor' === $this->page_edit_with ) {
218 $this->elementor_frontend();
219 } elseif ( 'gutenberg' === $this->page_edit_with ) {
220 $this->gutenberg_frontend();
221 }
222 }
223 }
224 /**
225 * Appl for gutenberg.
226 *
227 * @return void
228 */
229 public function elementor_frontend() {
230 // Priority 15: after Elementor registers style handles (priority 5) and before
231 // its enqueue_styles / after_enqueue_post_styles pass (priority 20) that renders
232 // the atomic-widget (V4) styles collected via the elementor/post/render action.
233 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_elementor_template_css' ], 15 );
234 add_action( 'rtsb/builder/before/header', [ $this, 'el_body_class' ] );
235 add_action( 'rtsb/builder/template/main/content', [ $this, 'elementor_template_main_content' ] );
236 }
237
238 /**
239 * Enqueue the Elementor builder template CSS in the document head.
240 *
241 * ShopBuilder renders the Elementor template on a WooCommerce object (e.g. a product),
242 * so Elementor's own wp_enqueue_scripts pass only fires elementor/post/render for the
243 * queried object — never for the builder template. As a result the template's
244 * document CSS (post-{id}.css) and, on Elementor V4, its atomic-widget styles
245 * (local-{id}-*.css rendered on elementor/frontend/after_enqueue_post_styles) are
246 * never loaded on builder pages although they load on Elementor's own preview.
247 *
248 * Firing elementor/post/render for the template id here — before Elementor's
249 * priority-20 style pass — registers the template with the atomic styles manager so
250 * its styles are generated and enqueued. enqueue_styles() loads the classic post CSS.
251 *
252 * @return void
253 */
254 public function enqueue_elementor_template_css() {
255 if ( ! $this->builder_page_id || ! class_exists( '\Elementor\Plugin' ) ) {
256 return;
257 }
258
259 $documents = \Elementor\Plugin::$instance->documents;
260
261 if ( ! $documents ) {
262 return;
263 }
264
265 $document = $documents->get( $this->builder_page_id );
266
267 if ( ! $document ) {
268 return;
269 }
270
271 // Register the template with Elementor's render pipeline (atomic V4 styles manager
272 // collects these post ids and renders their styles on after_enqueue_post_styles).
273 do_action( 'elementor/post/render', $this->builder_page_id );
274
275 // Classic per-post document CSS (post-{id}.css) and font dependencies.
276 if ( method_exists( $document, 'enqueue_styles' ) ) {
277 $document->enqueue_styles();
278 }
279
280 // Elementor hooks its own frontend enqueue_styles() pass only when the
281 // *queried* object is a singular Elementor document (Frontend::init).
282 // On ShopBuilder frontend pages the queried object (shop/archive/cart/
283 // checkout/product) is never the builder template, so that pass never
284 // runs — leaving the elementor-frontend base stylesheet and, critically,
285 // the V4 atomic styles manager (which renders the template's flexbox/
286 // container CSS on elementor/frontend/after_enqueue_post_styles)
287 // un-triggered, so containers collapse to full width. We drive the pass
288 // manually below, but ONLY on the real frontend: in the Elementor editor
289 // preview and the ShopBuilder single-template preview the queried object
290 // *is* the template (singular + built with Elementor), so Elementor runs
291 // its own pass and firing ours early would interfere with the editor
292 // preview load (the panel waits on that iframe). Skip both cases.
293 $preview = \Elementor\Plugin::$instance->preview ?? null;
294 $is_editor_view = $preview && method_exists( $preview, 'is_editor_or_preview' ) && $preview->is_editor_or_preview();
295 $queried_document = $documents->get( get_the_ID() );
296 $elementor_will_load = is_singular() && $queried_document && $queried_document->is_built_with_elementor();
297 $frontend = \Elementor\Plugin::$instance->frontend ?? null;
298
299 if ( $is_editor_view || $elementor_will_load || ! $frontend || ! method_exists( $frontend, 'enqueue_styles' ) ) {
300 return;
301 }
302
303 // Enqueues the elementor-frontend base stylesheet; one-shot (internal
304 // static guard) so it is a no-op if something already ran it.
305 $frontend->enqueue_styles();
306
307 // The V4 Atomic_Styles_Manager only renders CSS for the post ids collected
308 // via elementor/post/render *before* the style pass runs. When another
309 // plugin triggers that pass first — e.g. Elementor Pro's Theme Builder
310 // rendering its own archive location — our builder template id is
311 // registered too late, so its per-template file (local-{id}-frontend-*.css)
312 // is written to disk but never enqueued, and containers stay full width.
313 // Re-firing the atomic trigger after registering the template above makes
314 // the manager enqueue that file. This action's only listener is the atomic
315 // styles manager, and wp_enqueue_style() de-duplicates handles, so the
316 // extra pass is safe.
317 do_action( 'elementor/frontend/after_enqueue_post_styles' );
318 }
319
320 /**
321 * Appl for Elementor.
322 *
323 * @return void
324 */
325 public function gutenberg_frontend() {
326 add_action( 'rtsb/builder/template/main/content', [ $this, 'gutenberg_template_main_content' ] );
327 }
328 /**
329 * Apply For the supported builder page.
330 *
331 * @param string $type string.
332 * @return string
333 */
334 public function builder_page_type( $type ) {
335
336 if ( BuilderFns::is_shop() ) {
337 $type = 'shop';
338 } elseif ( BuilderFns::is_archive() ) {
339 $type = 'archive';
340 } elseif ( BuilderFns::is_product() ) {
341 $type = 'product';
342 } elseif ( BuilderFns::is_checkout() ) {
343 $type = 'checkout';
344 wp_enqueue_script( 'wc-checkout' );
345 } elseif ( BuilderFns::is_cart() ) {
346 $type = 'cart';
347 }
348 wp_enqueue_style( Fns::optimized_handle( 'rtsb-frontend' ) );
349 wp_enqueue_script( Fns::optimized_handle( 'rtsb-public' ) );
350
351 return apply_filters( 'rtsb/builder/set/current/page/type/external', $type );
352 }
353
354 /**
355 * Appl for Elementor.
356 *
357 * @return void
358 */
359 public function el_body_class() {
360 if ( ! class_exists( '\Elementor\Plugin' ) || empty( \Elementor\Plugin::$instance->frontend ) ) {
361 return;
362 }
363
364 $page_template = get_post_meta( $this->builder_page_id, '_wp_page_template', true );
365
366 if ( 'elementor_canvas' === $page_template ) {
367 \Elementor\Plugin::$instance->frontend->add_body_class( 'elementor-template-canvas' );
368 } elseif ( 'elementor_header_footer' === $page_template ) {
369 \Elementor\Plugin::$instance->frontend->add_body_class( 'elementor-template-full-width' );
370 }
371 }
372
373 /**
374 * Apply for Elementor.
375 *
376 * @return void
377 */
378 public function elementor_template_main_content() {
379 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
380 echo BuilderFns::get_builder_content( $this->builder_page_id );
381 }
382 /**
383 * Apply for Gutenberg.
384 *
385 * @return void
386 */
387 public function gutenberg_template_main_content() {
388 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
389 $output = '';
390 $content = get_the_content( null, false, $this->builder_page_id );
391 if ( has_blocks( $content ) ) {
392 $blocks = parse_blocks( $content );
393 foreach ( $blocks as $block ) {
394 $output .= render_block( $block );
395 }
396 } else {
397 $content = apply_filters( 'the_content', $content );
398 $output = str_replace( ']]>', ']]&gt;', $content );
399 }
400 echo wp_kses_post( $output );
401 }
402 }
403