menu.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. /*
  2. @licstart The following is the entire license notice for the JavaScript code in this file.
  3. The MIT License (MIT)
  4. Copyright (C) 1997-2020 by Dimitri van Heesch
  5. Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  6. and associated documentation files (the "Software"), to deal in the Software without restriction,
  7. including without limitation the rights to use, copy, modify, merge, publish, distribute,
  8. sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all copies or
  11. substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  13. BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  14. NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  15. DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  16. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  17. @licend The above is the entire license notice for the JavaScript code in this file
  18. */
  19. function initMenu(relPath,treeview) {
  20. const SHOW_DELAY = 250; // 250ms delay before showing
  21. const HIDE_DELAY = 500; // 500ms delay before hiding
  22. const SLIDE_DELAY = 250; // 250ms slide up/down delay
  23. const WHEEL_STEP = 30; // 30 pixel per mouse wheel tick
  24. const ARROW_STEP = 5; // 5 pixel when hovering arrow up/down
  25. const ARROW_POLL_INTERVAL = 20; // 20ms per arrow up/down check
  26. const MOBILE_WIDTH = 768; // switch point for mobile/desktop mode
  27. // Helper function for slideDown animation
  28. function slideDown(element, duration, callback) {
  29. if (element.dataset.animating) return;
  30. element.dataset.animating = 'true';
  31. element.style.removeProperty('display');
  32. let display = window.getComputedStyle(element).display;
  33. if (display === 'none') display = 'block';
  34. element.style.display = display;
  35. const height = element.offsetHeight;
  36. element.style.overflow = 'hidden';
  37. element.style.height = 0;
  38. element.offsetHeight; // force reflow
  39. element.style.transitionProperty = 'height';
  40. element.style.transitionDuration = duration + 'ms';
  41. element.style.height = height + 'px';
  42. window.setTimeout(() => {
  43. element.style.removeProperty('height');
  44. element.style.removeProperty('overflow');
  45. element.style.removeProperty('transition-duration');
  46. element.style.removeProperty('transition-property');
  47. delete element.dataset.animating;
  48. if (callback) callback();
  49. }, duration);
  50. }
  51. // Helper function for slideUp animation
  52. function slideUp(element, duration, callback) {
  53. if (element.dataset.animating) return;
  54. element.dataset.animating = 'true';
  55. element.style.transitionProperty = 'height';
  56. element.style.transitionDuration = duration + 'ms';
  57. element.style.height = element.offsetHeight + 'px';
  58. element.offsetHeight; // force reflow
  59. element.style.overflow = 'hidden';
  60. element.style.height = 0;
  61. window.setTimeout(() => {
  62. element.style.display = 'none';
  63. element.style.removeProperty('height');
  64. element.style.removeProperty('overflow');
  65. element.style.removeProperty('transition-duration');
  66. element.style.removeProperty('transition-property');
  67. delete element.dataset.animating;
  68. if (callback) callback();
  69. }, duration);
  70. }
  71. // Helper to create the menu tree structure
  72. function makeTree(data,relPath,topLevel=false) {
  73. let result='';
  74. if ('children' in data) {
  75. if (!topLevel) {
  76. result+='<ul>';
  77. }
  78. for (let i in data.children) {
  79. let url;
  80. const link = data.children[i].url;
  81. if (link.substring(0,1)=='^') {
  82. url = link.substring(1);
  83. } else {
  84. url = relPath+link;
  85. }
  86. result+='<li><a href="'+url+'">'+
  87. data.children[i].text+'</a>'+
  88. makeTree(data.children[i],relPath)+'</li>';
  89. }
  90. if (!topLevel) {
  91. result+='</ul>';
  92. }
  93. }
  94. return result;
  95. }
  96. const mainNav = document.getElementById('main-nav');
  97. if (mainNav && mainNav.children.length > 0) {
  98. const firstChild = mainNav.children[0];
  99. firstChild.insertAdjacentHTML('afterbegin', makeTree(menudata, relPath, true));
  100. }
  101. const searchBoxPos2 = document.getElementById('searchBoxPos2');
  102. let searchBoxContents = searchBoxPos2 ? searchBoxPos2.innerHTML : '';
  103. const mainMenuState = document.getElementById('main-menu-state');
  104. let prevWidth = 0;
  105. const initResizableIfExists = function() {
  106. if (typeof initResizableFunc === 'function') initResizableFunc(treeview);
  107. }
  108. // Dropdown menu functionality to replace smartmenus
  109. let closeAllDropdowns = null; // Will be set by initDropdownMenu
  110. const isMobile = () => window.innerWidth < MOBILE_WIDTH;
  111. if (mainMenuState) {
  112. const mainMenu = document.getElementById('main-menu');
  113. const searchBoxPos1 = document.getElementById('searchBoxPos1');
  114. // animate mobile main menu
  115. mainMenuState.addEventListener('change', function() {
  116. if (this.checked) {
  117. slideDown(mainMenu, SLIDE_DELAY, () => {
  118. mainMenu.style.display = 'block';
  119. initResizableIfExists();
  120. });
  121. } else {
  122. slideUp(mainMenu, SLIDE_DELAY, () => {
  123. mainMenu.style.display = 'none';
  124. initResizableIfExists();
  125. });
  126. }
  127. });
  128. // set default menu visibility
  129. const resetState = function() {
  130. const newWidth = window.innerWidth;
  131. if (newWidth !== prevWidth) {
  132. // Close all open dropdown menus when switching between mobile/desktop modes
  133. if (closeAllDropdowns) {
  134. closeAllDropdowns();
  135. }
  136. if (newWidth < MOBILE_WIDTH) {
  137. mainMenuState.checked = false;
  138. mainMenu.style.display = 'none';
  139. if (searchBoxPos2) {
  140. searchBoxPos2.innerHTML = '';
  141. searchBoxPos2.style.display = 'none';
  142. }
  143. if (searchBoxPos1) {
  144. searchBoxPos1.innerHTML = searchBoxContents;
  145. searchBoxPos1.style.display = '';
  146. }
  147. } else {
  148. mainMenu.style.display = '';
  149. if (searchBoxPos1) {
  150. searchBoxPos1.innerHTML = '';
  151. searchBoxPos1.style.display = 'none';
  152. }
  153. if (searchBoxPos2) {
  154. searchBoxPos2.innerHTML = searchBoxContents;
  155. searchBoxPos2.style.display = '';
  156. }
  157. }
  158. if (typeof searchBox !== 'undefined') {
  159. searchBox.CloseResultsWindow();
  160. }
  161. prevWidth = newWidth;
  162. }
  163. }
  164. if (document.readyState === 'loading') {
  165. document.addEventListener('DOMContentLoaded', function() {
  166. resetState();
  167. initResizableIfExists();
  168. });
  169. } else {
  170. resetState();
  171. initResizableIfExists();
  172. }
  173. window.addEventListener('resize', resetState);
  174. } else {
  175. initResizableIfExists();
  176. }
  177. function initDropdownMenu() {
  178. const mainMenu = document.getElementById('main-menu');
  179. if (!mainMenu) return;
  180. const menuItems = mainMenu.querySelectorAll('li');
  181. // Helper function to position nested submenu with viewport checking
  182. function positionNestedSubmenu(submenu, link) {
  183. const viewport = {
  184. height: window.innerHeight,
  185. scrollY: window.scrollY
  186. };
  187. // Set initial position - top aligned with parent (next to arrow)
  188. submenu.style.top = '0';
  189. if (isMobile()) {
  190. submenu.style.marginLeft = 0;
  191. } else {
  192. submenu.style.marginLeft = link.offsetWidth + 'px';
  193. }
  194. // Get submenu dimensions and position
  195. const submenuRect = submenu.getBoundingClientRect();
  196. const submenuHeight = submenuRect.height;
  197. const submenuTop = submenuRect.top;
  198. const submenuBottom = submenuRect.bottom+1; // add space for border
  199. // Check if submenu fits in viewport
  200. const fitsAbove = submenuTop >= 0;
  201. const fitsBelow = submenuBottom <= viewport.height;
  202. if (!fitsAbove || !fitsBelow) {
  203. // Submenu doesn't fit - try to adjust position
  204. // Overflows bottom, try to shift up
  205. const overflow = submenuBottom - viewport.height;
  206. const newTop = Math.max(0, submenuTop-overflow)-submenuTop;
  207. submenu.style.top = newTop + 'px';
  208. // Re-check after adjustment
  209. const adjustedRect = submenu.getBoundingClientRect();
  210. if (adjustedRect.height > viewport.height) {
  211. // Still doesn't fit - enable scrolling
  212. enableSubmenuScrolling(submenu, link);
  213. }
  214. }
  215. }
  216. // Helper function to enable scrolling for tall submenus
  217. function enableSubmenuScrolling(submenu, link) {
  218. // Check if scroll arrows already exist
  219. if (submenu.dataset.scrollEnabled) return;
  220. submenu.dataset.scrollEnabled = 'true';
  221. const viewport = {
  222. height: window.innerHeight,
  223. scrollY: window.scrollY
  224. };
  225. // Position submenu to fill available viewport space
  226. const parentRect = link.getBoundingClientRect();
  227. const availableHeight = viewport.height - 2; // Leave some margin
  228. submenu.style.maxHeight = availableHeight + 'px';
  229. submenu.style.overflow = 'hidden';
  230. submenu.style.position = 'absolute';
  231. // Create scroll arrows
  232. const scrollUpArrow = document.createElement('div');
  233. scrollUpArrow.className = 'submenu-scroll-arrow submenu-scroll-up';
  234. scrollUpArrow.innerHTML = '<span class="scroll-up-arrow"></span>';//'<span>▲</span>';
  235. scrollUpArrow.style.cssText = 'position:absolute;top:0;left:0;right:0;height:30px;background:transparent;text-align:center;line-height:30px;color:#fff;cursor:pointer;z-index:1000;display:none;';
  236. const scrollDownArrow = document.createElement('div');
  237. scrollDownArrow.className = 'submenu-scroll-arrow submenu-scroll-down';
  238. scrollDownArrow.innerHTML = '<span class="scroll-down-arrow"></span>';
  239. scrollDownArrow.style.cssText = 'position:absolute;bottom:0;left:0;right:0;height:30px;background:transparent;text-align:center;line-height:30px;color:#fff;cursor:pointer;z-index:1000;';
  240. // Create wrapper for submenu content
  241. const scrollWrapper = document.createElement('div');
  242. scrollWrapper.className = 'submenu-scroll-wrapper';
  243. scrollWrapper.style.cssText = 'height:100vh;overflow:hidden;position:relative;';
  244. // Move submenu children to wrapper
  245. while (submenu.firstChild) {
  246. scrollWrapper.appendChild(submenu.firstChild);
  247. }
  248. submenu.appendChild(scrollUpArrow);
  249. submenu.appendChild(scrollWrapper);
  250. submenu.appendChild(scrollDownArrow);
  251. let scrollPosition = 0;
  252. let scrollInterval = null;
  253. function updateScrollArrows() {
  254. const maxScroll = scrollWrapper.scrollHeight - availableHeight;
  255. scrollUpArrow.style.display = scrollPosition > 0 ? 'block' : 'none';
  256. scrollDownArrow.style.display = scrollPosition < maxScroll ? 'block' : 'none';
  257. }
  258. function startScrolling(direction) {
  259. if (scrollInterval) return;
  260. scrollInterval = setInterval(() => {
  261. const maxScroll = scrollWrapper.scrollHeight - availableHeight;
  262. if (direction === 'up') {
  263. scrollPosition = Math.max(0, scrollPosition - ARROW_STEP);
  264. } else {
  265. scrollPosition = Math.min(maxScroll, scrollPosition + ARROW_STEP);
  266. }
  267. scrollWrapper.scrollTop = scrollPosition;
  268. updateScrollArrows();
  269. if ((direction === 'up' && scrollPosition === 0) ||
  270. (direction === 'down' && scrollPosition === maxScroll)) {
  271. stopScrolling();
  272. }
  273. }, ARROW_POLL_INTERVAL);
  274. }
  275. function stopScrolling() {
  276. if (scrollInterval) {
  277. clearInterval(scrollInterval);
  278. scrollInterval = null;
  279. }
  280. }
  281. scrollUpArrow.addEventListener('mouseenter', () => startScrolling('up'));
  282. scrollUpArrow.addEventListener('mouseleave', stopScrolling);
  283. scrollDownArrow.addEventListener('mouseenter', () => startScrolling('down'));
  284. scrollDownArrow.addEventListener('mouseleave', stopScrolling);
  285. function wheelEvent(e) {
  286. e.preventDefault();
  287. e.stopPropagation();
  288. const maxScroll = scrollWrapper.scrollHeight - availableHeight;
  289. const wheelDelta = e.deltaY;
  290. const scrollAmount = wheelDelta > 0 ? WHEEL_STEP : -WHEEL_STEP; // Scroll 30px per wheel tick
  291. scrollPosition = Math.max(0, Math.min(maxScroll, scrollPosition + scrollAmount));
  292. scrollWrapper.scrollTop = scrollPosition;
  293. updateScrollArrows();
  294. }
  295. // Add mouse wheel scrolling support
  296. scrollWrapper.addEventListener('wheel', (e) => wheelEvent(e));
  297. // Also add wheel event to submenu itself to catch events
  298. submenu.addEventListener('wheel', function(e) {
  299. // Only handle if scrolling is enabled
  300. if (submenu.dataset.scrollEnabled) {
  301. wheelEvent(e);
  302. }
  303. });
  304. // Initial arrow state
  305. updateScrollArrows();
  306. }
  307. // Helper function to clean up scroll arrows
  308. function disableSubmenuScrolling(submenu) {
  309. if (!submenu.dataset.scrollEnabled) return;
  310. delete submenu.dataset.scrollEnabled;
  311. // Find and remove scroll elements
  312. const scrollArrows = submenu.querySelectorAll('.submenu-scroll-arrow');
  313. const scrollWrapper = submenu.querySelector('.submenu-scroll-wrapper');
  314. if (scrollWrapper) {
  315. // Move children back to submenu
  316. while (scrollWrapper.firstChild) {
  317. submenu.appendChild(scrollWrapper.firstChild);
  318. }
  319. scrollWrapper.remove();
  320. }
  321. scrollArrows.forEach(arrow => arrow.remove());
  322. // Reset styles
  323. submenu.style.maxHeight = '';
  324. submenu.style.overflow = '';
  325. }
  326. menuItems.forEach(item => {
  327. const submenu = item.querySelector('ul');
  328. if (submenu) {
  329. const link = item.querySelector('a');
  330. if (link) {
  331. // Add class and ARIA attributes for accessibility
  332. link.classList.add('has-submenu');
  333. link.setAttribute('aria-haspopup', 'true');
  334. link.setAttribute('aria-expanded', 'false');
  335. // Add sub-arrow indicator
  336. const span = document.createElement('span');
  337. span.classList.add('sub-arrow');
  338. link.append(span);
  339. // Calculate nesting level for z-index
  340. // Root menu (main-menu) is level 200 (above the search box at 102),
  341. // first submenus are level 201, etc.
  342. let nestingLevel = 200;
  343. let currentElement = item.parentElement;
  344. while (currentElement && currentElement.id !== 'main-menu') {
  345. if (currentElement.tagName === 'UL') {
  346. nestingLevel++;
  347. }
  348. currentElement = currentElement.parentElement;
  349. }
  350. // Apply z-index based on nesting level
  351. // This ensures child menus with shadows appear above parent menus
  352. submenu.style.zIndex = nestingLevel + 1;
  353. // Check if this is a level 2+ submenu (nested within another dropdown)
  354. const isNestedSubmenu = item.parentElement && item.parentElement.id !== 'main-menu';
  355. // Timeout management for smooth menu navigation
  356. let showTimeout = null;
  357. let hideTimeout = null;
  358. // Desktop: show on hover
  359. item.addEventListener('mouseenter', function() {
  360. if (!isMobile()) {
  361. // Clear any pending hide timeout
  362. if (hideTimeout) {
  363. clearTimeout(hideTimeout);
  364. hideTimeout = null;
  365. }
  366. // Set show timeout
  367. showTimeout = setTimeout(() => {
  368. // Hide all sibling menus at the same level before showing this one
  369. const parentElement = item.parentElement;
  370. if (parentElement) {
  371. const siblings = parentElement.querySelectorAll(':scope > li');
  372. siblings.forEach(sibling => {
  373. if (sibling !== item) {
  374. const siblingSubmenu = sibling.querySelector('ul');
  375. const siblingLink = sibling.querySelector('a');
  376. if (siblingSubmenu && siblingLink) {
  377. siblingSubmenu.style.display = 'none';
  378. siblingLink.setAttribute('aria-expanded', 'false');
  379. disableSubmenuScrolling(siblingSubmenu);
  380. }
  381. }
  382. });
  383. }
  384. submenu.style.display = 'block';
  385. // Only apply positioning for nested submenus (level 2+)
  386. if (isNestedSubmenu) {
  387. positionNestedSubmenu(submenu, link);
  388. }
  389. link.setAttribute('aria-expanded', 'true');
  390. showTimeout = null;
  391. }, SHOW_DELAY);
  392. }
  393. });
  394. item.addEventListener('mouseleave', function() {
  395. if (!isMobile()) {
  396. // Clear any pending show timeout
  397. if (showTimeout) {
  398. clearTimeout(showTimeout);
  399. showTimeout = null;
  400. }
  401. // Set hide timeout
  402. hideTimeout = setTimeout(() => {
  403. submenu.style.display = 'none';
  404. link.setAttribute('aria-expanded', 'false');
  405. // Clean up scrolling if enabled
  406. disableSubmenuScrolling(submenu);
  407. hideTimeout = null;
  408. }, HIDE_DELAY);
  409. }
  410. });
  411. if (isMobile() && isNestedSubmenu) {
  412. positionNestedSubmenu(submenu, link);
  413. }
  414. function toggleMenu() {
  415. const isExpanded = link.getAttribute('aria-expanded') === 'true';
  416. if (isExpanded) {
  417. slideUp(submenu, SLIDE_DELAY, () => {
  418. submenu.style.display = 'none';
  419. link.setAttribute('aria-expanded', 'false');
  420. link.classList.remove('highlighted')
  421. disableSubmenuScrolling(submenu);
  422. });
  423. } else {
  424. slideDown(submenu, SLIDE_DELAY, () => {
  425. submenu.style.display = 'block';
  426. link.classList.add('highlighted')
  427. link.setAttribute('aria-expanded', 'true');
  428. });
  429. }
  430. }
  431. // Mobile/Touch: toggle on click
  432. link.addEventListener('click', function(e) {
  433. if (isMobile()) {
  434. e.preventDefault();
  435. toggleMenu();
  436. }
  437. });
  438. // Keyboard navigation
  439. link.addEventListener('keydown', function(e) {
  440. if (e.key === 'Enter' || e.key === ' ') {
  441. e.preventDefault();
  442. toggleMenu();
  443. } else if (e.key === 'Escape') {
  444. submenu.style.display = 'none';
  445. link.setAttribute('aria-expanded', 'false');
  446. disableSubmenuScrolling(submenu);
  447. link.focus();
  448. }
  449. });
  450. }
  451. }
  452. });
  453. // Helper function to close all open dropdown menus
  454. closeAllDropdowns = function() {
  455. menuItems.forEach(item => {
  456. const submenu = item.querySelector('ul');
  457. const link = item.querySelector('a.has-submenu');
  458. if (submenu && link) {
  459. disableSubmenuScrolling(submenu);
  460. submenu.style.display = 'none';
  461. submenu.style.marginLeft = 0;
  462. link.setAttribute('aria-expanded', 'false');
  463. link.classList.remove('highlighted');
  464. }
  465. });
  466. };
  467. // Close all dropdown menus when clicking a link (navigation to new page or anchor)
  468. const allLinks = mainMenu.querySelectorAll('a');
  469. allLinks.forEach(link => {
  470. link.addEventListener('click', function() {
  471. // Close dropdowns when navigating (unless it's a has-submenu link in mobile mode)
  472. if (!link.classList.contains('has-submenu') || !isMobile()) {
  473. if (closeAllDropdowns) {
  474. closeAllDropdowns();
  475. }
  476. }
  477. });
  478. });
  479. }
  480. // Initialize dropdown menu behavior
  481. initDropdownMenu();
  482. // Close all open menus when browser back button is pressed
  483. window.addEventListener('popstate', function() {
  484. if (closeAllDropdowns) {
  485. closeAllDropdowns();
  486. }
  487. });
  488. }
  489. document.addEventListener('DOMContentLoaded', () => {
  490. const configElem = document.getElementById('doxygen-config');
  491. if (configElem) {
  492. const config = JSON.parse(configElem.textContent);
  493. initMenu(config.relPath, config.generateTreeView);
  494. if (config.searchEngine) {
  495. if (!config.serverBasedSearch) {
  496. if (!config.disableIndex && config.dynamicMenus && !config.fullSidebar) {
  497. if (typeof init_search === 'function') init_search();
  498. }
  499. } else {
  500. if (document.querySelector('.searchresults') && typeof searchBox !== 'undefined') {
  501. searchBox.DOMSearchField().focus();
  502. }
  503. }
  504. }
  505. }
  506. });
  507. /* @license-end */