PageSourceSearch

https://www.silverplatters.com/Scripts/public.navigation.js

js silverplatters.com collected 2026-10-02 11:06:41 UTC 23,270 bytes, 723 lines download raw bytes

1/*
2*   Menubar implementation for site navigation using WAI-ARIA 1.2 practices.
3*   Contains keyboard and mouse support.
4*   AHD 07/03/24 - TODO: since this is used for navigation, this should be refactored to use the disclosure pattern.
5*   
6*   @desc menubar       Represents a (usually horizontal) menu bar.
7*   @desc menu          Represents a set of links. Used for flyout/popup menus.
8*   @desc menuitem      An individual menu item. Either an item in a menubar or menu.
9*   
10*   @see                https://www.w3.org/WAI/tutorials/menus/application-menus/
11*                       for the key mapping/typical behavior of top-level menu and
12*                       submenu items that this implementation is based off of.
13*                       
14*   @since              March, 2022
15*/
16
17var popupTimeoutId = 0; // WEBSTORE-2506: global id used for clearing timeouts attached to submenus
18var _showing = false;
19
20/*
21*   @function showDarkness      Shows or hides a darkened background overlay
22*                               on opening or closing a menu.
23*/
24
25function showDarkness(doShow) {
26    var darkness = $('.darkness');
27
28    if (doShow) {
29        if (_showing) {
30            return;
31        }
32
33        _showing = true;
34
35        var d = document.createElement('div');
36        d.className = 'darkness';
37
38        darkness = $(d);
39        darkness.prependTo('.master-wrapper-page');
40        darkness.fadeIn(100);
41    }
42    else {
43        _showing = false;
44        darkness.fadeOut(100);
45        setTimeout(function () {
46            if (!_showing) {
47                darkness.remove();
48            }
49        }, 100);
50    }
51}
52
53/*
54*   @license    This software or document includes material copied from or derived from Navigation Menubar Example.
55*               Copyright © 2022 W3C® (MIT, ERCIM, Keio, Beihang)
56*
57*   @see        https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html
58*               6/28/22: W3 has replaced the above link and it now redirects to a different example.
59*/
60
61var Menubar = function (domNode) {
62    this.isMenubar = true;
63
64    this.domNode = domNode;
65
66    this.menubarItems = [];
67
68    this.firstItem = null;
69    this.lastItem = null;
70
71    this.hasFocus = false;
72    this.hasHover = false;
73};
74
75/*
76*   @function Menubar.prototype.init        Adds ARIA role to the menubar node.
77*                                           Traverse menubar children for focusable elements to 
78*                                           configure an ARIA menu button and populate menubuttons array. 
79*                                           Initialize firstItem and lastItem properties.
80*/
81Menubar.prototype.init = function () {
82    var menubarItem, menuElement;
83    var buttons = this.domNode.querySelectorAll('[class="menu-link"]');
84    var menuBottom = this.domNode.querySelector(".menu-bottom");
85
86    // Add menubar items and the container of dropdown menus to list.
87    for (let i = 0; i < buttons.length; i++) {
88        menuElement = buttons[i];
89
90        menubarItem = new MenubarItem(menuElement, this, menuBottom);
91        menubarItem.init();
92
93        this.menubarItems.push(menubarItem);
94    }
95
96    // Use populated menubuttons array to initialize firstItem and lastItem of menubar.
97    var numItems = this.menubarItems.length;
98    if (numItems > 0) {
99        this.firstItem = this.menubarItems[0];
100        this.lastItem = this.menubarItems[numItems - 1];
101    }
102
103    this.firstItem.domNode.tabIndex = 0;    // allow tabbing to first item on menubar init
104};
105
106/* MENUBAR FOCUS MANAGEMENT METHODS */
107Menubar.prototype.setFocusToItem = function (newItem) {
108    // This is needed for returning to item last focused on when returning to menubar.
109    this.menubarItems.forEach(function (item) {
110        if (item === newItem) {
111            item.domNode.tabIndex = 0;              // allow tabbing on next item with focus
112            newItem.domNode.focus();
113            if (newItem.popupMenu) {
114                newItem.popupMenu.open();
115            }
116        } else {
117            item.domNode.tabIndex = -1;             // disallow tabbing to other menu items
118            if (item.popupMenu) {
119                item.popupMenu.close();
120            }
121        }
122    });
123};
124
125Menubar.prototype.setFocusToFirstItem = function () {
126    this.setFocusToItem(this.firstItem);
127};
128
129Menubar.prototype.setFocusToLastItem = function () {
130    this.setFocusToItem(this.lastItem);
131};
132
133Menubar.prototype.setFocusToPreviousItem = function (currentItem) {
134    var index, newItem;
135
136    if (currentItem === this.firstItem) {
137        newItem = this.lastItem;
138    }
139    else {
140        index = this.menubarItems.indexOf(currentItem);
141        newItem = this.menubarItems[index - 1];
142    }
143
144    this.setFocusToItem(newItem);
145};
146
147Menubar.prototype.setFocusToNextItem = function (currentItem) {
148    var index, newItem;
149    if (currentItem === this.lastItem) {
150        newItem = this.firstItem;
151    }
152    else {
153        index = this.menubarItems.indexOf(currentItem);
154        newItem = this.menubarItems[index + 1];
155    }
156
157    this.setFocusToItem(newItem);
158};
159
160var MenubarItem = function (domNode, menuTopObj, menuBottomObj) {
161    this.menu = menuTopObj; // need a menu top + bottom; webstore menubar uses non-nested components
162    this.menuBottom = menuBottomObj;
163    this.domNode = domNode;
164    this.popupMenu = false;
165
166    this.hasFocus = false;
167    this.hasHover = false;
168
169    this.isMenubarItem = true;
170
171    this.keyCode = Object.freeze({
172        'TAB': 9,
173        'RETURN': 13,
174        'ESC': 27,
175        'SPACE': 32,
176        'PAGEUP': 33,
177        'PAGEDOWN': 34,
178        'END': 35,
179        'HOME': 36,
180        'LEFT': 37,
181        'UP': 38,
182        'RIGHT': 39,
183        'DOWN': 40
184    });
185};
186
187/*
188*   @function MenubarItem.prototype.init        Add domNode event listeners for mouse and keyboard events.
189*                                               Check if menubar item contains a dropdown menu.
190*/
191MenubarItem.prototype.init = function () {
192    this.domNode.tabIndex = -1;
193
194    this.domNode.addEventListener('focus', this.handleFocus.bind(this));
195    this.domNode.addEventListener('blur', this.handleBlur.bind(this));
196    this.domNode.addEventListener('keydown', this.handleKeydown.bind(this));
197
198    this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this));
199    this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this));
200
201    // Initialize any pop up menus
202    if (this.domNode.firstElementChild && (this.domNode.firstElementChild.getAttribute('data-id'))) {
203        var catId = 'mc-' + this.domNode.firstElementChild.getAttribute('data-id');
204        var el = document.getElementById(catId);
205        var hasSubmenu = el.querySelectorAll('.menu-more').length > 0;
206
207        this.popupMenu = new PopupMenu(el, this, hasSubmenu);
208        this.popupMenu.init();
209
210        this.domNode.setAttribute('aria-haspopup', 'true');
211    }
212};
213
214/* MENUBARITEM EVENT HANDLERS */
215MenubarItem.prototype.handleKeydown = function(event) {
216    var flag = false; // flag to stop event propagation
217
218    switch (event.keyCode) {
219    case this.keyCode.SPACE:
220    case this.keyCode.RETURN:
221    case this.keyCode.DOWN:
222        if (this.popupMenu) {
223            this.popupMenu.open();
224            this.popupMenu.setFocusToFirstItem();
225            flag = true;
226        }
227        break;
228
229    case this.keyCode.LEFT:
230        this.menu.setFocusToPreviousItem(this);
231        flag = true;
232        break;
233
234    case this.keyCode.RIGHT:
235        this.menu.setFocusToNextItem(this);
236        flag = true;
237        break;
238
239    case this.keyCode.UP:
240        if (this.popupMenu) {
241            this.popupMenu.open();
242            this.popupMenu.setFocusToLastItem();
243            flag = true;
244        }
245        break;
246
247    case this.keyCode.HOME:
248    case this.keyCode.PAGEUP:
249        this.menu.setFocusToFirstItem();
250        flag = true;
251        break;
252
253    case this.keyCode.END:
254    case this.keyCode.PAGEDOWN:
255        this.menu.setFocusToLastItem();
256        flag = true;
257        break;
258
259    case this.keyCode.TAB:
260    case this.keyCode.ESC:
261        if (this.popupMenu) {
262            this.popupMenu.close(true);
263            showDarkness(false);
264        }
265        break;
266
267    default:
268        break;
269    }
270
271    if (flag) {
272        event.stopPropagation();
273        event.preventDefault();
274    }
275};
276
277MenubarItem.prototype.setExpanded = function (value) {
278    if (value) {
279        this.domNode.setAttribute('aria-expanded', 'true');
280    }
281    else {
282        this.domNode.setAttribute('aria-expanded', 'false');
283    }
284};
285
286MenubarItem.prototype.handleFocus = function (event) {
287    this.menu.hasFocus = true;
288    this.domNode.firstElementChild.className += " menu-item-selected";
289    this.domNode.firstElementChild.setAttribute('active', '1');
290    if (!this.popupMenu) {
291        showDarkness(false);
292    }
293};
294
295MenubarItem.prototype.handleBlur = function (event) {
296    this.menu.hasFocus = false;
297    this.domNode.firstElementChild.className = "menu-item";
298    this.domNode.firstElementChild.removeAttribute('active');
299};
300
301MenubarItem.prototype.handleMouseover = function (event) {
302    this.hasHover = true;
303    if (this.popupMenu) {
304        this.popupMenu.open();
305    }
306    else {
307        this.domNode.firstElementChild.className += " menu-item-selected";
308        this.domNode.firstElementChild.setAttribute('active', '1');
309        if (!_showing) {
310            showDarkness(false);
311        }
312    }
313};
314
315MenubarItem.prototype.handleMouseout = function (event) {
316    this.hasHover = false;
317    if (this.popupMenu) {
318        setTimeout(this.popupMenu.close.bind(this.popupMenu, false), 300);
319    }
320    else {
321        this.domNode.firstElementChild.className = "menu-item";
322        this.domNode.firstElementChild.removeAttribute('active');
323    }
324};
325
326var MenuItem = function (domNode, menuObj, submenuObj = false) {
327    this.domNode = domNode;
328    this.menu = menuObj;
329    this.popupMenu = submenuObj;
330    this.isMenubarItem = false;
331
332    this.keyCode = Object.freeze({
333        'TAB': 9,
334        'RETURN': 13,
335        'ESC': 27,
336        'SPACE': 32,
337        'PAGEUP': 33,
338        'PAGEDOWN': 34,
339        'END': 35,
340        'HOME': 36,
341        'LEFT': 37,
342        'UP': 38,
343        'RIGHT': 39,
344        'DOWN': 40
345    });
346};
347
348/*
349*   @function MenuItem.prototype.init       Add domNode event listeners for mouse and keyboard events.
350*                                           Check if item contains submenu.
351*/
352MenuItem.prototype.init = function () {
353    this.domNode.tabIndex = -1;
354
355    this.domNode.addEventListener('keydown', this.handleKeydown.bind(this));
356    this.domNode.addEventListener('click', this.handleClick.bind(this));
357    this.domNode.addEventListener('focus', this.handleFocus.bind(this));
358    this.domNode.addEventListener('blur', this.handleBlur.bind(this));
359    this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this));
360    this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this));
361
362    if (this.popupMenu) {
363        this.popupMenu = new PopupMenu(this.popupMenu, this);
364        this.popupMenu.init();
365    }
366};
367
368/* MENUITEM EVENT HANDLERS */
369MenuItem.prototype.handleKeydown = function (event) {
370    var tgt = event.currentTarget,
371        flag = false,
372        clickEvent;
373
374    switch (event.keyCode) {
375    case this.keyCode.SPACE:
376    case this.keyCode.RETURN:
377        if (this.popupMenu) {
378            this.popupMenu.open();
379            this.popupMenu.setFocusToFirstItem();
380        }
381        else {
382            // Create simulated mouse event to mimic the behavior of assistive technologies
383            // and let the event handler handleClick do the housekeeping.
384            try {
385                clickEvent = new MouseEvent('click', {
386                        'view': window,
387                        'bubbles': true,
388                        'cancelable': true
389                    });
390            }
391            catch (err) {
392                if (document.createEvent) {
393                    // DOM Level 3 for IE 9+
394                    clickEvent = document.createEvent('MouseEvents');
395                    clickEvent.initEvent('click', true, true);
396                }
397            }
398            tgt.dispatchEvent(clickEvent);
399        }
400        flag = true;
401        break;
402
403    case this.keyCode.UP:
404        this.menu.setFocusToPreviousItem(this);
405        flag = true;
406        break;
407
408    case this.keyCode.DOWN:
409        this.menu.setFocusToNextItem(this);
410        flag = true;
411        break;
412
413    case this.keyCode.LEFT:
414        this.menu.setFocusToController('previous', true);
415        this.menu.close(true);
416        flag = true;
417        break;
418
419    case this.keyCode.RIGHT:
420        if (this.popupMenu) {
421            this.popupMenu.open();
422            this.popupMenu.setFocusToFirstItem();
423        }
424        else {
425            this.menu.setFocusToController('next', true);
426            this.menu.close(true);
427        }
428        flag = true;
429        break;
430
431    case this.keyCode.HOME:
432    case this.keyCode.PAGEUP:
433        this.menu.setFocusToFirstItem();
434        flag = true;
435        break;
436
437    case this.keyCode.END:
438    case this.keyCode.PAGEDOWN:
439        this.menu.setFocusToLastItem();
440        flag = true;
441        break;
442
443    case this.keyCode.ESC:
444        this.menu.setFocusToController();
445        this.menu.close(true);
446        showDarkness(false);
447        flag = true;
448        break;
449
450    case this.keyCode.TAB:
451        this.menu.setFocusToController();
452        showDarkness(false);
453        break;
454
455    default:
456        break;
457    }
458
459    if (flag) {
460        event.stopPropagation();
461        event.preventDefault();
462    }
463};
464
465MenuItem.prototype.setExpanded = function (value) {
466    if (value) {
467        this.domNode.setAttribute('aria-expanded', 'true');
468    }
469    else {
470        this.domNode.setAttribute('aria-expanded', 'false');
471    }
472};
473
474MenuItem.prototype.handleClick = function (event) {
475    this.menu.setFocusToController();
476    this.menu.close(true);
477};
478
479MenuItem.prototype.handleFocus = function (event) {
480    this.menu.hasFocus = true;
481};
482
483MenuItem.prototype.handleBlur = function (event) {
484    this.menu.hasFocus = false;
485    setTimeout(this.menu.close.bind(this.menu, false), 100);
486};
487
488MenuItem.prototype.handleMouseover = function (event) {
489    this.menu.hasHover = true;
490    this.menu.open();
491    if (this.popupMenu) {
492        this.popupMenu.hasHover = true;
493        this.popupMenu.open();
494    }
495};
496
497MenuItem.prototype.handleMouseout = function (event) {
498    if (this.popupMenu) {
499        this.popupMenu.hasHover = false;
500        popupTimeoutId = setTimeout(this.popupMenu.close.bind(this.popupMenu, true));
501    }
502
503    this.menu.hasHover = false;
504    setTimeout(this.menu.close.bind(this.menu, false), 300);
505};
506
507
508var PopupMenu = function (domNode, controllerObj, hasSubmenu) {
509    this.hasSubmenu = hasSubmenu;
510
511    this.domNode = domNode;
512    this.controller = controllerObj;
513
514    this.isSubmenu = !this.controller.isMenubarItem && this.controller.popupMenu;   // if my controller is not a menubar item
515                                                                                    // and has a popup, then i am a submenu
516    this.menuitems = [];
517
518    this.firstItem = null;
519    this.lastItem = null;
520
521    this.hasFocus = false;
522    this.hasHover = false;
523};
524
525/*
526*   @function PopupMenu.prototype.init      Add domNode event listeners for mouseover and mouseout. 
527*                                           Traverse domNode children to configure each menuitem 
528*                                           and populate menuitems array. 
529*                                           Initialize firstItem and lastItem properties.
530*/
531PopupMenu.prototype.init = function () {
532    var menuElement, menuItem;
533
534    this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this));
535    this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this));
536
537    var childElements = this.hasSubmenu     // different elements to grab depending on if this is a submenu
538        ? this.domNode.querySelectorAll('li[class="menu-child-item"] a[href]:not([disabled])')
539        : this.domNode.querySelectorAll('a[href]:not([disabled])');
540
541    // Traverse the element children of domNode: configure each with
542    // menuitem role behavior and store reference in menuitems array.
543    for (let j = 0; j < childElements.length; j++) {
544        menuElement = childElements[j];
545
546        if (menuElement.firstElementChild && menuElement.firstElementChild.classList.contains('menu-more')) {
547            // indicates menuitem opens a submenu, pass in submenu
548            var submenuId = 'msc-' + menuElement.parentNode.getAttribute('data-id');
549            var subMenu = document.getElementById(submenuId);
550
551            menuItem = new MenuItem(menuElement, this, subMenu);
552        } else {
553            // menu item is just a link
554            menuItem = new MenuItem(menuElement, this);
555        }
556        menuItem.init();
557        this.menuitems.push(menuItem);
558    }
559
560    // Use populated menuitems array to initialize firstItem and lastItem.
561    var numItems = this.menuitems.length;
562    if (numItems > 0) {
563        this.firstItem = this.menuitems[0];
564        this.lastItem = this.menuitems[numItems - 1];
565    }
566};
567
568/* POPUPMENU EVENT HANDLERS */
569
570PopupMenu.prototype.handleMouseover = function (event) {
571    this.hasHover = true;
572    if (this.isSubmenu) {               // a parent popup exists! do not close or this will close too.
573        clearTimeout(popupTimeoutId);   // clear the timer for closing the parent popup.
574    }
575};
576
577PopupMenu.prototype.handleMouseout = function (event) {
578    this.hasHover = false;
579    setTimeout(this.close.bind(this, false), 1);
580};
581
582/* POPUPMENU FOCUS MANAGEMENT METHODS */
583PopupMenu.prototype.setFocusToController = function (command, flag) {
584    // controller can be menu item or menubar item
585    if (typeof command !== 'string') {
586        command = '';
587    }
588
589    function setFocusToMenubarItem (controller, close) {
590        // if only one popup is open, then controller is a menubar item
591        // otherwise, keep closing popups until the controller is a menubar item
592        while (controller) {
593            if (controller.isMenubarItem) {
594                controller.domNode.focus();
595                return controller;
596            }
597            else {
598                if (close) {
599                    controller.menu.close(true);
600                }
601                controller.hasFocus = false;
602            }
603            controller = controller.menu.controller;
604        }
605        return false;
606    }
607
608    if (command === '') {
609        if (this.controller && this.controller.domNode) {
610            this.controller.domNode.focus();
611        }
612        return;
613    }
614
615    if (!this.controller.isMenubarItem) {
616        // a submenu is open: move focus to controller (menu item) & close current popup
617        this.controller.domNode.focus();
618        this.close();
619
620        if (command === 'next') {
621            // close ALL popups and move focus to next menubar item
622            var menubarItem = setFocusToMenubarItem(this.controller, false);
623            if (menubarItem) {
624                menubarItem.menu.setFocusToNextItem(menubarItem, flag);
625            }
626        }
627    }
628    else {
629        // want to move to previous or next menubar item
630        if (command === 'previous') {
631            this.controller.menu.setFocusToPreviousItem(this.controller, flag);
632        }
633        else if (command === 'next') {
634            this.controller.menu.setFocusToNextItem(this.controller, flag);
635        }
636    }
637};
638
639PopupMenu.prototype.setFocusToFirstItem = function () {
640    this.firstItem.domNode.focus();
641};
642
643PopupMenu.prototype.setFocusToLastItem = function () {
644    this.lastItem.domNode.focus();
645};
646
647PopupMenu.prototype.setFocusToPreviousItem = function (currentItem) {
648    if (currentItem === this.firstItem) {
649        this.lastItem.domNode.focus();
650    }
651    else {
652        var index = this.menuitems.indexOf(currentItem);
653        this.menuitems[index - 1].domNode.focus();
654    }
655};
656
657PopupMenu.prototype.setFocusToNextItem = function (currentItem) {
658    if (currentItem === this.lastItem) {
659        this.firstItem.domNode.focus();
660    }
661    else {
662        var index = this.menuitems.indexOf(currentItem);
663        this.menuitems[index + 1].domNode.focus();
664    }
665};
666
667/* POPUPMENU DISPLAY METHODS */
668PopupMenu.prototype.open = function () {
669    // Set CSS properties
670    if (this.controller.isMenubarItem) {
671        var menuItem = this.controller.domNode.firstElementChild;
672
673        // Menu should drop down below menubar
674        menuItem.className = "menu-item menu-item-selected";
675        menuItem.setAttribute('active', '1');
676        this.domNode.style.display = 'block';
677        this.domNode.style.zIndex = 100;
678        this.domNode.style.position = 'absolute';
679        this.controller.menuBottom.style.left = menuItem.offsetLeft + 'px';
680    }
681    else {
682        // Submenu should appear to right of menu item
683        this.domNode.parentNode.style.position = 'relative';
684        this.domNode.style.display = 'block';
685        this.domNode.style.position = 'absolute';
686        this.domNode.style.top = this.controller.domNode.offsetTop + 'px';
687        this.domNode.style.zIndex = 100;
688    }
689
690    this.controller.setExpanded(true);
691    showDarkness(true);
692};
693
694PopupMenu.prototype.close = function (force) {
695    var menuItem = this.controller.domNode.firstElementChild;
696    var controllerHasHover = this.controller.hasHover;
697    var hasFocus = this.hasFocus;
698
699    // Iterate thru items in a menu and check if their popups have focus.
700    // This is needed to keep a submenu open if moving onto it with keypress.
701    for (var i = 0; i < this.menuitems.length; i++) {
702        var mi = this.menuitems[i];
703        if (mi.popupMenu) {
704            hasFocus = hasFocus | mi.popupMenu.hasFocus;
705        }
706    }
707
708    if (!this.controller.isMenubarItem) {
709        controllerHasHover = false;
710    }
711
712    if (force || (!hasFocus && !this.hasHover && !controllerHasHover)) {
713        this.domNode.style.display = 'none';
714        this.domNode.style.zIndex = 0;
715
716        menuItem.className = "menu-item";
717        menuItem.removeAttribute('active');
718
719        this.controller.setExpanded(false);
720        showDarkness(false);
721    }
722};
723

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.