source: trunk/grails-app/taglib/dbnp/studycapturing/WizardTagLib.groovy @ 138

Last change on this file since 138 was 138, checked in by duh, 12 years ago
  • added development version of wizard subjects page and logic
  • modified some domain classes (again, who reverted them?) to implement Serializable
  • Property svn:keywords set to Rev Author Date
File size: 8.5 KB
Line 
1package dbnp.studycapturing
2
3import org.codehaus.groovy.grails.plugins.web.taglib.JavascriptTagLib
4import dbnp.data.Term
5
6/**
7 * Wizard tag library
8 *
9 * @author Jeroen Wesbeek
10 * @since 20100113
11 * @package wizard
12 *
13 * Revision information:
14 * $Rev: 138 $
15 * $Author: duh $
16 * $Date: 2010-01-26 15:46:01 +0000 (di, 26 jan 2010) $
17 */
18class WizardTagLib extends JavascriptTagLib {
19        // define the tag namespace (e.g.: <wizard:action ... />
20        static namespace = "wizard"
21
22        // define the AJAX provider to use
23        static ajaxProvider = "jquery"
24
25        // define default text field width
26        static defaultTextFieldSize = 25;
27
28        /**
29         * ajaxButton tag, this is a modified version of the default
30         * grails submitToRemote tag to work with grails webflows.
31         * Usage is identical to submitToRemote with the only exception
32         * that a 'name' form element attribute is required. E.g.
33         * <wizard:ajaxButton name="myAction" value="myButton ... />
34         *
35         * you can also provide a javascript function to execute after
36         * success. This behaviour differs from the default 'after'
37         * action which always fires after a button press...
38         *
39         * @see http://blog.osx.eu/2010/01/18/ajaxifying-a-grails-webflow/
40         * @see http://www.grails.org/WebFlow
41         * @see http://www.grails.org/Tag+-+submitToRemote
42         * @todo perhaps some methods should be moved to a more generic
43         *        'webflow' taglib
44         * @param Map attributes
45         * @param Closure body
46         */
47        def ajaxButton = { attrs, body ->
48                // get the jQuery version
49                def jQueryVersion = grailsApplication.getMetadata()['plugins.jquery']
50
51                // fetch the element name from the attributes
52                def elementName = attrs['name'].replaceAll(/ /, "_")
53
54                // javascript function to call after success
55                def afterSuccess = attrs['afterSuccess']
56
57                // generate a normal submitToRemote button
58                def button = submitToRemote(attrs, body)
59
60                /**
61                 * as of now (grails 1.2.0 and jQuery 1.3.2.4) the grails webflow does
62                 * not properly work with AJAX as the submitToRemote button does not
63                 * handle and submit the form properly. In order to support webflows
64                 * this method modifies two parts of a 'normal' submitToRemote button:
65                 *
66                 * 1) replace 'this' with 'this.form' as the 'this' selector in a button
67                 *    action refers to the button and / or the action upon that button.
68                 *    However, it should point to the form the button is part of as the
69                 *    the button should submit the form data.
70                 * 2) prepend the button name to the serialized data. The default behaviour
71                 *    of submitToRemote is to remove the element name altogether, while
72                 *    the grails webflow expects a parameter _eventId_BUTTONNAME to execute
73                 *    the appropriate webflow action. Hence, we are going to prepend the
74                 *    serialized formdata with an _eventId_BUTTONNAME parameter.
75                 */
76                if (jQueryVersion =~ /^1.([1|2|3]).(.*)/) {
77                        // fix for older jQuery plugin versions
78                        button = button.replaceFirst(/data\:jQuery\(this\)\.serialize\(\)/, "data:\'_eventId_${elementName}=1&\'+jQuery(this.form).serialize()")
79                } else {
80                        // as of jQuery plugin version 1.4.0.1 submitToRemote has been modified and the
81                        // this.form part has been fixed. Consequently, our wrapper has changed as well...
82                        button = button.replaceFirst(/data\:jQuery/, "data:\'_eventId_${elementName}=1&\'+jQuery")
83                }
84 
85                // add an after success function call?
86                // usefull for performing actions on success data (hence on refreshed
87                // wizard pages, such as attaching tooltips)
88                if (afterSuccess) {
89                        button = button.replaceFirst(/\.html\(data\)\;/, '.html(data);' + afterSuccess + ';')
90                }
91
92                // replace double semi colons
93                button = button.replaceAll(/;{2,}/, ';')
94               
95                // render button
96                out << button
97        }
98
99        /**
100         * generate ajax webflow redirect javascript
101         *
102         * As we have an Ajaxified webflow, the initial wizard page
103         * cannot contain a wizard form, as upon a failing submit
104         * (e.g. the form data does not validate) the form should be
105         * shown again. However, the Grails webflow then renders the
106         * complete initial wizard page into the success div. As this
107         * ruins the page layout (a page within a page) we want the
108         * initial page to redirect to the first wizard form to enter
109         * the webflow correctly. We do this by emulating an ajax post
110         * call which updates the wizard content with the first wizard
111         * form.
112         *
113         * Usage: <wizard:ajaxFlowRedirect form="form#wizardForm" name="next" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" />
114         * form = the form identifier
115         * name = the action to execute in the webflow
116         * update = the divs to update upon success or error
117         *
118         * Example initial webflow action to work with this javascript:
119         * ...
120         * mainPage {
121         *      render(view: "/wizard/index")
122         *      onRender {
123         *              flow.page = 1
124         *      }
125         *      on("next").to "pageOne"
126         * }
127         * ...
128         *
129         * @param Map attributes
130         * @param Closure body
131         */
132        def ajaxFlowRedirect = { attrs, body ->
133                // define AJAX provider
134                setProvider([library: ajaxProvider])
135
136                // generate an ajax button
137                def button = this.ajaxButton(attrs, body)
138
139                // strip the button part to only leave the Ajax call
140                button = button.replaceFirst(/<[^\"]*\"jQuery.ajax/,'jQuery.ajax')
141                button = button.replaceFirst(/return false.*/,'')
142
143                // change form if a form attribute is present
144                if (attrs.get('form')) {
145                        button = button.replaceFirst(/this\.form/,
146                                "\\\$('" + attrs.get('form') + "')"
147                        )
148                }
149
150                // generate javascript
151                out << '<script language="JavaScript">'
152                out << '$(document).ready(function() {'
153                out << button
154                out << '});'
155                out << '</script>'
156        }
157
158        /**
159         * render the content of a particular wizard page
160         * @param Map attrs
161         * @param Closure body  (help text)
162         */
163        def pageContent = {attrs, body ->
164                // define AJAX provider
165                setProvider([library: ajaxProvider])
166
167                // render new body content
168                out << render(template: "/wizard/common/tabs")
169                out << '<div class="content">'
170                out << body()
171                out << '</div>'
172                out << render(template: "/wizard/common/navigation")
173                out << render(template: "/wizard/common/error")
174        }
175
176        /**
177         * render a textFieldElement
178         * @param Map attrs
179         * @param Closure body  (help text)
180         */
181        def textFieldElement = {attrs, body ->
182                // set default size, or scale to max length if it is less than the default size
183                if (!attrs.get("size")) {
184                        if (attrs.get("maxlength")) {
185                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
186                        } else {
187                                attrs.size = defaultTextFieldSize
188                        }
189                }
190
191                // work variables
192                def addExampleElement = attrs.remove('addExampleElement')
193                //attrs.remove('addExampleElement')
194
195                // render a text element
196                out << '<div class="element">'
197                out << ' <div class="description">'
198                out << attrs.get('description')
199                out << ' </div>'
200                out << ' <div class="input">'
201
202                // add text input field
203                out << textField(attrs)
204
205                // add a help icon if help information is available
206                if (body()) {
207                        out << '        <div class="helpIcon" />'
208                }
209
210                // add an disabled input box for feedback purposes
211                // @see dateElement(...)
212                if (addExampleElement) {
213                        def exampleAttrs = new LinkedHashMap()
214                        exampleAttrs.name = attrs.get('name')+'Example'
215                        exampleAttrs.class  = 'isExample'
216                        exampleAttrs.disabled = 'disabled'
217                        out << textField(exampleAttrs)
218                }
219
220                // end HTML
221                out << ' </div>'
222
223                // add help content if it is available
224                if (body()) {
225                        out << '  <div class="helpContent">'
226                        out << '    ' + body()
227                        out << '  </div>'
228                }
229
230                out << '</div>'
231        }
232
233        /**
234         * render a dateElement
235         * @param Map attrs
236         * @param Closure body  (help text)
237         */
238        def dateElement = { attrs, body ->
239                // transform value?
240                if (attrs.value instanceof Date) {
241                        // transform date instance to formatted string (dd/mm/yyyy)
242                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
243                }
244               
245                // set some textfield values
246                attrs.maxlength = 10
247                attrs.addExampleElement = true
248               
249                // render a normal text field
250                out << textFieldElement(attrs,body)
251
252                // and attach the jquery-ui datepicker
253                out << '<script type="text/javascript">'
254                out << '$(document).ready(function() {'
255                out << '        $("#' + attrs.get('name') + '").datepicker({'
256                out << '                dateFormat: \'dd/mm/yy\','
257                out << '                altField: \'#' + attrs.get('name') + 'Example\', altFormat: \'DD, d MM, yy\''
258                out << '        });'
259                out << '});'
260                out << '</script>'
261        }
262
263        /**
264         * render a species select element
265         * @param Map attrs
266         * @param Closure body (help text)
267         */
268        def speciesSelect = { attrs, body ->
269                // fetch all species
270                attrs.from = Term.findAll()     // for now, all terms, should be refactored to be species ontology only!
271
272                // got a name?
273                if (!attrs.name) {
274                        attrs.name = 'species'
275                }
276
277                out << select(attrs)
278        }
279}
Note: See TracBrowser for help on using the repository browser.