/** * 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.selectMultiMenu; import java.io.IOException; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Logger; import javax.faces.FacesException; import javax.faces.component.UIComponent; import javax.faces.component.UIForm; import javax.faces.component.UISelectItem; import javax.faces.component.UISelectItems; import javax.faces.component.html.HtmlOutputText; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.convert.Converter; 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.form.Form; 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; import net.bootsfaces.utils.FacesMessages; /** This class generates the HTML code of <b:selectMultiMenu />. */ @FacesRenderer(componentFamily = "net.bootsfaces.component", rendererType = "net.bootsfaces.component.selectMultiMenu.SelectMultiMenu") public class SelectMultiMenuRenderer extends CoreInputRenderer { // http://davidstutz.github.io/bootstrap-multiselect/ 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) { SelectMultiMenu menu = (SelectMultiMenu) component; if (menu.isDisabled() || menu.isReadonly()) { return; } List<String> submittedValues = new ArrayList<String>(); String clientId = menu.getClientId().replace(":", "-"); Map<String, String> map = context.getExternalContext().getRequestParameterMap(); for (String key : map.keySet()) { if (key.startsWith(clientId + ":")) { submittedValues.add(map.get(key)); } } List<SelectItemAndComponent> items = SelectItemUtils.collectOptions(context, menu); if (!submittedValues.isEmpty()) { // check for manipulated input String result = null; for (String submittedOptionValue : submittedValues) { boolean found = false; for (int index = 0; index < items.size(); index++) { Object currentOption = items.get(index).getSelectItem(); String currentOptionValueAsString; Object currentOptionValue; if (currentOption instanceof SelectItem) { currentOptionValue = ((SelectItem) currentOption).getValue(); } else { currentOptionValue = ((UISelectItem) currentOption).getItemValue(); } if (currentOptionValue instanceof String) { currentOptionValueAsString = (String) currentOptionValue; } else currentOptionValueAsString = String.valueOf(index); if ("".equals(currentOptionValueAsString) && submittedOptionValue.equalsIgnoreCase("on")) { // this is a special case which occurs when the value is // set to "". // In this case, the browser sends "on" instead of the // empty string. submittedOptionValue = ""; } if (submittedOptionValue.equals(currentOptionValueAsString)) { if (null == result) result = currentOptionValueAsString; else result += "," + currentOptionValue; found = true; break; } } if (!found) { FacesMessages.error(clientId, "Invalid data", "Couldn't set the value of the b:selectMultiMenu because an option that wasn't defined by the JSF source code was passed."); menu.setSubmittedValue(result); menu.setValid(false); return; } } menu.setSubmittedValue(result); menu.setValid(true); new AJAXRenderer().decode(context, component, clientId); return; } menu.setSubmittedValue(""); menu.setValid(true); new AJAXRenderer().decode(context, component, clientId); } /** Generates the HTML code for this component. */ @Override public void encodeBegin(FacesContext context, UIComponent component) throws IOException { SelectMultiMenu menu = (SelectMultiMenu) component; if (!menu.isRendered()) { return; } ResponseWriter rw = context.getResponseWriter(); String clientId = menu.getClientId(context).replace(":", "-"); String span=null; boolean clientIdHasBeenRendered=false; if (!isHorizontalForm(component)) { span = startColSpanDiv(rw, menu, menu.getClientId(context), clientIdHasBeenRendered); if (null != span) { Tooltip.generateTooltip(context, menu, rw); clientIdHasBeenRendered=true; } } rw.startElement("div", menu); writeAttribute(rw, "dir", menu.getDir(), "dir"); if (!clientIdHasBeenRendered) { writeAttribute(rw, "id", menu.getClientId(context)); Tooltip.generateTooltip(context, menu, rw); clientIdHasBeenRendered=true; } rw.writeAttribute("class", getWithFeedback(getInputMode(menu.isInline()), component), "class"); addLabel(rw, clientId + "Inner", menu); if (isHorizontalForm(component)) { span = startColSpanDiv(rw, menu, menu.getClientId(context), clientIdHasBeenRendered); } 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 + "Inner", clientId, menu); addAppendingAddOnToInputGroup(context, rw, appendingAddOnFacet, (appendingAddOnFacet != null), menu); closeInputGroupForAddOn(rw, hasAddon); rw.endElement("div"); // form-group closeColSpanDiv(rw, span); Tooltip.activateTooltips(context, menu); String options = ""; int maxHeight = menu.getMaxHeight(); if (maxHeight > 0) { options += "," + "maxHeight:" + String.valueOf(maxHeight); } String nonSelectedText = menu.getNonSelectedText(); if (nonSelectedText != null) { options += "," + "nonSelectedText:'" + nonSelectedText + "'"; } String nSelectedText = menu.getNSelectedText(); nSelectedText = (String) menu.getAttributes().get("nSelectedText"); // workaround // - // the // regular // getter // always // yields // null if (nSelectedText != null) { options += "," + "nSelectedText:'" + nSelectedText + "'"; } String allSelectedText = menu.getAllSelectedText(); if (allSelectedText != null) { options += "," + "allSelectedText:'" + allSelectedText + "'"; } int numberDisplayed = menu.getNumberDisplayed(); if (numberDisplayed > 0) { options += "," + "numberDisplayed:" + String.valueOf(numberDisplayed); } if (menu.isIncludeSelectAllOption()) { options += "," + "includeSelectAllOption:" + "true"; } String selectAllText = menu.getSelectAllText(); if (selectAllText != null) { options += "," + "selectAllText:'" + selectAllText + "'"; } if (menu.isEnableFiltering()) { options += "," + "enableFiltering:true"; } String filterPlaceholder = menu.getFilterPlaceholder(); if (filterPlaceholder != null) { options += "," + "filterPlaceholder:'" + filterPlaceholder + "'"; } boolean enableCaseInsensitiveFiltering = menu.isEnableCaseInsensitiveFiltering(); if (enableCaseInsensitiveFiltering) { options += "," + "enableCaseInsensitiveFiltering:" + "true"; } boolean disableIfEmpty = menu.isDisableIfEmpty(); if (disableIfEmpty) { options += "," + "disableIfEmpty:" + "true"; } boolean dropRight = menu.isDropRight(); if (dropRight) { options += "," + "dropRight:" + "true"; } String buttonClass = menu.getButtonClass(); if (buttonClass != null) { options += "," + "buttonClass:'" + buttonClass + "'"; } String buttonContainer = menu.getButtonContainer(); if (buttonContainer != null) { options += "," + "buttonContainer:'" + buttonClass + "'"; if (menu.getStyleClass() != null) throw new FacesException( "b:selectMultiMenu can't process the styleClass attribute if the buttonContainer attribute is set."); } else { String styleClass = menu.getStyleClass(); String blockStyle = "display:block"; if (menu.isInline()) { blockStyle = "display:inline-block"; } else { UIForm currentForm = AJAXRenderer.getSurroundingForm((UIComponent)component, true); if (null != currentForm && currentForm instanceof Form) { if (((Form) currentForm).isInline()) { blockStyle = "display:inline-block"; } } } if (menu.getStyle()!=null) { blockStyle += ";" + menu.getStyle(); } if (styleClass != null) { options += "," + "buttonContainer:'<div class=\"" + styleClass + "\" style=\"" + blockStyle + "\"/>'"; } else { options += "," + "buttonContainer:'<div class=\"btn-group\" style=\"" + blockStyle + "\" />'"; } } int buttonWidth = menu.getButtonWidth(); if (buttonWidth > 0) { options += "," + "buttonWidth:'" + buttonWidth + "px'"; } for (String event: menu.getJQueryEvents().keySet()) { String optionalParameterList="event"; if (menu.getJQueryEventParameterLists()!=null) { if (menu.getJQueryEventParameterLists().get(event) != null) { optionalParameterList = menu.getJQueryEventParameterLists().get(event); } } StringBuffer code = new StringBuffer(); AJAXRenderer.generateAJAXCallForASingleEvent(context, menu, null, code, null, null, true, event, null, optionalParameterList); if (code.length()>0) { String result = menu.getJQueryEvents().get(event); result += ":"; String realCode = code.toString().replace(".callAjax(this", ".callAjax(document.getElementById('" + clientId + "Inner')" ); if (!realCode.startsWith("function")) { result += "function(" + optionalParameterList + ")"; result += "{"; result += realCode; result += "}"; } else { // backward compatibility to BootsFaces 1.0.0 and earlier result += realCode; } options += "," + result; } } if (options.length() > 0) { options = "{" + options.substring(1, options.length()) + "}"; } String js = "$(document).ready(function() {$('#" + clientId + "Inner').multiselect(" + options + ");});\n"; context.getResponseWriter().write("<script type='text/javascript'>\r\n" + js + "\r\n</script>"); } /** * 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, SelectMultiMenu menu) throws IOException { if (hasAppendingAddOn) { if (appendingAddOnFacet.getClass().getName().endsWith("Button") || (appendingAddOnFacet.getChildCount() > 0 && appendingAddOnFacet.getChildren().get(0).getClass().getName().endsWith("Button"))) { rw.startElement("div", menu); rw.writeAttribute("class", "input-group-btn", "class"); appendingAddOnFacet.encodeAll(context); rw.endElement("div"); } else { rw.startElement("span", menu); rw.writeAttribute("class", "input-group-addon", "class"); appendingAddOnFacet.encodeAll(context); rw.endElement("span"); } } } /** * 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, SelectMultiMenu menu) 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, clientId, menu.getLabelStyleClass(), Responsive.getResponsiveLabelClass(menu), "control-label"); //generateErrorAndRequiredClassForLabels(menu, rw, clientId, "control-label " + menu.getLabelStyleClass()); 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, SelectMultiMenu menu) throws IOException { if (hasPrependingAddOn) { if (prependingAddOnFacet.getClass().getName().endsWith("Button") || (prependingAddOnFacet.getChildCount() > 0 && prependingAddOnFacet.getChildren().get(0).getClass().getName().endsWith("Button"))) { rw.startElement("div", menu); rw.writeAttribute("class", "input-group-btn", "class"); prependingAddOnFacet.encodeAll(context); rw.endElement("div"); } else if (prependingAddOnFacet instanceof HtmlOutputText) { HtmlOutputText out = (HtmlOutputText) prependingAddOnFacet; String sc = out.getStyleClass(); if (sc == null) sc = "input-group-addon"; else sc = sc + " " + "input-group-addon"; out.setStyleClass(sc); prependingAddOnFacet.encodeAll(context); } else { rw.startElement("span", menu); rw.writeAttribute("class", "input-group-addon", "class"); prependingAddOnFacet.encodeAll(context); rw.endElement("span"); } } } /** * 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"); } } /** * Algorithm works as follows; - If it's an input component, submitted value * is checked first since it'd be the value to be used in case validation * errors terminates jsf lifecycle - Finally the value of the component is * retrieved from backing bean and if there's a converter, converted value * is returned * * @param context * FacesContext instance * @return End text */ public Object getValue2Render(FacesContext context, SelectMultiMenu menu) { Object sv = menu.getSubmittedValue(); if (sv != null) { return sv; } Object val = menu.getValue(); if (val != null) { Converter converter = menu.getConverter(); if (converter != null) return converter.getAsString(context, menu, val); else return val; } else { // component is a value holder but has no value return null; } } /** Renders the select tag. */ protected void renderSelectTag(FacesContext context, ResponseWriter rw, String clientId, String name, SelectMultiMenu menu) throws IOException { renderSelectTag(rw, menu); renderSelectTagAttributes(rw, clientId, name, menu); AJAXRenderer.generateBootsFacesAJAXAndJavaScript(FacesContext.getCurrentInstance(), menu, rw, false); Object selectedOption = getValue2Render(context, menu); String[] optionList; if (selectedOption == null) { optionList = new String[0]; } else if (!(selectedOption instanceof String)) { throw new FacesException("SelectMultiMenu only works with Strings!"); } else { optionList = ((String) selectedOption).split(","); } renderOptions(context, rw, optionList, menu); renderInputTagEnd(rw); } /** * Copied from the InputRenderer class of PrimeFaces 5.1. * * @param context * @param uiSelectItems * @param value * @param label * @return */ protected SelectItem createSelectItem(FacesContext context, UISelectItems uiSelectItems, Object value, Object label) { String var = (String) uiSelectItems.getAttributes().get("var"); Map<String, Object> attrs = uiSelectItems.getAttributes(); Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); if (var != null) { requestMap.put(var, value); } Object itemLabelValue = attrs.get("itemLabel"); Object itemValue = attrs.get("itemValue"); String description = (String) attrs.get("itemDescription"); Object itemDisabled = attrs.get("itemDisabled"); Object itemEscaped = attrs.get("itemLabelEscaped"); Object noSelection = attrs.get("noSelectionOption"); if (itemValue == null) { itemValue = value; } if (itemLabelValue == null) { itemLabelValue = label; } String itemLabel = itemLabelValue == null ? String.valueOf(value) : String.valueOf(itemLabelValue); boolean disabled = itemDisabled == null ? false : Boolean.valueOf(itemDisabled.toString()); boolean escaped = itemEscaped == null ? false : Boolean.valueOf(itemEscaped.toString()); boolean noSelectionOption = noSelection == null ? false : Boolean.valueOf(noSelection.toString()); if (var != null) { requestMap.remove(var); } return new SelectItem(itemValue, itemLabel, description, disabled, escaped, noSelectionOption); } /** * Parts of this class are an adapted version of * InputRenderer#getSelectItems() of PrimeFaces 5.1. * * @param rw * @param selectedOption * @throws IOException */ protected void renderOptions(FacesContext context, ResponseWriter rw, String[] selectedOption, SelectMultiMenu menu) throws IOException { List<SelectItemAndComponent> items = SelectItemUtils.collectOptions(context, menu); for (int index = 0; index < items.size(); index++) { Object option = items.get(index).getSelectItem(); if (option instanceof SelectItem) { renderOption(rw, (SelectItem) option, selectedOption, index); } else { renderOption(rw, (UISelectItem) option, selectedOption, index); } } } /** * 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 * @param selectedOption * the currently selected option * @throws IOException * thrown if something's wrong with the response writer */ protected void renderOption(ResponseWriter rw, SelectItem selectItem, String[] selectedOption, int index) throws IOException { String itemLabel = selectItem.getLabel(); final String description = selectItem.getDescription(); final Object itemValue = selectItem.getValue(); renderOption(rw, selectedOption, index, itemLabel, description, itemValue); } /** * 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 * @param selectedOption * the currently selected option * @throws IOException * thrown if something's wrong with the response writer */ protected void renderOption(ResponseWriter rw, UISelectItem selectItem, String[] selectedOption, int index) throws IOException { String itemLabel = selectItem.getItemLabel(); final String itemDescription = selectItem.getItemDescription(); final Object itemValue = selectItem.getItemValue(); boolean isItemLabelBlank = itemLabel == null || itemLabel.trim().length() == 0; itemLabel = isItemLabelBlank ? " " : itemLabel; renderOption(rw, selectedOption, index, itemLabel, itemDescription, itemValue); } private void renderOption(ResponseWriter rw, String[] selectedOption, int index, String itemLabel, final String description, final Object itemValue) throws IOException { boolean isItemLabelBlank = itemLabel == null || itemLabel.trim().length() == 0; itemLabel = isItemLabelBlank ? " " : itemLabel; rw.startElement("option", null); 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 (isInList(value, selectedOption)) { rw.writeAttribute("selected", "true", "selected"); } } else if (isInList(itemLabel, selectedOption)) { rw.writeAttribute("selected", "true", "selected"); } rw.write(itemLabel); rw.endElement("option"); } private boolean isInList(String value, String[] options) { if (null == options) return false; for (String s : options) { if (s == null) { if (value == null) return true; } else if (s.equals(value)) return true; } return false; } /** * 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, SelectMultiMenu menu) throws IOException { 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, String name, SelectMultiMenu menu) throws IOException { rw.writeAttribute("id", clientId, null); rw.writeAttribute("name", name, null); StringBuilder sb; String s; sb = new StringBuilder(20); // optimize int sb.append("form-control"); sb.append(" select-multi-menu"); 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, clientId)); 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); } if (!menu.isRadiobuttons()) { rw.writeAttribute("multiple", "multiple", null); } // 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, SelectMultiMenu menu, String clientId, boolean clientIdHasBeenRendered) 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"); if (!clientIdHasBeenRendered) { 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 rw * the response writer * @param hasPrependingAddOn * @param hasAppendingAddOn * @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, SelectMultiMenu menu) throws IOException { final boolean hasAddon = hasAppendingAddOn || hasPrependingAddOn; if (hasAddon) { rw.startElement("div", menu); rw.writeAttribute("class", "input-group", "class"); } return hasAddon; } }