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 2023/02/10 20:31:53 UTC
[myfaces-tobago] 01/02: feat: tc:selectOneList
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 11906d7dca37abd4772e574c79628ec9db5eacce
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Tue Feb 7 20:52:28 2023 +0100
feat: tc:selectOneList
* implement tc:selectOneList according to tc:selectManyList
* add demo
* make a typescript base class for Select[One/Many]List
* fix: preselection if filter is active
* fix: don't open dropdown menu if component is disabled
* adjust the behavior of selectManyList to act like selectOneList
** remove value of filter input if leaving component
** use input event for filterInput instead of keyup
** initial focus doesn't open dropdownMenu like the regular tc:selectOneChoice component
Issue: TOBAGO-2179
---
.../myfaces/tobago/component/RendererTypes.java | 2 +
.../org/apache/myfaces/tobago/component/Tags.java | 2 +
.../component/AbstractUISelectOneList.java | 36 +++
.../renderkit/renderer/SelectOneListRenderer.java | 212 +++++++++++++++++
.../component/SelectOneListTagDeclaration.java | 78 ++++++
.../tobago/renderkit/html/HtmlElements.java | 1 +
.../example/demo/SelectOneListController.java | 138 +++++++++++
.../45-selectOneList/SelectOneList.xhtml | 87 +++++++
tobago-theme/src/main/scss/_tobago.scss | 262 +++++++++++----------
.../src/main/resources/META-INF/tobago-config.xml | 5 +
.../src/main/ts/tobago-all.ts | 1 +
.../src/main/ts/tobago-select-list-base.ts | 230 ++++++++++++++++++
.../src/main/ts/tobago-select-many-list.ts | 238 +++----------------
.../src/main/ts/tobago-select-one-list.ts | 171 ++++++++++++++
14 files changed, 1126 insertions(+), 337 deletions(-)
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 d77b7b1c31..df34cb4c5b 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
@@ -73,6 +73,7 @@ public enum RendererTypes {
SelectManyShuttle,
SelectOneChoice,
SelectOneRadio,
+ SelectOneList,
SelectOneListbox,
SelectReference,
Separator,
@@ -147,6 +148,7 @@ public enum RendererTypes {
public static final String SELECT_MANY_SHUTTLE = "SelectManyShuttle";
public static final String SELECT_ONE_CHOICE = "SelectOneChoice";
public static final String SELECT_ONE_RADIO = "SelectOneRadio";
+ public static final String SELECT_ONE_LIST = "SelectOneList";
public static final String SELECT_ONE_LISTBOX = "SelectOneListbox";
public static final String SELECT_REFERENCE = "SelectReference";
public static final String SEPARATOR = "Separator";
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 9b6050a801..eb2624ec9b 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
@@ -80,6 +80,7 @@ public enum Tags {
selectManyShuttle,
selectOneChoice,
selectOneRadio,
+ selectOneList,
selectOneListbox,
selectReference,
separator,
@@ -153,6 +154,7 @@ public enum Tags {
public static final String SELECT_MANY_SHUTTLE = "selectManyShuttle";
public static final String SELECT_ONE_CHOICE = "selectOneChoice";
public static final String SELECT_ONE_RADIO = "selectOneRadio";
+ public static final String SELECT_ONE_LIST = "selectOneList";
public static final String SELECT_ONE_LISTBOX = "selectOneListbox";
public static final String SELECT_REFERENCE = "selectReference";
public static final String SEPARATOR = "separator";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneList.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneList.java
new file mode 100644
index 0000000000..7b73c5f615
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneList.java
@@ -0,0 +1,36 @@
+/*
+ * 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.util.ComponentUtils;
+
+import javax.faces.context.FacesContext;
+
+public abstract class AbstractUISelectOneList extends AbstractUISelectOneBase implements SupportFieldId {
+
+ @Override
+ public String getFieldId(final FacesContext facesContext) {
+ return getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + "field";
+ }
+
+ public abstract String getFilter();
+ public abstract boolean isExpanded();
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
new file mode 100644
index 0000000000..655712cf31
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
@@ -0,0 +1,212 @@
+/*
+ * 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.internal.component.AbstractUISelectOneList;
+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 SelectOneListRenderer<T extends AbstractUISelectOneList> extends SelectOneRendererBase<T> {
+
+ @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 expanded = component.isExpanded();
+ 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(
+ expanded ? BootstrapClass.LIST_GROUP : BootstrapClass.DROPDOWN,
+ expanded ? BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)) : null);
+
+ encodeSelectField(facesContext, component,
+ clientId, fieldId, filterId, filter, disabled, expanded, title, tabIndex);
+ encodeOptions(facesContext, component, items, clientId, expanded, disabled);
+
+ writer.endElement(HtmlElements.DIV);
+ }
+
+ 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);
+
+ final String submittedValue = (String) component.getSubmittedValue();
+ renderSelectItems(component, null, items, component.getValue(), submittedValue, 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 expanded, 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(
+ expanded ? BootstrapClass.FORM_CONTROL : BootstrapClass.FORM_SELECT,
+ TobagoClass.SELECT__FIELD,
+ expanded ? BootstrapClass.LIST_GROUP_ITEM : BootstrapClass.DROPDOWN_TOGGLE,
+ expanded ? 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.SPAN);
+ writer.endElement(HtmlElements.SPAN);
+
+ 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 expanded, final boolean disabled) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+ writer.startElement(HtmlElements.DIV);
+ writer.writeClassAttribute(
+ TobagoClass.OPTIONS,
+ expanded ? 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 value = component.getValue();
+ final Object submittedValue = component.getSubmittedValue();
+ for (SelectItem item : items) {
+ Object itemValue = item.getValue();
+ // when using selectItem tag with a literal value: use the converted value
+ if (itemValue instanceof String && value != null && !(value instanceof String)) {
+ itemValue = ComponentUtils.getConvertedValue(facesContext, component, (String) itemValue);
+ }
+ final String formattedValue = getFormattedValue(facesContext, (T) component, itemValue);
+ final boolean contains;
+ if (submittedValue != null) {
+ contains = submittedValue.equals(formattedValue);
+ } else {
+ contains = value != null && value.equals(itemValue);
+ }
+ 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);
+ }
+
+ @Override
+ public HtmlElements getComponentTag() {
+ return HtmlElements.TOBAGO_SELECT_ONE_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 void writeAdditionalAttributes(FacesContext facesContext, TobagoResponseWriter writer, T input)
+ throws IOException {
+ super.writeAdditionalAttributes(facesContext, writer, input);
+ writer.writeAttribute(CustomAttributes.FILTER, input.getFilter(), true);
+ }
+
+ @Override
+ protected String getFieldId(FacesContext facesContext, T component) {
+ return component.getFieldId(facesContext);
+ }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectOneListTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectOneListTagDeclaration.java
new file mode 100644
index 0000000000..eceb39fc85
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectOneListTagDeclaration.java
@@ -0,0 +1,78 @@
+/*
+ * 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.UIComponentTag;
+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.HasDecorationPosition;
+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.HasValue;
+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.IsExpanded;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsFocus;
+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 single selection option list.
+ */
+@Preliminary
+@Tag(name = "selectOneList")
+@UIComponentTag(
+ uiComponent = "org.apache.myfaces.tobago.component.UISelectOneList",
+ uiComponentFacesClass = "javax.faces.component.UISelectOne",
+ componentFamily = UISelectMany.COMPONENT_FAMILY,
+ rendererType = RendererTypes.SELECT_ONE_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 SelectOneListTagDeclaration
+ extends HasId, IsDisabled, IsRendered, HasBinding, HasTip, HasHelp, IsReadonly, HasConverter, IsRequiredForSelect,
+ HasLabel, HasValidator, HasValue, HasValueChangeListener, HasLabelLayout, HasValidatorMessage, HasConverterMessage,
+ HasRequiredMessageForSelect, HasTabIndex, IsFocus, IsVisual, HasAutoSpacing, HasFilter, IsExpanded,
+ HasDecorationPosition {
+}
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 727dc64f3d..253a13f0f5 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
@@ -176,6 +176,7 @@ public enum HtmlElements {
TOBAGO_SELECT_MANY_LISTBOX("tobago-select-many-listbox"),
TOBAGO_SELECT_MANY_SHUTTLE("tobago-select-many-shuttle"),
TOBAGO_SELECT_ONE_CHOICE("tobago-select-one-choice"),
+ TOBAGO_SELECT_ONE_LIST("tobago-select-one-list"),
TOBAGO_SELECT_ONE_LISTBOX("tobago-select-one-listbox"),
TOBAGO_SELECT_ONE_RADIO("tobago-select-one-radio"),
TOBAGO_SEPARATOR("tobago-separator"),
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectOneListController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectOneListController.java
new file mode 100644
index 0000000000..f807a1673d
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectOneListController.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 SelectOneListController implements Serializable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @Inject
+ private AstroData astroData;
+
+ private List<SolarObject> planets;
+ private SolarObject selected1;
+ private SolarObject selected2;
+ private SolarObject selected3;
+ private SolarObject selected4;
+ private List<String> names;
+ private String selected5;
+
+ 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/webapp/content/030-select/45-selectOneList/SelectOneList.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/45-selectOneList/SelectOneList.xhtml
new file mode 100644
index 0000000000..1798beb163
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/45-selectOneList/SelectOneList.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 expanded / no filter">
+ <tc:selectOneList id="selected1" value="#{selectOneListController.selected1}">
+ <tc:selectItems value="#{selectOneListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectOneList>
+ </tc:section>
+
+ <tc:section label="expanded / no filter">
+ <tc:selectOneList id="selected2" value="#{selectOneListController.selected2}" expanded="true">
+ <tc:selectItems value="#{selectOneListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectOneList>
+ <tc:button label="Submit"/>
+ </tc:section>
+
+ <tc:section label="not expanded / with filter">
+ <tc:selectOneList id="selected3" value="#{selectOneListController.selected3}" filter="contains">
+ <tc:selectItems value="#{selectOneListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectOneList>
+ <tc:button label="Submit"/>
+ </tc:section>
+
+ <tc:section label="expanded / with filter:">
+ <tc:selectOneList id="selected4" value="#{selectOneListController.selected4}" filter="contains" expanded="true">
+ <tc:selectItems value="#{selectOneListController.planets}"
+ var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+ </tc:selectOneList>
+ <tc:button label="Submit"/>
+ </tc:section>
+
+ <tc:section id="filter" label="Filter types: contains, startsWith, containsExact, startsWithExact">
+ <tc:selectOneRadio label="Filter type" value="#{selectOneListController.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:selectOneList id="selected5" value="#{selectOneListController.selected5}"
+ filter="#{selectOneListController.filterType}" expanded="true">
+ <tc:selectItems value="#{selectOneListController.names}"
+ var="name" itemLabel="#{name}" itemValue="#{name}"/>
+ <tc:style maxHeight="300px"/>
+ </tc:selectOneList>
+
+ 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/src/main/scss/_tobago.scss b/tobago-theme/src/main/scss/_tobago.scss
index a050c989e5..3440fae36c 100644
--- a/tobago-theme/src/main/scss/_tobago.scss
+++ b/tobago-theme/src/main/scss/_tobago.scss
@@ -195,6 +195,137 @@ $form-select-disabled-color: rgba($form-select-color, $tobago-form-disabled-alph
}
}
+@mixin selectListBase() {
+ display: block;
+
+ &.tobago-label-container {
+ .dropdown, .list-group {
+ flex: 1 0 0;
+ }
+ }
+
+ &.tobago-focus {
+ .dropdown .tobago-select-field {
+ @include formControlFocus();
+ @include selectListBorderColorShadow("danger", $danger);
+ @include selectListBorderColorShadow("warning", $warning);
+ @include selectListBorderColorShadow("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 selectListBorderColorShadow("danger", $danger);
+ @include selectListBorderColorShadow("warning", $warning);
+ @include selectListBorderColorShadow("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;
+ }
+
+ .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-preselect {
+ --bs-table-accent-bg: var(--bs-table-hover-bg);
+ color: var(--bs-table-hover-color);
+ }
+
+ &:last-of-type {
+ td {
+ border-bottom-width: 0;
+ }
+ }
+ }
+ }
+ }
+}
+
+@mixin selectListBorderColorShadow($name, $color) {
+ &.border-#{$name} {
+ box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity);
+ }
+}
+
@mixin tobagoTreeNodeToggle() {
.tobago-toggle:not(.invisible) {
cursor: pointer;
@@ -1194,6 +1325,10 @@ tobago-select-one-choice {
}
}
+tobago-select-one-list {
+ @include selectListBase();
+}
+
tobago-select-one-listbox {
display: block;
@@ -1216,81 +1351,9 @@ 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;
- }
- }
+ @include selectListBase();
.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;
}
@@ -1298,59 +1361,6 @@ tobago-select-many-list {
.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-preselect {
- --bs-table-accent-bg: var(--bs-table-hover-bg);
- color: var(--bs-table-hover-color);
- }
-
- &:last-of-type {
- td {
- border-bottom-width: 0;
- }
- }
- }
- }
}
}
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 9280f8d7b7..548a17ebfc 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
@@ -113,6 +113,11 @@
<attribute name="messagePosition" default="buttonRight"/>
<attribute name="helpPosition" default="buttonRight"/>
</tag>
+ <tag name="selectOneList">
+ <attribute name="labelLayout" default="flexLeft"/>
+ <attribute name="messagePosition" default="buttonRight"/>
+ <attribute name="helpPosition" default="buttonRight"/>
+ </tag>
<tag name="selectOneListbox">
<attribute name="labelLayout" default="flexLeft"/>
<attribute name="messagePosition" default="buttonRight"/>
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 d9f7acd722..2addc09525 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
@@ -41,6 +41,7 @@ import "./tobago-select-many-checkbox";
import "./tobago-select-many-listbox";
import "./tobago-select-many-shuttle";
import "./tobago-select-one-choice";
+import "./tobago-select-one-list";
import "./tobago-select-one-listbox";
import "./tobago-select-one-radio";
import "./tobago-sheet";
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-list-base.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-list-base.ts
new file mode 100644
index 0000000000..6059a0026f
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-list-base.ts
@@ -0,0 +1,230 @@
+/*
+ * 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 {createPopper, Instance} from "@popperjs/core";
+
+export class SelectListBase extends HTMLElement {
+ protected readonly CssClass = {
+ D_NONE: "d-none",
+ DROPDOWN_MENU: "dropdown-menu",
+ SHOW: "show",
+ TABLE_ACTIVE: "table-active",
+ TABLE_PRIMARY: "table-primary",
+ TOBAGO_DISABLED: "tobago-disabled",
+ TOBAGO_FOCUS: "tobago-focus",
+ TOBAGO_PRESELECT: "tobago-preselect",
+ TOBAGO_OPTIONS: "tobago-options"
+ };
+
+ protected readonly Key = {
+ ARROW_DOWN: "ArrowDown",
+ ARROW_UP: "ArrowUp",
+ BACKSPACE: "Backspace",
+ ENTER: "Enter",
+ ESCAPE: "Escape",
+ SPACE: " ",
+ TAB: "Tab"
+ };
+
+ private popper: Instance;
+
+ get disabled(): boolean {
+ return this.classList.contains(this.CssClass.TOBAGO_DISABLED);
+ }
+
+ get hiddenSelect(): HTMLSelectElement {
+ return this.querySelector("select");
+ }
+
+ get hiddenOptions(): NodeListOf<HTMLOptionElement> {
+ return this.hiddenSelect.querySelectorAll<HTMLOptionElement>("option");
+ }
+
+ get selectField(): HTMLDivElement {
+ return this.querySelector(".tobago-select-field");
+ }
+
+ get filter(): string {
+ return this.getAttribute("filter");
+ }
+
+ get filterEnabled(): boolean {
+ return this.filter?.length > 0;
+ }
+
+ 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 rows(): NodeListOf<HTMLTableRowElement> {
+ return this.tbody.querySelectorAll<HTMLTableRowElement>("tr");
+ }
+
+ get enabledRows(): NodeListOf<HTMLTableRowElement> {
+ return this.tbody.querySelectorAll<HTMLTableRowElement>("tr:not(." + this.CssClass.D_NONE + ")");
+ }
+
+ get preselectedRow(): HTMLTableRowElement {
+ return this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_PRESELECT);
+ }
+
+ connectedCallback(): void {
+ if (this.dropdownMenu) {
+ this.popper = createPopper(this.selectField, this.dropdownMenu, {});
+ window.addEventListener("resize", this.resizeEvent.bind(this));
+ }
+ }
+
+ protected focusEvent(): void {
+ if (!this.hiddenSelect.disabled) {
+ if (!this.classList.contains(this.CssClass.TOBAGO_FOCUS)) {
+ this.setFocus(true);
+ }
+ }
+ }
+
+ protected setFocus(focus: boolean): void {
+ if (focus) {
+ this.classList.add(this.CssClass.TOBAGO_FOCUS);
+ } else {
+ this.classList.remove(this.CssClass.TOBAGO_FOCUS);
+ }
+ }
+
+ protected preselectNextTableRow(): void {
+ const rows = this.enabledRows;
+ const index = this.preselectIndex(rows);
+ if (index >= 0) {
+ if (index + 1 < rows.length) {
+ rows.item(index).classList.remove(this.CssClass.TOBAGO_PRESELECT);
+ this.preselect(rows.item(index + 1));
+ } else {
+ rows.item(rows.length - 1).classList.remove(this.CssClass.TOBAGO_PRESELECT);
+ this.preselect(rows.item(0));
+ }
+ } else if (rows.length > 0) {
+ this.preselect(rows.item(0));
+ }
+ }
+
+ protected preselectPreviousTableRow(): void {
+ const rows = this.enabledRows;
+ const index = this.preselectIndex(rows);
+ if (index >= 0) {
+ if ((index - 1) >= 0) {
+ rows.item(index).classList.remove(this.CssClass.TOBAGO_PRESELECT);
+ this.preselect(rows.item(index - 1));
+ } else {
+ rows.item(0).classList.remove(this.CssClass.TOBAGO_PRESELECT);
+ this.preselect(rows.item(rows.length - 1));
+ }
+ } else if (rows.length > 0) {
+ this.preselect(rows.item(rows.length - 1));
+ }
+ }
+
+ protected removePreselection(): void {
+ this.preselectedRow?.classList.remove(this.CssClass.TOBAGO_PRESELECT);
+ }
+
+ protected 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;
+ }
+ }
+
+ protected 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;
+ }
+ }
+
+ protected 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();
+ }
+ }
+
+ protected 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 preselect(row: HTMLTableRowElement): void {
+ row.classList.add(this.CssClass.TOBAGO_PRESELECT);
+ if (!this.dropdownMenu) {
+ row.scrollIntoView({block: "center"});
+ }
+
+ this.filterInput.disabled = false;
+ this.filterInput.focus({preventScroll: true});
+ }
+
+ private preselectIndex(rows: NodeListOf<HTMLTableRowElement>): number {
+ for (let i = 0; i < rows.length; i++) {
+ if (rows.item(i).classList.contains(this.CssClass.TOBAGO_PRESELECT)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private resizeEvent(event: UIEvent): void {
+ this.updateDropdownMenuWidth();
+ }
+
+ private updateDropdownMenuWidth(): void {
+ if (this.dropdownMenu) {
+ this.dropdownMenu.style.width = `${this.selectField.offsetWidth}px`;
+ }
+ }
+}
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
index 8058b29db2..f92709d9ce 100644
--- 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
@@ -16,83 +16,19 @@
*/
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_PRESELECT: "tobago-preselect",
- TOBAGO_OPTIONS: "tobago-options"
- };
-
- private readonly Key = {
- ARROW_DOWN: "ArrowDown",
- ARROW_UP: "ArrowUp",
- ENTER: "Enter",
- ESCAPE: "Escape",
- SPACE: " ",
- TAB: "Tab"
- };
+import {SelectListBase} from "./tobago-select-list-base";
+class SelectManyList extends SelectListBase {
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 preselectedRow(): HTMLTableRowElement {
- return this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_PRESELECT);
- }
-
connectedCallback(): void {
- if (this.dropdownMenu) {
- this.popper = createPopper(this.selectField, this.dropdownMenu, {});
- window.addEventListener("resize", this.resizeEvent.bind(this));
- }
+ super.connectedCallback();
document.addEventListener("click", this.clickEvent.bind(this));
this.filterInput.addEventListener("focus", this.focusEvent.bind(this));
this.filterInput.addEventListener("blur", this.blurEvent.bind(this));
@@ -105,10 +41,8 @@ class SelectManyList extends HTMLElement {
this.initList();
- // init filter
if (this.filter) {
- const input = this.filterInput;
- input.addEventListener("keyup", this.filterEvent.bind(this));
+ this.filterInput.addEventListener("input", this.filterEvent.bind(this));
}
// handle autofocus; trigger focus event
@@ -179,7 +113,7 @@ class SelectManyList extends HTMLElement {
row.classList.remove(this.CssClass.TABLE_PRIMARY);
}
- if (!this.classList.contains(this.CssClass.TOBAGO_DISABLED) && !this.filter) {
+ if (!this.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();
@@ -212,30 +146,31 @@ class SelectManyList extends HTMLElement {
this.querySelectorAll("tr").forEach(row => {
const itemValue = row.dataset.tobagoValue;
if (filterFunction(itemValue, searchString)) {
- row.classList.remove("d-none");
+ row.classList.remove(this.CssClass.D_NONE);
} else {
- row.classList.add("d-none");
+ row.classList.add(this.CssClass.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.disabled) {
+ 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();
+ }
+ this.showDropdown();
- if (!this.filterInput.disabled) {
- this.filterInput.focus();
- } else if (this.badgeCloseButtons.length > 0) {
- this.badgeCloseButtons[0].focus();
+ } else {
+ this.leaveComponent();
}
- this.showDropdown();
-
- } else {
- this.hideDropdown();
- this.setFocus(false);
}
}
@@ -273,146 +208,27 @@ class SelectManyList extends HTMLElement {
}
}
- private preselectNextTableRow(): void {
- const rows = this.enabledRows;
- const index = this.preselectIndex(rows);
- if (index >= 0) {
- if (index + 1 < rows.length) {
- rows.item(index).classList.remove(this.CssClass.TOBAGO_PRESELECT);
- this.preselect(rows.item(index + 1));
- } else {
- rows.item(rows.length - 1).classList.remove(this.CssClass.TOBAGO_PRESELECT);
- this.preselect(rows.item(0));
- }
- } else if (rows.length > 0) {
- this.preselect(rows.item(0));
- }
- }
-
- private preselectPreviousTableRow(): void {
- const rows = this.enabledRows;
- const index = this.preselectIndex(rows);
- if (index >= 0) {
- if ((index - 1) >= 0) {
- rows.item(index).classList.remove(this.CssClass.TOBAGO_PRESELECT);
- this.preselect(rows.item(index - 1));
- } else {
- rows.item(0).classList.remove(this.CssClass.TOBAGO_PRESELECT);
- this.preselect(rows.item(rows.length - 1));
- }
- } else if (rows.length > 0) {
- this.preselect(rows.item(rows.length - 1));
- }
- }
-
- private preselect(row: HTMLTableRowElement): void {
- row.classList.add(this.CssClass.TOBAGO_PRESELECT);
- if (!this.dropdownMenu) {
- row.scrollIntoView({block: "center"});
- }
-
- this.filterInput.disabled = false;
- this.filterInput.focus({preventScroll: true});
- }
-
- private removePreselection(): void {
- this.preselectedRow?.classList.remove(this.CssClass.TOBAGO_PRESELECT);
- }
-
- private preselectIndex(rows: NodeListOf<HTMLTableRowElement>): number {
- for (let i = 0; i < rows.length; i++) {
- if (rows.item(i).classList.contains(this.CssClass.TOBAGO_PRESELECT)) {
- 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 leaveComponent(): void {
+ this.setFocus(false);
+ this.filterInput.value = null;
+ this.filterInput.dispatchEvent(new Event("input"));
+ this.hideDropdown();
}
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();
+ this.leaveComponent();
}
}
}
- 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));
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts
new file mode 100644
index 0000000000..39a65efed2
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts
@@ -0,0 +1,171 @@
+/*
+ * 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 {SelectListBase} from "./tobago-select-list-base";
+
+class SelectOneList extends SelectListBase {
+ constructor() {
+ super();
+ }
+
+ get spanText(): string {
+ return this.selectField.querySelector("span").textContent;
+ }
+
+ set spanText(text: string) {
+ this.selectField.querySelector("span").textContent = text;
+ }
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ 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));
+ this.tbody.addEventListener("click", this.select.bind(this));
+
+ this.sync();
+
+ if (this.filter) {
+ this.filterInput.addEventListener("input", this.filterEvent.bind(this));
+ }
+
+ 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;
+ const select = this.hiddenSelect;
+ const option: HTMLOptionElement = select.querySelector(`[value="${itemValue}"]`);
+ option.selected = true;
+ this.filterInput.value = null;
+ this.sync();
+ }
+
+ sync() {
+ this.rows.forEach((row) => {
+ if (row.dataset.tobagoValue === this.hiddenSelect.value) {
+ this.spanText = this.hiddenSelect.value;
+ row.classList.add(this.CssClass.TABLE_PRIMARY); // highlight list row
+ } else {
+ row.classList.remove(this.CssClass.TABLE_PRIMARY); // remove highlight list row
+ }
+ });
+ }
+
+ filterEvent(event: Event): void {
+ const input = event.currentTarget as HTMLInputElement;
+ const searchString = input.value;
+ if (searchString !== null) {
+ this.spanText = null;
+ this.showDropdown();
+ }
+ 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(this.CssClass.D_NONE);
+ } else {
+ row.classList.add(this.CssClass.D_NONE);
+ }
+ });
+ }
+ }
+
+ private clickEvent(event: MouseEvent): void {
+ if (!this.disabled) {
+ if (this.isPartOfSelectField(event.target as Element) || this.isPartOfTobagoOptions(event.target as Element)) {
+ if (!this.filterInput.disabled) {
+ this.filterInput.focus();
+ }
+ this.showDropdown();
+ } else {
+ this.leaveComponent();
+ }
+ }
+ }
+
+ private keydownEvent(event: KeyboardEvent) {
+ switch (event.key) {
+ case this.Key.ESCAPE:
+ this.hideDropdown();
+ this.removePreselection();
+ break;
+ case this.Key.ARROW_DOWN:
+ event.preventDefault();
+ this.showDropdown();
+ this.preselectNextTableRow();
+ break;
+ case this.Key.ARROW_UP:
+ event.preventDefault();
+ this.showDropdown();
+ this.preselectPreviousTableRow();
+ break;
+ case this.Key.BACKSPACE:
+ if (this.filterInput.value.length === 0) {
+ this.spanText = null;
+ this.filterInput.dispatchEvent(new Event("input"));
+ }
+ break;
+ case this.Key.ENTER:
+ case this.Key.SPACE:
+ if (this.preselectedRow) {
+ event.preventDefault();
+ const row = this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_PRESELECT);
+ this.selectRow(row);
+ } else if (document.activeElement.id === this.filterInput.id) {
+ this.showDropdown();
+ }
+ break;
+ }
+ }
+
+ private leaveComponent(): void {
+ this.setFocus(false);
+ this.filterInput.value = null;
+ this.filterInput.dispatchEvent(new Event("input"));
+ this.spanText = this.hiddenSelect.value;
+ this.hideDropdown();
+ }
+
+ 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.leaveComponent();
+ }
+ }
+ }
+}
+
+document.addEventListener("tobago.init", function (event: Event): void {
+ if (window.customElements.get("tobago-select-one-list") == null) {
+ window.customElements.define("tobago-select-one-list", SelectOneList);
+ }
+});