1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
287 if (image != null && !image.isEmpty()) {
288 writer.startElement(HtmlElements.IMG);
289 writer.writeAttribute(HtmlAttributes.SRC, image, true);
290
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 }