[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 class TabElement extends HTMLElement {} 2 3 customElements.define('joomla-tab-element', TabElement); 4 5 class TabsElement extends HTMLElement { 6 /* Attributes to monitor */ 7 static get observedAttributes() { return ['recall', 'orientation', 'view', 'breakpoint']; } 8 9 get recall() { return this.getAttribute('recall'); } 10 11 set recall(value) { this.setAttribute('recall', value); } 12 13 get view() { return this.getAttribute('view'); } 14 15 set view(value) { this.setAttribute('view', value); } 16 17 get orientation() { return this.getAttribute('orientation'); } 18 19 set orientation(value) { this.setAttribute('orientation', value); } 20 21 get breakpoint() { return parseInt(this.getAttribute('breakpoint'), 10); } 22 23 set breakpoint(value) { this.setAttribute('breakpoint', value); } 24 25 /* Lifecycle, element created */ 26 constructor() { 27 super(); 28 this.tabs = []; 29 this.tabsElements = []; 30 this.previousActive = null; 31 32 this.onMutation = this.onMutation.bind(this); 33 this.keyBehaviour = this.keyBehaviour.bind(this); 34 this.activateTab = this.activateTab.bind(this); 35 this.deactivateTabs = this.deactivateTabs.bind(this); 36 this.checkView = this.checkView.bind(this); 37 38 this.observer = new MutationObserver(this.onMutation); 39 this.observer.observe(this, { attributes: false, childList: true, subtree: true }); 40 } 41 42 /* Lifecycle, element appended to the DOM */ 43 connectedCallback() { 44 if (!this.orientation || (this.orientation && !['horizontal', 'vertical'].includes(this.orientation))) { 45 this.orientation = 'horizontal'; 46 } 47 48 if (!this.view || (this.view && !['tabs', 'accordion'].includes(this.view))) { 49 this.view = 'tabs'; 50 } 51 52 // get tab elements 53 this.tabsElements = [].slice.call(this.children).filter((el) => el.tagName.toLowerCase() === 'joomla-tab-element'); 54 55 // Sanity checks 56 if (!this.tabsElements.length) { 57 return; 58 } 59 60 this.isNested = this.parentNode.closest('joomla-tab') instanceof HTMLElement; 61 62 this.hydrate(); 63 if (this.hasAttribute('recall') && !this.isNested) { 64 this.activateFromState(); 65 } 66 67 // Activate tab from the URL hash 68 if (window.location.hash) { 69 const hash = window.location.hash.substr(1); 70 const tabToactivate = this.tabs.filter((tab) => tab.tab.id === hash); 71 if (tabToactivate.length) { 72 this.activateTab(tabToactivate[0].tab, false); 73 } 74 } 75 76 // If no active tab activate the first one 77 if (!this.tabs.filter((tab) => tab.tab.hasAttribute('active')).length) { 78 this.activateTab(this.tabs[0].tab, false); 79 } 80 81 this.addEventListener('keyup', this.keyBehaviour); 82 83 if (this.breakpoint) { 84 // Convert tabs to accordian 85 this.checkView(); 86 window.addEventListener('resize', () => { 87 this.checkView(); 88 }); 89 } 90 } 91 92 /* Lifecycle, element removed from the DOM */ 93 disconnectedCallback() { 94 this.tabs.map((tab) => { 95 tab.tabButton.removeEventListener('click', this.activateTab); 96 tab.accordionButton.removeEventListener('click', this.activateTab); 97 return tab; 98 }); 99 this.removeEventListener('keyup', this.keyBehaviour); 100 } 101 102 /* Respond to attribute changes */ 103 attributeChangedCallback(attr, oldValue, newValue) { 104 switch (attr) { 105 case 'view': 106 if (!newValue || (newValue && !['tabs', 'accordion'].includes(newValue))) { 107 this.view = 'tabs'; 108 } 109 if (newValue === 'tabs' && newValue !== oldValue) { 110 if (this.tabButtonContainer) this.tabButtonContainer.removeAttribute('hidden'); 111 this.tabs.map((tab) => tab.accordionButton.setAttribute('hidden', '')); 112 } else if (newValue === 'accordion' && newValue !== oldValue) { 113 if (this.tabButtonContainer) this.tabButtonContainer.setAttribute('hidden', ''); 114 this.tabs.map((tab) => tab.accordionButton.removeAttribute('hidden')); 115 } 116 break; 117 } 118 } 119 120 hydrate() { 121 // Ensure the tab links container exists 122 this.tabButtonContainer = document.createElement('div'); 123 this.tabButtonContainer.setAttribute('role', 'tablist'); 124 this.insertAdjacentElement('afterbegin', this.tabButtonContainer); 125 126 if (this.view === 'accordion') { 127 this.tabButtonContainer.setAttribute('hidden', ''); 128 } 129 130 this.tabsElements.map((tab) => { 131 // Create Accordion button 132 const accordionButton = document.createElement('button'); 133 accordionButton.setAttribute('aria-expanded', !!tab.hasAttribute('active')); 134 accordionButton.setAttribute('aria-controls', tab.id); 135 accordionButton.setAttribute('type', 'button'); 136 accordionButton.innerHTML = `<span class="accordion-title">$tab.getAttribute('name')}<span class="accordion-icon"></span></span>`; 137 tab.insertAdjacentElement('beforebegin', accordionButton); 138 139 if (this.view === 'tabs') { 140 accordionButton.setAttribute('hidden', ''); 141 } 142 143 accordionButton.addEventListener('click', this.activateTab); 144 145 // Create tab button 146 const tabButton = document.createElement('button'); 147 tabButton.setAttribute('aria-expanded', !!tab.hasAttribute('active')); 148 tabButton.setAttribute('aria-controls', tab.id); 149 tabButton.setAttribute('role', 'tab'); 150 tabButton.setAttribute('type', 'button'); 151 tabButton.innerHTML = `$tab.getAttribute('name')}`; 152 this.tabButtonContainer.appendChild(tabButton); 153 154 tabButton.addEventListener('click', this.activateTab); 155 156 if (this.view === 'tabs') { 157 tab.setAttribute('role', 'tabpanel'); 158 } else { 159 tab.setAttribute('role', 'region'); 160 } 161 162 this.tabs.push({ 163 tab, 164 tabButton, 165 accordionButton, 166 }); 167 168 return tab; 169 }); 170 } 171 172 /* Update on mutation */ 173 onMutation(mutationsList) { 174 // eslint-disable-next-line no-restricted-syntax 175 for (const mutation of mutationsList) { 176 if (mutation.type === 'childList') { 177 if (mutation.addedNodes.length) { 178 [].slice.call(mutation.addedNodes).map((inserted) => this.createNavs(inserted)); 179 // Add the tab buttons 180 } 181 if (mutation.removedNodes.length) { 182 // Remove the tab buttons 183 [].slice.call(mutation.addedNodes).map((inserted) => this.removeNavs(inserted)); 184 } 185 } 186 } 187 } 188 189 keyBehaviour(e) { 190 // Only the tabs/accordion buttons, no ⌘ or Alt modifier 191 if (![...this.tabs.map((el) => el.tabButton), ...this.tabs.map((el) => el.accordionButton)] 192 .includes(document.activeElement) 193 || e.metaKey 194 || e.altKey) { 195 return; 196 } 197 198 let previousTabItem; 199 let nextTabItem; 200 if (this.view === 'tabs') { 201 const currentTabIndex = this.tabs.findIndex((tab) => tab.tab.hasAttribute('active')); 202 previousTabItem = currentTabIndex - 1 >= 0 203 ? this.tabs[currentTabIndex - 1] : this.tabs[this.tabs.length - 1]; 204 nextTabItem = currentTabIndex + 1 <= this.tabs.length - 1 205 ? this.tabs[currentTabIndex + 1] : this.tabs[0]; 206 } else { 207 const currentTabIndex = this.tabs.map((el) => el.accordionButton) 208 .findIndex((tab) => tab === document.activeElement); 209 previousTabItem = currentTabIndex - 1 >= 0 210 ? this.tabs[currentTabIndex - 1] : this.tabs[this.tabs.length - 1]; 211 nextTabItem = currentTabIndex + 1 <= this.tabs.length - 1 212 ? this.tabs[currentTabIndex + 1] : this.tabs[0]; 213 } 214 215 // catch left/right and up/down arrow key events 216 switch (e.keyCode) { 217 case 37: 218 case 38: 219 if (this.view === 'tabs') { 220 previousTabItem.tabButton.click(); 221 previousTabItem.tabButton.focus(); 222 } else { 223 previousTabItem.accordionButton.focus(); 224 } 225 e.preventDefault(); 226 break; 227 case 39: 228 case 40: 229 if (this.view === 'tabs') { 230 nextTabItem.tabButton.click(); 231 nextTabItem.tabButton.focus(); 232 } else { 233 nextTabItem.accordionButton.focus(); 234 } 235 e.preventDefault(); 236 break; 237 } 238 } 239 240 deactivateTabs() { 241 this.tabs.map((tabObj) => { 242 tabObj.accordionButton.removeAttribute('aria-disabled'); 243 tabObj.tabButton.removeAttribute('aria-expanded'); 244 tabObj.accordionButton.setAttribute('aria-expanded', false); 245 246 if (tabObj.tab.hasAttribute('active')) { 247 this.dispatchCustomEvent('joomla.tab.hide', this.view === 'tabs' ? tabObj.tabButton : tabObj.accordionButton, this.previousActive); 248 tabObj.tab.removeAttribute('active'); 249 tabObj.tab.setAttribute('tabindex', '-1'); 250 // Emit hidden event 251 this.dispatchCustomEvent('joomla.tab.hidden', this.view === 'tabs' ? tabObj.tabButton : tabObj.accordionButton, this.previousActive); 252 this.previousActive = this.view === 'tabs' ? tabObj.tabButton : tabObj.accordionButton; 253 } 254 return tabObj; 255 }); 256 } 257 258 activateTab(input, state = true) { 259 let currentTrigger; 260 if (input.currentTarget) { 261 currentTrigger = this.tabs.find((tab) => ((this.view === 'tabs' ? tab.tabButton : tab.accordionButton) === input.currentTarget)); 262 } else if (input instanceof HTMLElement) { 263 currentTrigger = this.tabs.find((tab) => tab.tab === input); 264 } else if (Number.isInteger(input)) { 265 currentTrigger = this.tabs[input]; 266 } 267 268 if (currentTrigger) { 269 // Accordion can close the active panel 270 if (this.view === 'accordion' && this.tabs.find((tab) => tab.accordionButton.getAttribute('aria-expanded') === 'true') === currentTrigger) { 271 if (currentTrigger.tab.hasAttribute('active')) { 272 currentTrigger.tab.removeAttribute('active'); 273 return; 274 } 275 currentTrigger.tab.setAttribute('active', ''); 276 return; 277 } 278 279 // Remove current active 280 this.deactivateTabs(); 281 // Set new active 282 currentTrigger.tabButton.setAttribute('aria-expanded', true); 283 currentTrigger.accordionButton.setAttribute('aria-expanded', true); 284 currentTrigger.accordionButton.setAttribute('aria-disabled', true); 285 currentTrigger.tab.setAttribute('active', ''); 286 currentTrigger.tabButton.removeAttribute('tabindex'); 287 this.dispatchCustomEvent('joomla.tab.show', this.view === 'tabs' ? currentTrigger.tabButton : currentTrigger.accordionButton, this.previousActive); 288 if (state) { 289 if (this.view === 'tabs') { 290 currentTrigger.tabButton.focus(); 291 } else { 292 currentTrigger.accordionButton.focus(); 293 } 294 } 295 if (state) this.saveState(currentTrigger.tab.id); 296 this.dispatchCustomEvent('joomla.tab.shown', this.view === 'tabs' ? currentTrigger.tabButton : currentTrigger.accordionButton, this.previousActive); 297 } 298 } 299 300 // Create navigation elements for inserted tabs 301 createNavs(tab) { 302 if ((tab instanceof Element && tab.tagName.toLowerCase() !== 'joomla-tab-element') || ![].some.call(this.children, (el) => el === tab).length || !tab.getAttribute('name') || !tab.getAttribute('id')) return; 303 const tabs = [].slice.call(this.children).filter((el) => el.tagName.toLowerCase() === 'joomla-tab-element'); 304 const index = tabs.findIndex((tb) => tb === tab); 305 306 // Create Accordion button 307 const accordionButton = document.createElement('button'); 308 accordionButton.setAttribute('aria-expanded', !!tab.hasAttribute('active')); 309 accordionButton.setAttribute('aria-controls', tab.id); 310 accordionButton.setAttribute('type', 'button'); 311 accordionButton.innerHTML = `<span class="accordion-title">$tab.getAttribute('name')}<span class="accordion-icon"></span></span>`; 312 tab.insertAdjacentElement('beforebegin', accordionButton); 313 314 if (this.view === 'tabs') { 315 accordionButton.setAttribute('hidden', ''); 316 } 317 318 accordionButton.addEventListener('click', this.activateTab); 319 320 // Create tab button 321 const tabButton = document.createElement('button'); 322 tabButton.setAttribute('aria-expanded', !!tab.hasAttribute('active')); 323 tabButton.setAttribute('aria-controls', tab.id); 324 tabButton.setAttribute('role', 'tab'); 325 tabButton.setAttribute('type', 'button'); 326 tabButton.innerHTML = `$tab.getAttribute('name')}`; 327 if (tabs.length - 1 === index) { 328 // last 329 this.tabButtonContainer.appendChild(tabButton); 330 this.tabs.push({ 331 tab, 332 tabButton, 333 accordionButton, 334 }); 335 } else if (index === 0) { 336 // first 337 this.tabButtonContainer.insertAdjacentElement('afterbegin', tabButton); 338 this.tabs.slice(0, 0, { 339 tab, 340 tabButton, 341 accordionButton, 342 }); 343 } else { 344 // Middle 345 this.tabs[index - 1].tabButton.insertAdjacentElement('afterend', tabButton); 346 this.tabs.slice(index - 1, 0, { 347 tab, 348 tabButton, 349 accordionButton, 350 }); 351 } 352 353 tabButton.addEventListener('click', this.activateTab); 354 } 355 356 // Remove navigation elements for removed tabs 357 removeNavs(tab) { 358 if ((tab instanceof Element && tab.tagName.toLowerCase() !== 'joomla-tab-element') || ![].some.call(this.children, (el) => el === tab).length || !tab.getAttribute('name') || !tab.getAttribute('id')) return; 359 const accordionButton = tab.previousSilbingElement; 360 if (accordionButton && accordionButton.tagName.toLowerCase() === 'button') { 361 accordionButton.removeEventListener('click', this.keyBehaviour); 362 accordionButton.parentNode.removeChild(accordionButton); 363 } 364 const tabButton = this.tabButtonContainer.querySelector(`[aria-controls=$accordionButton.id}]`); 365 if (tabButton) { 366 tabButton.removeEventListener('click', this.keyBehaviour); 367 tabButton.parentNode.removeChild(tabButton); 368 } 369 const index = this.tabs.findIndex((tb) => tb.tabs === tab); 370 if (index - 1 === 0) { 371 this.tabs.shift(); 372 } else if (index - 1 === this.tabs.length) { 373 this.tabs.pop(); 374 } else { 375 this.tabs.splice(index - 1, 1); 376 } 377 } 378 379 /** Method to convert tabs to accordion and vice versa depending on screen size */ 380 checkView() { 381 if (!this.breakpoint) { 382 return; 383 } 384 385 if (document.body.getBoundingClientRect().width > this.breakpoint) { 386 if (this.view === 'tabs') { 387 return; 388 } 389 this.tabButtonContainer.removeAttribute('hidden'); 390 this.tabs.map((tab) => { 391 tab.accordionButton.setAttribute('hidden', ''); 392 tab.accordionButton.setAttribute('role', 'tabpanel'); 393 if (tab.accordionButton.getAttribute('aria-expanded') === 'true') { 394 tab.tab.setAttribute('active', ''); 395 } 396 return tab; 397 }); 398 this.setAttribute('view', 'tabs'); 399 } else { 400 if (this.view === 'accordion') { 401 return; 402 } 403 this.tabButtonContainer.setAttribute('hidden', ''); 404 this.tabs.map((tab) => { 405 tab.accordionButton.removeAttribute('hidden'); 406 tab.accordionButton.setAttribute('role', 'region'); 407 return tab; 408 }); 409 this.setAttribute('view', 'accordion'); 410 } 411 } 412 413 getStorageKey() { 414 return window.location.href.toString().split(window.location.host)[1].replace(/&return=[a-zA-Z0-9%]+/, '').split('#')[0]; 415 } 416 417 saveState(value) { 418 const storageKey = this.getStorageKey(); 419 sessionStorage.setItem(storageKey, value); 420 } 421 422 activateFromState() { 423 this.hasNested = this.querySelector('joomla-tab') instanceof HTMLElement; 424 // Use the sessionStorage state! 425 const href = sessionStorage.getItem(this.getStorageKey()); 426 if (href) { 427 const currentTabIndex = this.tabs.findIndex((tab) => tab.tab.id === href); 428 429 if (currentTabIndex >= 0) { 430 this.activateTab(currentTabIndex, false); 431 } else if (this.hasNested) { 432 const childTabs = this.querySelector('joomla-tab'); 433 if (childTabs) { 434 const activeTabs = [].slice.call(this.querySelectorAll('joomla-tab-element')) 435 .reverse() 436 .filter((activeTabEl) => activeTabEl.id === href); 437 if (activeTabs.length) { 438 // Activate the deepest tab 439 let activeTab = activeTabs[0].closest('joomla-tab'); 440 [].slice.call(activeTab.querySelectorAll('joomla-tab-element')) 441 .forEach((tabEl) => { 442 tabEl.removeAttribute('active'); 443 if (tabEl.id === href) { 444 tabEl.setAttribute('active', ''); 445 } 446 }); 447 448 // Activate all parent tabs 449 while (activeTab.parentNode.closest('joomla-tab') !== this) { 450 const parentTabContainer = activeTab.closest('joomla-tab'); 451 const parentTabEl = activeTab.parentNode.closest('joomla-tab-element'); 452 [].slice.call(parentTabContainer.querySelectorAll('joomla-tab-element')) 453 // eslint-disable-next-line no-loop-func 454 .forEach((tabEl) => { 455 tabEl.removeAttribute('active'); 456 if (parentTabEl === tabEl) { 457 tabEl.setAttribute('active', ''); 458 activeTab = parentTabEl; 459 } 460 }); 461 } 462 463 [].slice.call(this.children) 464 .filter((el) => el.tagName.toLowerCase() === 'joomla-tab-element') 465 .forEach((tabEl) => { 466 tabEl.removeAttribute('active'); 467 const isActiveChild = tabEl.querySelector('joomla-tab-element[active]'); 468 469 if (isActiveChild) { 470 this.activateTab(tabEl, false); 471 } 472 }); 473 } 474 } 475 } 476 } 477 } 478 479 /* Method to dispatch events */ 480 dispatchCustomEvent(eventName, element, related) { 481 const OriginalCustomEvent = new CustomEvent(eventName, { bubbles: true, cancelable: true }); 482 OriginalCustomEvent.relatedTarget = related; 483 element.dispatchEvent(OriginalCustomEvent); 484 } 485 } 486 487 customElements.define('joomla-tab', TabsElement);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |