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/10/05 14:32:50 UTC
[myfaces-tobago] 01/02: feature: Toasts
This is an automated email from the ASF dual-hosted git repository.
hnoeth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git
commit f41b2a827bd12579501b4ffdc99767ce2932bde0
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Fri Aug 18 16:40:57 2023 +0200
feature: Toasts
* tc:toasts implemented
* add an example
Issue: TOBAGO-2231
---
.../apache/myfaces/tobago/application/Toast.java | 30 ++
.../myfaces/tobago/component/Attributes.java | 2 +
.../apache/myfaces/tobago/component/Facets.java | 2 +
.../myfaces/tobago/component/RendererTypes.java | 2 +
.../org/apache/myfaces/tobago/component/Tags.java | 2 +
.../org/apache/myfaces/tobago/context/Markup.java | 2 +
.../internal/component/AbstractUIToasts.java | 72 +++++
.../internal/renderkit/renderer/PageRenderer.java | 6 +-
.../renderkit/renderer/ToastsRenderer.java | 334 +++++++++++++++++++++
.../taglib/component/ToastsTagDeclaration.java | 89 ++++++
.../apache/myfaces/tobago/layout/Placement.java | 36 +++
.../tobago/renderkit/css/BootstrapClass.java | 3 +
.../myfaces/tobago/renderkit/css/TobagoClass.java | 3 +
.../tobago/renderkit/html/DataAttributes.java | 2 +
.../tobago/renderkit/html/HtmlElements.java | 1 +
.../tobago/context/TobagoResourceBundle.properties | 1 +
.../context/TobagoResourceBundle_de.properties | 1 +
.../context/TobagoResourceBundle_es.properties | 1 +
.../tobago/example/demo/ToastsController.java | 194 ++++++++++++
.../src/main/webapp/content/065-toast/Toast.xhtml | 110 +++++++
tobago-theme/src/main/scss/_tobago.scss | 52 +++-
.../src/main/ts/tobago-all.ts | 1 +
.../src/main/ts/tobago-css.ts | 15 +
.../src/main/ts/tobago-placement.ts | 30 ++
.../src/main/ts/tobago-toasts.ts | 242 +++++++++++++++
.../org/apache/myfaces/tobago/apt/component.stg | 4 +
26 files changed, 1223 insertions(+), 14 deletions(-)
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/application/Toast.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/application/Toast.java
new file mode 100644
index 0000000000..2d43b23ce9
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/application/Toast.java
@@ -0,0 +1,30 @@
+/*
+ * 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.application;
+
+public interface Toast {
+
+ /**
+ * A toast must have an ID.
+ *
+ * @return toast id
+ */
+ String getId();
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
index 2cb97e7f92..46c5280352 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
@@ -95,6 +95,7 @@ public enum Attributes {
directLinkCount,
disabled,
display,
+ disposeDelay,
dropZone,
enctype,
escape,
@@ -341,6 +342,7 @@ public enum Attributes {
pagingTarget,
password,
placeholder,
+ placement,
plain,
popupClose,
popupList,
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java
index 16fa53b04d..6a37bb35b5 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java
@@ -35,6 +35,7 @@ public enum Facets {
contextMenu,
footer,
label,
+ header,
pagerPage,
pagerPageDirect,
pagerRow,
@@ -56,6 +57,7 @@ public enum Facets {
public static final String CONTEXT_MENU = "contextMenu";
public static final String FOOTER = "footer";
public static final String LABEL = "label";
+ public static final String HEADER = "header";
public static final String PAGER_PAGE = "pagerPage";
public static final String PAGER_PAGE_DIRECT = "pagerPageDirect";
public static final String PAGER_ROW = "pagerRow";
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 8c549a3a48..7571f6a23f 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
@@ -86,6 +86,7 @@ public enum RendererTypes {
Textarea,
Tab,
TabGroup,
+ Toasts,
ToolBar,
Tree,
/**
@@ -162,6 +163,7 @@ public enum RendererTypes {
public static final String TEXTAREA = "Textarea";
public static final String TAB = "Tab";
public static final String TAB_GROUP = "TabGroup";
+ public static final String TOASTS = "Toasts";
public static final String TOOL_BAR = "ToolBar";
public static final String TREE = "Tree";
/**
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 f5c183d191..252a7e92fc 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
@@ -94,6 +94,7 @@ public enum Tags {
textarea,
tab,
tabGroup,
+ toasts,
toolBar,
tree,
treeIcon,
@@ -169,6 +170,7 @@ public enum Tags {
public static final String TEXTAREA = "textarea";
public static final String TAB = "tab";
public static final String TAB_GROUP = "tabGroup";
+ public static final String TOASTS = "toasts";
public static final String TOOL_BAR = "toolBar";
public static final String TREE = "tree";
public static final String TREE_ICON = "treeIcon";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java
index d77d146a33..69be720c2a 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java
@@ -81,6 +81,7 @@ public final class Markup implements Serializable, Iterable<String> {
public static final Markup EXTRA_LARGE = valueOf("extraLarge");
public static final Markup FATAL = valueOf("fatal");
public static final Markup FOLDER = valueOf("folder");
+ public static final Markup HIDE_CLOSE_BUTTON = valueOf("hideCloseButton");
public static final Markup HIDE_TOGGLE_ICON = valueOf("hideToggleIcon");
public static final Markup HOVER = valueOf("hover");
public static final Markup INFO = valueOf("info");
@@ -144,6 +145,7 @@ public final class Markup implements Serializable, Iterable<String> {
public static final String STRING_EXTRA_LARGE = "extraLarge";
public static final String STRING_FATAL = "fatal";
public static final String STRING_FOLDER = "folder";
+ public static final String STRING_HIDE_CLOSE_BUTTON = "hideCloseButton";
public static final String STRING_HIDE_TOGGLE_ICON = "hideToggleIcon";
public static final String STRING_HOVER = "hover";
public static final String STRING_INFO = "info";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIToasts.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIToasts.java
new file mode 100644
index 0000000000..8754a06be1
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIToasts.java
@@ -0,0 +1,72 @@
+/*
+ * 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 jakarta.faces.component.UIData;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import org.apache.myfaces.tobago.application.Toast;
+import org.apache.myfaces.tobago.component.Visual;
+import org.apache.myfaces.tobago.internal.renderkit.renderer.ToastsRenderer;
+import org.apache.myfaces.tobago.layout.Placement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractUIToasts extends UIData implements ClientBehaviorHolder, Visual {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String STATES = "states";
+
+ @Override
+ public Collection<Toast> getValue() {
+ final Object value = super.getValue();
+ if (value instanceof Collection) {
+ return (Collection<Toast>) value;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ public Map<String, ToastsRenderer.StateData> getStates() {
+ Object hideTime = getStateHelper().eval(STATES);
+ if (hideTime instanceof Map) {
+ return (Map<String, ToastsRenderer.StateData>) hideTime;
+ }
+ return new HashMap<>();
+ }
+
+ public void setStates(Map<String, ToastsRenderer.StateData> states) {
+ getStateHelper().put(STATES, states);
+ }
+
+ public abstract Placement getPlacement();
+
+ public abstract Integer getDisposeDelay();
+
+ private enum PropertyKeys {
+ showedIds, hideTimes, closedIds
+ }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
index fc8d83a06d..f0619d508c 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
@@ -281,6 +281,11 @@ public class PageRenderer<T extends AbstractUIPage> extends RendererBase<T> {
writer.writeClassAttribute(TobagoClass.PAGE__MENU_STORE);
writer.endElement(HtmlElements.DIV);
+ // placeholder for toasts
+ writer.startElement(HtmlElements.DIV);
+ writer.writeClassAttribute(TobagoClass.PAGE__TOAST_STORE);
+ writer.endElement(HtmlElements.DIV);
+
writer.startElement(HtmlElements.SPAN);
writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "faces-state-container");
writer.flush();
@@ -435,6 +440,5 @@ public class PageRenderer<T extends AbstractUIPage> extends RendererBase<T> {
}
return false;
}
-
}
}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ToastsRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ToastsRenderer.java
new file mode 100644
index 0000000000..e7e0d58f18
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ToastsRenderer.java
@@ -0,0 +1,334 @@
+/*
+ * 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 jakarta.faces.component.UIComponent;
+import jakarta.faces.context.FacesContext;
+import org.apache.myfaces.tobago.application.Toast;
+import org.apache.myfaces.tobago.component.Facets;
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.internal.component.AbstractUIReload;
+import org.apache.myfaces.tobago.internal.component.AbstractUIStyle;
+import org.apache.myfaces.tobago.internal.component.AbstractUIToasts;
+import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
+import org.apache.myfaces.tobago.internal.util.RenderUtils;
+import org.apache.myfaces.tobago.layout.Placement;
+import org.apache.myfaces.tobago.renderkit.RendererBase;
+import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
+import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.Arias;
+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.renderkit.html.HtmlRoleValues;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.apache.myfaces.tobago.util.ResourceUtils;
+import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.stream.Collectors;
+
+public class ToastsRenderer<T extends AbstractUIToasts> extends RendererBase<T> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String SUFFIX_STATES = "states";
+
+ @Override
+ public void decodeInternal(final FacesContext facesContext, final T component) {
+ super.decodeInternal(facesContext, component);
+
+ final String clientId = component.getClientId(facesContext);
+ final String statesInputId = clientId + ComponentUtils.SUB_SEPARATOR + SUFFIX_STATES;
+
+ final Map<String, String> requestParameterMap = facesContext.getExternalContext().getRequestParameterMap();
+ if (requestParameterMap.containsKey(statesInputId)) {
+ final String json = requestParameterMap.get(statesInputId);
+ final Map<String, StateData> states = decodeStates(json);
+ final Collection<Toast> toasts = component.getValue();
+ final List<String> toastIds = toasts.stream()
+ .map(toast -> clientId + ComponentUtils.SUB_SEPARATOR + toast.getId())
+ .collect(Collectors.toList());
+
+ final List<String> idsForRemoval = new ArrayList<>();
+ states.forEach((key, value) -> {
+ if (value.state.equals(StateEnum.closed)
+ || value.hideTime != null && value.hideTime.isBefore(LocalTime.now())
+ || !toastIds.contains(key)) {
+ idsForRemoval.add(key);
+ }
+ });
+
+ toasts.removeIf(toast -> idsForRemoval.contains(clientId + ComponentUtils.SUB_SEPARATOR + toast.getId()));
+ states.entrySet().removeIf(entry -> idsForRemoval.contains(entry.getKey()));
+
+ component.setStates(states);
+ }
+ }
+
+ private Map<String, StateData> decodeStates(final String input) {
+ if (input == null) {
+ return null;
+ }
+ String string = input.trim().replaceAll(""", "\"");
+ final Map<String, StateData> result = new HashMap<>();
+ if (string.length() < 2 || string.charAt(0) != '{' || string.charAt(string.length() - 1) != '}') {
+ LOG.warn("Can't parse: no surrounding curly brackets: '{}'", string);
+ } else {
+ string = string.substring(1, string.length() - 1);
+
+ final StringTokenizer tokenizer = new StringTokenizer(string, "\",{}");
+
+ String id = null;
+ StateEnum state = null;
+ LocalTime hideTime = null;
+
+ while (tokenizer.hasMoreTokens()) {
+ final String token = tokenizer.nextToken().trim();
+
+ if (!token.equals(":") && !token.equals("state") && !token.equals("hideTime")) {
+ if (id == null) {
+ id = token;
+ } else if (state == null) {
+ state = StateEnum.valueOf(token);
+ } else {
+ if (!token.equals(":null")) {
+ hideTime = LocalTime.parse(token);
+ }
+
+ result.put(id, new StateData(state, hideTime));
+ id = null;
+ state = null;
+ hideTime = null;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void encodeBeginInternal(final FacesContext facesContext, final T component) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+ final String clientId = component.getClientId(facesContext);
+ final Placement placement = component.getPlacement();
+ final Integer disposeDelay = component.getDisposeDelay();
+ final Markup markup = component.getMarkup() != null ? component.getMarkup() : Markup.NULL;
+ final Map<String, StateData> states = component.getStates();
+
+ insideBegin(facesContext, HtmlElements.TOBAGO_TOASTS);
+ writer.startElement(HtmlElements.TOBAGO_TOASTS);
+ writer.writeIdAttribute(clientId);
+ HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
+
+ final AbstractUIReload reload = ComponentUtils.getReloadFacet(component);
+ if (reload != null) {
+ reload.encodeAll(facesContext);
+ }
+
+ final Collection<Toast> toasts = component.getValue();
+ final UIComponent header = ComponentUtils.getFacet(component, Facets.header);
+ final List<UIComponent> children = component.getChildren();
+
+ if (toasts.size() == component.getRowCount()) {
+ int rowIndex = component.getFirst();
+ for (Toast toast : toasts) {
+ component.setRowIndex(rowIndex);
+ if (!component.isRowAvailable()) {
+ break;
+ }
+
+ final String toastId = clientId + ComponentUtils.SUB_SEPARATOR + toast.getId();
+
+ if (!states.containsKey(toastId)) {
+ states.put(toastId, new StateData(StateEnum.created, null));
+ }
+ final StateData stateData = states.get(toastId);
+
+ writer.startElement(HtmlElements.DIV);
+ writer.writeIdAttribute(toastId);
+ writer.writeClassAttribute(
+ BootstrapClass.TOAST,
+ component.getCustomClass(),
+ markup.contains(Markup.HIDE_CLOSE_BUTTON) ? TobagoClass.HIDE_CLOSE_BUTTON : null);
+ writer.writeAttribute(HtmlAttributes.NAME, toastId, false);
+ writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.ALERT.toString(), false);
+ writer.writeAttribute(Arias.LIVE, "assertive", false);
+ writer.writeAttribute(Arias.ATOMIC, true);
+ writer.writeAttribute(DataAttributes.PLACEMENT, placement.name(), false);
+
+ if (stateData.state.equals(StateEnum.created)) {
+ writer.writeAttribute(DataAttributes.DISPOSE_DELAY, disposeDelay);
+ if (disposeDelay > 0) {
+ LocalTime hideTime = LocalTime.now().plus(disposeDelay, ChronoUnit.MILLIS);
+ states.put(toastId, new StateData(StateEnum.created, hideTime));
+ }
+ } else if (stateData.state.equals(StateEnum.showed)) {
+ if (stateData.hideTime != null) {
+ int decreasedDisposeDelay = (int) LocalTime.now().until(stateData.hideTime, ChronoUnit.MILLIS);
+ writer.writeAttribute(DataAttributes.DISPOSE_DELAY, Math.max(decreasedDisposeDelay, 0));
+
+ if (decreasedDisposeDelay <= 0) {
+ states.put(toastId, new StateData(StateEnum.closed, null));
+ }
+ } else {
+ writer.writeAttribute(DataAttributes.DISPOSE_DELAY, disposeDelay);
+ }
+ }
+
+ if (header != null) {
+ writer.startElement(HtmlElements.DIV);
+ writer.writeClassAttribute(BootstrapClass.TOAST_HEADER);
+ insideBegin(facesContext, Facets.header);
+ for (final UIComponent child : RenderUtils.getFacetChildren(header)) {
+ child.encodeAll(facesContext);
+ }
+ encodeCloseButton(facesContext, writer);
+ insideEnd(facesContext, Facets.header);
+ writer.endElement(HtmlElements.DIV);
+ }
+
+ writer.startElement(HtmlElements.DIV);
+ writer.writeClassAttribute(BootstrapClass.TOAST_BODY);
+ if (header == null) {
+ encodeCloseButton(facesContext, writer);
+ }
+ for (UIComponent child : children) {
+ child.encodeAll(facesContext);
+ }
+ writer.endElement(HtmlElements.DIV);
+ writer.endElement(HtmlElements.DIV);
+
+ rowIndex++;
+ }
+ }
+
+ component.setRowIndex(-1);
+
+ final String statesInputId = clientId + ComponentUtils.SUB_SEPARATOR + SUFFIX_STATES;
+ writer.startElement(HtmlElements.INPUT);
+ writer.writeIdAttribute(statesInputId);
+ writer.writeNameAttribute(statesInputId);
+ writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
+ writer.writeAttribute(HtmlAttributes.VALUE, encodeStates(states), false);
+ writer.endElement(HtmlElements.INPUT);
+ }
+
+ private void encodeCloseButton(final FacesContext facesContext, final TobagoResponseWriter writer)
+ throws IOException {
+ writer.startElement(HtmlElements.BUTTON);
+ writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.BUTTON);
+ writer.writeClassAttribute(BootstrapClass.BTN_CLOSE, TobagoClass.CLOSE);
+ writer.writeAttribute(DataAttributes.BS_DISMISS, "toast", false);
+ writer.writeAttribute(Arias.LABEL, ResourceUtils.getString(facesContext, "toast.close"), false);
+ writer.endElement(HtmlElements.BUTTON);
+ }
+
+ private String encodeStates(final Map<String, StateData> states) {
+ if (states == null) {
+ return null;
+ }
+
+ final StringBuilder builder = new StringBuilder();
+ boolean firstEntry = true;
+
+ builder.append("{");
+ for (Map.Entry<String, StateData> entry : states.entrySet()) {
+ if (firstEntry) {
+ firstEntry = false;
+ } else {
+ builder.append(",");
+ }
+
+ builder.append("\"");
+ builder.append(entry.getKey());
+ builder.append("\":{\"state\":\"");
+ builder.append(entry.getValue().state);
+ builder.append("\",\"hideTime\":");
+
+ if (entry.getValue().hideTime != null) {
+ builder.append("\"");
+ builder.append(entry.getValue().hideTime);
+ builder.append("\"");
+ } else {
+ builder.append("null");
+ }
+ builder.append("}");
+ }
+ builder.append("}");
+
+ return builder.toString();
+ }
+
+ @Override
+ public void encodeEndInternal(final FacesContext facesContext, final T component) throws IOException {
+ final TobagoResponseWriter writer = getResponseWriter(facesContext);
+ writer.endElement(HtmlElements.TOBAGO_TOASTS);
+ insideEnd(facesContext, HtmlElements.TOBAGO_TOASTS);
+ }
+
+ @Override
+ public boolean getRendersChildren() {
+ return true;
+ }
+
+ @Override
+ public void encodeChildrenInternal(final FacesContext facesContext, final T component) throws IOException {
+ for (final UIComponent child : component.getChildren()) {
+ if (child instanceof AbstractUIStyle) {
+ child.encodeAll(facesContext);
+ }
+ }
+ }
+
+ public static class StateData {
+ private final StateEnum state;
+ private final LocalTime hideTime;
+
+ public StateData(StateEnum state, LocalTime hideTime) {
+ this.state = state;
+ this.hideTime = hideTime;
+ }
+
+ public StateEnum getState() {
+ return state;
+ }
+
+ public LocalTime getHideTime() {
+ return hideTime;
+ }
+ }
+
+ private enum StateEnum {
+ created, showed, closed
+ }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/ToastsTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/ToastsTagDeclaration.java
new file mode 100644
index 0000000000..53d7e4efdb
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/ToastsTagDeclaration.java
@@ -0,0 +1,89 @@
+/*
+ * 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 jakarta.faces.component.UIData;
+import org.apache.myfaces.tobago.apt.annotation.Behavior;
+import org.apache.myfaces.tobago.apt.annotation.DynamicExpression;
+import org.apache.myfaces.tobago.apt.annotation.Facet;
+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.Facets;
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasIdBindingAndRendered;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasVar;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsVisual;
+import org.apache.myfaces.tobago.layout.Placement;
+
+/**
+ * Render toasts elements.
+ */
+@Preliminary
+@Tag(name = "toasts")
+@UIComponentTag(
+ uiComponent = "org.apache.myfaces.tobago.component.UIToasts",
+ uiComponentFacesClass = "jakarta.faces.component.UIData",
+ componentFamily = UIData.COMPONENT_FAMILY,
+ rendererType = RendererTypes.TOASTS,
+ facets = {
+ @Facet(name = Facets.RELOAD, description = "Contains an instance of UIReload",
+ allowedChildComponenents = "org.apache.myfaces.tobago.Reload"),
+ @Facet(name = Facets.HEADER, description = "Contains code to be placed in the header of the toast.")},
+ behaviors = {
+ @Behavior(name = ClientBehaviors.RELOAD, isDefault = true)
+ })
+public interface ToastsTagDeclaration extends HasIdBindingAndRendered, HasVar, IsVisual {
+
+ /**
+ * Must be a collection of #{@link org.apache.myfaces.tobago.application.Toast} objects.
+ */
+ @TagAttribute
+ @UIComponentTagAttribute(
+ type = {"java.util.Collection<org.apache.myfaces.tobago.application.Toast>"},
+ expression = DynamicExpression.VALUE_EXPRESSION_REQUIRED)
+ void setValue(String value);
+
+ /**
+ * Defines the placement of the toasts. Default is "bottomRight".
+ */
+ @TagAttribute
+ @UIComponentTagAttribute(
+ type = "org.apache.myfaces.tobago.layout.Placement",
+ allowedValues = {
+ Placement.TOP_LEFT, Placement.TOP_CENTER, Placement.TOP_RIGHT,
+ Placement.MIDDLE_LEFT, Placement.MIDDLE_CENTER, Placement.MIDDLE_RIGHT,
+ Placement.BOTTOM_LEFT, Placement.BOTTOM_CENTER, Placement.BOTTOM_RIGHT
+ },
+ defaultValue = Placement.BOTTOM_RIGHT,
+ defaultCode = "org.apache.myfaces.tobago.layout.Placement.bottomRight")
+ void setPlacement(String placement);
+
+ /**
+ * Delay in milliseconds before hiding the toast. Default is "5000".
+ * Set to "-1" to never automatically hide the toast.
+ */
+ @TagAttribute
+ @UIComponentTagAttribute(type = "java.lang.Integer", defaultValue = "5000")
+ void setDisposeDelay(String disposeDelay);
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/layout/Placement.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/layout/Placement.java
new file mode 100644
index 0000000000..3c3e728fca
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/layout/Placement.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.layout;
+
+public enum Placement {
+ topLeft, topCenter, topRight,
+ middleLeft, middleCenter, middleRight,
+ bottomLeft, bottomCenter, bottomRight;
+
+ public static final String TOP_LEFT = "topLeft";
+ public static final String TOP_CENTER = "topCenter";
+ public static final String TOP_RIGHT = "topRight";
+ public static final String MIDDLE_LEFT = "middleLeft";
+ public static final String MIDDLE_CENTER = "middleCenter";
+ public static final String MIDDLE_RIGHT = "middleRight";
+ public static final String BOTTOM_LEFT = "bottomLeft";
+ public static final String BOTTOM_CENTER = "bottomCenter";
+ public static final String BOTTOM_RIGHT = "bottomRight";
+}
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 66330f98e0..7b5c73e167 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
@@ -638,6 +638,9 @@ public enum BootstrapClass implements CssItem {
TABLE_PRIMARY("table-primary"),
TABLE_SM("table-sm"),
TABLE_STRIPED("table-striped"),
+ TOAST("toast"),
+ TOAST_BODY("toast-body"),
+ TOAST_HEADER("toast-header"),
TOOLTIP_ARROW("tooltip-arrow"),
VISUALLY_HIDDEN("visually-hidden"),
WARNING_FEEDBACK("warning-feedback"),
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 cd446fcd02..f7d5e47497 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
@@ -48,6 +48,7 @@ public enum TobagoClass implements CssItem {
CIRCLE__7("tobago-circle-7"),
CIRCLE__8("tobago-circle-8"),
CIRCLE__9("tobago-circle-9"),
+ CLOSE("tobago-close"),
COLLAPSED("tobago-collapsed"),
DELETED("tobago-deleted"),
DESCENDING("tobago-descending"),
@@ -61,6 +62,7 @@ public enum TobagoClass implements CssItem {
FILTER("tobago-filter"),
HEADER("tobago-header"),
HELP__CONTAINER("tobago-help-container"),
+ HIDE_CLOSE_BUTTON("tobago-hideCloseButton"),
HIDE_TOGGLE_ICON("tobago-hideToggleIcon"),
LABEL("tobago-label"),
LABEL__CONTAINER("tobago-label-container"),
@@ -79,6 +81,7 @@ public enum TobagoClass implements CssItem {
// PAGE("tobago-page"),
PAGE__MENU_STORE("tobago-page-menuStore"),
PAGE__NOSCRIPT("tobago-page-noscript"),
+ PAGE__TOAST_STORE("tobago-page-toastStore"),
PAGING("tobago-paging"),
PANEL("tobago-panel"),
POPOVER__BOX("tobago-popover-box"),
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index c5306f537b..b67691c8ed 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -60,6 +60,7 @@ public enum DataAttributes implements MarkupLanguageAttributes {
DISABLED("data-tobago-disabled"),
DISMISS("data-dismiss"),
+ DISPOSE_DELAY("data-tobago-dispose-delay"),
/**
* Custom form attribute. Used to show the virtual form of the component.
@@ -132,6 +133,7 @@ public enum DataAttributes implements MarkupLanguageAttributes {
* Custom attribute to describe a pattern, e. g. for an date input field.
*/
PATTERN("data-tobago-pattern"),
+ PLACEMENT("data-tobago-placement"),
/**
* Custom reload attribute. Used to reload a panel.
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 253a13f0f5..5a3f1a0fe5 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
@@ -187,6 +187,7 @@ public enum HtmlElements {
TOBAGO_TAB("tobago-tab"),
TOBAGO_TAB_GROUP("tobago-tab-group"),
TOBAGO_TEXTAREA("tobago-textarea"),
+ TOBAGO_TOASTS("tobago-toasts"),
TOBAGO_TREE("tobago-tree"),
TOBAGO_TREE_LISTBOX("tobago-tree-listbox"),
TOBAGO_TREE_NODE("tobago-tree-node"),
diff --git a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties
index a1cb457232..404c2b9dad 100644
--- a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties
+++ b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties
@@ -18,6 +18,7 @@ object.noframe=Your user agent does not support frames or is currently configure
page.noscript=This site requires JavaScript, but JavaScript is currently not activated in your browser. \
To enable JavaScript you may consult your browsers the documentation.
alert.close=Close
+toast.close=Close
bar.toggleNavigation=Toggle navigation
sheet.ascending=Ascending
sheet.descending=Descending
diff --git a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties
index 0d87cc47d6..060318dc72 100644
--- a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties
+++ b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties
@@ -17,6 +17,7 @@ object.noframe=Ihr Browser stellt keine Frames dar. Den Inhalt k\u00F6nnen Sie h
page.noscript=Diese Seite ben\u00F6tigt JavaScript, allerdings ist JavaScript in Ihrem Browser derzeit deaktiviert. \
Um JavaScript zu aktivieren, lesen Sie ggf. die Anleitung Ihres Browsers.
alert.close=Schlie�en
+toast.close=Schlie�en
bar.toggleNavigation=Navigation umschalten
sheet.ascending=aufsteigend sortiert
sheet.descending=absteigend sortiert
diff --git a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties
index 92a33c6465..c1506faad5 100644
--- a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties
+++ b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties
@@ -18,6 +18,7 @@ object.noframe=Tu navegador no soporta frames o est\u00E1 configurado para no de
page.noscript=Esta p\u00E1gina requiere JavaScript, pero JavaScript no se activa actualmente en su navegador. \
Para permitir JavaScript usted puede consultar la documentaci\u00F3n del navegador.
alert.close=Cerrar
+toast.close=Cerrar
bar.toggleNavigation=Alternar navegaci\u00F3n
sheet.ascending=Ascendente
sheet.descending=Descendente
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/ToastsController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/ToastsController.java
new file mode 100644
index 0000000000..51057096a3
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/ToastsController.java
@@ -0,0 +1,194 @@
+/*
+ * 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 jakarta.enterprise.context.SessionScoped;
+import jakarta.faces.context.FacesContext;
+import jakarta.inject.Named;
+import org.apache.myfaces.tobago.application.Toast;
+import org.apache.myfaces.tobago.util.ResourceUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Named
+@SessionScoped
+public class ToastsController implements Serializable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final List<ToastFacesMessage> toastFacesMessages = new ArrayList<>();
+ private final List<Progress> progressBars = new ArrayList<>();
+ private final List<CustomToast> customToasts = new ArrayList<>();
+ private String severity;
+ private ScheduledExecutorService scheduledExecutorService;
+ private String customToastMessage;
+
+ public List<ToastFacesMessage> getToastFacesMessages() {
+ return toastFacesMessages;
+ }
+
+ public void createToastFacesMessage() {
+ toastFacesMessages.add(new ToastFacesMessage(severity, "Message text"));
+ }
+
+ public String getSeverity() {
+ return severity;
+ }
+
+ public void setSeverity(String severity) {
+ this.severity = severity;
+ }
+
+ public List<Progress> getProgressBars() {
+ return progressBars;
+ }
+
+ public void createProgressBarToast() {
+ progressBars.add(new Progress(100));
+
+ if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
+ scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+
+ scheduledExecutorService.scheduleAtFixedRate(() -> {
+ for (int i = progressBars.size() - 1; i >= 0; i--) {
+ Progress progress = progressBars.get(i);
+ if (progress.getValue() >= progress.getMax()) {
+ progressBars.remove(i);
+ }
+ }
+
+ for (Progress progress : progressBars) {
+ progress.increase();
+ }
+
+ if (progressBars.isEmpty()) {
+ scheduledExecutorService.shutdown();
+ }
+ }, 100, 100, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public List<CustomToast> getCustomToasts() {
+ return customToasts;
+ }
+
+ public void createCustomToast() {
+ customToasts.add(new CustomToast(customToastMessage));
+ }
+
+ public String getCustomToastMessage() {
+ return customToastMessage;
+ }
+
+ public void setCustomToastMessage(String customToastMessage) {
+ this.customToastMessage = customToastMessage;
+ }
+
+ public static class ToastFacesMessage implements Toast {
+ private final String id = String.valueOf(UUID.randomUUID());
+ private final String severity;
+ private final String text;
+
+ public ToastFacesMessage(String severity, String text) {
+ this.severity = severity;
+ this.text = text;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public String getSeverityColor() {
+ switch (severity) {
+ default:
+ case "info":
+ return "bg-info";
+ case "warn":
+ return "bg-warning";
+ case "error":
+ case "fatal":
+ return "bg-danger";
+ }
+ }
+
+ public String getSeverityText() {
+ return ResourceUtils.getString(FacesContext.getCurrentInstance(), "severity." + severity);
+ }
+
+ public String getText() {
+ return text;
+ }
+ }
+
+ public static class Progress implements Toast {
+ private final String id = String.valueOf(UUID.randomUUID());
+ private final int max;
+ private int value = 0;
+
+ public Progress(int max) {
+ this.max = max;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public void increase() {
+ value++;
+ }
+
+ public int getMax() {
+ return max;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public static class CustomToast implements Toast {
+ private final String id = String.valueOf(UUID.randomUUID());
+ private final String text;
+
+ public CustomToast(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public String getText() {
+ return text;
+ }
+ }
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/065-toast/Toast.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/065-toast/Toast.xhtml
new file mode 100644
index 0000000000..18d3e618af
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/065-toast/Toast.xhtml
@@ -0,0 +1,110 @@
+<?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">
+ <p>Toasts represent a type of notification message, commonly associated with push notifications on mobile devices.
+ The <code>value</code> attribute of the <code
+ class="language-markup"><tc:toasts/></code> tag contains a list of toasts
+ objects, each representing a single toast.
+ A toast object MUST implement the <code>org.apache.myfaces.tobago.application.Toast</code> interface.
+ If a toast has been closed, the corresponding object is removed from the list.
+ The content of the toasts is fully customizable.</p>
+ <p>Tag Library Documentation:
+ <tc:link label="<tc:toasts/>" image="#{request.contextPath}/image/feather-leaf.png"
+ link="#{apiController.tobagoTld}/tc/toasts.html"/></p>
+
+ <tc:section label="ToastFacesMessage">
+ <p>FacesMessage but as a toast.</p>
+ <tc:selectOneChoice label="Severity" value="#{toastsController.severity}">
+ <tc:selectItem itemLabel="Information" itemValue="info"/>
+ <tc:selectItem itemLabel="Warning" itemValue="warn"/>
+ <tc:selectItem itemLabel="Error" itemValue="error"/>
+ <tc:selectItem itemLabel="Fatal" itemValue="fatal"/>
+ <f:ajax/>
+ </tc:selectOneChoice>
+ <tc:button label="ToastFacesMessage" action="#{toastsController.createToastFacesMessage()}"/>
+ <tc:button label="ToastFacesMessage (AJAX)" action="#{toastsController.createToastFacesMessage()}">
+ <f:ajax execute="toastFacesMessages" render="toastFacesMessages"/>
+ </tc:button>
+
+ <tc:toasts id="toastFacesMessages" value="#{toastsController.toastFacesMessages}" var="toastFacesMessage"
+ placement="bottomLeft">
+ <f:facet name="header">
+ <tc:panel>
+ <tc:style customClass="rounded me-2 #{toastFacesMessage.severityColor}"
+ width="1.25rem" height="1.25rem"/>
+ </tc:panel>
+ <strong>
+ <tc:out value="#{toastFacesMessage.severityText}" plain="true"/>
+ </strong>
+ </f:facet>
+ <tc:out value="#{toastFacesMessage.text}" plain="true"/>
+ </tc:toasts>
+ </tc:section>
+
+ <tc:section label="Progress bar">
+ <p>Create a toast with a progress bar. The toast is removed after the progress is 100%.
+ The close button is hidden.</p>
+ <tc:button label="Progress bar toast" action="#{toastsController.createProgressBarToast()}"/>
+ <tc:button label="Progress bar toast (AJAX)" action="#{toastsController.createProgressBarToast()}">
+ <f:ajax execute="progressBarToasts" render="progressBarToasts"/>
+ </tc:button>
+
+ <tc:toasts id="progressBarToasts" value="#{toastsController.progressBars}" var="progress" disposeDelay="-1"
+ markup="hideCloseButton">
+ <f:facet name="reload">
+ <tc:reload frequency="100"/>
+ </f:facet>
+ <f:facet name="header">
+ <tc:out value="Progress" plain="true"/>
+ </f:facet>
+ <tc:progress value="#{progress.value}" max="#{progress.max}"/>
+ </tc:toasts>
+ </tc:section>
+
+ <tc:section label="Custom toast">
+ <p>Create a custom toast. The message can be written in the input field. The toast also contains an AJAX refresh
+ button for the following timestamp.</p>
+ <tc:in id="customToastMessage" label="Message" value="#{toastsController.customToastMessage}"/>
+ <tc:button label="Custom toast" action="#{toastsController.createCustomToast()}"/>
+ <tc:button label="Custom toast (AJAX)" action="#{toastsController.createCustomToast()}">
+ <f:ajax execute="customToastMessage customToasts" render="customToasts"/>
+ </tc:button>
+
+ <tc:out id="now" label="Timestamp" value="#{eventController.currentTimestamp}"/>
+
+ <tc:toasts id="customToasts" value="#{toastsController.customToasts}" var="customToast" disposeDelay="-1">
+ <tc:style customClass="customCssClass"/>
+ <f:facet name="header">
+ <tc:out value="Custom toast" plain="true"/>
+ </f:facet>
+ <tc:out value="Message: #{customToast.text}" plain="true"/>
+ <tc:panel>
+ <tc:style height="1rem"/>
+ </tc:panel>
+ <tc:button label="refresh timestamp">
+ <f:ajax render=":::now"/>
+ </tc:button>
+ </tc:toasts>
+ </tc:section>
+</ui:composition>
diff --git a/tobago-theme/src/main/scss/_tobago.scss b/tobago-theme/src/main/scss/_tobago.scss
index bce29254c1..b48b0ce710 100644
--- a/tobago-theme/src/main/scss/_tobago.scss
+++ b/tobago-theme/src/main/scss/_tobago.scss
@@ -1091,12 +1091,45 @@ tobago-overlay {
}
}
-/* page ----------------------------------------------------------- */
-
tobago-page {
display: block;
padding-top: $tobago-page-padding-top;
padding-bottom: 1rem;
+
+ .tobago-page-menuStore {
+ @include tobagoDropdownMenu();
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 0;
+ z-index: $zindex-popover;
+ }
+
+ .tobago-page-toastStore {
+ .toast {
+ &.tobago-hideCloseButton .tobago-close {
+ display: none;
+ }
+
+ .toast-header {
+ .tobago-close {
+ margin-left: auto;
+ }
+ }
+
+ .toast-body {
+ .tobago-close {
+ float: right;
+ }
+ }
+
+ .tobago-auto-spacing {
+ margin-bottom: 0;
+ }
+ }
+ }
}
.tobago-page-noscript {
@@ -1120,17 +1153,6 @@ tobago-page {
}
}
-.tobago-page-menuStore {
- @include tobagoDropdownMenu();
-
- position: absolute;
- top: 0;
- left: 0;
- width: 0;
- height: 0;
- z-index: $zindex-popover;
-}
-
/* panel ------------------------------------------------------------- */
// todo: remove
@@ -1742,6 +1764,10 @@ tobago-textarea {
display: block;
}
+tobago-toasts {
+ display: none;
+}
+
/* tree ---------------------------------------------------------------------- */
tobago-tree {
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 2addc09525..f303c49048 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
@@ -50,6 +50,7 @@ import "./tobago-stars";
import "./tobago-suggest";
import "./tobago-tab";
import "./tobago-textarea";
+import "./tobago-toasts";
import "./tobago-tree";
import "./tobago-tree-listbox";
import "./tobago-tree-node";
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts
index e377eb5048..a62e727c8f 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts
@@ -21,6 +21,7 @@ export enum Css {
ACTIVE = "active",
AUTOCOMPLETE = "autocomplete",
AUTOCOMPLETE_INPUT = "autocomplete-input",
+ BOTTOM_0 = "bottom-0",
COLLAPSE = "collapse",
COLLAPSING = "collapsing",
D_NONE = "d-none",
@@ -29,13 +30,22 @@ export enum Css {
DROPDOWN_ITEM = "dropdown-item",
DROPDOWN_MENU = "dropdown-menu",
DROPDOWN_MENU_END = "dropdown-menu-end",
+ END_0 = "end-0",
+ FADE = "fade",
FIXED_BOTTOM = "fixed-bottom",
INPUT_GROUP = "input-group",
+ P_3 = "p-3",
+ POSITION_FIXED = "position-fixed",
POSITION_RELATIVE = "position-relative",
SHOW = "show",
+ START_0 = "start-0",
+ START_50 = "start-50",
TABLE_ACTIVE = "table-active",
TABLE_INFO = "table-info",
TABLE_PRIMARY = "table-primary",
+ TOAST_BODY = "toast-body",
+ TOAST_CONTAINER = "toast-container",
+ TOAST_HEADER = "toast-header",
TOBAGO_COLLAPSED = "tobago-collapsed",
TOBAGO_DISABLED = "tobago-disabled",
TOBAGO_DROP_ZONE = "tobago-drop-zone",
@@ -50,5 +60,10 @@ export enum Css {
TOBAGO_SELECTED = "tobago-selected",
TOBAGO_SHOW = "tobago-show",
TOBAGO_SPLITLAYOUT_HORIZONTAL = "tobago-splitLayout-horizontal",
+ TOP_0 = "top-0",
+ TOP_50 = "top-50",
+ TRANSLATE_MIDDLE = "translate-middle",
+ TRANSLATE_MIDDLE_X = "translate-middle-x",
+ TRANSLATE_MIDDLE_Y = "translate-middle-y",
TRASH = "trash"
}
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-placement.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-placement.ts
new file mode 100644
index 0000000000..2e089491de
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-placement.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 enum Placement {
+ topLeft,
+ topCenter,
+ topRight,
+ middleLeft,
+ middleCenter,
+ middleRight,
+ bottomLeft,
+ bottomCenter,
+ bottomRight
+}
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-toasts.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-toasts.ts
new file mode 100644
index 0000000000..d042827238
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-toasts.ts
@@ -0,0 +1,242 @@
+/*
+ * 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 {Toast} from "bootstrap";
+import {Placement} from "./tobago-placement";
+import {Css} from "./tobago-css";
+
+enum StateEnum {
+ created = "created",
+ showed = "showed",
+ closed = "closed"
+}
+
+interface StateData {
+ state: StateEnum;
+ hideTime: string;
+}
+
+const BootstrapToastEvent = {
+ HIDE: "hide.bs.toast",
+ HIDDEN: "hidden.bs.toast",
+ SHOW: "show.bs.toast",
+ SHOWN: "shown.bs.toast"
+};
+
+class Toasts extends HTMLElement {
+
+ constructor() {
+ super();
+ }
+
+ get toastStore(): HTMLDivElement {
+ const root = document.getRootNode() as ShadowRoot | Document;
+ return root.querySelector<HTMLDivElement>(".tobago-page-toastStore");
+ }
+
+ get localToasts(): NodeListOf<HTMLDivElement> {
+ return this.querySelectorAll(".toast");
+ }
+
+ get storeToasts(): NodeListOf<HTMLDivElement> {
+ return this.toastStore.querySelectorAll(".toast[name^='" + this.id + "::']");
+ }
+
+ get statesInput(): HTMLInputElement {
+ return this.querySelector("input[name='" + this.id + "::states']");
+ }
+
+ get states(): Map<string, StateData> {
+ return new Map(Object.entries(JSON.parse(this.statesInput.value)));
+ }
+
+ set states(states: Map<string, StateData>) {
+ this.statesInput.value = JSON.stringify(Object.fromEntries(states));
+ }
+
+ connectedCallback(): void {
+ for (const storeToast of this.storeToasts) {
+ const localToast = this.querySelector<HTMLDivElement>(".toast[id='" + storeToast.getAttribute("name") + "']");
+ if (!localToast) {
+ this.removeToast(storeToast);
+ }
+ }
+
+ for (const localToast of this.localToasts) {
+ const storeToast = this.toastStore.querySelector<HTMLDivElement>(".toast[name='" + localToast.id + "']");
+ if (storeToast) {
+ this.updateToast(storeToast, localToast);
+ } else {
+ this.showToast(localToast);
+ }
+ }
+ }
+
+ private removeToast(storeToast: HTMLDivElement): void {
+ const toast = new Toast(storeToast, {autohide: false, delay: 0});
+ this.addHiddenEventListeners(storeToast);
+ toast.hide();
+ }
+
+ private updateToast(storeToast: HTMLDivElement, localToast: HTMLDivElement): void {
+ storeToast.replaceChildren(
+ localToast.querySelector("." + Css.TOAST_HEADER),
+ localToast.querySelector("." + Css.TOAST_BODY));
+ }
+
+ private showToast(localToast: HTMLDivElement): void {
+ const placement: Placement = Placement[localToast.dataset.tobagoPlacement];
+ const disposeDelay = Number(localToast.dataset.tobagoDisposeDelay);
+
+ localToast.removeAttribute("id");
+ this.getToastContainer(placement).insertAdjacentElement("beforeend", localToast);
+
+ this.addShowEventListeners(localToast);
+ this.addHideEventListeners(localToast);
+ this.addHiddenEventListeners(localToast);
+
+ const toast = new Toast(localToast, {autohide: disposeDelay > 0, delay: Math.max(disposeDelay, 0)});
+ const id = localToast.getAttribute("name");
+ const stateData = this.states.get(id);
+
+ if (stateData.state === StateEnum.created) {
+ toast.show();
+ } else if (stateData.state === StateEnum.showed) {
+ localToast.classList.add(Css.SHOW);
+ localToast.classList.add(Css.FADE); //set fade for hide animation; set fade after show!
+
+ if (disposeDelay >= 0) {
+ setTimeout(() => {
+ toast.hide();
+ }, disposeDelay);
+ }
+ }
+ }
+
+ private addShowEventListeners(toast: HTMLDivElement) {
+ const id = toast.getAttribute("name");
+ toast.addEventListener("show.bs.toast", () => {
+ const states = this.states;
+ states.get(id).state = StateEnum.showed;
+ this.states = states;
+ faces.ajax.request(this.id, null, {
+ "javax.faces.behavior.event": "toastShown",
+ execute: this.id
+ });
+ });
+
+ }
+
+ private addHideEventListeners(toast: HTMLDivElement) {
+ const id = toast.getAttribute("name");
+ toast.addEventListener("hide.bs.toast", () => {
+ const states = this.states;
+ states.get(id).state = StateEnum.closed;
+ this.states = states;
+ faces.ajax.request(this.id, null, {
+ "javax.faces.behavior.event": "toastHide",
+ execute: this.id
+ });
+ });
+ }
+
+ private addHiddenEventListeners(toast: HTMLDivElement) {
+ toast.addEventListener("hidden.bs.toast", () => {
+ toast.remove();
+ });
+ }
+
+ private getToastContainer(placement: Placement): HTMLDivElement {
+ const cssClasses = this.getToastContainerCssClass(placement);
+ const selector = cssClasses.join(".");
+ const element = this.toastStore.querySelector<HTMLDivElement>("." + selector);
+ if (element) {
+ return element;
+ } else {
+ const div = document.createElement("div");
+ for (const cssClass of cssClasses) {
+ div.classList.add(cssClass);
+ }
+ return this.toastStore.insertAdjacentElement("beforeend", div) as HTMLDivElement;
+ }
+ }
+
+ private getToastContainerCssClass(placement: Placement): Css[] {
+ const css: Css[] = [];
+ css.push(Css.TOAST_CONTAINER);
+ css.push(Css.POSITION_FIXED);
+ css.push(Css.P_3);
+
+ switch (placement) {
+ case Placement.topLeft:
+ case Placement.topCenter:
+ case Placement.topRight:
+ css.push(Css.TOP_0);
+ break;
+ case Placement.middleLeft:
+ case Placement.middleCenter:
+ case Placement.middleRight:
+ css.push(Css.TOP_50);
+ break;
+ case Placement.bottomLeft:
+ case Placement.bottomCenter:
+ case Placement.bottomRight:
+ css.push(Css.BOTTOM_0);
+ break;
+ }
+
+ switch (placement) {
+ case Placement.topLeft:
+ case Placement.middleLeft:
+ case Placement.bottomLeft:
+ css.push(Css.START_0);
+ break;
+ case Placement.topCenter:
+ case Placement.middleCenter:
+ case Placement.bottomCenter:
+ css.push(Css.START_50);
+ break;
+ case Placement.topRight:
+ case Placement.middleRight:
+ case Placement.bottomRight:
+ css.push(Css.END_0);
+ break;
+ }
+
+ switch (placement) {
+ case Placement.topCenter:
+ case Placement.bottomCenter:
+ css.push(Css.TRANSLATE_MIDDLE_X);
+ break;
+ case Placement.middleLeft:
+ case Placement.middleRight:
+ css.push(Css.TRANSLATE_MIDDLE_Y);
+ break;
+ case Placement.middleCenter:
+ css.push(Css.TRANSLATE_MIDDLE);
+ break;
+ }
+
+ return css;
+ }
+}
+
+document.addEventListener("tobago.init", function (event: Event): void {
+ if (window.customElements.get("tobago-toasts") == null) {
+ window.customElements.define("tobago-toasts", Toasts);
+ }
+});
diff --git a/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg b/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg
index 3d1ee8b58f..9426af5b30 100644
--- a/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg
+++ b/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg
@@ -395,6 +395,10 @@ AlignItemsProperty(property) ::= <<
<NormalProperty(property)>
>>
+PlacementProperty(property) ::= <<
+<NormalProperty(property)>
+>>
+
LocalDateProperty(property) ::= <<
<NormalProperty(property)>
>>