PageSourceSearch

https://orakeimarine.co.nz/assets/themes/orakeimarine-theme/script…/a11y-accordion-tabs.js?ver=1766094702

js orakeimarine.co.nz collected 2026-10-09 00:37:35 UTC 18,103 bytes, 573 lines download raw bytes

1/*
2 *  Accessible AccordionTabs, by Matthias Ott (@m_ott)
3 *
4 *  Based on the work of @stowball (https://codepen.io/stowball/pen/xVWwWe)
5 *
6 */
7(function () {
8  "use strict";
9
10  function AccordionTabs(el, options) {
11    if (!el) {
12      return;
13    }
14
15    this.el = el;
16    this.tabTriggers = this.el.getElementsByClassName("js-tabs-trigger");
17    this.tabPanels = this.el.getElementsByClassName("js-tabs-panel");
18    this.accordionTriggers = this.el.getElementsByClassName("js-accordion-trigger");
19
20    this.options = this._extend(
21      {
22        breakpoint: 1020,
23        tabsAllowed: true,
24        selectedTab: 0,
25        startCollapsed: false,
26        allowAllClosed: false,
27        allowMultipleOpened: false,
28        scrollTo: true,
29        onChange: null,
30      },
31      options
32    );
33
34    if (el.getAttribute("data-tabs-allowed") == "true") {
35      this.options.tabsAllowed = true;
36    } else if (el.getAttribute("data-tabs-allowed") == "false") {
37      this.options.tabsAllowed = false;
38    }
39
40    if (el.getAttribute("data-breakpoint")) {
41      this.options.breakpoint = parseInt(el.getAttribute("data-breakpoint"));
42    }
43
44    if (el.getAttribute("data-selected-tab")) {
45      this.options.selectedTab = parseInt(el.getAttribute("data-selected-tab"));
46    }
47
48    if (el.getAttribute("data-start-collapsed") == "true") {
49      this.options.startCollapsed = true;
50    } else if (el.getAttribute("data-start-collapsed") == "false") {
51      this.options.startCollapsed = false;
52    }
53
54    if (this.tabTriggers.length === 0 || this.tabTriggers.length !== this.tabPanels.length) {
55      return;
56    }
57
58    this._init();
59  }
60
61  AccordionTabs.prototype._init = function () {
62    var _this = this;
63
64    this.tabTriggersLength = this.tabTriggers.length;
65    this.accordionTriggersLength = this.accordionTriggers.length;
66    this.selectedTab = 0;
67    this.prevSelectedTab = null;
68    this.clickListener = this._clickEvent.bind(this);
69    this.keydownListener = this._keydownEvent.bind(this);
70    this.keys = {
71      prev: 37,
72      next: 39,
73      space: 32,
74      enter: 13,
75    };
76
77    if (window.innerWidth >= this.options.breakpoint && this.options.tabsAllowed) {
78      this.isAccordion = false;
79    } else {
80      this.isAccordion = true;
81    }
82
83    for (var i = 0; i < this.tabTriggersLength; i++) {
84      this.tabTriggers[i].index = i;
85      this.tabTriggers[i].addEventListener("click", this.clickListener, false);
86      this.tabTriggers[i].addEventListener("keydown", this.keydownListener, false);
87
88      if (this.tabTriggers[i].classList.contains("is-selected")) {
89        this.selectedTab = i;
90      }
91
92      this._hide(i);
93    }
94
95    for (var i = 0; i < this.accordionTriggersLength; i++) {
96      this.accordionTriggers[i].index = i;
97      this.accordionTriggers[i].addEventListener("click", this.clickListener, false);
98      this.accordionTriggers[i].addEventListener("keydown", this.keydownListener, false);
99
100      if (this.accordionTriggers[i].classList.contains("is-selected")) {
101        this.selectedTab = i;
102      }
103    }
104
105    if (!isNaN(this.options.selectedTab)) {
106      this.selectedTab =
107        this.options.selectedTab < this.tabTriggersLength ? this.options.selectedTab : this.tabTriggersLength - 1;
108    }
109
110    this.el.classList.add("is-initialized");
111    if (this.options.tabsAllowed) {
112      this.el.classList.add("tabs-allowed");
113    }
114
115    // If the accordion should not start collapsed, open the first element
116    if (!this.options.startCollapsed || !this.isAccordion) {
117      this.selectTab(this.selectedTab, false);
118    }
119
120    var resizeTabs = this._debounce(function () {
121      // This gets delayed for performance reasons
122      if (window.innerWidth >= _this.options.breakpoint && _this.options.tabsAllowed) {
123        _this.isAccordion = false;
124        if (_this.options.tabsAllowed) {
125          _this.el.classList.add("tabs-allowed");
126        }
127        _this.selectTab(_this.selectedTab);
128      } else {
129        _this.isAccordion = true;
130        _this.el.classList.remove("tabs-allowed");
131        if (!_this.options.startCollapsed) {
132          _this.selectTab(_this.selectedTab);
133        }
134      }
135    }, 50);
136    resizeTabs();
137
138    window.addEventListener("resize", resizeTabs);
139  };
140
141  AccordionTabs.prototype._clickEvent = function (e) {
142    // if e.closest .showcoupon, bail
143    if (e.target.closest(".showcoupon")) {
144      return;
145    }
146
147    var closestTrigger = this._getClosest(e.target, ".js-tabs-trigger");
148    var closestTab = 0;
149
150    if (closestTrigger == null) {
151      closestTrigger = this._getClosest(e.target, ".js-accordion-trigger");
152      closestTab = this._getClosest(closestTrigger, ".js-tabs-panel");
153      this.isAccordion = true;
154    } else {
155      this.isAccordion = false;
156    }
157
158    if (closestTrigger.tagName == "A" && !closestTrigger.getAttribute("href").startsWith("#")) {
159      return;
160    }
161    e.preventDefault();
162
163    var targetIndex = closestTrigger.index != null ? closestTrigger.index : closestTab.index;
164
165    if (targetIndex === this.selectedTab && !this.isAccordion && !this.options.allowAllClosed) {
166      return;
167    }
168
169    this.selectTab(targetIndex, true);
170  };
171
172  AccordionTabs.prototype._keydownEvent = function (e) {
173    var targetIndex;
174
175    if (
176      e.keyCode === this.keys.prev ||
177      e.keyCode === this.keys.next ||
178      e.keyCode === this.keys.space ||
179      e.keyCode === this.keys.enter
180    ) {
181      e.preventDefault();
182    } else {
183      return;
184    }
185
186    if (e.keyCode === this.keys.prev && e.target.index > 0 && !this.isAccordion && !this.options.allowAllClosed) {
187      targetIndex = e.target.index - 1;
188    } else if (
189      e.keyCode === this.keys.next &&
190      e.target.index < this.tabTriggersLength - 1 &&
191      !this.isAccordion &&
192      !this.options.allowAllClosed
193    ) {
194      targetIndex = e.target.index + 1;
195    } else if (e.keyCode === this.keys.space || e.keyCode === this.keys.enter) {
196      targetIndex = e.target.index;
197    } else {
198      return;
199    }
200
201    this.selectTab(targetIndex, true);
202  };
203
204  AccordionTabs.prototype._show = function (index, userInvoked) {
205    //hide other
206    if (!this.options.allowMultipleOpened) {
207      for (var i = 0; i < this.tabTriggersLength; i++) {
208        this._hide(i);
209      }
210    }
211
212    const self = this;
213
214    this.tabPanels[index].removeAttribute("tabindex");
215
216    this.tabTriggers[index].removeAttribute("tabindex");
217    this.tabTriggers[index].classList.add("is-selected");
218    this.tabTriggers[index].setAttribute("aria-selected", true);
219
220    this.accordionTriggers[index].setAttribute("aria-expanded", true);
221
222    var tabPanel = this.tabPanels[index];
223    var panelContent = this.tabPanels[index].getElementsByClassName("content")[0];
224    var isOpen = panelContent.classList.contains("is-open");
225
226    tabPanel.classList.add("is-opening");
227
228    // only animate if not already open
229    if (!isOpen) {
230      //if accordion, animate
231      if (this.isAccordion == true) {
232        var element = panelContent;
233
234        // get the height of the element's inner content, regardless of its actual size
235        var sectionHeight = element.scrollHeight;
236
237        // have the element transition to the height of its inner content
238        element.style.height = sectionHeight + "px";
239
240        // when the next css transition finishes (which should be the one we just triggered)
241        element.addEventListener("transitionend", function (e) {
242          // remove this event listener so it only gets triggered once
243          element.removeEventListener("transitionend", element);
244          // remove "height" from the element's inline styles, so it can return to its initial value
245          element.style.height = null;
246
247          //After open, change the classes
248          panelContent.setAttribute("aria-hidden", false);
249          panelContent.classList.remove("is-hidden");
250          panelContent.classList.add("is-open");
251
252          tabPanel.classList.remove("is-hidden");
253          tabPanel.classList.add("is-open");
254
255          if (typeof self.options.onChange === "function") {
256            self.options.onChange.call(self, panelContent);
257          }
258        });
259      } else {
260        //After open, change the classes
261        panelContent.setAttribute("aria-hidden", false);
262        panelContent.classList.remove("is-hidden");
263        panelContent.classList.add("is-open");
264
265        tabPanel.classList.remove("is-hidden");
266        tabPanel.classList.add("is-open");
267
268        if (typeof self.options.onChange === "function") {
269          self.options.onChange.call(self, panelContent);
270        }
271      }
272    } else {
273      //After open, change the classes
274      panelContent.setAttribute("aria-hidden", false);
275      panelContent.classList.remove("is-hidden");
276      panelContent.classList.add("is-open");
277
278      tabPanel.classList.remove("is-hidden");
279      tabPanel.classList.add("is-open");
280
281      if (typeof self.options.onChange === "function") {
282        self.options.onChange.call(self, panelContent);
283      }
284    }
285
286    // if (userInvoked) {
287    //   this.tabTriggers[index].focus();
288
289    //   var y = (this.tabPanels[index].getBoundingClientRect().top + window.pageYOffset) - 60;
290
291    //   window.scrollTo({
292    //     top: y,
293    //     behavior: 'smooth'
294    //   });
295    // }
296  };
297
298  AccordionTabs.prototype._hide = function (index) {
299    this.tabTriggers[index].classList.remove("is-selected");
300    this.tabTriggers[index].setAttribute("aria-selected", false);
301    this.tabTriggers[index].setAttribute("tabindex", -1);
302
303    this.accordionTriggers[index].setAttribute("aria-expanded", false);
304
305    var panelContent = this.tabPanels[index].getElementsByClassName("content")[0];
306    var tabPanel = this.tabPanels[index];
307
308    var isOpen = panelContent.classList.contains("is-open");
309    tabPanel.classList.remove("is-opening");
310
311    // only animate if already open
312    if (isOpen) {
313      //if accordion, animate
314      if (this.isAccordion == true) {
315        var element = panelContent;
316        // get the height of the element's inner content, regardless of its actual size
317        var sectionHeight = element.scrollHeight;
318
319        // temporarily disable all css transitions
320        var elementTransition = element.style.transition;
321        element.style.transition = "";
322
323        // on the next frame (as soon as the previous style change has taken effect),
324        // explicitly set the element's height to its current pixel height, so we
325        // aren't transitioning out of 'auto'
326        requestAnimationFrame(function () {
327          element.style.height = sectionHeight + "px";
328          element.style.transition = elementTransition;
329
330          // on the next frame (as soon as the previous style change has taken effect),
331          // have the element transition to height: 0
332          requestAnimationFrame(function () {
333            element.style.height = 0 + "px";
334
335            // when the next css transition finishes (which should be the one we just triggered)
336            element.addEventListener("transitionend", function (e) {
337              // remove this event listener so it only gets triggered once
338              element.removeEventListener("transitionend", element);
339              // remove "height" from the element's inline styles, so it can return to its initial value
340              element.style.height = null;
341
342              panelContent.setAttribute("aria-hidden", true);
343              panelContent.classList.remove("is-open");
344              panelContent.classList.add("is-hidden");
345
346              tabPanel.classList.remove("is-open");
347              tabPanel.classList.add("is-hidden");
348              tabPanel.setAttribute("tabindex", -1);
349            });
350          });
351        });
352      } else {
353        panelContent.setAttribute("aria-hidden", true);
354        panelContent.classList.remove("is-open");
355        panelContent.classList.add("is-hidden");
356
357        tabPanel.classList.remove("is-open");
358        tabPanel.classList.add("is-hidden");
359        tabPanel.setAttribute("tabindex", -1);
360      }
361    } else {
362      panelContent.setAttribute("aria-hidden", true);
363      panelContent.classList.remove("is-open");
364      panelContent.classList.add("is-hidden");
365
366      tabPanel.classList.remove("is-open");
367      tabPanel.classList.add("is-hidden");
368      tabPanel.setAttribute("tabindex", -1);
369    }
370  };
371
372  AccordionTabs.prototype.selectTab = function (index, userInvoked) {
373    if (index === null) {
374      return;
375    }
376
377    if (!this.tabPanels[index].classList.contains("is-hidden") && userInvoked) {
378      if (index === this.selectedTab) {
379        this.selectedTab = null;
380      } else {
381        this.selectedTab = null;
382        this.prevSelectedTab = index;
383      }
384
385      this._hide(index);
386
387      return;
388    }
389
390    if (this.isAccordion) {
391      this.prevSelectedTab = this.selectedTab;
392      this.selectedTab = index;
393    } else {
394      if (this.prevSelectedTab === null || !this.isAccordion) {
395        for (var i = 0; i < this.tabTriggersLength; i++) {
396          if (i !== index) {
397            this._hide(i);
398          }
399        }
400      } else {
401        this._hide(this.selectedTab);
402      }
403
404      this.prevSelectedTab = this.selectedTab;
405      this.selectedTab = index;
406    }
407
408    this._show(this.selectedTab, userInvoked);
409  };
410
411  AccordionTabs.prototype.destroy = function () {
412    for (var i = 0; i < this.tabTriggersLength; i++) {
413      this.tabTriggers[i].classList.remove("is-selected");
414      this.tabTriggers[i].removeAttribute("aria-selected");
415      this.tabTriggers[i].removeAttribute("tabindex");
416
417      this.tabPanels[i].classList.remove("is-hidden");
418      this.tabPanels[i].removeAttribute("aria-hidden");
419      this.tabPanels[i].removeAttribute("tabindex");
420
421      this.tabTriggers[i].removeEventListener("click", this.clickListener, false);
422      this.tabTriggers[i].removeEventListener("keydown", this.keydownListener, false);
423
424      delete this.tabTriggers[i].index;
425    }
426
427    this.el.classList.remove("is-initialized");
428  };
429
430  /**
431   * Get the closest matching element up the DOM tree.
432   * @private
433   * @param  {Element} elem     Starting element
434   * @param  {String}  selector Selector to match against
435   * @return {Boolean|Element}  Returns null if not match found
436   */
437  AccordionTabs.prototype._getClosest = function (elem, selector) {
438    // Element.matches() polyfill
439    if (!Element.prototype.matches) {
440      Element.prototype.matches =
441        Element.prototype.matchesSelector ||
442        Element.prototype.mozMatchesSelector ||
443        Element.prototype.msMatchesSelector ||
444        Element.prototype.oMatchesSelector ||
445        Element.prototype.webkitMatchesSelector ||
446        function (s) {
447          var matches = (this.document || this.ownerDocument).querySelectorAll(s),
448            i = matches.length;
449          while (--i >= 0 && matches.item(i) !== this) { }
450          return i > -1;
451        };
452    }
453
454    // Get closest match
455    for (; elem && elem !== document; elem = elem.parentNode) {
456      if (elem.matches(selector)) return elem;
457    }
458
459    return null;
460  };
461
462  // Pass in the objects to merge as arguments.
463  // For a deep extend, set the first argument to `true`.
464  AccordionTabs.prototype._extend = function () {
465    // Variables
466    var extended = {};
467    var deep = false;
468    var i = 0;
469    var length = arguments.length;
470
471    // Check if a deep merge
472    if (Object.prototype.toString.call(arguments[0]) === "[object Boolean]") {
473      deep = arguments[0];
474      i++;
475    }
476
477    // Merge the object into the extended object
478    var merge = function (obj) {
479      for (var prop in obj) {
480        if (Object.prototype.hasOwnProperty.call(obj, prop)) {
481          // If deep merge and property is an object, merge properties
482          if (deep && Object.prototype.toString.call(obj[prop]) === "[object Object]") {
483            extended[prop] = extend(true, extended[prop], obj[prop]);
484          } else {
485            extended[prop] = obj[prop];
486          }
487        }
488      }
489    };
490
491    // Loop through each object and conduct a merge
492    for (; i < length; i++) {
493      var obj = arguments[i];
494      merge(obj);
495    }
496
497    return extended;
498  };
499
500  // Returns a function, that, as long as it continues to be invoked, will not
501  // be triggered. The function will be called after it stops being called for
502  // N milliseconds. If `immediate` is passed, trigger the function on the
503  // leading edge, instead of the trailing.
504  AccordionTabs.prototype._debounce = function (func, wait, immediate) {
505    var timeout;
506    return function () {
507      var context = this,
508        args = arguments;
509      var later = function () {
510        timeout = null;
511        if (!immediate) {
512          func.apply(context, args);
513        }
514      };
515      var callNow = immediate && !timeout;
516      clearTimeout(timeout);
517      timeout = setTimeout(later, wait);
518      if (callNow) {
519        func.apply(context, args);
520      }
521    };
522  };
523
524  var slice = Array.prototype.slice;
525
526  if (!String.prototype.startsWith) {
527    Object.defineProperty(String.prototype, "startsWith", {
528      value: function (search, rawPos) {
529        var pos = rawPos > 0 ? rawPos | 0 : 0;
530        return this.substring(pos, pos + search.length) === search;
531      },
532    });
533  }
534
535  function $(expr, con) {
536    return typeof expr === "string" ? (con || document).querySelector(expr) : expr || null;
537  }
538
539  function $$(expr, con) {
540    return slice.call((con || document).querySelectorAll(expr));
541  }
542
543  // Initialization
544
545  function init() {
546    $$(".js-tabs").forEach(function (input) {
547      new AccordionTabs(input);
548    });
549  }
550
551  // Are we in a browser? Check for Document constructor
552  if (typeof Document !== "undefined") {
553    // DOM already loaded?
554    if (document.readyState !== "loading") {
555      init();
556    } else {
557      // Wait for it
558      document.addEventListener("DOMContentLoaded", init);
559    }
560  }
561
562  // Export on self when in a browser
563  if (typeof self !== "undefined") {
564    self.AccordionTabs = AccordionTabs;
565  }
566
567  // Expose as a CJS module
568  if (typeof module === "object" && module.exports) {
569    module.exports = AccordionTabs;
570  }
571
572  return AccordionTabs;
573})();

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.