/** * Copyright 2014-2017 Riccardo Massera (TheCoder4.Eu) and Stephan Rauh (http://www.beyondjava.net). * * This file is part of BootsFaces. * * Licensed 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 net.bootsfaces.component.tabView; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.el.ELException; import javax.el.ValueExpression; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.FacesRenderer; import net.bootsfaces.component.ajax.AJAXRenderer; import net.bootsfaces.component.tab.Tab; import net.bootsfaces.render.CoreRenderer; import net.bootsfaces.render.H; import net.bootsfaces.render.R; import net.bootsfaces.render.Tooltip; import net.bootsfaces.utils.FacesMessages; /** This class generates the HTML code of <b:tabView />. */ @FacesRenderer(componentFamily = "net.bootsfaces.component", rendererType = "net.bootsfaces.component.tabView.TabView") public class TabViewRenderer extends CoreRenderer { @Override public void encodeChildren(FacesContext context, UIComponent component) throws IOException { // TODO Auto-generated method stub // super.encodeChildren(context, component); } /** * Decode to be used to implement an AJAX version of TabView. This methods * receives and processes input made by the user. More specifically, it * ckecks whether the user has interacted with the current b:tabView. The * default implementation simply stores the input value in the list of * submitted values. If the validation checks are passed, the values in the * <code>submittedValues</code> list are store in the backend bean. * * @param context * the FacesContext. * @param component * the current b:tabView. */ @Override public void decode(FacesContext context, UIComponent component) { TabView tabView = (TabView) component; if (!tabView.isRendered()) { return; } if (tabView.isDisabled()) { return; } decodeBehaviors(context, tabView); String clientId = tabView.getClientId(context); String activeIndexId = clientId.replace(":", "_") + "_activeIndex"; String newIndexValue = (String) context.getExternalContext().getRequestParameterMap().get(activeIndexId); new AJAXRenderer().decode(context, component); if (null != newIndexValue && newIndexValue.length() > 0) { try { if (Integer.valueOf(newIndexValue) != tabView.getActiveIndex()) { int newIndex = Integer.valueOf(newIndexValue); if (newIndex < tabView.getChildCount()) { if (!(((Tab) tabView.getChildren().get(newIndex)).isDisabled())) { ValueExpression ve = component.getValueExpression("activeIndex"); if (ve != null) { try { ve.setValue(context.getELContext(), newIndex); } catch (ELException e) { tabView.setActiveIndex(newIndex); } catch (Exception e) { tabView.setActiveIndex(newIndex); } } else { tabView.setActiveIndex(newIndex); } } } } } catch (NumberFormatException e) { } } } /** * This methods generates the HTML code of the current b:tabView. * * @param context * the FacesContext. * @param component * the current b:tabView. * @throws IOException * thrown if something goes wrong when writing the HTML code. */ @Override public void encodeBegin(FacesContext context, UIComponent component) throws IOException { assertComponentIsInsideForm(component, "The initially opened tab is opened after an post-back request, regardless which tab has been previously activated by the user"); if (!component.isRendered()) { return; } TabView tabView = (TabView) component; ResponseWriter writer = context.getResponseWriter(); String clientId = tabView.getClientId(); if (!tabView.isRendered()) { return; } writer.startElement("input", tabView); writer.writeAttribute("type", "hidden", null); final String hiddenInputFieldID = clientId.replace(":", "_") + "_activeIndex"; writer.writeAttribute("name", hiddenInputFieldID, "name"); writer.writeAttribute("id", hiddenInputFieldID, "id"); writer.writeAttribute("value", "", "value"); writer.endElement("input"); // set tab directions final String tabPosition = tabView.getTabPosition(); String wrapperClass = "tab-panel"; if ("bottom".equalsIgnoreCase(tabPosition)) wrapperClass += " tabs-below"; writer.startElement("div", tabView); writer.writeAttribute("class", wrapperClass, "class"); writer.writeAttribute("role", "tabpanel", "class"); writer.writeAttribute("dir", tabView.getDir(), "dir"); final List<UIComponent> tabs = getTabs(tabView); int activeIndex = forceActiveTabToBeEnabled(tabView, tabs); if ("bottom".equalsIgnoreCase(tabPosition)) { encodeTabContentPanes(context, writer, tabView, activeIndex, tabs); encodeTabLinks(context, writer, tabView, activeIndex, tabs, clientId, hiddenInputFieldID); } else if ("left".equalsIgnoreCase(tabPosition)) { writer.startElement("div", component); writer.writeAttribute("class", "col-md-2", "class"); encodeTabLinks(context, writer, tabView, activeIndex, tabs, clientId, hiddenInputFieldID); writer.endElement("div"); writer.startElement("div", component); writer.writeAttribute("class", "col-md-10", "class"); encodeTabContentPanes(context, writer, tabView, activeIndex, tabs); writer.endElement("div"); drawClearDiv(writer, tabView); } else if ("right".equalsIgnoreCase(tabPosition)) { writer.startElement("div", component); writer.writeAttribute("class", "col-md-10", "class"); encodeTabContentPanes(context, writer, tabView, activeIndex, tabs); writer.endElement("div"); writer.startElement("div", component); writer.writeAttribute("class", "col-md-2", "class"); encodeTabLinks(context, writer, tabView, activeIndex, tabs, clientId, hiddenInputFieldID); writer.endElement("div"); drawClearDiv(writer, tabView); } else { encodeTabLinks(context, writer, tabView, activeIndex, tabs, clientId, hiddenInputFieldID); encodeTabContentPanes(context, writer, tabView, activeIndex, tabs); } writer.endElement("div"); new AJAXRenderer().generateBootsFacesAJAXAndJavaScriptForJQuery(context, component, writer, "#" + clientId + " > li > a[data-toggle=\"tab\"]", null); Tooltip.activateTooltips(context, tabView); } /** * returns the currently active tab. If the attribute getActiveIndex() points to an inactive tab, the next active tag is returned. * Important: this method must not call setActiveIndex(), because this means the you can't control the tabs from a Java bean. * Setting an attributes overrides EL expressions defined by the JSF page. * @param tabView * @param tabs * @return Integer.MAX_VALUE if there's no active tab. */ private int forceActiveTabToBeEnabled(TabView tabView, final List<UIComponent> tabs) { int activeIndex = tabView.getActiveIndex(); if (tabView.isDisabled()) { activeIndex = Integer.MAX_VALUE; } if (activeIndex < 0) { activeIndex = tabs.size() + activeIndex; // -2 is the second tab // from the right hand // side } if (activeIndex >= 0 && activeIndex < Integer.MAX_VALUE) { if (activeIndex >= tabs.size()) { activeIndex = tabs.size() - 1; } int newActiveIndex = activeIndex; while (((Tab) tabs.get(newActiveIndex)).isDisabled()) { newActiveIndex++; if (newActiveIndex >= tabs.size()) { newActiveIndex = 0; } if (newActiveIndex == activeIndex) { newActiveIndex = Integer.MAX_VALUE; break; } } if (activeIndex != newActiveIndex) { ValueExpression ve = tabView.getValueExpression("activeIndex"); if (ve != null) { try { ve.setValue(FacesContext.getCurrentInstance().getELContext(), newActiveIndex); } catch (ELException e) { } catch (Exception e) { } } } activeIndex = newActiveIndex; } return activeIndex; } /** * Draw a clear div * * @param writer * @param tabView * @throws IOException */ private static void drawClearDiv(ResponseWriter writer, UIComponent tabView) throws IOException { writer.startElement("div", tabView); writer.writeAttribute("style", "clear:both;", "style"); writer.endElement("div"); } /** * Encode the list of links that render the tabs * * @param context * @param writer * @param tabView * @param currentlyActiveIndex * @param tabs * @param clientId * @param hiddenInputFieldID * @throws IOException */ private static void encodeTabLinks(FacesContext context, ResponseWriter writer, TabView tabView, int currentlyActiveIndex, List<UIComponent> tabs, String clientId, String hiddenInputFieldID) throws IOException { writer.startElement("ul", tabView); writer.writeAttribute("id", clientId, "id"); Tooltip.generateTooltip(context, tabView, writer); String classes = "nav "; if ("left".equalsIgnoreCase(tabView.getTabPosition()) || "right".equalsIgnoreCase(tabView.getTabPosition())) { classes += " nav-pills nav-stacked"; } else { classes = classes + (tabView.isPills() ? " nav-pills" : " nav-tabs"); } if (tabView.getStyleClass() != null) { classes += " "; classes += tabView.getStyleClass(); } writer.writeAttribute("class", classes, "class"); String role = "tablist"; AJAXRenderer.generateBootsFacesAJAXAndJavaScript(context, tabView, writer, false); R.encodeHTML4DHTMLAttrs(writer, tabView.getAttributes(), H.TAB_VIEW); if (tabView.getRole() != null) { role = tabView.getRole(); } writer.writeAttribute("role", role, "role"); encodeTabs(context, writer, tabs, currentlyActiveIndex, hiddenInputFieldID, tabView.isDisabled()); writer.endElement("ul"); } /** * Essentially, getTabs() does the same as getChildren(), but it filters * everything that's not a tab. In particular, comments are ignored. See * issue 77 (https://github.com/TheCoder4eu/BootsFaces-OSP/issues/77). * * @return */ private List<UIComponent> getTabs(TabView tabView) { List<UIComponent> children = tabView.getChildren(); List<UIComponent> filtered = new ArrayList<UIComponent>(children.size()); for (UIComponent c : children) { if (c instanceof Tab) filtered.add(c); } return filtered; } /** * Generates the HTML of the tab panes. * * @param context * the current FacesContext * @param writer * the response writer * @param tabs * @param children * the tabs * @throws IOException * only thrown if something's wrong with the response writer */ private static void encodeTabContentPanes(FacesContext context, ResponseWriter writer, TabView tabView, int currentlyActiveIndex, List<UIComponent> tabs) throws IOException { writer.startElement("div", tabView); String classes = "tab-content"; if (tabView.getContentClass() != null) { classes += " "; classes += tabView.getContentClass(); } writer.writeAttribute("class", classes, "class"); if (tabView.getContentStyle() != null) { String inlineCSS = tabView.getContentStyle(); writer.writeAttribute("style", inlineCSS, "style"); } String role = "tablist"; if (tabView.getRole() != null) { role = tabView.getRole(); } writer.writeAttribute("role", role, "role"); // int activeIndex = tabView.getActiveIndex(); if (null != tabs) { for (int index = 0; index < tabs.size(); index++) { if (tabs.get(index).isRendered()) { encodeTabPane(context, writer, tabs.get(index), (index == currentlyActiveIndex) && (!tabView.isDisabled())); } } } writer.endElement("div"); } /** * Generate an individual tab pane. Basically, that's <div * role="tabpanel" class="tab-pane active" id="home"< {{childContent}} * >/div> * * @param context * the current FacesContext * @param writer * the response writer * @param tab * the tab to be rendered. * @param isActive * is the current tab active? * @throws IOException * only thrown if something's wrong with the response writer */ private static void encodeTabPane(FacesContext context, ResponseWriter writer, UIComponent child, boolean isActive) throws IOException { Tab tab = (Tab) child; writer.startElement("div", tab); writer.writeAttribute("id", tab.getClientId().replace(":", "_") + "_pane", "id"); if (tab.getDir() != null) writer.writeAttribute("dir", tab.getDir(), "dir"); String classes = "tab-pane"; if (!tab.isDisabled()) { if (isActive) { classes += " active"; } } if (tab.getStyleClass() != null) { classes += " "; classes += tab.getStyleClass(); } if (tab.getContentStyle() != null) { writer.writeAttribute("style", tab.getContentStyle(), "style"); } writer.writeAttribute("class", classes, "class"); tab.encodeChildren(context); writer.endElement("div"); } /** * Generates the HTML of the tabs. * * @param context * the current FacesContext * @param writer * the response writer * @param children * the tabs * @throws IOException * only thrown if something's wrong with the response writer */ private static void encodeTabs(FacesContext context, ResponseWriter writer, List<UIComponent> children, int currentlyActiveIndex, String hiddenInputFieldID, boolean disabled) throws IOException { if (null != children) { for (int index = 0; index < children.size(); index++) { encodeTab(context, writer, children.get(index), index == currentlyActiveIndex, hiddenInputFieldID, index, disabled); } } } /** * Generate an individual tab. Basically, that's <li role="presentation" * class="active"><a href="#{clientID}" role="tab" * data-toggle="tab"< {{title}} >/a> * * @param context * the current FacesContext * @param writer * the response writer * @param tab * the tab to be rendered. * @param isActive * is the current tab active? * @throws IOException * only thrown if something's wrong with the response writer */ private static void encodeTab(FacesContext context, ResponseWriter writer, UIComponent child, boolean isActive, String hiddenInputFieldID, int tabIndex, boolean disabled) throws IOException { Tab tab = (Tab) child; if (!tab.isRendered()) return; writer.startElement("li", tab); if (tab.getDir() != null) writer.writeAttribute("dir", tab.getDir(), "dir"); writer.writeAttribute("id", tab.getClientId(), "id"); writer.writeAttribute("role", "presentation", "role"); Tooltip.generateTooltip(context, tab, writer); String classes = isActive ? "active" : ""; if (tab.getStyleClass() != null) { classes += " "; classes += tab.getStyleClass(); } if (tab.isDisabled() || disabled) { classes += " disabled"; } if (classes.length() > 0) { writer.writeAttribute("class", classes.trim(), "class"); } if (tab.isDisabled() || disabled) { writer.writeAttribute("onclick", "event.preventDefault(); return false;", null); } if (tab.getStyle() != null) { writer.writeAttribute("style", tab.getStyle(), "style"); } encodeTabAnchorTag(context, writer, tab, hiddenInputFieldID, tabIndex, disabled); writer.endElement("li"); Tooltip.activateTooltips(context, tab); } /** * Generate the clickable entity of the tab. * * @param writer * the response writer * @param tab * the tab to be rendered. * @throws IOException * only thrown if something's wrong with the response writer */ private static void encodeTabAnchorTag(FacesContext context, ResponseWriter writer, Tab tab, String hiddenInputFieldID, int tabindex, boolean disabled) throws IOException { writer.startElement("a", tab); writer.writeAttribute("id", tab.getClientId().replace(":", "_") + "_tab", "id"); writer.writeAttribute("role", "tab", "role"); if (tab.isDisabled() || disabled) { writer.writeAttribute("onclick", "event.preventDefault(); return false;", null); } else { writer.writeAttribute("data-toggle", "tab", "data-toggle"); writer.writeAttribute("href", "#" + tab.getClientId().replace(":", "_") + "_pane", "href"); String onclick = "document.getElementById('" + hiddenInputFieldID + "').value='" + String.valueOf(tabindex) + "';"; AJAXRenderer.generateBootsFacesAJAXAndJavaScript(context, tab, writer, "click", onclick, false, true); } R.encodeHTML4DHTMLAttrs(writer, tab.getAttributes(), H.TAB); UIComponent iconFacet = tab.getFacet("anchor"); if (null != iconFacet) { iconFacet.encodeAll(FacesContext.getCurrentInstance()); if (null != tab.getTitle()) { writer.writeText(" " + tab.getTitle(), null); } } else { writer.writeText(tab.getTitle(), null); } writer.endElement("a"); } }