View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.renderkit.renderer;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.ClientBehaviors;
24  import org.apache.myfaces.tobago.component.Facets;
25  import org.apache.myfaces.tobago.component.RendererTypes;
26  import org.apache.myfaces.tobago.component.Tags;
27  import org.apache.myfaces.tobago.context.Markup;
28  import org.apache.myfaces.tobago.event.TabChangeEvent;
29  import org.apache.myfaces.tobago.internal.behavior.EventBehavior;
30  import org.apache.myfaces.tobago.internal.component.AbstractUIEvent;
31  import org.apache.myfaces.tobago.internal.component.AbstractUIPanelBase;
32  import org.apache.myfaces.tobago.internal.component.AbstractUITab;
33  import org.apache.myfaces.tobago.internal.component.AbstractUITabGroup;
34  import org.apache.myfaces.tobago.internal.util.AccessKeyLogger;
35  import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
36  import org.apache.myfaces.tobago.model.SwitchType;
37  import org.apache.myfaces.tobago.renderkit.LabelWithAccessKey;
38  import org.apache.myfaces.tobago.renderkit.RendererBase;
39  import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
40  import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
41  import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
42  import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
43  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
44  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
45  import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
46  import org.apache.myfaces.tobago.renderkit.html.HtmlRoleValues;
47  import org.apache.myfaces.tobago.util.ComponentUtils;
48  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import javax.el.ValueExpression;
53  import javax.faces.application.FacesMessage;
54  import javax.faces.component.UIComponent;
55  import javax.faces.component.UINamingContainer;
56  import javax.faces.component.behavior.AjaxBehavior;
57  import javax.faces.context.FacesContext;
58  import javax.faces.event.ComponentSystemEvent;
59  import javax.faces.event.ComponentSystemEventListener;
60  import javax.faces.event.ListenerFor;
61  import javax.faces.event.PostAddToViewEvent;
62  import java.io.IOException;
63  import java.lang.invoke.MethodHandles;
64  import java.util.Collection;
65  import java.util.Collections;
66  import java.util.Map;
67  
68  @ListenerFor(systemEventClass = PostAddToViewEvent.class)
69  public class TabGroupRenderer<T extends AbstractUITabGroup> extends RendererBase<T>
70      implements ComponentSystemEventListener {
71  
72    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
73  
74    private static final String INDEX_POSTFIX = ComponentUtils.SUB_SEPARATOR + "index";
75  
76    @Override
77    public void processEvent(final ComponentSystemEvent event) {
78  
79      final AbstractUITabGroup tabGroup = (AbstractUITabGroup) event.getComponent();
80  
81      for (final UIComponent child : tabGroup.getChildren()) {
82        if (child instanceof AbstractUITab) {
83          final AbstractUITab tab = (AbstractUITab) child;
84          final FacesContext facesContext = FacesContext.getCurrentInstance();
85          final ClientBehaviors click = ClientBehaviors.click;
86          switch (tabGroup.getSwitchType()) {
87            case none:
88              break;
89            case client:
90              // todo: implement a client behavior which can call local scripts (respect CSP)
91              break;
92            case reloadTab:
93              final AjaxBehavior ajaxBehavior = new AjaxBehavior();
94              final Collection<String> ids = Collections.singleton(
95                  UINamingContainer.getSeparatorChar(facesContext) + tabGroup.getClientId(facesContext));
96              ajaxBehavior.setExecute(ids);
97              ajaxBehavior.setRender(ids);
98              tab.addClientBehavior(click.name(), ajaxBehavior);
99              break;
100           case reloadPage:
101             final AbstractUIEvent component = (AbstractUIEvent) ComponentUtils.createComponent(
102                 facesContext, Tags.event.componentType(), RendererTypes.Event, "_click");
103             component.setEvent(click);
104             tab.getChildren().add(component);
105             final EventBehavior eventBehavior = new EventBehavior();
106             eventBehavior.setFor(component.getId());
107             tab.addClientBehavior(click.name(), eventBehavior);
108             break;
109           default:
110             LOG.error("Unknown switch type: '{}'", tabGroup.getSwitchType());
111         }
112       }
113     }
114   }
115 
116   @Override
117   public void decodeInternal(final FacesContext facesContext, final T component) {
118     final int oldIndex = component.getRenderedIndex();
119 
120     final String clientId = component.getClientId(facesContext);
121     final Map<String, String> parameters = facesContext.getExternalContext().getRequestParameterMap();
122     final String newValue = parameters.get(clientId + INDEX_POSTFIX);
123     try {
124       final int newIndex = Integer.parseInt(newValue);
125       if (newIndex != oldIndex) {
126         final TabChangeEvent event = new TabChangeEvent(component, oldIndex, newIndex);
127         component.queueEvent(event);
128       }
129     } catch (final NumberFormatException e) {
130       LOG.error("Can't parse newIndex: '" + newValue + "'");
131     }
132   }
133 
134   @Override
135   public void encodeEndInternal(final FacesContext facesContext, final T uiComponent) throws IOException {
136 
137     final int selectedIndex = ensureRenderedSelectedIndex(facesContext, uiComponent);
138     final String clientId = uiComponent.getClientId(facesContext);
139     final String hiddenId = clientId + TabGroupRenderer.INDEX_POSTFIX;
140     final SwitchType switchType = uiComponent.getSwitchType();
141     final Markup markup = uiComponent.getMarkup();
142     final TobagoResponseWriter writer = getResponseWriter(facesContext);
143 
144     writer.startElement(HtmlElements.TOBAGO_TAB_GROUP);
145     writer.writeIdAttribute(clientId);
146     writer.writeClassAttribute(
147         BootstrapClass.CARD,
148         TobagoClass.TAB_GROUP.createMarkup(markup),
149         uiComponent.getCustomClass(),
150         markup != null && markup.contains(Markup.SPREAD) ? TobagoClass.SPREAD : null);
151     HtmlRendererUtils.writeDataAttributes(facesContext, writer, uiComponent);
152     writer.writeAttribute(CustomAttributes.SWITCH_TYPE, switchType.name(), false);
153 
154     encodeBehavior(writer, facesContext, uiComponent);
155 
156     writer.startElement(HtmlElements.INPUT);
157     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
158     writer.writeAttribute(HtmlAttributes.VALUE, selectedIndex);
159     writer.writeNameAttribute(hiddenId);
160     writer.writeIdAttribute(hiddenId);
161     writer.endElement(HtmlElements.INPUT);
162 
163     if (uiComponent.isShowNavigationBar()) {
164       encodeHeader(facesContext, writer, uiComponent, selectedIndex, switchType);
165     }
166 
167     encodeContent(facesContext, writer, uiComponent, selectedIndex, switchType);
168 
169     writer.endElement(HtmlElements.TOBAGO_TAB_GROUP);
170   }
171 
172   private int ensureRenderedSelectedIndex(final FacesContext context, final AbstractUITabGroup tabGroup) {
173     final int selectedIndex = tabGroup.getSelectedIndex();
174     // ensure to select a rendered tab
175     int index = -1;
176     int closestRenderedTabIndex = -1;
177     for (final UIComponent tab : tabGroup.getChildren()) {
178       if (tab instanceof AbstractUIPanelBase) {
179         index++;
180         if (index == selectedIndex) {
181           if (tab.isRendered()) {
182             return index;
183           } else if (closestRenderedTabIndex > -1) {
184             break;
185           }
186         }
187         if (tab.isRendered()) {
188           closestRenderedTabIndex = index;
189           if (index > selectedIndex) {
190             break;
191           }
192         }
193       }
194     }
195     if (closestRenderedTabIndex == -1) {
196       // resetting index to 0
197       closestRenderedTabIndex = 0;
198     }
199     final ValueExpression expression = tabGroup.getValueExpression(Attributes.selectedIndex.getName());
200     if (expression != null) {
201       expression.setValue(context.getELContext(), closestRenderedTabIndex);
202     } else {
203       tabGroup.setSelectedIndex(closestRenderedTabIndex);
204     }
205     return closestRenderedTabIndex;
206   }
207 
208   private void encodeHeader(
209       final FacesContext facesContext, final TobagoResponseWriter writer, final AbstractUITabGroup tabGroup,
210       final int selectedIndex, final SwitchType switchType)
211       throws IOException {
212 
213     final String tabGroupClientId = tabGroup.getClientId(facesContext);
214 
215     writer.startElement(HtmlElements.DIV);
216     writer.writeClassAttribute(BootstrapClass.CARD_HEADER);
217     writer.startElement(HtmlElements.UL);
218     writer.writeClassAttribute(
219         BootstrapClass.NAV,
220         BootstrapClass.NAV_TABS,
221         BootstrapClass.CARD_HEADER_TABS);
222     writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.TABLIST.toString(), false);
223 
224     int index = 0;
225     for (final UIComponent child : tabGroup.getChildren()) {
226       if (child instanceof AbstractUITab) {
227         final AbstractUITab tab = (AbstractUITab) child;
228         if (tab.isRendered()) {
229           final LabelWithAccessKey label = new LabelWithAccessKey(tab);
230           final UIComponent labelFacet = ComponentUtils.getFacet(tab, Facets.label);
231           final UIComponent barFacet = ComponentUtils.getFacet(tab, Facets.bar);
232           final boolean disabled = tab.isDisabled();
233           final String tabId = tab.getClientId(facesContext);
234           Markup markup = tab.getMarkup() != null ? tab.getMarkup() : Markup.NULL;
235 
236           final FacesMessage.Severity maxSeverity
237               = ComponentUtils.getMaximumSeverityOfChildrenMessages(facesContext, tab);
238           if (maxSeverity != null) {
239             markup = markup.add(ComponentUtils.markupOfSeverity(maxSeverity));
240           }
241 
242           writer.startElement(HtmlElements.TOBAGO_TAB);
243           writer.writeIdAttribute(tabId);
244           writer.writeClassAttribute(
245               BootstrapClass.NAV_ITEM,
246               TobagoClass.TAB.createMarkup(markup),
247               barFacet != null ? TobagoClass.TAB__BAR_FACET : null,
248               tab.getCustomClass());
249           writer.writeAttribute(HtmlAttributes.FOR, tabGroupClientId, true);
250           writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.PRESENTATION.toString(), false);
251           writer.writeAttribute(CustomAttributes.INDEX, index);
252           final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, tab);
253           if (title != null) {
254             writer.writeAttribute(HtmlAttributes.TITLE, title, true);
255           }
256 
257           writer.startElement(HtmlElements.A);
258           if (!tab.isDisabled()) {
259             writer.writeAttribute(DataAttributes.TOGGLE, "tab", false);
260           }
261           if (tab.isDisabled()) {
262             writer.writeClassAttribute(BootstrapClass.NAV_LINK, BootstrapClass.DISABLED);
263           } else if (selectedIndex == index) {
264             writer.writeClassAttribute(BootstrapClass.NAV_LINK, BootstrapClass.ACTIVE);
265           } else {
266             writer.writeClassAttribute(BootstrapClass.NAV_LINK);
267           }
268           if (!disabled && switchType == SwitchType.client) {
269             writer.writeAttribute(HtmlAttributes.HREF, '#' + getTabPanelId(facesContext, tab), false);
270             writer.writeAttribute(
271                 DataAttributes.TARGET, '#' + getTabPanelId(facesContext, tab).replaceAll(":", "\\\\:"), false);
272           }
273 
274           if (!disabled && label.getAccessKey() != null) {
275             writer.writeAttribute(HtmlAttributes.ACCESSKEY, Character.toString(label.getAccessKey()), false);
276             AccessKeyLogger.addAccessKey(facesContext, label.getAccessKey(), tabId);
277           }
278           writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.TAB.toString(), false);
279 
280           if (!disabled) {
281             encodeBehavior(writer, facesContext, tab);
282           }
283 
284           boolean labelEmpty = true;
285           final String image = tab.getImage();
286           // tab.getImage() resolves to empty string if el-expression resolves to null
287           if (image != null && !image.isEmpty()) {
288             writer.startElement(HtmlElements.IMG);
289             writer.writeAttribute(HtmlAttributes.SRC, image, true);
290 // TBD      writer.writeClassAttribute(Classes.create(tab, (label.getLabel() != null? "image-right-margin" : "image")));
291             writer.endElement(HtmlElements.IMG);
292             labelEmpty = false;
293           }
294           if (label.getLabel() != null) {
295             HtmlRendererUtils.writeLabelWithAccessKey(writer, label);
296             labelEmpty = false;
297           }
298           if (labelFacet != null) {
299             labelFacet.encodeAll(facesContext);
300             labelEmpty = false;
301           }
302           if (labelEmpty) {
303             writer.writeText(Integer.toString(index + 1));
304           }
305           writer.endElement(HtmlElements.A);
306 
307           if (barFacet != null) {
308             writer.startElement(HtmlElements.DIV);
309             barFacet.encodeAll(facesContext);
310             writer.endElement(HtmlElements.DIV);
311           }
312 
313           writer.endElement(HtmlElements.TOBAGO_TAB);
314         }
315         index++;
316       }
317     }
318     writer.endElement(HtmlElements.UL);
319     writer.endElement(HtmlElements.DIV);
320   }
321 
322   protected void encodeContent(
323       final FacesContext facesContext, final TobagoResponseWriter writer, final AbstractUITabGroup tabGroup,
324       final int selectedIndex, final SwitchType switchType) throws IOException {
325     writer.startElement(HtmlElements.DIV);
326     writer.writeClassAttribute(BootstrapClass.CARD_BODY, BootstrapClass.TAB_CONTENT);
327     int index = 0;
328     for (final UIComponent child : tabGroup.getChildren()) {
329       if (child instanceof AbstractUITab) {
330         final AbstractUITab tab = (AbstractUITab) child;
331         if (tab.isRendered() && (switchType == SwitchType.client || index == selectedIndex) && !tab.isDisabled()) {
332           final Markup markup = tab.getMarkup();
333 
334           writer.startElement(HtmlElements.TOBAGO_TAB_CONTENT);
335           writer.writeClassAttribute(
336               BootstrapClass.TAB_PANE,
337               TobagoClass.TAB__CONTENT.createMarkup(markup),
338               index == selectedIndex ? BootstrapClass.ACTIVE : null);
339           writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.TABPANEL.toString(), false);
340           writer.writeIdAttribute(getTabPanelId(facesContext, tab));
341 
342           writer.writeAttribute(CustomAttributes.INDEX, index);
343 
344           tab.encodeAll(facesContext);
345 
346           writer.endElement(HtmlElements.TOBAGO_TAB_CONTENT);
347         }
348         index++;
349       }
350     }
351     writer.endElement(HtmlElements.DIV);
352   }
353 
354   private String getTabPanelId(final FacesContext facesContext, final AbstractUITab tab) {
355     return tab.getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + "content";
356   }
357 }