1import {flip, computePosition} from "@floating-ui/dom"; 2 3jQuery(function ($) { 4 5 // const $navbar = $('#bootscore-navbar'); 6 const $offcanvas_navbar = $('#offcanvas-navbar'); 7 const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]'); 8 const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)); 9 const $submenuToggler = $('#bootscore-navbar a.dropdown-toggle'); 10 11 // --- A11y helpers for keyboard navigation in multi-level dropdowns --- 12 const AUTO_CLOSE_OPTS = { autoClose: false }; 13 function bsDropdown(toggleEl) { 14 return bootstrap.Dropdown.getOrCreateInstance(toggleEl, AUTO_CLOSE_OPTS); 15 } 16 function openMenu(toggleEl) { 17 bsDropdown(toggleEl).show(); 18 toggleEl.setAttribute('aria-expanded', 'true'); 19 } 20 function closeMenu(toggleEl) { 21 bsDropdown(toggleEl).hide(); 22 toggleEl.setAttribute('aria-expanded', 'false'); 23 } 24 function getMenu(toggleEl) { 25 return toggleEl?.parentElement?.querySelector(':scope > .dropdown-menu') || null; 26 } 27 function focusFirstItem(menuEl) { 28 const first = menuEl && menuEl.querySelector('.dropdown-item, .dropdown-toggle'); 29 if (first) first.focus(); 30 } 31 function getParentToggleFromItem(itemEl) { 32 const parentMenu = itemEl.closest('ul.dropdown-menu'); 33 const parentLi = parentMenu?.closest('li.dropdown'); 34 return parentLi?.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle') || null; 35 } 36 37 function hideOtherDropdowns() { 38 const $ul = $(this).closest('ul'); 39 // Only hide siblings at the current level 40 $ul.find('> li > .dropdown-toggle').not(this).each(function(){ 41 $(this).removeAttr('data-clicked'); 42 closeMenu(this); 43 }); 44 } 45 46 function hideAllDropdowns() { 47 $('.dropdown-menu.show').dropdown('hide'); 48 } 49 50 /** 51 * This event listener is used to reset the dropdown logic for tablet devices. 52 */ 53 $('.dropdown-toggle').on('click mouseenter', hideOtherDropdowns); 54 55 /** 56 * This event listener is used to reset the dropdown logic when navigating with the keyboard. 57 */ 58 $('.nav-link, .dropdown-item').focus(hideOtherDropdowns); 59 60 /** 61 * This event listener is used to handle the click event on the dropdown toggle in the navbar for tablet devices. 62 */ 63 $(('#bootscore-navbar .dropdown-toggle')).click(function(event) { 64 // Do nothing when on desktop device 65 if (!isTouchDevice()) { 66 window.location.href = $(this).attr('href'); 67 return; 68 } 69 70 // Check if data attribute is true 71 if ($(this).attr('data-clicked')) { 72 event.preventDefault(); 73 window.location.href = $(this).attr('href'); 74 return; 75 } 76 77 event.preventDefault(); 78 $(this).attr('data-clicked', true); 79 // Trigger bootstrap show 80 $(this).dropdown('toggle') 81 }); 82 83 // Comprehensive keyboard handler: Enter/Space/Arrows/Escape 84 $('#bootscore-navbar').on('keydown', 'a.dropdown-toggle, button.dropdown-toggle, a.dropdown-item', function (e) { 85 const el = this; 86 const isToggle = el.classList.contains('dropdown-toggle'); 87 const key = e.key; 88 const submenu = isToggle ? getMenu(el) : null; 89 const hasSubmenu = !!submenu; 90 91 // ENTER / SPACE toggles 92 if ((key === 'Enter' || key === ' ') && isToggle) { 93 e.preventDefault(); 94 e.stopPropagation(); 95 const expanded = el.getAttribute('aria-expanded') === 'true'; 96 if (expanded) { 97 closeMenu(el); 98 } else { 99 openMenu(el); 100 focusFirstItem(getMenu(el)); 101 } 102 return; 103 } 104 105 // SPACE on non-toggle links/items should activate the item, not scroll the page 106 if (key === ' ' && !isToggle) { 107 e.preventDefault(); 108 e.stopPropagation(); 109 // Trigger the default action of the link/button 110 if (typeof el.click === 'function') el.click(); 111 return; 112 } 113 114 // ArrowRight â open submenu ONLY if the focused entry itself has a submenu 115 if (key === 'ArrowRight' && isToggle && hasSubmenu) { 116 e.preventDefault(); 117 openMenu(el); 118 focusFirstItem(submenu); 119 return; 120 } 121 122 // ArrowLeft â ONLY act when the focused entry has its own submenu: close it 123 if (key === 'ArrowLeft' && isToggle && hasSubmenu) { 124 e.preventDefault(); 125 closeMenu(el); 126 el.focus(); 127 return; 128 } 129 130 // ArrowDown / ArrowUp navigate items within open menu 131 if (key === 'ArrowDown' || key === 'ArrowUp') { 132 let menu = null; 133 if (isToggle && el.getAttribute('aria-expanded') === 'true') { 134 menu = getMenu(el); 135 } else { 136 menu = el.closest('ul.dropdown-menu'); 137 } 138 if (menu) { 139 e.preventDefault(); 140 const items = Array.from(menu.querySelectorAll('.dropdown-item, .dropdown-toggle')); 141 const current = document.activeElement; 142 const idx = items.indexOf(current); 143 let nextIdx = idx; 144 if (key === 'ArrowDown') nextIdx = idx === -1 ? 0 : Math.min(items.length - 1, idx + 1); 145 if (key === 'ArrowUp') nextIdx = idx === -1 ? items.length - 1 : Math.max(0, idx
145- 1); 146 items[nextIdx]?.focus(); 147 } 148 return; 149 } 150 151 // ESC close current dropdown contextually (global handler remains) 152 if (key === 'Escape') { 153 const parentToggle = getParentToggleFromItem(el) || (isToggle ? el : null); 154 if (parentToggle) { 155 e.preventDefault(); 156 closeMenu(parentToggle); 157 parentToggle.focus(); 158 } 159 } 160 }); 161 162 $(document).keydown(function(event) { 163 if (event.key === 'Escape') { 164 event.preventDefault(); 165 hideAllDropdowns(); 166 } 167 }); 168 169 // Sync aria-expanded for all dropdowns on mouse-driven open/close as well 170 document.addEventListener('show.bs.dropdown', function (e) { 171 const toggle = e.target.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle'); 172 if (toggle) toggle.setAttribute('aria-expanded', 'true'); 173 }); 174 document.addEventListener('hide.bs.dropdown', function (e) { 175 const toggle = e.target.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle'); 176 if (toggle) toggle.setAttribute('aria-expanded', 'false'); 177 }); 178 179 $offcanvas_navbar.on('hide.bs.offcanvas show.bs.offcanvas hidePrevented.bs.offcanvas', function(event) { 180 // console.log('event 181 182 $('button[data-bs-target="#offcanvas-navbar"]').attr('aria-expanded', event.type === 'show'); 183 }); 184 185 // change the positioning of the dropdown menus in the main navigation if they don't fit into the view 186 // Note: key-driven openings are handled via 'shown.bs.dropdown' below to avoid keyup quirks 187 $submenuToggler.on('click mouseenter', async function(event) { 188 const submenu = $(this).next('.dropdown-menu').get(0); 189 190 if(submenu) { 191 let fallbacks = ['left-start', 'left']; 192 const isSubmenu = $(submenu).hasClass('sub-menu'); 193 194 195 if(!isSubmenu) { 196 fallbacks = ['bottom-end']; 197 } 198 199 const { x, y } = await computePosition(this, submenu, { 200 middleware: [flip({ 201 fallbackPlacements: fallbacks, 202 })], 203 // needs to be right-start, else it positions the submenu with multiple entries centered on the right side 204 placement: isSubmenu ? 'right-start' : 'bottom-start', 205 }); 206 207 // Set the position of the dropdown 208 Object.assign(submenu.style, { 209 left: `${x}px`, 210 top: `${y}px`, 211 }); 212 213 // submenu.style.visibility = 'visible'; 214 } 215 }); 216 217 // Also reposition when dropdowns are opened by keyboard (Enter) or via API 218 $('#bootscore-navbar').on('shown.bs.dropdown', 'a.dropdown-toggle, button.dropdown-toggle', async function () { 219 const submenu = $(this).next('.dropdown-menu').get(0); 220 if (!submenu) return; 221 const isSubmenu = $(submenu).hasClass('sub-menu'); 222 const fallbacks = isSubmenu ? ['left-start', 'left'] : ['bottom-end']; 223 const { x, y } = await computePosition(this, submenu, { 224 middleware: [flip({ fallbackPlacements: fallbacks })], 225 placement: isSubmenu ? 'right-start' : 'bottom-start', 226 }); 227 Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` }); 228 }); 229 230 // Close any open menus that are not part of the current focused branch 231 function closeMenusOutsideBranch(focusedEl) { 232 const openMenus = document.querySelectorAll('#bootscore-navbar .dropdown-menu.show'); 233 openMenus.forEach((menu) => { 234 const li = menu.parentElement; // <li class="dropdown"> 235 // If focus is neither inside this menu nor inside its parent <li> subtree, close it 236 if (!menu.contains(focusedEl) && !li.contains(focusedEl)) { 237 const toggle = li.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle'); 238 if (toggle) { 239 closeMenu(toggle); 240 } 241 } 242 }); 243 } 244 245 // Run the branch-closing logic whenever focus moves within the navbar 246 $('#bootscore-navbar').on('focusin', 'a, button', function () { 247 closeMenusOutsideBranch(this); 248 }); 249 250 // Also guard Tab navigation so stray menus donât remain open 251 $('#bootscore-navbar').on('keydown', function (e) { 252 if (e.key === 'Tab') { 253 // Let focus move first, then close others on the next task 254 setTimeout(() => { 255 closeMenusOutsideBranch(document.activeElement); 256 }, 0); 257 } 258 }); 259 260 function isTouchDevice() { 261 return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; 262 } 263 264 /** 265 * The function that finds the correct anchor link and marks it as active. 266 */ 267 function updateActiveLink() { 268 // Get the current URL's path and hash. 269 // e.g., for "https://example.com/path/page#section", path is "/path/page", hash is "#section" 270 const currentPath = window.location.pathname; 271 const currentHash = window.location.hash; 272 273 // Get all navigation links within the main menu. 274 const navLinks = document.querySelectorAll('#bootscore-navbar a.dropdown-item[href*="#"]'); 275 276 let targetLink = null; 277 278 // Loop through all found navigation links. 279 navLinks.forEach(link => { 280 // First, remove the 'active' class from the link itself. 281 // We will add it back only to the single correct one. 282 link.classList.remove('active'); 283 284 // Check if this link is the correct one to be active. 285 const linkUrl = new URL(link.href); 286 if (linkUrl.pathname === currentPath) { 287 // If the URL has a hash, the link's hash must match exactly. 288 if (currentHash && linkUrl.hash.toLowerCase() === currentHash.toLowerCase()) { 289 targetLink = link; 290 }
291 // If the URL does NOT have a hash, the link must also not have a hash. 292 else if (!currentHash && !linkUrl.hash) { 293 targetLink = link; 294 } 295 } 296 }); 297 298 // If we found the uniquely correct link to activate... 299 if (targetLink) { 300 targetLink.classList.add('active'); 301 } 302 } 303 304 // Run the function once the page has loaded. 305 updateActiveLink(); 306 307 // Also, run the function every time the URL hash changes (e.g., when a user clicks an in-page link). 308 $(window).on('hashchange', updateActiveLink); 309 310 /** 311 * This event listener is used to handle the orientation change and resize events 312 */ 313 $(window).on('orientationchange resize', function () { 314 const $container = $('.wpfd-container'); 315 $container.each(function () { 316 const $container = $(this); 317 handleMenuChange($container); 318 }); 319 }); 320 321 322 /** 323 * This function triggers the automatic column resizing of the media table plugin 324 */ 325 $('.wpfd-container').each( function () { 326 const $container = $(this); 327 handleMenuChange($container); 328 }); 329 330 /** 331 * This function handles the change of the menu in the media table plugin. 332 * @param $container A WPFD-Plugin Container 333 */ 334 function handleMenuChange($container) { 335 const $theadRow = $container.find('table.wpfd-search-result thead > tr'); 336 const $tbodyRow = $container.find('table.wpfd-search-result tbody > tr.file'); 337 let checkedCount = $container.find('.mediaTableMenu input:checked').length; 338 339 $container.find('.optional').removeClass('wpfd-col-hidden').css('display', 'block'); 340 341 $tbodyRow.each(function () { 342 checkedCount = $(this).children('.optional').not('.file_desc').length; 343 $(this).css('grid-template-columns', `repeat(${checkedCount}, 1fr)`); 344 $(this).find('.optional').last().css('padding-right', '10px'); 345 }); 346 347 $theadRow.css('grid-template-columns', `repeat(${checkedCount}, 1fr) 205px`); 348 } 349 350}); // jQuery End
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.