PageSourceSearch

http://ballooninghistory.com/rc_images/wsp_menu.js

js ballooninghistory.com collected 2026-10-02 11:06:40 UTC 20,036 bytes, 656 lines download raw bytes

1// RocketCake Menu implementation
2// (c) by Nikolaus Gebhardt / Ambiera e.U.
3// parameters:
4// elementid: Element id of the root menu item
5// animations: combination of 'fadeMenus', 'moveHeight'
6// openViaMouseHovering: if set to true, on desktop, open menu already via mouse hovering. If set to false, only close and open the menu when clicked or touched.
7
8function wsp_menu(elementid, menuidsuffix, panepadding, animations, openViaMouseHovering, advancedOptions)
9{
10	this.menuElementSubMenuParent = document.getElementById(elementid);
11	this.menuElementEntryHolder = null;
12	
13	this.generateAriaLabels = true;
14	this.setUsefulTabIndices = true;
15	this.closeWhenMouseOut = false;
16	this.mobileMenuIsFullscreen = false;
17	this.mobileMenuCloseButtonColor = '#A3A3A3';
18	
19	this.UseAnimationForAll = animations != null; // animation setting for all menupanes
20	this.EnabledAnimationsForAll = animations; // animations to be used for all menupanes
21	
22	this.UseAnimationForMobileMenu = animations != null;  // animation setting for mobile menupanes
23	this.EnabledAnimationsForMobileMenu = animations; // animations to be used for mobile menupanes
24	this.MenuPaddingUsed = 0;
25	
26	if (advancedOptions)
27	{
28		// generate missing aria labels
29		if (typeof advancedOptions.generateAriaLabels != 'undefined')
30			this.generateAriaLabels = advancedOptions.generateAriaLabels;
31		
32		// auto set tab indices
33		if (typeof advancedOptions.setUsefulTabIndices != 'undefined')
34			this.setUsefulTabIndices = advancedOptions.setUsefulTabIndices;
35		
36		// close when mouse out option
37		if (typeof advancedOptions.closeWhenMouseOut != 'undefined')
38			this.closeWhenMouseOut = advancedOptions.closeWhenMouseOut;
39		
40		// mobile menu is fullscreen
41		if (typeof advancedOptions.mobileMenuIsFullscreen != 'undefined')
42			this.mobileMenuIsFullscreen = advancedOptions.mobileMenuIsFullscreen;
43		
44		// mobile menu close button color
45		if (typeof advancedOptions.mobileMenuCloseButtonColor != 'undefined')
46			this.mobileMenuCloseButtonColor = advancedOptions.mobileMenuCloseButtonColor;
47		
48		// used padding for menu so we can scroll the fullscreen mobile menu correctly
49		if (typeof advancedOptions.menuPaddingUsed != 'undefined')
50			this.MenuPaddingUsed = advancedOptions.menuPaddingUsed;
51		
52		// forced animations for mobile menu
53		if (typeof advancedOptions.mobileMenuAnimations != 'undefined')
54		{
55			this.UseAnimationForMobileMenu = advancedOptions.mobileMenuAnimations != null;
56			this.EnabledAnimationsForMobileMenu = advancedOptions.mobileMenuAnimations;
57		}
58	}
59	
60	if (this.menuElementSubMenuParent)
61	{
62		var divs = this.menuElementSubMenuParent.getElementsByTagName('div');
63		if (divs.length)
64		{
65			this.menuElementEntryHolder = divs[0];		
66			
67			if (this.menuElementEntryHolder && this.menuElementEntryHolder.id.indexOf('_menualignmentwrapper') > -1)
68			{
69				// menu has an alignment wrapper, so the entry holder is below it
70				divs = this.menuElementEntryHolder.getElementsByTagName('div');
71				if (divs.length)
72				{
73					this.menuElementSubMenuParent = this.menuElementEntryHolder;
74					this.menuElementEntryHolder = divs[0];					
75				}
76			}				
77		}	
78	}
79	
80	this.rootMenuElements = new Array();
81	this.menuPanes = new Array();
82	this.menuidsuffix = menuidsuffix;
83	this.panepadding = panepadding;
84	this.initialClientHeight = 0;
85	this.currentlyVisibleMenuPane = null;
86	this.openViaMouseHovering = openViaMouseHovering; 
87	this.openAndCloseViaClick = !openViaMouseHovering
88	WspMenusLastTimeClicked = 0;  // global if using more than one menu
89	this.LastOpenedSubMenu = null;
90	this.TimeLastMenuFocused = 0;
91	
92	if ( typeof document["wspMenuGlobalTabIndex"] == "undefined" )
93	{
94		if (this.setUsefulTabIndices)
95			document.wspMenuGlobalTabIndex = 1; // variable for global tabindex tracking in case we have more than one menu
96		else
97			document.wspMenuGlobalTabIndex = 0; // keep at 0 and let browser assign tab indices
98	}
99	
100	try {
101		if (wsp_allmenus == null)
102			wsp_allmenus = new Array();
103	} catch(e)
104	{
105		wsp_allmenus = new Array();
106	}
107	
108	wsp_allmenus.push(this);
109	
110	var me = this;
111	
112	document.addEventListener("click", function(e) { me.clickedOutside(); } );
113	
114	if (this.closeWhenMouseOut)
115	{
116		document.addEventListener("mousemove", function(e)
117		{ 
118			me.closeMenuWhenFocusedElementIsNotPartOfMenu(e.target);  
119		});		
120	}
121				
122
123	if (this.menuElementSubMenuParent != null)
124		this.menuElementSubMenuParent.style.overflow = "visible";
125
126	if (this.menuElementEntryHolder != null)
127		this.menuElementEntryHolder.style.overflow = "hidden";
128	
129	
130	this.createMenuForItem = function(menuelementid, elementData, isForMobileMenu)
131	{
132		var elm = document.getElementById(menuelementid);
133		if (elm == null)
134			return;
135		
136		elm.tabIndex = document.wspMenuGlobalTabIndex; // ensure focusable by keyboard
137		if (this.setUsefulTabIndices)
138			document.wspMenuGlobalTabIndex += 1;
139		
140		this.rootMenuElements.push(elm);
141		var menupane = this.createMenuElements(elm, elementData, false, isForMobileMenu);
142		this.menuPanes.push(menupane);
143				
144		var me = this;
145		var openAndCloseViaClick = me.openAndCloseViaClick;
146		if (isForMobileMenu && me.mobileMenuIsFullscreen) 
147			openAndCloseViaClick = true; // for fullscreen mobile menu, force opening and closing via click so that it closes again when clicked on the menu button again, that's what users expect how it works
148		
149		elm.openAndCloseViaClick = openAndCloseViaClick;
150		
151		elm.onclick = function(e)  
152		{ 
153			if (openAndCloseViaClick && (me.getTimeMs() - me.TimeLastMenuFocused)< 250) // when menu is in click mode, we get focus and then click event. Prevent closing again in this case.
154				return;
155		
156			me.onMenuitemHoveredOrClicked(this, true); 
157		};
158		
159		elm.onfocus = function(e)   // support for tab traversal of menu: open menu when focused
160		{
161			me.TimeLastMenuFocused = me.getTimeMs(); // when menu is in click mode (openAndCloseViaClick), we get focus and then click event. Record time to prevent closing again in this case.
162			
163			me.onMenuitemHoveredOrClicked(this, true); 
164		}; 
165		
166		var onfocusoutfunction = function(e) { me.closeMenuWhenFocusedElementIsNotPartOfMenu(e.relatedTarget); };
167		elm.addEventListener("focusout", onfocusoutfunction );	
168		
169		var needsTouchForOpeningNotHover = isForMobileMenu && this.mobileMenuIsFullscreen; // when mobile menu is fullscreen, users won't like it when it opens via hover
170		
171		if (this.openViaMouseHovering && !needsTouchForOpeningNotHover)
172			elm.onmouseover = function(e) { me.onMenuitemHoveredOrClicked(this, false); };		
173	}
174	
175	
176	this.isElementPartOfTheMenu = function(elem)
177	{
178		var test = elem;
179		var isInMenu = false;
180		
181		while(test)
182		{
183			if (this.menuElementSubMenuParent === test ||
184				this.menuElementEntryHolder == test)
185			{
186				isInMenu = true;
187				break;
188			}
189			
190			test = test.parentNode;
191		}
192		
193		return isInMenu;
194	}
195	
196	
197	this.closeMenuWhenFocusedElementIsNotPartOfMenu = function(newElementWithFocus)
198	{				
199		if (!this.isElementPartOfTheMenu(newElementWithFocus))
200			this.closeAllMenus();
201	}
202	
203	
204	this.createMenuElements = function(htmlelement, elementData, issubmenu, isForMobileMenu)
205	{
206		if (htmlelement == null)
207			return;
208			
209		if (elementData == null || elementData.length == 0)
210			return;
211			
212		var me = this;
213		var menupane = document.createElement("div");
214		
215		menupane.style.position = "absolute";		
216		menupane.style.left = (htmlelement.offsetLeft) + "px";
217		menupane.style.top = (htmlelement.clientHeight + this.panepadding) + "px";
218		menupane.style.overflow = "visible";
219		menupane.style.zIndex = 10;
220		menupane.id = this.menuidsuffix + "_pane";	
221		menupane.creationParentMenuElement = htmlelement;
222		
223		menupane.style.visibility = 'hidden';	
224		menupane.style.display = 'block';	
225		
226		// copy animation settings for this pane
227		
228		if (isForMobileMenu)
229		{
230			menupane.UseAnimation = this.UseAnimationForMobileMenu;
231			menupane.EnabledAnimations = this.EnabledAnimationsForMobileMenu;
232		}
233		else
234		{
235			menupane.UseAnimation = this.UseAnimationForAll;
236			menupane.EnabledAnimations = this.EnabledAnimationsForAll;
237		}
238		
239		// set style based on animation
240				
241		if (menupane.UseAnimation && this.isUsingFadeMenuPaneAnimations(menupane))
242			menupane.style.transition = "opacity 0.5s ease-out";
243		
244		this.menuElementSubMenuParent.appendChild(menupane);	
245		
246		menupane.subMenus = new Array();
247		
248		var maxWidth = 0;
249		var maxHeight = 0;
250		
251		var aentries = new Array();
252		
253		var submenusExist = false;
254		
255		for (var i=0; i<elementData.length; i+=3)
256		{			
257			var elementContent = elementData[i+1];
258			var elementTarget  = elementData[i+2];
259			
260			// test width of text
261			var testElement = document.createElement("span");
262			var textToMeasure = elementData[i];
263			
264			if (elementContent instanceof Array )
265			{
266				// has a sub menu
267				
268				// for elements with submenu and sub menu indicator, only measure the inner text,
269				// which is the second appearing span				
270				
271				var beginText = textToMeasure.indexOf('<span', textToMeasure.indexOf('<span')+1);
272				var endText = textToMeasure.indexOf('</span><span style="display:inline-block;', beginText);
273				
274				textToMeasure = textToMeasure.substring(beginText, endText);
275			}
276			
277			testElement.innerHTML = textToMeasure;
278			this.menuElementEntryHolder.appendChild(testElement);
279						
280			var width = testElement.offsetWidth;
281			var height = testElement.offsetHeight;
282			this.menuElementEntryHolder.removeChild(testElement);
283			
284			// now create real element		
285			var aentry = document.createElement("a");	
286			aentry.tabIndex = -1;			
287			
288			var menuentry = document.createElement("div");
289			
290			menuentry.tabIndex = document.wspMenuGlobalTabIndex; // ensure focusable by keyboard
291			if (this.setUsefulTabIndices)
292				document.wspMenuGlobalTabIndex += 1;
293			
294			var txt = null;
295			
296			if (elementData[i] != '-')
297				//txt = document.createTextNode(elementData[i]);
298				menuentry.innerHTML = elementData[i];
299			else
300			{
301				txt = document.createElement('hr');
302				txt.id = this.menuidsuffix + "_hr";
303			}
304			
305			aentry.appendChild(menuentry);
306			if (txt != null)
307				menuentry.appendChild(txt);
308			menupane.appendChild(aentry);
309						
310			if (elementContent instanceof Array )
311			{
312				// sub menu
313				var submenu = this.createMenuElements(menupane, elementContent, true);
314				menupane.subMenus.push(submenu);
315							
316				menuentry.onclick = 
317					function(me, submenu) { return function() { me.onSubMenuEntryHovered(submenu); } }(me, submenu);
318					
319				menuentry.onmouseover = 
320					function(me, submenu) { return function() { me.onSubMenuEntryHovered(submenu); } }(me, submenu);
321					
322				menuentry.onfocus = 
323					function(me, submenu) { return function() { me.onSubMenuEntryHovered(submenu); } }(me, submenu); // support for tab traversal of menu: open menu when focused
324					
325				var onfocusoutfunction = function(me, submenu) { return function(e) { me.closeMenuWhenFocusedElementIsNotPartOfMenu(e.relatedTarget); } }(me, submenu);
326				menuentry.addEventListener("focusout", onfocusoutfunction );					
327								
328				submenusExist = true;
329			}
330			else
331			{
332				// normal link
333				aentry.href = elementContent;
334				
335				if (elementTarget != null && elementTarget != '')
336					aentry.setAttribute('target', elementTarget);
337				
338				// write to use link as tabindex, so that links can be activated using ENTER
339				var oldTabIndex = menuentry.tabIndex;
340				menuentry.tabIndex = -1;
341				aentry.tabIndex = oldTabIndex;	
342				
343				var onfocusoutfunction = function(me) { return function(e) { me.closeMenuWhenFocusedElementIsNotPartOfMenu(e.relatedTarget); } }(me);		
344				aentry.addEventListener("focusout", onfocusoutfunction );
345
346				// aria label				
347				if (this.generateAriaLabels)
348				{
349					var label = testElement.innerText || testElement.textContent;
350					if (label)
351						aentry.ariaLabel = label.trim();
352				}
353			}
354			
355			menuentry.id = this.menuidsuffix + "_entry";
356			aentries.push(menuentry);
357					
358			maxWidth = Math.max(maxWidth, width);
359			maxHeight += height; //txt.clientHeight;
360		}
361		
362		if (submenusExist)
363			maxWidth += 20; // add space for the arrow right symbol for submenus
364		
365		for (var j=0; j<aentries.length; ++j)
366			aentries[j].style.width = maxWidth + "px";
367			
368		for (var j=0; j<menupane.subMenus.length; ++j)
369			menupane.subMenus[j].style.left = (htmlelement.offsetLeft + maxWidth + panepadding*2) + "px";
370		
371		maxWidth += panepadding*2;
372		menupane.style.width = maxWidth + "px"; 
373		menupane.style.whiteSpace = 'nowrap'; // in case for embedded images
374		menupane.aentries = aentries;
375		
376		if (isForMobileMenu && this.mobileMenuIsFullscreen)
377		{		
378			// make mobile menu fullscreen with close button
379			menupane.style.width = "100vw";
380			menupane.style.left = "0px";
381			var startpos = this.menuElementEntryHolder.getBoundingClientRect().bottom;
382			var paddingAdd = this.MenuPaddingUsed * 2; // top and bottom
383			menupane.style.top =  startpos + "px";
384			menupane.style.position = 'fixed';
385			
386			var dvhsupported = false;
387			if (typeof CSS !== "undefined" && typeof CSS.supports !== "undefined")
388				dvhsupported = CSS.supports('height: 100dvh');
389			
390			if (dvhsupported)
391				menupane.style.maxHeight = 'calc(-' + (startpos+paddingAdd) + 'px + 100dvh)'; // dvh for mobile safari because floating address bar would overlap otherwise
392			else
393				menupane.style.maxHeight = 'calc(-' + (startpos+paddingAdd) + 'px + 100vh)';
394			
395			menupane.style.overflowY = 'scroll';
396						
397			// the mobile menu is fixed to overlap other elements, but we still need it to scroll vertically, so make it do this with
398			// the following code, *unless* the parent menu or its containing container is also fixed, then we must not do this since it 
399			// would scroll out of the view
400			if (!isElemFixed(htmlelement))
401				makeElementScroll(menupane, startpos);
402						
403			// add close button
404			var closebtn = document.createElement("a");							
405			closebtn.href = 'javascript:void(0)';									
406			closebtn.style.cssText = 'position: absolute; top: 0; right: 30px; font-size: 42px; text-decoration: none;'; 
407			closebtn.style.color = this.mobileMenuCloseButtonColor;
408			closebtn.textContent = '×'; // × or × or &times;					
409			closebtn.ariaLabel = 'close menu';
410			menupane.appendChild(closebtn);					
411		}
412		
413		this.setStylesForVisibilityOfMenuPane(menupane, false);
414						
415		return menupane;
416	}
417	
418	function makeElementScroll(element, startpos) 
419	{
420		function updatePosition() 
421		{
422			element.style.top = (startpos - window.scrollY) + 'px';
423
424			requestAnimationFrame(updatePosition);			
425		}
426
427		updatePosition();
428	}
429
430
431	function isElemFixed(elm) 
432	{
433		while (elm && elm.nodeName.toLowerCase() !== 'body')
434		{
435			if (window.getComputedStyle(elm).getPropertyValue('position').toLowerCase() === 'fixed')
436				return true;
437			elm = elm.parentNode;
438		}
439		return false; 
440	}
441
442	
443	this.closeAllMenus = function()
444	{
445		for (var i=0; i<wsp_allmenus.length; ++i)
446		{
447			var m = wsp_allmenus[i];
448			m.showMenuPaneWithIndex(-1);
449			
450			// also close sub menus
451			m.closeOpenSubMenu();
452		}		
453	}
454	
455	this.closeOpenSubMenu = function()
456	{
457		if (this.LastOpenedSubMenu != null)
458		{
459			this.setStylesForVisibilityOfMenuPane(this.LastOpenedSubMenu, false);
460			
461			this.LastOpenedSubMenu = null;
462		}
463	}
464	
465	this.isMenuPaneWithIndexOpen = function(i)
466	{
467		var paneToCheck = null;
468		if (i >= 0 && i < this.menuPanes.length)
469			paneToCheck = this.menuPanes[i];
470		return (paneToCheck && 	this.currentlyVisibleMenuPane === paneToCheck);
471	}
472	
473	this.showMenuPaneWithIndex = function(i)
474	{
475		if (this.currentlyVisibleMenuPane)
476			this.setStylesForVisibilityOfMenuPane(this.currentlyVisibleMenuPane, false);
477				
478		var newpane = null;
479		if (i >= 0 && i < this.menuPanes.length)
480			newpane = this.menuPanes[i];
481		
482		this.currentlyVisibleMenuPane = newpane;
483		
484		if (newpane)
485		{
486			this.setStylesForVisibilityOfMenuPane(newpane, true);
487			
488			// also, update position
489			var htmlelement = newpane.creationParentMenuElement;
490			
491			if (newpane.style.position != 'fixed') // don't modify position of fullscreen window
492			{
493				newpane.style.left = (htmlelement.offsetLeft) + "px";			
494				newpane.style.top = (htmlelement.clientHeight) + "px";
495				this.ensureNotOutsideOfScreen(newpane);
496			}
497		}
498	}
499	
500	// main menu item hovered
501	this.onMenuitemHoveredOrClicked = function(itemHovered, actuallyThisWasAClick)
502	{
503		WspMenusLastTimeClicked = this.getTimeMs();
504			
505		// check if a root menu item has been hovered. If so, show its menu
506		for (var i=0; i<this.rootMenuElements.length; ++i)
507		{
508			if (itemHovered === this.rootMenuElements[i])
509			{
510				var closeMenuAgain = false;
511				
512				if (actuallyThisWasAClick 
513					&& (this.openAndCloseViaClick || this.rootMenuElements[i].openAndCloseViaClick) // <- fullscreen mobile menus will open close via click and can override the global setting
514					&& this.isMenuPaneWithIndexOpen(i))
515				{
516					closeMenuAgain = true;
517				}
518				
519				this.closeAllMenus();
520				
521				if (!closeMenuAgain)
522					this.showMenuPaneWithIndex(i);
523				break;
524			}
525		}
526	}
527	
528	// menu entry hovered, probably show sub menu
529	this.onSubMenuEntryHovered = function(submenu)
530	{
531		this.closeOpenSubMenu();
532		
533		this.setStylesForVisibilityOfMenuPane(submenu, true)
534		
535		this.LastOpenedSubMenu = submenu;
536		
537		WspMenusLastTimeClicked = this.getTimeMs();
538	}
539	
540	
541	this.clickedOutside = function()
542	{
543		if ((this.getTimeMs() - WspMenusLastTimeClicked)< 250)
544			return;
545			
546		this.closeAllMenus();
547	}
548	
549	
550	this.getTimeMs = function()
551	{
552		var d = new Date();
553		return d.getTime();
554	}
555	
556	this.ensureNotOutsideOfScreen = function(elem)
557	{
558		try
559		{
560			var pos = this.getElementAbsPosition(elem);
561			var scrollbarMargin = 20;
562			
563			if (pos.left + pos.width > window.innerWidth - scrollbarMargin  )
564			{
565				var deltaX = (pos.left + pos.width) - (window.innerWidth - scrollbarMargin);
566				if (pos.left - deltaX < 0)
567					deltaX += pos.left - deltaX;
568				
569				var currentPos = parseInt(elem.style.left);				
570				elem.style.left = (currentPos - deltaX) + 'px';
571			}
572		}
573		catch(e)
574		{
575		}
576	}
577	
578	this.setStylesForVisibilityOfMenuPane = function(menupane, show)
579	{
580		if (show)
581		{
582			if (!menupane.UseAnimation)
583				menupane.style.display = 'block'; // reduces viewport size if animations are not necessary
584			
585			menupane.style.visibility = 'visible';	
586
587			if (menupane.UseAnimation && this.isUsingFadeMenuPaneAnimations(menupane))
588				menupane.style.opacity = 1;		
589
590			if (menupane.UseAnimation && this.isUsingMoveHeightAnimations(menupane))
591				for (var j=0; j<menupane.aentries.length; ++j)
592				{
593					menupane.aentries[j].style.transition = "margin 0.5s ease";
594					menupane.aentries[j].style.marginTop = '0px';							
595				}
596		}
597		else
598		{
599			// hide
600			if (!menupane.UseAnimation)
601				menupane.style.display = 'none'; // reduces viewport size if animations are not necessary
602			
603			menupane.style.visibility = 'hidden';
604			
605			if (menupane.UseAnimation && this.isUsingFadeMenuPaneAnimations(menupane))
606				menupane.style.opacity = 0;		
607
608			if (menupane.UseAnimation && this.isUsingMoveHeightAnimations(menupane))
609				for (var j=0; j<menupane.aentries.length; ++j)
610					menupane.aentries[j].style.marginTop = (10 + j*-10) + 'px';	
611		}
612	}
613	
614	this.isUsingMoveHeightAnimations = function(menupane)
615	{
616		return menupane.EnabledAnimations && menupane.EnabledAnimations.indexOf('moveHeight') >= 0;
617	}
618	
619	this.isUsingFadeMenuPaneAnimations = function(menupane)
620	{
621		return menupane.EnabledAnimations && menupane.EnabledAnimations.indexOf('fadeMenus') >= 0;
622	}
623		
624	this.getElementAbsPosition = function(element) 
625	{
626		var top = 0;
627		var left = 0;
628		var width = element.offsetWidth;
629		var height = element.offsetHeight;	
630		
631		do 
632		{
633			top += element.offsetTop  || 0;
634			left += element.offsetLeft || 0;
635			element = element.offsetParent;
636		} 
637		while(element);
638		
639		// this does the same but doesn't work on older browsers:
640		// var rct = element.getBoundingClientRect();
641		// top = rct.top;
642		// left = rct.left;
643		// width = rct.right - rct.left;
644		// height = rct.bottom - rct.top;
645		
646		var obj = new Object();
647		
648		obj.top = top;
649		obj.left = left;
650		obj.width = width;
651		obj.height = height;
652		
653		return  obj;
654	}
655	
656} // end function wsp_menu

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.