PageSourceSearch

https://dv-ciam.doubleverify.com/resources/c6893/login/pinnacle-v2/js/menu-button-links.js

js doubleverify.com collected 2026-09-24 07:26:23 UTC 7,518 bytes, 315 lines download raw bytes

1// @ts-check
2/*
3 *   This content is licensed according to the W3C Software License at
4 *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
5 *
6 *   File:   menu-button-links.js
7 *
8 *   Desc:   Creates a menu button that opens a menu of links
9 *
10 *   Modified by Peter Keuter to adhere to the coding standards of Keycloak
11 *   Original file: https://www.w3.org/WAI/content-assets/wai-aria-practices/patterns/menu-button/examples/js/menu-button-links.js
12 *   Source: https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html
13 */
14
15class MenuButtonLinks {
16  constructor(domNode) {
17    this.domNode = domNode;
18    this.buttonNode = domNode.querySelector("button");
19    this.menuNode = domNode.querySelector('[role="menu"]');
20    this.menuitemNodes = [];
21    this.firstMenuitem = false;
22    this.lastMenuitem = false;
23    this.firstChars = [];
24
25    this.buttonNode.addEventListener("keydown", (e) => this.onButtonKeydown(e));
26    this.buttonNode.addEventListener("click", (e) => this.onButtonClick(e));
27
28    const nodes = domNode.querySelectorAll('[role="menuitem"]');
29
30    for (const menuitem of nodes) {
31      this.menuitemNodes.push(menuitem);
32      menuitem.tabIndex = -1;
33      this.firstChars.push(menuitem.textContent.trim()[0].toLowerCase());
34
35      menuitem.addEventListener("keydown", (e) => this.onMenuitemKeydown(e));
36
37      menuitem.addEventListener("mouseover", (e) =>
38        this.onMenuitemMouseover(e)
39      );
40
41      if (!this.firstMenuitem) {
42        this.firstMenuitem = menuitem;
43      }
44      this.lastMenuitem = menuitem;
45    }
46
47    domNode.addEventListener("focusin", () => this.onFocusin());
48    domNode.addEventListener("focusout", () => this.onFocusout());
49
50    window.addEventListener(
51      "mousedown",
52      (e) => this.onBackgroundMousedown(e),
53      true
54    );
55  }
56
57  setFocusToMenuitem = (newMenuitem) =>
58    this.menuitemNodes.forEach((item) => {
59      if (item === newMenuitem) {
60        item.tabIndex = 0;
61        newMenuitem.focus();
62      } else {
63        item.tabIndex = -1;
64      }
65    });
66
67  setFocusToFirstMenuitem = () => this.setFocusToMenuitem(this.firstMenuitem);
68
69  setFocusToLastMenuitem = () => this.setFocusToMenuitem(this.lastMenuitem);
70
71  setFocusToPreviousMenuitem = (currentMenuitem) => {
72    let newMenuitem, index;
73
74    if (currentMenuitem === this.firstMenuitem) {
75      newMenuitem = this.lastMenuitem;
76    } else {
77      index = this.menuitemNodes.indexOf(currentMenuitem);
78      newMenuitem = this.menuitemNodes[index - 1];
79    }
80
81    this.setFocusToMenuitem(newMenuitem);
82
83    return newMenuitem;
84  };
85
86  setFocusToNextMenuitem = (currentMenuitem) => {
87    let newMenuitem, index;
88
89    if (currentMenuitem === this.lastMenuitem) {
90      newMenuitem = this.firstMenuitem;
91    } else {
92      index = this.menuitemNodes.indexOf(currentMenuitem);
93      newMenuitem = this.menuitemNodes[index + 1];
94    }
95    this.setFocusToMenuitem(newMenuitem);
96
97    return newMenuitem;
98  };
99
100  setFocusByFirstCharacter = (currentMenuitem, char) => {
101    let start, index;
102
103    if (char.length > 1) {
104      return;
105    }
106
107    char = char.toLowerCase();
108
109    // Get start index for search based on position of currentItem
110    start = this.menuitemNodes.indexOf(currentMenuitem) + 1;
111    if (start >= this.menuitemNodes.length) {
112      start = 0;
113    }
114
115    // Check remaining slots in the menu
116    index = this.firstChars.indexOf(char, start);
117
118    // If not found in remaining slots, check from beginning
119    if (index === -1) {
120      index = this.firstChars.indexOf(char, 0);
121    }
122
123    // If match was found...
124    if (index > -1) {
125      this.setFocusToMenuitem(this.menuitemNodes[index]);
126    }
127  };
128
129  // Utilities
130
131  getIndexFirstChars = (startIndex, char) => {
132    for (let i = startIndex; i < this.firstChars.length; i++) {
133      if (char === this.firstChars[i]) {
134        return i;
135      }
136    }
137    return -1;
138  };
139
140  // Popup menu methods
141
142  openPopup = () => {
143    this.menuNode.style.display = "block";
144    this.buttonNode.setAttribute("aria-expanded", "true");
145  };
146
147  closePopup = () => {
148    if (this.isOpen()) {
149      this.buttonNode.setAttribute("aria-expanded", "false");
150      this.menuNode.style.removeProperty("display");
151    }
152  };
153
154  isOpen = () => {
155    return this.buttonNode.getAttribute("aria-expanded") === "true";
156  };
157
158  // Menu event handlers
159
160  onFocusin = () => {
161    this.domNode.classList.add("focus");
162  };
163
164  onFocusout = () => {
165    this.domNode.classList.remove("focus");
166  };
167
168  onButtonKeydown = (event) => {
169    const key = event.key;
170    let flag = false;
171
172    switch (key) {
173      case " ":
174      case "Enter":
175      case "ArrowDown":
176      case "Down":
177        this.openPopup();
178        this.setFocusToFirstMenuitem();
179        flag = true;
180        break;
181
182      case "Esc":
183      case "Escape":
184        this.closePopup();
185        this.buttonNode.focus();
186        flag = true;
187        break;
188
189      case "Up":
190      case "ArrowUp":
191        this.openPopup();
192        this.setFocusToLastMenuitem();
193        flag = true;
194        break;
195
196      default:
197        break;
198    }
199
200    if (flag) {
201      event.stopPropagation();
202      event.preventDefault();
203    }
204  };
205
206  onButtonClick(event) {
207    if (this.isOpen()) {
208      this.closePopup();
209      this.buttonNode.focus();
210    } else {
211      this.openPopup();
212      this.setFocusToFirstMenuitem();
213    }
214
215    event.stopPropagation();
216    event.preventDefault();
217  }
218
219  onMenuitemKeydown(event) {
220    const tgt = event.currentTarget;
221    const key = event.key;
222    let flag = false;
223
224    const isPrintableCharacter = (str) => str.length === 1 && str.match(/\S/);
225
226    if (event.ctrlKey || event.altKey || event.metaKey) {
227      return;
228    }
229
230    if (event.shiftKey) {
231      if (isPrintableCharacter(key)) {
232        this.setFocusByFirstCharacter(tgt, key);
233        flag = true;
234      }
235
236      if (event.key === "Tab") {
237        this.buttonNode.focus();
238        this.closePopup();
239        flag = true;
240      }
241    } else {
242      switch (key) {
243        case " ":
244          window.location.href = tgt.href;
245          break;
246
247        case "Esc":
248        case "Escape":
249          this.closePopup();
250          this.buttonNode.focus();
251          flag = true;
252          break;
253
254        case "Up":
255        case "ArrowUp":
256          this.setFocusToPreviousMenuitem(tgt);
257          flag = true;
258          break;
259
260        case "ArrowDown":
261        case "Down":
262          this.setFocusToNextMenuitem(tgt);
263          flag = true;
264          break;
265
266        case "Home":
267        case "PageUp":
268          this.setFocusToFirstMenuitem();
269          flag = true;
270          break;
271
272        case "End":
273        case "PageDown":
274          this.setFocusToLastMenuitem();
275          flag = true;
276          break;
277
278        case "Tab":
279          this.closePopup();
280          break;
281
282        default:
283          if (isPrintableCharacter(key)) {
284            this.setFocusByFirstCharacter(tgt, key);
285            flag = true;
286          }
287          break;
288      }
289    }
290
291    if (flag) {
292      event.stopPropagation();
293      event.preventDefault();
294    }
295  }
296
297  onMenuitemMouseover(event) {
298    const tgt = event.currentTarget;
299    tgt.focus();
300  }
301
302  onBackgroundMousedown(event) {
303    if (!this.domNode.contains(event.target)) {
304      if (this.isOpen()) {
305        this.closePopup();
306        this.buttonNode.focus();
307      }
308    }
309  }
310}
311
312const menuButtons = document.querySelectorAll(".menu-button-links");
313for (const button of menuButtons) {
314  new MenuButtonLinks(button);
315}

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.