1/** 2 * This file contain the scrips for menu frontend. 3 * @author ExpressTech System 4 * 5 * @since 4.0.0 6 */ 7 8jQuery( document ).ready( function( jQuery ) { 9 10 /** 11 * RmpMenu Class 12 * This RMP class is handling the frontend events and action on menu elements. 13 * @since 4.0.0 14 * @access public 15 * 16 * @class RmpMenu 17 */ 18 class RmpMenu { 19 20 /** 21 * This is constructor function which is initialize the elements and options. 22 * @access public 23 * @since 4.0.0 24 * @param {Array} options List of options. 25 */ 26 constructor( options ) { 27 RmpMenu.activeToggleClass = 'is-active'; 28 RmpMenu.openContainerClass = 'rmp-menu-open'; 29 RmpMenu.activeSubMenuArrowClass = 'rmp-menu-subarrow-active'; 30 RmpMenu.subMenuClass = '.rmp-submenu'; 31 RmpMenu.activeTopMenuClass = 'rmp-topmenu-active'; 32 33 this.options = options; 34 this.menuId = this.options['menu_id']; 35 this.trigger = '#rmp_menu_trigger-' + this.menuId; 36 37 this.isOpen = false; 38 39 this.container = '#rmp-container-' + this.menuId; 40 this.headerBar = '#rmp-header-bar-' + this.menuId; 41 this.menuWrap = 'ul#rmp-menu-'+ this.menuId; 42 this.subMenuArrow = '.rmp-menu-subarrow'; 43 this.wrapper = '.rmp-container'; 44 this.linkElement = '.rmp-menu-item-link'; 45 this.pageWrapper = this.options['page_wrapper']; 46 this.use_desktop_menu = this.options['use_desktop_menu']; 47 this.originalHeight = '', 48 this.animationSpeed = this.options['animation_speed'] * 1000; 49 this.hamburgerBreakpoint = this.options['tablet_breakpoint']; 50 this.subMenuTransitionTime = this.options['sub_menu_speed'] * 1000; 51 52 // The element focus returns to when the panel closes. `this.trigger` may become 53 // a list of selectors once a custom click trigger is configured. 54 this.primaryTrigger = '#rmp_menu_trigger-' + this.menuId; 55 56 // Honour the visitor's motion preference: animating a menu they asked to keep 57 // still is a WCAG 2.3.3 failure, so collapse every duration to zero instead. 58 if ( window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) { 59 this.animationSpeed = 0; 60 this.subMenuTransitionTime = 0; 61 } 62 63 if ( this.options['button_click_trigger'].length > 0 ) { 64 this.trigger = this.trigger +' , '+ this.options['button_click_trigger']; 65 } 66 67 //Append hamburger icon inside an element 68 if ( this.options['button_position_type'] == 'inside-element' ) { 69 var destination = jQuery(this.trigger).attr('data-destination'); 70 jQuery(this.trigger).appendTo(jQuery(destination).parent()); 71 } 72 73 this.init(); 74 } 75 76 /** 77 * This function register the events and initiate the menu settings. 78 */ 79 init() { 80 const self = this; 81 82 /** 83 * Register click event of trigger. 84 * @fires click 85 */ 86 jQuery( this.trigger ).on( 'click', function( e ) { 87 e.stopPropagation(); 88 self.triggerMenu(); 89 } ); 90 91 // Show/Hide sub menu item when the item toggle is activated. The toggle is a 92 // <button>, so Enter and Space reach this handler through a native click. 93 jQuery( self.menuWrap ).find( self.subMenuArrow ).on( 'click', function( e ) { 94 e.preventDefault(); 95 e.stopPropagation(); 96 self.triggerSubArrow( this ); 97 }); 98 99 if ( 'on' == self.options['menu_close_on_body_click'] ) { 100 jQuery( document ).on( 'click', 'body', function ( e ) { 101 if ( jQuery( window ).width() < self.hamburgerBreakpoint ) { 102 if ( self.isOpen ) { 103 if ( jQuery( e.target ).closest( self.container ).length || jQuery( e.target ).closest( self.target ).length ) { 104 return; 105 } 106 } 107 self.closeMenu(); 108 } 109 }); 110 } 111 112 /** 113 * Close the menu when click on menu item link before load. 114 */ 115 if ( self.options['menu_close_on_link_click'] == 'on') { 116 117 jQuery( this.menuWrap +' '+ self.linkElement ).on( 'click', function(e) { 118 119 if( jQuery(window).width() < self.hamburgerBreakpoint ) { 120 e.preventDefault(); 121 122 // When close menu on parent clicks is on. 123 if ( self.options['menu_item_click_to_trigger_submenu'] == 'on' ) { 124 if( jQuery(this).is( '.rmp-menu-item-has-children > ' + self.linkElement ) ) { 125 return; 126 } 127 } 128 129 let _href = jQuery(this).attr('href'); 130 let _target = ( typeof jQuery(this).attr('target') ) == 'undefined' ? '_self' : jQuery(this).attr('target'); 131 132 if( self.isOpen ) { 133 if( jQuery(e.target).closest(self.subMenuArrow).length) { 134 return; 135 } 136 if( typeof _href != 'undefined' ) { 137 self.closeMenu(); 138 setTimeout(function() { 139 window.open( _href, _target); 140 }, self.animationSpeed); 141 } 142 } 143 } 144 }); 145 } 146 147 // Expand Sub items on Parent Item Click. 148 if ( 'on' == self.options['menu_item_click_to_trigger_submenu'] ) { 149 jQuery( this.menuWrap +' .rmp-menu-item-has-children > ' + self.linkElement ).on( 'click', function(e) { 150 if ( jQuery(window).width() < self.hamburgerBreakpoint ) { 151 e.preventDefault(); 152 self.triggerSubArrow( 153 self.arrowForLink( this ) 154 ); 155 } 156 }); 157 } 158 159 /* 160 * Keyboard support for the off-canvas panel. 161 * 162 * The previous implementation swallowed every Tab press on the whole document 163 * while a menu was open and drove focus with its own bookkeeping. That is a 164 * keyboard trap (WCAG 2.1.2): once inside the menu there was no way back out to 165 * the page, and Shift+Tab moved forwards like Tab. Tab order is now left to the 166 * browser; we only close the loop at the two ends of the panel, and only when 167 * focus is genuinely inside it. 168 */ 169 jQuery( document ).on( 'keydown.rmp-' + this.menuId, function ( event ) { 170 if ( ! self.isOpen || ! self.isOffCanvas() ) { 171 return; 172 } 173 174 if ( 'Escape' === event.key || 'Esc' === event.key || 27 === event.keyCode ) { 175 self.handleEscape( event ); 176 return; 177 } 178 179 if ( 'Tab' === event.key || 9 === event.keyCode ) { 180 self.handleTab( event ); 181 } 182 } ); 183 184 // Keep the closed panel out of the tab order and the accessibility tree, and 185 // re-evaluate when the viewport crosses the hamburger breakpoint. 186 this.syncHiddenState(); 187 jQuery( window ).on( 'resize.rmp-' + this.menuId, function () { 188 self.syncHiddenState(); 189 } ); 190 191 // Add rmp-topmenu-active class to current menu item on load 192 this.setActiveMenuItemOnLoad(); 193 194 } 195 /** 196 * True while the container behaves as an overlay panel, i.e. below the hamburger 197 * breakpoint. Above it the container is display:none and needs no special handling. 198 * 199 * @return {boolean} 200 */ 201 isOffCanvas() { 202 // Ask the same question the stylesheet asks. `@media (max-width: N)` is 203 // inclusive and measures the viewport including the scrollbar, while 204 // jQuery's width() is exclusive of both - so at exactly the breakpoint, and 205 // across the scrollbar's width, the two disagreed and the panel was visible 206 // while its keyboard handling was switched off. 207 if ( window.matchMedia ) { 208 return window.matchMedia( '(max-width: ' + parseInt( this.hamburgerBreakpoint, 10 ) + 'px)' ).matches; 209 } 210 211 return jQuery( window ).width() <= this.hamburgerBreakpoint; 212 } 213 214 /** 215 * The elements Tab cycles through while the panel is open: the trigger that opened 216 * it, followed by everything focusable and visible inside the panel. 217 * 218 * @return {Array} Ordered list of DOM elements. 219 */ 220 focusCycle() { 221 const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not
221([tabindex="-1"])'; 222 223 return jQuery( this.primaryTrigger ) 224 .add( jQuery( this.container ).find( focusableSelector ) ) 225 .filter( ':visible' ) 226 .toArray(); 227 } 228 229 /** 230 * Close the tab loop at the ends of the panel. Anything in between - and anything 231 * outside the menu entirely - keeps the browser's own tab order. 232 * 233 * @param {Event} event Keydown event. 234 */ 235 handleTab( event ) { 236 const cycle = this.focusCycle(); 237 238 // With a single focusable element it is both the first and the last, so both 239 // wrap branches would fire and Tab would never leave it. An empty panel has 240 // nothing to contain, so leave the tab order alone in both cases. 241 if ( cycle.length < 2 ) { 242 return; 243 } 244 245 const active = document.activeElement; 246 const container = jQuery( this.container ).get( 0 ); 247 248 // Focus sits on the panel itself right after opening; it is not tabbable, so 249 // wrap it manually rather than letting Shift+Tab escape behind the panel. 250 if ( event.shiftKey && active === container ) { 251 event.preventDefault(); 252 cycle[ cycle.length - 1 ].focus(); 253 return; 254 } 255 256 const index = cycle.indexOf( active ); 257 258 if ( -1 === index ) { 259 return; 260 } 261 262 if ( ! event.shiftKey && index === cycle.length - 1 ) { 263 event.preventDefault(); 264 cycle[0].focus(); 265 } else if ( event.shiftKey && 0 === index ) { 266 event.preventDefault(); 267 cycle[ cycle.length - 1 ].focus(); 268 } 269 } 270 271 /** 272 * Escape collapses the innermost open submenu the visitor is standing in, and 273 * closes the whole panel when there is none. 274 * 275 * @param {Event} event Keydown event. 276 */ 277 handleEscape( event ) { 278 const openSubmenu = jQuery( document.activeElement ).closest( '.rmp-submenu.rmp-submenu-open' ); 279 280 if ( openSubmenu.length ) { 281 const parentArrow = this.arrowForSubmenu( openSubmenu ); 282 283 if ( parentArrow.length ) { 284 event.preventDefault(); 285 this.triggerSubArrow( parentArrow ); 286 parentArrow.trigger( 'focus' ); 287 return; 288 } 289 } 290 291 event.preventDefault(); 292 this.closeMenu(); 293 } 294 295 /** 296 * Hide the panel from assistive technology and from the tab order whenever it is 297 * closed but still laid out - a slid-out panel is merely translated off screen, so 298 * without this its links stay tabbable and a keyboard user tabs into nothing. 299 */ 300 syncHiddenState() { 301 const container = jQuery( this.container ).get( 0 ); 302 303 if ( ! container ) { 304 return; 305 } 306 307 // Derive "open" from the DOM as well as our own flag, so a panel opened by 308 // third-party code that just adds the open class is not left inert. 309 const isOpen = this.isOpen || jQuery( this.container ).hasClass( RmpMenu.openContainerClass ); 310 311 if ( isOpen || ! this.isOffCanvas() ) { 312 container.removeAttribute( 'aria-hidden' ); 313 container.removeAttribute( 'inert' ); 314 return; 315 } 316 317 container.setAttribute( 'aria-hidden', 'true' ); 318 container.setAttribute( 'inert', '' ); 319 } 320 321 /** 322 * Move focus into the panel so the next Tab continues from the menu rather than 323 * from wherever the visitor was on the page. 324 */ 325 focusPanel() { 326 const container = jQuery( this.container ).get( 0 ); 327 328 if ( ! container ) { 329 return; 330 } 331 332 // The fade animation shows the container as it starts, so a task-queue turn is 333 // enough to make sure the element is focusable by the time we ask. 334 window.setTimeout( function () { 335 container.focus( { preventScroll: true } ); 336 }, 0 ); 337 } 338 339 /** 340 * Return focus to the trigger, but only if it currently sits inside the panel we 341 * are about to hide - closing by a click elsewhere must not steal focus. 342 */ 343 restoreFocus() { 344 const container = jQuery( this.container ).get( 0 ); 345 346 if ( ! container || ! container.contains( document.activeElement ) ) { 347 return; 348 } 349 350 jQuery( this.primaryTrigger ).trigger( 'focus' ); 351 } 352 353 // Add rmp-topmenu-active class to current menu item on load 354 setActiveMenuItemOnLoad() { 355 const currentURL = window.location.href; 356 const menuItems = jQuery(this.menuWrap).find('a'); 357 358 menuItems.each(function() { 359 if (this.href === currentURL) { 360 jQuery(this).closest('.rmp-menu-item-has-children').addClass(RmpMenu.activeTopMenuClass); 361 return false; // Exit the loop once we've found and marked the current item 362 } 363 }); 364 } 365 /** 366 * Set push translate for toggle and page wrapper. 367 */ 368 setWrapperTranslate() { 369 let translate,translateContainer; 370 switch( this.options['menu_appear_from'] ) { 371 case 'left': 372 translate = 'translateX(' + this.menuWidth() + 'px)'; 373 translateContainer = 'translateX(-' + this.menuWidth() + 'px)'; 374 break; 375 case 'right': 376 translate = 'translateX(-' + this.menuWidth() + 'px)'; 377 translateContainer = 'translateX(' + this.menuWidth() + 'px)'; 378 break; 379 case 'top': 380 translate = 'translateY(' + this.wrapperHeight() + 'px)'; 381 translateContainer = 'translateY(-' + this.menuHeight() + 'px)'; 382 break; 383 case 'bottom': 384 translate = 'translateY(-' + this.menuHeight() + 'px)'; 385 translateContainer = 'translateY(' + this.menuHeight() + 'px)'; 386 break; 387 } 388 389 if ( this.options['animation_type'] == 'push' ) { 390 jQuery(this.pageWrapper).css( { 'transform':translate } ); 391 392 //If push Wrapper has body element then handle menu position. 393 if ( 'body' == this.pageWrapper ) { 394 jQuery( this.container ).css( { 'transform' : translateContainer } ); 395 } 396 397 } 398 399 if ( this.options['button_push_with_animation'] == 'on' ) { 400 jQuery( this.trigger ).css( { 'transform' : translate } ); 401 } 402 403 } 404 405 /** 406 * Clear push translate on button and page wrapper. 407 */ 408 clearWrapperTranslate() { 409 410 if ( this.options['animation_type'] == 'push' ) { 411 jQuery(this.pageWrapper).css( { 'transform' : '' } ); 412 } 413 414 if ( this.options['button_push_with_animation'] == 'on' ) { 415 jQuery( this.trigger ).css( { 'transform' : '' } ); 416 } 417 } 418 419 /** 420 * Function to fadeIn the hamburger menu container. 421 */ 422 fadeMenuIn() { 423 jQuery(this.container).fadeIn(this.animationSpeed); 424 } 425 426 /** 427 * Function to fadeOut the hamburger menu container. 428 */ 429 fadeMenuOut() { 430 jQuery(this.container) 431 .fadeOut(this.animationSpeed, function() { 432 jQuery(this).css('display', ''); 433 }); 434 } 435 436 /** 437 * Function is use to open the hamburger menu. 438 * 439 * @since 4.0.0 440 */ 441 openMenu() { 442 jQuery(this.trigger).addClass(RmpMenu.activeToggleClass); 443 jQuery(this.container).addClass(RmpMenu.openContainerClass); 444 445 //this.pushMenuTrigger(); 446 447 if ( this.options['animation_type'] == 'fade'){ 448 this.fadeMenuIn(); 449 } else { 450 this.setWrapperTranslate(); 451 } 452 453 this.isOpen = true; 454 455 jQuery( this.trigger ).attr( 'aria-expanded', 'true' ); 456 this.syncHiddenState(); 457 this.focusPanel(); 458 } 459 460 /** 461 * Function is use to close the hamburger menu. 462 * 463 * @since 4.0.0 464 */ 465 closeMenu() { 466 // Hand focus back before hiding: leaving the focused element inside an 467 // aria-hidden/inert subtree is itself an accessibility failure. 468 this.restoreFocus(); 469 470 jQuery(this.trigger).removeClass(RmpMenu.activeToggleClass); 471 jQuery(this.container).removeClass(RmpMenu.openContainerClass); 472 473 if ( this.options['animation_type'] == 'fade') { 474 this.fadeMenuOut(); 475 } else { 476 this.clearWrapperTranslate(); 477 } 478 479 this.isOpen = false;
480 481 jQuery( this.trigger ).attr( 'aria-expanded', 'false' ); 482 this.syncHiddenState(); 483 } 484 485 /** 486 * Function is responsible for checking the menu is open or close. 487 * 488 * @since 4.0.0 489 * @param {Event} e 490 */ 491 triggerMenu() { 492 this.isOpen ? this.closeMenu() : this.openMenu(); 493 } 494 495 /** 496 * Put one submenu toggle into a given state: glyph, active class and - the part 497 * assistive technology actually reads - aria-expanded. 498 * 499 * @param {Object} arrow Toggle button (element or jQuery object). 500 * @param {boolean} expanded Whether the submenu it controls is now open. 501 */ 502 setArrowState( arrow, expanded ) { 503 const $arrow = jQuery( arrow ); 504 505 if ( ! $arrow.length ) { 506 return; 507 } 508 509 $arrow.html( expanded ? this.options['active_toggle_contents'] : this.options['inactive_toggle_contents'] ); 510 $arrow.toggleClass( RmpMenu.activeSubMenuArrowClass, !! expanded ); 511 $arrow.attr( 'aria-expanded', expanded ? 'true' : 'false' ); 512 } 513 514 /* 515 * Where the submenu toggle lives. 516 * 517 * Current markup puts the toggle BESIDE the item link. The 4.7.3 markup put it 518 * INSIDE the link. Both have to be understood, not just the new one: a page 519 * served from a full-page cache (Cloudflare APO, host caches, WP Super Cache) 520 * keeps the old HTML until it is purged, while the browser loads this script - 521 * the file on disk changed under the same URL. Looking only in the new place 522 * left every submenu on such a page dead, and with "parent link opens submenu" 523 * on, the parent link neither opened anything nor navigated. 524 * 525 * Every lookup between a toggle and its submenu goes through these three. 526 */ 527 528 /** 529 * The submenu a toggle controls. 530 * 531 * @param {Object} arrow Toggle (element or jQuery object). 532 * @return {Object} jQuery object, possibly empty. 533 */ 534 submenuFor( arrow ) { 535 const $arrow = jQuery( arrow ); 536 let submenu = $arrow.siblings( RmpMenu.subMenuClass ); 537 538 if ( ! submenu.length ) { 539 submenu = $arrow.parent( this.linkElement ).siblings( RmpMenu.subMenuClass ); 540 } 541 542 return submenu; 543 } 544 545 /** 546 * The toggle belonging to an item link. 547 * 548 * @param {Object} link Item link (element or jQuery object). 549 * @return {Object} jQuery object, possibly empty. 550 */ 551 arrowForLink( link ) { 552 const $link = jQuery( link ); 553 let arrow = $link.siblings( this.subMenuArrow ); 554 555 if ( ! arrow.length ) { 556 arrow = $link.children( this.subMenuArrow ); 557 } 558 559 return arrow.first(); 560 } 561 562 /** 563 * The toggle that controls a given submenu. 564 * 565 * @param {Object} submenu Submenu (element or jQuery object). 566 * @return {Object} jQuery object, possibly empty. 567 */ 568 arrowForSubmenu( submenu ) { 569 const $submenu = jQuery( submenu ); 570 let arrow = $submenu.siblings( this.subMenuArrow ); 571 572 if ( ! arrow.length ) { 573 arrow = $submenu.siblings( this.linkElement ).children( this.subMenuArrow ); 574 } 575 576 return arrow.first(); 577 } 578 579 triggerSubArrow( subArrow ) { 580 var self = this; 581 582 // The toggle is a sibling of the item link inside the <li>, so the submenu it 583 // controls is a sibling of the toggle itself. 584 var sub_menu = this.submenuFor( subArrow ); 585 586 //Accordion animation. 587 if ( self.options['accordion_animation'] == 'on' ) { 588 // Get Top Most Parent and the siblings. 589 var top_siblings = sub_menu.parents('.rmp-menu-item-has-children').last().siblings('.rmp-menu-item-has-children'); 590 var first_siblings = sub_menu.parents('.rmp-menu-item-has-children').first().siblings('.rmp-menu-item-has-children'); 591 592 // Close up just the top level parents to key the rest as it was. 593 top_siblings.children('.rmp-submenu').slideUp(self.subMenuTransitionTime, 'linear').removeClass('rmp-submenu-open'); 594 595 // Set each parent arrow to inactive. 596 top_siblings.each(function() { 597 self.setArrowState( jQuery(this).find(self.subMenuArrow).first(), false ); 598 }); 599 600 // Now Repeat for the current item siblings. 601 first_siblings.children('.rmp-submenu').slideUp(self.subMenuTransitionTime, 'linear').removeClass('rmp-submenu-open'); 602 first_siblings.each(function() { 603 self.setArrowState( jQuery(this).find(self.subMenuArrow).first(), false ); 604 }); 605 } 606 607 // Active sub menu as default behavior. 608 if( sub_menu.hasClass('rmp-submenu-open') ) { 609 sub_menu.slideUp(self.subMenuTransitionTime, 'linear',function() { 610 jQuery(this).css( 'display', '' ); 611 } ).removeClass('rmp-submenu-open'); 612 self.setArrowState( subArrow, false ); 613 } else { 614 sub_menu.slideDown(self.subMenuTransitionTime, 'linear').addClass( 'rmp-submenu-open' ); 615 self.setArrowState( subArrow, true ); 616 } 617 618 } 619 620 /** 621 * Function to add tranform style on trigger. 622 * 623 * @version 4.0.0 624 * 625 * @param {Event} e Event object. 626 */ 627 pushMenuTrigger( e ) { 628 if ( 'on' == this.options['button_push_with_animation'] ) { 629 jQuery( this.trigger ).css( { 'transform' : this.menuWidth() } ); 630 } 631 } 632 633 /** 634 * Returns the height of container. 635 * 636 * @version 4.0.0 637 * 638 * @return Number 639 */ 640 menuHeight() { 641 return jQuery( this.container ).height(); 642 } 643 644 /** 645 * Returns the width of the container. 646 * 647 * @version 4.0.0 648 * 649 * @return Number 650 */ 651 menuWidth() { 652 return jQuery( this.container ).width(); 653 } 654 655 wrapperHeight() { 656 return jQuery( this.wrapper ).height(); 657 } 658 659 backUpSlide( backButton ) { 660 let translateTo = parseInt( jQuery( this.menuWrap )[0].style.transform.replace( /^\D+/g, '' ) ) - 100; 661 jQuery( this.menuWrap ).css( { 'transform': 'translateX(-' + translateTo + '%)' } ); 662 let previousSubmenuHeight = jQuery( backButton ).parent( 'ul' ).parent( 'li' ).parent( '.rmp-submenu' ).height(); 663 if ( ! previousSubmenuHeight ) { 664 jQuery( this.menuWrap ).css( { 'height': this.originalHeight } ); 665 } else { 666 jQuery( this.menuWrap + this.menuId ).css( { 'height': previousSubmenuHeight + 'px' } ); 667 } 668 } 669 } 670
671 /** 672 * Create multiple instance of menu and pass the options. 673 * 674 * @version 4.0.0 675 */ 676 for ( let index = 0; index < rmp_menu.menu.length; index++ ) { 677 let rmp = new RmpMenu( rmp_menu.menu[index] ); 678 } 679 680} );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.