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.