Skip to content

Commit 3e2624e

Browse files
Merge pull request #1130 from cloudinary/feature/elementor-support
Add support for background images within Elementor
2 parents 413ebca + 8f7608f commit 3e2624e

File tree

2 files changed

+252
-1
lines changed

2 files changed

+252
-1
lines changed

php/class-plugin.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Cloudinary\Delivery\Lazy_Load;
1515
use Cloudinary\Delivery\Responsive_Breakpoints;
1616
use Cloudinary\Assets as CLD_Assets;
17+
use Cloudinary\Integrations\Elementor;
1718
use Cloudinary\Integrations\WPML;
1819
use Cloudinary\Media\Gallery;
1920
use Cloudinary\Sync\Storage;
@@ -31,7 +32,7 @@ final class Plugin {
3132
*
3233
* @since 0.1
3334
*
34-
* @var Admin|CLD_Assets|Connect|Dashboard|Deactivation|Delivery|Extensions|Gallery|Lazy_Load|Media|Meta_Box|Relate|Report|Responsive_Breakpoints|REST_API|State|Storage|SVG|Sync|URL[]|WPML|null
35+
* @var Admin|CLD_Assets|Connect|Dashboard|Deactivation|Delivery|Extensions|Gallery|Lazy_Load|Media|Meta_Box|Relate|Report|Responsive_Breakpoints|REST_API|State|Storage|SVG|Sync|URL[]|WPML|Elementor|null
3536
*/
3637
public $components;
3738
/**
@@ -136,6 +137,7 @@ public function plugins_loaded() {
136137
$this->components['metabox'] = new Meta_Box( $this );
137138
$this->components['url'] = new URL( $this );
138139
$this->components['wpml'] = new WPML( $this );
140+
$this->components['elementor'] = new Elementor( $this );
139141
$this->components['special_offer'] = new Special_Offer( $this );
140142
}
141143

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<?php
2+
/**
3+
* Elementor integration class for the Cloudinary plugin.
4+
*
5+
* @package Cloudinary
6+
*/
7+
8+
namespace Cloudinary\Integrations;
9+
10+
use Elementor\Core\Files\CSS\Post;
11+
use Elementor\Element_Base;
12+
use Elementor\Plugin;
13+
14+
/**
15+
* Class Elementor
16+
*/
17+
class Elementor extends Integrations {
18+
19+
/**
20+
* List of Elementor background image settings keys.
21+
*
22+
* @var array
23+
*/
24+
const ELEMENTOR_BACKGROUND_IMAGES = array(
25+
'background_image',
26+
'background_hover_image',
27+
'background_image_tablet',
28+
'background_hover_image_tablet',
29+
'background_image_mobile',
30+
'background_hover_image_mobile',
31+
'background_overlay_image',
32+
'background_overlay_hover_image',
33+
'background_overlay_image_tablet',
34+
'background_overlay_hover_image_tablet',
35+
'background_overlay_image_mobile',
36+
'background_overlay_hover_image_mobile',
37+
);
38+
39+
/**
40+
* Check if the integration can be enabled.
41+
*
42+
* @return bool
43+
*/
44+
public function can_enable() {
45+
return class_exists( 'Elementor\Plugin' );
46+
}
47+
48+
/**
49+
* Register hooks for the integration.
50+
*
51+
* @return void
52+
*/
53+
public function register_hooks() {
54+
add_action( 'elementor/element/parse_css', array( $this, 'replace_background_images_in_css' ), 10, 2 );
55+
add_action( 'cloudinary_flush_cache', array( $this, 'clear_elementor_css_cache' ) );
56+
}
57+
58+
/**
59+
* Replace all background images URLs with Cloudinary URLs, within the generated Elementor CSS file.
60+
*
61+
* @param Post $post_css The post CSS object.
62+
* @param Element_Base $element The Elementor element.
63+
* @return void
64+
*/
65+
public function replace_background_images_in_css( $post_css, $element ) {
66+
if ( ! method_exists( $element, 'get_settings_for_display' ) ) {
67+
return;
68+
}
69+
70+
$settings = $element->get_settings_for_display();
71+
$media = $this->plugin->get_component( 'media' );
72+
$delivery = $this->plugin->get_component( 'delivery' );
73+
74+
if ( ! $media || ! $delivery ) {
75+
return;
76+
}
77+
78+
foreach ( self::ELEMENTOR_BACKGROUND_IMAGES as $background_key ) {
79+
$background = null;
80+
$is_container = false;
81+
82+
if ( isset( $settings[ $background_key ] ) ) {
83+
// Elementor section/column elements store background settings without a leading underscore.
84+
$background = $settings[ $background_key ];
85+
$is_container = true;
86+
} elseif ( isset( $settings[ '_' . $background_key ] ) ) {
87+
// Elementor basic elements (e.g. heading) store background settings with a leading underscore.
88+
$background = $settings[ '_' . $background_key ];
89+
}
90+
91+
// If this specific background setting is not set, we can skip it and check for the next setting.
92+
if ( empty( $background ) || empty( $background['id'] ) ) {
93+
continue;
94+
}
95+
96+
$media_id = $background['id'];
97+
$media_size = isset( $background['size'] ) ? $background['size'] : array();
98+
99+
// Skip if the media is not deliverable via Cloudinary.
100+
if ( ! $delivery->is_deliverable( $media_id ) ) {
101+
continue;
102+
}
103+
104+
// Generate the Cloudinary URL.
105+
$cloudinary_url = $media->cloudinary_url( $media_id, $media_size );
106+
107+
// If URL generation failed, we should leave the original URL within the CSS.
108+
if ( empty( $cloudinary_url ) ) {
109+
continue;
110+
}
111+
112+
$unique_selector = $this->find_unique_selector( $post_css, $element );
113+
// If we can't find a unique selector via Elementor's internal API, we can't do any replacement.
114+
if ( null === $unique_selector ) {
115+
return;
116+
}
117+
118+
// Build the CSS selector and rule for background image replacement.
119+
$is_hover = ( strpos( $background_key, 'hover' ) !== false );
120+
$is_overlay = ( strpos( $background_key, 'overlay' ) !== false );
121+
$css_selector = $this->build_background_image_css_selector( $unique_selector, $is_container, $is_hover, $is_overlay );
122+
$css_rule = array( 'background-image' => "url('$cloudinary_url')" );
123+
124+
// Retrieve the specific media query rule for non-desktop devices based on the setting key.
125+
$media_query = null;
126+
if ( strpos( $background_key, 'tablet' ) !== false ) {
127+
$media_query = array( 'max' => 'tablet' );
128+
} elseif ( strpos( $background_key, 'mobile' ) !== false ) {
129+
$media_query = array( 'max' => 'mobile' );
130+
}
131+
132+
$success = $this->override_elementor_css_rule( $post_css, $css_selector, $css_rule, $media_query );
133+
if ( ! $success ) {
134+
// If we couldn't override the CSS rule, likely due to Elementor internal API changes, we should stop further processing.
135+
return;
136+
}
137+
}
138+
}
139+
140+
/**
141+
* Clear Elementor CSS cache.
142+
* This is called when Cloudinary cache is flushed, so that any change in media URLs is reflected in Elementor CSS files.
143+
*
144+
* @return void
145+
*/
146+
public function clear_elementor_css_cache() {
147+
if ( class_exists( 'Elementor\Plugin' ) ) {
148+
$elementor = Plugin::instance();
149+
$elementor->files_manager->clear_cache();
150+
}
151+
}
152+
153+
/**
154+
* Find the unique selector for an Elementor element.
155+
* Double-checks if the method exists before calling it, to ensure compatibility with different Elementor versions.
156+
*
157+
* @param Post $post_css The post CSS object.
158+
* @param Element_Base $element The Elementor element.
159+
*
160+
* @return string|null
161+
*/
162+
private function find_unique_selector( $post_css, $element ) {
163+
if ( ! method_exists( $element, 'get_unique_selector' ) ) {
164+
return null;
165+
}
166+
167+
return $post_css->get_element_unique_selector( $element );
168+
}
169+
170+
/**
171+
* Override the Elementor CSS rule for a specific selector.
172+
* Double-checks if the method exists before calling it, to ensure compatibility with different Elementor versions.
173+
*
174+
* @param Post $post_css The post CSS object.
175+
* @param string $css_selector The CSS selector.
176+
* @param array $css_rule The CSS rule to apply.
177+
* @param array|null $media_query The media query conditions. Null for default (desktop) styles.
178+
*
179+
* @return bool True if the rule could be overridden, false if the internal Elementor methods aren't available.
180+
*/
181+
private function override_elementor_css_rule( $post_css, $css_selector, $css_rule, $media_query ) {
182+
if ( ! method_exists( $post_css, 'get_stylesheet' ) ) {
183+
return false;
184+
}
185+
186+
$stylesheet = $post_css->get_stylesheet();
187+
if ( ! method_exists( $stylesheet, 'add_rules' ) ) {
188+
return false;
189+
}
190+
191+
$stylesheet->add_rules( $css_selector, $css_rule, $media_query );
192+
return true;
193+
}
194+
195+
/**
196+
* Build the full CSS selector for background image replacement.
197+
* We try to match the exact Elementor formatting and rules, so that our CSS overrides the previous rules,
198+
* instead of adding new rules within the CSS which may not apply for specific edge cases (e.g. specific child elements).
199+
*
200+
* @param string $unique_selector The unique selector for the element.
201+
* @param bool $is_container Whether the element is a container (section/column).
202+
* @param bool $is_hover Whether the background is for hover state.
203+
* @param bool $is_overlay Whether the background is for an overlay.
204+
*
205+
* @return string
206+
*/
207+
private function build_background_image_css_selector( $unique_selector, $is_container, $is_hover, $is_overlay ) {
208+
if ( $is_overlay ) {
209+
// Overlay backgrounds need to target multiple pseudo-elements and child elements.
210+
$overlay_selector = sprintf(
211+
'%1$s%2$s::before,
212+
%1$s%2$s > .elementor-background-video-container::before,
213+
%1$s%2$s > .e-con-inner > .elementor-background-video-container::before,
214+
%1$s > .elementor-background-slideshow%2$s::before,
215+
%1$s > .e-con-inner > .elementor-background-slideshow%2$s::before',
216+
$unique_selector,
217+
$is_hover ? ':hover' : ''
218+
);
219+
220+
// For non-hover overlays, we need to also target motion effects layers.
221+
if ( ! $is_hover ) {
222+
$overlay_selector = sprintf(
223+
'%1$s,
224+
%2$s > .elementor-motion-effects-container > .elementor-motion-effects-layer::before',
225+
$overlay_selector,
226+
$unique_selector
227+
);
228+
}
229+
230+
// Replace any newline and extra spaces to match the exact Elementor formatting.
231+
return preg_replace( '/\s+/', ' ', $overlay_selector );
232+
}
233+
// For hover backgrounds, we simply append :hover to the unique selector.
234+
if ( $is_hover ) {
235+
return $unique_selector . ':hover';
236+
}
237+
238+
// For non-container elements, we can return the unique selector as is.
239+
if ( ! $is_container ) {
240+
return $unique_selector;
241+
}
242+
243+
// For container elements, we need to target both the element itself and its motion effects layers.
244+
return sprintf(
245+
'%1$s:not(.elementor-motion-effects-element-type-background), %1$s > .elementor-motion-effects-container > .elementor-motion-effects-layer',
246+
$unique_selector
247+
);
248+
}
249+
}

0 commit comments

Comments
 (0)