PageSourceSearch

https://help.klaxoon.com/hc/theming_assets/505577/33529543214743/script.js?digest=38355143426066

js klaxoon.com collected 2026-10-01 10:17:10 UTC 19,898 bytes, 661 lines download raw bytes

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.