1(function (Drupal, $, once) { 2 "use strict"; 3 4 function ensureId($el, prefix) { 5 if (!$el.attr('id')) { 6 $el.attr('id', (prefix || 'sf-') + Math.random().toString(36).slice(2)); 7 } 8 return $el.attr('id'); 9 } 10 11 Drupal.behaviors.sfNoFocusOpen = { 12 attach(context) { 13 $(once('sf-no-focus-open', '.sf-menu', context)).each(function () { 14 const $menu = $(this); 15 16 const closeSiblings = ($li) => { 17 $li.siblings('.menuparent').each(function () { 18 $(this).data('sf-close')?.(); 19 }); 20 }; 21 22 $menu.find('li.menuparent').each(function () { 23 const $li = $(this); 24 const $sub = $li.children('ul').first(); 25 if (!$sub.length) return; 26 27 const $trigger = $li.children('a, span.sf-with-ul, span.nolink').first(); 28 if (!$trigger.length) return; 29 30 if ($trigger.is('span')) { 31 $trigger.attr({ tabindex: '0', role: 'button' }); 32 } 33 34 // IDs for relationships 35 const subId = ensureId($sub, 'sf-sub-'); 36 const trigId = ensureId($trigger, 'sf-trig-'); 37 38 // Baseline ARIA 39 $li.attr({ 'aria-haspopup': 'true', 'aria-expanded': 'false' }); 40 $trigger.attr({ 41 'aria-haspopup': 'true', 42 'aria-expanded': 'false', 43 'aria-controls': subId, 44 'id': trigId 45 }); 46 47 // submenu: aria-labelledby + aria-expanded mirror + aria-hidden 48 $sub 49 .attr({ 50 'id': subId, 51 'aria-labelledby': trigId, 52 'aria-hidden': 'true', 53 'aria-expanded': 'false' // <-- mirror state here per your ask 54 }) 55 .addClass('sf-hidden') 56 .hide(); 57 58 // helpers 59 const close = () => { 60 $li.removeClass('sfHover'); 61 $sub.addClass('sf-hidden').hide() 62 .attr({'aria-hidden': 'true', 'aria-expanded': 'false'}); 63 $li.attr('aria-expanded', 'false'); 64 $trigger.attr('aria-expanded', 'false'); 65 }; 66 const open = () => { 67 closeSiblings($li); 68 $li.addClass('sfHover'); 69 $sub.removeClass('sf-hidden').show() 70 .attr({'aria-hidden': 'false', 'aria-expanded': 'true'}); 71 $li.attr('aria-expanded', 'true'); 72 $trigger.attr('aria-expanded', 'true'); 73 }; 74 75 $li.data('sf-close', close); 76 close(); 77 78 // prevent open-on-focus 79 $trigger.on('focusin', function (e) { 80 e.stopImmediatePropagation(); 81 setTimeout(close, 0); 82 }); 83 84 // click/tap 85 $trigger.on('click', function (e) { 86 if ($trigger.is('span') || $trigger.attr('href') === '#' || $trigger.hasClass('sf-with-ul')) { 87 e.preventDefault(); 88 ($trigger.attr('aria-expanded') === 'true' ? close() : open()); 89 } 90 }); 91 92 // hover sync 93 $li.on('mouseenter', open).on('mouseleave', close); 94 95 // keyboard 96 $trigger.on('keydown', function (e) { 97 const k = e.key; 98 if (k === 'Enter' || k === ' ') { 99 e.preventDefault(); 100 ($trigger.attr('aria-expanded') === 'true' ? close() : open()); 101 } else if (k === 'ArrowDown') { 102 e.preventDefault(); 103 if ($trigger.attr('aria-expanded') !== 'true') open(); 104 const $first = $sub.find('a,button,[tabindex]:not([tabindex="-1"])').filter(':visible').first(); 105 if ($first.length) $first.focus(); 106 } else if (k === 'Escape') { 107 e.preventDefault(); 108 close(); 109 $trigger.focus(); 110 } else if (k === 'ArrowLeft') { 111 const $prev = $li.prevAll('li:visible').first().children('a,span').first(); 112 if ($prev.length) { e.preventDefault(); $prev.focus(); } 113 } else if (k === 'ArrowRight') { 114 const $next = $li.nextAll('li:visible').first().children('a,span').first(); 115 if ($next.length) { e.preventDefault(); $next.focus(); } 116 } 117 }); 118 119 // close when leaving subtree 120 $li.on('focusout', function (e) { 121 const next = e.relatedTarget; 122 if (!next || !$.contains($li.get(0), next)) close(); 123 }); 124 }); 125 }); 126 } 127 }; 128})(Drupal, jQuery, once);
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.