1(function () { 2 'use strict'; 3 4 // Key map 5 const ENTER = 13; 6 const ESCAPE = 27; 7 8 function toggleNavigation(toggle, menu) { 9 const isExpanded = menu.getAttribute("aria-expanded") === "true"; 10 menu.setAttribute("aria-expanded", !isExpanded); 11 toggle.setAttribute("aria-expanded", !isExpanded); 12 } 13 14 function closeNavigation(toggle, menu) { 15 menu.setAttribute("aria-expanded", false); 16 toggle.setAttribute("aria-expanded", false); 17 toggle.focus(); 18 } 19 20 // Navigation 21 22 window.addEventListener("DOMContentLoaded", () => { 23 const menuButton = document.querySelector(".header .menu-button-mobile"); 24 const menuList = document.querySelector("#user-nav-mobile"); 25 26 menuButton.addEventListener("click", (event) => { 27 event.stopPropagation(); 28 toggleNavigation(menuButton, menuList); 29 }); 30 31 menuList.addEventListener("keyup", (event) => { 32 if (event.keyCode === ESCAPE) { 33 event.stopPropagation(); 34 closeNavigation(menuButton, menuList); 35 } 36 }); 37 38 // Toggles expanded aria to collapsible elements 39 const collapsible = document.querySelectorAll( 40 ".collapsible-nav, .collapsible-sidebar" 41 ); 42 43 collapsible.forEach((element) => { 44 const toggle = element.querySelector( 45 ".collapsible-nav-toggle, .collapsible-sidebar-toggle" 46 ); 47 48 element.addEventListener("click", () => { 49 toggleNavigation(toggle, element); 50 }); 51 52 element.addEventListener("keyup", (event) => { 53 console.log("escape"); 54 if (event.keyCode === ESCAPE) { 55 closeNavigation(toggle, element); 56 } 57 }); 58 }); 59 60 // If multibrand search has more than 5 help centers or categories collapse the list 61 const multibrandFilterLists = document.querySelectorAll( 62 ".multibrand-filter-list" 63 ); 64 multibrandFilterLists.forEach((filter) => { 65 if (filter.children.length > 6) { 66 // Display the show more button 67 const trigger = filter.querySelector(".see-all-filters"); 68 trigger.setAttribute("aria-hidden", false); 69 70 // Add event handler for click 71 trigger.addEventListener("click", (event) => { 72 event.stopPropagation(); 73 trigger.parentNode.removeChild(trigger); 74 filter.classList.remove("multibrand-filter-list--collapsed"); 75 }); 76 } 77 }); 78 }); 79 80 const isPrintableChar = (str) => { 81 return str.length === 1 && str.match(/^\S$/); 82 }; 83 84 function Dropdown(toggle, menu) { 85 this.toggle = toggle; 86 this.menu = menu; 87 88 this.menuPlacement = { 89 top: menu.classList.contains("dropdown-menu-top"), 90 end: menu.classList.contains("dropdown-menu-end"), 91 }; 92 93 this.toggle.addEventListener("click", this.clickHandler.bind(this)); 94 this.toggle.addEventListener("keydown", this.toggleKeyHandler.bind(this)); 95 this.menu.addEventListener("keydown", this.menuKeyHandler.bind(this)); 96 document.body.addEventListener("click", this.outsideClickHandler.bind(this)); 97 98 const toggleId = this.toggle.getAttribute("id") || crypto.randomUUID(); 99 const menuId = this.menu.getAttribute("id") || crypto.randomUUID(); 100 101 this.toggle.setAttribute("id", toggleId); 102 this.menu.setAttribute("id", menuId); 103 104 this.toggle.setAttribute("aria-controls", menuId); 105 this.menu.setAttribute("aria-labelledby", toggleId); 106 107 this.menu.setAttribute("tabindex", -1); 108 this.menuItems.forEach((menuItem) => { 109 menuItem.tabIndex = -1; 110 }); 111 112 this.focusedIndex = -1; 113 } 114 115 Dropdown.prototype = { 116 get isExpanded() { 117 return this.toggle.getAttribute("aria-expanded") === "true"; 118 }, 119 120 get menuItems() { 121 return Array.prototype.slice.call( 122 this.menu.querySelectorAll("[role='menuitem'], [role='menuitemradio']") 123 ); 124 }, 125 126 dismiss: function () { 127 if (!this.isExpanded) return; 128 129 this.toggle.removeAttribute("aria-expanded"); 130 this.menu.classList.remove("dropdown-menu-end", "dropdown-menu-top"); 131 this.focusedIndex = -1; 132 }, 133 134 open: function () { 135 if (this.isExpanded) return; 136 137 this.toggle.setAttribute("aria-expanded", true); 138 this.handleOverflow(); 139 }, 140 141 handleOverflow: function () { 142 var rect = this.menu.getBoundingClientRect(); 143 144 var overflow = { 145 right: rect.left < 0 || rect.left + rect.width > window.innerWidth, 146 bottom: rect.top < 0 || rect.top + rect.height > window.innerHeight, 147 }; 148
149 if (overflow.right || this.menuPlacement.end) { 150 this.menu.classList.add("dropdown-menu-end"); 151 } 152 153 if (overflow.bottom || this.menuPlacement.top) { 154 this.menu.classList.add("dropdown-menu-top"); 155 } 156 157 if (this.menu.getBoundingClientRect().top < 0) { 158 this.menu.classList.remove("dropdown-menu-top"); 159 } 160 }, 161 162 focusByIndex: function (index) { 163 if (!this.menuItems.length) return; 164 165 this.menuItems.forEach((item, itemIndex) => { 166 if (itemIndex === index) { 167 item.tabIndex = 0; 168 item.focus(); 169 } else { 170 item.tabIndex = -1; 171 } 172 }); 173 174 this.focusedIndex = index; 175 }, 176 177 focusFirstMenuItem: function () { 178 this.focusByIndex(0); 179 }, 180 181 focusLastMenuItem: function () { 182 this.focusByIndex(this.menuItems.length - 1); 183 }, 184 185 focusNextMenuItem: function (currentItem) { 186 if (!this.menuItems.length) return; 187 188 const currentIndex = this.menuItems.indexOf(currentItem); 189 const nextIndex = (currentIndex + 1) % this.menuItems.length; 190 191 this.focusByIndex(nextIndex); 192 }, 193 194 focusPreviousMenuItem: function (currentItem) { 195 if (!this.menuItems.length) return; 196 197 const currentIndex = this.menuItems.indexOf(currentItem); 198 const previousIndex = 199 currentIndex <= 0 ? this.menuItems.length - 1 : currentIndex - 1; 200 201 this.focusByIndex(previousIndex); 202 }, 203 204 focusByChar: function (currentItem, char) { 205 char = char.toLowerCase(); 206 207 const itemChars = this.menuItems.map((menuItem) => 208 menuItem.textContent.trim()[0].toLowerCase() 209 ); 210 211 const startIndex = 212 (this.menuItems.indexOf(currentItem) + 1) % this.menuItems.length; 213 214 // look up starting from current index 215 let index = itemChars.indexOf(char, startIndex); 216 217 // if not found, start from start 218 if (index === -1) { 219 index = itemChars.indexOf(char, 0); 220 } 221 222 if (index > -1) { 223 this.focusByIndex(index); 224 } 225 }, 226 227 outsideClickHandler: function (e) { 228 if ( 229 this.isExpanded && 230 !this.toggle.contains(e.target) && 231 !e.composedPath().includes(this.menu) 232 ) { 233 this.dismiss(); 234 this.toggle.focus(); 235 } 236 }, 237 238 clickHandler: function (event) { 239 event.stopPropagation(); 240 event.preventDefault(); 241 242 if (this.isExpanded) { 243 this.dismiss(); 244 this.toggle.focus(); 245 } else { 246 this.open(); 247 this.focusFirstMenuItem(); 248 } 249 }, 250 251 toggleKeyHandler: function (e) { 252 const key = e.key; 253 254 switch (key) { 255 case "Enter": 256 case " ": 257 case "ArrowDown": 258 case "Down": { 259 e.stopPropagation(); 260 e.preventDefault(); 261 262 this.open(); 263 this.focusFirstMenuItem(); 264 break; 265 } 266 case "ArrowUp": 267 case "Up": { 268 e.stopPropagation(); 269 e.preventDefault(); 270 271 this.open(); 272 this.focusLastMenuItem(); 273 break; 274 } 275 case "Esc": 276 case "Escape": { 277 e.stopPropagation(); 278 e.preventDefault(); 279 280 this.dismiss(); 281 this.toggle.focus(); 282 break; 283 } 284 } 285 }, 286 287 menuKeyHandler: function (e) { 288 const key = e.key; 289 const currentElement = this.menuItems[this.focusedIndex]; 290 291 if (e.ctrlKey || e.altKey || e.metaKey) { 292 return; 293 } 294 295 switch (key) { 296 case "Esc": 297 case "Escape": { 298 e.stopPropagation(); 299 e.preventDefault(); 300 301 this.dismiss(); 302 this.toggle.focus(); 303 break; 304 } 305 case "ArrowDown": 306 case "Down": { 307 e.stopPropagation(); 308 e.preventDefault(); 309 310 this.focusNextMenuItem(currentElement); 311 break; 312 } 313 case "ArrowUp": 314 case "Up": { 315 e.stopPropagation(); 316 e.preventDefault(); 317 this.focusPreviousMenuItem(currentElement); 318 break; 319 } 320 case "Home": 321 case "PageUp": { 322 e.stopPropagation(); 323 e.preventDefault(); 324 this.focusFirstMenuItem(); 325 break; 326 } 327 case "End": 328 case "PageDown": { 329 e.stopPropagation(); 330 e.preventDefault(); 331 this.focusLastMenuItem(); 332 break; 333 } 334 case "Tab": { 335 if (e.shiftKey) { 336 e.stopPropagation(); 337 e.preventDefault(); 338 this.dismiss(); 339 this.toggle.focus(); 340 } else { 341 this.dismiss(); 342 } 343 break; 344 } 345 default: { 346 if (isPrintableChar(key)) { 347 e.stopPropagation(); 348 e.preventDefault(); 349 this.focusByChar(currentElement, key); 350 } 351 } 352 } 353 }, 354 }; 355 356 // Drodowns 357 358 window.addEventListener("DOMContentLoaded", () => { 359 const dropdowns = []; 360 const dropdownToggles = document.querySelectorAll(".dropdown-toggle"); 361
362 dropdownToggles.forEach((toggle) => { 363 const menu = toggle.nextElementSibling; 364 if (menu && menu.classList.contains("dropdown-menu")) { 365 dropdowns.push(new Dropdown(toggle, menu)); 366 } 367 }); 368 }); 369 370 // Share 371 372 window.addEventListener("DOMContentLoaded", () => { 373 const links = document.querySelectorAll(".share a"); 374 links.forEach((anchor) => { 375 anchor.addEventListener("click", (event) => { 376 event.preventDefault(); 377 window.open(anchor.href, "", "height = 500, width = 500"); 378 }); 379 }); 380 }); 381 382 // Vanilla JS debounce function, by Josh W. Comeau: 383 // https://www.joshwcomeau.com/snippets/javascript/debounce/ 384 function debounce(callback, wait) { 385 let timeoutId = null; 386 return (...args) => { 387 window.clearTimeout(timeoutId); 388 timeoutId = window.setTimeout(() => { 389 callback.apply(null, args); 390 }, wait); 391 }; 392 } 393 394 // Define variables for search field 395 let searchFormFilledClassName = "search-has-value"; 396 let searchFormSelector = "form[role='search']"; 397 398 // Clear the search input, and then return focus to it 399 function clearSearchInput(event) { 400 event.target 401 .closest(searchFormSelector) 402 .classList.remove(searchFormFilledClassName); 403 404 let input; 405 if (event.target.tagName === "INPUT") { 406 input = event.target; 407 } else if (event.target.tagName === "BUTTON") { 408 input = event.target.previousElementSibling; 409 } else { 410 input = event.target.closest("button").previousElementSibling; 411 } 412 input.value = ""; 413 input.focus(); 414 } 415 416 // Have the search input and clear button respond 417 // when someone presses the escape key, per: 418 // https://twitter.com/adambsilver/status/1152452833234554880 419 function clearSearchInputOnKeypress(event) { 420 const searchInputDeleteKeys = ["Delete", "Escape"]; 421 if (searchInputDeleteKeys.includes(event.key)) { 422 clearSearchInput(event); 423 } 424 } 425 426 // Create an HTML button that all users -- especially keyboard users -- 427 // can interact with, to clear the search input. 428 // To learn more about this, see: 429 // https://adrianroselli.com/2019/07/ignore-typesearch.html#Delete 430 // https://www.scottohara.me/blog/2022/02/19/custom-clear-buttons.html 431 function buildClearSearchButton(inputId) { 432 const button = document.createElement("button"); 433 button.setAttribute("type", "button"); 434 button.setAttribute("aria-controls", inputId); 435 button.classList.add("clear-button"); 436 const buttonLabel = window.searchClearButtonLabelLocalized; 437 const icon = `<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' focusable='false' role='img' viewBox='0 0 12 12' aria-label='${buttonLabel}'><path stroke='currentColor' stroke-linecap='round' stroke-width='2' d='M3 9l6-6m0 6L3 3'/></svg>`; 438 button.innerHTML = icon; 439 button.addEventListener("click", clearSearchInput); 440 button.addEventListener("keyup", clearSearchInputOnKeypress); 441 return button; 442 } 443 444 // Append the clear button to the search form 445 function appendClearSearchButton(input, form) { 446 const searchClearButton = buildClearSearchButton(input.id); 447 form.append(searchClearButton); 448 if (input.value.length > 0) { 449 form.classList.add(searchFormFilledClassName); 450 } 451 } 452 453 // Add a class to the search form when the input has a value; 454 // Remove that class from the search form when the input doesn't have a value. 455 // Do this on a delay, rather than on every keystroke. 456 const toggleClearSearchButtonAvailability = debounce((event) => { 457 const form = event.target.closest(searchFormSelector); 458 form.classList.toggle( 459 searchFormFilledClassName, 460 event.target.value.length > 0 461 ); 462 }, 200); 463 464 // Search 465 466 window.addEventListener("DOMContentLoaded", () => { 467 // Set up clear functionality for the search field 468 const searchForms = [...document.querySelectorAll(searchFormSelector)]; 469 const searchInputs = searchForms.map((form) => 470 form.querySelector("input[type='search']") 471 ); 472 searchInputs.forEach((input) => { 473 appendClearSearchButton(input, input.closest(searchFormSelector)); 474 input.addEventListener("keyup", clearSearchInputOnKeypress); 475 input.addEventListener("keyup", toggleClearSearchButtonAvailability); 476 }); 477 }); 478 479 const key = "returnFocusTo"; 480 481 function saveFocus() { 482 const activeElementId = document.activeElement.getAttribute("id"); 483 sessionStorage.setItem(key, "#" + activeElementId); 484 } 485 486 function returnFocus() { 487 const returnFocusTo = sessionStorage.getItem(key); 488 if (returnFocusTo) { 489 sessionStorage.removeItem("returnFocusTo"); 490 const returnFocusToEl = document.querySelector(returnFocusTo); 491 returnFocusToEl && returnFocusToEl.focus && returnFocusToEl.focus(); 492 } 493 } 494 495 // Forms 496 497 window.addEventListener("DOMContentLoaded", () => { 498 // In some cases we should preserve focus after page reload 499 returnFocus(); 500 501 // show form controls when the textarea receives focus or back button is used and value exists 502 const commentContainerTextarea = document.querySelector( 503 ".comment-container textarea" 504 ); 505 const commentContainerFormControls = document.querySelector(
506 ".comment-form-controls, .comment-ccs" 507 ); 508 509 if (commentContainerTextarea) { 510 commentContainerTextarea.addEventListener( 511 "focus", 512 function focusCommentContainerTextarea() { 513 commentContainerFormControls.style.display = "block"; 514 commentContainerTextarea.removeEventListener( 515 "focus", 516 focusCommentContainerTextarea 517 ); 518 } 519 ); 520 521 if (commentContainerTextarea.value !== "") { 522 commentContainerFormControls.style.display = "block"; 523 } 524 } 525 526 // Expand Request comment form when Add to conversation is clicked 527 const showRequestCommentContainerTrigger = document.querySelector( 528 ".request-container .comment-container .comment-show-container" 529 ); 530 const requestCommentFields = document.querySelectorAll( 531 ".request-container .comment-container .comment-fields" 532 ); 533 const requestCommentSubmit = document.querySelector( 534 ".request-container .comment-container .request-submit-comment" 535 ); 536 537 if (showRequestCommentContainerTrigger) { 538 showRequestCommentContainerTrigger.addEventListener("click", () => { 539 showRequestCommentContainerTrigger.style.display = "none"; 540 Array.prototype.forEach.call(requestCommentFields, (element) => { 541 element.style.display = "block"; 542 }); 543 requestCommentSubmit.style.display = "inline-block"; 544 545 if (commentContainerTextarea) { 546 commentContainerTextarea.focus(); 547 } 548 }); 549 } 550 551 // Mark as solved button 552 const requestMarkAsSolvedButton = document.querySelector( 553 ".request-container .mark-as-solved:not([data-disabled])" 554 ); 555 const requestMarkAsSolvedCheckbox = document.querySelector( 556 ".request-container .comment-container input[type=checkbox]" 557 ); 558 const requestCommentSubmitButton = document.querySelector( 559 ".request-container .comment-container input[type=submit]" 560 ); 561 562 if (requestMarkAsSolvedButton) { 563 requestMarkAsSolvedButton.addEventListener("click", () => { 564 requestMarkAsSolvedCheckbox.setAttribute("checked", true); 565 requestCommentSubmitButton.disabled = true; 566 requestMarkAsSolvedButton.setAttribute("data-disabled", true); 567 requestMarkAsSolvedButton.form.submit(); 568 }); 569 } 570 571 // Change Mark as solved text according to whether comment is filled 572 const requestCommentTextarea = document.querySelector( 573 ".request-container .comment-container textarea" 574 ); 575 576 const usesWysiwyg = 577 requestCommentTextarea && 578 requestCommentTextarea.dataset.helper === "wysiwyg"; 579 580 function isEmptyPlaintext(s) { 581 return s.trim() === ""; 582 } 583 584 function isEmptyHtml(xml) { 585 const doc = new DOMParser().parseFromString(`<_>${xml}</_>`, "text/xml"); 586 const img = doc.querySelector("img"); 587 return img === null && isEmptyPlaintext(doc.children[0].textContent); 588 } 589 590 const isEmpty = usesWysiwyg ? isEmptyHtml : isEmptyPlaintext; 591 592 if (requestCommentTextarea) { 593 requestCommentTextarea.addEventListener("input", () => { 594 if (isEmpty(requestCommentTextarea.value)) { 595 if (requestMarkAsSolvedButton) { 596 requestMarkAsSolvedButton.innerText = 597 requestMarkAsSolvedButton.getAttribute("data-solve-translation"); 598 } 599 } else { 600 if (requestMarkAsSolvedButton) { 601 requestMarkAsSolvedButton.innerText = 602 requestMarkAsSolvedButton.getAttribute( 603 "data-solve-and-submit-translation" 604 ); 605 } 606 } 607 }); 608 } 609 610 const selects = document.querySelectorAll( 611 "#request-status-select, #request-organization-select" 612 ); 613 614 selects.forEach((element) => { 615 element.addEventListener("change", (event) => { 616 event.stopPropagation(); 617 saveFocus(); 618 element.form.submit(); 619 }); 620 }); 621 622 // Submit requests filter form on search in the request list page 623 const quickSearch = document.querySelector("#quick-search"); 624 if (quickSearch) { 625 quickSearch.addEventListener("keyup", (event) => { 626 if (event.keyCode === ENTER) { 627 event.stopPropagation(); 628 saveFocus(); 629 quickSearch.form.submit(); 630 } 631 }); 632 } 633 634 // Submit organization form in the request page 635 const requestOrganisationSelect = document.querySelector( 636 "#request-organization select" 637 ); 638 639 if (requestOrganisationSelect) { 640 requestOrganisationSelect.addEventListener("change", () => { 641 requestOrganisationSelect.form.submit(); 642 }); 643 644 requestOrganisationSelect.addEventListener("click", (e) => { 645 // Prevents Ticket details collapsible-sidebar to close on mobile 646 e.stopPropagation(); 647 }); 648 } 649 650 // If there are any error notifications below an input field, focus that field 651 const notificationElm = document.querySelector(".notification-error"); 652 if ( 653 notificationElm && 654 notificationElm.previousElementSibling && 655 typeof notificationElm.previousElementSibling.focus === "function" 656 ) { 657 notificationElm.previousElementSibling.focus(); 658 } 659 }); 660 661})();
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.