Addition of tooltip behavior.
[utils] / wicket / components / src / main / java / org / wamblee / wicket / jquery / AbstractJQueryBehavior.java
1 /*
2  * Copyright 2005-2010 the original author or authors.
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  * 
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * 
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.wamblee.wicket.jquery;
17
18 import java.util.Arrays;
19
20 import org.apache.wicket.Component;
21 import org.apache.wicket.Page;
22 import org.apache.wicket.behavior.IBehavior;
23 import org.apache.wicket.markup.html.IHeaderResponse;
24 import org.wamblee.wicket.behavior.CompositeBehavior;
25
26 import flexjson.JSONSerializer;
27
28 /**
29  * Abstract JQuery hehavior class that makes it easy to write jQuery behaviors:
30  * <ul>
31  * <li>Creating a ready function which will be invoked for the component</li>
32  * <li>Checking whether or not the behavior may be attached to a page using
33  * {@link #isPageAllowed()}. By default, the behavior may be attached to
34  * pages. Behaviors that should not be allowed to be attached to pages should
35  * override this method.</li>
36  * <li>Ensuring that the markup id of the component is output</li>
37  * <li>Creating a call to an intialization function from the ready handler using
38  * the component id</li>
39  * </ul>
40  * 
41  * The ready function will be invoked as part of a ready handler and will invoke
42  * a function with two arguments. The first is the selector of the component and
43  * the second is a configuration object. In case the behavior is attached to a
44  * component, a selector is used based on the unique markup id. When used on a
45  * page, the selector matches with the "body" of the page.
46  * <p>
47  * The second parameter is obtained through a call to
48  * {@link #getConfigurationJavascript()}.
49  * 
50  * 
51  * @author Erik Brakkee
52  * 
53  *         <ConfigType>
54  */
55 public class AbstractJQueryBehavior<ConfigType> extends CompositeBehavior {
56
57     private static final String DEFAULT_NAMESPACE = "org.wamblee";
58     private static JSONSerializer DEFAULT_JSON_SERIALIZER = new JSONSerializer();
59
60     private Component component;
61     private String function;
62
63     /**
64      * Constructs the behavior.
65      * 
66      * @param aFunction
67      *            Function to be invoked from the ready handler. This function
68      *            is invoked with a CSS selector that identifies the component.
69      * @param aBehaviors
70      *            Behaviors to add in addition to the basic JQuery stuff.
71      */
72     public AbstractJQueryBehavior(String aFunction, IBehavior... aBehaviors) {
73         super(getBehaviors(aBehaviors));
74         function = aFunction;
75     }
76
77     /**
78      * Determines whether the behavior is allowed to be attached toa page.
79      * 
80      * @return True. 
81      */
82     protected boolean isPageAllowed() {
83         return true;
84     }
85
86     private static IBehavior[] getBehaviors(IBehavior[] aBehaviors) {
87         IBehavior[] behaviors = new IBehavior[aBehaviors.length + 1];
88         behaviors[0] = new JQueryHeaderContributor();
89         for (int i = 0; i < aBehaviors.length; i++) {
90             behaviors[i + 1] = aBehaviors[i];
91         }
92         return behaviors;
93     }
94
95     @Override
96     public void bind(Component aComponent) {
97         if (component != null) {
98             throw new IllegalStateException(
99                 "this kind of handler cannot be attached to " +
100                     "multiple components; it is already attached to component " +
101                     component + ", but component " + aComponent +
102                     " wants to be attached too");
103         }
104         if (!isPageAllowed() && aComponent instanceof Page) {
105             throw new IllegalStateException(
106                 "This behavior cannot be applied to a page: " + aComponent);
107         }
108         component = aComponent;
109         super.bind(aComponent);
110         if (!(aComponent instanceof Page)) {
111             aComponent.setOutputMarkupId(true);
112         }
113     }
114
115     @Override
116     public void renderHead(IHeaderResponse aResponse) {
117         super.renderHead(aResponse);
118         String jsString = createReadyFunction();
119         aResponse.renderJavascript(jsString, null);
120     }
121
122     /**
123      * Creates a jQuery ready handler that invokes a given javascript function
124      * with the id of a component and with a second parameter of parameters to
125      * pass additional information to the function.
126      * 
127      * @param aFunction
128      *            Javascript function to invoke, the name of this function must
129      *            include the namespace as it is called from a global scope.
130      * @param aComponent
131      *            Component to invoke the id for.
132      * @return
133      */
134     String createReadyFunction() {
135         if (!(component instanceof Page) && !component.getOutputMarkupId()) {
136             throw new IllegalStateException(
137                 "The component " +
138                     component +
139                     " does not have its markup id set so this ready handler will not have any effect");
140         }
141         StringBuffer js = new StringBuffer();
142         js.append("jQuery(function(){");
143         js.append(function + "(");
144         String selector = "body";
145         if (!(component instanceof Page)) {
146             selector = "#" + component.getMarkupId();
147         }
148         js.append("\"" + selector + "\"");
149         String config = getConfigurationJavascript();
150         if (config != null) {
151             js.append(",");
152             js.append(config);
153         }
154         js.append(");");
155         js.append("});");
156         String jsString = js.toString();
157         return jsString;
158     }
159
160     /**
161      * Returns a javascript object that is passed as second argument to the
162      * ready function. This method uses {@link #getConfigurationObject()} to
163      * obtain the configuration object to use which is then serialized to
164      * javascript using {@link JSONSerializer}. Subclasses can override the
165      * default JSONSerializer by implementing {@link #getCustomSerializer()}.
166      * <p>
167      * Subclasses should override this method to perform custom serialization.
168      * 
169      * @return Configuration object in javascript.
170      */
171     protected String getConfigurationJavascript() {
172         Object config = getConfigurationObject();
173         return getActualSerializer().serialize(config);
174     }
175
176     /**
177      * Gets the actual serializer to use. It uses {@link #getCustomSerializer()}
178      * to check if there is a custom serializer available.
179      * 
180      * @return Serializer.
181      */
182     private JSONSerializer getActualSerializer() {
183         JSONSerializer serializer = getCustomSerializer();
184         if (serializer != null) {
185             return serializer;
186         }
187         return DEFAULT_JSON_SERIALIZER;
188     }
189
190     /**
191      * Returns the serializer to use. Implementations can override this method
192      * to perform custom initialization of the serializer knowing the type of
193      * configuration object to use.
194      * 
195      * @return Custom serializer to use.
196      */
197     protected JSONSerializer getCustomSerializer() {
198         return null;
199     }
200
201     /**
202      * Gets the configuration object to use. This is transformed to JSON using
203      * the serializer.
204      * 
205      * @return Configuration object.
206      */
207     protected ConfigType getConfigurationObject() {
208         return null;
209     }
210 }