View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.custom.suggest;
20  
21  import java.io.IOException;
22  import java.util.Iterator;
23  import java.util.Map;
24  
25  import javax.faces.component.EditableValueHolder;
26  import javax.faces.component.UIComponent;
27  import javax.faces.component.UISelectItems;
28  import javax.faces.component.ValueHolder;
29  import javax.faces.component.html.HtmlInputText;
30  import javax.faces.context.FacesContext;
31  import javax.faces.context.ResponseWriter;
32  import javax.faces.render.Renderer;
33  
34  import org.apache.myfaces.component.html.ext.HtmlInputHidden;
35  import org.apache.myfaces.renderkit.html.util.AddResource;
36  import org.apache.myfaces.renderkit.html.util.AddResourceFactory;
37  import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
38  import org.apache.myfaces.shared_tomahawk.renderkit.JSFAttr;
39  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils;
40  
41  /**
42   * Basic HTML Renderer for the inputSuggest component.
43   *
44   * @JSFRenderer
45   *   renderKitId = "HTML_BASIC" 
46   *   family = "javax.faces.Input"
47   *   type = "org.apache.myfaces.InputSuggest"
48   * 
49   * @author Sean Schofield
50   * @author Matt Blum
51   * @version $Revision: $ $Date: $
52   */
53  public class InputSuggestRenderer
54      extends Renderer
55  {
56      private String NEW_TEXT_KEY = "-1";
57  
58      public boolean getRendersChildren()
59      {
60          // must return "true" so that the UISelectItem child has been added before encode() is called
61          return true;
62      }
63  
64      public void decode(FacesContext context, UIComponent component)
65      {
66  
67          if (isDisabledOrReadOnly(component))
68          {
69              return;
70          }
71  
72          Map params = context.getExternalContext().getRequestParameterMap();
73          String text = (String) params.get(getTextId(component, context));
74          String choice = (String) params.get(getChoiceId(component, context));
75          if (choice != null)
76          {
77              ( (EditableValueHolder) component).setSubmittedValue(choice);
78  
79              if (choice.equals(NEW_TEXT_KEY))
80              {
81                  Map choices = getChoices(component);
82                  choices.put(NEW_TEXT_KEY, text);
83              }
84          }
85      }
86  
87      public void encodeBegin(FacesContext context, UIComponent component) throws
88                                                                           IOException
89      {
90  
91          if (!component.isRendered())
92          {
93              return;
94          }
95  
96          // Get the current value
97          String value = (String) ( (EditableValueHolder) component).
98              getSubmittedValue();
99          if (value == null)
100         {
101             value = (String) ( (ValueHolder) component).getValue();
102         }
103 
104         String text = null;
105         Map choices = getChoices(component);
106         if (value != null && choices != null)
107         {
108             text = (String) choices.get(value);
109         }
110 
111         ResponseWriter out = context.getResponseWriter();
112         renderInputField(out, text, getTextId(component, context), component);
113 
114         // render hidden input field containing the user's choice
115         HtmlInputHidden hiddenChoice = new HtmlInputHidden();
116         hiddenChoice.setId(getChoiceId(component, context));
117         hiddenChoice.setValue(value);
118         hiddenChoice.getAttributes().put(JSFAttr.FORCE_ID_ATTR, Boolean.TRUE);
119         /** @todo use new encode recursive helper method once available */
120         hiddenChoice.encodeBegin(context);
121         hiddenChoice.encodeEnd(context);
122 
123         encodeSuggestions(context, out, choices,
124                           getSuggestionsId(component, context), component);
125         encodeStyles(component, context);
126         encodeJavascript(component, context);
127     }
128 
129     private void renderInputField(ResponseWriter out, String text,
130                                   String clientId, UIComponent component) throws
131                                                                           IOException
132     {
133 
134         out.startElement("input", component);
135         out.writeAttribute("name", clientId, null);
136         out.writeAttribute("id", clientId, null);
137 
138         if (text != null)
139         {
140             out.writeAttribute("value", text, "value");
141         }
142         else
143         {
144             out.writeAttribute("value", "", "value");
145         }
146 
147         component.getAttributes().put("autocomplete","off");
148 
149         HtmlRendererUtils.renderHTMLAttributes(out,
150                                                component, HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED);
151 
152         if((component instanceof HtmlInputText) && ((HtmlInputText) component).isDisabled())
153         {
154             out.writeAttribute(HTML.DISABLED_ATTR, Boolean.TRUE, null);
155         }
156 
157         out.endElement("input");
158     }
159 
160     private void encodeSuggestions(FacesContext context, ResponseWriter out,
161                                    Map choices, String clientId,
162                                    UIComponent component) throws IOException
163     {
164         //String choiceId = getChoiceId(component, context);
165         /** @todo have the div class be the one specified by user or default of ACDiv */
166 
167         out.startElement(HTML.DIV_ELEM, component);
168         out.writeAttribute(HTML.ID_ATTR, clientId, null);
169 
170         Iterator i = choices.keySet().iterator();
171         while (i.hasNext())
172         {
173             String choice = (String) i.next();
174             if(choice.compareTo("-1")==0) continue;
175             String text = (String) choices.get(choice);
176             out.startElement(HTML.DIV_ELEM, null);
177             out.writeAttribute(HTML.ID_ATTR,
178                                component.getClientId(context) + "_choice" +
179                                choice, null);
180             out.writeAttribute(HTML.CLASS_ATTR, "ACdiv", null);
181             out.writeText(text, null);
182             out.endElement(HTML.DIV_ELEM);
183         }
184 
185         out.endElement(HTML.DIV_ELEM);
186     }
187 
188     /**
189      * Returns true if one or both of the HTML attributes "disabled"
190      * or "readonly" are set to true.
191      */
192     private boolean isDisabledOrReadOnly(UIComponent component)
193     {
194         boolean disabled = false;
195         boolean readOnly = false;
196 
197         Object disabledAttr = component.getAttributes().get("disabled");
198         if (disabledAttr != null)
199         {
200             disabled = disabledAttr.equals(Boolean.TRUE);
201         }
202         Object readOnlyAttr = component.getAttributes().get("readonly");
203         if (readOnlyAttr != null)
204         {
205             readOnly = readOnlyAttr.equals(Boolean.TRUE);
206         }
207         return disabled || readOnly;
208     }
209 
210     private Map getChoices(UIComponent component)
211     {
212         // Get the choices
213         Object choices = null;
214         Iterator i = component.getChildren().iterator();
215         while (i.hasNext())
216         {
217             UIComponent kid = (UIComponent) i.next();
218             // Should handle UISelectItem as well
219             if (kid instanceof UISelectItems)
220             {
221                 choices = ( (UISelectItems) kid).getValue();
222             }
223         }
224 
225         /** @todo selectItems may not necessarily be a map */
226 
227 //        if (choices instanceof Map)
228 //        {
229             return (Map) choices;
230 //        }
231 //        else if (choices instanceof Collection)
232 //        {
233 //
234 //        }
235 //        else if (choices instanceof Object[])
236 //        {
237 //
238 //        }
239 //        else if (choices instanceof SelectItem)
240 //        {
241 //
242 //        }
243 //
244 //        throw new IllegalArgumentException(
245 //            "Unsupported object type used for choices.");
246     }
247 
248 
249 //  (this is not called from anywhere)
250 //    /**
251 //     * Helper method for getting the boolean value of an attribute.  If the attribute is not specified,
252 //     * then return the default value.
253 //     *
254 //     * @param component The component for which the attributes are to be checked.
255 //     * @param attributeName The name of the boolean attribute.
256 //     * @param defaultValue The default value of the attribute (to be returned if no value found).
257 //     * @return boolean
258 //     */
259 //    private boolean getBoolean(UIComponent component, String attributeName,
260 //                               boolean defaultValue)
261 //    {
262 //        Boolean booleanAttr = (Boolean) component.getAttributes().get(
263 //            attributeName);
264 //
265 //        if (booleanAttr == null)
266 //        {
267 //            return defaultValue;
268 //        }
269 //        else
270 //        {
271 //            return booleanAttr.booleanValue();
272 //        }
273 //    }
274 
275     /**
276      * Encodes necessary style information.
277      *
278      * @param component UIComponent
279      * @param context FacesContext
280      * @throws IOException
281      */
282     private void encodeStyles(UIComponent component, FacesContext context) throws
283                                                                            IOException
284     {
285         AddResource addResource = AddResourceFactory.getInstance(context);
286         String styleLocation = (String) component.getAttributes().get(JSFAttr.
287             STYLE_LOCATION);
288         if (styleLocation == null)
289         {
290             addResource.addStyleSheet(context, AddResource.HEADER_BEGIN, InputSuggestRenderer.class,
291                                       "css/suggest.css");
292         }
293         else
294         {
295             addResource.addStyleSheet(context, AddResource.HEADER_BEGIN, styleLocation + "/suggest.css");
296         }
297     }
298 
299     /**
300      * Encodes necessary javascript functions.
301      *
302      * @param component UIComponent
303      * @param context FacesContext
304      * @throws IOException
305      */
306     private void encodeJavascript(UIComponent component, FacesContext context) throws
307                                                                                IOException
308     {
309         ResponseWriter out = context.getResponseWriter();
310 
311         AddResource addResource = AddResourceFactory.getInstance(context);
312         String javascriptLocation = (String) component.getAttributes().get(
313             JSFAttr.JAVASCRIPT_LOCATION);
314         if (javascriptLocation == null)
315         {
316             addResource.addJavaScriptHere(context, InputSuggestRenderer.class,
317                                           "javascript/suggest.js");
318         }
319         else
320         {
321             addResource.addJavaScriptHere(context, javascriptLocation + "/suggest.js");
322         }
323 
324         // now add javascript that depends on the component and cannot be part of javascript file
325         out.startElement(HTML.SCRIPT_ELEM, null);
326         out.writeAttribute(HTML.TYPE_ATTR, "text/javascript", null);
327 
328         String textId = getTextId(component, context);
329         String choiceId = getChoiceId(component, context);
330         String suggestionsId = getSuggestionsId(component, context);
331 
332         // can't use ':', '|' or '.' chars in javascript variable name
333         String modifiedTextId = textId.replace(':', '_');
334         modifiedTextId = modifiedTextId.replace('|', '_');
335         modifiedTextId = modifiedTextId.replace('.', '_');
336 
337         /** @todo make these values dependent on component attributes */
338         out.writeText("\nvar " + modifiedTextId +
339                       "Row = -1; // this should always be initialized to -1\n", null);
340         out.writeText("var " + modifiedTextId +
341                       "RowDiv = null; // this should always be initialized to null\n", null);
342         out.writeText("var " + modifiedTextId +
343                       "MinRow = 0; // this should always be initialized to 0\n", null);
344         out.writeText("var ACrowHeight = 15;\n", null);
345         out.writeText("var ACfield = document.getElementById('" + textId +
346                       "');\n", null);
347         out.writeText("var " + modifiedTextId + "Scroll = true;\n", null);
348         out.writeText("var " + modifiedTextId + "CaseSensitive = false;\n", null);
349         out.writeText("var " + modifiedTextId + "DisplayRows = 5;\n", null);
350         out.writeText("var " + modifiedTextId +
351                       "Div = document.getElementById('" + suggestionsId +
352                       "');\n", null);
353         out.writeText("var " + modifiedTextId + "HiddenFldId = '" + choiceId +
354                       "';\n", null);
355         out.writeText("var " + modifiedTextId + "NormalClass = 'ACdiv';\n", null);
356         out.writeText("var " + modifiedTextId +
357                       "HighlightClass = 'AChighlighted';\n", null);
358         out.writeText("ACfield.onfocus = new Function('" + modifiedTextId +
359                       "Div.style.visibility = \"visible\"');\n", null);
360         out.writeText("ACfield.onblur = new Function('blurACfld(this)');\n", null);
361         out.writeText("ACfield.onkeyup = new Function(\"event\", \"return handleACkeyUp(this, event)\");\n", null);
362         out.writeText("ACfield.onkeydown = new Function(\"event\", \"return handleACkeyDown(this, event)\");\n", null);
363         out.writeText("var " + modifiedTextId + "Options = " + modifiedTextId +
364                       "Div.getElementsByTagName(\"DIV\");\n", null);
365         out.writeText(modifiedTextId +
366                       "Div.onscroll = new Function(\"setACfieldFocus('" +
367                       textId + "')\");\n", null);
368 
369         out.writeText("var optLen = " + modifiedTextId + "Options.length;\n", null);
370         out.writeText("for (var ii=0; ii<optLen; ii++) {\n", null);
371         out.writeText(modifiedTextId +
372                       "Options[ii].style.height = ACrowHeight + 'px';\n", null);
373         out.writeText(modifiedTextId +
374                       "Options[ii].onmouseover = new Function(\"highlightACDiv(this, '" +
375                       textId +
376                       "', \" + ii + \")\");\n", null);
377         out.writeText(modifiedTextId +
378                       "Options[ii].onmouseout = new Function(\"unHighlightACDiv(this, '" +
379                       textId + "')\");\n", null);
380         out.writeText(modifiedTextId +
381                       "Options[ii].onmousedown = new Function(\"selectACDiv('" +
382                       textId + "')\");\n", null);
383         out.writeText("}\n", null);
384 
385         out.writeText("if (navigator.appVersion.toLowerCase().indexOf('msie') != -1 && " +
386                       "navigator.userAgent.toLowerCase().indexOf('opera') == -1)\n", null);
387         out.writeText("document.writeln('<iframe id=\"" + modifiedTextId + "Shim\" src=\"javascript:false;\" " +
388                       "scrolling=\"no\" frameborder=\"0\" style=\"position:absolute; top:0px; left:0px;\">" +
389                       "</iframe>');\n", null);
390 
391 //       out.writeText("var backingBean_inputFieldScroll = true;\n", null);
392 //       out.writeText("var backingBean_inputFieldCaseSensitive = false;\n", null);
393 //       out.writeText("var backingBean_inputFieldDisplayRows = 5;\n", null);
394 //       out.writeText("var backingBean_inputFieldDiv = document.getElementById('" + suggestionsId + "');\n", null);
395 //       out.writeText("var backingBean_inputFieldNormalClass = 'ACdiv';\n", null);
396 //       out.writeText("var backingBean_inputFieldHighlightClass = 'AChighlighted';\n", null);
397 //       out.writeText("ACfield.onfocus = new Function('backingBean_inputFieldDiv.style.visibility = \"visible\"');\n", null);
398 //       out.writeText("ACfield.onblur = new Function('backingBean_inputFieldDiv.style.visibility = \"hidden\"');\n", null);
399 //       out.writeText("ACfield.onkeyup = new Function(\"event\", \"return handleACkeyUp(this, event)\");\n", null);
400 //       out.writeText("ACfield.onkeydown = new Function(\"event\", \"return handleACkeyDown(this, event)\");\n", null);
401 //       out.writeText("var backingBean_inputFieldOptions = backingBean_inputFieldDiv.getElementsByTagName(\"DIV\");\n", null);
402 //       out.writeText("backingBean_inputFieldDiv.onscroll = new Function(\"setACfieldFocus('" + textId + "')\");\n", null);
403 //
404 //       out.writeText("var optLen = backingBean_inputFieldOptions.length;\n", null);
405 //       out.writeText("for (var ii=0; ii<optLen; ii++) {\n", null);
406 //       out.writeText("backingBean_inputFieldOptions[ii].style.height = ACrowHeight + 'px';\n", null);
407 //       out.writeText("backingBean_inputFieldOptions[ii].onmouseover = new Function(\"highlightACDiv(this, '" + textId +
408 //                     "', \" + ii + \")\");\n", null);
409 //       out.writeText("backingBean_inputFieldOptions[ii].onmouseout = new Function(\"unHighlightACDiv(this, '" + textId + "')\");\n", null);
410 //       out.writeText("backingBean_inputFieldOptions[ii].onmousedown = new Function(\"selectACDiv('" + textId + "')\");\n", null);
411 //       out.writeText("}\n", null);
412 
413         out.endElement(HTML.SCRIPT_ELEM);
414 
415     }
416 
417     private String getTextId(UIComponent component, FacesContext context)
418     {
419         return (component.getId() + "_text");
420     }
421 
422     private String getChoiceId(UIComponent component, FacesContext context)
423     {
424         return (component.getId() + "_choice");
425     }
426 
427     private String getSuggestionsId(UIComponent component, FacesContext context)
428     {
429         return ("AC" + getTextId(component, context));
430     }
431 }