refactor TS for tc:tab / tc:tabGroup

issue: TOBAGO-1633: TS refactoring
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TabGroupRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TabGroupRenderer.java
index 664b3b8..9049d0e 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TabGroupRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TabGroupRenderer.java
@@ -41,6 +41,7 @@
 import org.apache.myfaces.tobago.renderkit.RendererBase;
 import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
 import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
 import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
@@ -72,7 +73,7 @@
 
   private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  private static final String ACTIVE_INDEX_POSTFIX = ComponentUtils.SUB_SEPARATOR + "activeIndex";
+  private static final String INDEX_POSTFIX = ComponentUtils.SUB_SEPARATOR + "index";
 
   @Override
   public void processEvent(final ComponentSystemEvent event) {
@@ -124,15 +125,15 @@
 
     final String clientId = component.getClientId(facesContext);
     final Map parameters = facesContext.getExternalContext().getRequestParameterMap();
-    final String newValue = (String) parameters.get(clientId + ACTIVE_INDEX_POSTFIX);
+    final String newValue = (String) parameters.get(clientId + INDEX_POSTFIX);
     try {
-      final int activeIndex = Integer.parseInt(newValue);
-      if (activeIndex != oldIndex) {
-        final TabChangeEvent event = new TabChangeEvent(component, oldIndex, activeIndex);
+      final int newIndex = Integer.parseInt(newValue);
+      if (newIndex != oldIndex) {
+        final TabChangeEvent event = new TabChangeEvent(component, oldIndex, newIndex);
         component.queueEvent(event);
       }
     } catch (final NumberFormatException e) {
-      LOG.error("Can't parse activeIndex: '" + newValue + "'");
+      LOG.error("Can't parse newIndex: '" + newValue + "'");
     }
   }
 
@@ -141,10 +142,10 @@
 
     final AbstractUITabGroup tabGroup = (AbstractUITabGroup) uiComponent;
 
-    final int activeIndex = ensureRenderedActiveIndex(facesContext, tabGroup);
+    final int selectedIndex = ensureRenderedSelectedIndex(facesContext, tabGroup);
 
     final String clientId = tabGroup.getClientId(facesContext);
-    final String hiddenId = clientId + TabGroupRenderer.ACTIVE_INDEX_POSTFIX;
+    final String hiddenId = clientId + TabGroupRenderer.INDEX_POSTFIX;
     final SwitchType switchType = tabGroup.getSwitchType();
     final Markup markup = tabGroup.getMarkup();
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
@@ -157,33 +158,33 @@
         tabGroup.getCustomClass(),
         markup != null && markup.contains(Markup.SPREAD) ? TobagoClass.SPREAD : null);
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, tabGroup);
-    writer.writeAttribute(DataAttributes.SWITCH_TYPE, switchType.name(), false);
+    writer.writeAttribute(CustomAttributes.SWITCH_TYPE, switchType.name(), false);
 
     writer.startElement(HtmlElements.INPUT);
     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
-    writer.writeAttribute(HtmlAttributes.VALUE, activeIndex);
+    writer.writeAttribute(HtmlAttributes.VALUE, selectedIndex);
     writer.writeNameAttribute(hiddenId);
     writer.writeIdAttribute(hiddenId);
     writer.endElement(HtmlElements.INPUT);
 
     if (tabGroup.isShowNavigationBar()) {
-      encodeHeader(facesContext, writer, tabGroup, activeIndex, switchType);
+      encodeHeader(facesContext, writer, tabGroup, selectedIndex, switchType);
     }
 
-    encodeContent(facesContext, writer, tabGroup, activeIndex, switchType);
+    encodeContent(facesContext, writer, tabGroup, selectedIndex, switchType);
 
     writer.endElement(HtmlElements.TOBAGO_TAB_GROUP);
   }
 
-  private int ensureRenderedActiveIndex(final FacesContext context, final AbstractUITabGroup tabGroup) {
-    final int activeIndex = tabGroup.getSelectedIndex();
+  private int ensureRenderedSelectedIndex(final FacesContext context, final AbstractUITabGroup tabGroup) {
+    final int selectedIndex = tabGroup.getSelectedIndex();
     // ensure to select a rendered tab
     int index = -1;
     int closestRenderedTabIndex = -1;
     for (final UIComponent tab : tabGroup.getChildren()) {
       if (tab instanceof AbstractUIPanelBase) {
         index++;
-        if (index == activeIndex) {
+        if (index == selectedIndex) {
           if (tab.isRendered()) {
             return index;
           } else if (closestRenderedTabIndex > -1) {
@@ -192,7 +193,7 @@
         }
         if (tab.isRendered()) {
           closestRenderedTabIndex = index;
-          if (index > activeIndex) {
+          if (index > selectedIndex) {
             break;
           }
         }
@@ -213,7 +214,7 @@
 
   private void encodeHeader(
       final FacesContext facesContext, final TobagoResponseWriter writer, final AbstractUITabGroup tabGroup,
-      final int activeIndex, final SwitchType switchType)
+      final int selectedIndex, final SwitchType switchType)
       throws IOException {
 
     final String tabGroupClientId = tabGroup.getClientId(facesContext);
@@ -241,7 +242,7 @@
           final String tabId = tab.getClientId(facesContext);
           Markup markup = tab.getMarkup() != null ? tab.getMarkup() : Markup.NULL;
 
-          if (activeIndex == index) {
+          if (selectedIndex == index) {
             markup = markup.add(Markup.SELECTED);
           }
           final FacesMessage.Severity maxSeverity
@@ -260,7 +261,7 @@
               tab.getCustomClass());
           writer.writeAttribute(HtmlAttributes.FOR, tabGroupClientId, true);
           writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.PRESENTATION.toString(), false);
-          writer.writeAttribute(DataAttributes.TAB_GROUP_INDEX, index);
+          writer.writeAttribute(CustomAttributes.INDEX, index);
           final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, tab);
           if (title != null) {
             writer.writeAttribute(HtmlAttributes.TITLE, title, true);
@@ -272,7 +273,7 @@
           }
           if (tab.isDisabled()) {
             writer.writeClassAttribute(BootstrapClass.NAV_LINK, BootstrapClass.DISABLED);
-          } else if (activeIndex == index) {
+          } else if (selectedIndex == index) {
             writer.writeClassAttribute(BootstrapClass.NAV_LINK, BootstrapClass.ACTIVE);
           } else {
             writer.writeClassAttribute(BootstrapClass.NAV_LINK);
@@ -286,7 +287,9 @@
           if (!disabled) {
             final CommandMap map = RenderUtils.getBehaviorCommands(facesContext, tab);
             CommandMap.merge(map, tabGroupMap);
-            writer.writeAttribute(DataAttributes.COMMANDS, JsonUtils.encode(map), false);
+            if (false) { // TBD
+              writer.writeAttribute(DataAttributes.COMMANDS, JsonUtils.encode(map), false);
+            }
           }
 
           if (!disabled && label.getAccessKey() != null) {
@@ -335,14 +338,14 @@
 
   protected void encodeContent(
       final FacesContext facesContext, final TobagoResponseWriter writer, final AbstractUITabGroup tabGroup,
-      final int activeIndex, final SwitchType switchType) throws IOException {
+      final int selectedIndex, final SwitchType switchType) throws IOException {
     writer.startElement(HtmlElements.DIV);
     writer.writeClassAttribute(BootstrapClass.CARD_BODY, BootstrapClass.TAB_CONTENT);
     int index = 0;
     for (final UIComponent child : tabGroup.getChildren()) {
       if (child instanceof AbstractUITab) {
         final AbstractUITab tab = (AbstractUITab) child;
-        if (tab.isRendered() && (switchType == SwitchType.client || index == activeIndex) && !tab.isDisabled()) {
+        if (tab.isRendered() && (switchType == SwitchType.client || index == selectedIndex) && !tab.isDisabled()) {
           final Markup markup = tab.getMarkup();
 
           writer.startElement(HtmlElements.DIV);
@@ -350,11 +353,11 @@
               TobagoClass.TAB__CONTENT,
               TobagoClass.TAB__CONTENT.createMarkup(markup),
               BootstrapClass.TAB_PANE,
-              index == activeIndex ? BootstrapClass.ACTIVE : null);
+              index == selectedIndex ? BootstrapClass.ACTIVE : null);
           writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.TABPANEL.toString(), false);
           writer.writeIdAttribute(getTabPanelId(facesContext, tab));
 
-          writer.writeAttribute(DataAttributes.TAB_GROUP_INDEX, index);
+          writer.writeAttribute(CustomAttributes.INDEX, index);
 
           tab.encodeAll(facesContext);
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
index 2955d36..2e49e66 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
@@ -21,14 +21,23 @@
 
 public enum CustomAttributes implements MarkupLanguageAttributes {
 
-  MIN_CHARS("min-chars"),
-  DELAY("delay"),
-  MAX_ITEMS("max-items"),
-  UPDATE("update"),
-  TOTAL_COUNT("total-count"),
-  LOCAL_MENU("local-menu"),
   DATA("data"),
-  ORIENTATION("orientation");
+  DELAY("delay"),
+  /**
+   * The index of the tab inside the tab group.
+   */
+  INDEX("index"),
+  LOCAL_MENU("local-menu"),
+  MAX_ITEMS("max-items"),
+  MIN_CHARS("min-chars"),
+  ORIENTATION("orientation"),
+  /**
+   * The mode of the tab switch: client, reloadTab, reloadPage.
+   */
+  SWITCH_TYPE("switch-type"),
+  TOTAL_COUNT("total-count"),
+  UPDATE("update");
+
 
   private final String value;
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index 5a28407..175737f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -169,16 +169,6 @@
 
   SCROLL_POSITION("data-tobago-scroll-position"),
 
-  /**
-   * The mode of the tab switch: client, reloadTab, reloadPage.
-   */
-  SWITCH_TYPE("data-tobago-switch-type"),
-
-  /**
-   * The index of the tab inside the tab group.
-   */
-  TAB_GROUP_INDEX("data-tobago-tab-group-index"),
-
   TARGET("data-target"),
 
   TITLE("data-title"),
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlAttributes.java
index 82f27ac..4dca011 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlAttributes.java
@@ -19,6 +19,9 @@
 
 package org.apache.myfaces.tobago.renderkit.html;
 
+/**
+ * HTML standard attributes. For non-standard attributes {@link CustomAttributes}
+ */
 public enum HtmlAttributes implements MarkupLanguageAttributes {
 
   ACCEPT_CHARSET("accept-charset"),
@@ -119,25 +122,7 @@
   VALIGN("valign"),
   VALUE("value"),
   WIDTH("width"),
-  XMLNS("xmlns"),
-
-  // Non standard attributes ///////////////////////////////////////////////////////////
-
-  /**
-   * The index of the tab inside the tab group.
-   *
-   * @deprecated since 4.3.0, please use {@link DataAttributes#TAB_GROUP_INDEX}
-   */
-  @Deprecated
-  TABGROUPINDEX("tabgroupindex"),
-  /**
-   * The mode of the tab switch: client, reloadTab, reloadPage.
-   *
-   * @deprecated since 4.3.0, please use {@link DataAttributes#SWITCH_TYPE}
-   */
-  @Deprecated
-  SWITCHTYPE("switchtype");
-
+  XMLNS("xmlns");
 
   private final String value;
 
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/070-tab/01-ajax/Tab_Ajax.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/070-tab/01-ajax/Tab_Ajax.xhtml
index 15a7fe0..15c5da0 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/070-tab/01-ajax/Tab_Ajax.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/070-tab/01-ajax/Tab_Ajax.xhtml
@@ -42,7 +42,7 @@
       <tc:tab id="t11" label="One">
         First tab.
       </tc:tab>
-      <tc:tab id="t12" label="Two" disabled="true">
+      <tc:tab id="t12" label="Two (disabled)" disabled="true">
         Second tab.
       </tc:tab>
       <tc:tab id="t13" label="Three">
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-command.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-command.ts
index eb1110a..d998774 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-command.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-command.ts
@@ -244,51 +244,47 @@
     }
   }
 
-  static init = function (element: HTMLElement, force: boolean = false) {
+  static init = function (element: HTMLElement) {
 
     for (const commandElement of DomUtils.selfOrQuerySelectorAll(element, "[data-tobago-commands]")) {
 
-      // TODO hack to set command eventListeners after tobago-tab EventListeners
-      if (force || commandElement.parentElement.tagName !== "TOBAGO-TAB") {
+      const commandMap = new CommandMap(commandElement.dataset["tobagoCommands"]);
 
-        const commandMap = new CommandMap(commandElement.dataset["tobagoCommands"]);
+      for (const entry of commandMap.commands.entries()) {
+        const key: string = entry[0];
+        const value: Command = entry[1];
 
-        for (const entry of commandMap.commands.entries()) {
-          const key: string = entry[0];
-          const value: Command = entry[1];
-
-          switch (key) {
-            case "change":
-              commandElement.addEventListener("change", CommandMap.change);
-              break;
-            case "complete":
-              if (parseFloat(commandElement.getAttribute("value")) >= parseFloat(commandElement.getAttribute("max"))) {
-                if (commandMap.complete.execute || commandMap.complete.render) {
-                  jsf.ajax.request(
-                      this.id,
-                      null,
-                      {
-                        "javax.faces.behavior.event": "complete",
-                        execute: commandMap.complete.execute,
-                        render: commandMap.complete.render
-                      });
-                } else {
-                  Command.submitAction(this, commandMap.complete.action, commandMap.complete);
-                }
+        switch (key) {
+          case "change":
+            commandElement.addEventListener("change", CommandMap.change);
+            break;
+          case "complete":
+            if (parseFloat(commandElement.getAttribute("value")) >= parseFloat(commandElement.getAttribute("max"))) {
+              if (commandMap.complete.execute || commandMap.complete.render) {
+                jsf.ajax.request(
+                    this.id,
+                    null,
+                    {
+                      "javax.faces.behavior.event": "complete",
+                      execute: commandMap.complete.execute,
+                      render: commandMap.complete.render
+                    });
+              } else {
+                Command.submitAction(this, commandMap.complete.action, commandMap.complete);
               }
-              break;
-            case "load":
-              setTimeout(function () {
-                    Command.submitAction(this, commandMap.load.action, commandMap.load);
-                  },
-                  commandMap.load.delay || 100);
-              break;
-            case "resize":
-              window.addEventListener("resize", CommandMap.resize);
-              break;
-            default:
-              commandElement.addEventListener(key, CommandMap.otherEvent);
-          }
+            }
+            break;
+          case "load":
+            setTimeout(function () {
+                  Command.submitAction(this, commandMap.load.action, commandMap.load);
+                },
+                commandMap.load.delay || 100);
+            break;
+          case "resize":
+            window.addEventListener("resize", CommandMap.resize);
+            break;
+          default:
+            commandElement.addEventListener(key, CommandMap.otherEvent);
         }
       }
     }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tab.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tab.ts
index e9f2ed4..fcb822c 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tab.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tab.ts
@@ -18,74 +18,103 @@
 import {Command} from "./tobago-command";
 
 class TabGroup extends HTMLElement {
-  private markupCssClass: string = "tobago-tab-markup-selected";
+
+  private hiddenInput: HTMLInputElement;
 
   constructor() {
     super();
+    this.hiddenInput = this.querySelector(":scope > input[type=hidden]");
   }
 
   connectedCallback() {
-    const tabGroup: TabGroup = this;
-    const hiddenInput: HTMLInputElement = this.querySelector(":scope > input[type=hidden]");
+  }
 
-    for (const tab of tabGroup.tabs) {
-      const navLink: HTMLLinkElement = tab.querySelector(":scope > .nav-link");
-      if (!navLink.classList.contains("disabled")) {
-        navLink.addEventListener('click', function () {
-          hiddenInput.value = tab.groupIndex;
-
-          if (tabGroup.dataset.tobagoSwitchType === "client") {
-            tabGroup.activeTab.classList.remove(tabGroup.markupCssClass);
-            tabGroup.activeNavLink.classList.remove("active");
-            tabGroup.activeTabContent.classList.remove("active");
-
-            tab.classList.add(tabGroup.markupCssClass);
-            navLink.classList.add("active");
-            tabGroup.getTabContent(tab.groupIndex).classList.add("active");
-          }
-        });
-      }
-    }
-
-    Command.init(tabGroup, true);
+  get switchType(): string {
+    return this.getAttribute("switch-type");
   }
 
   get tabs(): NodeListOf<Tab> {
-    return this.querySelectorAll("tobago-tab[for='" + this.id + "']");
+    return this.querySelectorAll(":scope > .card-header > .card-header-tabs > tobago-tab") as NodeListOf<Tab>;
   }
 
-  get activeTab(): Tab {
-    return this.querySelector("tobago-tab[for='" + this.id + "']." + this.markupCssClass);
+  getSelectedTab(): Tab {
+    return this.querySelector(":scope > .card-header > .card-header-tabs > tobago-tab[index='" + this.selected + "']") as Tab;
   }
 
-  get activeNavLink(): HTMLLinkElement {
-    return this.querySelector("tobago-tab[for='" + this.id + "'] > .nav-link.active");
+  get selected(): number {
+    return parseInt(this.hiddenInput.value);
   }
 
-  get activeTabContent(): HTMLDivElement {
-    return this.querySelector(":scope > .card-body.tab-content > .tobago-tab-content.active");
-  }
-
-  getTabContent(tabGroupIndex: string): HTMLDivElement {
-    return this.querySelector(":scope > .card-body > .tobago-tab-content[data-tobago-tab-group-index='"
-        + tabGroupIndex + "']");
+  set selected(index: number) {
+    this.hiddenInput.value = String(index);
   }
 }
 
-class Tab extends HTMLElement {
+export class Tab extends HTMLElement {
+
   constructor() {
     super();
   }
 
   connectedCallback() {
+    let navLink = this.navLink;
+    if (!navLink.classList.contains("disabled")) {
+      navLink.addEventListener("click", this.select.bind(this));
+    }
   }
 
-  get groupIndex(): string {
-    return this.dataset.tobagoTabGroupIndex;
+  get index(): number {
+    return parseInt(this.getAttribute("index"));
+  }
+
+  get navLink(): HTMLLinkElement {
+    return this.querySelector(".nav-link");
+  }
+
+  get tabGroup(): TabGroup {
+    return this.closest("tobago-tab-group") as TabGroup;
+  }
+
+  select(event: MouseEvent) {
+    const tabGroup = this.tabGroup;
+    const old = tabGroup.getSelectedTab();
+    tabGroup.selected = this.index;
+
+    switch (tabGroup.switchType) {
+      case "client":
+        old.navLink.classList.remove("active");
+        this.navLink.classList.add("active");
+        old.content.classList.remove("active");
+        this.content.classList.add("active");
+        break;
+      case "reloadTab":
+        jsf.ajax.request(
+            this.navLink,
+            event, {
+              //"javax.faces.behavior.event": "click",
+              execute: tabGroup.id,
+              render: tabGroup.id
+            });
+        break;
+      case "reloadPage":
+        Command.submitAction(this.navLink, this.id);
+        break;
+      case "none": // todo
+        console.error("Not implemented yet: %s", tabGroup.switchType);
+        break;
+      default:
+        console.error("Unknown switchType='%s'", tabGroup.switchType);
+        break;
+    }
+  }
+
+  get content(): HTMLElement {
+    return this.closest("tobago-tab-group")
+        .querySelector(":scope > .card-body.tab-content > .tab-pane[index='" + this.index + "']");
   }
 }
 
 document.addEventListener("DOMContentLoaded", function (event) {
-  window.customElements.define('tobago-tab-group', TabGroup);
   window.customElements.define('tobago-tab', Tab);
+  window.customElements.define('tobago-tab-group', TabGroup);
 });