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

Last change on this file since 213 was 213, checked in by duh, 11 years ago
  • restructured wizard
  • added information boxes
  • improved error feedback (highlighted error fields)
  • added confirmation page
  • several smaller bugfixes and improvements
  • Property svn:keywords set to
    Date
    Author
    Rev
File size: 13.5 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: 213 $
16 * $Author: duh $
17 * $Date: 2010-02-25 15:18:22 +0000 (do, 25 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 or plugin
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                def addExample2Element  = attrs.remove('addExample2Element')
188
189                // render a form element
190                out << '<div class="element">'
191                out << ' <div class="description">'
192                out << description
193                out << ' </div>'
194                out << ' <div class="input">'
195                out << "$inputElement"(attrs)
196                if(help()) {
197                        out << '        <div class="helpIcon"></div>'
198                }
199
200                // add an disabled input box for feedback purposes
201                // @see dateElement(...)
202                if (addExampleElement) {
203                        def exampleAttrs = new LinkedHashMap()
204                        exampleAttrs.name = attrs.get('name')+'Example'
205                        exampleAttrs.class  = 'isExample'
206                        exampleAttrs.disabled = 'disabled'
207                        exampleAttrs.size = 30
208                        out << textField(exampleAttrs)
209                }
210
211                // add an disabled input box for feedback purposes
212                // @see dateElement(...)
213                if (addExample2Element) {
214                        def exampleAttrs = new LinkedHashMap()
215                        exampleAttrs.name = attrs.get('name')+'Example2'
216                        exampleAttrs.class  = 'isExample'
217                        exampleAttrs.disabled = 'disabled'
218                        exampleAttrs.size = 30
219                        out << textField(exampleAttrs)
220                }
221
222                out << ' </div>'
223
224                // add help content if it is available
225                if (help()) {
226                        out << '  <div class="helpContent">'
227                        out << '    ' + help()
228                        out << '  </div>'
229                }
230
231                out << '</div>'
232        }
233
234        /**
235         * render a textFieldElement
236         * @param Map attrs
237         * @param Closure body  (help text)
238         */
239        def textFieldElement = { attrs, body ->
240                // set default size, or scale to max length if it is less than the default size
241                if (!attrs.get("size")) {
242                        if (attrs.get("maxlength")) {
243                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
244                        } else {
245                                attrs.size = defaultTextFieldSize
246                        }
247                }
248
249                // render template element
250                baseElement.call(
251                        'textField',
252                        attrs,
253                        body
254                )
255        }
256
257
258        /**
259         * render a select form element
260         * @param Map attrs
261         * @param Closure body  (help text)
262         */
263        def selectElement = { attrs, body ->
264                baseElement.call(
265                        'select',
266                        attrs,
267                        body
268                )
269        }
270
271        /**
272         * render a checkBox form element
273         * @param Map attrs
274         * @param Closure body  (help text)
275         */
276        def checkBoxElement = { attrs, body ->
277                baseElement.call(
278                        'checkBox',
279                        attrs,
280                        body
281                )
282        }
283
284        /**
285         * render a dateElement
286         * NOTE: datepicker is attached through wizard.js!
287         * @param Map attrs
288         * @param Closure body  (help text)
289         */
290        def dateElement = { attrs, body ->
291                // transform value?
292                if (attrs.value instanceof Date) {
293                        // transform date instance to formatted string (dd/mm/yyyy)
294                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
295                }
296               
297                // set some textfield values
298                attrs.maxlength = (attrs.maxlength) ? attrs.maxlength : 10
299                attrs.addExampleElement = true
300               
301                // render a normal text field
302                //out << textFieldElement(attrs,body)
303                textFieldElement.call(
304                        attrs,
305                        body
306                )
307        }
308
309        /**
310         * render a dateElement
311         * NOTE: datepicker is attached through wizard.js!
312         * @param Map attrs
313         * @param Closure body  (help text)
314         */
315        def timeElement = { attrs, body ->
316                // transform value?
317                if (attrs.value instanceof Date) {
318                        // transform date instance to formatted string (dd/mm/yyyy)
319                        attrs.value = String.format('%td/%<tm/%<tY %<tH:%<tM', attrs.value)
320                }
321
322                attrs.addExampleElement = true
323                attrs.addExample2Element = true
324                attrs.maxlength = 16
325
326                // render a normal text field
327                //out << textFieldElement(attrs,body)
328                textFieldElement.call(
329                        attrs,
330                        body
331                )
332        }
333       
334        /**
335         * Template form element
336         * @param Map           attributes
337         * @param Closure       help content
338         */
339        def speciesElement = { attrs, body ->
340                // render template element
341                baseElement.call(
342                        'speciesSelect',
343                        attrs,
344                        body
345                )
346        }
347
348        /**
349         * Button form element
350         * @param Map           attributes
351         * @param Closure       help content
352         */
353        def buttonElement = { attrs, body ->
354                // render template element
355                baseElement.call(
356                        'ajaxButton',
357                        attrs,
358                        body
359                )
360        }
361
362        /**
363         * render a species select element
364         * @param Map attrs
365         */
366        def speciesSelect = { attrs ->
367                // fetch the speciesOntology
368                // note that this is a bit nasty, probably the ontologyName should
369                // be configured in a configuration file... --> TODO: centralize species configuration
370                def speciesOntology = Ontology.findByName('NCBI Taxonomy')
371
372                // fetch all species
373                attrs.from = Term.findAllByOntology(speciesOntology)
374
375                // got a name?
376                if (!attrs.name) {
377                        // nope, use a default name
378                        attrs.name = 'species'
379                }
380
381                out << select(attrs)
382        }
383
384        /**
385         * Template form element
386         * @param Map           attributes
387         * @param Closure       help content
388         */
389        def templateElement = { attrs, body ->
390                // render template element
391                baseElement.call(
392                        'templateSelect',
393                        attrs,
394                        body
395                )
396        }
397       
398        /**
399         * render a template select element
400         * @param Map attrs
401         */
402        def templateSelect = { attrs ->
403                // fetch all templates
404                attrs.from = Template.findAll() // for now, all templates
405
406                // got a name?
407                if (!attrs.name) {
408                        attrs.name = 'template'
409                }
410               
411                out << select(attrs)
412        }
413
414        /**
415         * Term form element
416         * @param Map           attributes
417         * @param Closure       help content
418         */
419        def termElement = { attrs, body ->
420                // render term element
421                baseElement.call(
422                        'termSelect',
423                        attrs,
424                        body
425                )
426        }
427
428        /**
429         * render a term select element
430         * @param Map attrs
431         */
432        def termSelect = { attrs ->
433                // fetch all terms
434                attrs.from = Term.findAll()     // for now, all terms as we cannot identify terms as being treatment terms...
435
436                // got a name?
437                if (!attrs.name) {
438                        attrs.name = 'term'
439                }
440
441                out << select(attrs)
442        }
443
444        def show = { attrs ->
445                // is object parameter set?
446                def o = attrs.object
447
448                println o.getProperties();
449                o.getProperties().each {
450                        println it
451                }
452
453                out << "!! test version of 'show' tag !!"
454        }
455
456        /**
457         * render table headers for all subjectFields in a template
458         * @param Map attributes
459         */
460        def templateColumnHeaders = { attrs ->
461                def template = attrs.remove('template')
462
463                // output table headers for template fields
464                template.subjectFields.each() {
465                        out << '<div class="' + attrs.get('class') + '">' + it + '</div>'
466                }
467        }
468
469        /**
470         * render table input elements for all subjectFields in a template
471         * @param Map attributes
472         */
473        def templateColumns = { attrs, body ->
474                def subject                     = attrs.remove('subject')
475                def subjectId           = attrs.remove('id')
476                def template            = attrs.remove('template')
477                def intFields           = subject.templateIntegerFields
478                def stringFields        = subject.templateStringFields
479                def floatFields         = subject.templateFloatFields
480                def termFields          = subject.templateTermFields
481
482                // output columns for these subjectFields
483                template.subjectFields.each() {
484                        // output div
485                        out << '<div class="' + attrs.get('class') + '">'
486
487                        switch (it.type) {
488                                case 'STRINGLIST':
489                                        // render stringlist subjectfield
490                                        if (!it.listEntries.isEmpty()) {
491                                                out << select(
492                                                        name: attrs.name + '_' + it.name,
493                                                        from: it.listEntries,
494                                                        value: (stringFields) ? stringFields.get(it.name) : ''
495                                                )
496                                        } else {
497                                                out << '<span class="warning">no values!!</span>'
498                                        }
499                                        break;
500                                case 'INTEGER':
501                                        // render integer subjectfield
502                                        out << textField(
503                                                name: attrs.name + '_' + it.name,
504                                                value: (intFields) ? intFields.get(it.name) : ''
505                                        )
506                                        break;
507                                case 'FLOAT':
508                                        // render float subjectfield
509                                        out << textField(
510                                                name: attrs.name + '_' + it.name,
511                                                value: (floatFields) ? floatFields.get(it.name) : ''
512                                        )
513                                        break;
514                                default:
515                                        // unsupported field type
516                                        out << '<span class="warning">!'+it.type+'</span>'
517                                        break;
518                        }
519                        out << '</div>'
520                }
521        }
522}
Note: See TracBrowser for help on using the repository browser.