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

Last change on this file since 341 was 341, checked in by duh, 12 years ago
  • refactored wizard start page so that the big green buttons actually work
  • added a new page to load and modify a study, however this page is not yet functional
  • removed obsolete images
  • Property svn:keywords set to Date Rev Author
File size: 19.4 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: 341 $
16 * $Author: duh $
17 * $Date: 2010-04-14 09:36:53 +0000 (wo, 14 apr 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 a ajax submit JavaScript
115         * @see WizardTagLib::ajaxFlowRedirect
116         * @see WizardTagLib::baseElement (ajaxSubmitOnChange)
117         */
118        def ajaxSubmitJs = {attrs, body ->
119                // define AJAX provider
120                setProvider([library: ajaxProvider])
121
122                // got a function name?
123                def functionName = attrs.remove('functionName')
124                if (functionName && !attrs.get('name')) {
125                        attrs.name = functionName
126                }
127
128                // generate an ajax button
129                def button = this.ajaxButton(attrs, body)
130
131                // strip the button part to only leave the Ajax call
132                button = button.replaceFirst(/<[^\"]*\"jQuery.ajax/, 'jQuery.ajax')
133                button = button.replaceFirst(/return false.*/, '')
134
135                // change form if a form attribute is present
136                if (attrs.get('form')) {
137                        button = button.replaceFirst(/this\.form/,
138                                "\\\$('" + attrs.get('form') + "')"
139                        )
140                }
141
142                out << button
143        }
144
145        /**
146         * generate ajax webflow redirect javascript
147         *
148         * As we have an Ajaxified webflow, the initial wizard page
149         * cannot contain a wizard form, as upon a failing submit
150         * (e.g. the form data does not validate) the form should be
151         * shown again. However, the Grails webflow then renders the
152         * complete initial wizard page into the success div. As this
153         * ruins the page layout (a page within a page) we want the
154         * initial page to redirect to the first wizard form to enter
155         * the webflow correctly. We do this by emulating an ajax post
156         * call which updates the wizard content with the first wizard
157         * form.
158         *
159         * Usage: <wizard:ajaxFlowRedirect form="form#wizardForm" name="next" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" />
160         * form = the form identifier
161         * name = the action to execute in the webflow
162         * update = the divs to update upon success or error
163         *
164         * OR: to generate a JavaScript function you can call yourself, use 'functionName' instead of 'name'
165         *
166         * Example initial webflow action to work with this javascript:
167         * ...
168         * mainPage {*  render(view: "/wizard/index")
169         *      onRender {*             flow.page = 1
170         *}*    on("next").to "pageOne"
171         *}* ...
172         *
173         * @param Map attributes
174         * @param Closure body
175         */
176        def ajaxFlowRedirect = {attrs, body ->
177                // generate javascript
178                out << '<script type="text/javascript">'
179                out << '$(document).ready(function() {'
180                out << ajaxSubmitJs(attrs, body)
181                out << '});'
182                out << '</script>'
183        }
184
185        /**
186         * render the content of a particular wizard page
187         * @param Map attrs
188         * @param Closure body  (help text)
189         */
190        def pageContent = {attrs, body ->
191                // define AJAX provider
192                setProvider([library: ajaxProvider])
193
194                // render new body content
195                out << render(template: "/wizard/common/tabs")
196                out << '<div class="content">'
197                out << body()
198                out << '</div>'
199                out << render(template: "/wizard/common/navigation")
200                out << render(template: "/wizard/common/error")
201        }
202
203        /**
204         * generate a base form element
205         * @param String inputElement name
206         * @param Map attributes
207         * @param Closure help content
208         */
209        def baseElement = {inputElement, attrs, help ->
210                // work variables
211                def description = attrs.remove('description')
212                def addExampleElement = attrs.remove('addExampleElement')
213                def addExample2Element = attrs.remove('addExample2Element')
214
215                // got an ajax onchange action?
216                def ajaxOnChange = attrs.remove('ajaxOnChange')
217                if (ajaxOnChange) {
218                        if (!attrs.onChange) attrs.onChange = ''
219
220                        // add onChange AjaxSubmit javascript
221                        attrs.onChange += ajaxSubmitJs(
222                                [
223                                        functionName: ajaxOnChange,
224                                        url: attrs.get('url'),
225                                        update: attrs.get('update'),
226                                        afterSuccess: attrs.get('afterSuccess')
227                                ],
228                                ''
229                        )
230                }
231
232                // execute inputElement call
233                def renderedElement = "$inputElement"(attrs)
234
235                // if false, then we skip this element
236                if (!renderedElement) return false
237
238                // render a form element
239                out << '<div class="element">'
240                out << ' <div class="description">'
241                out << description
242                out << ' </div>'
243                out << ' <div class="input">'
244                out << renderedElement
245                if (help()) {
246                        out << '        <div class="helpIcon"></div>'
247                }
248
249                // add an disabled input box for feedback purposes
250                // @see dateElement(...)
251                if (addExampleElement) {
252                        def exampleAttrs = new LinkedHashMap()
253                        exampleAttrs.name = attrs.get('name') + 'Example'
254                        exampleAttrs.class = 'isExample'
255                        exampleAttrs.disabled = 'disabled'
256                        exampleAttrs.size = 30
257                        out << textField(exampleAttrs)
258                }
259
260                // add an disabled input box for feedback purposes
261                // @see dateElement(...)
262                if (addExample2Element) {
263                        def exampleAttrs = new LinkedHashMap()
264                        exampleAttrs.name = attrs.get('name') + 'Example2'
265                        exampleAttrs.class = 'isExample'
266                        exampleAttrs.disabled = 'disabled'
267                        exampleAttrs.size = 30
268                        out << textField(exampleAttrs)
269                }
270
271                out << ' </div>'
272
273                // add help content if it is available
274                if (help()) {
275                        out << '  <div class="helpContent">'
276                        out << '    ' + help()
277                        out << '  </div>'
278                }
279
280                out << '</div>'
281        }
282
283        /**
284         * render a textFieldElement
285         * @param Map attrs
286         * @param Closure body  (help text)
287         */
288        def textFieldElement = {attrs, body ->
289                // set default size, or scale to max length if it is less than the default size
290                if (!attrs.get("size")) {
291                        if (attrs.get("maxlength")) {
292                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
293                        } else {
294                                attrs.size = defaultTextFieldSize
295                        }
296                }
297
298                // render template element
299                baseElement.call(
300                        'textField',
301                        attrs,
302                        body
303                )
304        }
305
306        /**
307         * render a select form element
308         * @param Map attrs
309         * @param Closure body  (help text)
310         */
311        def selectElement = {attrs, body ->
312                baseElement.call(
313                        'select',
314                        attrs,
315                        body
316                )
317        }
318
319        /**
320         * render a checkBox form element
321         * @param Map attrs
322         * @param Closure body  (help text)
323         */
324        def checkBoxElement = {attrs, body ->
325                baseElement.call(
326                        'checkBox',
327                        attrs,
328                        body
329                )
330        }
331
332        /**
333         * render a dateElement
334         * NOTE: datepicker is attached through wizard.js!
335         * @param Map attrs
336         * @param Closure body  (help text)
337         */
338        def dateElement = {attrs, body ->
339                // transform value?
340                if (attrs.value instanceof Date) {
341                        // transform date instance to formatted string (dd/mm/yyyy)
342                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
343                }
344
345                // add 'rel' field to identity the datefield using javascript
346                attrs.rel = 'date'
347
348                // set some textfield values
349                attrs.maxlength = (attrs.maxlength) ? attrs.maxlength : 10
350                attrs.addExampleElement = true
351
352                // render a normal text field
353                //out << textFieldElement(attrs,body)
354                textFieldElement.call(
355                        attrs,
356                        body
357                )
358        }
359
360        /**
361         * render a dateElement
362         * NOTE: datepicker is attached through wizard.js!
363         * @param Map attrs
364         * @param Closure body  (help text)
365         */
366        def timeElement = {attrs, body ->
367                // transform value?
368                if (attrs.value instanceof Date) {
369                        // transform date instance to formatted string (dd/mm/yyyy)
370                        attrs.value = String.format('%td/%<tm/%<tY %<tH:%<tM', attrs.value)
371                }
372
373                // add 'rel' field to identity the field using javascript
374                attrs.rel = 'datetime'
375
376                attrs.addExampleElement = true
377                attrs.addExample2Element = true
378                attrs.maxlength = 16
379
380                // render a normal text field
381                //out << textFieldElement(attrs,body)
382                textFieldElement.call(
383                        attrs,
384                        body
385                )
386        }
387
388        /**
389         * Template form element
390         * @param Map attributes
391         * @param Closure help content
392         */
393        def speciesElement = {attrs, body ->
394                // render template element
395                baseElement.call(
396                        'speciesSelect',
397                        attrs,
398                        body
399                )
400        }
401
402        /**
403         * Button form element
404         * @param Map attributes
405         * @param Closure help content
406         */
407        def buttonElement = {attrs, body ->
408                // render template element
409                baseElement.call(
410                        'ajaxButton',
411                        attrs,
412                        body
413                )
414        }
415
416        /**
417         * render a species select element
418         * @param Map attrs
419         */
420        def speciesSelect = {attrs ->
421                // fetch the speciesOntology
422                // note that this is a bit nasty, probably the ontologyName should
423                // be configured in a configuration file... --> TODO: centralize species configuration
424                def speciesOntology = Ontology.findByName('NCBI Taxonomy')
425
426                // fetch all species
427                attrs.from = Term.findAllByOntology(speciesOntology)
428
429                // got a name?
430                if (!attrs.name) {
431                        // nope, use a default name
432                        attrs.name = 'species'
433                }
434
435                out << select(attrs)
436        }
437
438
439        /**
440         * Study form element
441         * @param Map attributes
442         * @param Closure help content
443         */
444        def studyElement = { attrs, body ->
445                // render study element
446                baseElement.call(
447                        'studySelect',
448                        attrs,
449                        body
450                )
451        }
452
453
454        /**
455         * render a study select element
456         * @param Map attrs
457         */
458        def studySelect = { attrs ->
459                // for now, just fetch all studies
460                attrs.from = Study.findAll()
461
462                // got a name?
463                if (!attrs.name) {
464                        attrs.name = "study"
465                }
466
467                // got result?
468                if (attrs.from.size() > 0) {
469                        out << select(attrs)
470                } else {
471                        // no, return false to make sure this element
472                        // is not rendered in the template
473                        return false
474                }
475        }
476
477        /**
478         * Template form element
479         * @param Map attributes
480         * @param Closure help content
481         */
482        def templateElement = {attrs, body ->
483                // render template element
484                baseElement.call(
485                        'templateSelect',
486                        attrs,
487                        body
488                )
489        }
490
491        /**
492         * render a template select element
493         * @param Map attrs
494         */
495        def templateSelect = {attrs ->
496                def entity = attrs.remove('entity')
497
498                // fetch templates
499                if (attrs.remove('addDummy')) {
500                        attrs.from = ['']
501                        if (entity && entity instanceof Class) {
502                                Template.findAllByEntity(entity).each() {
503                                        attrs.from[attrs.from.size()] = it
504                                }
505                        }
506                } else {
507                        attrs.from = (entity) ? Template.findAllByEntity(entity) : Template.findAll()
508                }
509
510                // got a name?
511                if (!attrs.name) {
512                        attrs.name = 'template'
513                }
514
515                // got result?
516                if (attrs.from.size() > 0) {
517                        out << select(attrs)
518                } else {
519                        // no, return false to make sure this element
520                        // is not rendered in the template
521                        return false
522                }
523        }
524
525        /**
526         * Term form element
527         * @param Map attributes
528         * @param Closure help content
529         */
530        def termElement = {attrs, body ->
531                // render term element
532                baseElement.call(
533                        'termSelect',
534                        attrs,
535                        body
536                )
537        }
538
539        /**
540         * render a term select element
541         * @param Map attrs
542         */
543        def termSelect = {attrs ->
544                // fetch all terms
545                attrs.from = Term.findAll()     // for now, all terms as we cannot identify terms as being treatment terms...
546
547                // got a name?
548                if (!attrs.name) {
549                        attrs.name = 'term'
550                }
551
552                out << select(attrs)
553        }
554
555        /**
556         * Protocol form element
557         * @param Map attributes
558         * @param Closure help content
559         */
560        def protocolElement = {attrs, body ->
561                // render protocol element
562                baseElement.call(
563                        'protocolSelect',
564                        attrs,
565                        body
566                )
567        }
568
569        /**
570         * render a protocol select element
571         * @param Map attrs
572         */
573        def protocolSelect = {attrs ->
574                // fetch all protocold
575                attrs.from = Protocol.findAll() // for now, all protocols
576
577                // got a name?
578                if (!attrs.name) {
579                        attrs.name = 'protocol'
580                }
581
582                out << select(attrs)
583        }
584
585        def show = {attrs ->
586                // is object parameter set?
587                def o = attrs.object
588
589                println o.getProperties();
590                o.getProperties().each {
591                        println it
592                }
593
594                out << "!! test version of 'show' tag !!"
595        }
596
597        /**
598         * render table headers for all subjectFields in a template
599         * @param Map attributes
600         */
601        def templateColumnHeaders = {attrs ->
602                def template = attrs.remove('template')
603
604                // output table headers for template fields
605                template.fields.each() {
606                        out << '<div class="' + attrs.get('class') + '">' + it + '</div>'
607                }
608        }
609
610        /**
611         * render table input elements for all subjectFields in a template
612         * @param Map attributes
613         */
614        def templateColumns = {attrs, body ->
615                def subject = attrs.remove('subject')
616                def subjectId = attrs.remove('id')
617                def template = attrs.remove('template')
618                def intFields = subject.templateIntegerFields
619                def stringFields = subject.templateStringFields
620                def floatFields = subject.templateFloatFields
621                def termFields = subject.templateTermFields
622
623                // output columns for these subjectFields
624                template.fields.each() {
625                        def fieldValue = subject.getFieldValue(it.name)
626
627                        // output div
628                        out << '<div class="' + attrs.get('class') + '">'
629
630                        // handle field types
631                        switch (it.type.toString()) {
632                                case ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'DOUBLE']:
633                                        out << textField(
634                                                name: attrs.name + '_' + it.escapedName(),
635                                                value: fieldValue
636                                        )
637                                        break
638                                case 'STRINGLIST':
639                                        // render stringlist subjectfield
640                                        if (!it.listEntries.isEmpty()) {
641                                                out << select(
642                                                        name: attrs.name + '_' + it.escapedName(),
643                                                        from: it.listEntries,
644                                                        value: fieldValue
645                                                )
646                                        } else {
647                                                out << '<span class="warning">no values!!</span>'
648                                        }
649                                        break
650                                case 'DATE':
651                                        // transform value?
652                                        if (fieldValue instanceof Date) {
653                                                if (fieldValue.getHours() == 0 && fieldValue.getMinutes() == 0) {
654                                                        // transform date instance to formatted string (dd/mm/yyyy)
655                                                        fieldValue = String.format('%td/%<tm/%<tY', fieldValue)
656                                                } else {
657                                                        // transform to date + time
658                                                        fieldValue = String.format('%td/%<tm/%<tY %<tH:%<tM', fieldValue)
659                                                }
660                                        }
661
662                                        // output a date field (not the 'rel' which makes the
663                                        // javascript front-end bind the jquery-ui datepicker)
664                                        out << textField(
665                                                name: attrs.name + '_' + it.escapedName(),
666                                                value: fieldValue,
667                                                rel: 'date'
668                                        )
669                                        break
670                                case 'ONTOLOGYTERM':
671                                        // @see http://www.bioontology.org/wiki/index.php/NCBO_Widgets#Term-selection_field_on_a_form
672                                        // @see ontology-chooser.js, table-editor.js
673                                        //out << it.getClass()
674                                        out << textField(
675                                                name: attrs.name + '_' + it.escapedName(),
676                                                value: fieldValue,
677                                                rel: 'ontology-all-name',
678                                                size: 100
679                                        )
680                                        out << hiddenField(
681                                                name: attrs.name + '_' + it.escapedName() + '-concept_id'
682                                        )
683                                        out << hiddenField(
684                                                name: attrs.name + '_' + it.escapedName() + '-ontology_id'
685                                        )
686                                        out << hiddenField(
687                                                name: attrs.name + '_' + it.escapedName() + '-full_id'
688                                        )
689                                        break
690                                default:
691                                        // unsupported field type
692                                        out << '<span class="warning">!' + it.type + '</span>'
693                                        break
694                        }
695
696                        out << '</div>'
697                }
698        }
699
700        /**
701         * render form elements based on an entity's template
702         * @param Map attributes
703         * @param String body
704         */
705        def templateElements = {attrs ->
706                def entity = (attrs.get('entity'))
707                def template = (entity && entity instanceof TemplateEntity) ? entity.template : null
708
709                // got a template?
710                if (template) {
711                        // render template fields
712                        template.fields.each() {
713                                def fieldValue = entity.getFieldValue(it.name)
714
715                                switch (it.type.toString()) {
716                                        case ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'DOUBLE']:
717                                                out << textFieldElement(
718                                                        description: it.name,
719                                                        name: it.escapedName(),
720                                                        value: fieldValue
721                                                )
722                                                break
723                                        case 'STRINGLIST':
724                                                if (!it.listEntries.isEmpty()) {
725                                                        out << selectElement(
726                                                                description: it.name,
727                                                                name: it.escapedName(),
728                                                                from: it.listEntries,
729                                                                value: fieldValue
730                                                        )
731                                                } else {
732                                                        out << '<span class="warning">no values!!</span>'
733                                                }
734                                                break
735                                        case 'ONTOLOGYTERM':
736                                                // @see http://www.bioontology.org/wiki/index.php/NCBO_Widgets#Term-selection_field_on_a_form
737                                                // @see ontology-chooser.js
738                                                out << textFieldElement(
739                                                        name: it.escapedName(),
740                                                        value: fieldValue,
741                                                        rel: 'ontology-all-name',
742                                                        size: 100
743                                                )
744                                                out << hiddenField(
745                                                        name: it.name + '-concept_id',
746                                                        value: fieldValue
747                                                )
748                                                out << hiddenField(
749                                                        name: it.escapedName() + '-ontology_id',
750                                                        value: fieldValue
751                                                )
752                                                out << hiddenField(
753                                                        name: it.escapedName() + '-full_id',
754                                                        value: fieldValue
755                                                )
756                                                break
757                                        case 'DATE':
758                                                out << dateElement(
759                                                        description: it.name,
760                                                        name: it.escapedName(),
761                                                        value: fieldValue
762                                                )
763                                                break
764                                        default:
765                                                out << "unkown field type '" + it.type + "'<br/>"
766                                                break
767                                }
768                        }
769                }
770        }
771}
Note: See TracBrowser for help on using the repository browser.