PageSourceSearch

https://vra.com/src/js/hover-dropdown-menu.js

js vra.com collected 2026-10-02 07:42:58 UTC 41,623 bytes, 1,134 lines download raw bytes

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.