| 1 |
<?php |
| 2 |
|
| 3 |
namespace Wpxero\Marqueex\Integrations\Elementor; |
| 4 |
|
| 5 |
use Wpxero\Marqueex\Traits\Singleton; |
| 6 |
use Wpxero\Marqueex\Core\Settings; |
| 7 |
use Wpxero\Marqueex\Core\Upsell; |
| 8 |
|
| 9 |
if (!defined('ABSPATH')) { |
| 10 |
exit; |
| 11 |
} |
| 12 |
|
| 13 |
// Import WordPress functions |
| 14 |
use function add_action; |
| 15 |
use function get_option; |
| 16 |
use function wp_enqueue_script; |
| 17 |
use function wp_enqueue_style; |
| 18 |
use function wp_register_script; |
| 19 |
use function wp_register_style; |
| 20 |
use function plugins_url; |
| 21 |
use function admin_url; |
| 22 |
use function wp_create_nonce; |
| 23 |
use function __; |
| 24 |
|
| 25 |
/** |
| 26 |
* Elementor Integration Class |
| 27 |
*/ |
| 28 |
class ElementorIntegration { |
| 29 |
use Singleton; |
| 30 |
|
| 31 |
/** |
| 32 |
* Oldest Elementor release these widgets support. |
| 33 |
* |
| 34 |
* `elementor/widgets/register` and `$widgets_manager->register()` — the APIs |
| 35 |
* used below — arrived in 3.5.0. On anything older the widgets silently fail |
| 36 |
* to register, which previously looked like a broken plugin. |
| 37 |
*/ |
| 38 |
const MIN_ELEMENTOR_VERSION = '3.5.0'; |
| 39 |
|
| 40 |
/** |
| 41 |
* Constructor |
| 42 |
*/ |
| 43 |
private function __construct() { |
| 44 |
add_action('init', [$this, 'init']); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Initialize Elementor integration |
| 49 |
*/ |
| 50 |
public function init() { |
| 51 |
// Check if Elementor is active |
| 52 |
if (!$this->is_elementor_active()) { |
| 53 |
return; |
| 54 |
} |
| 55 |
|
| 56 |
if (!$this->is_elementor_supported()) { |
| 57 |
add_action('admin_notices', [$this, 'outdated_elementor_notice']); |
| 58 |
return; |
| 59 |
} |
| 60 |
|
| 61 |
// Check if integration is enabled |
| 62 |
$elementor_enabled = Settings::get('builder_support.elementor.enabled', true); |
| 63 |
|
| 64 |
if (!$elementor_enabled) { |
| 65 |
return; |
| 66 |
} |
| 67 |
|
| 68 |
// Hook into Elementor |
| 69 |
add_action('elementor/widgets/register', [$this, 'register_widgets']); |
| 70 |
add_action('elementor/elements/categories_registered', [$this, 'add_widget_categories'], 1); |
| 71 |
add_action('elementor/editor/before_enqueue_scripts', [$this, 'enqueue_editor_scripts']); |
| 72 |
// add_action('elementor/frontend/after_enqueue_scripts', [$this, 'enqueue_elementor_assets']); |
| 73 |
add_action('wp_enqueue_scripts', [$this, 'enqueue_elementor_assets']); |
| 74 |
|
| 75 |
// Marketing: advertise the Pro scroll-interaction features (mouse wheel + drag) |
| 76 |
// as a locked section inside the free marquee widgets. Hidden when premium is |
| 77 |
// active — the pro add-on then injects the real working controls instead. |
| 78 |
// Note: post-marquee's section id is `marquee_settings`; the others use |
| 79 |
// `marquee_settings_section`. |
| 80 |
add_action('elementor/element/marqueex-text-marquee/marquee_settings_section/after_section_end', [$this, 'add_interaction_upsell_section'], 15, 2); |
| 81 |
add_action('elementor/element/marqueex-image-marquee/marquee_settings_section/after_section_end', [$this, 'add_interaction_upsell_section'], 15, 2); |
| 82 |
add_action('elementor/element/marqueex-news-ticker/marquee_settings_section/after_section_end', [$this, 'add_interaction_upsell_section'], 15, 2); |
| 83 |
add_action('elementor/element/marqueex-post-marquee/marquee_settings/after_section_end', [$this, 'add_interaction_upsell_section'], 15, 2); |
| 84 |
} |
| 85 |
|
| 86 |
/** |
| 87 |
* Add a locked "Scroll Interactions" section (Mouse Wheel + Drag) to free |
| 88 |
* marquee widgets. Shown only to non-premium users — marqueex-pro registers |
| 89 |
* the real working section at priority 20 when licensed. |
| 90 |
*/ |
| 91 |
public function add_interaction_upsell_section($element, $args) { |
| 92 |
if (!Upsell::should_show()) { |
| 93 |
return; |
| 94 |
} |
| 95 |
|
| 96 |
$element->start_controls_section( |
| 97 |
'marqueex_interactions_upsell', |
| 98 |
[ |
| 99 |
'label' => __('Scroll Interactions', 'marqueex'), |
| 100 |
'tab' => \Elementor\Controls_Manager::TAB_CONTENT, |
| 101 |
] |
| 102 |
); |
| 103 |
|
| 104 |
Upsell::add_elementor_notice($element, 'pro_interactions_notice', [ |
| 105 |
'title' => __('Let visitors drive the marquee', 'marqueex'), |
| 106 |
'description' => __('Turn a strip that scrolls past people into one they can grab. Visitors spin the wheel to speed it up or reverse it, or drag it to scrub through — and it eases back to your set speed on its own.', 'marqueex'), |
| 107 |
'items' => [ |
| 108 |
__('Mouse wheel control with adjustable sensitivity', 'marqueex'), |
| 109 |
__('Drag to scroll, with flick momentum on release', 'marqueex'), |
| 110 |
__('Optional "scroll sets direction" mode', 'marqueex'), |
| 111 |
], |
| 112 |
'cta' => __('Unlock scroll interactions', 'marqueex'), |
| 113 |
'source' => 'elementor-scroll-interactions', |
| 114 |
]); |
| 115 |
|
| 116 |
$element->end_controls_section(); |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Check if Elementor is active |
| 121 |
*/ |
| 122 |
private function is_elementor_active() { |
| 123 |
return class_exists('\Elementor\Plugin'); |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Whether the installed Elementor is new enough to register our widgets. |
| 128 |
* |
| 129 |
* @return bool |
| 130 |
*/ |
| 131 |
private function is_elementor_supported() { |
| 132 |
if (!defined('ELEMENTOR_VERSION')) { |
| 133 |
// Version constant missing — assume supported rather than disabling the |
| 134 |
// integration on an unexpected build. |
| 135 |
return true; |
| 136 |
} |
| 137 |
|
| 138 |
return version_compare(ELEMENTOR_VERSION, self::MIN_ELEMENTOR_VERSION, '>='); |
| 139 |
} |
| 140 |
|
| 141 |
/** |
| 142 |
* Explain why the MarqueeX widgets are missing from the Elementor panel. |
| 143 |
*/ |
| 144 |
public function outdated_elementor_notice() { |
| 145 |
if (!current_user_can('update_plugins')) { |
| 146 |
return; |
| 147 |
} |
| 148 |
?> |
| 149 |
<div class="notice notice-warning"> |
| 150 |
<p> |
| 151 |
<?php |
| 152 |
printf( |
| 153 |
/* translators: 1: required Elementor version, 2: installed Elementor version */ |
| 154 |
esc_html__('MarqueeX needs Elementor %1$s or newer to add its widgets. You are running %2$s — the MarqueeX widgets will not appear in the Elementor panel until it is updated. The MarqueeX blocks in the block editor are unaffected.', 'marqueex'), |
| 155 |
esc_html(self::MIN_ELEMENTOR_VERSION), |
| 156 |
esc_html(defined('ELEMENTOR_VERSION') ? ELEMENTOR_VERSION : __('an older version', 'marqueex')) |
| 157 |
); |
| 158 |
?> |
| 159 |
</p> |
| 160 |
</div> |
| 161 |
<?php |
| 162 |
} |
| 163 |
|
| 164 |
/** |
| 165 |
* Single source of truth for the registered Elementor widgets. |
| 166 |
* |
| 167 |
* Keyed by the settings element key. Each entry describes the widget class |
| 168 |
* plus its registered style/script handles, files and dependencies, so |
| 169 |
* registration, asset enqueuing and status reporting all derive from here |
| 170 |
* (previously these three lists were maintained separately and drifted). |
| 171 |
* |
| 172 |
* |
| 173 |
* @return array<string, array> |
| 174 |
*/ |
| 175 |
private function get_widget_definitions() { |
| 176 |
$frontend_deps = ['jquery', 'marqueex-elementor-frontend']; |
| 177 |
|
| 178 |
return [ |
| 179 |
'image_marquee' => [ |
| 180 |
'class' => 'ImageMarquee', |
| 181 |
'style_handle' => 'marqueex-image-marquee', |
| 182 |
'style_css' => 'image-marquee.css', |
| 183 |
'script_handle' => 'marqueex-image-marquee', |
| 184 |
'script_js' => 'image-marquee.js', |
| 185 |
'script_deps' => $frontend_deps, |
| 186 |
], |
| 187 |
'text_marquee' => [ |
| 188 |
'class' => 'TextMarquee', |
| 189 |
'style_handle' => 'marqueex-text-marquee', |
| 190 |
'style_css' => 'text-marquee.css', |
| 191 |
'script_handle' => 'marqueex-text-marquee', |
| 192 |
'script_js' => 'text-marquee.js', |
| 193 |
'script_deps' => $frontend_deps, |
| 194 |
], |
| 195 |
'animated_heading' => [ |
| 196 |
'class' => 'AnimatedHeading', |
| 197 |
'style_handle' => 'marqueex-animated-heading-style', |
| 198 |
'style_css' => 'animated-heading.css', |
| 199 |
'script_handle' => 'marqueex-animated-heading', |
| 200 |
'script_js' => 'animated-heading.js', |
| 201 |
// Animated Heading bundles its own animation logic; no frontend dep. |
| 202 |
'script_deps' => ['jquery'], |
| 203 |
], |
| 204 |
'animated_word_roller' => [ |
| 205 |
'class' => 'AnimatedWordRoller', |
| 206 |
'style_handle' => 'marqueex-animated-word-roller-style', |
| 207 |
'style_css' => 'animated-word-roller.css', |
| 208 |
'script_handle' => 'marqueex-animated-word-roller', |
| 209 |
'script_js' => 'animated-word-roller.js', |
| 210 |
'script_deps' => $frontend_deps, |
| 211 |
], |
| 212 |
'news_ticker' => [ |
| 213 |
'class' => 'NewsTicker', |
| 214 |
'style_handle' => 'marqueex-news-ticker', |
| 215 |
'style_css' => 'news-ticker.css', |
| 216 |
'script_handle' => 'marqueex-news-ticker', |
| 217 |
'script_js' => 'news-ticker.js', |
| 218 |
'script_deps' => $frontend_deps, |
| 219 |
], |
| 220 |
'post_marquee' => [ |
| 221 |
'class' => 'PostMarquee', |
| 222 |
'style_handle' => 'marqueex-post-marquee', |
| 223 |
'style_css' => 'post-marquee.css', |
| 224 |
'script_handle' => 'marqueex-post-marquee', |
| 225 |
'script_js' => 'post-marquee.js', |
| 226 |
'script_deps' => $frontend_deps, |
| 227 |
], |
| 228 |
'team_members_marquee' => [ |
| 229 |
'class' => 'TeamMembersMarquee', |
| 230 |
'style_handle' => 'marqueex-team-members-marquee', |
| 231 |
'style_css' => 'team-members-marquee.css', |
| 232 |
'script_handle' => 'marqueex-team-members-marquee', |
| 233 |
'script_js' => 'team-members-marquee.js', |
| 234 |
'script_deps' => $frontend_deps, |
| 235 |
], |
| 236 |
'testimonial_marquee' => [ |
| 237 |
'class' => 'TestimonialMarquee', |
| 238 |
'style_handle' => 'marqueex-testimonial-marquee', |
| 239 |
'style_css' => 'testimonial-marquee.css', |
| 240 |
'script_handle' => 'marqueex-testimonial-marquee', |
| 241 |
'script_js' => 'testimonial-marquee.js', |
| 242 |
'script_deps' => $frontend_deps, |
| 243 |
], |
| 244 |
]; |
| 245 |
} |
| 246 |
|
| 247 |
/** |
| 248 |
* Whether a widget is enabled in settings (defaults to true). |
| 249 |
* |
| 250 |
* @param string $key Settings element key. |
| 251 |
* @return bool |
| 252 |
*/ |
| 253 |
private function is_widget_enabled($key) { |
| 254 |
$elements = Settings::get('builder_support.elementor.elements', []); |
| 255 |
return (bool) ($elements[$key] ?? true); |
| 256 |
} |
| 257 |
|
| 258 |
/** |
| 259 |
* Register MarqueeX widgets |
| 260 |
*/ |
| 261 |
public function register_widgets($widgets_manager) { |
| 262 |
foreach ($this->get_widget_definitions() as $key => $def) { |
| 263 |
if (!$this->is_widget_enabled($key)) { |
| 264 |
continue; |
| 265 |
} |
| 266 |
require_once __DIR__ . '/Widgets/' . $def['class'] . '.php'; |
| 267 |
$fqcn = __NAMESPACE__ . '\\Widgets\\' . $def['class']; |
| 268 |
$widgets_manager->register(new $fqcn()); |
| 269 |
} |
| 270 |
} |
| 271 |
|
| 272 |
/** |
| 273 |
* Add custom widget categories |
| 274 |
*/ |
| 275 |
public function add_widget_categories($elements_manager) { |
| 276 |
$elements_manager->add_category( |
| 277 |
'marqueex', |
| 278 |
[ |
| 279 |
'title' => __('MarqueeX', 'marqueex'), |
| 280 |
'icon' => 'fa fa-sliders', |
| 281 |
] |
| 282 |
); |
| 283 |
} |
| 284 |
|
| 285 |
/** |
| 286 |
* Enqueue editor scripts |
| 287 |
*/ |
| 288 |
public function enqueue_editor_scripts() { |
| 289 |
wp_enqueue_style( |
| 290 |
'marqueex-elementor-editor', |
| 291 |
plugins_url('/assets/css/elementor-editor.css', MARQUEEX_FILE), |
| 292 |
[], |
| 293 |
MARQUEEX_VERSION |
| 294 |
); |
| 295 |
} |
| 296 |
|
| 297 |
/** |
| 298 |
* Enqueue frontend scripts |
| 299 |
*/ |
| 300 |
public function enqueue_elementor_assets() { |
| 301 |
// Register (don't enqueue) the shared base styles. Each widget style |
| 302 |
// below depends on this handle, so common.css loads only on pages that |
| 303 |
// actually render a MarqueeX widget — not on every front-end page. |
| 304 |
wp_register_style( |
| 305 |
'marqueex-common', |
| 306 |
plugins_url('/assets/css/common.css', MARQUEEX_FILE), |
| 307 |
[], |
| 308 |
MARQUEEX_VERSION |
| 309 |
); |
| 310 |
wp_register_script( |
| 311 |
'marqueex-elementor-frontend', |
| 312 |
plugins_url('/assets/js/elementor-frontend.js', MARQUEEX_FILE), |
| 313 |
['jquery'], |
| 314 |
MARQUEEX_VERSION, |
| 315 |
true |
| 316 |
); |
| 317 |
|
| 318 |
// Register each widget's style (chained to common.css) and script from |
| 319 |
// the canonical definition map. Elementor enqueues these on demand via |
| 320 |
// each widget's get_style_depends()/get_script_depends(). |
| 321 |
foreach ($this->get_widget_definitions() as $def) { |
| 322 |
wp_register_style( |
| 323 |
$def['style_handle'], |
| 324 |
plugins_url('/assets/css/' . $def['style_css'], MARQUEEX_FILE), |
| 325 |
['marqueex-common'], |
| 326 |
MARQUEEX_VERSION |
| 327 |
); |
| 328 |
wp_register_script( |
| 329 |
$def['script_handle'], |
| 330 |
plugins_url('/assets/js/' . $def['script_js'], MARQUEEX_FILE), |
| 331 |
$def['script_deps'], |
| 332 |
MARQUEEX_VERSION, |
| 333 |
true |
| 334 |
); |
| 335 |
} |
| 336 |
} |
| 337 |
|
| 338 |
/** |
| 339 |
* Get integration status |
| 340 |
*/ |
| 341 |
public function get_status() { |
| 342 |
$status = [ |
| 343 |
'active' => false, |
| 344 |
'version' => null, |
| 345 |
'widgets_registered' => false, |
| 346 |
'enabled_widgets' => [], |
| 347 |
]; |
| 348 |
|
| 349 |
if ($this->is_elementor_active()) { |
| 350 |
$status['active'] = true; |
| 351 |
$status['version'] = \Elementor\Plugin::$instance->get_version(); |
| 352 |
|
| 353 |
// Derive enabled widgets from the same canonical definition map so |
| 354 |
// status can never drift from what register_widgets() actually does. |
| 355 |
$enabled_widgets = []; |
| 356 |
foreach (array_keys($this->get_widget_definitions()) as $key) { |
| 357 |
if ($this->is_widget_enabled($key)) { |
| 358 |
$enabled_widgets[] = $key; |
| 359 |
} |
| 360 |
} |
| 361 |
|
| 362 |
$status['enabled_widgets'] = $enabled_widgets; |
| 363 |
$status['widgets_registered'] = !empty($enabled_widgets); |
| 364 |
} |
| 365 |
|
| 366 |
return $status; |
| 367 |
} |
| 368 |
} |
| 369 |
|