/** * 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.selectOneMenu; import java.io.IOException; import java.util.List; import java.util.logging.Logger; import javax.el.ELException; import javax.el.ExpressionFactory; import javax.el.ValueExpression; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.convert.Converter; import javax.faces.convert.ConverterException; import javax.faces.model.SelectItem; import javax.faces.render.FacesRenderer; import net.bootsfaces.component.SelectItemAndComponent; import net.bootsfaces.component.SelectItemUtils; import net.bootsfaces.component.ajax.AJAXRenderer; import net.bootsfaces.component.inputText.InputTextRenderer; import net.bootsfaces.render.CoreInputRenderer; import net.bootsfaces.render.H; import net.bootsfaces.render.R; import net.bootsfaces.render.Responsive; import net.bootsfaces.render.Tooltip; /** This class generates the HTML code of <b:SelectOneMenu />. */ @FacesRenderer(componentFamily = "net.bootsfaces.component", rendererType = "net.bootsfaces.component.selectOneMenu.SelectOneMenu") public class SelectOneMenuRenderer extends CoreInputRenderer { private static final Logger LOGGER = Logger.getLogger(InputTextRenderer.class.getName()); /** Receives the value from the client and sends it to the JSF bean. */ @Override public void decode(FacesContext context, UIComponent component) { SelectOneMenu menu = (SelectOneMenu) component; if (menu.isDisabled() || menu.isReadonly()) { return; } String outerClientId = menu.getClientId(context); String clientId = outerClientId+"Inner"; String submittedOptionValue = (String) context.getExternalContext().getRequestParameterMap().get(clientId); List<SelectItemAndComponent> items = SelectItemUtils.collectOptions(context, menu); if (null != submittedOptionValue) { for (int index = 0; index < items.size(); index++) { Object currentOption = items.get(index).getSelectItem(); String currentOptionValueAsString; Object currentOptionValue = null; if (currentOption instanceof SelectItem) { if (!((SelectItem) currentOption).isDisabled()) { currentOptionValue = ((SelectItem) currentOption).getValue(); if (null == currentOptionValue) // use the label as // fall-back currentOptionValue = ((SelectItem) currentOption).getLabel(); } } if (currentOptionValue instanceof String) { currentOptionValueAsString = (String) currentOptionValue; } else currentOptionValueAsString = String.valueOf(index); if (submittedOptionValue.equals(currentOptionValueAsString)) { menu.setSubmittedValue(currentOptionValue); menu.setValid(true); menu.validateValue(context, submittedOptionValue); new AJAXRenderer().decode(context, component, clientId); return; } } menu.validateValue(context, null); menu.setSubmittedValue(null); menu.setValid(false); return; } menu.setValid(true); menu.validateValue(context, submittedOptionValue); menu.setSubmittedValue(submittedOptionValue); new AJAXRenderer().decode(context, component, clientId); } /** Generates the HTML code for this component. */ @Override public void encodeBegin(FacesContext context, UIComponent component) throws IOException { SelectOneMenu menu = (SelectOneMenu) component; if (!menu.isRendered()) { return; } ResponseWriter rw = context.getResponseWriter(); String outerClientId = menu.getClientId(context); boolean clientIdHasBeenRendered=false; String clientId = outerClientId+"Inner"; String span=null; if (!isHorizontalForm(component)) { span = startColSpanDiv(rw, menu, outerClientId); if (null != span) { Tooltip.generateTooltip(context, menu, rw); clientIdHasBeenRendered=true; } } rw.startElement("div", menu); rw.writeAttribute("class", getWithFeedback(getInputMode(menu.isInline()), component), "class"); if (!clientIdHasBeenRendered) { rw.writeAttribute("id", outerClientId, "id"); Tooltip.generateTooltip(context, menu, rw); } writeAttribute(rw, "dir", menu.getDir(), "dir"); addLabel(rw, clientId, menu, outerClientId); if (isHorizontalForm(component)) { span=startColSpanDiv(rw, menu, null); } UIComponent prependingAddOnFacet = menu.getFacet("prepend"); UIComponent appendingAddOnFacet = menu.getFacet("append"); final boolean hasAddon = startInputGroupForAddOn(rw, (prependingAddOnFacet != null), (appendingAddOnFacet != null), menu); addPrependingAddOnToInputGroup(context, rw, prependingAddOnFacet, (prependingAddOnFacet != null), menu); renderSelectTag(context, rw, clientId, menu, outerClientId); addAppendingAddOnToInputGroup(context, rw, appendingAddOnFacet, (appendingAddOnFacet != null), menu); closeInputGroupForAddOn(rw, hasAddon); rw.endElement("div"); // form-group closeColSpanDiv(rw, span); Tooltip.activateTooltips(context, menu); } /** * Renders components added seamlessly behind the input field. * * @param context * the FacesContext * @param rw * the response writer * @param appendingAddOnFacet * optional facet behind the field. Can be null. * @param hasAppendingAddOn * optional facet in front of the field. Can be null. * @throws IOException * may be thrown by the response writer */ protected void addAppendingAddOnToInputGroup(FacesContext context, ResponseWriter rw, UIComponent appendingAddOnFacet, boolean hasAppendingAddOn, SelectOneMenu menu) throws IOException { if (hasAppendingAddOn) { R.decorateFacetComponent(menu, appendingAddOnFacet, context, rw); } } /** * Renders the optional label. This method is protected in order to allow * third-party frameworks to derive from it. * * @param rw * the response writer * @param clientId * the id used by the label to refernce the input field * @throws IOException * may be thrown by the response writer */ protected void addLabel(ResponseWriter rw, String clientId, SelectOneMenu menu, String outerClientId) throws IOException { String label = menu.getLabel(); { if (!menu.isRenderLabel()) { label = null; } } if (label != null) { rw.startElement("label", menu); rw.writeAttribute("for", clientId, "for"); generateErrorAndRequiredClass(menu, rw, outerClientId, menu.getLabelStyleClass(), Responsive.getResponsiveLabelClass(menu), "control-label"); writeAttribute(rw, "style", menu.getLabelStyle()); rw.writeText(label, null); rw.endElement("label"); } } /** * Renders components added seamlessly in front of the input field. * * @param context * the FacesContext * @param rw * the response writer * @param prependingAddOnFacet * @param hasPrependingAddOn * @throws IOException * may be thrown by the response writer */ protected void addPrependingAddOnToInputGroup(FacesContext context, ResponseWriter rw, UIComponent prependingAddOnFacet, boolean hasPrependingAddOn, SelectOneMenu menu) throws IOException { if (hasPrependingAddOn) { R.decorateFacetComponent(menu, prependingAddOnFacet, context, rw); } } /** * Terminate the column span div (if there's one). This method is protected * in order to allow third-party frameworks to derive from it. * * @param rw * the response writer * @param span * the width of the components (in BS columns). * @throws IOException * may be thrown by the response writer */ protected void closeColSpanDiv(ResponseWriter rw, String span) throws IOException { if (span != null && span.trim().length()>0) { rw.endElement("div"); } } /** * Terminates the input field group (if there's one). This method is * protected in order to allow third-party frameworks to derive from it. * * @param rw * the response writer * @param hasAddon * true if there is an add-on in front of or behind the input * field * @throws IOException * may be thrown by the response writer */ protected void closeInputGroupForAddOn(ResponseWriter rw, final boolean hasAddon) throws IOException { if (hasAddon) { rw.endElement("div"); } } /** Renders the select tag. */ protected void renderSelectTag(FacesContext context, ResponseWriter rw, String clientId, SelectOneMenu menu, String outerClientId) throws IOException { renderSelectTag(rw, menu); renderSelectTagAttributes(rw, clientId, menu, outerClientId); renderOptions(context, rw, menu); renderInputTagEnd(rw); } /** * Parts of this class are an adapted version of * InputRenderer#getSelectItems() of PrimeFaces 5.1. * * @param rw * @throws IOException */ protected void renderOptions(FacesContext context, ResponseWriter rw, SelectOneMenu menu) throws IOException { List<SelectItemAndComponent> items = SelectItemUtils.collectOptions(context, menu); for (int index = 0; index < items.size(); index++) { SelectItemAndComponent option = items.get(index); renderOption(context,menu, rw, (option.getSelectItem()), index, option.getComponent()); } } /** * Renders a single <option> tag. For some reason, * <code>SelectItem</code> and <code>UISelectItem</code> don't share a * common interface, so this method is repeated twice. * * @param rw * The response writer * @param selectItem * The current SelectItem * @throws IOException * thrown if something's wrong with the response writer */ protected void renderOption(FacesContext context, SelectOneMenu menu, ResponseWriter rw, SelectItem selectItem, int index, UIComponent itemComponent) throws IOException { String itemLabel = selectItem.getLabel(); final String description = selectItem.getDescription(); final Object itemValue = selectItem.getValue(); renderOption(context, menu, rw, index, itemLabel, description, itemValue, selectItem.isDisabled(), selectItem.isEscape(), itemComponent); } private Converter findImplicitConverter(FacesContext context, UIComponent component) { ValueExpression ve = component.getValueExpression("value"); if (ve != null) { Class<?> valueType = ve.getType(context.getELContext()); if (valueType != null) return context.getApplication().createConverter(valueType); } return null; } private String getOptionAsString(FacesContext context, SelectOneMenu menu, Object value, Converter converter) throws ConverterException { if (converter == null) { if (value == null) { return ""; } else if (value instanceof String) { return (String) value; } else { Converter implicitConverter = findImplicitConverter(context, menu); return implicitConverter == null ? value.toString() : implicitConverter.getAsString(context, menu, value); } } else { return converter.getAsString(context, menu, value); } } private Object coerceToModelType(FacesContext ctx, Object value, Class<? extends Object> itemValueType) { Object newValue; try { ExpressionFactory ef = ctx.getApplication().getExpressionFactory(); newValue = ef.coerceToType(value, itemValueType); } catch (ELException ele) { newValue = value; } catch (IllegalArgumentException iae) { newValue = value; } return newValue; } private boolean isSelected(FacesContext context, SelectOneMenu menu, Object value, Object itemValue, Converter converter) { if (itemValue == null && value == null) { return true; } if (value != null) { Object compareValue; if (converter == null) { compareValue = coerceToModelType(context, itemValue, value.getClass()); } else { compareValue = itemValue; if (compareValue instanceof String && !(value instanceof String)) { compareValue = converter.getAsObject(context, menu, (String) compareValue); } } if (value.equals(compareValue)) { return true; } } return false; } private void renderOption(FacesContext context, SelectOneMenu menu, ResponseWriter rw, int index, String itemLabel, final String description, final Object itemValue, boolean isDisabled, boolean isEscape, UIComponent itemComponent) throws IOException { Object submittedValue = menu.getSubmittedValue(); Object selectedOption; Object optionValue; Converter converter = menu.getConverter(); String itemValueAsString = getOptionAsString(context, menu, itemValue, converter); if (submittedValue != null) { selectedOption = submittedValue; optionValue = itemValueAsString; } else { selectedOption = menu.getValue(); optionValue = itemValue; } boolean isItemLabelBlank = itemLabel == null || itemLabel.trim().isEmpty(); itemLabel = isItemLabelBlank ? " " : itemLabel; rw.startElement("option", itemComponent); rw.writeAttribute("data-label", itemLabel, null); if (description != null) { rw.writeAttribute("title", description, null); } if (itemValue != null) { String value; if (itemValue instanceof String) { value = (String) itemValue; } else value = String.valueOf(index); rw.writeAttribute("value", value, "value"); if (isSelected(context, menu, selectedOption, optionValue, converter)) { rw.writeAttribute("selected", "true", "selected"); } } else if (itemLabel.equals(selectedOption)) { rw.writeAttribute("selected", "true", "selected"); } if (isDisabled) rw.writeAttribute("disabled", "disabled", "disabled"); if (isEscape && !isItemLabelBlank) rw.writeText(itemLabel, null); else rw.write(itemLabel); rw.endElement("option"); } /** * Renders the start of the input tag. This method is protected in order to * allow third-party frameworks to derive from it. * * @param rw * the response writer * @throws IOException * may be thrown by the response writer */ protected void renderSelectTag(ResponseWriter rw, SelectOneMenu menu) throws IOException { rw.write("\n"); rw.startElement("select", menu); } /** * Renders the attributes of the input tag. This method is protected in * order to allow third-party frameworks to derive from it. * * @param rw * the response writer * @param clientId * the client id (used both as id and name) * @throws IOException * may be thrown by the response writer */ protected void renderSelectTagAttributes(ResponseWriter rw, String clientId, SelectOneMenu menu, String outerClientId) throws IOException { rw.writeAttribute("id", clientId, null); //Tooltip.generateTooltip(FacesContext.getCurrentInstance(), menu, rw); rw.writeAttribute("name", clientId, null); StringBuilder sb; String s; sb = new StringBuilder(20); // optimize int sb.append("form-control"); String fsize = menu.getFieldSize(); if (fsize != null) { sb.append(" input-").append(fsize); } String cssClass = menu.getStyleClass(); if (cssClass != null) { sb.append(" ").append(cssClass); } sb.append(" ").append(getErrorAndRequiredClass(menu, outerClientId)); s = sb.toString().trim(); if (s != null && s.length() > 0) { rw.writeAttribute("class", s, "class"); } if (menu.isDisabled()) { rw.writeAttribute("disabled", "disabled", null); } if (menu.isReadonly()) { rw.writeAttribute("readonly", "readonly", null); } AJAXRenderer.generateBootsFacesAJAXAndJavaScript(FacesContext.getCurrentInstance(), menu, rw, false); // Encode attributes (HTML 4 pass-through + DHTML) R.encodeHTML4DHTMLAttrs(rw, menu.getAttributes(), H.SELECT_ONE_MENU); } /** * Closes the input tag. This method is protected in order to allow * third-party frameworks to derive from it. * * @param rw * the response writer * @throws IOException * may be thrown by the response writer */ protected void renderInputTagEnd(ResponseWriter rw) throws IOException { rw.endElement("select"); } /** * Start the column span div (if there's one). This method is protected in * order to allow third-party frameworks to derive from it. * * @param rw * the response writer * @throws IOException * may be thrown by the response writer */ protected String startColSpanDiv(ResponseWriter rw, SelectOneMenu menu, String clientId) throws IOException { String clazz = Responsive.getResponsiveStyleClass(menu, false); if (clazz!= null && clazz.trim().length()>0) { clazz=clazz.trim(); rw.startElement("div", menu); rw.writeAttribute("class", clazz, "class"); rw.writeAttribute("id", clientId, "id"); return clazz; } return null; } /** * Starts the input field group (if needed to display a component seamlessly * in front of or behind the input field). This method is protected in order * to allow third-party frameworks to derive from it. * * @param hasAppendingAddOn * * @param rw * the response writer * @param hasPrependingAddOn * @return true if there is an add-on in front of or behind the input field * @throws IOException * may be thrown by the response writer */ protected boolean startInputGroupForAddOn(ResponseWriter rw, boolean hasPrependingAddOn, boolean hasAppendingAddOn, SelectOneMenu menu) throws IOException { final boolean hasAddon = hasAppendingAddOn || hasPrependingAddOn; if (hasAddon) { rw.startElement("div", menu); rw.writeAttribute("class", "input-group", "class"); } return hasAddon; } }