1/*! 2 * SmartMenus jQuery Plugin Bootstrap 4 Addon - v0.1.0 - July 23, 2020 3 * http://www.smartmenus.org/ 4 * 5 * Copyright Vasil Dinkov, Vadikom Web Ltd. 6 * http://vadikom.com 7 * 8 * Licensed MIT 9 */ 10 11/*! 12 * SmartMenus jQuery Plugin - v1.1.1 - July 23, 2020 13 * http://www.smartmenus.org/ 14 * 15 * Copyright Vasil Dinkov, Vadikom Web Ltd. 16 * http://vadikom.com 17 * 18 * Licensed MIT 19 */ 20 21(function(factory) { 22 if (typeof define === 'function' && define.amd) { 23 // AMD 24 define(['jquery'], factory); 25 } else if (typeof module === 'object' && typeof module.exports === 'object') { 26 // CommonJS 27 module.exports = factory(require('jquery')); 28 } else { 29 // Global jQuery 30 factory(jQuery); 31 } 32} (function($) { 33 34 var menuTrees = [], 35 mouse = false, // optimize for touch by default - we will detect for mouse input 36 touchEvents = 'ontouchstart' in window, // we use this just to choose between toucn and pointer events, not for touch screen detection 37 mouseDetectionEnabled = false, 38 requestAnimationFrame = window.requestAnimationFrame || function(callback) { return setTimeout(callback, 1000 / 60); }, 39 cancelAnimationFrame = window.cancelAnimationFrame || function(id) { clearTimeout(id); }, 40 canAnimate = !!$.fn.animate; 41 42 // Handle detection for mouse input (i.e. desktop browsers, tablets with a mouse, etc.) 43 function initMouseDetection(disable) { 44 var eNS = '.smartmenus_mouse'; 45 if (!mouseDetectionEnabled && !disable) { 46 // if we get two consecutive mousemoves within 2 pixels from each other and within 300ms, we assume a real mouse/cursor is present 47 // in practice, this seems like impossible to trick unintentianally with a real mouse and a pretty safe detection on touch devices (even with older browsers that do not support touch events) 48 var firstTime = true, 49 lastMove = null, 50 events = { 51 'mousemove': function(e) { 52 var thisMove = { x: e.pageX, y: e.pageY, timeStamp: new Date().getTime() }; 53 if (lastMove) { 54 var deltaX = Math.abs(lastMove.x - thisMove.x), 55 deltaY = Math.abs(lastMove.y - thisMove.y); 56 if ((deltaX > 0 || deltaY > 0) && deltaX <= 4 && deltaY <= 4 && thisMove.timeStamp - lastMove.timeStamp <= 300) { 57 mouse = true; 58 // if this is the first check after page load, check if we are not over some item by chance and call the mouseenter handler if yes 59 if (firstTime) { 60 var $a = $(e.target).closest('a'); 61 if ($a.is('a')) { 62 $.each(menuTrees, function() { 63 if ($.contains(this.$root[0], $a[0])) { 64 this.itemEnter({ currentTarget: $a[0] }); 65 return false; 66 } 67 }); 68 } 69 firstTime = false; 70 } 71 } 72 } 73 lastMove = thisMove; 74 } 75 }; 76 events[touchEvents ? 'touchstart' : 'pointerover pointermove pointerout MSPointerOver MSPointerMove MSPointerOut'] = function(e) { 77 if (isTouchEvent(e.originalEvent)) { 78 mouse = false; 79 } 80 }; 81 $(document).on(getEventsNS(events, eNS)); 82 mouseDetectionEnabled = true; 83 } else if (mouseDetectionEnabled && disable) { 84 $(document).off(eNS); 85 mouseDetectionEnabled = false; 86 } 87 } 88 89 function isTouchEvent(e) { 90 return !/^(4|mouse)$/.test(e.pointerType); 91 } 92 93 // returns a jQuery on() ready object 94 function getEventsNS(events, eNS) { 95 if (!eNS) { 96 eNS = ''; 97 } 98 var eventsNS = {}; 99 for (var i in events) { 100 eventsNS[i.split(' ').join(eNS + ' ') + eNS] = events[i]; 101 } 102 return eventsNS; 103 } 104 105 $.SmartMenus = function(elm, options) { 106 this.$root = $(elm); 107 this.opts = options; 108 this.rootId = ''; // internal 109 this.accessIdPrefix = ''; 110 this.$subArrow = null; 111 this.activatedItems = []; // stores last activated A's for each level 112 this.visibleSubMenus = []; // stores visible sub menus UL's (might be in no particular order) 113 this.showTimeout = 0; 114 this.hideTimeout = 0; 115 this.scrollTimeout = 0; 116 this.clickActivated = false; 117 this.focusActivated = false; 118 this.zIndexInc = 0; 119 this.idInc = 0; 120 this.$firstLink = null; // we'll use these for some tests 121 this.$firstSub = null; // at runtime so we'll cache them 122 this.disabled = false; 123 this.$disableOverlay = null; 124 this.$touchScrollingSub = null; 125 this.cssTransforms3d = 'perspective' in elm.style || 'webkitPerspective' in elm.style; 126 this.wasCollapsible = false; 127 this.init(); 128 }; 129 130 $.extend($.SmartMenus, { 131 hideAll: function() { 132 $.each(menuTrees, function() { 133 this.menuHideAll(); 134 }); 135 }, 136 destroy: function() { 137 while (menuTrees.length) { 138 menuTrees[0].destroy(); 139 } 140 initMouseDetection(true); 141 }, 142 prototype: { 143 init: function(refresh) { 144 var self = this; 145 146 if (!refresh) { 147 menuTrees.push(this); 148 149 this.rootId = (new Date().getTime() + Math.random() + '').replace(/\D/g, ''); 150 this.accessIdPrefix = 'sm-' + this.rootId + '-'; 151 152 if (this.$root.hasClass('sm-rtl')) { 153 this.opts.rightToLeftSubMenus = true; 154 } 155 156 // init root (main menu) 157 var eNS = '.smartmenus'; 158 this.$root 159 .data('smartmenus', this) 160 .attr('data-smartmenus-id', this.rootId) 161 .dataSM('level', 1) 162 .on(getEventsNS({ 163 'mouseover focusin': $.proxy(this.rootOver, this), 164 'mouseout focusout': $.proxy(this.rootOut, this), 165 'keydown': $.proxy(this.rootKeyDown, this) 166 }, eNS)) 167 .on(getEventsNS({ 168 'mouseenter': $.proxy(this.itemEnter, this), 169 'mouseleave': $.proxy(this.itemLeave, this), 170 'mousedown': $.proxy(this.itemDown, this),
171 'focus': $.proxy(this.itemFocus, this), 172 'blur': $.proxy(this.itemBlur, this), 173 'click': $.proxy(this.itemClick, this) 174 }, eNS), 'a'); 175 176 // hide menus on tap or click outside the root UL 177 eNS += this.rootId; 178 if (this.opts.hideOnClick) { 179 $(document).on(getEventsNS({ 180 'touchstart': $.proxy(this.docTouchStart, this), 181 'touchmove': $.proxy(this.docTouchMove, this), 182 'touchend': $.proxy(this.docTouchEnd, this), 183 // for Opera Mobile < 11.5, webOS browser, etc. we'll check click too 184 'click': $.proxy(this.docClick, this) 185 }, eNS)); 186 } 187 // hide sub menus on resize 188 $(window).on(getEventsNS({ 'resize orientationchange': $.proxy(this.winResize, this) }, eNS)); 189 190 if (this.opts.subIndicators) { 191 this.$subArrow = $('<span/>').addClass('sub-arrow'); 192 if (this.opts.subIndicatorsText) { 193 this.$subArrow.html(this.opts.subIndicatorsText); 194 } 195 } 196 197 // make sure mouse detection is enabled 198 initMouseDetection(); 199 } 200 201 // init sub menus 202 this.$firstSub = this.$root.find('ul').each(function() { self.menuInit($(this)); }).eq(0); 203 204 this.$firstLink = this.$root.find('a').eq(0); 205 206 // find current item 207 if (this.opts.markCurrentItem) { 208 var reDefaultDoc = /(index|default)\.[^#\?\/]*/i, 209 reHash = /#.*/, 210 locHref = window.location.href.replace(reDefaultDoc, ''), 211 locHrefNoHash = locHref.replace(reHash, ''); 212 this.$root.find('a:not(.mega-menu a)').each(function() { 213 var href = this.href.replace(reDefaultDoc, ''), 214 $this = $(this); 215 if (href == locHref || href == locHrefNoHash) { 216 $this.addClass('current'); 217 if (self.opts.markCurrentTree) { 218 $this.parentsUntil('[data-smartmenus-id]', 'ul').each(function() { 219 $(this).dataSM('parent-a').addClass('current'); 220 });
221 } 222 } 223 }); 224 } 225 226 // save initial state 227 this.wasCollapsible = this.isCollapsible(); 228 }, 229 destroy: function(refresh) { 230 if (!refresh) { 231 var eNS = '.smartmenus'; 232 this.$root 233 .removeData('smartmenus') 234 .removeAttr('data-smartmenus-id') 235 .removeDataSM('level') 236 .off(eNS); 237 eNS += this.rootId; 238 $(document).off(eNS); 239 $(window).off(eNS); 240 if (this.opts.subIndicators) { 241 this.$subArrow = null; 242 } 243 } 244 this.menuHideAll(); 245 var self = this; 246 this.$root.find('ul').each(function() { 247 var $this = $(this); 248 if ($this.dataSM('scroll-arrows')) { 249 $this.dataSM('scroll-arrows').remove(); 250 } 251 if ($this.dataSM('shown-before')) { 252 if (self.opts.subMenusMinWidth || self.opts.subMenusMaxWidth) { 253 $this.css({ width: '', minWidth: '', maxWidth: '' }).removeClass('sm-nowrap'); 254 } 255 if ($this.dataSM('scroll-arrows')) { 256 $this.dataSM('scroll-arrows').remove(); 257 } 258 $this.css({ zIndex: '', top: '', left: '', marginLeft: '', marginTop: '', display: '' }); 259 } 260 if (($this.attr('id') || '').indexOf(self.accessIdPrefix) == 0) { 261 $this.removeAttr('id'); 262 } 263 }) 264 .removeDataSM('in-mega') 265 .removeDataSM('shown-before') 266 .removeDataSM('scroll-arrows') 267 .removeDataSM('parent-a') 268 .removeDataSM('level') 269 .removeDataSM('beforefirstshowfired') 270 .removeAttr('role') 271 .removeAttr('aria-hidden') 272 .removeAttr('aria-labelledby') 273 .removeAttr('aria-expanded'); 274 this.$root.find('a.has-submenu').each(function() { 275 var $this = $(this); 276 if ($this.attr('id').indexOf(self.accessIdPrefix) == 0) { 277 $this.removeAttr('id'); 278 } 279 }) 280 .removeClass('has-submenu') 281 .removeDataSM('sub') 282 .removeAttr('aria-haspopup') 283 .removeAttr('aria-controls') 284 .removeAttr('aria-expanded') 285 .closest('li').removeDataSM('sub'); 286 if (this.opts.subIndicators) { 287 this.$root.find('span.sub-arrow').remove(); 288 } 289 if (this.opts.markCurrentItem) { 290 this.$root.find('a.current').removeClass('current'); 291 } 292 if (!refresh) { 293 this.$root = null; 294 this.$firstLink = null; 295 this.$firstSub = null; 296 if (this.$disableOverlay) { 297 this.$disableOverlay.remove(); 298 this.$disableOverlay = null; 299 } 300 menuTrees.splice($.inArray(this, menuTrees), 1); 301 } 302 }, 303 disable: function(noOverlay) { 304 if (!this.disabled) { 305 this.menuHideAll(); 306 // display overlay over the menu to prevent interaction 307 if (!noOverlay && !this.opts.isPopup && this.$root.is(':visible')) { 308 var pos = this.$root.offset(); 309 this.$disableOverlay = $('<div class="sm-jquery-disable-overlay"/>').css({ 310 position: 'absolute', 311 top: pos.top, 312 left: pos.left, 313 width: this.$root.outerWidth(), 314 height: this.$root.outerHeight(), 315 zIndex: this.getStartZIndex(true), 316 opacity: 0 317 }).appendTo(document.body); 318 } 319 this.disabled = true; 320 } 321 }, 322 docClick: function(e) { 323 if (this.$touchScrollingSub) { 324 this.$touchScrollingSub = null; 325 return; 326 } 327 // hide on any click outside the menu or on a menu link 328 if (this.visibleSubMenus.length && !$.contains(this.$root[0], e.target) || $(e.target).closest('a').length) { 329 this.menuHideAll(); 330 } 331 }, 332 docTouchEnd: function(e) { 333 if (!this.lastTouch) { 334 return; 335 } 336 if (this.visibleSubMenus.length && (this.lastTouch.x2 === undefined || this.lastTouch.x1 == this.lastTouch.x2) && (this.lastTouch.y2 === undefined || this.lastTouch.y1 == this.lastTouch.y2) && (!this.lastTouch.target || !$.contains(this.$root[0], this.lastTouch.target))) { 337 if (this.hideTimeout) { 338 clearTimeout(this.hideTimeout); 339 this.hideTimeout = 0; 340 } 341 // hide with a delay to prevent triggering accidental unwanted click on some page element 342 var self = this; 343 this.hideTimeout = setTimeout(function() { self.menuHideAll(); }, 350); 344 } 345 this.lastTouch = null; 346 }, 347 docTouchMove: function(e) { 348 if (!this.lastTouch) { 349 return; 350 } 351 var touchPoint = e.originalEvent.touches[0]; 352 this.lastTouch.x2 = touchPoint.pageX; 353 this.lastTouch.y2 = touchPoint.pageY; 354 }, 355 docTouchStart: function(e) { 356 var touchPoint = e.originalEvent.touches[0]; 357 this.lastTouch = { x1: touchPoint.pageX, y1: touchPoint.pageY, target: touchPoint.target }; 358 }, 359 enable: function() { 360 if (this.disabled) { 361 if (this.$disableOverlay) { 362 this.$disableOverlay.remove(); 363 this.$disableOverlay = null; 364 } 365 this.disabled = false; 366 } 367 }, 368 getClosestMenu: function(elm) { 369 var $closestMenu = $(elm).closest('ul'); 370 while ($closestMenu.dataSM('in-mega')) { 371 $closestMenu = $closestMenu.parent().closest('ul'); 372 } 373 return $closestMenu[0] || null; 374 }, 375 getHeight: function($elm) { 376 return this.getOffset($elm, true); 377 }, 378 // returns precise width/height float values 379 getOffset: function($elm, height) { 380 var old; 381 if ($elm.css('display') == 'none') { 382 old = { position: $elm[0].style.position, visibility: $elm[0].style.visibility }; 383 $elm.css({ position: 'absolute', visibility: 'hidden' }).show(); 384 } 385 var box = $elm[0].getBoundingClientRect && $elm[0].getBoundingClientRect(), 386 val = box && (height ? box.height || box.bottom - box.top : box.width || box.right - box.left); 387 if (!val && val !== 0) { 388 val = height ? $elm[0].offsetHeight : $elm[0].offsetWidth; 389 } 390 if (old) { 391 $elm.hide().css(old); 392 } 393 return val; 394 }, 395 getStartZIndex: function(root) { 396 var zIndex = parseInt(this[root ? '$root' : '$firstSub'].css('z-index')); 397 if (!root && isNaN(zIndex)) { 398 zIndex = parseInt(this.$root.css('z-index')); 399 } 400 return !isNaN(zIndex) ? zIndex : 1; 401 }, 402 getTouchPoint: function(e) { 403 return e.touches && e.touches[0] || e.changedTouches && e.changedTouches[0] || e; 404 }, 405 getViewport: function(height) { 406 var name = height ? 'Height' : 'Width', 407 val = document.documentElement['client' + name], 408 val2 = window['inner' + name]; 409 if (val2) { 410 val = Math.min(val, val2); 411 } 412 return val; 413 },
414 getViewportHeight: function() { 415 return this.getViewport(true); 416 }, 417 getViewportWidth: function() { 418 return this.getViewport(); 419 }, 420 getWidth: function($elm) { 421 return this.getOffset($elm); 422 }, 423 handleEvents: function() { 424 return !this.disabled && this.isCSSOn(); 425 }, 426 handleItemEvents: function($a) { 427 return this.handleEvents() && !this.isLinkInMegaMenu($a); 428 }, 429 isCollapsible: function() { 430 return this.$firstSub.css('position') == 'static'; 431 }, 432 isCSSOn: function() { 433 return this.$firstLink.css('display') != 'inline'; 434 }, 435 isFixed: function() { 436 var isFixed = this.$root.css('position') == 'fixed'; 437 if (!isFixed) { 438 this.$root.parentsUntil('body').each(function() { 439 if ($(this).css('position') == 'fixed') { 440 isFixed = true; 441 return false; 442 } 443 }); 444 } 445 return isFixed; 446 }, 447 isLinkInMegaMenu: function($a) { 448 return $(this.getClosestMenu($a[0])).hasClass('mega-menu'); 449 }, 450 isTouchMode: function() { 451 return !mouse || this.opts.noMouseOver || this.isCollapsible(); 452 }, 453 itemActivate: function($a, hideDeeperSubs) { 454 var $ul = $a.closest('ul'), 455 level = $ul.dataSM('level'); 456 // if for some reason the parent item is not activated (e.g. this is an API call to activate the item), activate all parent items first 457 if (level > 1 && (!this.activatedItems[level - 2] || this.activatedItems[level - 2][0] != $ul.dataSM('parent-a')[0])) { 458 var self = this; 459 $($ul.parentsUntil('[data-smartmenus-id]', 'ul').get().reverse()).add($ul).each(function() { 460 self.itemActivate($(this).dataSM('parent-a')); 461 }); 462 } 463 // hide any visible deeper level sub menus 464 if (!this.isCollapsible() || hideDeeperSubs) { 465 this.menuHideSubMenus(!this.activatedItems[level - 1] || this.activatedItems[level - 1][0] != $a[0] ? level - 1 : level); 466 } 467 // save new active item for this level 468 this.activatedItems[level - 1] = $a; 469 if (this.$root.triggerHandler('activate.smapi', $a[0]) === false) { 470 return; 471 } 472 // show the sub menu if this item has one 473 var $sub = $a.dataSM('sub'); 474 if ($sub && (this.isTouchMode() || (!this.opts.showOnClick || this.clickActivated))) { 475 this.menuShow($sub); 476 } 477 }, 478 itemBlur: function(e) { 479 var $a = $(e.currentTarget); 480 if (!this.handleItemEvents($a)) { 481 return; 482 } 483 this.$root.triggerHandler('blur.smapi', $a[0]); 484 }, 485 itemClick: function(e) { 486 var $a = $(e.currentTarget); 487 if (!this.handleItemEvents($a)) { 488 return; 489 } 490 if (this.$touchScrollingSub && this.$touchScrollingSub[0] == $a.closest('ul')[0]) { 491 this.$touchScrollingSub = null; 492 e.stopPropagation(); 493 return false; 494 } 495 if (this.$root.triggerHandler('click.smapi', $a[0]) === false) { 496 return false; 497 } 498 var $sub = $a.dataSM('sub'), 499 firstLevelSub = $sub ? $sub.dataSM('level') == 2 : false; 500 if ($sub) { 501 var subArrowClicked = $(e.target).is('.sub-arrow'), 502 collapsible = this.isCollapsible(), 503 behaviorToggle = /toggle$/.test(this.opts.collapsibleBehavior), 504 behaviorLink = /link$/.test(this.opts.collapsibleBehavior), 505 behaviorAccordion = /^accordion/.test(this.opts.collapsibleBehavior); 506 // if the sub is hidden, try to show it 507 if (!$sub.is(':visible')) { 508 if (!behaviorLink || !collapsible || subArrowClicked) { 509 if (!collapsible && this.opts.showOnClick && firstLevelSub) { 510 this.clickActivated = true; 511 } 512 // try to activate the item and show the sub 513 this.itemActivate($a, behaviorAccordion); 514 // if "itemActivate" showed the sub, prevent the click so that the link is not loaded 515 // if it couldn't show it, then the sub menus are disabled with an !important declaration (e.g. via mobile styles) so let the link get loaded 516 if ($sub.is(':visible')) { 517 this.focusActivated = true; 518 return false; 519 } 520 } 521 // if the sub is visible and showOnClick: true, hide the sub 522 } else if (!collapsible && this.opts.showOnClick && firstLevelSub) { 523 this.menuHide($sub); 524 this.clickActivated = false; 525 this.focusActivated = false; 526 return false; 527 // if the sub is visible and we are in collapsible mode 528 } else if (collapsible && (behaviorToggle || subArrowClicked)) { 529 this.itemActivate($a, behaviorAccordion); 530 this.menuHide($sub); 531 return false; 532 } 533 } 534 if (!collapsible && this.opts.showOnClick && firstLevelSub || $a.hasClass('disabled') || this.$root.triggerHandler('select.smapi', $a[0]) === false) { 535 return false; 536 } 537 }, 538 itemDown: function(e) { 539 var $a = $(e.currentTarget); 540 if (!this.handleItemEvents($a)) { 541 return; 542 } 543 $a.dataSM('mousedown', true); 544 }, 545 itemEnter: function(e) { 546 var $a = $(e.currentTarget); 547 if (!this.handleItemEvents($a)) { 548 return; 549 } 550 if (!this.isTouchMode()) { 551 if (this.showTimeout) { 552 clearTimeout(this.showTimeout); 553 this.showTimeout = 0; 554 } 555 var self = this; 556 this.showTimeout = setTimeout(function() { self.itemActivate($a); }, this.opts.showOnClick && $a.closest('ul').dataSM('level') == 1 ? 1 : this.opts.showTimeout); 557 } 558 this.$root.triggerHandler('mouseenter.smapi', $a[0]); 559 }, 560 itemFocus: function(e) { 561 var $a = $(e.currentTarget); 562 if (!this.handleItemEvents($a)) { 563 return; 564 } 565 // fix (the mousedown check): in some browsers a tap/click produces consecutive focus + click events so we don't need to activate the item on focus 566 if (this.focusActivated && (!this.isTouchMode() || !$a.dataSM('mousedown')) && (!this.activatedItems.length || this.activatedItems[this.activatedItems.length - 1][0] != $a[0])) { 567 this.itemActivate($a, true); 568 } 569 this.$root.triggerHandler('focus.smapi', $a[0]); 570 }, 571 itemLeave: function(e) { 572 var $a = $(e.currentTarget); 573 if (!this.handleItemEvents($a)) { 574 return; 575 } 576 if (!this.isTouchMode()) { 577 $a[0].blur(); 578 if (this.showTimeout) { 579 clearTimeout(this.showTimeout); 580 this.showTimeout = 0; 581 } 582 } 583 $a.removeDataSM('mousedown');
584 this.$root.triggerHandler('mouseleave.smapi', $a[0]); 585 }, 586 menuHide: function($sub) { 587 if (this.$root.triggerHandler('beforehide.smapi', $sub[0]) === false) { 588 return; 589 } 590 if (canAnimate) { 591 $sub.stop(true, true); 592 } 593 if ($sub.css('display') != 'none') { 594 var complete = function() { 595 // unset z-index 596 $sub.css('z-index', ''); 597 }; 598 // if sub is collapsible (mobile view) 599 if (this.isCollapsible()) { 600 if (canAnimate && this.opts.collapsibleHideFunction) { 601 this.opts.collapsibleHideFunction.call(this, $sub, complete); 602 } else { 603 $sub.hide(this.opts.collapsibleHideDuration, complete); 604 } 605 } else { 606 if (canAnimate && this.opts.hideFunction) { 607 this.opts.hideFunction.call(this, $sub, complete); 608 } else { 609 $sub.hide(this.opts.hideDuration, complete); 610 } 611 } 612 // deactivate scrolling if it is activated for this sub 613 if ($sub.dataSM('scroll')) { 614 this.menuScrollStop($sub); 615 $sub.css({ 'touch-action': '', '-ms-touch-action': '', '-webkit-transform': '', transform: '' }) 616 .off('.smartmenus_scroll').removeDataSM('scroll').dataSM('scroll-arrows').hide(); 617 } 618 // unhighlight parent item + accessibility 619 $sub.dataSM('parent-a').removeClass('highlighted').attr('aria-expanded', 'false'); 620 $sub.attr({ 621 'aria-expanded': 'false', 622 'aria-hidden': 'true' 623 }); 624 var level = $sub.dataSM('level'); 625 this.activatedItems.splice(level - 1, 1); 626 this.visibleSubMenus.splice($.inArray($sub, this.visibleSubMenus), 1); 627 this.$root.triggerHandler('hide.smapi', $sub[0]); 628 } 629 }, 630 menuHideAll: function() { 631 if (this.showTimeout) { 632 clearTimeout(this.showTimeout); 633 this.showTimeout = 0; 634 } 635 // hide all subs 636 // if it's a popup, this.visibleSubMenus[0] is the root UL 637 var level = this.opts.isPopup ? 1 : 0; 638 for (var i = this.visibleSubMenus.length - 1; i >= level; i--) { 639 this.menuHide(this.visibleSubMenus[i]); 640 } 641 // hide root if it's popup 642 if (this.opts.isPopup) { 643 if (canAnimate) { 644 this.$root.stop(true, true); 645 } 646 if (this.$root.is(':visible')) { 647 if (canAnimate && this.opts.hideFunction) { 648 this.opts.hideFunction.call(this, this.$root); 649 } else { 650 this.$root.hide(this.opts.hideDuration); 651 } 652 } 653 } 654 this.activatedItems = []; 655 this.visibleSubMenus = []; 656 this.clickActivated = false; 657 this.focusActivated = false; 658 // reset z-index increment 659 this.zIndexInc = 0; 660 this.$root.triggerHandler('hideAll.smapi'); 661 }, 662 menuHideSubMenus: function(level) { 663 for (var i = this.activatedItems.length - 1; i >= level; i--) { 664 var $sub = this.activatedItems[i].dataSM('sub'); 665 if ($sub) { 666 this.menuHide($sub); 667 } 668 } 669 }, 670 menuInit: function($ul) { 671 if (!$ul.dataSM('in-mega')) { 672 // mark UL's in mega drop downs (if any) so we can neglect them 673 if ($ul.hasClass('mega-menu')) { 674 $ul.find('ul').dataSM('in-mega', true); 675 } 676 // get level (much faster than, for example, using parentsUntil) 677 var level = 2, 678 par = $ul[0]; 679 while ((par = par.parentNode.parentNode) != this.$root[0]) { 680 level++; 681 } 682 // cache stuff for quick access 683 var $a = $ul.prevAll('a').eq(-1); 684 // if the link is nested (e.g. in a heading) 685 if (!$a.length) { 686 $a = $ul.prevAll().find('a').eq(-1); 687 } 688 $a.addClass('has-submenu').dataSM('sub', $ul); 689 $ul.dataSM('parent-a', $a) 690 .dataSM('level', level) 691 .parent().dataSM('sub', $ul); 692 // accessibility 693 var aId = $a.attr('id') || this.accessIdPrefix + (++this.idInc), 694 ulId = $ul.attr('id') || this.accessIdPrefix + (++this.idInc); 695 $a.attr({ 696 id: aId, 697 'aria-haspopup': 'true', 698 'aria-controls': ulId, 699 'aria-expanded': 'false' 700 }); 701 $ul.attr({ 702 id: ulId, 703 'role': 'group', 704 'aria-hidden': 'true', 705 'aria-labelledby': aId, 706 'aria-expanded': 'false' 707 });
708 // add sub indicator to parent item 709 if (this.opts.subIndicators) { 710 $a[this.opts.subIndicatorsPos](this.$subArrow.clone()); 711 } 712 } 713 }, 714 menuPosition: function($sub) { 715 var $a = $sub.dataSM('parent-a'), 716 $li = $a.closest('li'), 717 $ul = $li.parent(), 718 level = $sub.dataSM('level'), 719 subW = this.getWidth($sub), 720 subH = this.getHeight($sub), 721 itemOffset = $a.offset(), 722 itemX = itemOffset.left, 723 itemY = itemOffset.top, 724 itemW = this.getWidth($a), 725 itemH = this.getHeight($a), 726 $win = $(window), 727 winX = $win.scrollLeft(), 728 winY = $win.scrollTop(), 729 winW = this.getViewportWidth(), 730 winH = this.getViewportHeight(), 731 horizontalParent = $ul.parent().is('[data-sm-horizontal-sub]') || level == 2 && !$ul.hasClass('sm-vertical'), 732 rightToLeft = this.opts.rightToLeftSubMenus && !$li.is('[data-sm-reverse]') || !this.opts.rightToLeftSubMenus && $li.is('[data-sm-reverse]'), 733 subOffsetX = level == 2 ? this.opts.mainMenuSubOffsetX : this.opts.subMenusSubOffsetX, 734 subOffsetY = level == 2 ? this.opts.mainMenuSubOffsetY : this.opts.subMenusSubOffsetY, 735 x, y; 736 if (horizontalParent) { 737 x = rightToLeft ? itemW - subW - subOffsetX : subOffsetX; 738 y = this.opts.bottomToTopSubMenus ? -subH - subOffsetY : itemH + subOffsetY; 739 } else { 740 x = rightToLeft ? subOffsetX - subW : itemW - subOffsetX; 741 y = this.opts.bottomToTopSubMenus ? itemH - subOffsetY - subH : subOffsetY; 742 } 743 if (this.opts.keepInViewport) { 744 var absX = itemX + x, 745 absY = itemY + y; 746 if (rightToLeft && absX < winX) { 747 x = horizontalParent ? winX - absX + x : itemW - subOffsetX; 748 } else if (!rightToLeft && absX + subW > winX + winW) { 749 x = horizontalParent ? winX + winW - subW - absX + x : subOffsetX - subW; 750 } 751 if (!horizontalParent) { 752 if (subH < winH && absY + subH > winY + winH) { 753 y += winY + winH - subH - absY; 754 } else if (subH >= winH || absY < winY) { 755 y += winY - absY; 756 } 757 } 758 // do we need scrolling? 759 // 0.49 used for better precision when dealing with float values 760 if (horizontalParent && (absY + subH > winY + winH + 0.49 || absY < winY) || !horizontalParent && subH > winH + 0.49) { 761 var self = this; 762 if (!$sub.dataSM('scroll-arrows')) { 763 $sub.dataSM('scroll-arrows', $([$('<span class="scroll-up"><span class="scroll-up-arrow"></span></span>')[0], $('<span class="scroll-down"><span class="scroll-down-arrow"></span></span>')[0]]) 764 .on({ 765 mouseenter: function() { 766 $sub.dataSM('scroll').up = $(this).hasClass('scroll-up'); 767 self.menuScroll($sub); 768 }, 769 mouseleave: function(e) { 770 self.menuScrollStop($sub); 771 self.menuScrollOut($sub, e); 772 }, 773 'mousewheel DOMMouseScroll': function(e) { e.preventDefault(); } 774 }) 775 .insertAfter($sub) 776 ); 777 } 778 // bind scroll events and save scroll data for this sub 779 var eNS = '.smartmenus_scroll'; 780 $sub.dataSM('scroll', { 781 y: this.cssTransforms3d ? 0 : y - itemH, 782 step: 1, 783 // cache stuff for faster recalcs later 784 itemH: itemH, 785 subH: subH, 786 arrowDownH: this.getHeight($sub.dataSM('scroll-arrows').eq(1)) 787 }) 788 .on(getEventsNS({ 789 'mouseover': function(e) { self.menuScrollOver($sub, e); }, 790 'mouseout': function(e) { self.menuScrollOut($sub, e); }, 791 'mousewheel DOMMouseScroll': function(e) { self.menuScrollMousewheel($sub, e); } 792 }, eNS)) 793 .dataSM('scroll-arrows').css({ top: 'auto', left: '0', marginLeft: x + (parseInt($sub.css('border-left-width')) || 0), width: subW - (parseInt($sub.css('border-left-width')) || 0) - (parseInt($sub.css('border-right-width')) || 0), zIndex: $sub.css('z-index') }) 794 .eq(horizontalParent && this.opts.bottomToTopSubMenus ? 0 : 1).show(); 795 // when a menu tree is fixed positioned we allow scrolling via touch too 796 // since there is no other way to access such long sub menus if no mouse is present 797 if (this.isFixed()) { 798 var events = {}; 799 events[touchEvents ? 'touchstart touchmove touchend' : 'pointerdown pointermove pointerup MSPointerDown MSPointerMove MSPointerUp'] = function(e) { 800 self.menuScrollTouch($sub, e); 801 }; 802 $sub.css({ 'touch-action': 'none', '-ms-touch-action': 'none' }).on(getEventsNS(events, eNS)); 803 } 804 } 805 } 806 $sub.css({ top: 'auto', left: '0', marginLeft: x, marginTop: y - itemH }); 807 }, 808 menuScroll: function($sub, once, step) { 809 var data = $sub.dataSM('scroll'), 810 $arrows = $sub.dataSM('scroll-arrows'), 811 end = data.up ? data.upEnd : data.downEnd, 812 diff; 813 if (!once && data.momentum) { 814 data.momentum *= 0.92; 815 diff = data.momentum; 816 if (diff < 0.5) { 817 this.menuScrollStop($sub); 818 return; 819 } 820 } else { 821 diff = step || (once || !this.opts.scrollAccelerate ? this.opts.scrollStep : Math.floor(data.step)); 822 } 823 // hide any visible deeper level sub menus 824 var level = $sub.dataSM('level'); 825 if (this.activatedItems[level - 1] && this.activatedItems[level - 1].dataSM('sub') && this.activatedItems[level - 1].dataSM('sub').is(':visible')) {
826 this.menuHideSubMenus(level - 1); 827 } 828 data.y = data.up && end <= data.y || !data.up && end >= data.y ? data.y : (Math.abs(end - data.y) > diff ? data.y + (data.up ? diff : -diff) : end); 829 $sub.css(this.cssTransforms3d ? { '-webkit-transform': 'translate3d(0, ' + data.y + 'px, 0)', transform: 'translate3d(0, ' + data.y + 'px, 0)' } : { marginTop: data.y }); 830 // show opposite arrow if appropriate 831 if (mouse && (data.up && data.y > data.downEnd || !data.up && data.y < data.upEnd)) { 832 $arrows.eq(data.up ? 1 : 0).show(); 833 } 834 // if we've reached the end 835 if (data.y == end) { 836 if (mouse) { 837 $arrows.eq(data.up ? 0 : 1).hide(); 838 } 839 this.menuScrollStop($sub); 840 } else if (!once) { 841 if (this.opts.scrollAccelerate && data.step < this.opts.scrollStep) { 842 data.step += 0.2; 843 } 844 var self = this; 845 this.scrollTimeout = requestAnimationFrame(function() { self.menuScroll($sub); }); 846 } 847 }, 848 menuScrollMousewheel: function($sub, e) { 849 if (this.getClosestMenu(e.target) == $sub[0]) { 850 e = e.originalEvent; 851 var up = (e.wheelDelta || -e.detail) > 0; 852 if ($sub.dataSM('scroll-arrows').eq(up ? 0 : 1).is(':visible')) { 853 $sub.dataSM('scroll').up = up; 854 this.menuScroll($sub, true); 855 } 856 } 857 e.preventDefault(); 858 }, 859 menuScrollOut: function($sub, e) { 860 if (mouse) { 861 if (!/^scroll-(up|down)/.test((e.relatedTarget || '').className) && ($sub[0] != e.relatedTarget && !$.contains($sub[0], e.relatedTarget) || this.getClosestMenu(e.relatedTarget) != $sub[0])) { 862 $sub.dataSM('scroll-arrows').css('visibility', 'hidden'); 863 } 864 } 865 }, 866 menuScrollOver: function($sub, e) { 867 if (mouse) { 868 if (!/^scroll-(up|down)/.test(e.target.className) && this.getClosestMenu(e.target) == $sub[0]) { 869 this.menuScrollRefreshData($sub); 870 var data = $sub.dataSM('scroll'), 871 upEnd = $(window).scrollTop() - $sub.dataSM('parent-a').offset().top - data.itemH; 872 $sub.dataSM('scroll-arrows').eq(0).css('margin-top', upEnd).end() 873 .eq(1).css('margin-top', upEnd + this.getViewportHeight() - data.arrowDownH).end() 874 .css('visibility', 'visible'); 875 } 876 } 877 }, 878 menuScrollRefreshData: function($sub) { 879 var data = $sub.dataSM('scroll'), 880 upEnd = $(window).scrollTop() - $sub.dataSM('parent-a').offset().top - data.itemH; 881 if (this.cssTransforms3d) { 882 upEnd = -(parseFloat($sub.css('margin-top')) - upEnd); 883 } 884 $.extend(data, { 885 upEnd: upEnd, 886 downEnd: upEnd + this.getViewportHeight() - data.subH 887 }); 888 }, 889 menuScrollStop: function($sub) { 890 if (this.scrollTimeout) { 891 cancelAnimationFrame(this.scrollTimeout); 892 this.scrollTimeout = 0; 893 $sub.dataSM('scroll').step = 1; 894 return true; 895 } 896 }, 897 menuScrollTouch: function($sub, e) { 898 e = e.originalEvent; 899 if (isTouchEvent(e)) { 900 var touchPoint = this.getTouchPoint(e); 901 // neglect event if we touched a visible deeper level sub menu 902 if (this.getClosestMenu(touchPoint.target) == $sub[0]) { 903 var data = $sub.dataSM('scroll'); 904 if (/(start|down)$/i.test(e.type)) { 905 if (this.menuScrollStop($sub)) { 906 // if we were scrolling, just stop and don't activate any link on the first touch 907 e.preventDefault(); 908 this.$touchScrollingSub = $sub; 909 } else { 910 this.$touchScrollingSub = null; 911 } 912 // update scroll data since the user might have zoomed, etc. 913 this.menuScrollRefreshData($sub); 914 // extend it with the touch properties 915 $.extend(data, { 916 touchStartY: touchPoint.pageY, 917 touchStartTime: e.timeStamp 918 }); 919 } else if (/move$/i.test(e.type)) { 920 var prevY = data.touchY !== undefined ? data.touchY : data.touchStartY; 921 if (prevY !== undefined && prevY != touchPoint.pageY) { 922 this.$touchScrollingSub = $sub; 923 var up = prevY < touchPoint.pageY; 924 // changed direction? reset... 925 if (data.up !== undefined && data.up != up) { 926 $.extend(data, { 927 touchStartY: touchPoint.pageY, 928 touchStartTime: e.timeStamp 929 });
930 } 931 $.extend(data, { 932 up: up, 933 touchY: touchPoint.pageY 934 }); 935 this.menuScroll($sub, true, Math.abs(touchPoint.pageY - prevY)); 936 } 937 e.preventDefault(); 938 } else { // touchend/pointerup 939 if (data.touchY !== undefined) { 940 if (data.momentum = Math.pow(Math.abs(touchPoint.pageY - data.touchStartY) / (e.timeStamp - data.touchStartTime), 2) * 15) { 941 this.menuScrollStop($sub); 942 this.menuScroll($sub); 943 e.preventDefault(); 944 } 945 delete data.touchY; 946 } 947 } 948 } 949 } 950 }, 951 menuShow: function($sub) { 952 if (!$sub.dataSM('beforefirstshowfired')) { 953 $sub.dataSM('beforefirstshowfired', true); 954 if (this.$root.triggerHandler('beforefirstshow.smapi', $sub[0]) === false) { 955 return; 956 } 957 } 958 if (this.$root.triggerHandler('beforeshow.smapi', $sub[0]) === false) { 959 return; 960 } 961 $sub.dataSM('shown-before', true); 962 if (canAnimate) { 963 $sub.stop(true, true); 964 } 965 if (!$sub.is(':visible')) { 966 // highlight parent item 967 var $a = $sub.dataSM('parent-a'), 968 collapsible = this.isCollapsible(); 969 if (this.opts.keepHighlighted || collapsible) { 970 $a.addClass('highlighted'); 971 } 972 if (collapsible) { 973 $sub.removeClass('sm-nowrap').css({ zIndex: '', width: 'auto', minWidth: '', maxWidth: '', top: '', left: '', marginLeft: '', marginTop: '' }); 974 } else { 975 // set z-index 976 $sub.css('z-index', this.zIndexInc = (this.zIndexInc || this.get
976StartZIndex()) + 1); 977 // min/max-width fix - no way to rely purely on CSS as all UL's are nested 978 if (this.opts.subMenusMinWidth || this.opts.subMenusMaxWidth) { 979 $sub.css({ width: 'auto', minWidth: '', maxWidth: '' }).addClass('sm-nowrap'); 980 if (this.opts.subMenusMinWidth) { 981 $sub.css('min-width', this.opts.subMenusMinWidth); 982 } 983 if (this.opts.subMenusMaxWidth) { 984 var noMaxWidth = this.getWidth($sub); 985 $sub.css('max-width', this.opts.subMenusMaxWidth); 986 if (noMaxWidth > this.getWidth($sub)) { 987 $sub.removeClass('sm-nowrap').css('width', this.opts.subMenusMaxWidth); 988 } 989 } 990 } 991 this.menuPosition($sub); 992 } 993 var complete = function() { 994 // fix: "overflow: hidden;" is not reset on animation complete in jQuery < 1.9.0 in Chrome when global "box-sizing: border-box;" is used 995 $sub.css('overflow', ''); 996 }; 997 // if sub is collapsible (mobile view) 998 if (collapsible) { 999 if (canAnimate && this.opts.collapsibleShowFunction) { 1000 this.opts.collapsibleShowFunction.call(this, $sub, complete); 1001 } else { 1002 $sub.show(this.opts.collapsibleShowDuration, complete); 1003 } 1004 } else { 1005 if (canAnimate && this.opts.showFunction) { 1006 this.opts.showFunction.call(this, $sub, complete); 1007 } else { 1008 $sub.show(this.opts.showDuration, complete); 1009 } 1010 } 1011 // accessibility 1012 $a.attr('aria-expanded', 'true'); 1013 $sub.attr({ 1014 'aria-expanded': 'true', 1015 'aria-hidden': 'false' 1016 }); 1017 // store sub menu in visible array 1018 this.visibleSubMenus.push($sub); 1019 this.$root.triggerHandler('show.smapi', $sub[0]); 1020 } 1021 }, 1022 popupHide: function(noHideTimeout) { 1023 if (this.hideTimeout) { 1024 clearTimeout(this.hideTimeout); 1025 this.hideTimeout = 0; 1026 } 1027 var self = this; 1028 this.hideTimeout = setTimeout(function() { 1029 self.menuHideAll(); 1030 }, noHideTimeout ? 1 : this.opts.hideTimeout); 1031 }, 1032 popupShow: function(left, top) { 1033 if (!this.opts.isPopup) { 1034 alert('SmartMenus jQuery Error:\n\nIf you want to show this menu via the "popupShow" method, set the isPopup:true option.'); 1035 return; 1036 } 1037 if (this.hideTimeout) { 1038 clearTimeout(this.hideTimeout); 1039 this.hideTimeout = 0; 1040 } 1041 this.$root.dataSM('shown-before', true); 1042 if (canAnimate) { 1043 this.$root.stop(true, true); 1044 } 1045 if (!this.$root.is(':visible')) { 1046 this.$root.css({ left: left, top: top }); 1047 // show menu 1048 var self = this, 1049 complete = function() { 1050 self.$root.css('overflow', ''); 1051 }; 1052 if (canAnimate && this.opts.showFunction) { 1053 this.opts.showFunction.call(this, this.$root, complete); 1054 } else { 1055 this.$root.show(this.opts.showDuration, complete); 1056 } 1057 this.visibleSubMenus[0] = this.$root; 1058 } 1059 }, 1060 refresh: function() { 1061 this.destroy(true); 1062 this.init(true); 1063 }, 1064 rootKeyDown: function(e) { 1065 if (!this.handleEvents()) { 1066 return; 1067 } 1068 switch (e.keyCode) { 1069 case 27: // reset on Esc 1070 var $activeTopItem = this.activatedItems[0]; 1071 if ($activeTopItem) { 1072 this.menuHideAll(); 1073 $activeTopItem[0].focus(); 1074 var $sub = $activeTopItem.dataSM('sub'); 1075 if ($sub) { 1076 this.menuHide($sub); 1077 } 1078 } 1079 break; 1080 case 32: // activate item's sub on Space 1081 var $target = $(e.target); 1082 if ($target.is('a') && this.handleItemEvents($target)) { 1083 var $sub = $target.dataSM('sub'); 1084 if ($sub && !$sub.is(':visible')) { 1085 this.itemClick({ currentTarget: e.target }); 1086 e.preventDefault(); 1087 } 1088 } 1089 break; 1090 } 1091 }, 1092 rootOut: function(e) { 1093 if (!this.handleEvents() || this.isTouchMode() || e.target == this.$root[0]) { 1094 return; 1095 } 1096 if (this.hideTimeout) { 1097 clearTimeout(this.hideTimeout); 1098 this.hideTimeout = 0; 1099 } 1100 if (!this.opts.showOnClick || !this.opts.hideOnClick) { 1101 var self = this; 1102 this.hideTimeout = setTimeout(function() { self.menuHideAll(); }, this.opts.hideTimeout); 1103 } 1104 }, 1105 rootOver: function(e) { 1106 if (!this.handleEvents() || this.isTouchMode() || e.target == this.$root[0]) { 1107 return; 1108 } 1109 if (this.hideTimeout) { 1110 clearTimeout(this.hideTimeout); 1111 this.hideTimeout = 0; 1112 } 1113 }, 1114 winResize: function(e) { 1115 if (!this.handleEvents()) { 1116 // we still need to resize the disable overlay if it's visible 1117 if (this.$disableOverlay) { 1118 var pos = this.$root.offset(); 1119 this.$disableOverlay.css({ 1120 top: pos.top, 1121 left: pos.left, 1122 width: this.$root.outerWidth(), 1123 height: this.$root.outerHeight() 1124 });
1125 } 1126 return; 1127 } 1128 // hide sub menus on resize - on mobile do it only on orientation change 1129 if (!('onorientationchange' in window) || e.type == 'orientationchange') { 1130 var collapsible = this.isCollapsible(); 1131 // if it was collapsible before resize and still is, don't do it 1132 if (!(this.wasCollapsible && collapsible)) { 1133 if (this.activatedItems.length) { 1134 this.activatedItems[this.activatedItems.length - 1][0].blur(); 1135 } 1136 this.menuHideAll(); 1137 } 1138 this.wasCollapsible = collapsible; 1139 } 1140 } 1141 } 1142 }); 1143 1144 $.fn.dataSM = function(key, val) { 1145 if (val) { 1146 return this.data(key + '_smartmenus', val); 1147 } 1148 return this.data(key + '_smartmenus'); 1149 }; 1150 1151 $.fn.removeDataSM = function(key) { 1152 return this.removeData(key + '_smartmenus'); 1153 }; 1154 1155 $.fn.smartmenus = function(options) { 1156 if (typeof options == 'string') { 1157 var args = arguments, 1158 method = options; 1159 Array.prototype.shift.call(args); 1160 return this.each(function() { 1161 var smartmenus = $(this).data('smartmenus'); 1162 if (smartmenus && smartmenus[method]) { 1163 smartmenus[method].apply(smartmenus, args); 1164 } 1165 }); 1166 } 1167 return this.each(function() { 1168 // [data-sm-options] attribute on the root UL 1169 var dataOpts = $(this).data('sm-options') || null; 1170 if (dataOpts && typeof dataOpts !== 'object') { 1171 try { 1172 dataOpts = eval('(' + dataOpts + ')'); 1173 } catch(e) { 1174 dataOpts = null; 1175 alert('ERROR\n\nSmartMenus jQuery init:\nInvalid "data-sm-options" attribute value syntax.'); 1176 }; 1177 } 1178 new $.SmartMenus(this, $.extend({}, $.fn.smartmenus.defaults, options, dataOpts)); 1179 }); 1180 }; 1181 1182 // default settings 1183 $.fn.smartmenus.defaults = { 1184 isPopup: false, // is this a popup menu (can be shown via the popupShow/popupHide methods) or a permanent menu bar 1185 mainMenuSubOffsetX: 0, // pixels offset from default position 1186 mainMenuSubOffsetY: 0, // pixels offset from default position 1187 subMenusSubOffsetX: 0, // pixels offset from default position 1188 subMenusSubOffsetY: 0, // pixels offset from default position 1189 subMenusMinWidth: '10em', // min-width for the sub menus (any CSS unit) - if set, the fixed width set in CSS will be ignored 1190 subMenusMaxWidth: '20em', // max-width for the sub menus (any CSS unit) - if set, the fixed width set in CSS will be ignored 1191 subIndicators: true, // create sub menu indicators - creates a SPAN and inserts it in the A 1192 subIndicatorsPos: 'append', // position of the SPAN relative to the menu item content ('append', 'prepend') 1193 subIndicatorsText: '', // [optionally] add text in the SPAN (e.g. '+') (you may want to check the CSS for the sub indicators too) 1194 scrollStep: 30, // pixels step when scrolling long sub menus that do not fit in the viewport height 1195 scrollAccelerate: true, // accelerate scrolling or use a fixed step 1196 showTimeout: 0, // timeout before showing the sub menus 1197 hideTimeout: 500, // timeout before hiding the sub menus 1198 showDuration: 0, // duration for show animation - set to 0 for no animation - matters only if showFunction:null 1199 showFunction: null, // custom function to use when showing a sub menu (the default is the jQuery 'show') 1200 // don't forget to call complete() at the end of whatever you do 1201 // e.g.: function($ul, complete) { $ul.fadeIn(250, complete); } 1202 hideDuration: 0, // duration for hide animation - set to 0 for no animation - matters only if hideFunction:null 1203 hideFunction: function($ul, complete) { $ul.fadeOut(200, complete); }, // custom function to use when hiding a sub menu (the default is the jQuery 'hide') 1204 // don't forget to call complete() at the end of whatever you do 1205 // e.g.: function($ul, complete) { $ul.fadeOut(250, complete); } 1206 collapsibleShowDuration:0, // duration for show animation for collapsible sub menus - matters only if collapsibleShowFunction:null 1207 collapsibleShowFunction:function($ul, complete) { $ul.slideDown(200, complete); }, // custom function to use when showing a collapsible sub menu 1208 // (i.e. when mobile styles are used to make the sub menus collapsible) 1209 collapsibleHideDuration:0, // duration for hide animation for collapsible sub menus - matters only if c
1209ollapsibleHideFunction:null 1210 collapsibleHideFunction:function($ul, complete) { $ul.slideUp(200, complete); }, // custom function to use when hiding a collapsible sub menu 1211 // (i.e. when mobile styles are used to make the sub menus collapsible) 1212 showOnClick: false, // show the first-level sub menus onclick instead of onmouseover (i.e. mimic desktop app menus) (matters only for mouse input) 1213 hideOnClick: true, // hide the sub menus on click/tap anywhere on the page 1214 noMouseOver: false, // disable sub menus activation onmouseover (i.e. behave like in touch mode - use just mouse clicks) (matters only for mouse input) 1215 keepInViewport: true, // reposition the sub menus if needed to make sure they always appear inside the viewport 1216 keepHighlighted: true, // keep all ancestor items of the current sub menu highlighted (adds the 'highlighted' class to the A's) 1217 markCurrentItem: false, // automatically add the 'current' class to the A element of the item linking to the current URL 1218 markCurrentTree: true, // add the 'current' class also to the A elements of all ancestor items of the current item 1219 rightToLeftSubMenus: false, // right to left display of the sub menus (check the CSS for the sub indicators' position) 1220 bottomToTopSubMenus: false, // bottom to top display of the sub menus 1221 collapsibleBehavior: 'default' // parent items behavior in collapsible (mobile) view ('default', 'toggle', 'link', 'accordion', 'accordion-toggle', 'accordion-link') 1222 // 'default' - first tap on parent item expands sub, second tap loads its link 1223 // 'toggle' - the whole parent item acts just as a toggle button for its sub menu (expands/collapses on each tap) 1224 // 'link' - the parent item acts as a regular item (first tap loads its link), the sub menu can be expanded only via the +/- button 1225 // 'accordion' - like 'default' but on expand also resets any visible sub menus from deeper levels or other branches 1226 // 'accordion-toggle' - like 'toggle' but on expand also resets any visible sub menus from deeper levels or other branches 1227 // 'accordion-link' - like 'link' but on expand also resets any visible sub menus from deeper levels or other branches 1228 }; 1229 1230 return $; 1231})); 1232 1233(function(factory) { 1234 if (typeof define === 'function' && define.amd) { 1235 // AMD 1236 define(['jquery', 'smartmenus'], factory); 1237 } else if (typeof module === 'object' && typeof module.exports === 'object') { 1238 // CommonJS 1239 module.exports = factory(require('jquery')); 1240 } else { 1241 // Global jQuery 1242 factory(jQuery); 1243 } 1244} (function($) { 1245 1246 $.extend($.SmartMenus.Bootstrap = {}, { 1247 keydownFix: false, 1248 init: function() { 1249 // init all navbars that don't have the "data-sm-skip" attribute set 1250 var $navbars = $('ul.navbar-nav:not([data-sm-skip])'); 1251 $navbars.each(function() { 1252 var $this = $(this), 1253 obj = $this.data('smartmenus'); 1254 // if this navbar is not initialized 1255 if (!obj) { 1256 var skipBehavior = $this.is('[data-sm-skip-collapsible-behavior]'), 1257 rightAligned = $this.hasClass('ml-auto') || $this.prevAll('.mr-auto').length > 0; 1258 1259 $this.smartmenus({ 1260 // these are some good default options that should work for all 1261 subMenusSubOffsetX: 2, 1262 subMenusSubOffsetY: -9, 1263 subIndicators: !skipBehavior, 1264 collapsibleShowFunction: null, 1265 collapsibleHideFunction: null, 1266 rightToLeftSubMenus: rightAligned, 1267 bottomToTopSubMenus: $this.closest('.fixed-bottom').length > 0, 1268 // custom option(s) for the Bootstrap 4 addon 1269 bootstrapHighlightClasses: 'text-dark bg-light' 1270 }) 1271 .on({ 1272 // set/unset proper Bootstrap classes for some menu elements 1273 'show.smapi': function(e, menu) { 1274 var $menu = $(menu), 1275 $scrollArrows = $menu.dataSM('scroll-arrows'); 1276 if ($scrollArrows) { 1277 $scrollArrows.css('background-color', $menu.css('background-color')); 1278 } 1279 $menu.parent().addClass('show'); 1280 if (obj.opts.keepHighlighted && $menu.dataSM('level') > 2) { 1281 $menu.prevAll('a').addClass(obj.opts.bootstrapHighlightClasses); 1282 } 1283 }, 1284 'hide.smapi': function(e, menu) { 1285 var $menu = $(menu); 1286 $menu.parent().removeClass('show'); 1287 if (obj.opts.keepHighlighted && $menu.dataSM('level') > 2) { 1288 $menu.prevAll('a').removeClass(obj.opts.bootstrapHighlightClasses); 1289 } 1290 } 1291 });
1292 1293 obj = $this.data('smartmenus'); 1294 1295 function onInit() { 1296 // set Bootstrap's "active" class to SmartMenus "current" items (should someone decide to enable markCurrentItem: true) 1297 $this.find('a.current').each(function() { 1298 var $this = $(this); 1299 // dropdown items require the class to be set to the A's while for nav items it should be set to the parent LI's 1300 ($this.hasClass('dropdown-item') ? $this : $this.parent()).addClass('active'); 1301 }); 1302 // parent items fixes 1303 $this.find('a.has-submenu').each(function() { 1304 var $this = $(this); 1305 // remove Bootstrap required attributes that might cause conflicting issues with the SmartMenus script 1306 if ($this.is('[data-toggle="dropdown"]')) { 1307 $this.dataSM('bs-data-toggle-dropdown', true).removeAttr('data-toggle'); 1308 } 1309 // remove Bootstrap's carets generating class 1310 if (!skipBehavior && $this.hasClass('dropdown-toggle')) { 1311 $this.dataSM('bs-dropdown-toggle', true).removeClass('dropdown-toggle'); 1312 } 1313 }); 1314 } 1315 1316 onInit(); 1317 1318 function onBeforeDestroy() { 1319 $this.find('a.current').each(function() { 1320 var $this = $(this); 1321 ($this.hasClass('active') ? $this : $this.parent()).removeClass('active'); 1322 }); 1323 $this.find('a.has-submenu').each(function() { 1324 var $this = $(this); 1325 if ($this.dataSM('bs-dropdown-toggle')) { 1326 $this.addClass('dropdown-toggle').removeDataSM('bs-dropdown-toggle'); 1327 } 1328 if ($this.dataSM('bs-data-toggle-dropdown')) { 1329 $this.attr('data-toggle', 'dropdown').removeDataSM('bs-data-toggle-dropdown'); 1330 } 1331 }); 1332 } 1333 1334 // custom "refresh" method for Bootstrap 1335 obj.refresh = function() { 1336 $.SmartMenus.prototype.refresh.call(this); 1337 onInit(); 1338 // update collapsible detection 1339 detectCollapsible(true); 1340 }; 1341 1342 // custom "destroy" method for Bootstrap 1343 obj.destroy = function(refresh) { 1344 onBeforeDestroy(); 1345 $.SmartMenus.prototype.destroy.call(this, refresh); 1346 }; 1347 1348 // keep Bootstrap's default behavior (i.e. use the whole item area just as a sub menu toggle) 1349 if (skipBehavior) { 1350 obj.opts.collapsibleBehavior = 'toggle'; 1351 } 1352 1353 // onresize detect when the navbar becomes collapsible and add it the "sm-collapsible" class 1354 var winW; 1355 function detectCollapsible(force) { 1356 var newW = obj.getViewportWidth(); 1357 if (newW != winW || force) { 1358 if (obj.isCollapsible()) { 1359 $this.addClass('sm-collapsible'); 1360 } else { 1361 $this.removeClass('sm-collapsible'); 1362 } 1363 winW = newW; 1364 } 1365 } 1366 detectCollapsible(); 1367 $(window).on('resize.smartmenus' + obj.rootId, detectCollapsible); 1368 } 1369 }); 1370 // keydown fix for Bootstrap 4 conflict 1371 if ($navbars.length && !$.SmartMenus.Bootstrap.keydownFix) { 1372 // unhook BS keydown handler for all dropdowns 1373 $(document).off('keydown.bs.dropdown.data-api', '.dropdown-menu'); 1374 // restore BS keydown handler for dropdowns that are not inside SmartMenus navbars 1375 // SmartMenus won't add the "show" class so it's handy here 1376 if ($.fn.dropdown && $.fn.dropdown.Constructor) { 1377 $(document).on('keydown.bs.dropdown.data-api', '.dropdown-menu.show', $.fn.dropdown.Constructor._dataApiKeydownHandler); 1378 } 1379 $.SmartMenus.Bootstrap.keydownFix = true; 1380 } 1381 } 1382 }); 1383 1384 // init ondomready 1385 $($.SmartMenus.Bootstrap.init); 1386 1387 return $; 1388}));
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.