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

Last change on this file since 157 was 157, checked in by duh, 9 years ago
  • temporary commit
  • Property svn:keywords set to Rev Author Date
File size: 10.3 KB
Line 
1package dbnp.studycapturing
2
3import org.codehaus.groovy.grails.plugins.web.taglib.JavascriptTagLib
4import dbnp.studycapturing.*
5import dbnp.data.*
6
7/**
8 * Wizard tag library
9 *
10 * @author Jeroen Wesbeek
11 * @since 20100113
12 * @package wizard
13 *
14 * Revision information:
15 * $Rev: 157 $
16 * $Author: duh $
17 * $Date: 2010-02-02 09:31:25 +0000 (di, 02 feb 2010) $
18 */
19class WizardTagLib extends JavascriptTagLib {
20        // define the tag namespace (e.g.: <wizard:action ... />
21        static namespace = "wizard"
22
23        // define the AJAX provider to use
24        static ajaxProvider = "jquery"
25
26        // define default text field width
27        static defaultTextFieldSize = 25;
28
29        /**
30         * ajaxButton tag, this is a modified version of the default
31         * grails submitToRemote tag to work with grails webflows.
32         * Usage is identical to submitToRemote with the only exception
33         * that a 'name' form element attribute is required. E.g.
34         * <wizard:ajaxButton name="myAction" value="myButton ... />
35         *
36         * you can also provide a javascript function to execute after
37         * success. This behaviour differs from the default 'after'
38         * action which always fires after a button press...
39         *
40         * @see http://blog.osx.eu/2010/01/18/ajaxifying-a-grails-webflow/
41         * @see http://www.grails.org/WebFlow
42         * @see http://www.grails.org/Tag+-+submitToRemote
43         * @todo perhaps some methods should be moved to a more generic
44         *        'webflow' taglib
45         * @param Map attributes
46         * @param Closure body
47         */
48        def ajaxButton = { attrs, body ->
49                // get the jQuery version
50                def jQueryVersion = grailsApplication.getMetadata()['plugins.jquery']
51
52                // fetch the element name from the attributes
53                def elementName = attrs['name'].replaceAll(/ /, "_")
54
55                // javascript function to call after success
56                def afterSuccess = attrs['afterSuccess']
57
58                // generate a normal submitToRemote button
59                def button = submitToRemote(attrs, body)
60
61                /**
62                 * as of now (grails 1.2.0 and jQuery 1.3.2.4) the grails webflow does
63                 * not properly work with AJAX as the submitToRemote button does not
64                 * handle and submit the form properly. In order to support webflows
65                 * this method modifies two parts of a 'normal' submitToRemote button:
66                 *
67                 * 1) replace 'this' with 'this.form' as the 'this' selector in a button
68                 *    action refers to the button and / or the action upon that button.
69                 *    However, it should point to the form the button is part of as the
70                 *    the button should submit the form data.
71                 * 2) prepend the button name to the serialized data. The default behaviour
72                 *    of submitToRemote is to remove the element name altogether, while
73                 *    the grails webflow expects a parameter _eventId_BUTTONNAME to execute
74                 *    the appropriate webflow action. Hence, we are going to prepend the
75                 *    serialized formdata with an _eventId_BUTTONNAME parameter.
76                 */
77                if (jQueryVersion =~ /^1.([1|2|3]).(.*)/) {
78                        // fix for older jQuery plugin versions
79                        button = button.replaceFirst(/data\:jQuery\(this\)\.serialize\(\)/, "data:\'_eventId_${elementName}=1&\'+jQuery(this.form).serialize()")
80                } else {
81                        // as of jQuery plugin version 1.4.0.1 submitToRemote has been modified and the
82                        // this.form part has been fixed. Consequently, our wrapper has changed as well...
83                        button = button.replaceFirst(/data\:jQuery/, "data:\'_eventId_${elementName}=1&\'+jQuery")
84                }
85 
86                // add an after success function call?
87                // usefull for performing actions on success data (hence on refreshed
88                // wizard pages, such as attaching tooltips)
89                if (afterSuccess) {
90                        button = button.replaceFirst(/\.html\(data\)\;/, '.html(data);' + afterSuccess + ';')
91                }
92
93                // replace double semi colons
94                button = button.replaceAll(/;{2,}/, ';')
95               
96                // render button
97                out << button
98        }
99
100        /**
101         * generate ajax webflow redirect javascript
102         *
103         * As we have an Ajaxified webflow, the initial wizard page
104         * cannot contain a wizard form, as upon a failing submit
105         * (e.g. the form data does not validate) the form should be
106         * shown again. However, the Grails webflow then renders the
107         * complete initial wizard page into the success div. As this
108         * ruins the page layout (a page within a page) we want the
109         * initial page to redirect to the first wizard form to enter
110         * the webflow correctly. We do this by emulating an ajax post
111         * call which updates the wizard content with the first wizard
112         * form.
113         *
114         * Usage: <wizard:ajaxFlowRedirect form="form#wizardForm" name="next" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" />
115         * form = the form identifier
116         * name = the action to execute in the webflow
117         * update = the divs to update upon success or error
118         *
119         * Example initial webflow action to work with this javascript:
120         * ...
121         * mainPage {
122         *      render(view: "/wizard/index")
123         *      onRender {
124         *              flow.page = 1
125         *      }
126         *      on("next").to "pageOne"
127         * }
128         * ...
129         *
130         * @param Map attributes
131         * @param Closure body
132         */
133        def ajaxFlowRedirect = { attrs, body ->
134                // define AJAX provider
135                setProvider([library: ajaxProvider])
136
137                // generate an ajax button
138                def button = this.ajaxButton(attrs, body)
139
140                // strip the button part to only leave the Ajax call
141                button = button.replaceFirst(/<[^\"]*\"jQuery.ajax/,'jQuery.ajax')
142                button = button.replaceFirst(/return false.*/,'')
143
144                // change form if a form attribute is present
145                if (attrs.get('form')) {
146                        button = button.replaceFirst(/this\.form/,
147                                "\\\$('" + attrs.get('form') + "')"
148                        )
149                }
150
151                // generate javascript
152                out << '<script language="JavaScript">'
153                out << '$(document).ready(function() {'
154                out << button
155                out << '});'
156                out << '</script>'
157        }
158
159        /**
160         * render the content of a particular wizard page
161         * @param Map attrs
162         * @param Closure body  (help text)
163         */
164        def pageContent = {attrs, body ->
165                // define AJAX provider
166                setProvider([library: ajaxProvider])
167
168                // render new body content
169                out << render(template: "/wizard/common/tabs")
170                out << '<div class="content">'
171                out << body()
172                out << '</div>'
173                out << render(template: "/wizard/common/navigation")
174                out << render(template: "/wizard/common/error")
175        }
176
177        /**
178         * generate a base form element
179         * @param String        inputElement name
180         * @param Map           attributes
181         * @param Closure       help content
182         */
183        def baseElement = { inputElement, attrs, help ->
184                // work variables
185                def description = attrs.remove('description')
186                def addExampleElement = attrs.remove('addExampleElement')
187
188                // render a form element
189                out << '<div class="element">'
190                out << ' <div class="description">'
191                out << description
192                out << ' </div>'
193                out << ' <div class="input">'
194                out << "$inputElement"(attrs)
195                if(help()) {
196                        out << '        <div class="helpIcon"></div>'
197                }
198
199                // add an disabled input box for feedback purposes
200                // @see dateElement(...)
201                if (addExampleElement) {
202                        def exampleAttrs = new LinkedHashMap()
203                        exampleAttrs.name = attrs.get('name')+'Example'
204                        exampleAttrs.class  = 'isExample'
205                        exampleAttrs.disabled = 'disabled'
206                        exampleAttrs.size = 30
207                        out << textField(exampleAttrs)
208                }
209
210                out << ' </div>'
211
212                // add help content if it is available
213                if (help()) {
214                        out << '  <div class="helpContent">'
215                        out << '    ' + help()
216                        out << '  </div>'
217                }
218
219                out << '</div>'
220        }
221
222        /**
223         * render a textFieldElement
224         * @param Map attrs
225         * @param Closure body  (help text)
226         */
227        def textFieldElement = { attrs, body ->
228                // set default size, or scale to max length if it is less than the default size
229                if (!attrs.get("size")) {
230                        if (attrs.get("maxlength")) {
231                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
232                        } else {
233                                attrs.size = defaultTextFieldSize
234                        }
235                }
236
237                // render template element
238                baseElement.call(
239                        'textField',
240                        attrs,
241                        body
242                )
243        }
244
245        /**
246         * render a dateElement
247         * NOTE: datepicker is attached through wizard.js!
248         * @param Map attrs
249         * @param Closure body  (help text)
250         */
251        def dateElement = { attrs, body ->
252                // transform value?
253                if (attrs.value instanceof Date) {
254                        // transform date instance to formatted string (dd/mm/yyyy)
255                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
256                }
257               
258                // set some textfield values
259                attrs.maxlength = 10
260                attrs.addExampleElement = true
261               
262                // render a normal text field
263                //out << textFieldElement(attrs,body)
264                textFieldElement.call(
265                        attrs,
266                        body
267                )
268        }
269       
270        /**
271         * Template form element
272         * @param Map           attributes
273         * @param Closure       help content
274         */
275        def speciesElement = { attrs, body ->
276                // render template element
277                baseElement.call(
278                        'speciesSelect',
279                        attrs,
280                        body
281                )
282        }
283
284        /**
285         * render a species select element
286         * @param Map attrs
287         */
288        def speciesSelect = { attrs ->
289                // fetch all species
290                attrs.from = Term.findAll()     // for now, all terms, should be refactored to be species ontology only!
291                def speciesOntology = Ontology.findAllByName('NCBI Taxonomy')
292
293                println speciesOntology
294                //println Term.findAllByOntology(speciesOntology)
295
296                // got a name?
297                if (!attrs.name) {
298                        attrs.name = 'species'
299                }
300
301                out << select(attrs)
302        }
303
304        /**
305         * Template form element
306         * @param Map           attributes
307         * @param Closure       help content
308         */
309        def templateElement = { attrs, body ->
310                // render template element
311                baseElement.call(
312                        'templateSelect',
313                        attrs,
314                        body
315                )
316        }
317       
318        /**
319         * render a template select element
320         * @param Map attrs
321         */
322        def templateSelect = { attrs ->
323                // fetch all templates
324                attrs.from = Template.findAll() // for now, all templates
325
326                // got a name?
327                if (!attrs.name) {
328                        attrs.name = 'template'
329                }
330               
331                out << select(attrs)
332        }
333
334        /**
335         * render table headers for all subjectFields in a template
336         * @param Map attributes
337         */
338        def templateColumnHeaders = { attrs ->
339                def template = attrs.remove('template')
340
341                // output table headers for template fields
342                template.subjectFields.each() {
343                        out << '<div class="' + attrs.get('class') + '">' + it + '</div>'
344                }
345        }
346
347        /**
348         * render table input elements for all subjectFields in a template
349         * @param Map attributes
350         */
351        def templateColumns = { attrs, body ->
352                def subjectId = attrs.remove('id')
353                def template = attrs.remove('template')
354
355                // output columns for these subjectFields
356                template.subjectFields.each() {
357                        println it.type
358                        out << '<div class="' + attrs.get('class') + '">'
359
360                        switch (it.type) {
361                                case 'STRINGLIST':
362                                        // render stringlist subjectfield
363                                        out << '<select><option>TODO</option></select>'
364                                        break;
365                                case 'INTEGER':
366                                        // render integer subjectfield
367                                        out << '<input type="text" value="TODO">'
368                                        break;
369                                default:
370                                        // unsupported field type
371                                        out << '<b>!! ' + it.type + '</b>'
372                                        break;
373                        }
374                        out << '</div>'
375                }
376        }
377}
Note: See TracBrowser for help on using the repository browser.