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(window.innerWidth <= self.options.breakpoint && self.state == 'desktop') self.initMobileBehavior(); 160 else if( window.innerWidth > 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(window.innerWidth <= this.options.breakpoint) this.initMobileBehavior(); 180 else if( window.innerWidth > 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 this.$element 214 .on( 215 'mouseenter.HSMegaMenu', 216 this.options.classMap.hasMegaMenu + ':not([data-event="click"]),' + 217 this.options.classMap.hasSubMenu + ':not([data-event="click"])', 218 function(e) { 219 220 var $this = $(this), 221 $chain = $this.parents( self.itemsSelector); 222 223 // Lazy initialization 224 if( !$this.data('HSMenuItem') ) { 225 self.initMenuItem( $this, self.getType( $this ) ); 226 } 227 228 $chain = $chain.add($this); 229 230 self.closeAll( $chain ); 231 232 $chain.each(function(i, el){ 233 234 var HSMenuItem = $(el).data('HSMenuItem'); 235 236 if(HSMenuItem.hideTimeOutId) clearTimeout(HSMenuItem.hideTimeOutId); 237 HSMenuItem.show(); 238 239 }); 240 241 self._items = self._items.not( $chain ); 242 self._tempChain = $chain; 243 244 e.preventDefault(); 245 e.stopPropagation(); 246 } 247 ) 248 .on( 249 'mouseleave.HSMegaMenu', 250 this.options.classMap.hasMegaMenu + ':not([data-event="click"]),' + 251 this.options.classMap.hasSubMenu + ':not([data-event="click"])', 252 function(e) { 253 254 var $this = $(this), 255 HSMenuItem = $this.data('HSMenuItem'), 256 $chain = $(e.relatedTarget).parents( self.itemsSelector ); 257 258 HSMenuItem.hideTimeOutId = setTimeout( function(){ 259 self.closeAll( $chain ); 260 }, self.options.hideTimeOut ); 261 262 self._items = self._items.add( self._tempChain ); 263 self._tempChain = null; 264 265 e.preventDefault(); 266 e.stopPropagation(); 267 } 268 ) 269 .on( 270 'click.HSMegaMenu', 271 this.options.classMap.hasMegaMenu + '[data-event="click"] > a, ' + 272 this.options.classMap.hasSubMenu + '[data-event="click"] > a', 273 function(e) { 274 275 var $this = $(this).parent('[data-event="click"]'), 276 HSMenuItem; 277 278 // Lazy initialization 279 if(!$this.data('HSMenuItem')) { 280 self.initMenuItem( $this, self.getType( $this ) ); 281 } 282 283 284 self.closeAll( $this.add( 285 $this.parents(self.itemsSelector) 286 ) ); 287 288 HSMenuItem = $this 289 .data('HSMenuItem'); 290 291 if(HSMenuItem.isOpened) { 292 HSMenuItem.hide(); 293 } 294 else{ 295 HSMenuItem.show(); 296 } 297 298 299 e.preventDefault(); 300 e.stopPropagation(); 301 302 } 303 ); 304 } 305 // Click case 306 else { 307 308 this.$element 309 .on( 310 'click.HSMegaMenu', 311 (_isTouch() ? 312 this.options.classMap.hasMegaMenu + ' > a, ' + 313 this.options.classMap.hasSubMenu + ' > a' : 314 this.options.classMap.hasMegaMenu + ':not([data-event="hover"]) > a,' + 315 this.options.classMap.hasSubMenu + ':not([data-event="hover"]) > a'), 316 function(e) { 317 318 var $this = $(this).parent(), 319 HSMenuItem, 320 $parents = $this.parents(self.itemsSelector); 321 322 // Lazy initialization 323 if(!$this.data('HSMenuItem')) { 324 self.initMenuItem( $this, self.getType( $this ) ); 325 } 326 327 self.closeAll( $this.add( 328 $this.parents( self.itemsSelector ) 329 ) ); 330 331 HSMenuItem = $this 332 .addClass('hs-event-prevented') 333 .data('HSMenuItem'); 334 335 if(HSMenuItem.isOpened) { 336 HSMenuItem.hide(); 337 } 338 else{ 339 HSMenuItem.show(); 340 } 341 342 e.preventDefault(); 343 e.stopPropagation(); 344 } 345 ); 346 347 if(!_isTouch()) { 348 this.$element 349 .on( 350 'mouseenter.HSMegaMenu', 351 this.options.classMap.hasMegaMenu + '[data-event="hover"],' + 352 this.options.classMap.hasSubMenu + '[data-event="hover"]', 353 function(e) { 354 355 var $this = $(this), 356 $parents = $this.parents( self.itemsSelector); 357 358 // Lazy initialization 359 if( !$this.data('HSMenuItem') ) { 360 self.initMenuItem( $this, self.getType( $this ) ); 361 } 362 363 self.closeAll( $this.add($parents) ); 364 365 $parents.add($this).each(function(i, el){ 366 367 var HSMenuItem = $(el).data('HSMenuItem'); 368 369 if(HSMenuItem.hideTimeOutId) clearTimeout(HSMenuItem.hideTimeOutId); 370 HSMenuItem.show(); 371 372 }); 373 374 e.preventDefault(); 375 e.stopPropagation(); 376 } 377 ) 378 .on( 379 'mouseleave.HSMegaMenu', 380 this.options.classMap.hasMegaMenu + '[data-event="hover"],' + 381 this.options.classMap.hasSubMenu + '[data-event="hover"]', 382 function(e) { 383 384 var $this = $(this), 385 HSMenuItem = $this.data('HSMenuItem'); 386 387 HSMenuItem.hideTimeOutId = setTimeout( function(){ 388 389 self.closeAll( 390 $(e.relatedTarget).parents(self.itemsSelector) 391 ); 392 393 }, self.options.hideTimeOut ); 394 395 e.preventDefault(); 396 e.stopPropagation(); 397 } 398 ) 399 } 400 } 401 402 }; 403 404 /** 405 * Initialization of certain menu item. 406 * 407 * @protected 408 */ 409 MegaMenu.prototype.initMenuItem = function(element, type) { 410 411 var self = this, 412 Item = new MenuItem( 413 element, 414 element.children( 415 self.options.classMap[type === 'mega-menu' ? 'megaMenu' : 'subMenu'] 416 ), 417 $.extend(true, {type: type}, self.options, element.data()), 418 self.$element 419 ); 420 421 element.data( 'HSMenuItem', Item ); 422 this._items = this._items.add( element ); 423 424 }; 425 426 /** 427 * Destroys of desktop behavior, then makes initialization of mobile behavior. 428 * 429 * @protected 430 */ 431 MegaMenu.prototype.initMobileBehavior = function() { 432 433 var self = this; 434 435 this.state = 'mobile'; 436 437 this.$element 438 .off('.HSMegaMenu') 439 .addClass( this.options.classMap.mobileState.slice(1) ) 440 .on('click.HSMegaMenu', self.options.classMap.hasSubMenu + ' > a, ' + self.options.classMap.hasMegaMenu + ' > a', function(e){ 441 442 var $this = $(this).parent(), 443 MenuItemInstance; 444 445 // Lazy initialization 446 if( !$this.data('HSMenuItem') ) { 447 self.initMenuItem( $this, self.getType( $this ) ); 448 } 449 450 self.closeAll( $this.parents( self.itemsSelector ).add($this) ); 451 452 MenuItemInstance = $this 453 .data('HSMenuItem'); 454 455 console.log(MenuItemInstance.isOpened); 456 457 if(MenuItemInstance.isOpened) { 458 MenuItemInstance.mobileHide(); 459 } 460 else { 461 MenuItemInstance.mobileShow(); 462 } 463 464 e.preventDefault(); 465 e.stopPropagation(); 466 467 }) 468 .find( this.itemsSelector ) 469 .not( 470 this.options.classMap.hasSubMenuActive + ',' + 471 this.options.classMap.hasMegaMenuActive 472 ) 473 .children( 474 this.options.classMap.subMenu + ',' + 475 this.options.classMap.megaMenu 476 ) 477 .hide(); 478 479 }; 480 481 /** 482 * Destroys of mobile behavior, then makes initialization of desktop behavior. 483 * 484 * @protected 485 */ 486 MegaMenu.prototype.initDesktopBehavior = function() { 487 488 this.state = 'desktop'; 489 490 this.$element 491 .removeClass(this.options.classMap.mobileState.slice(1)) 492 .off('.HSMegaMenu') 493 .find( this.itemsSelector ) 494 .not( 495 this.options.classMap.hasSubMenuActive + ',' + 496 this.options.classMap.hasMegaMenuActive 497 ) 498 .children( 499 this.options.classMap.subMenu + ',' + 500 this.options.classMap.megaMenu 501 ) 502 .hide(); 503 504 this.bindEvents(); 505 506 }; 507 508 /** 509 * Hides all of opened submenus/megamenus. 510 * 511 * @param {jQuery} except - collection of elements, which shouldn't be closed. 512 * @return {jQuery} 513 * @public 514 */ 515 MegaMenu.prototype.closeAll = function(except) { 516 517 var self = this; 518 519 return this._items.not(except && except.length ? except : $()).each(function(i, el){ 520 521 $(el) 522 .removeClass('hs-event-prevented') 523 .data('HSMenuItem')[self.state == 'mobile' ? 'mobileHide' : 'hide'](); 524 525 }); 526 527 }; 528 529 /** 530 * Returns type of sub menu based on specified menu item. 531 * 532 * @param {jQuery} item 533 * @return {String|null} 534 * @public 535 */ 536 MegaMenu.prototype.getType = function( item ) { 537 538 if(!item || !item.length) return null; 539 540 return item.hasClass(this.options.classMap.hasSubMenu.slice(1)) ? 'sub-menu' : 541 (item.hasClass(this.options.classMap.hasMegaMenu.slice(1)) ? 'mega-menu' : null); 542 543 }; 544 545 /** 546 * Returns current menu state. 547 * 548 * @return {String} 549 * @public 550 */ 551 MegaMenu.prototype.getState = function() { 552 return this.state; 553 }; 554 555 /** 556 * Updates bounds of all menu items. 557 * 558 * @return {jQuery} 559 * @public 560 */ 561 MegaMenu.prototype.refresh = function() { 562 563 return this._items.add( this._tempChain ).each(function(i, el){ 564 $(el).data('HSMenuItem')._updateMenuBounds(); 565 }); 566 567 }; 568 569 570 /** 571 * Creates a mega-menu element. 572 * 573 * @param {jQuery} element 574 * @param {jQuery} menu 575 * @param {Object} options 576 * @param {jQuery} container 577 * @constructor 578 */ 579 function MenuItem(element, menu, options, container) { 580 581 var self = this; 582 583 /** 584 * Current menu item element. 585 * 586 * @public 587 */ 588 this.$element = element; 589 590 /** 591 * Current mega menu element. 592 * 593 * @public 594 */ 595 this.menu = menu; 596 597 /** 598 * Item options. 599 * 600 * @public 601 */ 602 this.options = options; 603 604 605 /** 606 * MegaMenu container. 607 * 608 * @public 609 */ 610 this.$container = container; 611 612 Object.defineProperties(this, { 613 614 /** 615 * Contains css class of menu item element. 616 * 617 * @public 618 */ 619 itemClass: { 620 get: function() { 621 return self.options.type === 'mega-menu' ? 622 self.options.classMap.hasMegaMenu : 623 self.options.classMap.hasSubMenu; 624 } 625 }, 626 627 /** 628 * Contains css active-class of menu item element. 629 * 630 * @public 631 */
632 activeItemClass: { 633 get: function() { 634 return self.options.type === 'mega-menu' ? 635 self.options.classMap.hasMegaMenuActive : 636 self.options.classMap.hasSubMenuActive; 637 } 638 }, 639 640 /** 641 * Contains css class of menu element. 642 * 643 * @public 644 */ 645 menuClass: { 646 get: function() { 647 return self.options.type === 'mega-menu' ? 648 self.options.classMap.megaMenu : 649 self.options.classMap.subMenu; 650 } 651 }, 652 653 isOpened: { 654 get: function() { 655 return this.$element.hasClass(this.activeItemClass.slice(1)); 656 } 657 } 658 659 }); 660 661 662 this.menu.addClass('animated').on('click.HSMegaMenu', function(e){ 663 self._updateMenuBounds(); 664 }); 665 666 if( this.$element.data('max-width') ) this.menu.css('max-width', this.$element.data('max-width') ); 667 if( this.$element.data('position') ) this.menu.addClass( 'hs-position-' + this.$element.data('position') ); 668 669 670 if( this.options.animationOut ) { 671 672 this.menu.on('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(e) { 673 674 if(self.menu.hasClass(self.options.animationOut)) { 675 self.$element.removeClass(self.activeItemClass.slice(1)); 676 self.options.afterClose.call(self, self.$element, self.menu); 677 } 678 679 if(self.menu.hasClass(self.options.animationIn)) { 680 self.options.afterOpen.call(self, self.$element, self.menu); 681 } 682 683 e.stopPropagation(); 684 e.preventDefault(); 685 }); 686 } 687 688 } 689 690 /** 691 * Shows the mega-menu item. 692 * 693 * @public 694 * @return {MenuItem} 695 */ 696 MenuItem.prototype.show = function() { 697 if( !this.menu.length ) return this; 698 699 this.$element.addClass(this.activeItemClass.slice(1)); 700 701 if(this.options.direction == 'horizontal') this.smartPosition( this.menu, this.options ); 702 703 this._updateMenuBounds(); 704 705 if(this.options.animationOut) { 706 this.menu.removeClass(this.options.animationOut); 707 } 708 else { 709 this.options.afterOpen.call(this, this.$element, this.menu); 710 } 711 712 if(this.options.animationIn) this.menu.addClass(this.options.animationIn); 713 714 return this; 715 } 716 717 /** 718 * Hides the mega-menu item. 719 * 720 * @public 721 * @return {MenuItem} 722 */ 723 MenuItem.prototype.hide = function() { 724 725 var self = this; 726 727 if( !this.menu.length ) return this; 728 729 if(!this.options.animationOut) { 730 this.$element.removeClass(this.activeItemClass.slice(1)); 731 } 732 733 if(this.options.animationIn) this.menu.removeClass(this.options.animationIn); 734 if(this.options.animationOut) { 735 this.menu 736 .addClass(this.options.animationOut); 737 } 738 else{ 739 this.options.afterClose.call(this, this.$element, this.menu); 740 } 741 742 return this; 743 } 744 745 /** 746 * Shows the mega-menu item. 747 * 748 * @public 749 * @return {MenuItem} 750 */ 751 MenuItem.prototype.mobileShow = function() { 752 var self = this; 753 754 if( !this.menu.length ) return this; 755 756 757 758 this.menu 759 .removeClass( this.options.animationIn ) 760 .removeClass( this.options.animationOut ) 761 .stop() 762 .slideDown({ 763 duration: self.options.mobileSpeed, 764 easing: self.options.mobileEasing, 765 complete: function() { 766 self.options.afterOpen.call(self, self.$element, self.menu); 767 } 768 }); 769 770 this.$element.addClass(this.activeItemClass.slice(1)); 771 772 return this; 773 }; 774 775 /** 776 * Hides the mega-menu item. 777 * 778 * @public 779 * @return {MenuItem} 780 */ 781 MenuItem.prototype.mobileHide = function() { 782 var self = this; 783 784 if( !this.menu.length ) return this; 785 786 this.menu.stop().slideUp({ 787 duration: self.options.mobileSpeed, 788 easing: self.options.mobileEasing, 789 complete: function() { 790 self.options.afterClose.call(self, self.$element, self.menu); 791 } 792 }); 793 794 this.$element.removeClass(this.activeItemClass.slice(1)); 795 796 return this; 797 }; 798 799 /** 800 * Check, if element is in viewport. 801 * 802 * @param {jQuery} element 803 * @param {Object} options 804 * @public 805 */ 806 MenuItem.prototype.smartPosition = function( element, options ) { 807 808 MenuItem.smartPosition(element, options); 809 810 }; 811 812 /** 813 * Check, if element is in viewport. 814 * 815 * @param {jQuery} element 816 * @param {Object} options 817 * @static 818 * @public 819 */ 820 MenuItem.smartPosition = function( element, options ) { 821 if(!element && !element.length) return; 822 823 var $w = $(window); 824 825 element.removeClass('hs-reversed'); 826 827 if(!options.rtl) { 828 829 if( element.offset().left + element.outerWidth() > window.innerWidth ) { 830 element.addClass('hs-reversed'); 831 } 832 833 } 834 else { 835 836 if(element.offset().left < 0) { 837 element.addClass('hs-reversed'); 838 } 839 840 } 841 }; 842 843 /** 844 * Updates bounds of current opened menu. 845 * 846 * @private 847 */ 848 MenuItem.prototype._updateMenuBounds = function() { 849 850 var width = 'auto'; 851 852 if( this.options.direction == 'vertical' && this.options.type == 'mega-menu' ) { 853 854 if( this.$container && this.$container.data('HSMegaMenu').get
854State() == 'desktop' ) { 855 if( !this.options.pageContainer.length ) this.options.pageContainer = $('body'); 856 width = this.options.pageContainer.outerWidth() * (1 - this.options.sideBarRatio); 857 } 858 else { 859 width = 'auto'; 860 } 861 862 863 this.menu.css({ 864 'width': width, 865 'height': 'auto' 866 }); 867 868 if( this.menu.outerHeight() > this.$container.outerHeight() ) { 869 return; 870 } 871 872 this.menu.css('height', '100%'); 873 } 874 875 }; 876 877 /** 878 * The jQuery plugin for the MegaMenu. 879 * 880 * @public 881 */ 882 $.fn.HSMegaMenu = function(options) { 883 884 return this.each(function(i, el) { 885 886 var $this = $(this); 887 888 if(!$this.data('HSMegaMenu')) { 889 $this.data('HSMegaMenu', new MegaMenu($this, options)); 890 } 891 892 }); 893 894 }; 895 896 /** 897 * Helper function for detect touch events in the environment. 898 * 899 * @return {Boolean} 900 * @private 901 */ 902 function _isTouch() { 903 return ('ontouchstart' in window); 904 } 905 906})(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.