1/*! 2 * 3 * Hover Dropdown And Megamenu 4 * http://www.zozothemes.com/ 5 * 6 */ 7 8(function($) { 9 10 var menuTrees = [], 11 IE = !!window.createPopup, // detect it for the iframe shim 12 mouse = false, // optimize for touch by default - we will detect for mouse input 13 mouseDetectionEnabled = false; 14 15 // Handle detection for mouse input (i.e. desktop browsers, tablets with a mouse, etc.) 16 function initMouseDetection(disable) { 17 var eNS = '.smartmenus_mouse'; 18 if (!mouseDetectionEnabled && !disable) { 19 // if we get two consecutive mousemoves within 2 pixels from each other and within 300ms, we assume a real mouse/cursor is present 20 // 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) 21 var firstTime = true, 22 lastMove = null; 23 $(document).bind(getEventsNS([ 24 ['mousemove', function(e) { 25 var thisMove = { x: e.pageX, y: e.pageY, timeStamp: new Date().getTime() }; 26 if (lastMove) { 27 var deltaX = Math.abs(lastMove.x - thisMove.x), 28 deltaY = Math.abs(lastMove.y - thisMove.y); 29 if ((deltaX > 0 || deltaY > 0) && deltaX <= 2 && deltaY <= 2 && thisMove.timeStamp - lastMove.timeStamp <= 300) { 30 mouse = true; 31 // 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 32 if (firstTime) { 33 var $a = $(e.target).closest('a'); 34 if ($a.is('a')) { 35 $.each(menuTrees, function() { 36 if ($.contains(this.$root[0], $a[0])) { 37 this.itemEnter({ currentTarget: $a[0] }); 38 return false; 39 } 40 }); 41 } 42 firstTime = false; 43 } 44 } 45 } 46 lastMove = thisMove; 47 }], 48 [touchEvents() ? 'touchstart' : 'pointerover pointermove pointerout MSPointerOver MSPointerMove MSPointerOut', function(e) { 49 if (isTouchEvent(e.originalEvent)) { 50 mouse = false; 51 } 52 }] 53 ], eNS)); 54 mouseDetectionEnabled = true; 55 } else if (mouseDetectionEnabled && disable) { 56 $(document).unbind(eNS); 57 mouseDetectionEnabled = false; 58 } 59 } 60 61 function isTouchEvent(e) { 62 return !/^(4|mouse)$/.test(e.pointerType); 63 } 64 65 // we use this just to choose between toucn and pointer events when we need to, not for touch screen detection 66 function touchEvents() { 67 return 'ontouchstart' in window; 68 } 69 70 // returns a jQuery bind() ready object 71 function getEventsNS(defArr, eNS) { 72 if (!eNS) { 73 eNS = ''; 74 } 75 var obj = {}; 76 $.each(defArr, function(index, value) { 77 obj[value[0].split(' ').join(eNS + ' ') + eNS] = value[1]; 78 }); 79 return obj; 80 } 81 82 $.SmartMenus = function(elm, options) { 83 this.$root = $(elm); 84 this.opts = options; 85 this.rootId = ''; // internal 86 this.$subArrow = null; 87 this.subMenus = []; // all sub menus in the tree (UL elms) in no particular order (only real - e.g. UL's in mega sub menus won't be counted) 88 this.activatedItems = []; // stores last activated A's for each level 89 this.visibleSubMenus = []; // stores visible sub menus UL's 90 this.showTimeout = 0; 91 this.hideTimeout = 0; 92 this.scrollTimeout = 0; 93 this.clickActivated = false; 94 this.zIndexInc = 0; 95 this.$firstLink = null; // we'll use these for some tests 96 this.$firstSub = null; // at runtime so we'll cache them 97 this.disabled = false; 98 this.$disableOverlay = null; 99 this.isTouchScrolling = false; 100 this.init(); 101 }; 102 103 $.extend($.SmartMenus, { 104 hideAll: function() { 105 $.each(menuTrees, function() { 106 this.menuHideAll(); 107 }); 108 }, 109 destroy: function() { 110 while (menuTrees.length) { 111 menuTrees[0].destroy(); 112 } 113 initMouseDetection(true); 114 }, 115 prototype: { 116 init: function(refresh) { 117 var self = this; 118 119 if (!refresh) { 120 menuTrees.push(this); 121 122 this.rootId = (new Date().getTime() + Math.random() + '').replace(/\D/g, ''); 123 124 if (this.$root.hasClass('sm-rtl')) { 125 this.opts.rightToLeftSubMenus = true; 126 } 127 128 // init root (main menu) 129 var eNS = '.smartmenus'; 130 this.$root 131 .data('smartmenus', this) 132 .attr('data-smartmenus-id', this.rootId) 133 .dataSM('level', 1) 134 .bind(getEventsNS([ 135 ['mouseover focusin', $.proxy(this.rootOver, this)], 136 ['mouseout focusout', $.proxy(this.rootOut, this)] 137 ], eNS)) 138 .delegate('a', getEventsNS([ 139 ['mouseenter', $.proxy(this.itemEnter, this)], 140 ['mouseleave', $.proxy(this.itemLeave, this)], 141 ['mousedown', $.proxy(this.itemDown, this)], 142 ['focus', $.proxy(this.itemFocus, this)], 143 ['blur', $.proxy(this.itemBlur, this)], 144 ['click', $.proxy(this.itemClick, this)], 145 ['touchend', $.proxy(this.itemTouchEnd, this)] 146 ], eNS)); 147 148 // hide menus on tap or click outside the root UL 149 eNS += this.rootId; 150 if (this.opts.hideOnClick) { 151 $(document).bind(getEventsNS([ 152 ['touchstart', $.proxy(this.docTouchStart, this)], 153 ['touchmove', $.proxy(this.docTouchMove, this)], 154 ['touchend', $.proxy(this.docTouchEnd, this)], 155 // for Opera Mobile < 11.5, webOS browser, etc. we'll check click too 156 ['click', $.proxy(this.docClick, this)] 157 ], eNS)); 158 } 159 // hide sub menus on resize 160 $(window).bind(getEventsNS([['resize orientationchange', $.proxy(this.winResize, this)]], eNS)); 161 162 if (this.opts.subIndicators) { 163 this.$subArrow = $('<span/>').addClass('sub-arrow'); 164 if (this.opts.subIndicatorsText) { 165 this.$subArrow.html(this.opts.subIndicatorsText); 166 } 167 } 168 169 // make sure mouse detection is enabled 170 initMouseDetection(); 171 } 172 173 // init sub menus 174 this.$firstSub = this.$root.find('ul').each(function() { self.menuInit($(this)); }).eq(0); 175 176 this.$firstLink = this.$root.find('a').eq(0); 177 178 // find current item 179 if (this.opts.markCurrentItem) { 180 var reDefaultDoc = /(index|default)\.[^#\?\/]*/i, 181 reHash = /#.*/, 182 locHref = window.location.href.replace(reDefaultDoc, ''), 183 locHrefNoHash = locHref.replace(reHash, ''); 184 this.$root.find('a').each(function() { 185 var href = this.href.replace(reDefaultDoc, ''), 186 $this = $(this); 187 if (href == locHref || href == locHrefNoHash) { 188 $this.addClass('current'); 189 if (self.opts.markCurrentTree) { 190 $this.parent().parentsUntil('[data-smartmenus-id]', 'li').children('a').addClass('current'); 191 } 192 } 193 }); 194 } 195 }, 196 destroy: function() { 197 this.menuHideAll(); 198 var eNS = '.smartmenus'; 199 this.$root 200 .removeData('smartmenus') 201 .removeAttr('data-smartmenus-id') 202 .removeDataSM('level') 203 .unbind(eNS) 204 .undelegate(eNS); 205 eNS += this.rootId; 206 $(document).unbind(eNS); 207 $(window).unbind(eNS); 208 if (this.opts.subIndicators) { 209 this.$subArrow = null; 210 } 211 var self = this; 212 $.each(this.subMenus, function() { 213 if (this.hasClass('mega-menu')) { 214 this.find('ul').removeDataSM('in-mega'); 215 } 216 if (this.dataSM('shown-before')) { 217 if (self.opts.subMenusMinWidth || self.opts.subMenusMaxWidth) { 218 this.css({ width: '', minWidth: '', maxWidth: '' }).removeClass('sm-nowrap'); 219 } 220 if (this.dataSM('scroll-arrows')) { 221 this.dataSM('scroll-arrows').remove(); 222 } 223 this.css({ zIndex: '', top: '', left: '', marginLeft: '', marginTop: '', display: '' }); 224 } 225 if (self.opts.subIndicators) { 226 this.dataSM('parent-a').removeClass('has-submenu').children('span.sub-arrow').remove(); 227 } 228 this.removeDataSM('shown-before') 229 .removeDataSM('ie-shim') 230 .removeDataSM('scroll-arrows') 231 .removeDataSM('parent-a') 232 .removeDataSM('level') 233 .removeDataSM('beforefirstshowfired') 234 .parent().removeDataSM('sub'); 235 }); 236 if (this.opts.markCurrentItem) { 237 this.$root.find('a.current').removeClass('current'); 238 } 239 this.$root = null; 240 this.$firstLink = null; 241 this.$firstSub = null; 242 if (this.$disableOverlay) { 243 this.$disableOverlay.remove(); 244 this.$disableOverlay = null; 245 } 246 menuTrees.splice($.inArray(this, menuTrees), 1); 247 }, 248 disable: function(noOverlay) { 249 if (!this.disabled) { 250 this.menuHideAll(); 251 // display overlay over the menu to prevent interaction 252 if (!noOverlay && !this.opts.isPopup && this.$root.is(':visible')) { 253 var pos = this.$root.offset(); 254 this.$disableOverlay = $('<div class="sm-jquery-disable-overlay"/>').css({ 255 position: 'absolute', 256 top: pos.top, 257 left: pos.left, 258 width: this.$root.outerWidth(), 259 height: this.$root.outerHeight(),
260 zIndex: this.getStartZIndex(true), 261 opacity: 0 262 }).appendTo(document.body); 263 } 264 this.disabled = true; 265 } 266 }, 267 docClick: function(e) { 268 if (this.isTouchScrolling) { 269 this.isTouchScrolling = false; 270 return; 271 } 272 // hide on any click outside the menu or on a menu link 273 if (this.visibleSubMenus.length && !$.contains(this.$root[0], e.target) || $(e.target).is('a')) { 274 this.menuHideAll(); 275 } 276 }, 277 docTouchEnd: function(e) { 278 if (!this.lastTouch) { 279 return; 280 } 281 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))) { 282 if (this.hideTimeout) { 283 clearTimeout(this.hideTimeout); 284 this.hideTimeout = 0; 285 } 286 // hide with a delay to prevent triggering accidental unwanted click on some page element 287 var self = this; 288 this.hideTimeout = setTimeout(function() { self.menuHideAll(); }, 350); 289 } 290 this.lastTouch = null; 291 }, 292 docTouchMove: function(e) { 293 if (!this.lastTouch) { 294 return; 295 } 296 var touchPoint = e.originalEvent.touches[0]; 297 this.lastTouch.x2 = touchPoint.pageX; 298 this.lastTouch.y2 = touchPoint.pageY; 299 }, 300 docTouchStart: function(e) { 301 var touchPoint = e.originalEvent.touches[0]; 302 this.lastTouch = { x1: touchPoint.pageX, y1: touchPoint.pageY, target: touchPoint.target }; 303 }, 304 enable: function() { 305 if (this.disabled) { 306 if (this.$disableOverlay) { 307 this.$disableOverlay.remove(); 308 this.$disableOverlay = null; 309 } 310 this.disabled = false; 311 } 312 }, 313 getClosestMenu: function(elm) { 314 var $closestMenu = $(elm).closest('ul'); 315 while ($closestMenu.dataSM('in-mega')) { 316 $closestMenu = $closestMenu.parent().closest('ul'); 317 } 318 return $closestMenu[0] || null; 319 }, 320 getHeight: function($elm) { 321 return this.getOffset($elm, true); 322 }, 323 // returns precise width/height float values 324 getOffset: function($elm, height) { 325 var old; 326 if ($elm.css('display') == 'none') { 327 old = { position: $elm[0].style.position, visibility: $elm[0].style.visibility }; 328 $elm.css({ position: 'absolute', visibility: 'hidden' }).show(); 329 } 330 var box = $elm[0].getBoundingClientRect && $elm[0].getBoundingClientRect(), 331 val = box && (height ? box.height || box.bottom - box.top : box.width || box.right - box.left); 332 if (!val && val !== 0) { 333 val = height ? $elm[0].offsetHeight : $elm[0].offsetWidth; 334 } 335 if (old) { 336 $elm.hide().css(old); 337 } 338 return val; 339 }, 340 getStartZIndex: function(root) { 341 var zIndex = parseInt(this[root ? '$root' : '$firstSub'].css('z-index')); 342 if (!root && isNaN(zIndex)) { 343 zIndex = parseInt(this.$root.css('z-index')); 344 } 345 return !isNaN(zIndex) ? zIndex : 1; 346 }, 347 getTouchPoint: function(e) { 348 return e.touches && e.touches[0] || e.changedTouches && e.changedTouches[0] || e; 349 }, 350 getViewport: function(height) { 351 var name = height ? 'Height' : 'Width', 352 val = document.documentElement['client' + name], 353 val2 = window['inner' + name]; 354 if (val2) { 355 val = Math.min(val, val2); 356 } 357 return val; 358 }, 359 getViewportHeight: function() { 360 return this.getViewport(true); 361 }, 362 getViewportWidth: function() { 363 return this.getViewport(); 364 }, 365 getWidth: function($elm) { 366 return this.getOffset($elm); 367 }, 368 handleEvents: function() { 369 return !this.disabled && this.isCSSOn(); 370 }, 371 handleItemEvents: function($a) { 372 return this.handleEvents() && !this.isLinkInMegaMenu($a); 373 }, 374 isCollapsible: function() { 375 return this.$firstSub.css('position') == 'static'; 376 }, 377 isCSSOn: function() { 378 return this.$firstLink.css('display') == 'block'; 379 }, 380 isFixed: function() { 381 var isFixed = this.$root.css('position') == 'fixed'; 382 if (!isFixed) { 383 this.$root.parentsUntil('body').each(function() { 384 if ($(this).css('position') == 'fixed') { 385 isFixed = true; 386 return false;
387 } 388 }); 389 } 390 return isFixed; 391 }, 392 isLinkInMegaMenu: function($a) { 393 return !$a.parent().parent().dataSM('level'); 394 }, 395 isTouchMode: function() { 396 return !mouse || this.isCollapsible(); 397 }, 398 itemActivate: function($a) { 399 var $li = $a.parent(), 400 $ul = $li.parent(), 401 level = $ul.dataSM('level'); 402 // 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 403 if (level > 1 && (!this.activatedItems[level - 2] || this.activatedItems[level - 2][0] != $ul.dataSM('parent-a')[0])) { 404 var self = this; 405 $($ul.parentsUntil('[data-smartmenus-id]', 'ul').get().reverse()).add($ul).each(function() { 406 self.itemActivate($(this).dataSM('parent-a')); 407 }); 408 } 409 // hide any visible deeper level sub menus 410 if (this.visibleSubMenus.length > level) { 411 this.menuHideSubMenus(!this.activatedItems[level - 1] || this.activatedItems[level - 1][0] != $a[0] ? level - 1 : level); 412 } 413 // save new active item and sub menu for this level 414 this.activatedItems[level - 1] = $a; 415 this.visibleSubMenus[level - 1] = $ul; 416 if (this.$root.triggerHandler('activate.smapi', $a[0]) === false) { 417 return; 418 } 419 // show the sub menu if this item has one 420 var $sub = $li.dataSM('sub'); 421 if ($sub && (this.isTouchMode() || (!this.opts.showOnClick || this.clickActivated))) { 422 this.menuShow($sub); 423 } 424 }, 425 itemBlur: function(e) { 426 var $a = $(e.currentTarget); 427 if (!this.handleItemEvents($a)) { 428 return; 429 } 430 this.$root.triggerHandler('blur.smapi', $a[0]); 431 }, 432 itemClick: function(e) { 433 if (this.isTouchScrolling) { 434 this.isTouchScrolling = false; 435 e.stopPropagation(); 436 return false; 437 } 438 var $a = $(e.currentTarget); 439 if (!this.handleItemEvents($a)) { 440 return; 441 } 442 $a.removeDataSM('mousedown'); 443 if (this.$root.triggerHandler('click.smapi', $a[0]) === false) { 444 return false; 445 } 446 var $sub = $a.parent().dataSM('sub'); 447 if (this.isTouchMode()) { 448 // undo fix: prevent the address bar on iPhone from sliding down when expanding a sub menu 449 if ($a.dataSM('href')) { 450 $a.attr('href', $a.dataSM('href')).removeDataSM('href'); 451 } 452 // if the sub is not visible 453 if ($sub && (!$sub.dataSM('shown-before') || !$sub.is(':visible'))) { 454 // try to activate the item and show the sub 455 this.itemActivate($a); 456 // if "itemActivate" showed the sub, prevent the click so that the link is not loaded 457 // 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 458 if ($sub.is(':visible')) { 459 return false; 460 } 461 } 462 } else if (this.opts.showOnClick && $a.parent().parent().dataSM('level') == 1 && $sub) { 463 this.clickActivated = true; 464 this.menuShow($sub); 465 return false; 466 } 467 if ($a.hasClass('disabled')) { 468 return false; 469 } 470 if (this.$root.triggerHandler('select.smapi', $a[0]) === false) { 471 return false; 472 } 473 }, 474 itemDown: function(e) { 475 var $a = $(e.currentTarget); 476 if (!this.handleItemEvents($a)) { 477 return; 478 } 479 $a.dataSM('mousedown', true); 480 }, 481 itemEnter: function(e) { 482 var $a = $(e.currentTarget); 483 if (!this.handleItemEvents($a)) { 484 return; 485 } 486 if (!this.isTouchMode()) { 487 if (this.showTimeout) { 488 clearTimeout(this.showTimeout); 489 this.showTimeout = 0; 490 } 491 var self = this; 492 this.showTimeout = setTimeout(function() { self.itemActivate($a); }, this.opts.showOnClick && $a.parent().parent().dataSM('level') == 1 ? 1 : this.opts.showTimeout); 493 } 494 this.$root.triggerHandler('mouseenter.smapi', $a[0]); 495 }, 496 itemFocus: function(e) { 497 var $a = $(e.currentTarget); 498 if (!this.handleItemEvents($a)) { 499 return; 500 } 501 // 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 502 if ((!this.isTouchMode() || !$a.dataSM('mousedown')) && (!this.activatedItems.length || this.activatedItems[this.activatedItems.length - 1][0] != $a[0])) { 503 this.itemActivate($a); 504 } 505 this.$root.triggerHandler('focus.smapi', $a[0]); 506 }, 507 itemLeave: function(e) { 508 var $a = $(e.currentTarget); 509 if (!this.handleItemEvents($a)) { 510 return; 511 } 512 if (!this.isTouchMode()) { 513 if ($a[0].blur) { 514 $a[0].blur(); 515 } 516 if (this.showTimeout) { 517 clearTimeout(this.showTimeout); 518 this.showTimeout = 0; 519 } 520 } 521 $a.removeDataSM('mousedown');
522 this.$root.triggerHandler('mouseleave.smapi', $a[0]); 523 }, 524 itemTouchEnd: function(e) { 525 var $a = $(e.currentTarget); 526 if (!this.handleItemEvents($a)) { 527 return; 528 } 529 // prevent the address bar on iPhone from sliding down when expanding a sub menu 530 var $sub = $a.parent().dataSM('sub'); 531 if ($a.attr('href').charAt(0) !== '#' && $sub && (!$sub.dataSM('shown-before') || !$sub.is(':visible'))) { 532 $a.dataSM('href', $a.attr('href')); 533 $a.attr('href', '#'); 534 } 535 }, 536 menuFixLayout: function($ul) { 537 // fixes a menu that is being shown for the first time 538 if (!$ul.dataSM('shown-before')) { 539 $ul.hide().dataSM('shown-before', true); 540 } 541 }, 542 menuHide: function($sub) { 543 if (this.$root.triggerHandler('beforehide.smapi', $sub[0]) === false) { 544 return; 545 } 546 $sub.stop(true, true); 547 if ($sub.is(':visible')) { 548 var complete = function() { 549 // unset z-index 550 $sub.css('z-index', ''); 551 }; 552 // if sub is collapsible (mobile view) 553 if (this.isCollapsible()) { 554 if (this.opts.collapsibleHideFunction) { 555 this.opts.collapsibleHideFunction.call(this, $sub, complete); 556 } else { 557 $sub.hide(this.opts.collapsibleHideDuration, complete); 558 } 559 } else { 560 if (this.opts.hideFunction) { 561 this.opts.hideFunction.call(this, $sub, complete); 562 } else { 563 $sub.hide(this.opts.hideDuration, complete); 564 } 565 } 566 // remove IE iframe shim 567 if ($sub.dataSM('ie-shim')) { 568 $sub.dataSM('ie-shim').remove(); 569 } 570 // deactivate scrolling if it is activated for this sub 571 if ($sub.dataSM('scroll')) { 572 this.menuScrollStop($sub); 573 $sub.css({ 'touch-action': '', '-ms-touch-action': '' }) 574 .unbind('.smartmenus_scroll').removeDataSM('scroll').dataSM('scroll-arrows').hide(); 575 } 576 // unhighlight parent item 577 $sub.dataSM('parent-a').removeClass('highlighted'); 578 var level = $sub.dataSM('level'); 579 this.activatedItems.splice(level - 1, 1); 580 this.visibleSubMenus.splice(level - 1, 1); 581 this.$root.triggerHandler('hide.smapi', $sub[0]); 582 } 583 }, 584 menuHideAll: function() { 585 if (this.showTimeout) { 586 clearTimeout(this.showTimeout); 587 this.showTimeout = 0; 588 } 589 // hide all subs 590 this.menuHideSubMenus(); 591 // hide root if it's popup 592 if (this.opts.isPopup) { 593 this.$root.stop(true, true); 594 if (this.$root.is(':visible')) { 595 if (this.opts.hideFunction) { 596 this.opts.hideFunction.call(this, this.$root); 597 } else { 598 this.$root.hide(this.opts.hideDuration); 599 } 600 // remove IE iframe shim 601 if (this.$root.dataSM('ie-shim')) { 602 this.$root.dataSM('ie-shim').remove(); 603 } 604 } 605 } 606 this.activatedItems = []; 607 this.visibleSubMenus = []; 608 this.clickActivated = false; 609 // reset z-index increment 610 this.zIndexInc = 0; 611 }, 612 menuHideSubMenus: function(level) { 613 if (!level) 614 level = 0; 615 for (var i = this.visibleSubMenus.length - 1; i > level; i--) { 616 this.menuHide(this.visibleSubMenus[i]); 617 } 618 }, 619 menuIframeShim: function($ul) { 620 // create iframe shim for the menu 621 if (IE && this.opts.overlapControlsInIE && !$ul.dataSM('ie-shim')) { 622 $ul.dataSM('ie-shim', $('<iframe/>').attr({ src: 'javascript:0', tabindex: -9 }) 623 .css({ position: 'absolute', top: 'auto', left: '0', opacity: 0, border: '0' }) 624 ); 625 } 626 }, 627 menuInit: function($ul) { 628 if (!$ul.dataSM('in-mega')) { 629 this.subMenus.push($ul); 630 // mark UL's in mega drop downs (if any) so we can neglect them 631 if ($ul.hasClass('mega-menu')) { 632 $ul.find('ul').dataSM('in-mega', true); 633 } 634 // get level (much faster than, for example, using parentsUntil) 635 var level = 2, 636 par = $ul[0]; 637 while ((par = par.parentNode.parentNode) != this.$root[0]) { 638 level++; 639 } 640 // cache stuff 641 $ul.dataSM('parent-a', $ul.prevAll('a').eq(-1)) 642 .dataSM('level', level) 643 .parent().dataSM('sub', $ul); 644 // add sub indicator to parent item 645 if (this.opts.subIndicators) { 646 $ul.dataSM('parent-a').addClass('has-submenu')[this.opts.subIndicatorsPos](this.$subArrow.clone()); 647 } 648 } 649 }, 650 menuPosition: function($sub) { 651 var $a = $sub.dataSM('parent-a'), 652 $ul = $sub.parent().parent(), 653 level = $sub.dataSM('level'), 654 subW = this.getWidth($sub), 655 subH = this.getHeight($sub), 656 itemOffset = $a.offset(), 657 itemX = itemOffset.left, 658 itemY = itemOffset.top, 659 itemW = this.getWidth($a), 660 itemH = this.getHeight($a), 661 $win = $(window), 662 winX = $win.scrollLeft(), 663 winY = $win.scrollTop(), 664 winW = this.getViewportWidth(),
665 winH = this.getViewportHeight(), 666 horizontalParent = $ul.hasClass('sm') && !$ul.hasClass('sm-vertical'), 667 subOffsetX = level == 2 ? this.opts.mainMenuSubOffsetX : this.opts.subMenusSubOffsetX, 668 subOffsetY = level == 2 ? this.opts.mainMenuSubOffsetY : this.opts.subMenusSubOffsetY, 669 x, y; 670 if (horizontalParent) { 671 x = this.opts.rightToLeftSubMenus ? itemW - subW - subOffsetX : subOffsetX; 672 y = this.opts.bottomToTopSubMenus ? -subH - subOffsetY : itemH + subOffsetY; 673 } else { 674 x = this.opts.rightToLeftSubMenus ? subOffsetX - subW : itemW - subOffsetX; 675 y = this.opts.bottomToTopSubMenus ? itemH - subOffsetY - subH : subOffsetY; 676 } 677 if (this.opts.keepInViewport && !this.isCollapsible()) { 678 var absX = itemX + x, 679 absY = itemY + y; 680 if (this.opts.rightToLeftSubMenus && absX < winX) { 681 x = horizontalParent ? winX - absX + x : itemW - subOffsetX; 682 } else if (!this.opts.rightToLeftSubMenus && absX + subW > winX + winW) { 683 x = horizontalParent ? winX + winW - subW - absX + x : subOffsetX - subW; 684 } 685 if (!horizontalParent) { 686 if (subH < winH && absY + subH > winY + winH) { 687 y += winY + winH - subH - absY; 688 } else if (subH >= winH || absY < winY) { 689 y += winY - absY; 690 } 691 } 692 // do we need scrolling? 693 // 0.49 used for better precision when dealing with float values 694 if (horizontalParent && (absY + subH > winY + winH + 0.49 || absY < winY) || !horizontalParent && subH > winH + 0.49) { 695 var self = this; 696 if (!$sub.dataSM('scroll-arrows')) { 697 $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]]) 698 .bind({ 699 mouseenter: function() { 700 $sub.dataSM('scroll').up = $(this).hasClass('scroll-up'); 701 self.menuScroll($sub); 702 }, 703 mouseleave: function(e) { 704 self.menuScrollStop($sub); 705 self.menuScrollOut($sub, e); 706 }, 707 'mousewheel DOMMouseScroll': function(e) { e.preventDefault(); } 708 }) 709 .insertAfter($sub) 710 ); 711 } 712 // bind scroll events and save scroll data for this sub 713 var eNS = '.smartmenus_scroll'; 714 $sub.dataSM('scroll', { 715 step: 1, 716 // cache stuff for faster recalcs later 717 itemH: itemH, 718 subH: subH, 719 arrowDownH: this.getHeight($sub.dataSM('scroll-arrows').eq(1)) 720 }) 721 .bind(getEventsNS([ 722 ['mouseover', function(e) { self.menuScrollOver($sub, e); }], 723 ['mouseout', function(e) { self.menuScrollOut($sub, e); }], 724 ['mousewheel DOMMouseScroll', function(e) { self.menuScrollMousewheel($sub, e); }] 725 ], eNS)) 726 .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') }) 727 .eq(horizontalParent && this.opts.bottomToTopSubMenus ? 0 : 1).show(); 728 // when a menu tree is fixed positioned we allow scrolling via touch too 729 // since there is no other way to access such long sub menus if no mouse is present 730 if (this.isFixed()) { 731 $sub.css({ 'touch-action': 'none', '-ms-touch-action': 'none' }) 732 .bind(getEventsNS([ 733 [touchEvents() ? 'touchstart touchmove touchend' : 'pointerdown pointermove pointerup MSPointerDown MSPointerMove MSPointerUp', function(e) { 734 self.menuScrollTouch($sub, e); 735 }] 736 ], eNS)); 737 } 738 } 739 } 740 $sub.css({ top: 'auto', left: '0', marginLeft: x, marginTop: y - itemH }); 741 // IE iframe shim 742 this.menuIframeShim($sub); 743 if ($sub.dataSM('ie-shim')) { 744 $sub.dataSM('ie-shim').css({ zIndex: $sub.css('z-index'), width: subW, height: subH, marginLeft: x, marginTop: y - itemH }); 745 } 746 }, 747 menuScroll: function($sub, once, step) { 748 var data = $sub.dataSM('scroll'), 749 $arrows = $sub.dataSM('scroll-arrows'), 750 y = parseFloat($sub.css('margin-top')), 751 end = data.up ? data.upEnd : data.downEnd, 752 diff; 753 if (!once && data.velocity) { 754 data.velocity *= 0.9; 755 diff = data.velocity; 756 if (diff < 0.5) { 757 this.menuScrollStop($sub); 758 return; 759 } 760 } else { 761 diff = step || (once || !this.opts.scrollAccelerate ? this.opts.scrollStep : Math.floor(data.step)); 762 } 763 // hide any visible deeper level sub menus 764 var level = $sub.dataSM('level'); 765 if (this.visibleSubMenus.length > level) {
766 this.menuHideSubMenus(level - 1); 767 } 768 var newY = data.up && end <= y || !data.up && end >= y ? y : (Math.abs(end - y) > diff ? y + (data.up ? diff : -diff) : end); 769 $sub.add($sub.dataSM('ie-shim')).css('margin-top', newY); 770 // show opposite arrow if appropriate 771 if (mouse && (data.up && newY > data.downEnd || !data.up && newY < data.upEnd)) { 772 $arrows.eq(data.up ? 1 : 0).show(); 773 } 774 // if we've reached the end 775 if (newY == end) { 776 if (mouse) { 777 $arrows.eq(data.up ? 0 : 1).hide(); 778 } 779 this.menuScrollStop($sub); 780 } else if (!once) { 781 if (this.opts.scrollAccelerate && data.step < this.opts.scrollStep) { 782 data.step += 0.5; 783 } 784 var self = this; 785 this.scrollTimeout = setTimeout(function() { self.menuScroll($sub); }, this.opts.scrollInterval); 786 } 787 }, 788 menuScrollMousewheel: function($sub, e) { 789 if (this.getClosestMenu(e.target) == $sub[0]) { 790 e = e.originalEvent; 791 var up = (e.wheelDelta || -e.detail) > 0; 792 if ($sub.dataSM('scroll-arrows').eq(up ? 0 : 1).is(':visible')) { 793 $sub.dataSM('scroll').up = up; 794 this.menuScroll($sub, true); 795 } 796 } 797 e.preventDefault(); 798 }, 799 menuScrollOut: function($sub, e) { 800 if (mouse) { 801 if (!/^scroll-(up|down)/.test((e.relatedTarget || '').className) && ($sub[0] != e.relatedTarget && !$.contains($sub[0], e.relatedTarget) || this.getClosestMenu(e.relatedTarget) != $sub[0])) { 802 $sub.dataSM('scroll-arrows').css('visibility', 'hidden'); 803 } 804 } 805 }, 806 menuScrollOver: function($sub, e) { 807 if (mouse) { 808 if (!/^scroll-(up|down)/.test(e.target.className) && this.getClosestMenu(e.target) == $sub[0]) { 809 this.menuScrollRefreshData($sub); 810 var data = $sub.dataSM('scroll'); 811 $sub.dataSM('scroll-arrows').eq(0).css('margin-top', data.upEnd).end() 812 .eq(1).css('margin-top', data.downEnd + data.subH - data.arrowDownH).end() 813 .css('visibility', 'visible'); 814 } 815 } 816 }, 817 menuScrollRefreshData: function($sub) { 818 var data = $sub.dataSM('scroll'), 819 $win = $(window), 820 vportY = $win.scrollTop() - $sub.dataSM('parent-a').offset().top - data.itemH; 821 $.extend(data, { 822 upEnd: vportY, 823 downEnd: vportY + this.getViewportHeight() - data.subH 824 }); 825 }, 826 menuScrollStop: function($sub) { 827 if (this.scrollTimeout) { 828 clearTimeout(this.scrollTimeout); 829 this.scrollTimeout = 0; 830 $.extend($sub.dataSM('scroll'), { 831 step: 1, 832 velocity: 0 833 }); 834 return true; 835 } 836 }, 837 menuScrollTouch: function($sub, e) { 838 e = e.originalEvent; 839 if (isTouchEvent(e)) { 840 var touchPoint = this.getTouchPoint(e); 841 // neglect event if we touched a visible deeper level sub menu 842 if (this.getClosestMenu(touchPoint.target) == $sub[0]) { 843 var data = $sub.dataSM('scroll'); 844 if (/(start|down)$/i.test(e.type)) { 845 if (this.menuScrollStop($sub)) { 846 // if we were scrolling, just stop and don't activate any link on the first touch 847 e.preventDefault(); 848 this.isTouchScrolling = true; 849 } else { 850 this.isTouchScrolling = false; 851 } 852 // update scroll data since the user might have zoomed, etc. 853 this.menuScrollRefreshData($sub); 854 // extend it with the touch properties 855 $.extend(data, { 856 touchY: touchPoint.pageY, 857 touchTimestamp: e.timeStamp, 858 velocity: 0 859 }); 860 } else if (/move$/i.test(e.type)) { 861 var prevY = data.touchY; 862 if (prevY !== undefined && prevY != touchPoint.pageY) { 863 this.isTouchScrolling = true; 864 $.extend(data, { 865 up: prevY < touchPoint.pageY, 866 touchY: touchPoint.pageY, 867 touchTimestamp: e.timeStamp, 868 velocity: data.velocity + Math.abs(touchPoint.pageY - prevY) * 0.5 869 }); 870 this.menuScroll($sub, true, Math.abs(data.touchY - prevY)); 871 } 872 e.preventDefault(); 873 } else { // touchend/pointerup 874 if (data.touchY !== undefined) { 875 // check if we need to scroll 876 if (e.timeStamp - data.touchTimestamp < 120 && data.velocity > 0) { 877 data.velocity *= 0.5; 878 this.menuScrollStop($sub); 879 this.menuScroll($sub); 880 e.preventDefault(); 881 } 882 delete data.touchY; 883 } 884 } 885 } 886 } 887 }, 888 menuShow: function($sub) { 889 if (!$sub.dataSM('beforefirstshowfired')) { 890 $sub.dataSM('beforefirstshowfired', true); 891 if (this.$root.triggerHandler('beforefirstshow.smapi', $sub[0]) === false) { 892 return; 893 } 894 } 895 if (this.$root.triggerHandler('beforeshow.smapi', $sub[0]) === false) { 896 return; 897 } 898 this.menuFixLayout($sub); 899 $sub.stop(true, true); 900 if (!$sub.is(':visible')) { 901 // set z-index 902 $sub.css('z-index', this.zIndexInc = (this.zIndexInc || this.get
902StartZIndex()) + 1); 903 // highlight parent item 904 if (this.opts.keepHighlighted || this.isCollapsible()) { 905 $sub.dataSM('parent-a').addClass('highlighted'); 906 } 907 // min/max-width fix - no way to rely purely on CSS as all UL's are nested 908 if (this.opts.subMenusMinWidth || this.opts.subMenusMaxWidth) { 909 $sub.css({ width: 'auto', minWidth: '', maxWidth: '' }).addClass('sm-nowrap'); 910 if (this.opts.subMenusMinWidth) { 911 $sub.css('min-width', this.opts.subMenusMinWidth); 912 } 913 if (this.opts.subMenusMaxWidth) { 914 var noMaxWidth = this.getWidth($sub); 915 $sub.css('max-width', this.opts.subMenusMaxWidth); 916 if (noMaxWidth > this.getWidth($sub)) { 917 $sub.removeClass('sm-nowrap').css('width', this.opts.subMenusMaxWidth); 918 } 919 } 920 } 921 this.menuPosition($sub); 922 // insert IE iframe shim 923 if ($sub.dataSM('ie-shim')) { 924 $sub.dataSM('ie-shim').insertBefore($sub); 925 } 926 var complete = function() { 927 // fix: "overflow: hidden;" is not reset on animation complete in jQuery < 1.9.0 in Chrome when global "box-sizing: border-box;" is used 928 $sub.css('overflow', ''); 929 }; 930 // if sub is collapsible (mobile view) 931 if (this.isCollapsible()) { 932 if (this.opts.collapsibleShowFunction) { 933 this.opts.collapsibleShowFunction.call(this, $sub, complete); 934 } else { 935 $sub.show(this.opts.collapsibleShowDuration, complete); 936 } 937 } else { 938 if (this.opts.showFunction) { 939 this.opts.showFunction.call(this, $sub, complete); 940 } else { 941 $sub.show(this.opts.showDuration, complete); 942 } 943 } 944 // save new sub menu for this level 945 this.visibleSubMenus[$sub.dataSM('level') - 1] = $sub; 946 this.$root.triggerHandler('show.smapi', $sub[0]); 947 } 948 }, 949 popupHide: function(noHideTimeout) { 950 if (this.hideTimeout) { 951 clearTimeout(this.hideTimeout); 952 this.hideTimeout = 0; 953 } 954 var self = this; 955 this.hideTimeout = setTimeout(function() { 956 self.menuHideAll(); 957 }, noHideTimeout ? 1 : this.opts.hideTimeout); 958 }, 959 popupShow: function(left, top) { 960 if (!this.opts.isPopup) { 961 alert('SmartMenus jQuery Error:\n\nIf you want to show this menu via the "popupShow" method, set the isPopup:true option.'); 962 return; 963 } 964 if (this.hideTimeout) { 965 clearTimeout(this.hideTimeout); 966 this.hideTimeout = 0; 967 } 968 this.menuFixLayout(this.$root); 969 this.$root.stop(true, true); 970 if (!this.$root.is(':visible')) { 971 this.$root.css({ left: left, top: top }); 972 // IE iframe shim 973 this.menuIframeShim(this.$root); 974 if (this.$root.dataSM('ie-shim')) { 975 this.$root.dataSM('ie-shim').css({ zIndex: this.$root.css('z-index'), width: this.getWidth(this.$root), height: this.getHeight(this.$root), left: left, top: top }).insertBefore(this.$root); 976 } 977 // show menu 978 var self = this, 979 complete = function() { 980 self.$root.css('overflow', ''); 981 }; 982 if (this.opts.showFunction) { 983 this.opts.showFunction.call(this, this.$root, complete); 984 } else { 985 this.$root.show(this.opts.showDuration, complete); 986 } 987 this.visibleSubMenus[0] = this.$root; 988 } 989 }, 990 refresh: function() { 991 this.menuHideAll(); 992 this.$root.find('ul').each(function() { 993 var $this = $(this); 994 if ($this.dataSM('scroll-arrows')) { 995 $this.dataSM('scroll-arrows').remove(); 996 } 997 }) 998 .removeDataSM('in-mega') 999 .removeDataSM('shown-before') 1000 .removeDataSM('ie-shim') 1001 .removeDataSM('scroll-arrows') 1002 .removeDataSM('parent-a') 1003 .removeDataSM('level') 1004 .removeDataSM('beforefirstshowfired'); 1005 this.$root.find('a.has-submenu').removeClass('has-submenu') 1006 .parent().removeDataSM('sub'); 1007 if (this.opts.subIndicators) { 1008 this.$root.find('span.sub-arrow').remove(); 1009 } 1010 if (this.opts.markCurrentItem) { 1011 this.$root.find('a.current').removeClass('current'); 1012 } 1013 this.subMenus = []; 1014 this.init(true); 1015 }, 1016 rootOut: function(e) { 1017 if (!this.handleEvents() || this.isTouchMode() || e.target == this.$root[0]) { 1018 return; 1019 } 1020 if (this.hideTimeout) { 1021 clearTimeout(this.hideTimeout); 1022 this.hideTimeout = 0; 1023 } 1024 if (!this.opts.showOnClick || !this.opts.hideOnClick) { 1025 var self = this; 1026 this.hideTimeout = setTimeout(function() { self.menuHideAll(); }, this.opts.hideTimeout); 1027 } 1028 }, 1029 rootOver: function(e) { 1030 if (!this.handleEvents() || this.isTouchMode() || e.target == this.$root[0]) { 1031 return; 1032 } 1033 if (this.hideTimeout) { 1034 clearTimeout(this.hideTimeout); 1035 this.hideTimeout = 0; 1036 } 1037 }, 1038 winResize: function(e) { 1039 if (!this.handleEvents()) { 1040 // we still need to resize the disable overlay if it's visible 1041 if (this.$disableOverlay) { 1042 var pos = this.$root.offset(); 1043 this.$disableOverlay.css({ 1044 top: pos.top, 1045 left: pos.left, 1046 width: this.$root.outerWidth(), 1047 height: this.$root.outerHeight() 1048 }); 1049 } 1050 return; 1051 } 1052 // hide sub menus on resize - on mobile do it only on orientation change 1053 if (!this.isCollapsible() && (!('onorientationchange' in window) || e.type == 'orientationchange')) { 1054 if (this.activatedItems.length) { 1055 this.activatedItems[this.activatedItems.length - 1][0].blur(); 1056 } 1057 this.menuHideAll(); 1058 } 1059 } 1060 } 1061 }); 1062 1063 $.fn.dataSM = function(key, val) { 1064 if (val) { 1065 return this.data(key + '_smartmenus', val); 1066 } 1067 return this.data(key + '_smartmenus'); 1068 } 1069 1070 $.fn.removeDataSM = function(key) { 1071 return this.removeData(key + '_smartmenus'); 1072 } 1073 1074 $.fn.smartmenus = function(options) { 1075 if (typeof options == 'string') { 1076 var args = arguments, 1077 method = options; 1078 Array.prototype.shift.call(args); 1079 return this.each(function() { 1080 var smartmenus = $(this).data('smartmenus'); 1081 if (smartmenus && smartmenus[method]) { 1082 smartmenus[method].apply(smartmenus, args); 1083 } 1084 }); 1085 } 1086 var opts = $.extend({}, $.fn.smartmenus.defaults, options); 1087 return this.each(function() { 1088 new $.SmartMenus(this, opts); 1089 }); 1090 } 1091 1092 // default settings 1093 $.fn.smartmenus.defaults = { 1094 isPopup: false, // is this a popup menu (can be shown via the popupS
1094how/popupHide methods) or a permanent menu bar 1095 mainMenuSubOffsetX: 0, // pixels offset from default position 1096 mainMenuSubOffsetY: 0, // pixels offset from default position 1097 subMenusSubOffsetX: 0, // pixels offset from default position 1098 subMenusSubOffsetY: 0, // pixels offset from default position 1099 subMenusMinWidth: '200px', // min-width for the sub menus (any CSS unit) - if set, the fixed width set in CSS will be ignored 1100 subMenusMaxWidth: '400px', // max-width for the sub menus (any CSS unit) - if set, the fixed width set in CSS will be ignored 1101 subIndicators: true, // create sub menu indicators - creates a SPAN and inserts it in the A 1102 subIndicatorsPos: 'prepend', // position of the SPAN relative to the menu item content ('prepend', 'append') 1103 subIndicatorsText: '+', // [optionally] add text in the SPAN (e.g. '+') (you may want to check the CSS for the sub indicators too) 1104 scrollStep: 0, // pixels step when scrolling long sub menus that do not fit in the viewport height 1105 scrollInterval: 0, // interval between each scrolling step 1106 scrollAccelerate: false, // accelerate scrolling or use a fixed step 1107 showTimeout: 0, // timeout before showing the sub menus 1108 hideTimeout: 0, // timeout before hiding the sub menus 1109 showDuration: 300, // duration for show animation - set to 0 for no animation - matters only if showFunction:null 1110 showFunction: null, // custom function to use when showing a sub menu (the default is the jQuery 'show') 1111 // don't forget to call complete() at the end of whatever you do 1112 // e.g.: function($ul, complete) { $ul.fadeIn(250, complete); } 1113 hideDuration: 0, // duration for hide animation - set to 0 for no animation - matters only if hideFunction:null 1114 hideFunction: function($ul, complete) { $ul.fadeOut(200, complete); }, // custom function to use when hiding a sub menu (the default is the jQuery 'hide') 1115 // don't forget to call complete() at the end of whatever you do 1116 // e.g.: function($ul, complete) { $ul.fadeOut(250, complete); } 1117 collapsibleShowDuration:0, // duration for show animation for collapsible sub menus - matters only if collapsibleShowFunction:null 1118 collapsibleShowFunction:function($ul, complete) { $ul.slideDown(200, complete); }, // custom function to use when showing a collapsible sub menu 1119 // (i.e. when mobile styles are used to make the sub menus collapsible) 1120 collapsibleHideDuration:0, // duration for hide animation for collapsible sub menus - matters only if collapsibleHideFunction:null 1121 collapsibleHideFunction:function($ul, complete) { $ul.slideUp(200, complete); }, // custom function to use when hiding a collapsible sub menu 1122 // (i.e. when mobile styles are used to make the sub menus collapsible) 1123 showOnClick: false, // show the first-level sub menus onclick instead of onmouseover (matters only for mouse input) 1124 hideOnClick: true, // hide the sub menus on click/tap anywhere on the page 1125 keepInViewport: true, // reposition the sub menus if needed to make sure they always appear inside the viewport 1126 keepHighlighted: true, // keep all ancestor items of the current sub menu highlighted (adds the 'highlighted' class to the A's) 1127 markCurrentItem: false, // automatically add the 'current' class to the A element of the item linking to the current URL 1128 markCurrentTree: true, // add the 'current' class also to the A elements of all ancestor items of the current item 1129 rightToLeftSubMenus: false, // right to left display of the sub menus (check the CSS for the sub indicators' position) 1130 bottomToTopSubMenus: false, // bottom to top display of the sub menus 1131 overlapControlsInIE: true // make sure sub menus appear on top of special OS controls in IE (i.e. SELECT, OBJECT, EMBED, etc.) 1132 }; 1133 1134})(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.