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  
20  package org.apache.myfaces.custom.dialog;
21  
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.StringTokenizer;
27  
28  import javax.faces.component.UIComponent;
29  import javax.faces.context.FacesContext;
30  import javax.faces.context.ResponseWriter;
31  
32  import org.apache.myfaces.custom.dojo.DojoUtils;
33  import org.apache.myfaces.renderkit.html.util.AddResource;
34  import org.apache.myfaces.renderkit.html.util.AddResourceFactory;
35  import org.apache.myfaces.shared_tomahawk.renderkit.JSFAttr;
36  import org.apache.myfaces.shared_tomahawk.renderkit.RendererUtils;
37  import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
38  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRenderer;
39  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils;
40  
41  /**
42   * Renderer for the s:modalDialog component.
43   * <p>
44   * This component works in one of two different ways:
45   * <ul>
46   * <li>The component can contain child components, in which case the child components
47   * are initially hidden but are displayed in a "popup" modal window when the "show"
48   * javascript method is invoked. In this mode, a DIV is rendered to wrap the child
49   * components. 
50   * <li>The component can have a "viewId" property defined, in which case the specified
51   * JSF view will be fetched into a "popup" modal window when the "show" javascript
52   * method is invoked. In this mode, a DIV containing an IFrame is rendered into the
53   * html page; the iframe is used to load the specified view.
54   * </ul>
55   * It is the page author's responsibility to have some other HTML control on the
56   * page whose "onclick" attribute contains javascript to invoke the "show" method
57   * of the modal dialog. Component property "dialogVar" specifies the name of a global
58   * javascript variable to be created, and its "show" method can be invoked to display
59   * the popup.
60   * <p>
61   * The Dojo library uses css tricks to make the DIV component act like a modal
62   * window.
63   * 
64   * @JSFRenderer
65   *   renderKitId = "HTML_BASIC" 
66   *   family = "javax.faces.Panel"
67   *   type = "org.apache.myfaces.ModalDialog"
68   */
69  public class ModalDialogRenderer extends HtmlRenderer
70  {
71      public static final String RENDERER_TYPE = "org.apache.myfaces.ModalDialog";
72  
73      public static final String DIV_ID_PREFIX = "_div";
74  
75      /**
76       * Writes a DIV opening tag, plus javascript to make sure that the Dojo
77       * library is initialised.
78       */
79      //@Override
80      public void encodeBegin(FacesContext context, UIComponent component)
81              throws IOException
82      {
83          String javascriptLocation = (String) component.getAttributes().get(JSFAttr.JAVASCRIPT_LOCATION);
84          DojoUtils.addMainInclude(context, component, javascriptLocation,
85                                   DojoUtils.getDjConfigInstance(context));
86          DojoUtils.addRequire(context, component, "dojo.widget.Dialog");
87  
88          writeModalDialogBegin(context, (ModalDialog) component, context.getResponseWriter());
89      }
90  
91      //@Override
92      /**
93       * Writes a DIV closing tag, plus javascript to declare the global "dialogVar"
94       * object that can be invoked to show the modal dialog.
95       */
96      public void encodeEnd(FacesContext context, UIComponent component) throws IOException
97      {
98          ModalDialog dlg = (ModalDialog) component;
99  
100         StringBuffer buf = new StringBuffer();
101 
102         buf.append("</div>");
103 
104         writeDialogLoader(context, dlg, buf);
105 
106         context.getResponseWriter().write(buf.toString());
107 
108         if (dlg.getViewId() != null)
109         {
110             // when getViewId is set, we did not render the children in the
111             // encodeChildren method, so instead do it now.
112             RendererUtils.renderChildren(context, component);
113             HtmlRendererUtils.writePrettyLineSeparator(context);
114         }
115     }
116 
117     private void appendHiderIds(StringBuffer buf, ModalDialog dlg)
118     {
119         List hiders = new ArrayList();
120 
121         if (dlg.getHiderIds() != null)
122         {
123             hiders.addAll(Arrays.asList(dlg.getHiderIds().split(",")));
124         }
125 
126         if (isRenderCloseButton(dlg) && dlg.getDialogTitle() != null)
127         {
128             hiders.add(dlg.getDialogVar() + "Closer");
129         }
130 
131         for (int i = 0; i < hiders.size(); i++)
132         {
133             String varName = "btn" + i;
134             buf
135                 .append("var ")
136                 .append(varName)
137                 .append(" = document.getElementById(\"")
138                 .append(((String) hiders.get(i)).trim())
139                 .append("\");")
140 
141                 .append(dlg.getDialogVar())
142                 .append(".setCloseControl(")
143                 .append(varName)
144                 .append(");");
145         }
146     }
147 
148     private void appendDialogAttributes(StringBuffer buf, ModalDialog dlg)
149     {
150         if(dlg.getDialogAttr() == null)
151         {
152             return;
153         }
154 
155         StringTokenizer it = new StringTokenizer(dlg.getDialogAttr(), " ");
156         while(it.hasMoreElements())
157         {
158             String[] pair = it.nextToken().split("=");
159             String attribute = pair[0];
160             String value = pair[1].replaceAll("'", "");
161             try
162             {
163                 // try to parse a double from the attribute value
164                 new Double(value);
165             }
166             catch(NumberFormatException e)
167             {
168                 // parsing failed - attribute is not numeric
169                 value = new StringBuffer("\"").append(value).append("\"").toString();
170             }
171             buf
172                 .append(", ")
173                 .append(attribute)
174                 .append(":")
175                 .append(value);
176         }
177     }
178 
179     /**
180      * Write an html DIV whose css style the Dojo library will manipulate to make it
181      * look like a popup window. 
182      * <p>
183      * The child components (or the nested iframe) will be rendered inside this div.
184      */
185     private void writeModalDialogBegin(FacesContext context, ModalDialog dlg, ResponseWriter writer)
186     throws IOException
187     {
188         StringBuffer buf = new StringBuffer();
189 
190         String dlgId = getDialogWrapperId(dlg);
191         buf.append("<div id=\"").append(dlgId).append("\"");
192         if(dlg.getStyle() != null)
193         {
194             buf.append(" style=\"").append(dlg.getStyle()).append("\"");
195         }
196         if(dlg.getStyleClass() != null)
197         {
198             buf.append(" class=\"").append(dlg.getStyleClass()).append("\"");
199         }
200         buf.append(">");
201 
202         writer.write(buf.toString());
203     }
204 
205     /**
206      * Gets the id for the HTML wrapper of the dialog.
207      * @param dlg
208      * @return
209      */
210     private String getDialogWrapperId(ModalDialog dlg)
211     {
212         // use dlg.getDialogId() with prefix if it is non-null,
213         // otherwise use the component id.
214         
215         String dlgId = dlg.getDialogId() != null ?
216                        new StringBuffer(dlg.getDialogId()).append(DIV_ID_PREFIX).toString() :
217                        dlg.getId();
218         return dlgId;
219     }
220 
221     /**
222      * Write a javascript block that declares a single global variable of type Object that
223      * provides methods for showing and hiding the modal dialog.
224      * <p>
225      * The name of the javascript variable is specified by component property "dialogVar".
226      */
227     private String writeDialogLoader(FacesContext context, ModalDialog dlg, StringBuffer buf)
228     {
229         String dlgWrapperId = getDialogWrapperId(dlg);
230         String dialogVar = dlg.getDialogVar();
231         buf.append("<script type=\"text/javascript\">");
232 
233         // Declare a global variable whose name is specified by dialogVar.
234         // This variable will be initialized to point to a Dojo Widget object
235         // when page load is complete.
236         buf.append("var ").append(dialogVar).append(";");
237 
238         // Declare a function that will be called on page load to initialize dialogVar
239         buf .append("function " + dialogVar + "_loader(e) {")
240             .append(dialogVar)
241             .append(" = dojo.widget.createWidget(\"dialog\", {id:")
242             .append("\"")
243             .append(dlg.getDialogId()) // use the dialogId from the component attribute
244             .append("\"");
245 
246         appendDialogAttributes(buf, dlg);
247 
248         buf.append("}, dojo.byId(\"").append(dlgWrapperId).append("\"));");
249 
250         appendHiderIds(buf, dlg);
251 
252         String viewId = dlg.getViewId();
253         String contentURL = dlg.getContentURL();
254         if (viewId != null)
255         {
256             StringBuffer sbUrl = new StringBuffer();
257             sbUrl.append(context.getExternalContext().getRequestContextPath());
258             sbUrl.append("/");
259             sbUrl.append(viewId);
260             String encodedUrl = context.getExternalContext().encodeActionURL(sbUrl.toString());
261             appendShowHideView(context, buf, dialogVar, encodedUrl);
262         }
263         else if (contentURL != null)
264         {
265             String encodedUrl = context.getExternalContext().encodeActionURL(contentURL);
266             appendShowHideView(context, buf, dialogVar, encodedUrl);
267         }
268 
269         buf.append("}");
270 
271         // Emit some global javascript that causes the initialization function
272         // defined above.
273         //
274         // We cannot use a standard javascript setTimeout call, as this breaks the
275         // submitOnEvent component.
276         //
277         // We cannot call the loader function immediately, as it appears that this
278         // breaks IE sometimes (always?).
279         //
280         // So it looks like using dojo's addOnLoad function is the best solution.. 
281         buf.append("dojo.addOnLoad(function() {" + dialogVar + "_loader();});");
282 
283         buf.append("</script>");
284         return dlgWrapperId;
285     }
286 
287     /**
288      * This is invoked only when the ModalDialog component has a viewId property
289      * defined, ie the dialog should automatically load a specific JSF view when
290      * it is shown.
291      * <p>
292      * Javascript is generated which override the standard Dojo modal dialog
293      * show and hide methods to do some initialisation, including loading the
294      * required view into the iframe.
295      */
296     private void appendShowHideView(
297             FacesContext context,
298             StringBuffer buf,
299             String dialogVar,
300             String url)
301     {
302         // save original onShow function (the standard dojo widget implementation)
303         buf .append(dialogVar)
304             .append(".oldOnShow=")
305             .append(dialogVar)
306             .append(".onShow;");
307 
308         // Define a new onShow function which first shows the modal window then
309         // causes it to do a GET to the server to fetch a specific page that is
310         // defined by the "url" parameter (which is defined via property viewId
311         // or contentURL on the JSF component).
312         //
313         // TODO: What is the purpose of variable window._myfaces_currentModal?
314         // There doesn't appear to be anything that *reads* it...
315         //
316         // TODO: What is the purpose of ${dialogVar}._myfaces_ok? Nothing appears
317         // to read it. 
318         buf .append(dialogVar)
319             .append(".onShow = function() {")
320             .append("this.oldOnShow();")
321             .append("var content = document.getElementById(\"modalDialogContent")
322             .append(dialogVar)
323             .append("\"); ")
324             .append("window._myfaces_currentModal=")
325             .append(dialogVar)
326             .append("; ")
327             .append(dialogVar)
328             .append("._myfaces_ok=false; ")
329             .append("content.contentWindow.location.replace('")
330             .append(url)
331             .append("'); ")
332             .append("}; ");
333 
334         // save original onHide function (the standard dojo widget implementation)
335         buf .append(dialogVar)
336             .append(".oldOnHide=")
337             .append(dialogVar)
338             .append(".onHide;");
339 
340         // Define a new onHide function which first shows the modal window then
341         // causes it to do a GET to the server to fetch a specific page that is
342         // defined by the "viewId" property on the JSF component.
343         buf .append(dialogVar)
344             .append(".onHide = function() {")
345             .append("this.oldOnHide();")
346             .append("window._myfaces_currentModal=null;")
347             .append("var content = document.getElementById(\"modalDialogContent")
348             .append(dialogVar)
349             .append("\"); ")
350             .append("content.contentWindow.location.replace('javascript:false;'); ")
351             .append("}; ");
352     }
353 
354     public boolean getRendersChildren()
355     {
356         return true;
357     }
358 
359     /**
360      * Override normal "encodeChildren" method to render the necessary dynamic
361      * parts of this component as well as the children.
362      * <p>
363      * If the user specified a titleBar facet, then that is rendered as the
364      * "window decoration" for the popup window. Otherwise if the user specified
365      * a dialogTitle, then a standard "window decoration" is rendered.
366      * <p>
367      * Then if the user did NOT specify a content page to be loaded into the popup,
368      * then the rest of the child components are rendered as normal.
369      * <p>
370      * But if the user DID specify a content page to be loaded, then an empty
371      * IFrame is rendered (javascript will be used to load it later), and the
372      * rendering of the child components is delayed until the encodeEnd method.
373      * TODO: why is child component rendering delayed?
374      *
375      * @see javax.faces.render.Renderer#encodeChildren(javax.faces.context.FacesContext,
376      *      javax.faces.component.UIComponent)
377      */
378     public void encodeChildren(FacesContext facesContext, UIComponent uiComponent) throws IOException
379     {
380         ModalDialog dlg = (ModalDialog) uiComponent;
381         ResponseWriter writer = facesContext.getResponseWriter();
382 
383         UIComponent titleFacet = dlg.getFacet("titleBar");
384         if (titleFacet != null)
385         {
386             RendererUtils.renderChild(facesContext, titleFacet);
387         }
388         else if (dlg.getDialogTitle() != null)
389         {
390             AddResourceFactory.getInstance(facesContext).addStyleSheet(facesContext, AddResource.HEADER_BEGIN,  ModalDialog.class, "modalDialog.css");
391 
392             writer.startElement(HTML.TABLE_ELEM, dlg);
393             writer.writeAttribute(HTML.CLASS_ATTR, "modalDialogDecoration " + getStyleName(dlg, "Decoration") , null);
394             writer.writeAttribute(HTML.CELLPADDING_ATTR, "2", null);
395             writer.writeAttribute(HTML.CELLSPACING_ATTR, "0", null);
396 
397             writer.startElement(HTML.TR_ELEM, dlg);
398             writer.writeAttribute(HTML.CLASS_ATTR, "modalDialogTitle " + getStyleName(dlg, "Title"), null);
399 
400             writer.startElement(HTML.TD_ELEM, dlg);
401             writer.writeAttribute(HTML.CLASS_ATTR, "modalDialogTitleLeft " + getStyleName(dlg, "TitleLeft"), null);
402             writer.writeText(dlg.getDialogTitle(), null);
403             writer.endElement(HTML.TD_ELEM);
404 
405             writer.startElement(HTML.TD_ELEM, dlg);
406             writer.writeAttribute(HTML.CLASS_ATTR, "modalDialogTitleRight " + getStyleName(dlg, "TitleRight"), null);
407             if (isRenderCloseButton(dlg))
408             {
409                 String imageUri = AddResourceFactory.getInstance(facesContext).getResourceUri(facesContext, ModalDialog.class, "close.gif");
410                 writer.startElement(HTML.IMG_ELEM, dlg);
411                 writer.writeAttribute(HTML.ID_ATTR, dlg.getDialogVar() + "Closer", null);
412                 writer.writeAttribute(HTML.SRC_ATTR, imageUri, null);
413                 writer.writeAttribute(HTML.CLASS_ATTR, "modalDialogCloser " + getStyleName(dlg, "Closer"), null);
414                 writer.endElement(HTML.IMG_ELEM);
415             }
416             writer.endElement(HTML.TD_ELEM);
417 
418             writer.endElement(HTML.TR_ELEM);
419             writer.endElement(HTML.TABLE_ELEM);
420         }
421 
422         if ((dlg.getViewId() != null) || (dlg.getContentURL() != null))
423         {
424             renderDialogViewFrame(facesContext, dlg);
425             // TODO: why are the rest of the child components not rendered here?
426             // is it so that subclasses of this component can insert stuff between
427             // the iframe and the normal children? 
428         }
429         else
430         {
431             RendererUtils.renderChildren(facesContext, uiComponent);
432             HtmlRendererUtils.writePrettyLineSeparator(facesContext);
433         }
434     }
435 
436     protected boolean isRenderCloseButton(ModalDialog dlg)
437     {
438         return !Boolean.FALSE.equals(dlg.getCloseButton());
439     }
440 
441     private String getStyleName(ModalDialog dlg, String suffix)
442     {
443         if (dlg.getStyleClass() != null)
444         {
445             return dlg.getStyleClass() + suffix;
446         }
447 
448         return "";
449     }
450 
451     /**
452      * Invoked only when the ModalDialog component has property viewId or contentURL defined.
453      */
454     private void renderDialogViewFrame(FacesContext facesContext, ModalDialog dlg) throws IOException
455     {
456         ResponseWriter writer = facesContext.getResponseWriter();
457 
458         writer.startElement(HTML.IFRAME_ELEM, dlg);
459         writer.writeAttribute(HTML.ID_ATTR, "modalDialogContent" + dlg.getDialogVar(), null);
460         writer.writeAttribute(HTML.CLASS_ATTR, "modalDialogContent " + getStyleName(dlg, "Content"), null);
461         writer.writeAttribute(HTML.SCROLLING_ATTR, "auto", null);
462         writer.writeAttribute(HTML.FRAMEBORDER_ATTR, "0", null);
463         writer.endElement(HTML.IFRAME_ELEM);
464     }
465 }