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