tobago-dropdown: custom elements, remove jQuery

Using custom elements for dropdown menus and remove jQuery

issue: TOBAGO-1633: TS refactoring
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java
index 2e6b5bc..6a9e0ac 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java
@@ -174,6 +174,7 @@
           BootstrapClass.DROPDOWN_MENU,
           getDropdownCssItems(facesContext, command));
       writer.writeAttribute(Arias.LABELLEDBY, "dropdownMenuButton", false);
+      writer.writeAttribute(HtmlAttributes.NAME, command.getClientId(facesContext), false);
 
       for (final UIComponent child : component.getChildren()) {
         if (child.isRendered()
@@ -238,7 +239,7 @@
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
 
     if (parentOfCommands) {
-      writer.startElement(HtmlElements.SPAN);
+      writer.startElement(HtmlElements.TOBAGO_DROPDOWN);
       writer.writeIdAttribute(clientId);
 
       writer.writeClassAttribute(
@@ -250,7 +251,7 @@
   protected void encodeEndOuter(final FacesContext facesContext, final AbstractUICommand command) throws IOException {
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
     if (command.isParentOfCommands()) {
-      writer.endElement(HtmlElements.SPAN);
+      writer.endElement(HtmlElements.TOBAGO_DROPDOWN);
     }
   }
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java
index 7e1d62d..0345145 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java
@@ -37,7 +37,7 @@
 
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
 
-    writer.startElement(HtmlElements.LI);
+    writer.startElement(HtmlElements.TOBAGO_DROPDOWN);
     if (parentOfCommands) {
       writer.writeIdAttribute(clientId);
     }
@@ -50,7 +50,7 @@
   @Override
   protected void encodeEndOuter(final FacesContext facesContext, final AbstractUICommand command) throws IOException {
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
-    writer.endElement(HtmlElements.LI);
+    writer.endElement(HtmlElements.TOBAGO_DROPDOWN);
   }
 
   @Override
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
index 82b99a5..3993af1 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
@@ -135,6 +135,7 @@
 
   TOBAGO_BEHAVIOR("tobago-behavior"),
   TOBAGO_DATE("tobago-date"),
+  TOBAGO_DROPDOWN("tobago-dropdown"),
   TOBAGO_FILE("tobago-file"),
   TOBAGO_LABEL("tobago-label"),
   TOBAGO_IN("tobago-in"),
diff --git a/tobago-core/src/main/resources/scss/_tobago.scss b/tobago-core/src/main/resources/scss/_tobago.scss
index 36f5902..a56b6a5 100644
--- a/tobago-core/src/main/resources/scss/_tobago.scss
+++ b/tobago-core/src/main/resources/scss/_tobago.scss
@@ -581,6 +581,15 @@
   display: block;
 }
 
+/* dropdown ------------------------------------------------------- */
+tobago-dropdown {
+  display: inline-block;
+}
+
+ul > tobago-dropdown {
+  display: list-item;
+}
+
 /* dropdown-submenu ------------------------------------------------------- */
 .tobago-dropdown-submenu {
   cursor: pointer;
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts
index 7041506..86d18d2 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts
@@ -15,61 +15,80 @@
  * limitations under the License.
  */
 
-import {Listener, Order, Phase} from "./tobago-listener";
+import "/webjars/popper.js/1.14.3/umd/popper.js";
+import Popper from "popper.js";
 
-class Dropdown {
+class Dropdown extends HTMLElement {
 
-  static init(element: HTMLElement): void {
-    const $dropdownMenus = jQuery(":not(.tobago-page-menuStore) > .dropdown-menu");
-    const $tobagoPageMenuStore = jQuery(".tobago-page-menuStore");
+  private blurFlag: boolean = false;
 
-    $dropdownMenus.each(function (): void {
-      const $dropdownMenu = jQuery(this);
-      const $parent = $dropdownMenu.parent();
+  constructor() {
+    super();
+  }
 
-      if (!$parent.hasClass("tobago-dropdown-submenu")
-          && $parent.closest(".navbar").length === 0) {
+  connectedCallback(): void {
+    this.toggle.addEventListener("click", this.toggleDropdown.bind(this));
+    this.toggle.addEventListener("blur", this.deselectComponent.bind(this));
+    window.addEventListener("mouseup", this.deselectComponent.bind(this));
+  }
 
-        // remove duplicated dropdown menus from menu store
-        // this could happen if the dropdown component is updated by ajax
-        removeDuplicates($dropdownMenu);
+  toggleDropdown(event: Event): void {
+    const visible: boolean = this.dropdownMenu.classList.contains("show");
+    if (visible) {
+      this.closeDropdown();
+    } else {
+      this.openDropdown();
+    }
+  }
 
-        $parent.on("shown.bs.dropdown", function (event: Event): void {
-          $tobagoPageMenuStore.append($dropdownMenu.detach());
-        }).on("hidden.bs.dropdown", function (event: Event): void {
-          $parent.append($dropdownMenu.detach());
-        });
-      }
-    });
+  deselectComponent(event: Event): void {
+    if (event.type === "blur") {
+      this.blurFlag = true;
+    } else if (this.blurFlag) {
+      this.blurFlag = false;
+
+      const target: HTMLElement = event.target as HTMLElement;
+      this.closeDropdown();
+      target.dispatchEvent(new MouseEvent("click", {bubbles: true}));
+    }
+  }
+
+  openDropdown(): void {
+    if (!this.inStickyHeader()) {
+      this.menuStore.appendChild(this.dropdownMenu);
+      new Popper(this.toggle, this.dropdownMenu, {
+        placement: "bottom-start"
+      });
+    }
+
+    this.dropdownMenu.classList.add("show");
+  }
+
+  closeDropdown(): void {
+    this.dropdownMenu.classList.remove("show");
+    this.appendChild(this.dropdownMenu);
+  }
+
+  private get toggle(): HTMLElement {
+    return this.querySelector(":scope > [data-toggle='dropdown']");
+  }
+
+  private inStickyHeader(): boolean {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelector("header.tobago-header.sticky-top tobago-dropdown[id='" + this.id + "']") !== null;
+  }
+
+  private get dropdownMenu(): HTMLDivElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelector(".dropdown-menu[name='" + this.id + "']");
+  }
+
+  private get menuStore(): HTMLDivElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelector(".tobago-page-menuStore");
   }
 }
 
-function removeDuplicates($dropdownMenu): void {
-  const $menuStoreDropdowns = jQuery(".tobago-page-menuStore .dropdown-menu");
-  // XXX todo: remove ts-ignore
-  // @ts-ignore
-  $menuStoreDropdowns.each(function (): boolean {
-    const $menuStoreDropdown = jQuery(this);
-
-    const dropdownIds = getIds($dropdownMenu);
-    const menuStoreIds = getIds($menuStoreDropdown);
-
-    for (let i = 0; i < dropdownIds.length; i++) {
-      if (jQuery.inArray(dropdownIds[i], menuStoreIds) >= 0) {
-        $menuStoreDropdown.remove();
-        return false;
-      }
-    }
-  });
-}
-
-// XXX todo: remove tslint
-// tslint:disable-next-line:typedef
-function getIds($dropdownMenu) {
-  return $dropdownMenu.find("[id]").map(function (): string {
-    return this.id;
-  });
-}
-
-Listener.register(Dropdown.init, Phase.DOCUMENT_READY, Order.NORMAL);
-Listener.register(Dropdown.init, Phase.AFTER_UPDATE, Order.NORMAL);
+document.addEventListener("DOMContentLoaded", function (event: Event): void {
+  window.customElements.define("tobago-dropdown", Dropdown);
+});