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 × 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.