TOBAGO-1994: TreeListbox is not working correctly
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUITree.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUITree.java
index 7bb1fb4..4a345af 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUITree.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUITree.java
@@ -37,23 +37,7 @@
  */
 public abstract class AbstractUITree extends AbstractUIData implements NamingContainer, Visual {
 
-  /**
-   * @deprecated since 2.0.0
-   */
-  @Deprecated
-  public static final String SEP = "-";
-
-  /**
-   * @deprecated since 2.0.0
-   */
-  @Deprecated
-  public static final String SELECT_STATE = SEP + "selectState";
-
-  /**
-   * @deprecated since 2.0.0
-   */
-  @Deprecated
-  public static final String MARKED = "marked";
+  public static final String SUFFIX_PARENT = "parent";
 
   private TreeState state;
 
@@ -87,20 +71,6 @@
     setRowIndex(-1);
   }
 
-  /**
-   * @deprecated since 2.0.0
-   */
-  @Deprecated
-  public UIComponent getRoot() {
-    // find the UITreeNode in the children.
-    for (final UIComponent child : getChildren()) {
-      if (child instanceof AbstractUITreeNodeBase) {
-        return child;
-      }
-    }
-    return null;
-  }
-
   @Override
   public boolean getRendersChildren() {
     return true;
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
index eb59af5..4f3ac05 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
@@ -656,6 +656,9 @@
       }
       final String parentId = sheet.getRowParentClientId();
       if (parentId != null) {
+        // TODO: replace with
+        // todo writer.writeIdAttribute(parentId + SUB_SEPARATOR + AbstractUITree.SUFFIX_PARENT);
+        // todo like in TreeListboxRenderer
         writer.writeAttribute(DataAttributes.TREE_PARENT, parentId, false);
       }
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeListboxRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeListboxRenderer.java
index ec2ca3d..c0a7991 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeListboxRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeListboxRenderer.java
@@ -20,6 +20,7 @@
 package org.apache.myfaces.tobago.internal.renderkit.renderer;
 
 import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.internal.component.AbstractUIData;
 import org.apache.myfaces.tobago.internal.component.AbstractUITree;
 import org.apache.myfaces.tobago.internal.component.AbstractUITreeLabel;
 import org.apache.myfaces.tobago.internal.component.AbstractUITreeListbox;
@@ -27,6 +28,7 @@
 import org.apache.myfaces.tobago.internal.component.AbstractUITreeSelect;
 import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
 import org.apache.myfaces.tobago.internal.util.JsonUtils;
+import org.apache.myfaces.tobago.internal.util.RenderUtils;
 import org.apache.myfaces.tobago.renderkit.RendererBase;
 import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
 import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
@@ -42,9 +44,17 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.apache.myfaces.tobago.util.ComponentUtils.SUB_SEPARATOR;
+
 public class TreeListboxRenderer extends RendererBase {
 
   @Override
+  public void decode(final FacesContext facesContext, final UIComponent component) {
+    final AbstractUITree tree = (AbstractUITree) component;
+    RenderUtils.decodedStateOfTreeData(facesContext, tree);
+  }
+
+  @Override
   public void encodeChildren(final FacesContext context, final UIComponent component) throws IOException {
     // will be rendered in encodeEnd()
   }
@@ -56,47 +66,23 @@
     final AbstractUITreeListbox tree = (AbstractUITreeListbox) component;
     final String clientId = tree.getClientId(facesContext);
     final Markup markup = tree.getMarkup();
-    //    final Style scrollDivStyle = new Style();
 
     writer.startElement(HtmlElements.DIV);
-//    scrollDivStyle.setWidth(Measure.valueOf(6 * 160)); // todo: depth * width of a select
-//    scrollDivStyle.setHeight(style.getHeight() // todo: what, when there is no scrollbar?
-//        .subtract(15)); // todo: scrollbar height
-//    scrollDivStyle.setPosition(Position.ABSOLUTE);
-//    writer.writeStyleAttribute(scrollDivStyle);
-
-    writer.startElement(HtmlElements.DIV);
-    // todo: the id must be in this DIV
+    writer.writeIdAttribute(clientId);
     writer.writeAttribute(DataAttributes.MARKUP, JsonUtils.encode(markup), false);
     writer.writeClassAttribute(
         TobagoClass.TREE_LISTBOX,
         TobagoClass.TREE_LISTBOX.createMarkup(markup));
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, tree);
+    writer.writeAttribute(DataAttributes.SELECTION_MODE, tree.getSelectable().name(), false);
 
     writer.startElement(HtmlElements.INPUT);
     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
-    writer.writeNameAttribute(clientId);
-    writer.writeIdAttribute(clientId);
-    writer.writeAttribute(HtmlAttributes.VALUE, ";", false);
+    writer.writeNameAttribute(clientId + SUB_SEPARATOR + AbstractUIData.SUFFIX_SELECTED);
+    writer.writeIdAttribute(clientId + SUB_SEPARATOR + AbstractUIData.SUFFIX_SELECTED);
+    writer.writeAttribute(HtmlAttributes.VALUE, JsonUtils.encodeEmptyArray(), false);
     writer.endElement(HtmlElements.INPUT);
 
-    writer.startElement(HtmlElements.INPUT);
-    writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
-    writer.writeNameAttribute(clientId + ComponentUtils.SUB_SEPARATOR + AbstractUITree.SUFFIX_MARKED);
-    writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + AbstractUITree.SUFFIX_MARKED);
-    writer.writeAttribute(HtmlAttributes.VALUE, "", false);
-    writer.endElement(HtmlElements.INPUT);
-
-    if (tree.getSelectable().isSupportedByTreeListbox()) {
-      writer.startElement(HtmlElements.INPUT);
-      writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
-      writer.writeNameAttribute(clientId + AbstractUITree.SELECT_STATE);
-      writer.writeIdAttribute(clientId + AbstractUITree.SELECT_STATE);
-      writer.writeAttribute(HtmlAttributes.VALUE, ";", false);
-      writer.writeAttribute(DataAttributes.SELECTION_MODE, tree.getSelectable().name(), false);
-      writer.endElement(HtmlElements.INPUT);
-    }
-
     List<Integer> thisLevel = new ArrayList<>();
     thisLevel.add(0);
     List<Integer> nextLevel = new ArrayList<>();
@@ -126,7 +112,7 @@
         writer.endElement(HtmlElements.SELECT);
       }
 
-      for(final Integer rowIndex : thisLevel) {
+      for (final Integer rowIndex : thisLevel) {
         encodeSelectBox(facesContext, tree, writer, rowIndex, nextLevel, size);
       }
 
@@ -139,7 +125,6 @@
     }
 
     writer.endElement(HtmlElements.DIV);
-    writer.endElement(HtmlElements.DIV);
 
     tree.setRowIndex(-1);
   }
@@ -156,9 +141,7 @@
 
     writer.startElement(HtmlElements.SELECT);
     writer.writeClassAttribute(TobagoClass.TREE_LISTBOX__SELECT);
-    if (parentId != null) {
-      writer.writeAttribute(DataAttributes.TREE_PARENT, parentId, false);
-    }
+    writer.writeIdAttribute(parentId + SUB_SEPARATOR + AbstractUITree.SUFFIX_PARENT);
 
     writer.writeAttribute(HtmlAttributes.SIZE, size);
 //    writer.writeAttribute(HtmlAttributes.MULTIPLE, siblingMode);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
index 80c1f9d..27d7251 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
@@ -30,6 +30,8 @@
 import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
 import org.apache.myfaces.tobago.internal.util.JsonUtils;
 import org.apache.myfaces.tobago.model.Selectable;
+import org.apache.myfaces.tobago.model.SelectedState;
+import org.apache.myfaces.tobago.model.TreePath;
 import org.apache.myfaces.tobago.renderkit.RendererBase;
 import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
 import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
@@ -68,12 +70,13 @@
       final String clientId = data.getClientId(facesContext);
       final String nodeStateId = node.nodeStateId(facesContext);
       final Map<String, String> requestParameterMap = facesContext.getExternalContext().getRequestParameterMap();
-      final String id = node.getClientId(facesContext);
+      final String nodeId = node.getClientId(facesContext);
       final boolean folder = node.isFolder();
 
       // expand state
       if (folder) {
-        final boolean expanded = Boolean.parseBoolean(requestParameterMap.get(id + "-expanded"));
+        final boolean expanded = Boolean.parseBoolean(requestParameterMap.get(
+            nodeId + ComponentUtils.SUB_SEPARATOR + AbstractUITree.SUFFIX_EXPANDED));
 /* XXX check
       if (node.isExpanded() != expanded) {
         new TreeExpansionEvent(node, node.isExpanded(), expanded).queue();
@@ -83,7 +86,12 @@
 
       // select
       if (data.getSelectable() != Selectable.none) { // selection
-        final String selected = requestParameterMap.get(clientId + AbstractUITree.SELECT_STATE);
+         String selected = requestParameterMap.get(
+            clientId + ComponentUtils.SUB_SEPARATOR + AbstractUIData.SUFFIX_SELECTED);
+// todo        JsonUtils.decodeIntegerArray()StringArray()
+        selected = selected.replaceAll("\\[", ";");
+        selected = selected.replaceAll("]", ";");
+        selected = selected.replaceAll(",", ";");
         final String searchString = ";" + node.getClientId(facesContext) + ";";
         final AbstractUITreeSelect treeSelect = ComponentUtils.findDescendant(node, AbstractUITreeSelect.class);
         if (treeSelect != null) {
@@ -120,12 +128,14 @@
     final boolean visible = data.isRowVisible();
     final boolean folder = node.isFolder();
     Markup markup = Markup.NULL;
-    if (data instanceof AbstractUITree && data.getSelectedState().isSelected(node.getPath())) {
+    final TreePath path = node.getPath();
+    final SelectedState selectedState = data.getSelectedState();
+    if (data instanceof AbstractUITree && selectedState.isSelected(path)) {
       markup = markup.add(Markup.SELECTED);
     }
     if (folder) {
       markup = markup.add(Markup.FOLDER);
-      if (data.getExpandedState().isExpanded(node.getPath())) {
+      if (data.getExpandedState().isExpanded(path)) {
         markup = markup.add(Markup.EXPANDED);
       }
     }
@@ -139,7 +149,8 @@
       writer.writeAttribute(HtmlAttributes.VALUE, clientId, true);
       writer.writeIdAttribute(clientId);
       writer.writeAttribute(DataAttributes.MARKUP, JsonUtils.encode(markup), false);
-      writer.writeAttribute(HtmlAttributes.SELECTED, folder);
+      writer.writeAttribute(HtmlAttributes.SELECTED, selectedState.isAncestorOfSelected(path));
+      writer.writeAttribute(DataAttributes.ROW_INDEX, data.getRowIndex());
     } else {
       writer.startElement(HtmlElements.DIV);
 
@@ -157,6 +168,9 @@
           node.getCustomClass());
       HtmlRendererUtils.writeDataAttributes(facesContext, writer, node);
       if (parentId != null) {
+        // TODO: replace with
+        // todo writer.writeIdAttribute(parentId + SUB_SEPARATOR + AbstractUITree.SUFFIX_PARENT);
+        // todo like in TreeListboxRenderer
         writer.writeAttribute(DataAttributes.TREE_PARENT, parentId, false);
       }
       writer.writeAttribute(DataAttributes.LEVEL, data.isShowRoot() ? node.getLevel() : node.getLevel() - 1);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/JsonUtils.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/JsonUtils.java
index 70aa35e..4d9d18f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/JsonUtils.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/JsonUtils.java
@@ -362,4 +362,8 @@
     builder.append(']');
     return builder.toString();
   }
+
+  public static String encodeEmptyArray() {
+    return "[]";
+  }
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/RenderUtils.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/RenderUtils.java
index b67a8b2..9f25509 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/RenderUtils.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/RenderUtils.java
@@ -226,7 +226,11 @@
     try {
       string = facesContext.getExternalContext().getRequestParameterMap().get(key);
       if (string != null) {
-        return StringUtils.parseIntegerList(string);
+        if (string.startsWith("[")) {
+          return JsonUtils.decodeIntegerArray(string);
+        } else {
+          return StringUtils.parseIntegerList(string); // todo remove this case after migrating all to JSON
+        }
       }
     } catch (final Exception e) {
       // should not happen
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/StringUtils.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/StringUtils.java
index a677b1b..30ad0e8 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/StringUtils.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/StringUtils.java
@@ -31,7 +31,7 @@
   }
 
   public static List<Integer> parseIntegerList(final String integerList) throws NumberFormatException {
-    return parseIntegerList(integerList, ", ");
+    return parseIntegerList(integerList, ", ;");
   }
 
   public static List<Integer> parseIntegerList(final String integerList, final String delimiters)
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectedState.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectedState.java
index f1e4357..d01d97e 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectedState.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectedState.java
@@ -32,13 +32,30 @@
   private Set<TreePath> selectedPaths = new HashSet<>();
 
   /**
-   * Checks if the given is selected.
+   * Checks if the given path is selected.
    */
   public boolean isSelected(final TreePath path) {
     return selectedPaths.contains(path);
   }
 
   /**
+   * Checks if the given path is an ancestor of a selected node.
+   */
+  public boolean isAncestorOfSelected(final TreePath ancestorPath) {
+    if (ancestorPath.isRoot()) {
+      return !selectedPaths.isEmpty();
+    }
+    for (TreePath selectedPath : selectedPaths) {
+      for (TreePath p = selectedPath; !p.isRoot(); p = p.getParent()) {
+        if (p.equals(ancestorPath)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
    * Select the given path.
    */
   public void select(final TreePath path) {
@@ -53,8 +70,7 @@
   }
 
   /**
-   * Set the selected path and remove all prior selections.
-   * This is useful for "single selection" mode.
+   * Set the selected path and remove all prior selections. This is useful for "single selection" mode.
    */
   public void clearAndSelect(final TreePath path) {
     clear();
@@ -78,4 +94,9 @@
       unselect(path);
     }
   }
+
+  @Override
+  public String toString() {
+    return selectedPaths.toString();
+  }
 }
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 b78c536..7825257 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
@@ -147,7 +147,7 @@
   ROW_ACTION("data-tobago-row-action"),
 
   /*
-   * Holds the index of the row in a sheet, if the sheed has a rowRendered attribute.
+   * Holds the index of the row in a sheet, if the sheet has a rowRendered attribute.
    */
   ROW_INDEX("data-tobago-row-index"),
 
diff --git a/tobago-core/src/main/resources/scss/_tobago.scss b/tobago-core/src/main/resources/scss/_tobago.scss
index 012c974..eeb9999 100644
--- a/tobago-core/src/main/resources/scss/_tobago.scss
+++ b/tobago-core/src/main/resources/scss/_tobago.scss
@@ -1500,9 +1500,6 @@
 .tobago-tree-expanded,
 .tobago-tree-selected,
 .tobago-treeLabel,
-.tobago-treeListbox,
-.tobago-treeListbox-level,
-.tobago-treeListbox-select,
 .tobago-treeSelect,
 .tobago-treeSelect-label {
 }
@@ -1534,6 +1531,19 @@
   }
 }
 
+/* treeListbox ---------------------------------------------------------------------- */
+.tobago-treeListbox {
+}
+
+.tobago-treeListbox-level {
+  display: inline-block;
+  min-width: 10rem;
+}
+
+.tobago-treeListbox-select {
+  width: 100%;
+}
+
 /* textarea --------------------------------------------------------- */
 .tobago-textarea {
   &:disabled {
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectedStateUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectedStateUnitTest.java
new file mode 100644
index 0000000..318c3ba
--- /dev/null
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectedStateUnitTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.model;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SelectedStateUnitTest {
+
+  @Test
+  public void testAncestorOfSelected() {
+    SelectedState state = new SelectedState();
+    state.select(new TreePath(0, 0));
+    state.select(new TreePath(1, 1, 1));
+
+    Assert.assertTrue(state.isAncestorOfSelected(new TreePath()));
+    Assert.assertTrue(state.isAncestorOfSelected(new TreePath(0)));
+    Assert.assertTrue(state.isAncestorOfSelected(new TreePath(0, 0)));
+    Assert.assertTrue(state.isAncestorOfSelected(new TreePath(1)));
+    Assert.assertTrue(state.isAncestorOfSelected(new TreePath(1, 1)));
+    Assert.assertTrue(state.isAncestorOfSelected(new TreePath(1, 1, 1)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(2)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(0, 1)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(1, 0)));
+  }
+
+  @Test
+  public void testAncestorOfSelectedEmpty() {
+    SelectedState state = new SelectedState();
+
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath()));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(0)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(0, 0)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(1)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(1, 1)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(1, 1, 1)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(2)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(0, 1)));
+    Assert.assertFalse(state.isAncestorOfSelected(new TreePath(1, 0)));
+  }
+
+  @Test
+  public void testSelected() {
+    SelectedState state = new SelectedState();
+    state.select(new TreePath(0, 0));
+    state.select(new TreePath(1, 1, 1));
+
+    Assert.assertFalse(state.isSelected(new TreePath()));
+    Assert.assertFalse(state.isSelected(new TreePath(0)));
+    Assert.assertTrue(state.isSelected(new TreePath(0, 0)));
+    Assert.assertFalse(state.isSelected(new TreePath(1)));
+    Assert.assertFalse(state.isSelected(new TreePath(1, 1)));
+    Assert.assertTrue(state.isSelected(new TreePath(1, 1, 1)));
+    Assert.assertFalse(state.isSelected(new TreePath(2)));
+    Assert.assertFalse(state.isSelected(new TreePath(0, 1)));
+    Assert.assertFalse(state.isSelected(new TreePath(1, 0)));
+  }
+
+}
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeListboxController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeListboxController.java
new file mode 100644
index 0000000..97ca6a8
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeListboxController.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.example.demo;
+
+import org.apache.myfaces.tobago.model.TreePath;
+import org.apache.myfaces.tobago.model.TreeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.enterprise.context.SessionScoped;
+import javax.faces.context.FacesContext;
+import javax.inject.Named;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+
+@SessionScoped
+@Named
+public class TreeListboxController implements Serializable {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private DefaultMutableTreeNode sample;
+
+  private TreeState state;
+
+  public TreeListboxController() {
+    sample = CategoryTree.createSample();
+    state = new TreeState();
+    state.getSelectedState().select(new TreePath(2, 2)); // world music
+  }
+
+  public String submit() {
+    LOG.info("Selected: {}", state.getSelectedState());
+    return FacesContext.getCurrentInstance().getViewRoot().getViewId();
+  }
+
+  public DefaultMutableTreeNode getSample() {
+    return sample;
+  }
+
+  public TreeState getState() {
+    return state;
+  }
+
+  public void setState(TreeState state) {
+    this.state = state;
+  }
+
+//  public String getSelectedNodes() {
+//    return TreeUtils.getSelectedNodes(sample);
+//  }
+
+}
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeSelectController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeSelectController.java
index 80fcd9a..7165cde 100644
--- a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeSelectController.java
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeSelectController.java
@@ -60,37 +60,10 @@
 
   public void setSelectable(final String selectable) {
     this.selectable = selectable;
-    resetSelection(sample);
-  }
-
-  public void resetSelection(final DefaultMutableTreeNode node) {
-    final Node userObject = (Node) node.getUserObject();
-    userObject.setSelected(false);
-    for (int i = 0; i < node.getChildCount(); i++) {
-      final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i);
-      resetSelection(child);
-    }
+    TreeUtils.resetSelection(sample);
   }
 
   public String getSelectedNodes() {
-    final StringBuilder stringBuilder = new StringBuilder();
-    buildSelectedNodesString(stringBuilder, sample);
-    if (stringBuilder.length() > 2) {
-      return stringBuilder.substring(2); // Remove ', '.
-    } else {
-      return "";
-    }
-  }
-
-  private void buildSelectedNodesString(final StringBuilder stringBuilder, final DefaultMutableTreeNode node) {
-    final Node userObject = (Node) node.getUserObject();
-    if (userObject.isSelected()) {
-      stringBuilder.append(", ");
-      stringBuilder.append(userObject.getName());
-    }
-    for (int i = 0; i < node.getChildCount(); i++) {
-      final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i);
-      buildSelectedNodesString(stringBuilder, child);
-    }
+    return TreeUtils.getSelectedNodes(sample);
   }
 }
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeUtils.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeUtils.java
new file mode 100644
index 0000000..3401454
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/TreeUtils.java
@@ -0,0 +1,41 @@
+package org.apache.myfaces.tobago.example.demo;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+public class TreeUtils {
+
+  private TreeUtils() {
+  }
+
+  public static void resetSelection(final DefaultMutableTreeNode node) {
+    final Node userObject = (Node) node.getUserObject();
+    userObject.setSelected(false);
+    for (int i = 0; i < node.getChildCount(); i++) {
+      final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i);
+      resetSelection(child);
+    }
+  }
+
+  public static String getSelectedNodes(final DefaultMutableTreeNode treeNode) {
+    final StringBuilder stringBuilder = new StringBuilder();
+    buildSelectedNodesString(stringBuilder, treeNode);
+    if (stringBuilder.length() > 2) {
+      return stringBuilder.substring(2); // Remove ', '.
+    } else {
+      return "";
+    }
+  }
+
+  private static void buildSelectedNodesString(final StringBuilder stringBuilder, final DefaultMutableTreeNode node) {
+    final Node userObject = (Node) node.getUserObject();
+    if (userObject.isSelected()) {
+      stringBuilder.append(", ");
+      stringBuilder.append(userObject.getName());
+    }
+    for (int i = 0; i < node.getChildCount(); i++) {
+      final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i);
+      buildSelectedNodesString(stringBuilder, child);
+    }
+  }
+
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/04-listbox/Tree_Listbox.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/04-listbox/Tree_Listbox.xhtml
index 7324073..3304530 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/04-listbox/Tree_Listbox.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/04-listbox/Tree_Listbox.xhtml
@@ -17,7 +17,7 @@
  * limitations under the License.
 -->
 
-<ui:composition template="/main.xhtml"
+<ui:composition template="/plain.xhtml"
                 xmlns="http://www.w3.org/1999/xhtml"
                 xmlns:tc="http://myfaces.apache.org/tobago/component"
                 xmlns:ui="http://java.sun.com/jsf/facelets">
@@ -28,12 +28,16 @@
            link="#{apiController.base}/doc/#{apiController.currentRelease}/tld/tc/treeListbox.html"/>
 
   <tc:section label="Example">
-    <pre><code class="language-markup">&lt;tc:treeListbox value="\#{treeController.sample}" ...></code></pre>
-    <tc:treeListbox value="#{treeController.sample}" var="node">
-      <tc:treeNode>
-        <tc:treeIndent/>
-        <tc:treeLabel value="#{node.userObject.name}"/>
+    <pre><code class="language-markup">&lt;tc:treeListbox value="\#{treeListboxController.sample}" ...></code></pre>
+    <tc:treeListbox id="listbox" value="#{treeListboxController.sample}" var="node" state="#{treeListboxController.state}">
+      <tc:treeNode id="node">
+        <tc:treeLabel id="label" value="#{node.userObject.name}"/>
       </tc:treeNode>
     </tc:treeListbox>
+
+    <tc:button label="Submit" action="#{treeListboxController.submit}"/>
+
+    <tc:in readonly="true" label="Selection" tip="as set of tree pathes" value="#{treeListboxController.state.selectedState}"/>
+<!--    <tc:in readonly="true" label="Selection" value="#{treeListboxController.selectedNodes}"/>-->
   </tc:section>
 </ui:composition>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/90000-attic/treeListbox/TreeListbox.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/90000-attic/treeListbox/TreeListbox.xhtml
deleted file mode 100644
index fb4720d..0000000
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/90000-attic/treeListbox/TreeListbox.xhtml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
--->
-
-<!-- XXX This is an old page. Content might not be up to date. Needs to be refactored, or just deleted. -->
-<f:view
-    xmlns:tc="http://myfaces.apache.org/tobago/component"
-    xmlns:ui="http://java.sun.com/jsf/facelets"
-    xmlns:f="http://java.sun.com/jsf/core">
-
-  <tc:page>
-    <!-- <tc:gridLayoutConstraint width="600px" height="300px"/> -->
-
-    <tc:treeListbox id="tree" value="#{treeTestController.tree}" var="node">
-      <tc:treeNode id="template">
-        <tc:treeLabel value="#{node.userObject.name}"/>
-      </tc:treeNode>
-    </tc:treeListbox>
-
-  </tc:page>
-</f:view>
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts
index 128229e..eebe119 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts
@@ -34,14 +34,14 @@
   mousemoveData: any;
   mousedownOnRowData: any;
 
-  static init = function (element: HTMLElement) {
+  static init(element: HTMLElement) {
     console.time("[tobago-sheet] init");
     for (const sheetElement of DomUtils.selfOrElementsByClassName(element, "tobago-sheet")) {
       const sheet = new Sheet(sheetElement);
       Sheet.SHEETS.set(sheet.id, sheet);
     }
     console.timeEnd("[tobago-sheet] init");
-  };
+  }
 
   private static getScrollBarSize() {
     const body = document.getElementsByTagName("body").item(0);
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree-listbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree-listbox.ts
index 94b8821..8026b54 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree-listbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree-listbox.ts
@@ -16,74 +16,88 @@
  */
 
 import {Listener, Phase} from "./tobago-listener";
-import {Tobago4Utils} from "./tobago-utils";
+import {DomUtils} from "./tobago-utils";
 
 class TreeListbox {
-  static init = function (elements) {
-    elements = elements.jQuery ? elements : jQuery(elements); // fixme jQuery -> ES5
-    var treeListbox = Tobago4Utils.selectWithJQuery(elements, ".tobago-treeListbox");
-    // hide select tags for level > root
-    treeListbox.children().find("select:not(:first)").hide();
 
-    var listboxSelects = treeListbox.find("select");
+  id: string;
 
-    listboxSelects.children("option").each(TreeListbox.initNextLevel);
-    listboxSelects.each(TreeListbox.initListeners);
-  };
-
-// find all option tags and add the dedicated select tag in its data section.
-  static initNextLevel = function () {
-    var option = jQuery(this);
-    var select = option.closest(".tobago-treeListbox-level").next()
-        .find("[data-tobago-tree-parent='" + option.attr("id") + "']");
-    if (select.length == 1) {
-      option.data("tobago-select", select);
-    } else {
-      var empty = option.closest(".tobago-treeListbox-level").next().children(":first");
-      option.data("tobago-select", empty);
+  static init = function (element) {
+    for (const treeListbox of DomUtils.selfOrElementsByClassName(element, "tobago-treeListbox")) {
+      new TreeListbox(treeListbox);
     }
   };
 
-// add on change on all select tag, all options that are not selected hide there dedicated
-// select tag, and the selected option show its dedicated select tag.
-  static initListeners = function () {
+  constructor(element: HTMLElement) {
 
-    jQuery(this).change(TreeListbox.onChange);
+    this.id = element.id;
 
-    jQuery(this).focus(function () {
-      jQuery(this).change();
-    });
-  };
+    const selects = element.getElementsByTagName("select");
+    for (let i = 0; i < selects.length; i++) {
+      const listbox = <HTMLSelectElement>selects.item(i);
+      // hide select tags for level > root
+      if (listbox.previousElementSibling) {
+        listbox.classList.add("d-none");
+      }
 
-  static onChange = function () {
-    var listbox = jQuery(this);
-    listbox.children("option:not(:selected)").each(function () {
-      jQuery(this).data("tobago-select").hide();
-    });
-    listbox.children("option:selected").each(function () {
-      jQuery(this).data("tobago-select").show();
-    });
-    TreeListbox.setSelected(listbox);
+      // add on change on all select tag, all options that are not selected hide there dedicated
+      // select tag, and the selected option show its dedicated select tag.
+      if (!listbox.disabled) {
+        listbox.addEventListener("change", this.onChange.bind(this));
+      }
+    }
+  }
+
+  onChange(event: TextEvent) {
+    let listbox = <HTMLSelectElement>event.currentTarget;
+    for (const child of listbox.children) {
+      const option = <HTMLOptionElement>child;
+      if (option.tagName === "OPTION") {
+        if (option.selected) {
+          this.setSelected(option);
+          let select = <HTMLSelectElement>document.getElementById(option.id + DomUtils.SUB_COMPONENT_SEP + "parent");
+          if (!select) {
+            select = <HTMLSelectElement>listbox.parentElement.nextElementSibling.children[0]; // dummy
+          }
+          select.classList.remove("d-none");
+          for (const sibling of listbox.parentElement.nextElementSibling.children) {
+            if (sibling === select) {
+              (<HTMLElement>sibling).classList.remove("d-none");
+            } else {
+              (<HTMLElement>sibling).classList.add("d-none");
+            }
+          }
+        }
+      }
+    }
 
     // Deeper level (2nd and later) should only show the empty select tag.
     // The first child is the empty selection.
-    listbox.parent().nextAll(":not(:first)").each(function () {
-      jQuery(this).children(":not(:first)").hide();
-      jQuery(this).children(":first").show();
-    });
 
-  };
-
-  static setSelected = function (listbox) {
-    var hidden = listbox.closest(".tobago-treeListbox").children("[data-tobago-selection-mode]");
-    if (hidden.length == 1) {
-      var selectedValue = ";";
-      listbox.children("option:selected").each(function () {
-        selectedValue += jQuery(this).attr("id") + ";";
-      });
-      hidden.val(selectedValue);
+    let next = listbox.parentElement.nextElementSibling;
+    if (next) {
+      for (next = next.nextElementSibling; next; next = next.nextElementSibling) {
+        for (const child of next.children) {
+          const select = <HTMLSelectElement>child;
+          if (select.previousElementSibling) { // is not the first
+            select.classList.add("d-none");
+          } else { // is the first
+            select.classList.remove("d-none");
+          }
+        }
+      }
     }
-  };
+  }
+
+  setSelected(option: HTMLOptionElement) {
+    const hidden = <HTMLInputElement>document.getElementById(this.id + DomUtils.SUB_COMPONENT_SEP + "selected");
+    if (hidden) {
+      let value = <number[]>JSON.parse(hidden.value);
+      value = []; // todo: multi-select
+      value.push(parseInt(option.dataset["tobagoRowIndex"]));
+      hidden.value = JSON.stringify(value);
+    }
+  }
 }
 
 Listener.register(TreeListbox.init, Phase.DOCUMENT_READY);