[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/media/vendor/joomla-custom-elements/js/ -> joomla-tab.js (source)

   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);


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer