View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.component.tristatemanycheckbox;
23  
24  import java.io.IOException;
25  import java.util.*;
26  import java.util.Map.Entry;
27  
28  import javax.faces.FacesException;
29  import javax.faces.component.UIComponent;
30  import javax.faces.component.UIInput;
31  import javax.faces.component.UINamingContainer;
32  import javax.faces.component.UISelectMany;
33  import javax.faces.context.FacesContext;
34  import javax.faces.context.ResponseWriter;
35  import javax.faces.convert.Converter;
36  import javax.faces.model.SelectItem;
37  
38  import org.primefaces.component.selectmanycheckbox.SelectManyCheckbox;
39  import org.primefaces.extensions.util.Attrs;
40  import org.primefaces.renderkit.SelectManyRenderer;
41  import org.primefaces.util.Constants;
42  import org.primefaces.util.EscapeUtils;
43  import org.primefaces.util.HTML;
44  import org.primefaces.util.WidgetBuilder;
45  
46  /**
47   * TriStateManyCheckboxRenderer
48   *
49   * @author Mauricio Fenoglio / last modified by $Author$
50   * @version $Revision$
51   * @since 0.3
52   */
53  public class TriStateManyCheckboxRenderer extends SelectManyRenderer {
54  
55      @Override
56      public Object getConvertedValue(final FacesContext context, final UIComponent component, final Object submittedValue) {
57          // only convert the values of the maps
58          if (submittedValue instanceof Map) {
59              final Map<String, Object> mapSub = (Map<String, Object>) submittedValue;
60              final List<String> keyValues = new ArrayList<>(mapSub.keySet());
61              final Map<String, Object> mapSubConv = new LinkedHashMap<>();
62              final TriStateManyCheckbox checkbox = (TriStateManyCheckbox) component;
63              final Converter<?> converter = checkbox.getConverter();
64              if (converter != null) {
65                  for (final String keyVal : keyValues) {
66                      final Object mapVal = converter.getAsObject(context, checkbox, (String) mapSub.get(keyVal));
67                      mapSubConv.put(keyVal, mapVal);
68                  }
69  
70                  return mapSubConv;
71              }
72              else {
73                  return mapSub;
74              }
75          }
76          else {
77              throw new FacesException("Value of '" + component.getClientId() + "'must be a Map instance");
78          }
79      }
80  
81      @Override
82      public void decode(final FacesContext context, final UIComponent component) {
83          final TriStateManyCheckbox checkbox = (TriStateManyCheckbox) component;
84          if (!shouldDecode(checkbox)) {
85              return;
86          }
87  
88          decodeBehaviors(context, checkbox);
89  
90          final String submitParam = getSubmitParam(context, checkbox);
91          final Map<String, String[]> params = context.getExternalContext().getRequestParameterValuesMap();
92  
93          String[] valuesArray = null;
94          if (params.containsKey(submitParam)) {
95              valuesArray = params.get(submitParam);
96          }
97  
98          checkbox.setSubmittedValue(getSubmitedMap(context, checkbox, valuesArray));
99      }
100 
101     @Override
102     public void encodeEnd(final FacesContext context, final UIComponent component) throws IOException {
103         final TriStateManyCheckbox checkbox = (TriStateManyCheckbox) component;
104         encodeMarkup(context, checkbox);
105         encodeScript(context, checkbox);
106     }
107 
108     protected void encodeMarkup(final FacesContext context, final TriStateManyCheckbox checkbox) throws IOException {
109         final ResponseWriter writer = context.getResponseWriter();
110         final String clientId = checkbox.getClientId(context);
111         final String style = checkbox.getStyle();
112         final String styleClass = getStyleClassBuilder(context)
113                     .add(SelectManyCheckbox.STYLE_CLASS)
114                     .add(checkbox.getStyleClass())
115                     .build();
116 
117         writer.startElement("table", checkbox);
118         writer.writeAttribute("id", clientId, "id");
119         writer.writeAttribute(Attrs.CLASS, styleClass, "styleClass");
120         if (style != null) {
121             writer.writeAttribute(Attrs.STYLE, style, Attrs.STYLE);
122         }
123 
124         encodeSelectItems(context, checkbox);
125 
126         writer.endElement("table");
127     }
128 
129     protected void encodeSelectItems(final FacesContext context, final TriStateManyCheckbox checkbox) throws IOException {
130         final ResponseWriter writer = context.getResponseWriter();
131         final List<SelectItem> selectItems = getSelectItems(context, checkbox);
132         final Converter<Object> converter = checkbox.getConverter();
133         Map<String, Object> values = getValues(checkbox);
134         final Map<String, Object> submittedMap = getSubmittedFromComp(checkbox);
135         final String layout = checkbox.getLayout();
136         final boolean pageDirection = layout != null && "pageDirection".equals(layout);
137 
138         if (submittedMap != null) {
139             values = submittedMap;
140         }
141 
142         if (converter != null && submittedMap == null) {
143             for (final Entry<String, Object> entry : values.entrySet()) {
144                 final String keyValue = converter.getAsString(context, checkbox, entry.getValue());
145                 values.put(entry.getKey(), keyValue);
146             }
147         }
148 
149         int idx = -1;
150         for (final SelectItem selectItem : selectItems) {
151             idx++;
152             if (pageDirection) {
153                 writer.startElement("tr", null);
154             }
155 
156             encodeOption(context, checkbox, values, selectItem, idx);
157             if (pageDirection) {
158                 writer.endElement("tr");
159             }
160         }
161     }
162 
163     protected void encodeOption(final FacesContext context, final UIInput component, final Map<String, Object> values,
164                 final SelectItem option, final int idx) throws IOException {
165         final ResponseWriter writer = context.getResponseWriter();
166         final TriStateManyCheckbox checkbox = (TriStateManyCheckbox) component;
167         final String itemValueAsString = String.valueOf(option.getValue());
168         final String name = checkbox.getClientId(context);
169         final String id = name + UINamingContainer.getSeparatorChar(context) + idx;
170         final boolean disabled = option.isDisabled() || checkbox.isDisabled();
171 
172         final String itemValue = (String) option.getValue();
173 
174         final int valueInput = getValueForInput(component, itemValue, values);
175         if (option.isNoSelectionOption() && values != null && Constants.EMPTY_STRING.equals(itemValue)) {
176             return;
177         }
178 
179         writer.startElement("td", null);
180 
181         writer.startElement("div", null);
182         writer.writeAttribute(Attrs.CLASS, HTML.CHECKBOX_CLASS, null);
183 
184         encodeOptionInput(context, checkbox, id, name, disabled, itemValueAsString, valueInput);
185         encodeOptionOutput(context, checkbox, valueInput, disabled);
186 
187         writer.endElement("div");
188         writer.endElement("td");
189 
190         writer.startElement("td", null);
191         encodeOptionLabel(context, id, option, disabled);
192         writer.endElement("td");
193     }
194 
195     protected void encodeOptionInput(final FacesContext context, final TriStateManyCheckbox checkbox, final String id, final String name,
196                 final boolean disabled, final String value, final int valueInput) throws IOException {
197         final ResponseWriter writer = context.getResponseWriter();
198 
199         writer.startElement("div", null);
200         writer.writeAttribute(Attrs.CLASS, HTML.CHECKBOX_INPUT_WRAPPER_CLASS, null);
201 
202         writer.startElement("input", null);
203         writer.writeAttribute("id", id, null);
204         writer.writeAttribute("name", name, null);
205         writer.writeAttribute("type", "text", null);
206         writer.writeAttribute("value", valueInput, null);
207         writer.writeAttribute("itemValue", value, null);
208 
209         if (disabled) {
210             writer.writeAttribute("disabled", "disabled", null);
211         }
212 
213         if (checkbox.getOnchange() != null) {
214             writer.writeAttribute("onchange", checkbox.getOnchange(), null);
215         }
216 
217         writer.endElement("input");
218 
219         writer.endElement("div");
220     }
221 
222     protected void encodeOptionOutput(final FacesContext context, final TriStateManyCheckbox checkbox, final int valCheck,
223                 final boolean disabled) throws IOException {
224         final ResponseWriter writer = context.getResponseWriter();
225         final String styleClass = getStyleClassBuilder(context)
226                     .add(HTML.CHECKBOX_BOX_CLASS)
227                     .add(valCheck == 1 || valCheck == 2, "ui-state-active")
228                     .add(disabled, "ui-state-disabled")
229                     .build();
230 
231         // if stateIcon is defined use it insted of default icons.
232         final String stateOneIconClass = checkbox.getStateOneIcon() != null ? TriStateManyCheckbox.UI_ICON + checkbox.getStateOneIcon()
233                     : Constants.EMPTY_STRING;
234         final String stateTwoIconClass = checkbox.getStateTwoIcon() != null ? TriStateManyCheckbox.UI_ICON + checkbox.getStateTwoIcon()
235                     : TriStateManyCheckbox.UI_ICON + "ui-icon-check";
236         final String stataThreeIconClass = checkbox.getStateThreeIcon() != null ? TriStateManyCheckbox.UI_ICON + checkbox.getStateThreeIcon()
237                     : TriStateManyCheckbox.UI_ICON + "ui-icon-closethick";
238 
239         final String comma = "\",\"";
240         final String statesIconsClasses = "[\"" + stateOneIconClass + comma + stateTwoIconClass + comma + stataThreeIconClass + "\"]";
241 
242         final String statesTitles = "[\"" + EscapeUtils.forJavaScript(checkbox.getStateOneTitle()) + comma
243                     + EscapeUtils.forJavaScript(checkbox.getStateTwoTitle()) + comma + EscapeUtils.forJavaScript(checkbox.getStateThreeTitle()) + "\"]";
244 
245         String iconClass = "ui-chkbox-icon ui-icon ui-c";
246         String activeTitle = Constants.EMPTY_STRING;
247         if (valCheck == 0) {
248             iconClass = iconClass + " " + stateOneIconClass;
249             activeTitle = checkbox.getStateOneTitle();
250         }
251         else if (valCheck == 1) {
252             iconClass = iconClass + " " + stateTwoIconClass;
253             activeTitle = checkbox.getStateTwoTitle();
254         }
255         else if (valCheck == 2) {
256             iconClass = iconClass + " " + stataThreeIconClass;
257             activeTitle = checkbox.getStateThreeTitle();
258         }
259 
260         String dataTitles = Constants.EMPTY_STRING;
261         String titleAtt = Constants.EMPTY_STRING;
262 
263         if (!checkbox.getStateOneTitle().isEmpty()
264                     || !checkbox.getStateTwoTitle().isEmpty() || !checkbox.getStateThreeTitle().isEmpty()) {
265             // preparation with singe quotes for .data('titlestates')
266             dataTitles = "data-titlestates='" + statesTitles + "' ";
267             // active title Att
268             titleAtt = " title=\"" + EscapeUtils.forJavaScript(activeTitle) + "\" ";
269         }
270 
271         String tabIndexTag = " tabIndex=0 ";
272         if (checkbox.getTabindex() != null) {
273             tabIndexTag = "tabIndex=" + checkbox.getTabindex() + " ";
274         }
275 
276         // preparation with singe quotes for .data('iconstates')
277         writer.write("<div " + tabIndexTag + titleAtt + "class=\"" + styleClass + "\" data-iconstates='" + statesIconsClasses + "' "
278                     + dataTitles + ">"
279                     + "<span class=\"" + iconClass + "\"></span></div>");
280 
281     }
282 
283     protected void encodeScript(final FacesContext context, final TriStateManyCheckbox checkbox) throws IOException {
284         final WidgetBuilder wb = getWidgetBuilder(context);
285         wb.init("ExtTriStateManyCheckbox", checkbox);
286         encodeClientBehaviors(context, checkbox);
287         wb.finish();
288     }
289 
290     protected void encodeOptionLabel(final FacesContext context, final String containerClientId,
291                 final SelectItem option, final boolean disabled) throws IOException {
292         final ResponseWriter writer = context.getResponseWriter();
293 
294         writer.startElement(Attrs.LABEL, null);
295         writer.writeAttribute("for", containerClientId, null);
296         if (disabled) {
297             writer.writeAttribute(Attrs.CLASS, "ui-state-disabled", null);
298         }
299 
300         if (option.isEscape()) {
301             writer.writeText(option.getLabel(), null);
302         }
303         else {
304             writer.write(option.getLabel());
305         }
306 
307         writer.endElement(Attrs.LABEL);
308     }
309 
310     @Override
311     protected String getSubmitParam(final FacesContext context, final UISelectMany selectMany) {
312         return selectMany.getClientId(context);
313     }
314 
315     /*
316      * return the value for the triState based on the value of the selectItems on the iteration
317      */
318     protected int getValueForInput(final UIInput component, final String itemValue, final Map<String, Object> valueArray) {
319         try {
320             int retInt = 0;
321             if (itemValue == null || valueArray == null) {
322                 return retInt;
323             }
324 
325             if (valueArray.containsKey(itemValue)) {
326                 retInt = Integer.parseInt((String) valueArray.get(itemValue));
327 
328                 return retInt % 3;
329             }
330             else {
331                 return retInt;
332             }
333         }
334         catch (final NumberFormatException ex) {
335             throw new FacesException("State of '" + component.getClientId() + "' must be an integer representation");
336         }
337     }
338 
339     @Override
340     protected Map getValues(final UIComponent component) {
341         final UISelectMany selectMany = (UISelectMany) component;
342         final Object value = selectMany.getValue();
343 
344         if (value == null) {
345             return null;
346         }
347         else if (value instanceof Map) {
348             // it should be a Map instance for <ItemStringValue,Value>
349             return (Map) value;
350         }
351         else {
352             throw new FacesException("Value of '" + component.getClientId() + "'must be a Map instance");
353         }
354     }
355 
356     protected Map<String, Object> getSubmitedMap(final FacesContext context, final TriStateManyCheckbox checkbox, final String[] valuesArray) {
357         final List<SelectItem> selectItems = getSelectItems(context, checkbox);
358         final Map<String, Object> subValues = new LinkedHashMap<>();
359         if (valuesArray != null && valuesArray.length == selectItems.size()) {
360             int idx = -1;
361             for (final SelectItem item : selectItems) {
362                 idx++;
363 
364                 final String keyMap = (String) item.getValue();
365                 final Object valueMap = valuesArray[idx];
366                 subValues.put(keyMap, valueMap);
367             }
368 
369             return subValues;
370         }
371         else {
372             return null;
373         }
374     }
375 
376     protected Map<String, Object> getSubmittedFromComp(final UIComponent component) {
377         final TriStateManyCheckbox checkbox = (TriStateManyCheckbox) component;
378         final Map<String, Object> ret = (Map<String, Object>) checkbox.getSubmittedValue();
379         if (ret != null) {
380             final Map<String, Object> subValues = new LinkedHashMap<>();
381 
382             // need to reverse the order of element on the map to take the value as on decode.
383             final Set<String> keys = ret.keySet();
384             final String[] tempArray = keys.toArray(new String[0]);
385 
386             final int length = tempArray.length;
387             for (int i = length - 1; i >= 0; i--) {
388                 final String key = tempArray[i];
389                 final Object val = ret.get(key);
390                 subValues.put(key, val);
391             }
392 
393             return subValues;
394         }
395         else {
396             return null;
397         }
398     }
399 }