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