You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2022/12/02 16:06:06 UTC
[myfaces-tobago] 01/02: feat: selectManyList component
This is an automated email from the ASF dual-hosted git repository.
hnoeth pushed a commit to branch tobago-5.x
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git
commit eb2cca850e1f692304ff7173bfd1f878b9b792a5
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Wed Oct 26 22:01:10 2022 +0200
feat: selectManyList component
* add new tc:selectManyList component
Issue: TOBAGO-2159
---
.../myfaces/tobago/component/RendererTypes.java | 2 +
.../myfaces/tobago/component/SupportsFilter.java | 25 ++
.../org/apache/myfaces/tobago/component/Tags.java | 2 +
.../component/AbstractUISelectManyList.java | 84 ++++
.../renderkit/renderer/SelectManyListRenderer.java | 209 ++++++++++
.../component/SelectManyListTagDeclaration.java | 94 +++++
.../internal/taglib/declaration/HasFilter.java | 29 ++
.../tobago/renderkit/css/BootstrapClass.java | 3 +
.../myfaces/tobago/renderkit/css/TobagoClass.java | 5 +
.../tobago/renderkit/html/HtmlElements.java | 1 +
.../example/demo/SelectManyListController.java | 138 +++++++
.../apache/myfaces/tobago/example/demo/names.txt | 75 ++++
.../80-selectManyList/SelectManyList.xhtml | 87 +++++
tobago-theme/jest.config.js | 5 +-
tobago-theme/src/main/scss/_tobago.scss | 161 ++++++++
.../src/main/resources/META-INF/tobago-config.xml | 3 +
.../src/main/ts/tobago-all.ts | 1 +
.../src/main/ts/tobago-filter-registry.test.ts | 124 ++++++
.../src/main/ts/tobago-filter-registry.ts | 102 +++++
.../src/main/ts/tobago-select-many-list.ts | 421 +++++++++++++++++++++
20 files changed, 1570 insertions(+), 1 deletion(-)
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
index 27e90ae5fe..d77b7b1c31 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
@@ -68,6 +68,7 @@ public enum RendererTypes {
SelectBooleanCheckbox,
SelectBooleanToggle,
SelectManyCheckbox,
+ SelectManyList,
SelectManyListbox,
SelectManyShuttle,
SelectOneChoice,
@@ -141,6 +142,7 @@ public enum RendererTypes {
public static final String SELECT_BOOLEAN_CHECKBOX = "SelectBooleanCheckbox";
public static final String SELECT_BOOLEAN_TOGGLE = "SelectBooleanToggle";
public static final String SELECT_MANY_CHECKBOX = "SelectManyCheckbox";
+ public static final String SELECT_MANY_LIST = "SelectManyList";
public static final String SELECT_MANY_LISTBOX = "SelectManyListbox";
public static final String SELECT_MANY_SHUTTLE = "SelectManyShuttle";
public static final String SELECT_ONE_CHOICE = "SelectOneChoice";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/SupportsFilter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/SupportsFilter.java
new file mode 100644
index 0000000000..a61bdbf953
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/SupportsFilter.java
@@ -0,0 +1,25 @@
+/*
+ * 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.component;
+
+public interface SupportsFilter {
+
+ String getFilter();
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
index 6620e764e2..9b6050a801 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
@@ -75,6 +75,7 @@ public enum Tags {
selectItem,
selectItems,
selectManyCheckbox,
+ selectManyList,
selectManyListbox,
selectManyShuttle,
selectOneChoice,
@@ -147,6 +148,7 @@ public enum Tags {
public static final String SELECT_ITEM = "selectItem";
public static final String SELECT_ITEMS = "selectItems";
public static final String SELECT_MANY_CHECKBOX = "selectManyCheckbox";
+ public static final String SELECT_MANY_LIST = "selectManyList";
public static final String SELECT_MANY_LISTBOX = "selectManyListbox";
public static final String SELECT_MANY_SHUTTLE = "selectManyShuttle";
public static final String SELECT_ONE_CHOICE = "selectOneChoice";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyList.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyList.java
new file mode 100644
index 0000000000..952c1ddb9f
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyList.java
@@ -0,0 +1,84 @@
+/*
+ * 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.internal.component;
+
+import org.apache.myfaces.tobago.component.SupportFieldId;
+import org.apache.myfaces.tobago.component.SupportsAutoSpacing;
+import org.apache.myfaces.tobago.component.SupportsFilter;
+import org.apache.myfaces.tobago.component.SupportsHelp;
+import org.apache.myfaces.tobago.component.SupportsLabelLayout;
+import org.apache.myfaces.tobago.component.Visual;
+import org.apache.myfaces.tobago.internal.taglib.component.SelectManyListTagDeclaration;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+
+import javax.faces.component.behavior.ClientBehaviorHolder;
+import javax.faces.context.FacesContext;
+import java.util.Collection;
+
+/**
+ * {@link SelectManyListTagDeclaration}
+ */
+public abstract class AbstractUISelectManyList extends AbstractUISelectManyBase
+ implements SupportsAutoSpacing, Visual, SupportsLabelLayout, ClientBehaviorHolder, SupportsHelp, SupportFieldId,
+ SupportsFilter {
+
+ private transient boolean nextToRenderIsLabel;
+
+ @Override
+ public Object[] getSelectedValues() {
+ final Object value = getValue();
+ if (value instanceof Collection) {
+ return ((Collection) value).toArray();
+ } else {
+ return (Object[]) value;
+ }
+ }
+
+ @Override
+ public String getFieldId(final FacesContext facesContext) {
+ return getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + "field";
+ }
+
+ public abstract Integer getTabIndex();
+
+ public abstract boolean isDisabled();
+
+ public abstract boolean isInline();
+
+ public boolean isError() {
+ final FacesContext facesContext = FacesContext.getCurrentInstance();
+ return !isValid()
+ || !facesContext.getMessageList(getClientId(facesContext)).isEmpty();
+ }
+
+ public abstract boolean isFocus();
+
+ public abstract String getFilter();
+
+ @Override
+ public boolean isNextToRenderIsLabel() {
+ return nextToRenderIsLabel;
+ }
+
+ @Override
+ public void setNextToRenderIsLabel(final boolean nextToRenderIsLabel) {
+ this.nextToRenderIsLabel = nextToRenderIsLabel;
+ }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
new file mode 100644
index 0000000000..d6ebee277d
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
@@ -0,0 +1,209 @@
+/*
+ * 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.internal.renderkit.renderer;
+
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.internal.component.AbstractUISelectManyList;
+import org.apache.myfaces.tobago.internal.util.ArrayUtils;
+import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
+import org.apache.myfaces.tobago.internal.util.SelectItemUtils;
+import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
+import org.apache.myfaces.tobago.renderkit.css.CssItem;
+import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.Arias;
+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;
+import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
+
+import javax.faces.context.FacesContext;
+import javax.faces.model.SelectItem;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectManyListRenderer<T extends AbstractUISelectManyList> extends SelectManyRendererBase<T> {
+ @Override
+ public HtmlElements getComponentTag() {
+ return HtmlElements.TOBAGO_SELECT_MANY_LIST;
+ }
+
+ @Override
+ protected CssItem[] getComponentCss(final FacesContext facesContext, final T component) {
+ final List<SelectItem> items = SelectItemUtils.getItemList(facesContext, component);
+ final boolean disabled = !items.iterator().hasNext() || component.isDisabled() || component.isReadonly();
+
+ List<CssItem> cssItems = new ArrayList<>();
+ if (disabled) {
+ cssItems.add(TobagoClass.DISABLED);
+ }
+ return cssItems.toArray(new CssItem[0]);
+ }
+
+ @Override
+ protected String getFieldId(FacesContext facesContext, T component) {
+ return component.getFieldId(facesContext);
+ }
+
+ @Override
+ protected void encodeBeginField(FacesContext facesContext, T component) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+ final String clientId = component.getClientId(facesContext);
+ final String fieldId = component.getFieldId(facesContext);
+ final String filterId = clientId + ComponentUtils.SUB_SEPARATOR + "filter";
+ final String selectedId = clientId + ComponentUtils.SUB_SEPARATOR + "selected";
+ final List<SelectItem> items = SelectItemUtils.getItemList(facesContext, component);
+ final boolean disabled = !items.iterator().hasNext() || component.isDisabled() || component.isReadonly();
+ final String filter = component.getFilter();
+ final boolean inline = component.isInline();
+ final Markup markup = component.getMarkup();
+ final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, component);
+ final Integer tabIndex = component.getTabIndex();
+
+ encodeHiddenSelect(facesContext, component, items, clientId, selectedId, disabled);
+
+ writer.startElement(HtmlElements.DIV);
+ writer.writeClassAttribute(
+ inline ? BootstrapClass.LIST_GROUP : BootstrapClass.DROPDOWN,
+ inline ? BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)) : null);
+
+ encodeSelectField(facesContext, component, clientId, fieldId, filterId, filter, disabled, inline, title, tabIndex);
+ encodeOptions(facesContext, component, items, clientId, inline, disabled);
+
+ writer.endElement(HtmlElements.DIV);
+ }
+
+ @Override
+ protected void writeAdditionalAttributes(FacesContext facesContext, TobagoResponseWriter writer, T input)
+ throws IOException {
+ super.writeAdditionalAttributes(facesContext, writer, input);
+ writer.writeAttribute(CustomAttributes.FILTER, input.getFilter(), true);
+ }
+
+ private void encodeHiddenSelect(final FacesContext facesContext, final T component, final List<SelectItem> items,
+ final String clientId, final String selectedId, final boolean disabled) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+ writer.startElement(HtmlElements.SELECT);
+ writer.writeIdAttribute(selectedId);
+ writer.writeNameAttribute(clientId);
+ writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
+ writer.writeAttribute(HtmlAttributes.REQUIRED, component.isRequired());
+ writer.writeClassAttribute(BootstrapClass.D_NONE);
+ writer.writeAttribute(HtmlAttributes.MULTIPLE, true);
+
+ final Object[] values = component.getSelectedValues();
+ final String[] submittedValues = getSubmittedValues(component);
+ renderSelectItems(component, null, items, values, submittedValues, writer, facesContext);
+ writer.endElement(HtmlElements.SELECT);
+ }
+
+ private void encodeSelectField(final FacesContext facesContext, final T component,
+ final String clientId, final String fieldId, final String filterId, final String filter, final boolean disabled,
+ final boolean inline, final String title, final Integer tabIndex) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+ writer.startElement(HtmlElements.DIV);
+ writer.writeIdAttribute(fieldId);
+ writer.writeNameAttribute(clientId);
+ HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
+ writer.writeClassAttribute(
+ inline ? BootstrapClass.FORM_CONTROL : BootstrapClass.FORM_SELECT,
+ TobagoClass.SELECT__FIELD,
+ inline ? BootstrapClass.LIST_GROUP_ITEM : BootstrapClass.DROPDOWN_TOGGLE,
+ inline ? null : BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)),
+ component.getCustomClass());
+ writer.writeAttribute(HtmlAttributes.TITLE, title, true);
+ writer.writeAttribute(Arias.EXPANDED, Boolean.FALSE.toString(), false);
+ writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
+
+ writer.startElement(HtmlElements.INPUT);
+ writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.TEXT);
+ writer.writeIdAttribute(filterId);
+ writer.writeClassAttribute(TobagoClass.FILTER, BootstrapClass.FORM_CONTROL);
+ writer.writeAttribute(HtmlAttributes.AUTOCOMPLETE, "off", false);
+ writer.writeAttribute(HtmlAttributes.READONLY, filter == null || filter.isEmpty());
+ writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
+ writer.writeAttribute(HtmlAttributes.TABINDEX, tabIndex);
+ renderFocus(clientId, component.isFocus(), component.isError(), facesContext, writer);
+
+ writer.endElement(HtmlElements.INPUT);
+
+ writer.endElement(HtmlElements.DIV);
+ }
+
+ private void encodeOptions(final FacesContext facesContext, final T component, final List<SelectItem> items,
+ final String clientId, final boolean inline, final boolean disabled) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+ writer.startElement(HtmlElements.DIV);
+ writer.writeClassAttribute(
+ TobagoClass.OPTIONS,
+ inline ? BootstrapClass.LIST_GROUP_ITEM : BootstrapClass.DROPDOWN_MENU);
+ writer.writeNameAttribute(clientId);
+
+ writer.startElement(HtmlElements.TABLE);
+ writer.writeClassAttribute(BootstrapClass.TABLE, BootstrapClass.TABLE_HOVER, BootstrapClass.TABLE_SM);
+ writer.startElement(HtmlElements.TBODY);
+
+ final Object[] values = component.getSelectedValues();
+ final String[] submittedValues = getSubmittedValues(component);
+ for (SelectItem item : items) {
+ Object itemValue = item.getValue();
+ // when using selectItem tag with a literal value: use the converted value
+ if (itemValue instanceof String && values != null && values.length > 0 && !(values[0] instanceof String)) {
+ itemValue = ComponentUtils.getConvertedValue(facesContext, component, (String) itemValue);
+ }
+ final String formattedValue = getFormattedValue(facesContext, (T) component, itemValue);
+ final boolean contains;
+ if (submittedValues == null) {
+ contains = ArrayUtils.contains(values, itemValue);
+ } else {
+ contains = ArrayUtils.contains(submittedValues, formattedValue);
+ }
+ writer.startElement(HtmlElements.TR);
+ writer.writeAttribute(DataAttributes.VALUE, formattedValue, true);
+ writer.writeClassAttribute(
+ contains ? BootstrapClass.TABLE_PRIMARY : null,
+ disabled || item.isDisabled() ? TobagoClass.DISABLED : null);
+
+ writer.startElement(HtmlElements.TD);
+ writer.writeAttribute(HtmlAttributes.VALUE, formattedValue, true);
+ writer.writeText(item.getLabel());
+ writer.endElement(HtmlElements.TD);
+ writer.endElement(HtmlElements.TR);
+ }
+
+ writer.endElement(HtmlElements.TBODY);
+ writer.endElement(HtmlElements.TABLE);
+ writer.endElement(HtmlElements.DIV);
+ }
+
+ @Override
+ protected void encodeEndField(FacesContext facesContext, T component) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+ encodeBehavior(writer, facesContext, component);
+ }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectManyListTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectManyListTagDeclaration.java
new file mode 100644
index 0000000000..e7572d504b
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectManyListTagDeclaration.java
@@ -0,0 +1,94 @@
+/*
+ * 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.internal.taglib.component;
+
+import org.apache.myfaces.tobago.apt.annotation.Behavior;
+import org.apache.myfaces.tobago.apt.annotation.Preliminary;
+import org.apache.myfaces.tobago.apt.annotation.Tag;
+import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTag;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
+import org.apache.myfaces.tobago.component.ClientBehaviors;
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasAutoSpacing;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasBinding;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasConverter;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasConverterMessage;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasFilter;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasHelp;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasId;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasLabel;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasLabelLayout;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasRequiredMessageForSelect;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasTabIndex;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasTip;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasValidator;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasValidatorMessage;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasValueChangeListener;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsDisabled;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsFocus;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsInline;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsReadonly;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsRendered;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsRequiredForSelect;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsVisual;
+
+import javax.faces.component.UISelectMany;
+
+/**
+ * Render a multi selection option listbox.
+ */
+@Preliminary
+@Tag(name = "selectManyList")
+@UIComponentTag(
+ uiComponent = "org.apache.myfaces.tobago.component.UISelectManyList",
+ uiComponentFacesClass = "javax.faces.component.UISelectMany",
+ componentFamily = UISelectMany.COMPONENT_FAMILY,
+ rendererType = RendererTypes.SELECT_MANY_LIST,
+ allowedChildComponenents = {"javax.faces.SelectItem", "javax.faces.SelectItems"},
+ behaviors = {
+ @Behavior(
+ name = ClientBehaviors.CHANGE,
+ isDefault = true),
+ @Behavior(
+ name = ClientBehaviors.INPUT),
+ @Behavior(
+ name = ClientBehaviors.CLICK),
+ @Behavior(
+ name = ClientBehaviors.DBLCLICK),
+ @Behavior(
+ name = ClientBehaviors.FOCUS),
+ @Behavior(
+ name = ClientBehaviors.BLUR)
+ })
+
+public interface SelectManyListTagDeclaration
+ extends HasId, IsDisabled, IsRendered, HasBinding, HasTip, HasHelp,
+ IsReadonly, HasConverter, IsRequiredForSelect, HasLabel, HasValidator, HasValueChangeListener, HasLabelLayout,
+ HasValidatorMessage, HasConverterMessage, HasRequiredMessageForSelect, HasTabIndex, IsFocus, IsVisual,
+ HasAutoSpacing, HasFilter, IsInline {
+
+ /**
+ * The value of the multi select.
+ */
+ @TagAttribute
+ @UIComponentTagAttribute(type = {"java.lang.Object[]", "java.util.List"})
+ void setValue(String value);
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/HasFilter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/HasFilter.java
new file mode 100644
index 0000000000..b11db54a21
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/HasFilter.java
@@ -0,0 +1,29 @@
+/*
+ * 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.internal.taglib.declaration;
+
+import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
+
+public interface HasFilter {
+ @TagAttribute
+ @UIComponentTagAttribute
+ void setFilter(String filter);
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
index 538a92ae08..f41e2921db 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
@@ -410,6 +410,8 @@ public enum BootstrapClass implements CssItem {
JUSTIFY_CONTENT_CENTER("justify-content-center"),
JUSTIFY_CONTENT_END("justify-content-end"),
JUSTIFY_CONTENT_START("justify-content-start"),
+ LIST_GROUP("list-group"),
+ LIST_GROUP_ITEM("list-group-item"),
MB_0("mb-0"),
MB_1("mb-1"),
MB_2("mb-2"),
@@ -614,6 +616,7 @@ public enum BootstrapClass implements CssItem {
TABLE_DARK("table-dark"),
TABLE_HOVER("table-hover"),
TABLE_INFO("table-info"),
+ TABLE_PRIMARY("table-primary"),
TABLE_SM("table-sm"),
TABLE_STRIPED("table-striped"),
TOOLTIP_ARROW("tooltip-arrow"),
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
index 6e2212b726..91af83d7ed 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
@@ -49,12 +49,15 @@ public enum TobagoClass implements CssItem {
// DATE__PICKER("tobago-date-picker"),
DELETED("tobago-deleted"),
DESCENDING("tobago-descending"),
+ DISABLED("tobago-disabled"),
DISPLAY__INLINE__BLOCK("tobago-display-inline-block"),
DROPDOWN__SUBMENU("tobago-dropdown-submenu"),
EXPANDED("tobago-expanded"),
// FILE("tobago-file"),
// FIGURE("tobago-figure"),
+ FOCUS("tobago-focus"),
FOLDER("tobago-folder"),
+ FILTER("tobago-filter"),
HEADER("tobago-header"),
// IMAGE("tobago-image"),
// tbd: can be removed?
@@ -69,6 +72,7 @@ public enum TobagoClass implements CssItem {
NOW("tobago-now"),
NUMBER("tobago-number"),
OBJECT("tobago-object"),
+ OPTIONS("tobago-options"),
OUT("tobago-out"),
// PAGE("tobago-page"),
PAGE__MENU_STORE("tobago-page-menuStore"),
@@ -80,6 +84,7 @@ public enum TobagoClass implements CssItem {
REQUIRED("tobago-required"),
RESIZE("tobago-resize"),
SECTION__CONTENT("tobago-section-content"),
+ SELECT__FIELD("tobago-select-field"),
SELECT_MANY_LISTBOX__OPTION("tobago-selectManyListbox-option"),
// SELECT_MANY_SHUTTLE("tobago-selectManyShuttle"),
// SELECT_MANY_SHUTTLE__ADD("tobago-selectManyShuttle-add"),
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 5e98ec05ea..727dc64f3d 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
@@ -172,6 +172,7 @@ public enum HtmlElements {
TOBAGO_SELECT_BOOLEAN_CHECKBOX("tobago-select-boolean-checkbox"),
TOBAGO_SELECT_BOOLEAN_TOGGLE("tobago-select-boolean-toggle"),
TOBAGO_SELECT_MANY_CHECKBOX("tobago-select-many-checkbox"),
+ TOBAGO_SELECT_MANY_LIST("tobago-select-many-list"),
TOBAGO_SELECT_MANY_LISTBOX("tobago-select-many-listbox"),
TOBAGO_SELECT_MANY_SHUTTLE("tobago-select-many-shuttle"),
TOBAGO_SELECT_ONE_CHOICE("tobago-select-one-choice"),
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectManyListController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectManyListController.java
new file mode 100644
index 0000000000..023b3c826e
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectManyListController.java
@@ -0,0 +1,138 @@
+/*
+ * 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.SessionScoped;
+import javax.faces.application.FacesMessage;
+import javax.faces.context.FacesContext;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+@SessionScoped
+@Named
+public class SelectManyListController implements Serializable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @Inject
+ private AstroData astroData;
+
+ private List<SolarObject> planets;
+ private SolarObject[] selected1 = new SolarObject[0];
+ private SolarObject[] selected2 = new SolarObject[0];
+ private SolarObject[] selected3 = new SolarObject[0];
+ private SolarObject[] selected4 = new SolarObject[0];
+ private List<String> names;
+ private String[] selected5 = new String[0];
+
+ private String filterType;
+
+ @PostConstruct
+ public void init() {
+ planets = astroData.getSatellites("Sun");
+
+ names = new ArrayList<>();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+ Thread.currentThread().getContextClassLoader().getResourceAsStream(
+ "org/apache/myfaces/tobago/example/demo/names.txt"), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (!line.startsWith("#") && line.length() > 0) {
+ names.add(line);
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("Can't load names", e);
+ }
+ }
+
+ public List<SolarObject> getPlanets() {
+ return planets;
+ }
+
+ public SolarObject[] getSelected1() {
+ return selected1;
+ }
+
+ public void setSelected1(SolarObject[] selected1) {
+ this.selected1 = selected1;
+ }
+
+ public SolarObject[] getSelected2() {
+ return selected2;
+ }
+
+ public void setSelected2(SolarObject[] selected2) {
+ this.selected2 = selected2;
+ }
+
+ public SolarObject[] getSelected3() {
+ return selected3;
+ }
+
+ public void setSelected3(SolarObject[] selected3) {
+ this.selected3 = selected3;
+ }
+
+ public SolarObject[] getSelected4() {
+ return selected4;
+ }
+
+ public void setSelected4(SolarObject[] selected4) {
+ this.selected4 = selected4;
+ }
+
+ public List<String> getNames() {
+ if (names.size() < 1) {
+ FacesContext.getCurrentInstance().addMessage(null,
+ new FacesMessage(FacesMessage.SEVERITY_ERROR, "Names not loaded!", null));
+ return new ArrayList<>();
+ }
+ return names;
+ }
+
+ public String[] getSelected5() {
+ return selected5;
+ }
+
+ public void setSelected5(String[] selected5) {
+ this.selected5 = selected5;
+ }
+
+ public String getFilterType() {
+ return filterType;
+ }
+
+ public void setFilterType(String filterType) {
+ this.filterType = filterType;
+ }
+}
diff --git a/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/names.txt b/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/names.txt
new file mode 100644
index 0000000000..cff719831c
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/names.txt
@@ -0,0 +1,75 @@
+# 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.
+
+Adrián
+Agnès
+Amélie
+André
+Asbjörn
+Asbjørn
+Behlül
+Björn
+Börje
+Çağlar
+Celâl
+Chloé
+Chloë
+François
+Gökay
+Gökhan
+Göran
+Gösta
+Gustav
+Güvençe
+Héloise
+Inès
+János
+Jokūbas
+Jöran
+KŠthe
+Léa
+Léo
+Maél
+Maël
+Maël
+Mathéo
+Mátyás
+Miraç
+Mjølnir
+Mónica
+Noël
+Nurdoğan
+Örjan
+Özer
+Özgür
+Öztürk
+Raphaël
+Renée
+Ruairí
+Rüzgar
+Rüştü
+Seán
+Siân
+Sölve
+Sönke
+Sørina
+Timéo
+Tunçay
+Ümit
+Uğur
+Yağmur
+Yiğitcan
+Zoë
+Zülfikar
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/80-selectManyList/SelectManyList.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/80-selectManyList/SelectManyList.xhtml
new file mode 100644
index 0000000000..69294f9ec2
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/80-selectManyList/SelectManyList.xhtml
@@ -0,0 +1,87 @@
+<?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.
+-->
+
+<ui:composition template="/main.xhtml"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tc="http://myfaces.apache.org/tobago/component"
+ xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
+ xmlns:f="http://xmlns.jcp.org/jsf/core">
+
+ <!-- todo -->
+ <tc:badge markup="warning"
+ value="Preliminary feature, may be subject to change!"/><br/>
+
+ <tc:section label="not inline / no filter">
+ <tc:selectManyList id="selected1" value="#{selectManyListController.selected1}">
+ <tc:selectItems value="#{selectManyListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectManyList>
+ </tc:section>
+
+ <tc:section label="inline / no filter">
+ <tc:selectManyList id="selected2" value="#{selectManyListController.selected2}" inline="true">
+ <tc:selectItems value="#{selectManyListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectManyList>
+ <tc:button label="Submit"/>
+ </tc:section>
+
+ <tc:section label="not inline / with filter">
+ <tc:selectManyList id="selected3" value="#{selectManyListController.selected3}" filter="contains">
+ <tc:selectItems value="#{selectManyListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectManyList>
+ <tc:button label="Submit"/>
+ </tc:section>
+
+ <tc:section label="inline / with filter:">
+ <tc:selectManyList id="selected4" value="#{selectManyListController.selected4}" filter="contains" inline="true">
+ <tc:selectItems value="#{selectManyListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectManyList>
+ <tc:button label="Submit"/>
+ </tc:section>
+
+ <tc:section id="filter" label="Filter types: contains, startsWith, containsExact, startsWithExact">
+ <tc:selectOneRadio label="Filter type" value="#{selectManyListController.filterType}">
+ <tc:selectItem itemValue="#{null}" itemLabel="please select"/>
+ <tc:selectItem itemValue="contains"/>
+ <tc:selectItem itemValue="startsWith"/>
+ <tc:selectItem itemValue="containsExact"/>
+ <tc:selectItem itemValue="startsWithExact"/>
+ <f:ajax render="filter" execute="filter"/>
+ </tc:selectOneRadio>
+ <tc:selectManyList id="selected5" value="#{selectManyListController.selected5}"
+ filter="#{selectManyListController.filterType}" inline="true">
+ <tc:selectItems value="#{selectManyListController.names}"
+ var="name" itemLabel="#{name}" itemValue="#{name}"/>
+ <tc:style maxHeight="300px"/>
+ </tc:selectManyList>
+
+ Dependent of the type of the filter:
+ Ignores case.
+ Ignores acute (é), grave (è), circumflex (â, î or ô), tilde (ñ), umlaut and
+ dieresis (ü or ï), and cedilla (ç).
+ </tc:section>
+
+ <tc:section label="Filter types: custom">
+ todo
+ </tc:section>
+
+</ui:composition>
diff --git a/tobago-theme/jest.config.js b/tobago-theme/jest.config.js
index 80e4883ab1..58aebcbf99 100644
--- a/tobago-theme/jest.config.js
+++ b/tobago-theme/jest.config.js
@@ -18,5 +18,8 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
- rootDir: "tobago-theme-standard/"
+ rootDir: "tobago-theme-standard/",
+ globals: {
+ "window": {}
+ }
};
diff --git a/tobago-theme/src/main/scss/_tobago.scss b/tobago-theme/src/main/scss/_tobago.scss
index e75b2f42ea..ecb0d9346c 100644
--- a/tobago-theme/src/main/scss/_tobago.scss
+++ b/tobago-theme/src/main/scss/_tobago.scss
@@ -50,6 +50,9 @@ $tobago-page-padding-top: 1rem;
$tobago-header-margin-bottom: $tobago-page-padding-top;
$tobago-flex-layout-spacing: 0.5rem;
+/* bootstrap variables --------------------------------------- */
+$form-select-disabled-color: rgba($form-select-color, $tobago-form-disabled-alpha) !default;
+
/* utilities ----------------------------------------------------- */
.tobago-display-inline-block {
display: inline-block;
@@ -111,6 +114,25 @@ $tobago-flex-layout-spacing: 0.5rem;
}
}
+@mixin formControlFocusShadows() {
+ //_form-control:focus from bootstrap
+ @if $enable-shadows {
+ @include box-shadow($input-box-shadow, $input-focus-box-shadow);
+ } @else {
+ // Avoid using mixin so we can pass custom focus shadow properly
+ box-shadow: $input-focus-box-shadow;
+ }
+}
+
+@mixin formControlFocus() {
+ //_form-control:focus from bootstrap
+ color: $input-focus-color;
+ background-color: $input-focus-bg;
+ border-color: $input-focus-border-color;
+ outline: 0;
+ @include formControlFocusShadows();
+}
+
@mixin formControlSelectListDisabled() {
&:disabled option, option:disabled {
color: rgba($input-color, $tobago-form-disabled-alpha);
@@ -1162,6 +1184,145 @@ tobago-select-one-radio {
}
}
+tobago-select-many-list {
+ display: block;
+
+ &.tobago-label-container {
+ .dropdown, .list-group {
+ flex: 1 0 0;
+ }
+ }
+
+ &.tobago-focus {
+ @mixin borderColorShadow($name, $color) {
+ &.border-#{$name} {
+ box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity);
+ }
+ }
+
+ .dropdown .tobago-select-field {
+ @include formControlFocus();
+ @include borderColorShadow("danger", $danger);
+ @include borderColorShadow("warning", $warning);
+ @include borderColorShadow("info", $info);
+ }
+
+ .list-group {
+ --bs-list-group-color: #{$input-focus-color};
+ --bs-list-group-bg: #{$input-focus-bg};
+ --bs-list-group-border-color: #{$input-focus-border-color};
+
+ @include formControlFocusShadows();
+ @include borderColorShadow("danger", $danger);
+ @include borderColorShadow("warning", $warning);
+ @include borderColorShadow("info", $info);
+ }
+ }
+
+ &.tobago-disabled {
+ .tobago-select-field {
+ color: $form-select-disabled-color;
+ background-color: $form-select-disabled-bg;
+ border-color: $form-select-disabled-border-color;
+ }
+ }
+
+ .list-group {
+ height: inherit;
+ max-height: inherit;
+ @include box-shadow($form-select-box-shadow);
+ @include transition($form-select-transition);
+
+ &.border-danger, &.border-warning, &.border-info {
+ .list-group-item {
+ border-color: inherit;
+ }
+ }
+
+ .list-group-item.tobago-select-field {
+ border-bottom-color: $list-group-border-color;
+ }
+ }
+
+ .tobago-select-field {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+
+ &.dropdown-toggle::after {
+ content: none;
+ }
+
+ &.list-group-item.form-control {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+
+ padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x;
+ }
+
+ .btn-group + .tobago-filter {
+ margin-left: 0.25rem;
+ }
+
+ .btn-group {
+ margin-right: 0.25rem;
+ }
+
+ .tobago-filter {
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ padding: 0;
+ flex: 1 0 0;
+ min-width: 8rem;
+ background-color: transparent;
+
+ &:focus {
+ box-shadow: none;
+ }
+
+ &:read-only {
+ width: 0;
+ min-width: 0;
+ }
+ }
+ }
+
+ .tobago-options {
+ &.list-group-item {
+ padding: 0;
+ overflow-y: auto;
+ }
+
+ .table {
+ margin-bottom: 0;
+
+ tr {
+ cursor: pointer;
+
+ &.tobago-disabled {
+ color: $form-select-disabled-color;
+ background-color: $form-select-disabled-bg;
+ border-color: $form-select-disabled-border-color;
+ cursor: initial;
+ pointer-events: none;
+ }
+
+ &.tobago-mark {
+ --bs-table-accent-bg: var(--bs-table-hover-bg);
+ color: var(--bs-table-hover-color);
+ }
+
+ &:last-of-type {
+ td {
+ border-bottom-width: 0;
+ }
+ }
+ }
+ }
+ }
+}
+
/* selectManyCheckbox ----------------------------------------------------- */
tobago-select-many-checkbox {
display: block;
diff --git a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
index e07725496b..c9faad6aed 100644
--- a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
+++ b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
@@ -77,6 +77,9 @@
<tag name="selectManyCheckbox">
<attribute name="labelLayout" default="flexLeft"/>
</tag>
+ <tag name="selectManyList">
+ <attribute name="labelLayout" default="flexLeft"/>
+ </tag>
<tag name="selectManyListbox">
<attribute name="labelLayout" default="flexLeft"/>
</tag>
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
index 15bd6e250a..d9f7acd722 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
@@ -36,6 +36,7 @@ import "./tobago-reload";
import "./tobago-scroll";
import "./tobago-select-boolean-checkbox";
import "./tobago-select-boolean-toggle";
+import "./tobago-select-many-list";
import "./tobago-select-many-checkbox";
import "./tobago-select-many-listbox";
import "./tobago-select-many-shuttle";
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.test.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.test.ts
new file mode 100644
index 0000000000..c63ece9144
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.test.ts
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+import {TobagoFilterRegistry} from "./tobago-filter-registry";
+
+beforeEach(() => {
+ Object.defineProperty(window, "navigator", {value: {}, configurable: true});
+ // "es" is only one example
+ Object.defineProperty(window.navigator, "language", {value: "es", configurable: true});
+});
+
+test("a startsWith a", () => {
+ const p = TobagoFilterRegistry.get("startsWith");
+ expect(p("a", "a")).toBeTruthy();
+});
+
+test("a startsWith b", () => {
+ const p = TobagoFilterRegistry.get("startsWith");
+ expect(p("a", "b")).toBeFalsy();
+});
+
+test("a startsWith A", () => {
+ const p = TobagoFilterRegistry.get("startsWith");
+ expect(p("a", "A")).toBeTruthy();
+});
+
+test("AB startsWith á", () => {
+ const p = TobagoFilterRegistry.get("startsWith");
+ expect(p("AB", "á")).toBeTruthy();
+});
+
+test("AB contains á", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("aB", "á")).toBeTruthy();
+});
+
+test("BAB contains á", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("BAB", "á")).toBeTruthy();
+});
+
+test("El niño startsWith \u00F1", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+ expect(c("El niño", "\u00F1")).toBeFalsy();
+});
+
+test("El niño contains \u00F1", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("El niño", "\u00F1")).toBeTruthy();
+});
+
+test("El niño startsWith \u006E\u0303", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+ expect(c("El niño", "\u006E\u0303")).toBeFalsy();
+});
+
+test("El niño contains \u006E\u0303", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("El niño", "\u006E\u0303")).toBeTruthy();
+});
+
+test("El niño startsWith Ñ", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+ expect(c("El niño", "Ñ")).toBeFalsy();
+});
+
+test("El niño contains Ñ", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("El niño", "Ñ")).toBeTruthy();
+});
+
+test("Am\u00e9lie startsWith Am\u0065\u0301lie", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+ expect(c("Am\u00e9lie", "Am\u0065\u0301lie")).toBeTruthy();
+});
+
+test("Am\u00e9lie contains Am\u0065\u0301lie", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("Am\u00e9lie", "Am\u0065\u0301lie")).toBeTruthy();
+});
+
+test("Barış startsWith Baris", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+// XXX skip expect(c("Barış", "Baris")).toBeTruthy();
+});
+
+test("Barış contains Baris", () => {
+ const c = TobagoFilterRegistry.get("contains");
+// XXX skip expect(c("Barış", "Baris")).toBeTruthy();
+});
+
+test("Uğur startsWith Ugur", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+ expect(c("Uğur", "Ugur")).toBeTruthy();
+});
+
+test("Uğur contains Ugur", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("Uğur", "Ugur")).toBeTruthy();
+});
+
+test("Mjølnir startsWith Mjolnir", () => {
+ const c = TobagoFilterRegistry.get("startsWith");
+ expect(c("Mjølnir", "Mjolnir")).toBeTruthy();
+});
+
+test("Mjølnir contains Mjolnir", () => {
+ const c = TobagoFilterRegistry.get("contains");
+ expect(c("Mjølnir", "Mjolnir")).toBeTruthy();
+});
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.ts
new file mode 100644
index 0000000000..27d3bab0ed
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.ts
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+export class TobagoFilterRegistry {
+
+ // todo: use "function(string, string): boolean" instead of "any"
+ private static map: Map<string, any> = new Map<string, any>();
+
+ static {
+ /**
+ * Filter for "contains" search.
+ * Ignores case.
+ * Ignores acute (é), grave (è), circumflex (â, î or ô), tilde (ñ), umlaut and
+ * dieresis (ü or ï), and cedilla (ç).
+ */
+ TobagoFilterRegistry.set("contains",
+ (item: string, search: string): boolean =>
+ TobagoFilterRegistry.localeContains(item, search, false)
+ );
+
+ /**
+ * Filter for "startsWith" search.
+ * Ignores case.
+ * Ignores acute (é), grave (è), circumflex (â, î or ô), tilde (ñ), umlaut and
+ * dieresis (ü or ï), and cedilla (ç).
+ */
+ TobagoFilterRegistry.set("startsWith",
+ (item: string, search: string): boolean =>
+ TobagoFilterRegistry.localeContains(item, search, true)
+ );
+
+ /**
+ * Filter for "containsExact" search.
+ */
+ TobagoFilterRegistry.set("containsExact",
+ (item: string, search: string): boolean =>
+ item.indexOf(search) >= 0
+ );
+
+ /**
+ * Filter for "startsWithExact" search.
+ */
+ TobagoFilterRegistry.set("startsWithExact",
+ (item: string, search: string): boolean =>
+ item.indexOf(search) == 0
+ );
+
+ }
+
+ static set(key: string, value: any): void {
+ this.map.set(key, value);
+ }
+
+ static get(key: string): any {
+ const value = this.map.get(key);
+ if (value) {
+ return value;
+ } else {
+ console.warn("TobagoFilterRegistry.get(" + key + ") = undefined");
+ return null;
+ }
+ }
+
+ private static localeContains(item: string, search: string, startOnly: boolean) {
+ item = item.normalize();
+ search = search.normalize();
+
+ const searchLength = search.length;
+ const diffLength = startOnly ? 0 : item.length - searchLength;
+
+ // console.log("a", item);
+ // console.log("b", search);
+ // console.log("diffLength", diffLength);
+
+ for (let i = 0; i <= diffLength; i++) {
+ // console.log("i", i);
+ const s = item.substring(i, i + searchLength);
+ // console.log("s", s);
+ if (s
+ .localeCompare(search, window.navigator.language, {sensitivity: "base"}) === 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-list.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-list.ts
new file mode 100644
index 0000000000..fdc7a4cae5
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-list.ts
@@ -0,0 +1,421 @@
+/*
+ * 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.
+ */
+
+import {TobagoFilterRegistry} from "./tobago-filter-registry";
+import {createPopper, Instance} from "@popperjs/core";
+
+class SelectManyList extends HTMLElement {
+ private popper: Instance;
+
+ private readonly CssClass = {
+ DROPDOWN_MENU: "dropdown-menu",
+ SHOW: "show",
+ TABLE_ACTIVE: "table-active",
+ TABLE_PRIMARY: "table-primary",
+ TOBAGO_DISABLED: "tobago-disabled",
+ TOBAGO_FOCUS: "tobago-focus",
+ TOBAGO_MARK: "tobago-mark",
+ TOBAGO_OPTIONS: "tobago-options"
+ };
+
+ private readonly Key = {
+ ARROW_DOWN: "ArrowDown",
+ ARROW_UP: "ArrowUp",
+ ENTER: "Enter",
+ ESCAPE: "Escape",
+ SPACE: " "
+ };
+
+ constructor() {
+ super();
+ }
+
+ get hiddenSelect(): HTMLSelectElement {
+ return this.querySelector("select");
+ }
+
+ get selectField(): HTMLDivElement {
+ return this.querySelector(".tobago-select-field");
+ }
+
+ get badgeCloseButtons(): NodeListOf<HTMLButtonElement> {
+ return this.selectField.querySelectorAll("button.btn.badge");
+ }
+
+ get filter(): string {
+ return this.getAttribute("filter");
+ }
+
+ get filterInput(): HTMLInputElement {
+ return this.querySelector(".tobago-filter");
+ }
+
+ get dropdownMenu(): HTMLDivElement {
+ const root = this.getRootNode() as ShadowRoot | Document;
+ return root.querySelector(`.dropdown-menu[name='${this.id}']`);
+ }
+
+ get menuStore(): HTMLDivElement {
+ const root = this.getRootNode() as ShadowRoot | Document;
+ return root.querySelector(".tobago-page-menuStore");
+ }
+
+ get tbody(): HTMLElement {
+ const root = this.getRootNode() as ShadowRoot | Document;
+ return root.querySelector(`.tobago-options[name='${this.id}'] tbody`);
+ }
+
+ get enabledRows(): NodeListOf<HTMLTableRowElement> {
+ return this.tbody.querySelectorAll<HTMLTableRowElement>("tr:not(.tobago-disabled)");
+ }
+
+ get markedRow(): HTMLTableRowElement {
+ return this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_MARK);
+ }
+
+ connectedCallback(): void {
+ if (this.dropdownMenu) {
+ this.popper = createPopper(this.selectField, this.dropdownMenu, {});
+ window.addEventListener("resize", this.resizeEvent.bind(this));
+ }
+ document.addEventListener("click", this.clickEvent.bind(this));
+ this.filterInput.addEventListener("focus", this.focusEvent.bind(this));
+ this.filterInput.addEventListener("blur", this.blurEvent.bind(this));
+ this.selectField.addEventListener("keydown", this.keydownEvent.bind(this));
+
+ // init badges
+ this.querySelectorAll("option:checked").forEach(
+ option => this.sync(<HTMLOptionElement>option)
+ );
+
+ this.initList();
+
+ // init filter
+ if (this.filter) {
+ const input = this.filterInput;
+ input.addEventListener("keyup", this.filterEvent.bind(this));
+ }
+
+ // handle autofocus; trigger focus event
+ if (document.activeElement.id === this.filterInput.id) {
+ this.focusEvent();
+ }
+ }
+
+ select(event: MouseEvent): void {
+ const target = <HTMLElement>event.target;
+ const row = target.closest("tr");
+ this.selectRow(row);
+ }
+
+ selectRow(row: HTMLTableRowElement): void {
+ const itemValue = row.dataset.tobagoValue;
+ console.info("itemValue", itemValue);
+ const select = this.hiddenSelect;
+ const option: HTMLOptionElement = select.querySelector(`[value="${itemValue}"]`);
+ option.selected = !option.selected;
+ this.sync(option);
+ }
+
+ removeBadge(event: MouseEvent): void {
+ const target = <HTMLElement>event.target;
+ const group: HTMLElement = target.closest(".btn-group");
+ const itemValue = group.dataset.tobagoValue;
+ const select = this.hiddenSelect;
+ const option: HTMLOptionElement = select.querySelector(`[value="${itemValue}"]`);
+ option.selected = false;
+ this.sync(option);
+ }
+
+ sync(option: HTMLOptionElement) {
+ const itemValue = option.value;
+ const row: HTMLTableRowElement = this.tbody.querySelector(`[data-tobago-value="${itemValue}"]`);
+ if (option.selected) {
+ // create badge
+ const tabIndex: number = this.filterInput.tabIndex;
+ this.filterInput.insertAdjacentHTML("beforebegin",
+ this.getRowTemplate(itemValue, row.innerText, option.disabled || this.hiddenSelect.disabled, tabIndex));
+
+ // todo: nicer adding the @click with lit-html
+ const closeButton = this.selectField
+ .querySelector(".btn-group[data-tobago-value='" + itemValue + "'] button.btn.badge");
+ closeButton?.addEventListener("click", this.removeBadge.bind(this));
+ closeButton?.addEventListener("focus", this.focusEvent.bind(this));
+ closeButton?.addEventListener("blur", this.blurEvent.bind(this));
+
+ // highlight list row
+ row.classList.add(this.CssClass.TABLE_PRIMARY);
+ } else {
+ // remove badge
+ const badge = this.selectField.querySelector(`[data-tobago-value="${itemValue}"]`);
+ const previousBadge = badge.previousElementSibling;
+ const nextBadge = badge.nextElementSibling.tagName === "SPAN" ? badge.nextElementSibling : null;
+ badge.remove();
+ if (previousBadge) {
+ previousBadge.querySelector<HTMLButtonElement>("button.btn.badge").focus();
+ } else if (nextBadge) {
+ nextBadge.querySelector<HTMLButtonElement>("button.btn.badge").focus();
+ } else {
+ this.filterInput.disabled = false;
+ this.filterInput.focus();
+ }
+
+ // remove highlight list row
+ row.classList.remove(this.CssClass.TABLE_PRIMARY);
+ }
+
+ if (!this.classList.contains(this.CssClass.TOBAGO_DISABLED) && !this.filter) {
+ // disable input field to prevent focus.
+ if (this.badgeCloseButtons.length > 0 && this.filterInput.id === document.activeElement.id) {
+ this.badgeCloseButtons.item(this.badgeCloseButtons.length - 1).focus();
+ }
+ this.filterInput.disabled = this.badgeCloseButtons.length > 0;
+ }
+ }
+
+ getRowTemplate(value: string, text: string, disabled: boolean, tabIndex: number): string {
+ return disabled ? `
+<span class="btn-group" role="group" data-tobago-value="${value}">
+ <tobago-badge class="badge text-bg-primary btn disabled">${text}</tobago-badge>
+</span>` : `
+<span class="btn-group" role="group" data-tobago-value="${value}">
+ <tobago-badge class="badge text-bg-primary btn">${text}</tobago-badge>
+ <button type='button' class='tobago-button btn btn-secondary badge'
+ ${tabIndex > 0 ? " tabindex='" + String(tabIndex) + "'" : ""}><i class='bi-x-lg'></i></button>
+</span>`;
+ }
+
+ filterEvent(event: Event): void {
+ const input = event.currentTarget as HTMLInputElement;
+ const searchString = input.value;
+ console.info("searchString", searchString);
+ const filterFunction = TobagoFilterRegistry.get(this.filter);
+ // XXX todo: if filterFunction not found?
+ if (filterFunction != null) {
+ this.querySelectorAll("tr").forEach(row => {
+ const itemValue = row.dataset.tobagoValue;
+ if (filterFunction(itemValue, searchString)) {
+ row.classList.remove("d-none");
+ } else {
+ row.classList.add("d-none");
+ }
+ });
+ }
+ }
+
+ private clickEvent(event: MouseEvent): void {
+ if (this.isDeleted(event.target as Element)) {
+ // do nothing, this is probably a removed badge
+ } else if (this.isPartOfSelectField(event.target as Element)
+ || this.isPartOfTobagoOptions(event.target as Element)) {
+
+ if (!this.filterInput.disabled) {
+ this.filterInput.focus();
+ } else if (this.badgeCloseButtons.length > 0) {
+ this.badgeCloseButtons[0].focus();
+ }
+
+ } else {
+ this.hideDropdown();
+ this.setFocus(false);
+ }
+ }
+
+ private keydownEvent(event: KeyboardEvent) {
+ switch (event.key) {
+ case this.Key.ESCAPE:
+ this.hideDropdown();
+ this.removeTableRowMark();
+ break;
+ case this.Key.ARROW_DOWN:
+ event.preventDefault();
+ this.showDropdown();
+ this.markNextTableRow();
+ break;
+ case this.Key.ARROW_UP:
+ event.preventDefault();
+ this.showDropdown();
+ this.markPreviousTableRow();
+ break;
+ case this.Key.ENTER:
+ case this.Key.SPACE:
+ if (this.markedRow) {
+ event.preventDefault();
+ this.selectMarkedOption();
+ } else if (document.activeElement.id === this.filterInput.id) {
+ this.showDropdown();
+ }
+ break;
+ }
+ }
+
+ private markNextTableRow(): void {
+ const rows = this.enabledRows;
+ const indexOfMark = this.indexOfTobagoMark(rows);
+ if (indexOfMark >= 0) {
+ if (indexOfMark + 1 < rows.length) {
+ rows.item(indexOfMark).classList.remove(this.CssClass.TOBAGO_MARK);
+ this.addTableRowMark(rows.item(indexOfMark + 1));
+ } else {
+ rows.item(rows.length - 1).classList.remove(this.CssClass.TOBAGO_MARK);
+ this.addTableRowMark(rows.item(0));
+ }
+ } else if (rows.length > 0) {
+ this.addTableRowMark(rows.item(0));
+ }
+ }
+
+ private markPreviousTableRow(): void {
+ const rows = this.enabledRows;
+ const indexOfMark = this.indexOfTobagoMark(rows);
+ if (indexOfMark >= 0) {
+ if ((indexOfMark - 1) >= 0) {
+ rows.item(indexOfMark).classList.remove(this.CssClass.TOBAGO_MARK);
+ this.addTableRowMark(rows.item(indexOfMark - 1));
+ } else {
+ rows.item(0).classList.remove(this.CssClass.TOBAGO_MARK);
+ this.addTableRowMark(rows.item(rows.length - 1));
+ }
+ } else if (rows.length > 0) {
+ this.addTableRowMark(rows.item(rows.length - 1));
+ }
+ }
+
+ private addTableRowMark(row: HTMLTableRowElement): void {
+ row.classList.add(this.CssClass.TOBAGO_MARK);
+ if (!this.dropdownMenu) {
+ row.scrollIntoView({block: "center"});
+ }
+ }
+
+ private removeTableRowMark(): void {
+ this.markedRow?.classList.remove(this.CssClass.TOBAGO_MARK);
+ }
+
+ private selectMarkedOption(): void {
+ const row = this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_MARK);
+ this.selectRow(row);
+ }
+
+ private indexOfTobagoMark(rows: NodeListOf<HTMLTableRowElement>): number {
+ for (let i = 0; i < rows.length; i++) {
+ if (rows.item(i).classList.contains(this.CssClass.TOBAGO_MARK)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private isPartOfSelectField(element: Element): boolean {
+ if (element) {
+ if (this.selectField.id === element.id) {
+ return true;
+ } else {
+ return element.parentElement ? this.isPartOfSelectField(element.parentElement) : false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ private isPartOfTobagoOptions(element: Element): boolean {
+ if (element) {
+ if (element.classList.contains(this.CssClass.TOBAGO_OPTIONS)
+ && this.id === element.getAttribute("name")) {
+ return true;
+ } else {
+ return element.parentElement ? this.isPartOfTobagoOptions(element.parentElement) : false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ private isDeleted(element: Element): boolean {
+ return element.closest("html") === null;
+ }
+
+ private showDropdown(): void {
+ if (this.dropdownMenu && !this.dropdownMenu.classList.contains(this.CssClass.SHOW)) {
+ this.selectField.classList.add(this.CssClass.SHOW);
+ this.selectField.ariaExpanded = "true";
+ this.dropdownMenu.classList.add(this.CssClass.SHOW);
+ this.updateDropdownMenuWidth();
+ this.popper.update();
+ }
+ }
+
+ private hideDropdown(): void {
+ if (this.dropdownMenu?.classList.contains(this.CssClass.SHOW)) {
+ this.selectField.classList.remove(this.CssClass.SHOW);
+ this.selectField.ariaExpanded = "false";
+ this.dropdownMenu.classList.remove(this.CssClass.SHOW);
+ }
+ }
+
+ private resizeEvent(event: UIEvent): void {
+ this.updateDropdownMenuWidth();
+ }
+
+ private updateDropdownMenuWidth(): void {
+ if (this.dropdownMenu) {
+ this.dropdownMenu.style.width = `${this.selectField.offsetWidth}px`;
+ }
+ }
+
+ private focusEvent(): void {
+ if (!this.hiddenSelect.disabled) {
+ if (!this.classList.contains(this.CssClass.TOBAGO_FOCUS)) {
+ this.setFocus(true);
+ this.showDropdown();
+ }
+ }
+ }
+
+ private blurEvent(event: FocusEvent): void {
+ if (event.relatedTarget !== null) {
+ //relatedTarget is the new focused element; null indicate a mouseclick or an inactive browser window
+ if (!this.isPartOfSelectField(event.relatedTarget as Element)
+ && !this.isPartOfTobagoOptions(event.relatedTarget as Element)) {
+ this.setFocus(false);
+ this.hideDropdown();
+ }
+ }
+ }
+
+ private setFocus(focus: boolean): void {
+ if (focus) {
+ this.classList.add(this.CssClass.TOBAGO_FOCUS);
+ } else {
+ this.classList.remove(this.CssClass.TOBAGO_FOCUS);
+ }
+ }
+
+ private initList() {
+ const tbody = this.tbody;
+ tbody.addEventListener("click", this.select.bind(this));
+ tbody.querySelectorAll("tr").forEach((row: HTMLTableRowElement) => {
+ // row stuff
+ });
+ }
+}
+
+document.addEventListener("tobago.init", function (event: Event): void {
+ if (window.customElements.get("tobago-select-many-list") == null) {
+ window.customElements.define("tobago-select-many-list", SelectManyList);
+ }
+});