PluginProbe ʕ •ᴥ•ʔ
ECS – Ele Custom Skin for Elementor / 4.3.8
ECS – Ele Custom Skin for Elementor v4.3.8
4.3.11 4.3.10 4.3.9 4.3.8 4.3.7 4.3.6 4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.11 4.1.10 4.1.9 4.1.8 4.1.6 4.1.7 4.1.5 4.1.4 4.1.1 4.1.2 trunk 1.0.0 1.0.1 1.0.9 1.1.3 1.1.4 1.1.5 1.2.0 1.2.1 1.2.4 1.2.5 1.3.10 1.3.11 1.3.3 1.3.4 1.3.6 1.3.7 1.3.9 1.4.0 2.0.2 2.1.0 2.2.0 2.2.1 2.2.2 3.0.0 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 4.1.0
ele-custom-skin / modules / mobile-menu / class-ecs-mobile-menu-module.php
ele-custom-skin / modules / mobile-menu Last commit date
assets 1 month ago class-ecs-mobile-menu-module.php 3 weeks ago
class-ecs-mobile-menu-module.php
644 lines
1 <?php
2 /**
3 * Module: WordPress Menu Responsive Controls
4 *
5 * Extends the built-in Elementor Pro "WordPress Menu" (nav-menu) widget
6 * by upgrading Layout, Alignment, Pointer, and Animation controls to
7 * responsive (Desktop / Tablet / Mobile) via Elementor's control API.
8 *
9 * No separate DTE widget — the module only hooks into the existing widget.
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 use Elementor\Controls_Manager;
17
18 class ECS_Mobile_Menu_Module extends ECS_Module_Base {
19
20 public function get_id(): string {
21 return 'mobile_menu';
22 }
23
24 public function get_title(): string {
25 return __( 'WordPress Menu Responsive', 'ele-custom-skin' );
26 }
27
28 public function get_description(): string {
29 return __( 'Makes layout, alignment, and animation controls on the Nav Menu widget responsive — set different values for desktop, tablet, and mobile.', 'ele-custom-skin' );
30 }
31
32 public function boot(): void {
33 add_action(
34 'elementor/element/nav-menu/section_layout/before_section_end',
35 [ $this, 'upgrade_nav_menu_controls' ],
36 10,
37 2
38 );
39
40 add_action(
41 'elementor/element/nav-menu/section_style_main-menu/before_section_end',
42 [ $this, 'upgrade_nav_menu_style_controls' ],
43 10,
44 2
45 );
46 }
47
48 /**
49 * Upgrade Layout, Alignment, Pointer, and Animation controls
50 * of the nav-menu widget to responsive variants.
51 *
52 * Call order matters: each upgrade uses the previous control's ID
53 * as its position anchor so controls stay in their original place.
54 *
55 * @param \Elementor\Element_Base $element
56 * @param array $args
57 */
58 public function upgrade_nav_menu_controls( $element, $args ): void {
59 $this->upgrade_layout( $element );
60 $this->upgrade_alignment( $element );
61 $this->upgrade_pointer( $element );
62 $this->upgrade_animations( $element );
63 $this->fix_mobile_dropdown_conditions( $element );
64 }
65
66 // ── Layout ────────────────────────────────────────────────────────────────
67
68 private function upgrade_layout( $element ): void {
69 $element->remove_control( 'layout' );
70
71 $element->add_responsive_control(
72 'layout',
73 [
74 'label' => esc_html__( 'Layout', 'elementor-pro' ),
75 'type' => Controls_Manager::SELECT,
76 'default' => 'horizontal',
77 'options' => [
78 'horizontal' => esc_html__( 'Horizontal', 'elementor-pro' ),
79 'vertical' => esc_html__( 'Vertical', 'elementor-pro' ),
80 'dropdown' => esc_html__( 'Dropdown', 'elementor-pro' ),
81 ],
82 // prefix_class marks this widget as "DTE-controlled" so the CSS
83 // variable rules in ecs-mobile-menu.css apply only to it.
84 // NOTE: Elementor PHP does NOT add breakpoint prefixes (tablet-/mobile-)
85 // to prefix_class values, so ALL breakpoint values are added as
86 // ecs-nav-layout-{value} without prefix. The actual responsive
87 // behaviour is handled via CSS custom properties (selectors below).
88 'prefix_class' => 'ecs-nav-layout-',
89 'render_type' => 'template',
90 'frontend_available' => true,
91 // CSS custom properties — Elementor generates proper @media rules
92 // AND body.elementor-device-* rules (for editor preview) from these.
93 'selectors_dictionary' => [
94 'horizontal' => '--ecs-nav-main-display:flex;--ecs-nav-main-dir:row;--ecs-nav-toggle-display:none',
95 'vertical' => '--ecs-nav-main-display:flex;--ecs-nav-main-dir:column;--ecs-nav-toggle-display:none',
96 'dropdown' => '--ecs-nav-main-display:none;--ecs-nav-main-dir:row;--ecs-nav-toggle-display:flex',
97 ],
98 'selectors' => [
99 '{{WRAPPER}}' => '{{VALUE}}',
100 ],
101 ],
102 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'menu' ] ]
103 );
104 }
105
106 // ── Alignment ─────────────────────────────────────────────────────────────
107
108 private function upgrade_alignment( $element ): void {
109 $element->remove_control( 'align_items' );
110
111 $element->add_responsive_control(
112 'align_items',
113 [
114 'label' => esc_html__( 'Alignment', 'elementor-pro' ),
115 'type' => Controls_Manager::CHOOSE,
116 'options' => [
117 'flex-start' => [
118 'title' => esc_html__( 'Start', 'elementor-pro' ),
119 'icon' => 'eicon-text-align-left',
120 ],
121 'center' => [
122 'title' => esc_html__( 'Center', 'elementor-pro' ),
123 'icon' => 'eicon-text-align-center',
124 ],
125 'flex-end' => [
126 'title' => esc_html__( 'End', 'elementor-pro' ),
127 'icon' => 'eicon-text-align-right',
128 ],
129 'space-between' => [
130 'title' => esc_html__( 'Stretch', 'elementor-pro' ),
131 'icon' => 'eicon-text-align-justify',
132 ],
133 ],
134 // Direct selectors — Elementor generates scoped per-element CSS with
135 // proper @media breakpoints. When no value is set for a breakpoint,
136 // Elementor emits no rule, so native Elementor alignment is preserved.
137 // (Previous CSS-variable approach used a flex-start fallback that
138 // overwrote the user's existing alignment when the CSS cache was stale.)
139 //
140 // space-between also sets --ecs-nav-li-grow:1 so <li> items expand to
141 // fill equal widths (equivalent to Elementor native flex-grow:1 on >li).
142 // All values must be mapped; unmapped values would output the raw key
143 // as CSS (e.g. "flex-start;" alone), which is invalid.
144 'selectors_dictionary' => [
145 'flex-start' => 'justify-content:flex-start',
146 'center' => 'justify-content:center',
147 'flex-end' => 'justify-content:flex-end',
148 'space-between' => 'justify-content:space-between;--ecs-nav-li-grow:1',
149 ],
150 // Also targets .elementor-nav-menu--dropdown .elementor-nav-menu so
151 // alignment still applies when the native Elementor Breakpoint (not
152 // the ECS Layout control) switches this horizontal/vertical menu into
153 // its dropdown/burger state. Without this, Elementor Pro's own
154 // .elementor-nav-menu__align-{value} CSS would have covered both
155 // variants, but that class is no longer added since this control
156 // replaces the native one.
157 'selectors' => [
158 '{{WRAPPER}} .elementor-nav-menu--main,
159 {{WRAPPER}} .elementor-nav-menu--main .elementor-nav-menu,
160 {{WRAPPER}} .elementor-nav-menu--dropdown .elementor-nav-menu' => '{{VALUE}};',
161 ],
162 'condition' => [ 'layout!' => 'dropdown' ],
163 ],
164 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'layout' ] ]
165 );
166 }
167
168 // ── Pointer ───────────────────────────────────────────────────────────────
169
170 private function upgrade_pointer( $element ): void {
171 $element->remove_control( 'pointer' );
172
173 // Re-add pointer as non-responsive to preserve prefix_class:'e--pointer-'.
174 // With a responsive control, Elementor adds ALL device values as classes
175 // simultaneously (e.g. e--pointer-underline + e--pointer-none), causing the
176 // 'none' class to cancel out the active pointer type on desktop.
177 $element->add_control(
178 'pointer',
179 [
180 'label' => esc_html__( 'Pointer', 'elementor-pro' ),
181 'type' => Controls_Manager::SELECT,
182 'default' => 'underline',
183 'options' => [
184 'none' => esc_html__( 'None', 'elementor-pro' ),
185 'underline' => esc_html__( 'Underline', 'elementor-pro' ),
186 'overline' => esc_html__( 'Overline', 'elementor-pro' ),
187 'double-line' => esc_html__( 'Double Line', 'elementor-pro' ),
188 'framed' => esc_html__( 'Framed', 'elementor-pro' ),
189 'background' => esc_html__( 'Background', 'elementor-pro' ),
190 'text' => esc_html__( 'Text', 'elementor-pro' ),
191 ],
192 'prefix_class' => 'e--pointer-',
193 'style_transfer' => true,
194 'render_type' => 'template',
195 'condition' => [ 'layout!' => 'dropdown' ],
196 ],
197 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'align_items' ] ]
198 );
199
200 // Separate responsive control: hide pointer on specific devices.
201 // --ecs-nav-ptr-vis (default: visible) is read by ecs-mobile-menu.css.
202 $element->add_responsive_control(
203 'ecs_pointer_hide',
204 [
205 'label' => esc_html__( 'Hide Pointer', 'ele-custom-skin' ),
206 'type' => Controls_Manager::SWITCHER,
207 'label_on' => esc_html__( 'Hidden', 'ele-custom-skin' ),
208 'label_off' => esc_html__( 'Visible', 'ele-custom-skin' ),
209 'selectors_dictionary' => [ 'yes' => '--ecs-nav-ptr-vis:hidden' ],
210 'selectors' => [ '{{WRAPPER}}' => '{{VALUE}}' ],
211 'condition' => [
212 'layout!' => 'dropdown',
213 'pointer!' => 'none',
214 ],
215 ],
216 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'pointer' ] ]
217 );
218 }
219
220 // ── Animation controls ─────────────────────────────────────────────────────
221
222 private function upgrade_animations( $element ): void {
223 $target = '{{WRAPPER}} .elementor-nav-menu--main .elementor-item::before,
224 {{WRAPPER}} .elementor-nav-menu--main .elementor-item::after,
225 {{WRAPPER}} .elementor-nav-menu--main .elementor-item';
226
227 $none_css = [ 'none' => 'transition-duration:0s!important;animation:none!important;' ];
228
229 // Line pointer animations (underline / overline / double-line)
230 $element->remove_control( 'animation_line' );
231 $element->add_responsive_control(
232 'animation_line',
233 [
234 'label' => esc_html__( 'Animation', 'elementor-pro' ),
235 'type' => Controls_Manager::SELECT,
236 'default' => 'fade',
237 'options' => [
238 'fade' => 'Fade',
239 'slide' => 'Slide',
240 'grow' => 'Grow',
241 'drop-in' => 'Drop In',
242 'drop-out' => 'Drop Out',
243 'none' => 'None',
244 ],
245 // prefix_class activates Elementor Pro's animation CSS (e--animation-fade etc.)
246 'prefix_class' => 'e--animation-',
247 'render_type' => 'template',
248 'condition' => [
249 'layout!' => 'dropdown',
250 'pointer' => [ 'underline', 'overline', 'double-line' ],
251 ],
252 'selectors_dictionary' => $none_css,
253 'selectors' => [ $target => '{{VALUE}}' ],
254 ],
255 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'pointer' ] ]
256 );
257
258 // Framed pointer animation
259 $element->remove_control( 'animation_framed' );
260 $element->add_responsive_control(
261 'animation_framed',
262 [
263 'label' => esc_html__( 'Animation', 'elementor-pro' ),
264 'type' => Controls_Manager::SELECT,
265 'default' => 'fade',
266 'options' => [
267 'fade' => 'Fade',
268 'grow' => 'Grow',
269 'shrink' => 'Shrink',
270 'draw' => 'Draw',
271 'corners' => 'Corners',
272 'none' => 'None',
273 ],
274 'prefix_class' => 'e--animation-',
275 'render_type' => 'template',
276 'condition' => [
277 'layout!' => 'dropdown',
278 'pointer' => 'framed',
279 ],
280 'selectors_dictionary' => $none_css,
281 'selectors' => [ $target => '{{VALUE}}' ],
282 ],
283 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'animation_line' ] ]
284 );
285
286 // Background pointer animation
287 $element->remove_control( 'animation_background' );
288 $element->add_responsive_control(
289 'animation_background',
290 [
291 'label' => esc_html__( 'Animation', 'elementor-pro' ),
292 'type' => Controls_Manager::SELECT,
293 'default' => 'fade',
294 'options' => [
295 'fade' => 'Fade',
296 'grow' => 'Grow',
297 'shrink' => 'Shrink',
298 'sweep-left' => 'Sweep Left',
299 'sweep-right' => 'Sweep Right',
300 'sweep-up' => 'Sweep Up',
301 'sweep-down' => 'Sweep Down',
302 'shutter-in-vertical' => 'Shutter In Vertical',
303 'shutter-out-vertical' => 'Shutter Out Vertical',
304 'shutter-in-horizontal' => 'Shutter In Horizontal',
305 'shutter-out-horizontal' => 'Shutter Out Horizontal',
306 'none' => 'None',
307 ],
308 'prefix_class' => 'e--animation-',
309 'render_type' => 'template',
310 'condition' => [
311 'layout!' => 'dropdown',
312 'pointer' => 'background',
313 ],
314 'selectors_dictionary' => $none_css,
315 'selectors' => [ $target => '{{VALUE}}' ],
316 ],
317 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'animation_framed' ] ]
318 );
319
320 // Text pointer animation
321 $element->remove_control( 'animation_text' );
322 $element->add_responsive_control(
323 'animation_text',
324 [
325 'label' => esc_html__( 'Animation', 'elementor-pro' ),
326 'type' => Controls_Manager::SELECT,
327 'default' => 'grow',
328 'options' => [
329 'grow' => 'Grow',
330 'shrink' => 'Shrink',
331 'sink' => 'Sink',
332 'float' => 'Float',
333 'skew' => 'Skew',
334 'rotate' => 'Rotate',
335 'none' => 'None',
336 ],
337 'prefix_class' => 'e--animation-',
338 'render_type' => 'template',
339 'condition' => [
340 'layout!' => 'dropdown',
341 'pointer' => 'text',
342 ],
343 'selectors_dictionary' => $none_css,
344 'selectors' => [ $target => '{{VALUE}}' ],
345 ],
346 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'animation_background' ] ]
347 );
348 }
349
350 // ── Mobile Dropdown conditions fix ────────────────────────────────────────
351
352 /**
353 * Remove the 'dropdown!' => 'none' condition from full_width, text_align,
354 * toggle, and toggle_align so they remain visible when Breakpoint = None.
355 * This allows using DTE's Layout=Dropdown per-device without losing access
356 * to the toggle button and dropdown settings.
357 */
358 private function fix_mobile_dropdown_conditions( $element ): void {
359
360 // full_width
361 $element->remove_control( 'full_width' );
362 $element->add_control(
363 'full_width',
364 [
365 'label' => esc_html__( 'Full Width', 'elementor-pro' ),
366 'type' => Controls_Manager::SWITCHER,
367 'description' => esc_html__( 'Stretch the dropdown of the menu to full width.', 'elementor-pro' ),
368 'prefix_class' => 'elementor-nav-menu--',
369 'return_value' => 'stretch',
370 'frontend_available' => true,
371 // no 'dropdown!' => 'none' condition
372 ],
373 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'dropdown' ] ]
374 );
375
376 // ecs_dropdown_width — visible only when Full Width is OFF
377 $element->add_control(
378 'ecs_dropdown_width',
379 [
380 'label' => esc_html__( 'Dropdown Width', 'ele-custom-skin' ),
381 'type' => Controls_Manager::SLIDER,
382 'size_units' => [ 'px', '%', 'vw' ],
383 'range' => [
384 'px' => [ 'min' => 50, 'max' => 1000, 'step' => 1 ],
385 '%' => [ 'min' => 10, 'max' => 100, 'step' => 1 ],
386 'vw' => [ 'min' => 10, 'max' => 100, 'step' => 1 ],
387 ],
388 'condition' => [ 'full_width!' => 'stretch' ],
389 // !important needed to override width:auto !important from ecs-mobile-menu.css
390 'selectors' => [
391 '{{WRAPPER}} .elementor-nav-menu--dropdown.elementor-nav-menu__container' => 'width: {{SIZE}}{{UNIT}} !important; min-width: 0 !important;',
392 ],
393 ],
394 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'full_width' ] ]
395 );
396
397 // force_breakpoint — force dropdown at Elementor's native Breakpoint,
398 // regardless of the DTE Layout setting for tablet/mobile.
399 $element->add_control(
400 'ecs_force_breakpoint',
401 [
402 'label' => esc_html__( 'Force Breakpoint', 'ele-custom-skin' ),
403 'type' => Controls_Manager::SWITCHER,
404 'description' => esc_html__( 'Force dropdown mode at the selected Breakpoint, regardless of the Layout setting.', 'ele-custom-skin' ),
405 'prefix_class' => 'ecs-',
406 'return_value' => 'force-breakpoint',
407 'frontend_available' => true,
408 'condition' => [ 'dropdown!' => 'none' ],
409 ],
410 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'ecs_dropdown_width' ] ]
411 );
412
413 // text_align — replaced with Left/Center/Right CHOOSE control
414 $element->remove_control( 'text_align' );
415 $element->add_control(
416 'text_align',
417 [
418 'label' => esc_html__( 'Text Align', 'elementor-pro' ),
419 'type' => Controls_Manager::CHOOSE,
420 'default' => 'left',
421 'options' => [
422 'left' => [
423 'title' => esc_html__( 'Left', 'elementor-pro' ),
424 'icon' => 'eicon-text-align-left',
425 ],
426 'center' => [
427 'title' => esc_html__( 'Center', 'elementor-pro' ),
428 'icon' => 'eicon-text-align-center',
429 ],
430 'right' => [
431 'title' => esc_html__( 'Right', 'elementor-pro' ),
432 'icon' => 'eicon-text-align-right',
433 ],
434 ],
435 // .elementor-item is display:flex → text-align has no effect.
436 // justify-content controls horizontal alignment of flex children.
437 'selectors_dictionary' => [
438 'left' => 'flex-start',
439 'center' => 'center',
440 'right' => 'flex-end',
441 ],
442 'selectors' => [
443 '{{WRAPPER}} .elementor-nav-menu--dropdown .elementor-item,
444 {{WRAPPER}} .elementor-nav-menu--dropdown .elementor-sub-item' => 'justify-content: {{VALUE}}',
445 ],
446 ],
447 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'ecs_force_breakpoint' ] ]
448 );
449
450 // toggle
451 $element->remove_control( 'toggle' );
452 $element->add_control(
453 'toggle',
454 [
455 'label' => esc_html__( 'Toggle Button', 'elementor-pro' ),
456 'type' => Controls_Manager::SELECT,
457 'default' => 'burger',
458 'options' => [
459 '' => esc_html__( 'None', 'elementor-pro' ),
460 'burger' => esc_html__( 'Hamburger', 'elementor-pro' ),
461 ],
462 'prefix_class' => 'elementor-nav-menu--toggle elementor-nav-menu--',
463 'render_type' => 'template',
464 'frontend_available' => true,
465 // no 'dropdown!' => 'none' condition
466 ],
467 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'text_align' ] ]
468 );
469
470 // toggle_align — keep 'toggle!' condition, remove 'dropdown!' condition.
471 // prefix_class 'ecs-toggle-align-' adds ecs-toggle-align-{left|center|right}
472 // to the wrapper so CSS can align the dropdown panel to match.
473 $element->remove_control( 'toggle_align' );
474 $element->add_control(
475 'toggle_align',
476 [
477 'label' => esc_html__( 'Toggle Align', 'elementor-pro' ),
478 'type' => Controls_Manager::CHOOSE,
479 'default' => 'center',
480 'options' => [
481 'left' => [
482 'title' => esc_html__( 'Left', 'elementor-pro' ),
483 'icon' => 'eicon-h-align-left',
484 ],
485 'center' => [
486 'title' => esc_html__( 'Center', 'elementor-pro' ),
487 'icon' => 'eicon-h-align-center',
488 ],
489 'right' => [
490 'title' => esc_html__( 'Right', 'elementor-pro' ),
491 'icon' => 'eicon-h-align-right',
492 ],
493 ],
494 'prefix_class' => 'ecs-toggle-align-',
495 'selectors_dictionary' => [
496 'left' => 'margin-right: auto',
497 'center' => 'margin: 0 auto',
498 'right' => 'margin-left: auto',
499 ],
500 'selectors' => [
501 '{{WRAPPER}} .elementor-menu-toggle' => '{{VALUE}}',
502 ],
503 'condition' => [ 'toggle!' => '' ], // removed 'dropdown!' => 'none'
504 'separator' => 'before',
505 ]
506 // no position → goes at end of section, after the nav_icon_options tabs
507 );
508 }
509
510 // ── Style tab upgrades ────────────────────────────────────────────────────
511
512 public function upgrade_nav_menu_style_controls( $element, $args ): void {
513 $this->upgrade_colors( $element );
514 }
515
516 private function upgrade_colors( $element ): void {
517 $tabs = 'tabs_menu_item_style';
518
519 // Normal tab
520 $element->remove_control( 'color_menu_item' );
521 $element->add_responsive_control(
522 'color_menu_item',
523 [
524 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
525 'type' => Controls_Manager::COLOR,
526 'default' => '',
527 'render_type' => 'ui',
528 'tabs_wrapper' => $tabs,
529 'inner_tab' => 'tab_menu_item_normal',
530 'selectors' => [
531 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item' => 'color: {{VALUE}}; fill: {{VALUE}};',
532 ],
533 ],
534 );
535 // No position arg — inner_tab places it in the correct tab; all other Normal tab
536 // controls are removed by ECS so order within the tab doesn't matter.
537
538 // Hover tab
539 $element->remove_control( 'color_menu_item_hover' );
540 $element->add_responsive_control(
541 'color_menu_item_hover',
542 [
543 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
544 'type' => Controls_Manager::COLOR,
545 'render_type' => 'ui',
546 'tabs_wrapper' => $tabs,
547 'inner_tab' => 'tab_menu_item_hover',
548 'selectors' => [
549 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item:hover,
550 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.elementor-item-active,
551 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.highlighted,
552 {{WRAPPER}} .elementor-nav-menu--main .elementor-item:focus' => 'color: {{VALUE}}; fill: {{VALUE}};',
553 ],
554 'condition' => [ 'pointer!' => 'background' ],
555 ],
556 // pointer_color_menu_item_hover has both tabs_wrapper and inner_tab set,
557 // so it's safe as a position anchor (avoids the undefined 'inner_tab' warning
558 // that tab-type controls trigger in Elementor's get_position_info()).
559 [ 'position' => [ 'type' => 'control', 'at' => 'before', 'of' => 'pointer_color_menu_item_hover' ] ]
560 );
561
562 $element->remove_control( 'color_menu_item_hover_pointer_bg' );
563 $element->add_responsive_control(
564 'color_menu_item_hover_pointer_bg',
565 [
566 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
567 'type' => Controls_Manager::COLOR,
568 'default' => '#fff',
569 'render_type' => 'ui',
570 'tabs_wrapper' => $tabs,
571 'inner_tab' => 'tab_menu_item_hover',
572 'selectors' => [
573 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item:hover,
574 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.elementor-item-active,
575 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.highlighted,
576 {{WRAPPER}} .elementor-nav-menu--main .elementor-item:focus' => 'color: {{VALUE}}',
577 ],
578 'condition' => [ 'pointer' => 'background' ],
579 ],
580 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'color_menu_item_hover' ] ]
581 );
582
583 // Active tab
584 $element->remove_control( 'color_menu_item_active' );
585 $element->add_responsive_control(
586 'color_menu_item_active',
587 [
588 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
589 'type' => Controls_Manager::COLOR,
590 'default' => '',
591 'render_type' => 'ui',
592 'tabs_wrapper' => $tabs,
593 'inner_tab' => 'tab_menu_item_active',
594 'selectors' => [
595 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item.elementor-item-active' => 'color: {{VALUE}}',
596 ],
597 ],
598 // pointer_color_menu_item_active has both tabs_wrapper and inner_tab — safe anchor.
599 [ 'position' => [ 'type' => 'control', 'at' => 'before', 'of' => 'pointer_color_menu_item_active' ] ]
600 );
601 }
602
603 // ── Assets ────────────────────────────────────────────────────────────────
604
605 public function enqueue_frontend_assets(): void {
606 wp_enqueue_style(
607 'ecs-mobile-menu',
608 $this->module_url() . 'assets/css/ecs-mobile-menu.css',
609 [],
610 ECS_VERSION
611 );
612
613 // Loaded in <head> (no in_footer) so our DOMContentLoaded listener is
614 // registered before Elementor Pro's scripts (which load in the footer).
615 // The script reads the computed ECS CSS variables to determine the active
616 // layout per viewport and applies the correct elementor-nav-menu--layout-*
617 // class so pointer animations and dropdown positioning both work correctly.
618 wp_enqueue_script(
619 'ecs-mobile-menu',
620 $this->module_url() . 'assets/js/ecs-mobile-menu.js',
621 [],
622 ECS_VERSION,
623 false
624 );
625 }
626
627 public function enqueue_editor_assets(): void {
628 wp_enqueue_style(
629 'ecs-mobile-menu',
630 $this->module_url() . 'assets/css/ecs-mobile-menu.css',
631 [],
632 ECS_VERSION
633 );
634
635 wp_enqueue_script(
636 'ecs-mobile-menu-editor',
637 $this->module_url() . 'assets/js/ecs-mobile-menu-editor.js',
638 [],
639 ECS_VERSION,
640 true
641 );
642 }
643 }
644