1(function () { 2 const { mergeObjects, matchMedia } = BreakdanceFrontend.utils; 3 4 const verticalTabsSelector = "is-vertical"; 5 6 class BreakdanceTabs { 7 defaultOptions = { 8 activeTab: 1, 9 horizontalAt: "breakpoint_phone_landscape", 10 isVertical: false, 11 openOnHover: false 12 }; 13 14 activateTabEventTypes = ["click"]; 15 16 keys = { 17 end: 35, 18 home: 36, 19 left: 37, 20 up: 38, 21 right: 39, 22 down: 40, 23 delete: 46 24 }; 25 26 // Add or substract depending on key pressed 27 direction = { 28 37: -1, 29 38: -1, 30 39: 1, 31 40: 1 32 }; 33 34 constructor(selector, options = {}) { 35 this.selector = selector; 36 this.element = document.querySelector(`${this.selector}`); 37 this.isBuilder = !!window?.BreakdanceFrontend.utils.isBuilder(); 38 this.options = mergeObjects(this.defaultOptions, options); 39 40 this.init(); 41 } 42 43 bindListeners() { 44 if (this.tabs) { 45 this.tabs.forEach((tab, index) => { 46 this.addListeners(tab, index); 47 }); 48 if (this.tabSelect) { 49 this.tabSelect.addEventListener("change", event => { 50 this.changeEventListener(event); 51 }); 52 } 53 } 54 } 55 56 onResizeVertical() { 57 if (!this.options.isVertical) return; 58 const breakpoint = this.options.horizontalAt; 59 const { BASE_BREAKPOINT_ID } = window.BreakdanceFrontend.data; 60 61 if ( 62 (matchMedia(breakpoint) && this.options.isVertical) || 63 (breakpoint === BASE_BREAKPOINT_ID && this.options.isVertical) 64 ) { 65 this.tabsContainer.classList.remove(verticalTabsSelector); 66 this.tabList.setAttribute("aria-orientation", "horizontal"); 67 } else { 68 if (!this.tabsContainer.classList.contains(verticalTabsSelector)) { 69 this.tabsContainer.classList.add(verticalTabsSelector); 70 this.tabList.setAttribute("aria-orientation", "vertical"); 71 } 72 } 73 } 74 75 addListeners(tab, index) { 76 this.tabs[index].index = index; 77 this.onClick = this.clickEventListener.bind(this); 78 this.onKeydown = this.keydownEventListener.bind(this); 79 80 this.activateTabEventTypes.forEach((eventType) => { 81 tab.addEventListener(eventType, this.onClick); 82 }); 83 84 if (this.isBuilder) return; 85 tab.addEventListener("keydown", this.onKeydown); 86 } 87 88 changeEventListener(event) { 89 const tab = document.querySelector(`#${event.target.value}`); 90 this.activateTab(tab, false); 91 } 92 93 clickEventListener(event) { 94 const tab = event.target.closest(".js-tab"); 95 this.activateTab(tab, false); 96 } 97 98 keydownEventListener(event) { 99 const key = event.keyCode; 100 101 switch (key) { 102 case this.keys.end: 103 event.preventDefault(); 104 // Activate last tab 105 this.activateTab(this.tabs[this.tabs.length - 1], true); 106 break; 107 case this.keys.home: 108 event.preventDefault(); 109 // Activate first tab 110 this.activateTab(this.tabs[0], true); 111 break; 112 113 // Up and down are in keydown 114 // because we need to prevent page scroll >:) 115 case this.keys.up: 116 case this.keys.down: 117 case this.keys.left: 118 case this.keys.right: 119 this.determineOrientation(event); 120 break; 121 } 122 } 123 124 determineOrientation(event) { 125 const key = event.keyCode; 126 127 const vertical = 128 this.tabList.getAttribute("aria-orientation") == "vertical"; 129 let proceed = false; 130 131 if (vertical) { 132 if (key === this.keys.up || key === this.keys.down) { 133 event.preventDefault(); 134 proceed = true; 135 } 136 } else { 137 if (key === this.keys.left || key === this.keys.right) { 138 proceed = true; 139 } 140 } 141 142 if (proceed) { 143 this.switchTabOnArrowPress(event); 144 } 145 } 146 147 switchTabOnArrowPress(event) { 148 const pressed = event.keyCode; 149 150 this.onFocus = this.focusEventHandler.bind(this); 151
152 this.tabs.forEach(tab => { 153 tab.addEventListener("focus", event => this.onFocus(event)); 154 }); 155 156 if (this.direction[pressed]) { 157 const target = event.target; 158 if (target.index !== undefined) { 159 if (this.tabs[target.index + this.direction[pressed]]) { 160 this.tabs[target.index + this.direction[pressed]].focus(); 161 } else if (pressed === this.keys.left || pressed === this.keys.up) { 162 this.focusLastTab(); 163 } else if (pressed === this.keys.right || pressed == this.keys.down) { 164 this.focusFirstTab(); 165 } 166 } 167 } 168 } 169 170 activateTab(tab, setFocus) { 171 if (tab) { 172 setFocus = setFocus || false; 173 // Deactivate all other tabs 174 this.deactivateTabs(); 175 176 // Remove tabindex attribute 177 tab.removeAttribute("tabindex"); 178 179 // Set the tab as selected 180 tab.setAttribute("aria-selected", "true"); 181 182 tab.classList.add("is-active"); 183 184 // Get the value of aria-controls (which is an ID) 185 const controls = tab.getAttribute("aria-controls"); 186 187 if (this.tabSelect) { 188 this.tabSelect.value = tab.getAttribute("id"); 189 this.tabSelect[this.tabSelect.selectedIndex].setAttribute( 190 "selected", 191 "selected" 192 ); 193 } 194 195 const panel = document.getElementById(controls); 196 197 if (!panel) return; 198 199 panel.classList.add("is-active"); 200 201 // Set focus when required 202 if (setFocus && !this.isBuilder) { 203 tab.focus(); 204 } 205 206 // Force-refresh animations to allow the ones inside hidden tabs to work. 207 const event = new Event("breakdance_play_animations", { bubbles: true }); 208 panel.parentElement.dispatchEvent(event); 209 } 210 } 211 212 refreshPanelAttributes() { 213 const firstPanel = document.querySelector(`${this.selector} .js-panel`); 214 const panelsContainer = firstPanel.closest(".bde-tabs-content-container"); 215 216 this.panels = panelsContainer.querySelectorAll( 217 ":scope > .js-panel, :scope > .bde-advanced-tabs-content > .js-panel" 218 ); 219 220 this.panels.forEach((panel, index) => { 221 panel.setAttribute("id", `tab-panel-${this.tabsId}-${index + 1}`); 222 panel.setAttribute( 223 "aria-labelledby", 224 `tab-${this.tabsId}-${index + 1}` 225 ); 226 }); 227 } 228 229 deactivateTabs() { 230 if (this.tabs) {
231 this.tabs.forEach(tab => { 232 tab.setAttribute("tabindex", "-1"); 233 tab.setAttribute("aria-selected", "false"); 234 tab.removeEventListener("focus", event => this.onFocus(event)); 235 this.activateTabEventTypes.forEach((eventType) => { 236 tab.removeEventListener(eventType, event => this.onClick(event)); 237 }); 238 tab.removeEventListener("keydown", event => this.onKeydown(event)); 239 tab.classList.remove("is-active"); 240 }); 241 } 242 243 if (this.tabSelect) { 244 [...this.tabSelect.options].forEach(option => 245 option.removeAttribute("selected") 246 ); 247 } 248 249 if (this.tabSelect) { 250 this.tabSelect.removeEventListener("change", event => 251 this.changeEventListener(event) 252 ); 253 } 254 255 if (this.panels) { 256 this.panels.forEach(panel => { 257 panel.classList.remove("is-active"); 258 }); 259 } 260 } 261 262 focusFirstTab() { 263 this.tabs[0].focus(); 264 } 265 266 focusLastTab() { 267 this.tabs[this.tabs.length - 1].focus(); 268 } 269 270 determineDelay() { 271 const hasDelay = this.tabList.hasAttribute("data-delay"); 272 const delay = 0; 273 274 if (hasDelay) { 275 const delayValue = this.tabList.getAttribute("data-delay"); 276 if (delayValue) { 277 this.delay = delayValue; 278 } else { 279 // If no value is specified, default to 300ms 280 this.delay = 300; 281 } 282 } 283 284 return delay; 285 } 286 287 focusEventHandler(event) { 288 const target = event.target; 289 setTimeout(() => this.checkTabFocus(target), this.delay); 290 } 291 292 checkTabFocus(target) { 293 const focused = document.activeElement; 294 295 if (target === focused) { 296 this.activateTab(target, false); 297 } 298 } 299 300 destroy() { 301 if (!this.tabs) return; 302 this.deactivateTabs(); 303 this.resetTabstoVerticalLayout(); 304 this.destroyResizeObserver(); 305 } 306 307 destroyResizeObserver() { 308 if (!this.resizeObserver) return; 309 this.resizeObserver.unobserve(this.element); 310 } 311 312 resetTabstoVerticalLayout() { 313 this.tabsContainer.classList.remove(verticalTabsSelector); 314 this.tabList.setAttribute("aria-orientation", "vertical"); 315 } 316 317 onResize(callback) { 318 this.resizeObserver = new ResizeObserver(callback); 319 this.resizeObserver.observe(this.element); 320 321 return () => { 322 this.resizeObserver.disconnect(); 323 }; 324 } 325 326 update(options) { 327 if (options) { 328 this.options = mergeObjects(this.defaultOptions, options); 329 } 330 this.destroy(); 331 this.init(); 332 } 333 334 updateAndActivateTabFromSelector(selector) { 335 this.refreshPanelAttributes(); 336 const panel = selector.querySelector(".js-panel"); 337 const tabId = panel.getAttribute("aria-labelledby"); 338 if (!tabId) return; 339 const tab = document.getElementById(tabId); 340 this.activateTab(tab); 341 } 342 343 init() { 344 if (!this.element) return; 345 346 if (this.options.openOnHover && !this.isBuilder) { 347 this.activateTabEventTypes.push("mouseover"); 348 } 349 350 this.tabList = this.element.querySelector(".js-tablist"); 351 if (!this.tabList) return; 352 353 this.tabs = this.tabList.querySelectorAll(".js-tab"); 354 this.tabsContainer = this.tabList.closest(".js-tabs-container"); 355 this.tabsId = 356 this.tabList && this.tabList.dataset.tabsId 357 ? this.tabList.dataset.tabsId 358 : this.selector.match(/\d+$/)[0]; 359 this.tabSelect = this.element.querySelector(".js-tab-select"); 360 361 this.refreshPanelAttributes(); 362 this.bindListeners(); 363 this.activateTab(this.tabs[this.options.activeTab - 1]); 364 365 if (this.options.isVertical) { 366 this.onResize(() => this.onResizeVertical()); 367 } 368 } 369 } 370 371 window.BreakdanceTabs = BreakdanceTabs; 372})();
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.