PageSourceSearch

https://cityofbrandon.org/repository/designs/parts/scripts/menunav_resp.js

js cityofbrandon.org collected 2026-10-05 05:53:51 UTC 31,438 bytes, 900 lines download raw bytes

1// global pause variable
2window.menusPaused = false;
3
4// menu nav controller object
5function MenuNav(nav, options) {
6
7	// set up the object
8	var self = this;
9	var $nav = $(nav);
10	$nav[0].controller = this;
11	var lastOver = null;
12	var $otherMenuNavs = $(null);
13
14	// information about self
15	var isRightAligned = $nav.is(".horizontal.rightAligned");
16	var isHorizontal = $nav.is(".horizontal");
17	var isFullWidth = $nav.is(".horizontal.block.fullWidth");
18	var isSlab = $nav.is(".horizontal.block.slab");
19	var isSlabText = $nav.is(".horizontal.block.slab.slabtext") && $("nav.horizontal.block.slab.slabtext > div").is(".slabtext");
20	var isSlabMaxTwo = $nav.is(".horizontal.block.slab.slabmaxtwocol");
21	var isSlabMaxThree = $nav.is(".horizontal.block.slab.slabmaxthreecol");
22	var isOneDeep = $nav.is(".horizontal.block.slab.onedeep");
23	var isClean = $nav.is(".horizontal.block.clean");
24
25	this.$menus = $nav.find("ul ul");
26
27	this.options = $.extend({
28		foo: "bar",
29		leftOverlap: 5,
30		topOverlap: 5,
31		bounceOffset: 25,
32		fadeOutDuration: 400
33	}, options || {});
34
35	function initialize() {
36
37		// Add in the triangles
38		if (isClean)
39			createTrianglesClean();
40		else
41			createTriangles();
42
43		if (isSlab) {
44			buildSlab();
45		}
46		// find other menu navs to talk to
47		$("nav.menu").each(function () {
48			if (this !== $nav[0]) {
49				$otherMenuNavs = $otherMenuNavs.add(this);
50			}
51		});
52
53		// event delegates for all menus
54		$nav.on("mouseenter", "ul li", onMouseEnter)
55			.on("mouseleave", "ul li", onMouseLeave)
56			// .on("blur", "ul li", onMouseLeave)
57			.on("click", "ul li .triangle", arrowClick)
58			.on("keydown", "ul li .triangle", arrowClick)
59			.on("click", "ul li a", onLinkClicked)
60			.on("touchstart", "ul li a", onLinkTouchStart)
61			.on("touchmove", "ul li a", onLinkTouchMove)
62			.on("touchend", "ul li a", onLinkTouchEnd);
63	}
64
65	function buildSlab() {
66		var $topLevelSectionsWithChildren = $nav.children("ul").children("li").has("ul");
67		$topLevelSectionsWithChildren.each(function (i) {
68			var numOfCat = $(this).children("ul").children("li").length;
69			//remove the deep links - they won't be shown, and they just mess up the counts
70			$(this).children("ul").children("li").children("ul").children("li").children("ul").remove();
71			if (isOneDeep)
72				$(this).children("ul").children("li").children("ul").remove();
73			var numOfLinks = numOfCat + $(this).children("ul").children("li").children("ul").children("li").length;
74			if ((numOfCat >= 12 || numOfLinks >= 40 && numOfCat >= 4) && !isSlabMaxTwo && !isSlabMaxThree) {
75				$(this).addClass("fourcol");
76				var forth = parseInt(numOfLinks / 4);
77				var cut = 0;
78				var $links = $(this).children("ul").children("li").find("a").slice(forth, forth + 1);
79				if ($links.parentsUntil("nav").length === 6)
80					cut = $links.parent().parent().parent().index();
81				else if ($links.parentsUntil("nav").length === 4)
82					cut = $links.parent().index();
83				if (numOfCat > 5 && cut < 3) cut++;
84				if (cut == 0) cut = 1;
85				$(this).children("ul").children("li").slice(0, cut).wrapAll("<li class='col one'><ul>");
86				var $links = $(this).children("ul").children("li").find("a").slice(forth * 2, forth * 2 + 1);
87				if ($links.parentsUntil("nav").length === 6)
88					cut = $links.parent().parent().parent().index() + 1;
89				else if ($links.parentsUntil("nav").length === 4)
90					cut = $links.parent().index() + 1;
91				$(this).children("ul").children("li").slice(1, cut).wrapAll("<li class='col two'><ul>");
92				var $links = $(this).children("ul").children("li").find("a").slice(forth * 3, forth * 3 + 1);
93				if ($links.parentsUntil("nav").length === 6)
94					cut = $links.parent().parent().parent().index() + 1;
95				else if ($links.parentsUntil("nav").length === 4)
96					cut = $links.parent().index() + 1;
97				$(this).children("ul").children("li").slice(2, cut).wrapAll("<li class='col three'><ul>");
98				$(this).children("ul").children("li").slice(3).wrapAll("<li class='col four'><ul>");
99			}
100			else if ((numOfCat >= 9 || numOfLinks >= 30 && numOfCat >= 3) && !isSlabMaxTwo) {
101				$(this).addClass("threecol");
102				var third = parseInt(numOfLinks / 3);
103				var cut = 0;
104				var $links = $(this).children("ul").children("li").find("a").slice(third, third + 1);
105				if ($links.parentsUntil("nav").length === 6)
106					cut = $links.parent().parent().parent().index();
107				else if ($links.parentsUntil("nav").length === 4)
108					cut = $links.parent().index();
109				if (numOfCat >= 5 && cut < 2) cut++;
110				if (cut == 0) cut = 1;
111				//console.log(cut);
112				$(this).children("ul").children("li").slice(0, cut).wrapAll("<li class='col one'><ul>");
113				var $links = $(this).children("ul").children("li").find("a").slice(third*2,third*2+1);
114				if ($links.parentsUntil("nav").length === 6)
115					cut = $links.parent().parent().parent().index() + 1;
116				else if ($links.parentsUntil("nav").length === 4)
117					cut = $links.parent().index() + 1;
118				$(this).children("ul").children("li").slice(1, cut).wrapAll("<li class='col two'><ul>");
119				$(this).children("ul").children("li").slice(2).wrapAll("<li class='col three'><ul>");
120			}
121			else if (numOfCat >= 2 && numOfLinks >= 10) {
122				$(this).addClass("twocol");
123				var half = parseInt(numOfLinks / 2);
124				var cut = 0;
125				//find the middle
126				var $links = $(this).children("ul").children("li").find("a").slice(half, half+1);
127				if ($links.parentsUntil("nav").length === 6)
128					cut = $links.parent().parent().parent().index();
129				else if ($links.parentsUntil("nav").length === 4)
130					cut = $links.parent().index();
131				if (cut == 0) cut = 1;
132				$(this).children("ul").children("li").slice(0, cut).wrapAll("<li class='col one'><ul>");
133				$(this).children("ul").children("li").slice(1).wrapAll("<li class='col two'><ul>");
134			}
135			else {
136				$(this).addClass("onecol");
137				$(this).children("ul").children("li").slice(0).wrapAll("<li class='col one'><ul>");
138			}
139		});
140		if (isSlabText) {
141			$('div.slabtext').removeClass('hidden').detach().appendTo('nav.horizontal.block.slab.slabtext > ul > li > ul').wrap("<li class='slabtext'>");
142		}
143		else
144		{
145			$nav.removeClass('slabtext');
146		}
147	}
148
149	function createTriangles() {
150		if (isHorizontal) {
151			var $topLevelSectionsWithChildren = $nav.children("ul").children("li").has("ul");
152			$topLevelSectionsWithChildren.children("a").each(function (index, value) {
153				$(value).attr('aria-haspopup', 'true').attr('aria-expanded', 'false')
154				.append($("<button class='triangle' tabindex='0'><span class='hidden'>show submenu for " + $(value).text() + "</span>&#9660;</button>"));
155			});
156			if (!$nav.is(".slab")) {
157				// add triangles to subsections
158				$topLevelSectionsWithChildren.find("li").has("ul").children("a").each(function (index, value) {
159					$(value).attr('aria-haspopup', 'true').attr('aria-expanded', 'false')
160						.prepend($("<button class='triangle' tabindex='0'><span class='hidden'>show submenu for " + $(value).text() + "</span>&#9658;</button>"));
161				});
162			}
163		} else {
164			$nav.find("li").has("ul").children("a").each(function (index, value) {
165				$(value).attr('aria-haspopup', 'true').attr('aria-expanded', 'false')
166					.prepend($("<button class='triangle' tabindex='0'><span class='hidden'>show submenu for " + $(value).text() + "</span>&#9658;</button>"));
167			});
168		}
169	}
170
171	function createTrianglesClean() {
172		if (isHorizontal) {
173			var $topLevelSectionsWithChildren = $nav.children("ul").children("li").has("ul");
174			$topLevelSectionsWithChildren.children("a").each(function (index, value) {
175				let $childmenuid = $(value).next('ul').attr('id');
176				$(value).after($("<button class='triangle' aria-expanded='false' aria-controls='" + $childmenuid + "' aria-label='show submenu for " + $(value).text() + "'>&#9660;</button>"));
177			});
178			if (!$nav.is(".slab")) {
179				//let $childmenuid = $(value).next('ul').attr('id');
180				$topLevelSectionsWithChildren.find("li").has("ul").children("a").each(function (index, value) {
181					$(value).after($("<button class='triangle' aria-label='show submenu for " + $(value).text() + "'>&#9658;</button>"));
182				});
183			}
184		} else {
185			//let $childmenuid = $(value).next('ul').attr('id');
186			$nav.find("li").has("ul").children("a").each(function (index, value) {
187				$(value).after($("<button class='triangle' aria-label='show submenu for " + $(value).text() + "'>&#9658;</button>"));
188			});
189		}
190	}
191
192	//  event handlers
193	function onMouseEnter(event) {
194		var $thisLI = $(this);	// the element the event landed on
195		if (hasNoMenu($thisLI)) { return; } // we have no business here
196		var $menuUL = $thisLI.children("ul");
197		var $openSiblings = self.getOpenSiblingMenus($menuUL);
198
199		// we're over a menu title, open the menu!
200		if (!$thisLI.parent().parent().is("nav.horizontal.block") && $openSiblings.length) {
201			if (isInRightBit(event, $thisLI) && fromAdjacent(this)) {
202				// we should delay this open a bit to see if user was
203				// aiming for an already open menu and is here by accident
204				self.dwelledOpenTimer = setTimeout(function () {
205					self.openMenu($menuUL);
206				}, 500);
207			} else { // nah, just open the menu right away
208				if (self.dwelledOpenTimer) {
209					clearTimeout(self.dwelledOpenTimer);
210				}
211				self.openMenu($menuUL);
212			}
213		} else { // no other sibling menus open, all clear
214			if (isSlab)
215				self.dwelledOpenTimer = setTimeout(function () { self.openMenu($menuUL); }, 400);
216			else
217				self.openMenu($menuUL);
218		}
219		lastOver = this;
220	}
221	function arrowClick(event) {
222		if (event.keyCode === 13 || event.type === 'click') {
223			var $thisLI = $(this).parent().parent();	// the element the event landed on
224			if (hasNoMenu($thisLI)) { $thisLI = $(this).parent(); } // maybe new clean version
225			if (hasNoMenu($thisLI)) { return; } // we have no business here
226			var $menuUL = $thisLI.children("ul");
227			var $openSiblings = self.getOpenSiblingMenus($menuUL);
228
229			if ($menuUL.hasClass('arrowclicked')) {
230				$menuUL.removeClass('arrowclicked');
231				// clearTimeout(self.dwelledOpenTimer);
232				self.closeMenu($menuUL);
233			}
234			else {
235				// we've clicked an arrow open the menu
236				if (!$thisLI.parent().parent().is("nav.horizontal.block") && $openSiblings.length) {
237					if (self.dwelledOpenTimer) { clearTimeout(self.dwelledOpenTimer); }
238					self.openMenu($menuUL);
239				} else { // no other sibling menus open, all clear
240					self.openMenu($menuUL);
241				}
242				$menuUL.addClass('arrowclicked');
243			}
244			event.stopPropagation();
245			event.preventDefault();
246			return false;
247		}
248	}
249
250	function onMouseLeave() {
251		var $this = $(this);	// the element the event landed on
252		var $menu = $this.children("ul");
253		if (hasNoMenu($this)) { return; } // we have no business here
254		if ($menu.hasClass('arrowclicked')) { return; }
255
256		// if we leave the menu item's LI (which includes all its sub menus), we need to set a close timer
257		this.closeTimer = setTimeout(function () {
258			self.closeMenu($menu);
259		}, 1000);
260
261		// if this is a dwelled-upon rightmost third (or whatever) of an item, and cursor leaves it, we need to cancel the delayed open timer
262		if (self.dwelledOpenTimer) {
263			clearTimeout(self.dwelledOpenTimer);
264		}
265	}
266
267	function onLinkClicked() {
268		// console.log("onLinkClicked");
269	}
270
271	function onLinkTouchStart(event) {
272		var firstTouch = event.originalEvent.touches[0];
273		// console.log("onLinkTouchStart");
274
275		// keep track of the movement of this touch.
276		// SWIPE HANDLING DEPRECATED in favor of the global swipe tracking
277
278		self.swipeDistance = 0;
279		self.swipeOrigin = {
280			x: firstTouch.pageX,
281			y: firstTouch.pageY
282		};
283
284		event.preventDefault();
285		event.stopPropagation();
286	}
287
288	function onLinkTouchMove(event) {
289		// SWIPE HANDLING DEPRECATED in favor of the global swipe tracking
290		var firstTouch = event.originalEvent.touches[0];
291		var position = {
292			x: firstTouch.pageX,
293			y: firstTouch.pageY
294		};
295		self.swipeDistance = calculateSwipeDistance(position);
296
297		// console.log("onLinkTouchMove");
298		// console.log("swipeDistance = ", self.swipeDistance);
299
300		// call preventDefault(), or else a mouseenter event will be synthesized
301		event.preventDefault();
302		event.stopPropagation();
303	}
304
305	function onLinkTouchEnd(event) {
306		// console.log("onLinkTouchEnd");
307
308		self.swipeDistance = 0;
309		self.swipeOrigin = null;
310
311		var $this = $(this),
312			$li = $this.parent(),
313			$menu = $li.children("ul");
314
315		if (wasATap()) {
316			// console.log("that was a tap.");
317		}
318
319		// alert("touched");
320		// if there's no menu under this, we want it to act like a normal link.
321		// if it has an already open menu, we want this second tap to be treated
322		// as an attempt to navigate to the menu title's section.
323		if (hasNoMenu($li) || $menu.is(".open")) {
324			// go to the link
325			// console.log("navigating to link...");
326			window.location.href = $this.attr("href");
327		} else {
328			// console.log("opening menu.");
329			self.openMenu($menu);
330		}
331
332		// call event.preventDefault(), or else a click event will be synthesized
333		event.preventDefault();
334		event.stopPropagation();
335	}
336
337	// event handler utilities
338	function wasATap() {
339		return (window.globalMenuNavController.swipeState === null ||
340				(window.globalMenuNavController.swipeState &&
341				 window.globalMenuNavController.swipeState.isARealSwipe === false));
342	}
343
344	function isInRightBit(event, $e) {
345		var width = $e.outerWidth();
346		var itemLeft = $e.offset().left;
347		var bit = (itemLeft + ((8 / 10) * width));
348
349		return (event.pageX > bit);
350	}
351	// returns true if the mouse arrived at "item" from an adjacent
352	// menu title note: does NOT include those without any children
353	// (and therefore a menu)
354	function fromAdjacent(item) {
355		var $neighbors = $(item).prev().add($(item).next());
356		if ($neighbors.is($(lastOver))) {
357			return true;
358		} else {
359			return false;
360		}
361	}
362
363	//  actions
364	this.openMenu = function openMenu(menu) {
365		if (window.menusPaused) { return; }
366
367		var $menu = $(menu);
368		var $title = $menu.parent();
369
370		// clear close timer and/or cancel animations
371		if ($title[0].closeTimer) { clearTimeout($title[0].closeTimer); }
372
373		if ($menu.is(".open")) {
374			// the menu is still technically open, we just need to
375			// stop it from animating closed if that's what it's doing
376			$title.add($menu).removeClass("closing");
377			$menu.stop().css("opacity", "");
378		} else {
379			// if we're in a deep subsection somewhere, and we're trying
380			// to open the menu of its top-level ancestor, that top-level
381			// section will (and should) be marked "open", so we need to
382			// remember that so we don't blow that marker away once we
383			// close the menu
384			// if ($title.is(".open"))
385			// 	$title.data("wasOpen", true);
386
387			self.closeOtherMenuNavs();
388
389			// this is necessary to get IE6/7 to play nice
390			$menu.css({ visibility: "hidden" });
391
392			// this seems verbose, but in profiling, this was the
393			// fastest way to do it
394			$title.addClass("open");
395			$menu.addClass("open");
396			$nav.addClass("open");
397			//$title.children('a').attr('aria-expanded', 'true');
398			$title.children('button').attr('aria-expanded', 'true');
399
400			if (!isSlab)
401				positionMenu($menu);
402
403			// self.raiseBox();
404			$menu.css({ visibility: "visible" });
405			self.closeSiblingMenus($menu);
406
407			if (isSlab) {
408				var highestCol = 0;
409				if ($menu.children('li.col.three').length)
410					highestCol = Math.max(Math.max($menu.children('li.col.one').height(),$menu.children('li.col.two').height()), $menu.children('li.col.three').height());
411				else if ($menu.children('li.col.two').length)
412					highestCol = Math.max($menu.children('li.col.one').height(),$menu.children('li.col.two').height());
413				else
414					highestCol = $menu.children('li.col.one').height();
415				if (isSlabText)
416					highestCol = Math.max(highestCol, $menu.children('li.slabtext').height());
417				if (highestCol > 101)
418					$menu.children('li.col').height(highestCol);
419			}
420		}
421	};
422
423	this.closeMenu = function closeMenu(menu, closeImmediately) {
424		var $menu = $(menu);
425		if ($menu.length === 0 || window.menusPaused) { return; }
426		var $title = $(menu).parent();
427
428		function closeNow() {
429			// clear any close timer still pending, we're going to close right
430			// away instead
431			if ($title[0].closeTimer) { clearTimeout($title[0].closeTimer); }
432
433			// halt animations on descendants and self
434			// $menu.find("ul").andSelf().stop();
435			$menu.find("ul").addBack().stop();
436
437			// $title.removeClass("closing" + ($title.data("wasOpen") ? "" : " open"));
438			$title.removeClass("closing open");
439			$menu.removeClass("closing open");
440			//$title.children('a').attr('aria-expanded', 'false');
441			$title.children('button').attr('aria-expanded', 'false');
442
443			if ($nav.find("ul.open").length === 0) {
444				$nav.removeClass("open");
445				// self.lowerBox();
446			}
447
448			// if it was in the middle of an animation before we
449			// pulled the plug, reset it
450			$menu.css({opacity: ""});
451
452			// clean up any stray descendant menus
453			closeDescendantMenus(menu);
454		}
455
456		if (closeImmediately || $title.siblings(".open").length) {
457			closeNow();
458		} else {
459			$menu.add($menu.parent()).addClass("closing");
460			$menu.animate({opacity: 0}, self.options.fadeOutDuration, function () {
461				closeNow();
462			});
463		}
464	};
465	this.toggleMenu = function toggleMenu(menu) {
466		var $menu = $(menu);
467		// UNFINISHED
468	};
469
470	this.closeAllMenus = function closeAllMenus() {
471		self.$menus.filter(".open").each(function (i, menu) {
472			self.closeMenu(menu, true);
473		});
474	};
475
476	function closeDescendantMenus(menu) {
477		var $menu = $(menu);
478		var $openChildren = $menu.children("li.open").children("ul");
479		if ($openChildren.length) {
480			$openChildren.each(function () {
481				self.closeMenu(this, true);
482			});
483		}
484	}
485
486	// this should get moved into the global menu nav controller
487	this.closeOtherMenuNavs = function closeOtherMenuNavs() {
488		$otherMenuNavs.filter(".open").each(function () {
489			this.controller.closeAllMenus();
490		});
491	};
492	this.closeSiblingMenus = function closeSiblingMenus(menu) {
493		var $title = $(menu).parent();
494		$title.siblings(".open").children("ul").each(function () {
495			self.closeMenu(this, true);
496		});
497	};
498
499	//  accessors
500	this.getSiblingMenus = function getSiblingMenus(menuUL) {
501		return $(menuUL).parent().siblings("li").children("ul");
502	};
503	this.getOpenSiblingMenus = function getOpenSiblingMenus(menuUL) {
504		return this.getSiblingMenus(menuUL).filter(".open");
505	};
506	this.getOrientation = function getOrientation() {
507		return $nav.hasClass("vertical") ? "vertical" : "horizontal";
508	};
509	this.isSlab = function isSlab() {
510		return $nav.hasClass("slab");
511	};
512	this.isSub = function isSub(menuUL) {
513		var $parentSections = $(menuUL).parents("nav li");
514		var depth = $parentSections.length - 1;
515		return depth;
516	};
517	// function getMenuTitle(menuUL) {
518	// 	return $(menuUL).parent().children("a").text();
519	// }
520	// takes an section's LI. returns true if the section has children
521	function hasNoMenu(li) {
522		var $menu = $(li).children("ul");
523		return ($menu.length === 0 || (self.isSlab() && sectionLevel(li) > 0));
524	}
525	// returns the apparent "level" of a section. "home" and children of "home"
526	// are level 0, the children of those are level 1, etc.
527	function sectionLevel(li) {
528		return $(li).parentsUntil("nav", "li").length;
529	}
530	// returns an integer. takes a current position object to compare against
531	// the last recorded swipe origin position
532	function calculateSwipeDistance(currentPosition) {
533		var delta = lineLength(currentPosition.x, currentPosition.y,
534							   self.swipeOrigin.x, self.swipeOrigin.y);
535		return Math.round(Math.abs(delta));
536	}
537
538	// ===================================================
539	//  internal positioning and utility functions
540	// ===================================================
541	// takes a menu (ul), and a positionInfo hash
542	function calcLeft(menu, i) {
543		var $menu = $(menu);
544
545		if ($menu.parents(".bounced").length) {
546			return -i.menu.outerWidth + self.options.leftOverlap;
547		} else {
548			if (self.getOrientation() === "vertical" || i.menu.isSub) {
549				return i.parent.outerWidth - self.options.leftOverlap;
550			} else { // horizontal
551				if (isFullWidth && $("html").is(".mozilla-cell-context-bug")) {
552					// work around FF's lack of position: relative on
553					// display:table-cell elements
554					return i.parent.position.left + i.navLink.position.left;
555				} else if (isSlab) {
556					return 0;
557				} else if (isRightAligned) {
558					// console.log(i.navLink.position.left);
559					return i.navLink.position.left + (i.navLink.outerWidth - i.menu.outerWidth);
560				}
561				else {
562					return i.navLink.position.left;
563				}
564				// return 0;
565			}
566		}
567	}
568
569	// takes a menu (ul), and a positionInfo hash
570	function calcTop(menu, i) {
571		var $menu = $(menu);
572		var topBorder = parseInt($menu.css("border-top-width"), 10);
573		var paddingTop = parseInt($menu.css("padding-top"), 10);
574		if (isNaN(topBorder)) { topBorder = 0; }
575		if (isNaN(paddingTop)) { paddingTop = 0; }
576		var topOverlap = (topBorder + paddingTop);
577		// var topOverlap = self.options.topOverlap;
578		var top;
579
580		if (self.getOrientation() === "vertical" || i.menu.isSub) {
581			top = i.navLink.position.top - topOverlap;
582		} else { // horizontal
583			top = i.navLink.outerHeight + i.navLink.position.top;
584		}
585
586		return top;
587	}
588
589	// FIXME: this overreacts to tall root horizontal menus in short windows, like with open debuggers
590	// takes a menu (ul), and a positionInfo hash
591	function bounceBottom($menu, i) {
592		// do the bottom bounce
593		var bottomOverlap = (i.menu.offset.top + i.menu.outerHeight) - (i.window.height + i.window.scrollTop);
594		if (bottomOverlap > 0) {
595			var bouncedMenuTop = i.menu.position.top - bottomOverlap;
596
597			// we don't bounce if this is a top-level horizontal menu
598			if (isHorizontal && !self.isSub($menu[0])) { return; }
599
600			// if the bottom bounce puts it beyond the top of the window, then just start at the top of the window.
601			//if (bouncedMenuTop < 0) bouncedMenuTop = (i.window.scrollTop - i.menu.offset.top);
602
603			// keep accounting up to date
604			i.menu.position.top = bouncedMenuTop;
605
606			// move the menu
607			$menu.css({ top: i.menu.position.top + "px" });
608		}
609	}
610
611	// takes a menu (ul), and a positionInfo hash
612	function bounceTop($menu, i) {
613		// do the top bounce
614		var topOverlap = i.window.scrollTop - i.menu.offset.top;
615		// we don't bounce if this is a top-level horizontal menu
616		if (isHorizontal && !self.isSub($menu[0])) { return; }
617
618		console.log(i.menu.position.top);
619		if (topOverlap > 0) {
620			// keep accounting up to date
621			i.menu.position.top = i.menu.position.top + topOverlap;
622			$menu.css({ top: i.menu.position.top + "px" });
623		}
624		console.log(i.menu.position.top);
625		if (i.menu.position.top < topOverlap) {
626			i.menu.position.top = topOverlap;
627			$menu.css({ top: i.menu.position.top + "px" });
628		}
629		console.log(i.menu.position.top);
630
631	}
632
633	// takes a menu (ul), and a positionInfo hash
634	function bounceRight($menu, i) {
635		// do the right edge bounce -- this one is more involved
636		var rightOverlap =  i.window.width - (i.menu.offset.left + i.menu.outerWidth);
637		if (rightOverlap < 0) {
638			var bouncedMenuLeft = i.menu.position.left + rightOverlap;
639			//var bouncedMenuLeft = i.menu.position.left - (2 * i.menu.outerWidth) + (2 * self.options.leftOverlap);
640
641			// if this is a 1st level subsection's menu in a
642			// horizontal nav, and we need to bounce, we should just
643			// go left instead of doing a naive bounce, which will
644			// obscure the subsection's title with the new submenu
645			if (isHorizontal && i.menu.isSub === 1) {
646				bouncedMenuLeft = -i.menu.outerWidth + self.options.leftOverlap;
647			}
648
649			$menu.css({ left: bouncedMenuLeft + "px" });
650
651			// keep our accounting up to date
652			i.menu.position.left = bouncedMenuLeft;
653			$menu.addClass("bounced");
654
655			// if this is a second sub-menu, offset it vertically a bit
656			if (i.menu.isSub > 1) {
657				// keep our accounting up to date
658				i.menu.position.top = i.menu.position.top + self.options.bounceOffset;
659				$menu.css({ top: i.menu.position.top + "px" });
660			}
661		}
662		return false;
663	}
664
665	function matchTitleWidth($menu, i) {
666		if (!isHorizontal) { return; }
667
668		var titleWidth = $menu.closest("li").outerWidth();
669		if (titleWidth > i.menu.outerWidth) {
670			$menu.css({ width: titleWidth + 20 + "px" });
671			// keep our accounting of the width up to date
672			i.menu.outerWidth = $menu.outerWidth();
673		}
674	}
675
676	function positionMenu(menu) {
677		// menu must not be display:none when we get here
678		var $menu = $(menu);
679		var $navLink = $menu.parent().children("a");
680
681		// EXPERIMENTAL STUFF, commented out for now
682		// if ($menu[0].positioned) return;
683
684		// var originalStyle = {
685		// visibility: $menu.css("visibility"),
686		// display: $menu.css("display")
687		// };
688		// $menu.css({visibility:"hidden", display:"block"});
689
690		// capture all relevant position data, minus the calculated
691		// position of the menu
692		var positionInfo = {
693			menu: {
694				dom: $menu,
695				outerHeight: $menu.outerHeight(),
696				outerWidth: $menu.outerWidth(),
697				// offset: $menu.offset(),
698				// position: $menu.position(),
699				offset: null,
700				position: null,
701				isSub: self.isSub($menu),
702				$navLink: $navLink
703			},
704			parent: {
705				outerWidth: $menu.parent().outerWidth(),
706				outerHeight: $menu.parent().outerHeight(),
707				position: $menu.parent().position()
708			},
709			navLink: {
710				dom: $navLink,
711				position: $navLink.position(),
712				outerWidth: $navLink.outerWidth(),
713				outerHeight: $navLink.outerHeight()
714			},
715			window: {
716				width: $(window).width(),
717				height: $(window).height(),
718				scrollTop: $(window).scrollTop(),
719				offset: $menu.offset()
720			}
721		};
722
723		// place it, initially
724		$menu.css({
725			left: calcLeft($menu, positionInfo) + "px",
726			top: calcTop($menu, positionInfo) + "px"
727		});
728
729		// now record the calculated position of the menu
730		positionInfo.menu.offset = $menu.offset();
731		positionInfo.menu.position = $menu.position();
732
733		// make adjustments and do bounces
734		if (!positionInfo.menu.isSub) {
735			matchTitleWidth($menu, positionInfo);
736		}
737		bounceBottom($menu, positionInfo);
738		bounceTop($menu, positionInfo);
739		bounceRight($menu, positionInfo);
740
741		// $menu.css(originalStyle);
742		$menu[0].positioned = true;
743	}
744
745	//  do the setup
746	initialize();
747}
748
749function GlobalMenuNavController()
750{
751	var self = this;
752
753	this.swipeState = null;
754	this.swipeThreshold = 5;
755
756	// PUBLIC PROPERTIES
757	this.navs = []; // list of all menu navs controllers
758
759	// EVENT HANDLERS
760	function onWindowKeyPress(event) {
761		// console.log("got key press, code " + event.keyCode);
762		if (event.keyCode === 80) {
763			if (window.menusPaused) {
764				// console.log("unpausing menus.");
765				window.menusPaused = false;
766			} else {
767				if (getOpenMenus().length > 0) {
768					// console.log("pausing menus.");
769					window.menusPaused = true;
770				}
771			}
772		}
773	}
774
775	function onWindowTouchMove(event) {
776		var oe = event.originalEvent;
777
778		// touch move, this means a scroll or something has happened that
779		// shouldn't close all menus when the touch event ends.
780		if (self.swipeState) {
781			self.swipeState.currentPosition = {
782				x: oe.touches[0].pageX,
783				y: oe.touches[0].pageY
784			};
785			self.swipeState.swipeLength = calculateSwipeDistance(self.swipeState);
786			self.swipeState.isARealSwipe = (self.swipeState.swipeLength >= self.swipeThreshold);
787
788			// console.log("onWindowTouchMove: swipe length: " + self.swipeState.swipeLength + (self.swipeState.isARealSwipe ? "; real swipe now" : "; not a real swipe"));
789		} else {
790			self.swipeState =  {
791				completed: false,
792				currentPosition: {
793					x: null,
794					y: null
795				},
796				startPosition: {
797					x: oe.touches[0].pageX,
798					y: oe.touches[0].pageY
799				}
800			};
801		}
802	}
803
804	function onWindowTouchEnd() {
805		// var oe = event.originalEvent;
806		// console.log("onWindowTouchEnd");
807		if (self.swipeState && self.swipeState.isARealSwipe) {
808			// console.log("self.swipeState.isARealSwipe = " + self.swipeState.isARealSwipe);
809		}
810		if (!self.swipeState || !self.swipeState.isARealSwipe) {
811			closeAllMenus();
812		}
813
814		// above here was the last call for responding to the swipe state
815		self.swipeState = null;
816	}
817
818	// ACTIONS
819	function closeAllMenus() {
820		// if the cancel flag is set, reset it and don't close the menus...but
821		// just this once.
822		// if (cancelCloseAll) {
823		// 	cancelCloseAll = false;
824		// 	return;
825		// }
826		$.each(self.navs, function () {
827			this.closeAllMenus();
828		});
829	}
830	this.initializeAllMenuNavs = function () {
831		$("nav.menu").each(function (i, nav) {
832			self.addNav(nav);
833		});
834	};
835
836	this.addNav = function (navElement) {
837		self.navs.push(new MenuNav(navElement));
838	};
839
840	// INFO FUNCTIONS
841	function getOpenMenus() {
842		return $("nav.menu li.open");
843	}
844
845	// returns an integer. takes a current position object to compare against
846	// the last recorded swipe origin position
847	function calculateSwipeDistance(swipeState) {
848		var delta = lineLength(swipeState.currentPosition.x, swipeState.currentPosition.y,
849							   swipeState.startPosition.x, swipeState.startPosition.y);
850		return Math.round(Math.abs(delta));
851	}
852
853	// WIRE UP EVENTS
854	$(window)
855		.on("keyup", onWindowKeyPress)
856		.on("touchmove", onWindowTouchMove)
857		.on("touchend", onWindowTouchEnd);
858}
859
860// FREE FUNCTIONS
861function lineLength(x, y, x0, y0) {
862	return Math.sqrt((x -= x0) * x + (y -= y0) * y);
863}
864
865// BUG TESTS
866// this tests for this bug:
867// https://bugzilla.mozilla.org/show_bug.cgi?id=63895
868function detectMozillaCellContextBug() {
869	var debug = false;
870	var $box = $("<ul id='fftest'style='" + (debug ? "" : "visibility:hidden;") + "padding:0;margin:0;display:table;position:absolute;top:0;left:0;'><li style='margin:0;background:blue;position:relative;display:table-cell;'>woo woo</li><li style='margin:0;background:blue;position:relative;display:table-cell;'>hahhaha<ul style='padding:0;display:block;position:absolute;left:0;top:0;'><li class='spud' style='margin:0;display:block;background:red;height:5px;width:5px;'></li></ul></li></ul>");
871
872	// test
873	var originalPosition = $(document.body).css("position");
874	$(document.body).css("position", "static").append($box);
875	var offsetLeft = $box.find(".spud").offset().left;
876	var result = (offsetLeft === 0  && $.browser.mozilla);
877
878	// clean up
879	$(document.body).css("position", originalPosition);
880	if (debug) {
881		// console.log("spud offset: " + offsetLeft);
882		// console.log("firefox context bug: " + (result ? "present" : "not present"));
883	} else {
884		$box.remove();
885	}
886
887	// report
888	if (result) { $("html").addClass("mozilla-cell-context-bug"); }
889	return result;
890}
891
892// instantiate controllers for each menu nav on the page
893$(document).ready(function documentReady() {
894	// fire up the global menu nav controller and init all menus navs
895	var gmnc = window.globalMenuNavController = new GlobalMenuNavController();
896	gmnc.initializeAllMenuNavs();
897
898	// runtime test for nasty gecko bug
899	// detectMozillaCellContextBug();
900});

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.