PageSourceSearch

https://www.limesurvey.org/templates/limesurvey/js/menu-accessibility.js?1d843c

js limesurvey.org collected 2026-10-01 08:42:51 UTC 7,902 bytes, 272 lines download raw bytes

1document.addEventListener("DOMContentLoaded", () => {
2  /**
3   * --- Configuration & Selectors ---
4   */
5  const SELECTORS = {
6    NAV_LINKS: '.t4-megamenu .nav-link, .t4-megamenu .dropdown-toggle, .t4-megamenu .dropdown-item',
7    LEVEL0: [
8      '.t4-header-inner .t4-megamenu .navbar-nav.level0 > .nav-item > .nav-link:visible',
9      '.t4-header-inner .navbar-brand.logo-image > a:visible',
10      '.mod-languages .langChoose:visible',
11    ].join(','),
12    DROPDOWN_LINKS: '.t4-megamenu .dropdown-menu a',
13    DROPDOWN_LINKS_VISIBLE: '.t4-megamenu .dropdown-menu a:visible',
14    LEVEL1_DROPDOWNS: '.t4-megamenu .nav-item[data-level="1"] > .dropdown-menu',
15    LANG_MODULE: '.mod-languages',
16  };
17
18  const KEYS = {
19    ARROW_RIGHT: 'ArrowRight',
20    ARROW_LEFT: 'ArrowLeft',
21    ARROW_UP: 'ArrowUp',
22    ARROW_DOWN: 'ArrowDown',
23    ESCAPE: 'Escape',
24    ENTER: 'Enter',
25    SPACE: ' '
26  };
27
28  /**
29   * --- Helper Functions ---
30   */
31
32  const focusItem = ($items, index) => {
33    if (index >= 0 && index < $items.length) {
34      $items.eq(index).focus();
35    }
36  };
37
38  const getMenuItems = ($container) => $container.find('.dropdown-menu a').filter(':visible');
39
40  const getFirstMenuItem = ($container) => getMenuItems($container).first();
41
42  /**
43   * --- Logic & Initialization ---
44   */
45
46  const ensureKeyboardAccessible = () => {
47    $(SELECTORS.NAV_LINKS).each((_, el) => {
48      const $el = $(el);
49      if ($el.is('a')) {
50        const href = $el.attr('href');
51        const isPlaceholder = href === '#' || href === 'javascript:;' || !href || (href.indexOf('#') === 0 && href.length === 1);
52
53        if (isPlaceholder && !$el.attr('tabindex')) {
54          $el.attr('tabindex', '0');
55        }
56      }
57    });
58  };
59
60  const setupLevel0Handlers = () => {
61    const $level0 = $(SELECTORS.LEVEL0);
62    const isRtl = document.documentElement.dir === "rtl" || $("html").attr("dir") === "rtl";
63
64    $level0.off('keydown.level0').on('keydown.level0', (event) => {
65      const $current = $(event.currentTarget);
66      const currentIndex = $level0.index($current);
67
68      switch (event.key) {
69        case KEYS.ARROW_RIGHT:
70          focusItem($level0, isRtl ? currentIndex - 1 : currentIndex + 1);
71          break;
72
73        case KEYS.ARROW_LEFT:
74          focusItem($level0, isRtl ? currentIndex + 1 : currentIndex - 1);
75          break;
76
77        case KEYS.ARROW_UP:
78          event.preventDefault();
79          event.stopPropagation();
80          break;
81
82        case KEYS.ARROW_DOWN:
83        case KEYS.SPACE:
84          event.preventDefault();
85          event.stopPropagation();
86
87          if ($current.hasClass('dropdown-toggle')) {
88            const $megaItem = $current.parent();
89            const focusFirst = () => {
90              const $firstChild = getFirstMenuItem($megaItem);
91              if ($firstChild.length) $firstChild.focus();
92            };
93
94            if ($megaItem.hasClass('show')) {
95              focusFirst();
96            } else if (typeof T4MegaMenu !== 'undefined') {
97              T4MegaMenu.showDropdown($megaItem);
98              setTimeout(focusFirst, 150);
99            }
100          }
101          break;
102      }
103    });
104  };
105
106  const setupDropdownHandlers = () => {
107    $(SELECTORS.DROPDOWN_LINKS).off('keydown.inside-dropdown').on('keydown.inside-dropdown', (event) => {
108      const $current = $(event.currentTarget);
109      const $visibleLinks = $(SELECTORS.DROPDOWN_LINKS_VISIBLE);
110      const currentIndex = $visibleLinks.index($current);
111
112      switch (event.key) {
113        case KEYS.ARROW_DOWN:
114          event.preventDefault();
115          event.stopPropagation();
116          focusItem($visibleLinks, currentIndex + 1);
117          break;
118
119        case KEYS.ARROW_UP:
120          event.preventDefault();
121          event.stopPropagation();
122          focusItem($visibleLinks, currentIndex - 1);
123          break;
124
125        case KEYS.ESCAPE: {
126          event.preventDefault();
127          event.stopPropagation();
128          const $navItem = $current.parents('.nav-item[data-level="1"]').last();
129          if ($navItem.length) $navItem.children('.dropdown-toggle').focus();
130          break;
131        }
132
133        case KEYS.SPACE:
134          event.preventDefault();
135          event.stopPropagation();
136          break;
137      }
138    });
139
140    $(SELECTORS.LEVEL1_DROPDOWNS).off('focusout.outside-dropdown').on('focusout.outside-dropdown', (event) => {
141      setTimeout(() => {
142        const $currentDropdown = $(event.currentTarget);
143        if (!$(':focus').closest($currentDropdown).length) {
144          if (typeof T4MegaMenu !== 'undefined') {
145            T4MegaMenu.hideDropdowns($currentDropdown.parent());
146          }
147        }
148      }, 100);
149    });
150  };
151
152  const setupLanguageHandlers = () => {
153    const $langModules = $(SELECTORS.LANG_MODULE);
154    if (!$langModules.length) return;
155
156    $langModules.each((_, module) => {
157      const $langModule = $(module);
158      const $btn = $langModule.find('.langChoose').first();
159      const $menu = $langModule.find('.lang-block, .lang-inline').first();
160      const $links = $menu.find('a');
161      let showTimer = null;
162      let hideTimer = null;
163
164      if (!$btn.length || !$menu.length) return;
165
166      $menu.hide();
167
168      const clearTimers = () => {
169        if (showTimer) {
170          clearTimeout(showTimer);
171          showTimer = null;
172        }
173        if (hideTimer) {
174          clearTimeout(hideTimer);
175          hideTimer = null;
176        }
177      };
178
179      const isFocusInsideModule = () => $(document.activeElement).closest($langModule).length > 0;
180
181      const showMenu = (focusFirst) => {
182        clearTimers();
183        $menu.show();
184
185        showTimer = setTimeout(() => {
186          $menu.addClass('displayed');
187
188          if (focusFirst) {
189            $links.first().focus();
190          }
191        }, 100);
192      };
193
194      const hideMenu = (focusBack) => {
195        clearTimers();
196        $menu.removeClass('displayed');
197
198        if (focusBack) {
199          $btn.focus();
200        }
201
202        hideTimer = setTimeout(() => {
203          $menu.hide();
204        }, 100);
205      };
206
207      $langModule.off('mouseenter.language-dropdown').on('mouseenter.language-dropdown', () => {
208        showMenu();
209      });
210
211      $langModule.off('mouseleave.language-dropdown').on('mouseleave.language-dropdown', () => {
212        // Keep menu open while keyboard focus is still inside the language module.
213        if (!isFocusInsideModule()) {
214          hideMenu();
215        }
216      });
217
218      $langModule.off('focusout.language-dropdown').on('focusout.language-dropdown', () => {
219        setTimeout(() => {
220          if (!isFocusInsideModule()) {
221            hideMenu(false);
222          }
223        }, 0);
224      });
225
226      $btn.off('keydown.language-dropdown').on('keydown.language-dropdown', (event) => {
227        if (event.key === KEYS.ARROW_DOWN || event.key === KEYS.SPACE || event.key === KEYS.ENTER) {
228          event.preventDefault();
229          event.stopPropagation();
230          showMenu(true);
231        }
232      });
233
234      $links.off('keydown.lang-link').on('keydown.lang-link', (event) => {
235        const $current = $(event.currentTarget);
236        const currentIndex = $links.index($current);
237
238        switch (event.key) {
239          case KEYS.ARROW_DOWN:
240            event.preventDefault();
241            event.stopPropagation();
242            focusItem($links, currentIndex + 1);
243            break;
244
245          case KEYS.ARROW_UP:
246            event.preventDefault();
247            event.stopPropagation();
248            focusItem($links, currentIndex - 1);
249            break;
250
251          case KEYS.ESCAPE:
252            hideMenu(true);
253            break;
254
255          case KEYS.SPACE:
256            event.preventDefault();
257            event.stopPropagation();
258            break;
259        }
260      });
261    });
262  };
263
264  const init = () => {
265    setupDropdownHandlers();
266    setupLanguageHandlers();
267    ensureKeyboardAccessible();
268    setupLevel0Handlers();
269  };
270
271  init();
272});

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.