1/* 2 * HS Mega Menu - jQuery Plugin 3 * @version: 1.0.0 (Sun, 26 Feb 2017) 4 * @requires: jQuery v1.6 or later 5 * @author: HtmlStream 6 * @event-namespace: .HSMegaMenu 7 * @browser-support: IE9+ 8 * @license: 9 * 10 * Copyright 2017 HtmlStream 11 * 12 */ 13;(function($){ 14 'use strict'; 15 16 17 /** 18 * Creates a mega menu. 19 * 20 * @constructor 21 * @param {HTMLElement|jQuery} element - The element to create the mega menu for. 22 * @param {Object} options - The options 23 */ 24 function MegaMenu(element, options) { 25 26 var self = this; 27 28 /** 29 * Current element. 30 * 31 * @public 32 */ 33 this.$element = $(element); 34 35 /** 36 * Current options set by the caller including defaults. 37 * 38 * @public 39 */ 40 this.options = $.extend(true, {}, MegaMenu.defaults, options); 41 42 43 /** 44 * Collection of menu elements. 45 * 46 * @private 47 */ 48 this._items = $(); 49 50 51 Object.defineProperties( this, { 52 53 /** 54 * Contains composed selector of menu items. 55 * 56 * @public 57 */ 58 itemsSelector: { 59 get: function() { 60 return self.options.classMap.hasSubMenu + ',' + 61 self.options.classMap.hasMegaMenu; 62 } 63 }, 64 65 /** 66 * Contains chain of active items. 67 * 68 * @private 69 */ 70 _tempChain: { 71 value: null, 72 writable: true 73 }, 74 75 /** 76 * Contains current behavior state. 77 * 78 * @public 79 */ 80 state: { 81 value: null, 82 writable: true 83 } 84 85 }); 86 87 this.initialize(); 88 89 }; 90 91 92 /** 93 * Default options of the mega menu. 94 * 95 * @public 96 */ 97 MegaMenu.defaults = { 98 event: 'hover', 99 direction: 'horizontal', 100 breakpoint: 991, 101 animationIn: false, 102 animationOut: false, 103 104 rtl: false, 105 hideTimeOut: 300, 106 107 // For 'vertical' direction only 108 sideBarRatio: 1/4, 109 pageContainer: $('body'), 110 111 classMap: { 112 initialized: '.hs-menu-initialized', 113 mobileState: '.hs-mobile-state', 114 115 subMenu: '.hs-sub-menu', 116 hasSubMenu: '.hs-has-sub-menu', 117 hasSubMenuActive: '.hs-sub-menu-opened', 118 119 megaMenu: '.hs-mega-menu', 120 hasMegaMenu: '.hs-has-mega-menu', 121 hasMegaMenuActive: '.hs-mega-menu-opened' 122 }, 123 124 mobileSpeed: 400, 125 mobileEasing: 'linear', 126 127 beforeOpen: function(){}, 128 beforeClose: function(){}, 129 afterOpen: function(){}, 130 afterClose: function(){} 131 }; 132 133 134 /** 135 * Initialization of the plugin. 136 * 137 * @protected 138 */ 139 MegaMenu.prototype.initialize = function() { 140 141 var self = this, 142 $w = $(window); 143 144 if( this.options.rtl ) this.$element.addClass('hs-rtl'); 145 146 this.$element 147 .addClass(this.options.classMap.initialized.slice(1)) 148 .addClass('hs-menu-' + this.options.direction); 149 150 151 152 // Independent events 153 $w.on('resize.HSMegaMenu', function(e){ 154 155 if( self.resizeTimeOutId ) clearTimeout( self.resizeTimeOutId ); 156 157 self.resizeTimeOutId = setTimeout( function(){ 158 159 if($w.width() <= self.options.breakpoint && self.state == 'desktop') self.initMobileBehavior(); 160 else if( $w.width() > self.options.breakpoint && self.state == 'mobile' ) self.initDesktopBehavior(); 161 162 self.refresh(); 163 164 }, 50 ); 165 166 }); 167 168 $(document) 169 .on('click.HSMegaMenu', function(e){ 170 171 var $parents = $(e.target).parents(self.itemsSelector); 172 self.closeAll( $parents.add($(e.target)) ); 173 174 }) 175 .on('keyup.HSMegaMenu', function(e){ 176 if( e.keyCode && e.keyCode == 27 ) self.closeAll(); 177 }); 178 179 if($w.width() <= this.options.breakpoint) this.initMobileBehavior(); 180 else if( $w.width() > this.options.breakpoint) this.initDesktopBehavior(); 181 182 this.smartPositions(); 183 184 return this; 185 186 }; 187 188 MegaMenu.prototype.smartPositions = function() { 189 190 var self = this, 191 $submenus = this.$element.find( this.options.classMap.subMenu ); 192 193 $submenus.each(function(i, el) { 194 195 MenuItem.smartPosition( $(el), self.options ); 196 197 }); 198 199 }; 200 201 /** 202 * Binding events to menu elements. 203 * 204 * @protected 205 */ 206 MegaMenu.prototype.bindEvents = function() { 207 208 var self = this, 209 selector; 210 211 // Hover case 212 // if(this.options.event === 'hover' && !_isTouch()) { 213 if(this.options.event === 'hover') { 214 this.$element 215 .on( 216 'mouseenter.HSMegaMenu', 217 this.options.classMap.hasMegaMenu + ':not([data-event="click"]),' + 218 this.options.classMap.hasSubMenu + ':not([data-event="click"])', 219 function(e) { 220 221 var $this = $(this), 222 $chain = $this.parents( self.itemsSelector); 223 224 // Lazy initialization 225 if( !$this.data('HSMenuItem') ) { 226 self.initMenuItem( $this, self.getType( $this ) ); 227 } 228 229 $chain = $chain.add($this); 230 231 self.closeAll( $chain ); 232 233 $chain.each(function(i, el){ 234 235 var HSMenuItem = $(el).data('HSMenuItem'); 236 237 if(HSMenuItem.hideTimeOutId) clearTimeout(HSMenuItem.hideTimeOutId); 238 HSMenuItem.show(); 239 240 }); 241 242 self._items = self._items.not( $chain ); 243 self._tempChain = $chain; 244 245 e.preventDefault(); 246 e.stopPropagation(); 247 } 248 ) 249 .on( 250 'mouseleave.HSMegaMenu', 251 this.options.classMap.hasMegaMenu + ':not([data-event="click"]),' + 252 this.options.classMap.hasSubMenu + ':not([data-event="click"])', 253 function(e) { 254 255 var $this = $(this), 256 HSMenuItem = $this.data('HSMenuItem'), 257 $chain = $(e.relatedTarget).parents( self.itemsSelector ); 258 259 HSMenuItem.hideTimeOutId = setTimeout( function(){ 260 self.closeAll( $chain ); 261 }, self.options.hideTimeOut ); 262 263 self._items = self._items.add( self._tempChain ); 264 self._tempChain = null; 265 266 e.preventDefault(); 267 e.stopPropagation(); 268 } 269 ) 270 .on( 271 'click.HSMegaMenu', 272 this.options.classMap.hasMegaMenu + '[data-event="click"] > a, ' + 273 this.options.classMap.hasSubMenu + '[data-event="click"] > a', 274 function(e) { 275 276 var $this = $(this).parent('[data-event="click"]'), 277 HSMenuItem; 278 279 // Lazy initialization 280 if(!$this.data('HSMenuItem')) { 281 self.initMenuItem( $this, self.getType( $this ) ); 282 } 283 284 285 self.closeAll( $this.add( 286 $this.parents(self.itemsSelector) 287 ) ); 288 289 HSMenuItem = $this 290 .data('HSMenuItem'); 291 292 if(HSMenuItem.isOpened) { 293 HSMenuItem.hide(); 294 } 295 else{ 296 HSMenuItem.show(); 297 } 298 299 300 e.preventDefault(); 301 e.stopPropagation(); 302 303 } 304 ); 305 } 306 // Click case 307 else { 308 309 this.$element 310 .on( 311 'click.HSMegaMenu', 312 (_isTouch() ? 313 this.options.classMap.hasMegaMenu + ' > a, ' + 314 this.options.classMap.hasSubMenu + ' > a' : 315 this.options.classMap.hasMegaMenu + ':not([data-event="hover"]) > a,' + 316 this.options.classMap.hasSubMenu + ':not([data-event="hover"]) > a'), 317 function(e) { 318 319 var $this = $(this).parent(), 320 HSMenuItem, 321 $parents = $this.parents(self.itemsSelector); 322 323 // Lazy initialization 324 if(!$this.data('HSMenuItem')) { 325 self.initMenuItem( $this, self.getType( $this ) ); 326 } 327 328 self.closeAll( $this.add( 329 $this.parents( self.itemsSelector ) 330 ) ); 331 332 HSMenuItem = $this 333 .addClass('hs-event-prevented') 334 .data('HSMenuItem'); 335 336 if(HSMenuItem.isOpened) { 337 HSMenuItem.hide(); 338 } 339 else{ 340 HSMenuItem.show(); 341 } 342 343 e.preventDefault(); 344 e.stopPropagation(); 345 } 346 ); 347 348 if(!_isTouch()) { 349 this.$element 350 .on( 351 'mouseenter.HSMegaMenu', 352 this.options.classMap.hasMegaMenu + '[data-event="hover"],' + 353 this.options.classMap.hasSubMenu + '[data-event="hover"]', 354 function(e) { 355 356 var $this = $(this), 357 $parents = $this.parents( self.itemsSelector); 358 359 // Lazy initialization 360 if( !$this.data('HSMenuItem') ) { 361 self.initMenuItem( $this, self.getType( $this ) ); 362 } 363 364 self.closeAll( $this.add($parents) ); 365 366 $parents.add($this).each(function(i, el){ 367 368 var HSMenuItem = $(el).data('HSMenuItem'); 369 370 if(HSMenuItem.hideTimeOutId) clearTimeout(HSMenuItem.hideTimeOutId); 371 HSMenuItem.show(); 372 373 }); 374 375 e.preventDefault(); 376 e.stopPropagation(); 377 } 378 ) 379 .on( 380 'mouseleave.HSMegaMenu', 381 this.options.classMap.hasMegaMenu + '[data-event="hover"],' + 382 this.options.classMap.hasSubMenu + '[data-event="hover"]', 383 function(e) { 384 385 var $this = $(this), 386 HSMenuItem = $this.data('HSMenuItem'); 387 388 HSMenuItem.hideTimeOutId = setTimeout( function(){ 389 390 self.closeAll( 391 $(e.relatedTarget).parents(self.itemsSelector) 392 ); 393 394 }, self.options.hideTimeOut ); 395 396 e.preventDefault(); 397 e.stopPropagation(); 398 } 399 ) 400 } 401 } 402 403 }; 404 405 /** 406 * Initialization of certain menu item. 407 * 408 * @protected 409 */ 410 MegaMenu.prototype.initMenuItem = function(element, type) { 411 412 var self = this, 413 Item = new MenuItem( 414 element, 415 element.children( 416 self.options.classMap[type === 'mega-menu' ? 'megaMenu' : 'subMenu'] 417 ), 418 $.extend(true, {type: type}, self.options, element.data()), 419 self.$element 420 ); 421 422 element.data( 'HSMenuItem', Item ); 423 this._items = this._items.add( element ); 424 425 }; 426 427 /** 428 * Destroys of desktop behavior, then makes initialization of mobile behavior. 429 * 430 * @protected 431 */ 432 MegaMenu.prototype.initMobileBehavior = function() { 433 434 var self = this; 435 436 this.state = 'mobile'; 437 438 this.$element 439 .off('.HSMegaMenu') 440 .addClass( this.options.classMap.mobileState.slice(1) ) 441 .on('click.HSMegaMenu', self.options.classMap.hasSubMenu + ' > a, ' + self.options.classMap.hasMegaMenu + ' > a', function(e){ 442 443 var $this = $(this).parent(), 444 MenuItemInstance; 445 446 // Lazy initialization 447 if( !$this.data('HSMenuItem') ) { 448 self.initMenuItem( $this, self.getType( $this ) ); 449 } 450 451 self.closeAll( $this.parents( self.itemsSelector ).add($this) ); 452 453 MenuItemInstance = $this 454 .data('HSMenuItem'); 455 456 console.log(MenuItemInstance.isOpened); 457 458 if(MenuItemInstance.isOpened) { 459 MenuItemInstance.mobileHide(); 460 } 461 else { 462 MenuItemInstance.mobileShow(); 463 } 464 465 e.preventDefault(); 466 e.stopPropagation(); 467 468 }) 469 .find( this.itemsSelector ) 470 .not( 471 this.options.classMap.hasSubMenuActive + ',' + 472 this.options.classMap.hasMegaMenuActive 473 ) 474 .children( 475 this.options.classMap.subMenu + ',' + 476 this.options.classMap.megaMenu 477 ) 478 .hide(); 479 480 }; 481 482 /** 483 * Destroys of mobile behavior, then makes initialization of desktop behavior. 484 * 485 * @protected 486 */ 487 MegaMenu.prototype.initDesktopBehavior = function() { 488 489 this.state = 'desktop'; 490 491 this.$element 492 .removeClass(this.options.classMap.mobileState.slice(1)) 493 .off('.HSMegaMenu') 494 .find( this.itemsSelector ) 495 .not( 496 this.options.classMap.hasSubMenuActive + ',' + 497 this.options.classMap.hasMegaMenuActive 498 ) 499 .children( 500 this.options.classMap.subMenu + ',' + 501 this.options.classMap.megaMenu 502 ) 503 .hide(); 504 505 this.bindEvents(); 506 507 }; 508 509 /** 510 * Hides all of opened submenus/megamenus. 511 * 512 * @param {jQuery} except - collection of elements, which shouldn't be closed. 513 * @return {jQuery} 514 * @public 515 */ 516 MegaMenu.prototype.closeAll = function(except) { 517 518 var self = this; 519 520 return this._items.not(except && except.length ? except : $()).each(function(i, el){ 521 522 $(el) 523 .removeClass('hs-event-prevented') 524 .data('HSMenuItem')[self.state == 'mobile' ? 'mobileHide' : 'hide'](); 525 526 }); 527 528 }; 529 530 /** 531 * Returns type of sub menu based on specified menu item. 532 * 533 * @param {jQuery} item 534 * @return {String|null} 535 * @public 536 */ 537 MegaMenu.prototype.getType = function( item ) { 538 539 if(!item || !item.length) return null; 540 541 return item.hasClass(this.options.classMap.hasSubMenu.slice(1)) ? 'sub-menu' : 542 (item.hasClass(this.options.classMap.hasMegaMenu.slice(1)) ? 'mega-menu' : null); 543 544 }; 545 546 /** 547 * Returns current menu state. 548 * 549 * @return {String} 550 * @public 551 */ 552 MegaMenu.prototype.getState = function() { 553 return this.state; 554 }; 555 556 /** 557 * Updates bounds of all menu items. 558 * 559 * @return {jQuery} 560 * @public 561 */ 562 MegaMenu.prototype.refresh = function() { 563 564 return this._items.add( this._tempChain ).each(function(i, el){ 565 $(el).data('HSMenuItem')._updateMenuBounds(); 566 }); 567 568 }; 569 570 571 /** 572 * Creates a mega-menu element. 573 * 574 * @param {jQuery} element 575 * @param {jQuery} menu 576 * @param {Object} options 577 * @param {jQuery} container 578 * @constructor 579 */ 580 function MenuItem(element, menu, options, container) { 581 582 var self = this; 583 584 /** 585 * Current menu item element. 586 * 587 * @public 588 */ 589 this.$element = element; 590 591 /** 592 * Current mega menu element. 593 * 594 * @public 595 */ 596 this.menu = menu; 597 598 /** 599 * Item options. 600 * 601 * @public 602 */ 603 this.options = options; 604 605 606 /** 607 * MegaMenu container. 608 * 609 * @public 610 */ 611 this.$container = container; 612 613 Object.defineProperties(this, { 614 615 /** 616 * Contains css class of menu item element. 617 * 618 * @public 619 */ 620 itemClass: { 621 get: function() { 622 return self.options.type === 'mega-menu' ? 623 self.options.classMap.hasMegaMenu : 624 self.options.classMap.hasSubMenu; 625 } 626 }, 627 628 /** 629 * Contains css active-class of menu item element. 630 * 631 * @public 632 */
633 activeItemClass: { 634 get: function() { 635 return self.options.type === 'mega-menu' ? 636 self.options.classMap.hasMegaMenuActive : 637 self.options.classMap.hasSubMenuActive; 638 } 639 }, 640 641 /** 642 * Contains css class of menu element. 643 * 644 * @public 645 */ 646 menuClass: { 647 get: function() { 648 return self.options.type === 'mega-menu' ? 649 self.options.classMap.megaMenu : 650 self.options.classMap.subMenu; 651 } 652 }, 653 654 isOpened: { 655 get: function() { 656 return this.$element.hasClass(this.activeItemClass.slice(1)); 657 } 658 } 659 660 }); 661 662 663 this.menu.addClass('animated').on('click.HSMegaMenu', function(e){ 664 self._updateMenuBounds(); 665 }); 666 667 if( this.$element.data('max-width') ) this.menu.css('max-width', this.$element.data('max-width') ); 668 if( this.$element.data('position') ) this.menu.addClass( 'hs-position-' + this.$element.data('position') ); 669 670 671 if( this.options.animationOut ) { 672 673 this.menu.on('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(e) { 674 675 if(self.menu.hasClass(self.options.animationOut)) { 676 self.$element.removeClass(self.activeItemClass.slice(1)); 677 self.options.afterClose.call(self, self.$element, self.menu); 678 } 679 680 if(self.menu.hasClass(self.options.animationIn)) { 681 self.options.afterOpen.call(self, self.$element, self.menu); 682 } 683 684 e.stopPropagation(); 685 e.preventDefault(); 686 }); 687 } 688 689 } 690 691 /** 692 * Shows the mega-menu item. 693 * 694 * @public 695 * @return {MenuItem} 696 */ 697 MenuItem.prototype.show = function() { 698 if( !this.menu.length ) return this; 699 700 this.$element.addClass(this.activeItemClass.slice(1)); 701 702 if(this.options.direction == 'horizontal') this.smartPosition( this.menu, this.options ); 703 704 this._updateMenuBounds(); 705 706 if(this.options.animationOut) { 707 this.menu.removeClass(this.options.animationOut); 708 } 709 else { 710 this.options.afterOpen.call(this, this.$element, this.menu); 711 } 712 713 if(this.options.animationIn) this.menu.addClass(this.options.animationIn); 714 715 return this; 716 } 717 718 /** 719 * Hides the mega-menu item. 720 * 721 * @public 722 * @return {MenuItem} 723 */ 724 MenuItem.prototype.hide = function() { 725 726 var self = this; 727 728 if( !this.menu.length ) return this; 729 730 if(!this.options.animationOut) { 731 this.$element.removeClass(this.activeItemClass.slice(1)); 732 } 733 734 if(this.options.animationIn) this.menu.removeClass(this.options.animationIn); 735 if(this.options.animationOut) { 736 this.menu 737 .addClass(this.options.animationOut); 738 } 739 else{ 740 this.options.afterClose.call(this, this.$element, this.menu); 741 } 742 743 return this; 744 } 745 746 /** 747 * Shows the mega-menu item. 748 * 749 * @public 750 * @return {MenuItem} 751 */ 752 MenuItem.prototype.mobileShow = function() { 753 var self = this; 754 755 if( !this.menu.length ) return this; 756 757 758 759 this.menu 760 .removeClass( this.options.animationIn ) 761 .removeClass( this.options.animationOut ) 762 .stop() 763 .slideDown({ 764 duration: self.options.mobileSpeed, 765 easing: self.options.mobileEasing, 766 complete: function() { 767 self.options.afterOpen.call(self, self.$element, self.menu); 768 } 769 }); 770 771 this.$element.addClass(this.activeItemClass.slice(1)); 772 773 return this; 774 }; 775 776 /** 777 * Hides the mega-menu item. 778 * 779 * @public 780 * @return {MenuItem} 781 */ 782 MenuItem.prototype.mobileHide = function() { 783 var self = this; 784 785 if( !this.menu.length ) return this; 786 787 this.menu.stop().slideUp({ 788 duration: self.options.mobileSpeed, 789 easing: self.options.mobileEasing, 790 complete: function() { 791 self.options.afterClose.call(self, self.$element, self.menu); 792 } 793 }); 794 795 this.$element.removeClass(this.activeItemClass.slice(1)); 796 797 return this; 798 }; 799 800 /** 801 * Check, if element is in viewport. 802 * 803 * @param {jQuery} element 804 * @param {Object} options 805 * @public 806 */ 807 MenuItem.prototype.smartPosition = function( element, options ) { 808 809 MenuItem.smartPosition(element, options); 810 811 }; 812 813 /** 814 * Check, if element is in viewport. 815 * 816 * @param {jQuery} element 817 * @param {Object} options 818 * @static 819 * @public 820 */ 821 MenuItem.smartPosition = function( element, options ) { 822 if(!element && !element.length) return; 823 824 var $w = $(window); 825 826 element.removeClass('hs-reversed'); 827 828 if(!options.rtl) { 829 830 if( element.offset().left + element.outerWidth() > $w.width() ) { 831 element.addClass('hs-reversed'); 832 } 833 834 } 835 else { 836 837 if(element.offset().left < 0) { 838 element.addClass('hs-reversed'); 839 } 840 841 } 842 }; 843 844 /** 845 * Updates bounds of current opened menu. 846 * 847 * @private 848 */ 849 MenuItem.prototype._updateMenuBounds = function() { 850 851 var width = 'auto'; 852 853 if( this.options.direction == 'vertical' && this.options.type == 'mega-menu' ) { 854 855 if( this.$container && this.$container.data('HSMegaMenu').get
855State() == 'desktop' ) { 856 if( !this.options.pageContainer.length ) this.options.pageContainer = $('body'); 857 width = this.options.pageContainer.outerWidth() * (1 - this.options.sideBarRatio); 858 } 859 else { 860 width = 'auto'; 861 } 862 863 864 this.menu.css({ 865 'width': width, 866 'height': 'auto' 867 }); 868 869 if( this.menu.outerHeight() > this.$container.outerHeight() ) { 870 return; 871 } 872 873 this.menu.css('height', '100%'); 874 } 875 876 }; 877 878 /** 879 * The jQuery plugin for the MegaMenu. 880 * 881 * @public 882 */ 883 $.fn.HSMegaMenu = function(options) { 884 885 return this.each(function(i, el) { 886 887 var $this = $(this); 888 889 if(!$this.data('HSMegaMenu')) { 890 $this.data('HSMegaMenu', new MegaMenu($this, options)); 891 } 892 893 }); 894 895 }; 896 897 /** 898 * Helper function for detect touch events in the environment. 899 * 900 * @return {Boolean} 901 * @private 902 */ 903 function _isTouch() { 904 return ('ontouchstart' in window); 905 } 906 907})(jQuery);
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.