Source: Tabs.js

import BaseCustomElement from "./BaseCustomElement"

/** Implements an in-page tab selector.  It's intended to wrap a set of `<a>` or `<button>` elements
 * that represent the tabs of a tabbed UI, as defined by ARIA roles.  
 *
 * Each direct child must be an `<a>` or a `<button>`, though `<a>` is recommended.
 * Any other elements are ignored.  Each `<a>` or `<button>`
 * (herafter referred to as "tab") must have the correct ARIA attributes:
 *
 * * `role="tab"`
 * * `aria-selected` as true or false, depending on what tab is selected when the page is first rendered.  This
 *   custom element will ensure this value is updated as different tabs are selected.
 * * `tabindex` should be 0 if selected, -1 otherwise. This custom element will ensure this value is updated as
 *   different tabs are selected.
 * * `aria-controls` to the ID or list of IDs of panels that should be shown when this tab is selected.
 * * `id` to allow the `tab-panel` to refer back to this tab.
 *
 * This custom element will set click listeners on all tabs and, when clicked, hide all panels referred to by
 * every tab (by setting the `hidden` attribute), then show only those panels referred to by the clicked
 * tab.  You can use CSS to style everything the way you like it.
 *
 * @property {boolean} tab-selection-pushes-and-restores-state if set, this custom element will use the 
 *                                                             history API to manage state. When a tab
 *                                                             implemented by an `<a>` with an `href` is
 *                                                             clicked, that `href` will be pushed into 
 *                                                             the state.  When the back button is hit,
 *                                                             this will select the previous tab as selected.
 *                                                             Note that this will conflict with anything else
 *                                                             on the page that manipulates state, so only
 *                                                             set this if your UI is a "full page tab"
 *                                                             style UI.
 *
 * @fires Tabs#brut:tabselected whenever the tab selection has changed
 * @example
 * <brut-tabs>
 *   <a role="tab" aria-selected="true"  tabindex="0"  aria-controls="inbox-panel"  id="inbox-tab"
 *      href="?tab=inbox">Inbox</a>
 *   <a role="tab" aria-selected="false" tabindex="-1" aria-controls="drafts-panel" id="drafts-tab"
 *      href="?tab=drafts">Drafts</a>
 *   <a role="tab" aria-selected="false" tabindex="-1" aria-controls="spam-panel"   id="spam-tab"
 *      href="?tab=spam">Spam</a>
 * </brut-tabs>
 * <section role="tabpanel" tabindex="0"  id="inbox-panel">
 *   <h3>Inbox</h3>
 * </section>
 * <section role="tabpanel" tabindex="0" id="drafts-panel" hidden>
 *   <h3>Drafts</h3>
 * </section>
 * <section role="tabpanel" tabindex="0" id="spam-panel"   hidden>
 *   <h3>Spam</h3>
 * </section>
 * <!-- if a user clicks on 'Drafts', the DOM will be updated to look
 *      effectively like so: -->
 * <brut-tabs>
 *   <a role="tab" aria-selected="false" tabindex="-1" aria-controls="inbox-panel"  id="inbox-tab"
 *      href="?tab=inbox">Inbox</a>
 *   <a role="tab" aria-selected="true"  tabindex="0"  aria-controls="drafts-panel" id="drafts-tab"
 *      href="?tab=drafts">Drafts</a>
 *   <a role="tab" aria-selected="false" tabindex="-1" aria-controls="spam-panel"   id="spam-tab"
 *      href="?tab=spam">Spam</a>
 * </brut-tabs>
 * <section role="tabpanel" tabindex="0"  id="inbox-panel"  hidden>
 *   <h3>Inbox</h3>
 * </section>
 * <section role="tabpanel" tabindex="-1" id="drafts-panel">
 *   <h3>Drafts</h3>
 * </section>
 * <section role="tabpanel" tabindex="-1" id="spam-panel"   hidden>
 *   <h3>Spam</h3>
 * </section>
 *
 * @customElement brut-tabs
 */
class Tabs extends BaseCustomElement {
  static tagName = "brut-tabs"
  static observedAttributes = [
    "tab-selection-pushes-and-restores-state",
    "show-warnings",
  ]

  tabSelectionPushesAndRestoresStateChangedCallback({newValue,oldValue}) {
    this.#pushAndRestoreTabState = newValue != null
  }

  update() {
    this.#tabs().forEach( (tab) => {
      tab.addEventListener("click", this.#tabClicked)
    })
  }

  #pushAndRestoreTabState = false

  #tabClicked = (event) => {
    event.preventDefault()
    this.#setTabAsSelected(event.target)
    event.preventDefault()
  }

  #reloadTab = (event) => {
    const tab = document.getElementById(event.state.tabId)
    if (tab) {
      this.#setTabAsSelected(tab, { skipPushState: true })
    }
  }

  #setTabAsSelected(selectedTab, { skipPushState = false } = {}) {
    this.#tabs().forEach( (tab) => {
      const tabPanels = []
      const ariaControls = tab.getAttribute("aria-controls")
      if (ariaControls) {
        ariaControls.split(/\s+/).forEach( (id) => {
          const panel = document.getElementById(id)
          if (panel) {
            tabPanels.push(panel)
          }
          else {
            this.logger.warn("Tab %o references panel with id %s, but no such element exists with that id",tab,id)
          }
        })
      }
      if (tab == selectedTab) {
        tab.setAttribute("aria-selected",true)
        tab.setAttribute("tabindex","0")
        tabPanels.forEach( (panel) => panel.removeAttribute("hidden") )
        if (this.#pushAndRestoreTabState && !skipPushState)  {
          let href = tab.getAttribute("href") || ""
          if (href.startsWith("?")) {
            let hrefQueryString = href.slice(1)
            const anchorIndex = hrefQueryString.indexOf("#")
            if (anchorIndex != -1) {
              hrefQueryString = hrefQueryString.slice(-1 * (hrefQueryString.length - anchorIndex - 1))
            }
            const currentQuery = new URLSearchParams(window.location.search)
            const hrefQuery    = new URLSearchParams(hrefQueryString)
            hrefQuery.forEach( (value,key) => {
              currentQuery.set(key,value)
            })
            href = "?" + currentQuery.toString() + (anchorIndex == -1 ? "" : hrefQueryString.slice(anchorIndex))
          }
          window.history.pushState({ tabId: tab.id },"",href)
          window.addEventListener("popstate", this.#reloadTab)
        }
        this.dispatchEvent(new CustomEvent("brut:tabselected", { tabId: tab.id }))
      }
      else {
        tab.setAttribute("aria-selected",false)
        tab.setAttribute("tabindex","-1")
        tabPanels.forEach( (panel) => panel.setAttribute("hidden", true) )
      }
    })
  }


  #tabs() {
    const tabs = []
    this.querySelectorAll("[role=tab]").forEach( (tab) => {
      if ( (tab.tagName.toLowerCase() == "a") || (tab.tagName.toLowerCase() == "button") ) {
        tabs.push(tab)
      }
      else {
        this.logger.warn("An element with tag %s was assigned role=tab, and %s doesn't work that way. Use an <a> or a <button>",tab.tagName,this.constructor.name)
      }
    })
    return tabs
  }

}
export default Tabs