TOBAGO-2001: Evaluate a suggest implemenation without jQuery - Replace suggest implementation with vaadin
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SuggestRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SuggestRenderer.java
index 95bf01d..1fbc1c9 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SuggestRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SuggestRenderer.java
@@ -29,6 +29,7 @@
 import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
+import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
 import org.apache.myfaces.tobago.util.ComponentUtils;
 import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
 import org.slf4j.Logger;
@@ -121,12 +122,17 @@
     writer.writeAttribute(CustomAttributes.UPDATE, suggest.isUpdate());
     writer.writeAttribute(CustomAttributes.TOTAL_COUNT, totalCount);
     writer.writeAttribute(CustomAttributes.LOCAL_MENU, suggest.isLocalMenu());
-    writer.writeAttribute(CustomAttributes.DATA, JsonUtils.encode(array), true);
+    writer.writeAttribute(CustomAttributes.ITEMS, JsonUtils.encode(array), true);
 
     if (LOG.isDebugEnabled()) {
       LOG.debug("suggest list: {}", JsonUtils.encode(array));
     }
 
+    writer.startElement(HtmlElements.INPUT);
+    writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
+    writer.writeAttribute(HtmlAttributes.NAME, clientId, false);
+    writer.endElement(HtmlElements.INPUT);
+
     writer.endElement(HtmlElements.TOBAGO_SUGGEST);
   }
 
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 d6a003d..7b8030b 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
@@ -24,7 +24,6 @@
   COLLAPSE_ACTION("collapse-action"),
   COLLAPSE_TARGET("collapse-target"),
   CONFIRMATION("confirmation"),
-  DATA("data"),
   DELAY("delay"),
   EVENT("event"),
   /**
@@ -37,6 +36,7 @@
    * The index of the tab inside the tab group.
    */
   INDEX("index"),
+  ITEMS("items"),
   LOCALE("locale"),
   LOCAL_MENU("local-menu"),
   MAX_ITEMS("max-items"),
diff --git a/tobago-core/src/main/resources/scss/_tobago.scss b/tobago-core/src/main/resources/scss/_tobago.scss
index 95017c9..0528fce 100644
--- a/tobago-core/src/main/resources/scss/_tobago.scss
+++ b/tobago-core/src/main/resources/scss/_tobago.scss
@@ -1384,94 +1384,6 @@
   display: none;
 }
 
-.typeahead {
-  background-color: #FFFFFF;
-
-  &:focus {
-    border: 2px solid #0097CF;
-  }
-}
-
-.tt-query {
-  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
-}
-
-.tt-hint {
-  color: #aaaaaa;
-}
-
-.tt-menu {
-  display: none;
-  position: absolute;
-  z-index: $zindex-popover;
-  background-color: #FFFFFF;
-  border: 1px solid rgba(0, 0, 0, 0.2);
-  border-radius: 3px;
-  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-  margin-top: 1px;
-  padding: 8px 0;
-}
-
-.tt-open {
-  display: block;
-}
-
-.twitter-typeahead .tt-open {
-  /* for 'localMenu' - if suggest menu rendered directly on component - not in .tobago-page-menuStore */
-  min-width: 100%;
-}
-
-.tt-empty {
-  display: none;
-}
-
-.tt-suggestion {
-  padding: 3px 20px;
-
-  &:hover {
-    background-color: #0097CF;
-    color: #FFFFFF;
-    cursor: pointer;
-  }
-
-  &.tt-cursor {
-    background-color: #0097CF;
-    color: #FFFFFF;
-  }
-
-  p {
-    margin: 0;
-  }
-}
-
-.twitter-typeahead {
-  display: flex !important;
-  flex-grow: 1;
-}
-
-.input-group > .twitter-typeahead {
-  /* suggest in center of an input group */
-  > .tobago-in {
-    width: 100%;
-    border-radius: 0;
-  }
-
-  &:first-child {
-    /* suggest on the left of an input group */
-    > .tobago-in {
-      border-top-left-radius: $border-radius;
-      border-bottom-left-radius: $border-radius;
-    }
-  }
-  &:last-child {
-    /* suggest on the right of an input group */
-    > .tobago-in {
-      border-top-right-radius: $border-radius;
-      border-bottom-right-radius: $border-radius;
-    }
-  }
-}
-
 /* tab / tab-group ----------------------------------------------------------------- */
 .tobago-tabGroup, .tobago-tab, .tobago-tab-content {
 }
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/010-input/20-suggest/Suggest.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/010-input/20-suggest/Suggest.xhtml
index 549d76b..8459a0a 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/010-input/20-suggest/Suggest.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/010-input/20-suggest/Suggest.xhtml
@@ -51,6 +51,7 @@
         <tc:selectItems value="#{suggestController.solarObjects}" var="name" itemValue="#{name}"/>
       </tc:suggest>
     </tc:in>
+    <h1>Warning: Tab key doesn't work here!</h1> <!-- fixme -->
   </tc:section>
 
   <tc:section label="Advanced">
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/package.json b/tobago-theme/tobago-theme-standard/src/main/npm/package.json
index d79929c..1c79281 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/package.json
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/package.json
@@ -31,7 +31,8 @@
     "test": "jest"
   },
   "dependencies": {
-    "@vaadin/vaadin-date-picker": "^4.0.5"
+    "@vaadin/vaadin-date-picker": "^4.0.5",
+    "@vaadin/vaadin-combo-box": "^5.0.9"
   },
   "devDependencies": {
     "@babel/core": "^7.4.5",
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/declare.d.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/declare.d.ts
index 152dc7e..ea981ed 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/declare.d.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/declare.d.ts
@@ -24,6 +24,4 @@
   popover(data?: any, options?: any): JQuery;
 
   modal(data?: any, options?: any): JQuery;
-
-  typeahead(data?: any, options?: any): JQuery;
 }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
index e9f78c5..b8cb52b 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
@@ -23,7 +23,7 @@
 import "@vaadin/vaadin-date-picker/vaadin-date-picker-light.js";
 // import "@vaadin/vaadin-date-time-picker";
 // import "@vaadin/vaadin-time-picker";
-// import "@vaadin/vaadin-combo-box";
+import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
 // import "moment"; //XXX moment seems not to work with rollup.js, need to re-check
 import "./tobago-date";
 import "./tobago-command";
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-suggest.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-suggest.ts
index 31ceeea..f935664 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-suggest.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-suggest.ts
@@ -15,70 +15,25 @@
  * limitations under the License.
  */
 
-import {DomUtils} from "./tobago-utils";
+import {ComboBoxLightElement} from "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
 
 class Suggest extends HTMLElement {
 
-  static asyncResults: (data: string[]) => void;
+  static callback: (items: String[], size: number) => {};// todo string vs String
 
-  hiddenInput: HTMLInputElement;
-
-  static loadFromServer = function (input: HTMLInputElement): (query, syncResults, asyncResults) => void {
-
-    let timeout;
-
-    return function findMatches(query, syncResults, asyncResults): void {
-
-      const root = input.getRootNode() as ShadowRoot | Document;
-      let suggest = root.getElementById(input.dataset.tobagoSuggestFor) as Suggest;
-
-      // todo: suggest.hiddenInput.value should contain the last query value
-      if (suggest.hiddenInput.value !== query) {
-
-        if (timeout) {
-          clearTimeout(timeout);
-        }
-
-        const delay = suggest.delay;
-
-        timeout = setTimeout(function (): void {
-          suggest.hiddenInput.value = query;
-          Suggest.asyncResults = asyncResults;
-          delete suggest.dataset.tobagoSuggestData;
-          console.info("query: '" + query + "'");
-
-          jsf.ajax.request(
-              suggest.id,
-              null, // todo: event?
-              {
-                "javax.faces.behavior.event": "suggest",
-                execute: suggest.id,
-                render: suggest.id
-              });
-        }, delay);
-
-      }
-    };
-  };
-
-  static fromClient = function (data): (query, syncResults) => void {
-    return (query, syncResults): void => {
-      const result = [];
-      for (let i = 0; i < data.length; i++) {
-        if (data[i].indexOf(query) >= 0) {
-          result.push(data[i]);
-        }
-      }
-      syncResults(result);
-    };
-  };
+  static timeout: number;
 
   constructor() {
     super();
-    this.hiddenInput = document.createElement("input");
-    this.hiddenInput.setAttribute("type", "hidden");
-    this.hiddenInput.setAttribute("name", this.id);
-    this.appendChild(this.hiddenInput);
+  }
+
+  get hiddenInput(): HTMLInputElement {
+    return this.querySelector(":scope > input[type=hidden]");
+  }
+
+  get suggestInput(): HTMLInputElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.getElementById(this.for) as HTMLInputElement;
   }
 
   get for(): string {
@@ -133,12 +88,12 @@
     this.setAttribute("total-count", String(totalCount));
   }
 
-  get data(): string[] {
-    return JSON.parse(this.getAttribute("data"));
+  get items(): string[] {
+    return JSON.parse(this.getAttribute("items"));
   }
 
-  set data(data: string[]) {
-    this.setAttribute("data", JSON.stringify(data));
+  set items(items: string[]) {
+    this.setAttribute("items", JSON.stringify(items));
   }
 
   get localMenu(): boolean {
@@ -154,76 +109,65 @@
   }
 
   connectedCallback(): void {
-    const root = this.getRootNode() as ShadowRoot | Document;
-    const input = root.getElementById(this.for) as HTMLInputElement;
-    const $input = jQuery(input);
 
-    if (this.update && input.classList.contains("tt-input")) { // already initialized: so only update data
-      if (Suggest.asyncResults) {
-        Suggest.asyncResults(this.data);
-        Suggest.asyncResults = null;
-      }
-    } else { // new
-      input.dataset.tobagoSuggestFor = this.id;
-      input.autocomplete = "off";
+    let vaadinComboBox: ComboBoxLightElement = this.suggestInput.parentElement;
 
-      let source;
-      if (this.update) {
-        source = Suggest.loadFromServer(input);
+    if (vaadinComboBox.tagName !== "VAADIN-COMBO-BOX-LIGHT") { // new
+      vaadinComboBox = document.createElement("vaadin-combo-box-light");
+      vaadinComboBox.attrForValue = "value";
+      vaadinComboBox.allowCustomValue = true;
+      vaadinComboBox.readOnly = this.suggestInput.readOnly;
+      vaadinComboBox.disabled = this.suggestInput.disabled;
+      this.suggestInput.classList.add("input"); // todo do this in SuggestRenderer?
+      const parent = this.suggestInput.parentElement;
+      vaadinComboBox.appendChild(this.suggestInput);
+      parent.appendChild(vaadinComboBox);
+
+      vaadinComboBox.dataProvider = function dataProvider(
+          params: { page: number, pageSize: number, filter: string },
+          callback: (items: String[], size: number) => {}): void {
+        console.info("call for data: %o", params);
+        console.info("vaadinComboBox id: %s", vaadinComboBox.id);
+        const suggest = vaadinComboBox.closest("tobago-in").querySelector("tobago-suggest") as Suggest;
+        suggest.hiddenInput.value = params.filter;
+        if (suggest.update) {
+          if (params.filter.length >= suggest.minChars) {
+            if (Suggest.timeout) {
+              clearTimeout(Suggest.timeout);
+            }
+            Suggest.timeout = window.setTimeout(function (): void {
+              Suggest.callback = callback;
+              jsf.ajax.request(
+                  suggest.id,
+                  null, // todo: event?
+                  {
+                    "javax.faces.behavior.event": "suggest",
+                    execute: suggest.id,
+                    render: suggest.id
+                  });
+            }, suggest.delay);
+          } else {
+            callback([], 0);
+          }
+        } else {
+          const items = suggest.items;
+          const filteredItems:string[] = [];
+          const lowerFilter = params.filter.toLocaleLowerCase();
+          for (const item of items) {
+            if (item.toLowerCase().indexOf(lowerFilter) > -1) {
+              filteredItems.push(item);
+            }
+          }
+          callback(filteredItems, filteredItems.length);
+        }
+      };
+    } else { // already initialized: so update items (from AJAX) only
+      if (Suggest.callback) {
+        Suggest.callback(this.items, this.totalCount);
+        Suggest.callback = null;
       } else {
-        source = Suggest.fromClient(this.data);
+        console.warn("Missing Suggest.callback!");
       }
-
-      let suggestPopup = root.getElementById(this.id + "::popup");
-      if (suggestPopup) {
-        suggestPopup.parentElement.removeChild(suggestPopup);
-      }
-      suggestPopup = document.createElement("div");
-      suggestPopup.id = this.id + "::popup";
-      suggestPopup.classList.add("tt-menu", "tt-empty");
-      root.querySelector(".tobago-page-menuStore").appendChild(suggestPopup);
-
-      const menu = this.localMenu ? null : suggestPopup;
-
-      $input.typeahead({
-        menu: menu,
-        minLength: this.minChars,
-        hint: true,// todo
-        highlight: true // todo
-      }, {
-        //name: 'test',// todo
-        limit: this.maxItems,
-        source: source
-      });
-      // old with jQuery:
-      $input.on("typeahead:change", function (event: JQuery.Event): void {
-        const input = this;
-        input.dispatchEvent(new Event("change"));
-      });
-      // new without jQuery:
-      // input.addEventListener("typeahead:change", (event: Event) => {
-      //   const input = event.currentTarget as HTMLInputElement;
-      //   input.dispatchEvent(new Event("change"));
-      // });
-
-      // old with jQuery:
-      $input.on("typeahead:open", function (event: JQuery.Event): void {
-        const input = this;
-        const suggestPopup = root.getElementById(input.dataset.tobagoSuggestFor + "::popup");
-        suggestPopup.style.top = DomUtils.offset(input).top + input.offsetHeight + "px";
-        suggestPopup.style.left = DomUtils.offset(input).left + "px";
-        suggestPopup.style.minWidth = input.offsetWidth + "px";
-      });
-
-      // new without jQuery:
-      // input.addEventListener("typeahead:open", (event: Event) => {
-      //   const input = event.currentTarget as HTMLInputElement;
-      //   const suggestPopup = document.getElementById(input.dataset.tobagoSuggestFor + "::popup");
-      //   suggestPopup.style.top = DomUtils.offset(input).top + input.offsetHeight + "px";
-      //   suggestPopup.style.left = DomUtils.offset(input).left + "px";
-      //   suggestPopup.style.minWidth = input.offsetWidth + "px";
-      // });
-
     }
   }
 }