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

Last change on this file since 238 was 238, checked in by duh, 11 years ago

Refectored version of the wizard

  • initial template page has been removed, now is a generic 'start' page where one (in the future) may create a new study, or load and modify an already stored study
  • study page incorporates study template select element, but does not yet incorporate the study template fields
  • subjects page now allows creation of subjects based on a template. This change also implied the study page altogether had to change into a seperate table entity. Now the the page lists as many tables as unique templates have been selected. These tables contain all subjects that were added using that particular template. NOTE: data is not stored yet, due to the fact that templateEntity does not work properly yey (key/value pairs need to be set correctly when calling the setTemplate method)
  • the JavaScript? now handles multiple tables in a page as well, and automatically initializes any underlying slider div if that is required
  • Property svn:keywords set to
    Date
    Author
    Rev
File size: 14.2 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: 238 $
16 * $Author: duh $
17 * $Date: 2010-03-05 14:21:52 +0000 (vr, 05 mrt 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                // src parameter?
59                def src = attrs['src']
60                def alt = attrs['alt']
61
62                // generate a normal submitToRemote button
63                def button = submitToRemote(attrs, body)
64
65                /**
66                 * as of now (grails 1.2.0 and jQuery 1.3.2.4) the grails webflow does
67                 * not properly work with AJAX as the submitToRemote button does not
68                 * handle and submit the form properly. In order to support webflows
69                 * this method modifies two parts of a 'normal' submitToRemote button:
70                 *
71                 * 1) replace 'this' with 'this.form' as the 'this' selector in a button
72                 *    action refers to the button and / or the action upon that button.
73                 *    However, it should point to the form the button is part of as the
74                 *    the button should submit the form data.
75                 * 2) prepend the button name to the serialized data. The default behaviour
76                 *    of submitToRemote is to remove the element name altogether, while
77                 *    the grails webflow expects a parameter _eventId_BUTTONNAME to execute
78                 *    the appropriate webflow action. Hence, we are going to prepend the
79                 *    serialized formdata with an _eventId_BUTTONNAME parameter.
80                 */
81                if (jQueryVersion =~ /^1.([1|2|3]).(.*)/) {
82                        // fix for older jQuery plugin versions
83                        button = button.replaceFirst(/data\:jQuery\(this\)\.serialize\(\)/, "data:\'_eventId_${elementName}=1&\'+jQuery(this.form).serialize()")
84                } else {
85                        // as of jQuery plugin version 1.4.0.1 submitToRemote has been modified and the
86                        // this.form part has been fixed. Consequently, our wrapper has changed as well...
87                        button = button.replaceFirst(/data\:jQuery/, "data:\'_eventId_${elementName}=1&\'+jQuery")
88                }
89 
90                // add an after success function call?
91                // usefull for performing actions on success data (hence on refreshed
92                // wizard pages, such as attaching tooltips)
93                if (afterSuccess) {
94                        button = button.replaceFirst(/\.html\(data\)\;/, '.html(data);' + afterSuccess + ';')
95                }
96
97                // got an src parameter?
98                if (src) {
99                        def replace = 'type="image" src="' + src + '"'
100
101                        if (alt) replace = replace + ' alt="' + alt + '"'
102
103                        button = button.replaceFirst(/type="button"/, replace)
104                }
105
106                // replace double semi colons
107                button = button.replaceAll(/;{2,}/, ';')
108               
109                // render button
110                out << button
111        }
112
113        /**
114         * generate ajax webflow redirect javascript
115         *
116         * As we have an Ajaxified webflow, the initial wizard page
117         * cannot contain a wizard form, as upon a failing submit
118         * (e.g. the form data does not validate) the form should be
119         * shown again. However, the Grails webflow then renders the
120         * complete initial wizard page into the success div. As this
121         * ruins the page layout (a page within a page) we want the
122         * initial page to redirect to the first wizard form to enter
123         * the webflow correctly. We do this by emulating an ajax post
124         * call which updates the wizard content with the first wizard
125         * form.
126         *
127         * Usage: <wizard:ajaxFlowRedirect form="form#wizardForm" name="next" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" />
128         * form = the form identifier
129         * name = the action to execute in the webflow
130         * update = the divs to update upon success or error
131         *
132         * Example initial webflow action to work with this javascript:
133         * ...
134         * mainPage {
135         *      render(view: "/wizard/index")
136         *      onRender {
137         *              flow.page = 1
138         *      }
139         *      on("next").to "pageOne"
140         * }
141         * ...
142         *
143         * @param Map attributes
144         * @param Closure body
145         */
146        def ajaxFlowRedirect = { attrs, body ->
147                // define AJAX provider
148                setProvider([library: ajaxProvider])
149
150                // generate an ajax button
151                def button = this.ajaxButton(attrs, body)
152
153                // strip the button part to only leave the Ajax call
154                button = button.replaceFirst(/<[^\"]*\"jQuery.ajax/,'jQuery.ajax')
155                button = button.replaceFirst(/return false.*/,'')
156
157                // change form if a form attribute is present
158                if (attrs.get('form')) {
159                        button = button.replaceFirst(/this\.form/,
160                                "\\\$('" + attrs.get('form') + "')"
161                        )
162                }
163
164                // generate javascript
165                out << '<script language="JavaScript">'
166                out << '$(document).ready(function() {'
167                out << button
168                out << '});'
169                out << '</script>'
170        }
171
172        /**
173         * render the content of a particular wizard page
174         * @param Map attrs
175         * @param Closure body  (help text)
176         */
177        def pageContent = {attrs, body ->
178                // define AJAX provider
179                setProvider([library: ajaxProvider])
180
181                // render new body content
182                out << render(template: "/wizard/common/tabs")
183                out << '<div class="content">'
184                out << body()
185                out << '</div>'
186                out << render(template: "/wizard/common/navigation")
187                out << render(template: "/wizard/common/error")
188        }
189
190        /**
191         * generate a base form element
192         * @param String        inputElement name
193         * @param Map           attributes
194         * @param Closure       help content
195         */
196        def baseElement = { inputElement, attrs, help ->
197                // work variables
198                def description = attrs.remove('description')
199                def addExampleElement = attrs.remove('addExampleElement')
200                def addExample2Element  = attrs.remove('addExample2Element')
201
202                // execute inputElement call
203                def renderedElement = "$inputElement"(attrs)
204
205                // if false, then we skip this element
206                if (!renderedElement) return false
207
208                // render a form element
209                out << '<div class="element">'
210                out << ' <div class="description">'
211                out << description
212                out << ' </div>'
213                out << ' <div class="input">'
214                out << renderedElement
215                if(help()) {
216                        out << '        <div class="helpIcon"></div>'
217                }
218
219                // add an disabled input box for feedback purposes
220                // @see dateElement(...)
221                if (addExampleElement) {
222                        def exampleAttrs = new LinkedHashMap()
223                        exampleAttrs.name = attrs.get('name')+'Example'
224                        exampleAttrs.class  = 'isExample'
225                        exampleAttrs.disabled = 'disabled'
226                        exampleAttrs.size = 30
227                        out << textField(exampleAttrs)
228                }
229
230                // add an disabled input box for feedback purposes
231                // @see dateElement(...)
232                if (addExample2Element) {
233                        def exampleAttrs = new LinkedHashMap()
234                        exampleAttrs.name = attrs.get('name')+'Example2'
235                        exampleAttrs.class  = 'isExample'
236                        exampleAttrs.disabled = 'disabled'
237                        exampleAttrs.size = 30
238                        out << textField(exampleAttrs)
239                }
240
241                out << ' </div>'
242
243                // add help content if it is available
244                if (help()) {
245                        out << '  <div class="helpContent">'
246                        out << '    ' + help()
247                        out << '  </div>'
248                }
249
250                out << '</div>'
251        }
252
253        /**
254         * render a textFieldElement
255         * @param Map attrs
256         * @param Closure body  (help text)
257         */
258        def textFieldElement = { attrs, body ->
259                // set default size, or scale to max length if it is less than the default size
260                if (!attrs.get("size")) {
261                        if (attrs.get("maxlength")) {
262                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
263                        } else {
264                                attrs.size = defaultTextFieldSize
265                        }
266                }
267
268                // render template element
269                baseElement.call(
270                        'textField',
271                        attrs,
272                        body
273                )
274        }
275
276
277        /**
278         * render a select form element
279         * @param Map attrs
280         * @param Closure body  (help text)
281         */
282        def selectElement = { attrs, body ->
283                baseElement.call(
284                        'select',
285                        attrs,
286                        body
287                )
288        }
289
290        /**
291         * render a checkBox form element
292         * @param Map attrs
293         * @param Closure body  (help text)
294         */
295        def checkBoxElement = { attrs, body ->
296                baseElement.call(
297                        'checkBox',
298                        attrs,
299                        body
300                )
301        }
302
303        /**
304         * render a dateElement
305         * NOTE: datepicker is attached through wizard.js!
306         * @param Map attrs
307         * @param Closure body  (help text)
308         */
309        def dateElement = { attrs, body ->
310                // transform value?
311                if (attrs.value instanceof Date) {
312                        // transform date instance to formatted string (dd/mm/yyyy)
313                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
314                }
315               
316                // set some textfield values
317                attrs.maxlength = (attrs.maxlength) ? attrs.maxlength : 10
318                attrs.addExampleElement = true
319               
320                // render a normal text field
321                //out << textFieldElement(attrs,body)
322                textFieldElement.call(
323                        attrs,
324                        body
325                )
326        }
327
328        /**
329         * render a dateElement
330         * NOTE: datepicker is attached through wizard.js!
331         * @param Map attrs
332         * @param Closure body  (help text)
333         */
334        def timeElement = { attrs, body ->
335                // transform value?
336                if (attrs.value instanceof Date) {
337                        // transform date instance to formatted string (dd/mm/yyyy)
338                        attrs.value = String.format('%td/%<tm/%<tY %<tH:%<tM', attrs.value)
339                }
340
341                attrs.addExampleElement = true
342                attrs.addExample2Element = true
343                attrs.maxlength = 16
344
345                // render a normal text field
346                //out << textFieldElement(attrs,body)
347                textFieldElement.call(
348                        attrs,
349                        body
350                )
351        }
352       
353        /**
354         * Template form element
355         * @param Map           attributes
356         * @param Closure       help content
357         */
358        def speciesElement = { attrs, body ->
359                // render template element
360                baseElement.call(
361                        'speciesSelect',
362                        attrs,
363                        body
364                )
365        }
366
367        /**
368         * Button form element
369         * @param Map           attributes
370         * @param Closure       help content
371         */
372        def buttonElement = { attrs, body ->
373                // render template element
374                baseElement.call(
375                        'ajaxButton',
376                        attrs,
377                        body
378                )
379        }
380
381        /**
382         * render a species select element
383         * @param Map attrs
384         */
385        def speciesSelect = { attrs ->
386                // fetch the speciesOntology
387                // note that this is a bit nasty, probably the ontologyName should
388                // be configured in a configuration file... --> TODO: centralize species configuration
389                def speciesOntology = Ontology.findByName('NCBI Taxonomy')
390
391                // fetch all species
392                attrs.from = Term.findAllByOntology(speciesOntology)
393
394                // got a name?
395                if (!attrs.name) {
396                        // nope, use a default name
397                        attrs.name = 'species'
398                }
399
400                out << select(attrs)
401        }
402
403        /**
404         * Template form element
405         * @param Map           attributes
406         * @param Closure       help content
407         */
408        def templateElement = { attrs, body ->
409                // render template element
410                baseElement.call(
411                        'templateSelect',
412                        attrs,
413                        body
414                )
415        }
416       
417        /**
418         * render a template select element
419         * @param Map attrs
420         */
421        def templateSelect = { attrs ->
422                def entity = attrs.remove('entity')
423
424                // fetch templates
425                attrs.from = (entity) ? Template.findAllByEntity(entity) : Template.findAll()
426
427                // got a name?
428                if (!attrs.name) {
429                        attrs.name = 'template'
430                }
431
432                // got result?
433                if (attrs.from.size() >0) {
434                        out << select(attrs)
435                } else {
436                        // no, return false to make sure this element
437                        // is not rendered in the template
438                        return false
439                }
440        }
441
442        /**
443         * Term form element
444         * @param Map           attributes
445         * @param Closure       help content
446         */
447        def termElement = { attrs, body ->
448                // render term element
449                baseElement.call(
450                        'termSelect',
451                        attrs,
452                        body
453                )
454        }
455
456        /**
457         * render a term select element
458         * @param Map attrs
459         */
460        def termSelect = { attrs ->
461                // fetch all terms
462                attrs.from = Term.findAll()     // for now, all terms as we cannot identify terms as being treatment terms...
463
464                // got a name?
465                if (!attrs.name) {
466                        attrs.name = 'term'
467                }
468
469                out << select(attrs)
470        }
471
472        def show = { attrs ->
473                // is object parameter set?
474                def o = attrs.object
475
476                println o.getProperties();
477                o.getProperties().each {
478                        println it
479                }
480
481                out << "!! test version of 'show' tag !!"
482        }
483
484        /**
485         * render table headers for all subjectFields in a template
486         * @param Map attributes
487         */
488        def templateColumnHeaders = { attrs ->
489                def template = attrs.remove('template')
490
491                // output table headers for template fields
492                template.fields.each() {
493                        out << '<div class="' + attrs.get('class') + '">' + it + '</div>'
494                }
495        }
496
497        /**
498         * render table input elements for all subjectFields in a template
499         * @param Map attributes
500         */
501        def templateColumns = { attrs, body ->
502                def subject                     = attrs.remove('subject')
503                def subjectId           = attrs.remove('id')
504                def template            = attrs.remove('template')
505                def intFields           = subject.templateIntegerFields
506                def stringFields        = subject.templateStringFields
507                def floatFields         = subject.templateFloatFields
508                def termFields          = subject.templateTermFields
509
510                // output columns for these subjectFields
511                template.fields.each() {
512                        // output div
513                        out << '<div class="' + attrs.get('class') + '">'
514
515                        switch (it.type) {
516                                case 'STRINGLIST':
517                                        // render stringlist subjectfield
518                                        if (!it.listEntries.isEmpty()) {
519                                                out << select(
520                                                        name: attrs.name + '_' + it.name,
521                                                        from: it.listEntries,
522                                                        value: (stringFields) ? stringFields.get(it.name) : ''
523                                                )                                               
524                                        } else {
525                                                out << '<span class="warning">no values!!</span>'
526                                        }
527                                        break;
528                                case 'INTEGER':
529                                        // render integer subjectfield
530                                        out << textField(
531                                                name: attrs.name + '_' + it.name,
532                                                value: (intFields) ? intFields.get(it.name) : ''
533                                        )
534                                        break;
535                                case 'FLOAT':
536                                        // render float subjectfield
537                                        out << textField(
538                                                name: attrs.name + '_' + it.name,
539                                                value: (floatFields) ? floatFields.get(it.name) : ''
540                                        )
541                                        break;
542                                default:
543                                        // unsupported field type
544                                        out << '<span class="warning">!'+it.type+'</span>'
545                                        //out << subject.getFieldValue(it.name)
546                                        break;
547                        }
548
549                        out << '</div>'
550                }
551        }
552}
Note: See TracBrowser for help on using the repository browser.