PluginProbe ʕ •ᴥ•ʔ
ECS – Ele Custom Skin for Elementor / 4.3.6
ECS – Ele Custom Skin for Elementor v4.3.6
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 6 days ago class-ecs-mobile-menu-module.php 6 days ago
class-ecs-mobile-menu-module.php
636 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 'selectors' => [
151 '{{WRAPPER}} .elementor-nav-menu--main,
152 {{WRAPPER}} .elementor-nav-menu--main .elementor-nav-menu' => '{{VALUE}};',
153 ],
154 'condition' => [ 'layout!' => 'dropdown' ],
155 ],
156 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'layout' ] ]
157 );
158 }
159
160 // ── Pointer ───────────────────────────────────────────────────────────────
161
162 private function upgrade_pointer( $element ): void {
163 $element->remove_control( 'pointer' );
164
165 // Re-add pointer as non-responsive to preserve prefix_class:'e--pointer-'.
166 // With a responsive control, Elementor adds ALL device values as classes
167 // simultaneously (e.g. e--pointer-underline + e--pointer-none), causing the
168 // 'none' class to cancel out the active pointer type on desktop.
169 $element->add_control(
170 'pointer',
171 [
172 'label' => esc_html__( 'Pointer', 'elementor-pro' ),
173 'type' => Controls_Manager::SELECT,
174 'default' => 'underline',
175 'options' => [
176 'none' => esc_html__( 'None', 'elementor-pro' ),
177 'underline' => esc_html__( 'Underline', 'elementor-pro' ),
178 'overline' => esc_html__( 'Overline', 'elementor-pro' ),
179 'double-line' => esc_html__( 'Double Line', 'elementor-pro' ),
180 'framed' => esc_html__( 'Framed', 'elementor-pro' ),
181 'background' => esc_html__( 'Background', 'elementor-pro' ),
182 'text' => esc_html__( 'Text', 'elementor-pro' ),
183 ],
184 'prefix_class' => 'e--pointer-',
185 'style_transfer' => true,
186 'render_type' => 'template',
187 'condition' => [ 'layout!' => 'dropdown' ],
188 ],
189 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'align_items' ] ]
190 );
191
192 // Separate responsive control: hide pointer on specific devices.
193 // --ecs-nav-ptr-vis (default: visible) is read by ecs-mobile-menu.css.
194 $element->add_responsive_control(
195 'ecs_pointer_hide',
196 [
197 'label' => esc_html__( 'Hide Pointer', 'ele-custom-skin' ),
198 'type' => Controls_Manager::SWITCHER,
199 'label_on' => esc_html__( 'Hidden', 'ele-custom-skin' ),
200 'label_off' => esc_html__( 'Visible', 'ele-custom-skin' ),
201 'selectors_dictionary' => [ 'yes' => '--ecs-nav-ptr-vis:hidden' ],
202 'selectors' => [ '{{WRAPPER}}' => '{{VALUE}}' ],
203 'condition' => [
204 'layout!' => 'dropdown',
205 'pointer!' => 'none',
206 ],
207 ],
208 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'pointer' ] ]
209 );
210 }
211
212 // ── Animation controls ─────────────────────────────────────────────────────
213
214 private function upgrade_animations( $element ): void {
215 $target = '{{WRAPPER}} .elementor-nav-menu--main .elementor-item::before,
216 {{WRAPPER}} .elementor-nav-menu--main .elementor-item::after,
217 {{WRAPPER}} .elementor-nav-menu--main .elementor-item';
218
219 $none_css = [ 'none' => 'transition-duration:0s!important;animation:none!important;' ];
220
221 // Line pointer animations (underline / overline / double-line)
222 $element->remove_control( 'animation_line' );
223 $element->add_responsive_control(
224 'animation_line',
225 [
226 'label' => esc_html__( 'Animation', 'elementor-pro' ),
227 'type' => Controls_Manager::SELECT,
228 'default' => 'fade',
229 'options' => [
230 'fade' => 'Fade',
231 'slide' => 'Slide',
232 'grow' => 'Grow',
233 'drop-in' => 'Drop In',
234 'drop-out' => 'Drop Out',
235 'none' => 'None',
236 ],
237 // prefix_class activates Elementor Pro's animation CSS (e--animation-fade etc.)
238 'prefix_class' => 'e--animation-',
239 'render_type' => 'template',
240 'condition' => [
241 'layout!' => 'dropdown',
242 'pointer' => [ 'underline', 'overline', 'double-line' ],
243 ],
244 'selectors_dictionary' => $none_css,
245 'selectors' => [ $target => '{{VALUE}}' ],
246 ],
247 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'pointer' ] ]
248 );
249
250 // Framed pointer animation
251 $element->remove_control( 'animation_framed' );
252 $element->add_responsive_control(
253 'animation_framed',
254 [
255 'label' => esc_html__( 'Animation', 'elementor-pro' ),
256 'type' => Controls_Manager::SELECT,
257 'default' => 'fade',
258 'options' => [
259 'fade' => 'Fade',
260 'grow' => 'Grow',
261 'shrink' => 'Shrink',
262 'draw' => 'Draw',
263 'corners' => 'Corners',
264 'none' => 'None',
265 ],
266 'prefix_class' => 'e--animation-',
267 'render_type' => 'template',
268 'condition' => [
269 'layout!' => 'dropdown',
270 'pointer' => 'framed',
271 ],
272 'selectors_dictionary' => $none_css,
273 'selectors' => [ $target => '{{VALUE}}' ],
274 ],
275 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'animation_line' ] ]
276 );
277
278 // Background pointer animation
279 $element->remove_control( 'animation_background' );
280 $element->add_responsive_control(
281 'animation_background',
282 [
283 'label' => esc_html__( 'Animation', 'elementor-pro' ),
284 'type' => Controls_Manager::SELECT,
285 'default' => 'fade',
286 'options' => [
287 'fade' => 'Fade',
288 'grow' => 'Grow',
289 'shrink' => 'Shrink',
290 'sweep-left' => 'Sweep Left',
291 'sweep-right' => 'Sweep Right',
292 'sweep-up' => 'Sweep Up',
293 'sweep-down' => 'Sweep Down',
294 'shutter-in-vertical' => 'Shutter In Vertical',
295 'shutter-out-vertical' => 'Shutter Out Vertical',
296 'shutter-in-horizontal' => 'Shutter In Horizontal',
297 'shutter-out-horizontal' => 'Shutter Out Horizontal',
298 'none' => 'None',
299 ],
300 'prefix_class' => 'e--animation-',
301 'render_type' => 'template',
302 'condition' => [
303 'layout!' => 'dropdown',
304 'pointer' => 'background',
305 ],
306 'selectors_dictionary' => $none_css,
307 'selectors' => [ $target => '{{VALUE}}' ],
308 ],
309 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'animation_framed' ] ]
310 );
311
312 // Text pointer animation
313 $element->remove_control( 'animation_text' );
314 $element->add_responsive_control(
315 'animation_text',
316 [
317 'label' => esc_html__( 'Animation', 'elementor-pro' ),
318 'type' => Controls_Manager::SELECT,
319 'default' => 'grow',
320 'options' => [
321 'grow' => 'Grow',
322 'shrink' => 'Shrink',
323 'sink' => 'Sink',
324 'float' => 'Float',
325 'skew' => 'Skew',
326 'rotate' => 'Rotate',
327 'none' => 'None',
328 ],
329 'prefix_class' => 'e--animation-',
330 'render_type' => 'template',
331 'condition' => [
332 'layout!' => 'dropdown',
333 'pointer' => 'text',
334 ],
335 'selectors_dictionary' => $none_css,
336 'selectors' => [ $target => '{{VALUE}}' ],
337 ],
338 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'animation_background' ] ]
339 );
340 }
341
342 // ── Mobile Dropdown conditions fix ────────────────────────────────────────
343
344 /**
345 * Remove the 'dropdown!' => 'none' condition from full_width, text_align,
346 * toggle, and toggle_align so they remain visible when Breakpoint = None.
347 * This allows using DTE's Layout=Dropdown per-device without losing access
348 * to the toggle button and dropdown settings.
349 */
350 private function fix_mobile_dropdown_conditions( $element ): void {
351
352 // full_width
353 $element->remove_control( 'full_width' );
354 $element->add_control(
355 'full_width',
356 [
357 'label' => esc_html__( 'Full Width', 'elementor-pro' ),
358 'type' => Controls_Manager::SWITCHER,
359 'description' => esc_html__( 'Stretch the dropdown of the menu to full width.', 'elementor-pro' ),
360 'prefix_class' => 'elementor-nav-menu--',
361 'return_value' => 'stretch',
362 'frontend_available' => true,
363 // no 'dropdown!' => 'none' condition
364 ],
365 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'dropdown' ] ]
366 );
367
368 // ecs_dropdown_width — visible only when Full Width is OFF
369 $element->add_control(
370 'ecs_dropdown_width',
371 [
372 'label' => esc_html__( 'Dropdown Width', 'ele-custom-skin' ),
373 'type' => Controls_Manager::SLIDER,
374 'size_units' => [ 'px', '%', 'vw' ],
375 'range' => [
376 'px' => [ 'min' => 50, 'max' => 1000, 'step' => 1 ],
377 '%' => [ 'min' => 10, 'max' => 100, 'step' => 1 ],
378 'vw' => [ 'min' => 10, 'max' => 100, 'step' => 1 ],
379 ],
380 'condition' => [ 'full_width!' => 'stretch' ],
381 // !important needed to override width:auto !important from ecs-mobile-menu.css
382 'selectors' => [
383 '{{WRAPPER}} .elementor-nav-menu--dropdown.elementor-nav-menu__container' => 'width: {{SIZE}}{{UNIT}} !important; min-width: 0 !important;',
384 ],
385 ],
386 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'full_width' ] ]
387 );
388
389 // force_breakpoint — force dropdown at Elementor's native Breakpoint,
390 // regardless of the DTE Layout setting for tablet/mobile.
391 $element->add_control(
392 'ecs_force_breakpoint',
393 [
394 'label' => esc_html__( 'Force Breakpoint', 'ele-custom-skin' ),
395 'type' => Controls_Manager::SWITCHER,
396 'description' => esc_html__( 'Force dropdown mode at the selected Breakpoint, regardless of the Layout setting.', 'ele-custom-skin' ),
397 'prefix_class' => 'ecs-',
398 'return_value' => 'force-breakpoint',
399 'frontend_available' => true,
400 'condition' => [ 'dropdown!' => 'none' ],
401 ],
402 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'ecs_dropdown_width' ] ]
403 );
404
405 // text_align — replaced with Left/Center/Right CHOOSE control
406 $element->remove_control( 'text_align' );
407 $element->add_control(
408 'text_align',
409 [
410 'label' => esc_html__( 'Text Align', 'elementor-pro' ),
411 'type' => Controls_Manager::CHOOSE,
412 'default' => 'left',
413 'options' => [
414 'left' => [
415 'title' => esc_html__( 'Left', 'elementor-pro' ),
416 'icon' => 'eicon-text-align-left',
417 ],
418 'center' => [
419 'title' => esc_html__( 'Center', 'elementor-pro' ),
420 'icon' => 'eicon-text-align-center',
421 ],
422 'right' => [
423 'title' => esc_html__( 'Right', 'elementor-pro' ),
424 'icon' => 'eicon-text-align-right',
425 ],
426 ],
427 // .elementor-item is display:flex → text-align has no effect.
428 // justify-content controls horizontal alignment of flex children.
429 'selectors_dictionary' => [
430 'left' => 'flex-start',
431 'center' => 'center',
432 'right' => 'flex-end',
433 ],
434 'selectors' => [
435 '{{WRAPPER}} .elementor-nav-menu--dropdown .elementor-item,
436 {{WRAPPER}} .elementor-nav-menu--dropdown .elementor-sub-item' => 'justify-content: {{VALUE}}',
437 ],
438 ],
439 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'ecs_force_breakpoint' ] ]
440 );
441
442 // toggle
443 $element->remove_control( 'toggle' );
444 $element->add_control(
445 'toggle',
446 [
447 'label' => esc_html__( 'Toggle Button', 'elementor-pro' ),
448 'type' => Controls_Manager::SELECT,
449 'default' => 'burger',
450 'options' => [
451 '' => esc_html__( 'None', 'elementor-pro' ),
452 'burger' => esc_html__( 'Hamburger', 'elementor-pro' ),
453 ],
454 'prefix_class' => 'elementor-nav-menu--toggle elementor-nav-menu--',
455 'render_type' => 'template',
456 'frontend_available' => true,
457 // no 'dropdown!' => 'none' condition
458 ],
459 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'text_align' ] ]
460 );
461
462 // toggle_align — keep 'toggle!' condition, remove 'dropdown!' condition.
463 // prefix_class 'ecs-toggle-align-' adds ecs-toggle-align-{left|center|right}
464 // to the wrapper so CSS can align the dropdown panel to match.
465 $element->remove_control( 'toggle_align' );
466 $element->add_control(
467 'toggle_align',
468 [
469 'label' => esc_html__( 'Toggle Align', 'elementor-pro' ),
470 'type' => Controls_Manager::CHOOSE,
471 'default' => 'center',
472 'options' => [
473 'left' => [
474 'title' => esc_html__( 'Left', 'elementor-pro' ),
475 'icon' => 'eicon-h-align-left',
476 ],
477 'center' => [
478 'title' => esc_html__( 'Center', 'elementor-pro' ),
479 'icon' => 'eicon-h-align-center',
480 ],
481 'right' => [
482 'title' => esc_html__( 'Right', 'elementor-pro' ),
483 'icon' => 'eicon-h-align-right',
484 ],
485 ],
486 'prefix_class' => 'ecs-toggle-align-',
487 'selectors_dictionary' => [
488 'left' => 'margin-right: auto',
489 'center' => 'margin: 0 auto',
490 'right' => 'margin-left: auto',
491 ],
492 'selectors' => [
493 '{{WRAPPER}} .elementor-menu-toggle' => '{{VALUE}}',
494 ],
495 'condition' => [ 'toggle!' => '' ], // removed 'dropdown!' => 'none'
496 'separator' => 'before',
497 ]
498 // no position → goes at end of section, after the nav_icon_options tabs
499 );
500 }
501
502 // ── Style tab upgrades ────────────────────────────────────────────────────
503
504 public function upgrade_nav_menu_style_controls( $element, $args ): void {
505 $this->upgrade_colors( $element );
506 }
507
508 private function upgrade_colors( $element ): void {
509 $tabs = 'tabs_menu_item_style';
510
511 // Normal tab
512 $element->remove_control( 'color_menu_item' );
513 $element->add_responsive_control(
514 'color_menu_item',
515 [
516 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
517 'type' => Controls_Manager::COLOR,
518 'default' => '',
519 'render_type' => 'ui',
520 'tabs_wrapper' => $tabs,
521 'inner_tab' => 'tab_menu_item_normal',
522 'selectors' => [
523 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item' => 'color: {{VALUE}}; fill: {{VALUE}};',
524 ],
525 ],
526 );
527 // No position arg — inner_tab places it in the correct tab; all other Normal tab
528 // controls are removed by ECS so order within the tab doesn't matter.
529
530 // Hover tab
531 $element->remove_control( 'color_menu_item_hover' );
532 $element->add_responsive_control(
533 'color_menu_item_hover',
534 [
535 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
536 'type' => Controls_Manager::COLOR,
537 'render_type' => 'ui',
538 'tabs_wrapper' => $tabs,
539 'inner_tab' => 'tab_menu_item_hover',
540 'selectors' => [
541 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item:hover,
542 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.elementor-item-active,
543 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.highlighted,
544 {{WRAPPER}} .elementor-nav-menu--main .elementor-item:focus' => 'color: {{VALUE}}; fill: {{VALUE}};',
545 ],
546 'condition' => [ 'pointer!' => 'background' ],
547 ],
548 // pointer_color_menu_item_hover has both tabs_wrapper and inner_tab set,
549 // so it's safe as a position anchor (avoids the undefined 'inner_tab' warning
550 // that tab-type controls trigger in Elementor's get_position_info()).
551 [ 'position' => [ 'type' => 'control', 'at' => 'before', 'of' => 'pointer_color_menu_item_hover' ] ]
552 );
553
554 $element->remove_control( 'color_menu_item_hover_pointer_bg' );
555 $element->add_responsive_control(
556 'color_menu_item_hover_pointer_bg',
557 [
558 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
559 'type' => Controls_Manager::COLOR,
560 'default' => '#fff',
561 'render_type' => 'ui',
562 'tabs_wrapper' => $tabs,
563 'inner_tab' => 'tab_menu_item_hover',
564 'selectors' => [
565 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item:hover,
566 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.elementor-item-active,
567 {{WRAPPER}} .elementor-nav-menu--main .elementor-item.highlighted,
568 {{WRAPPER}} .elementor-nav-menu--main .elementor-item:focus' => 'color: {{VALUE}}',
569 ],
570 'condition' => [ 'pointer' => 'background' ],
571 ],
572 [ 'position' => [ 'type' => 'control', 'at' => 'after', 'of' => 'color_menu_item_hover' ] ]
573 );
574
575 // Active tab
576 $element->remove_control( 'color_menu_item_active' );
577 $element->add_responsive_control(
578 'color_menu_item_active',
579 [
580 'label' => esc_html__( 'Text Color', 'elementor-pro' ),
581 'type' => Controls_Manager::COLOR,
582 'default' => '',
583 'render_type' => 'ui',
584 'tabs_wrapper' => $tabs,
585 'inner_tab' => 'tab_menu_item_active',
586 'selectors' => [
587 '{{WRAPPER}} .elementor-nav-menu--main .elementor-item.elementor-item-active' => 'color: {{VALUE}}',
588 ],
589 ],
590 // pointer_color_menu_item_active has both tabs_wrapper and inner_tab — safe anchor.
591 [ 'position' => [ 'type' => 'control', 'at' => 'before', 'of' => 'pointer_color_menu_item_active' ] ]
592 );
593 }
594
595 // ── Assets ────────────────────────────────────────────────────────────────
596
597 public function enqueue_frontend_assets(): void {
598 wp_enqueue_style(
599 'ecs-mobile-menu',
600 $this->module_url() . 'assets/css/ecs-mobile-menu.css',
601 [],
602 ECS_VERSION
603 );
604
605 // Loaded in <head> (no in_footer) so our DOMContentLoaded listener is
606 // registered before Elementor Pro's scripts (which load in the footer).
607 // The script reads the computed ECS CSS variables to determine the active
608 // layout per viewport and applies the correct elementor-nav-menu--layout-*
609 // class so pointer animations and dropdown positioning both work correctly.
610 wp_enqueue_script(
611 'ecs-mobile-menu',
612 $this->module_url() . 'assets/js/ecs-mobile-menu.js',
613 [],
614 ECS_VERSION,
615 false
616 );
617 }
618
619 public function enqueue_editor_assets(): void {
620 wp_enqueue_style(
621 'ecs-mobile-menu',
622 $this->module_url() . 'assets/css/ecs-mobile-menu.css',
623 [],
624 ECS_VERSION
625 );
626
627 wp_enqueue_script(
628 'ecs-mobile-menu-editor',
629 $this->module_url() . 'assets/js/ecs-mobile-menu-editor.js',
630 [],
631 ECS_VERSION,
632 true
633 );
634 }
635 }
636