root/trunk/grails-app/taglib/dbnp/studycapturing/WizardTagLib.groovy @ 367

Revision 367, 20.2 KB (checked in by duh, 4 years ago)

- commented buggy bootstrap code
- refactored wizard subject page
- removed wizard:speciesElement from taglibrary
- changed speciesElement to termElement
- used SelectAddMore?.js to add dialog functionality to template select elements
- changed bug report icon
- added more famfamfam icons

  • Property svn:keywords set to Date Author Rev
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$
16 * $Author$
17 * $Date$
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                def helpText = help().trim()
215
216                // got an ajax onchange action?
217                def ajaxOnChange = attrs.remove('ajaxOnChange')
218                if (ajaxOnChange) {
219                        if (!attrs.onChange) attrs.onChange = ''
220
221                        // add onChange AjaxSubmit javascript
222                        attrs.onChange += ajaxSubmitJs(
223                                [
224                                        functionName: ajaxOnChange,
225                                        url: attrs.get('url'),
226                                        update: attrs.get('update'),
227                                        afterSuccess: attrs.get('afterSuccess')
228                                ],
229                                ''
230                        )
231                }
232
233                // execute inputElement call
234                def renderedElement = "$inputElement"(attrs)
235
236                // if false, then we skip this element
237                if (!renderedElement) return false
238
239                // render a form element
240                out << '<div class="element">'
241                out << ' <div class="description">'
242                out << description
243                out << ' </div>'
244                out << ' <div class="input">'
245                out << renderedElement
246                if (helpText.size() > 0) {
247                        out << '        <div class="helpIcon"></div>'
248                }
249
250                // add an disabled input box for feedback purposes
251                // @see dateElement(...)
252                if (addExampleElement) {
253                        def exampleAttrs = new LinkedHashMap()
254                        exampleAttrs.name = attrs.get('name') + 'Example'
255                        exampleAttrs.class = 'isExample'
256                        exampleAttrs.disabled = 'disabled'
257                        exampleAttrs.size = 30
258                        out << textField(exampleAttrs)
259                }
260
261                // add an disabled input box for feedback purposes
262                // @see dateElement(...)
263                if (addExample2Element) {
264                        def exampleAttrs = new LinkedHashMap()
265                        exampleAttrs.name = attrs.get('name') + 'Example2'
266                        exampleAttrs.class = 'isExample'
267                        exampleAttrs.disabled = 'disabled'
268                        exampleAttrs.size = 30
269                        out << textField(exampleAttrs)
270                }
271
272                out << ' </div>'
273
274                // add help content if it is available
275                if (helpText.size() > 0) {
276                        out << '  <div class="helpContent">'
277                        out << '    ' + helpText
278                        out << '  </div>'
279                }
280
281                out << '</div>'
282        }
283
284        /**
285         * render an ajaxButtonElement
286         * @param Map attrs
287         * @param Closure body  (help text)
288         */
289        def ajaxButtonElement = { attrs, body ->
290                baseElement.call(
291                        'ajaxButton',
292                        attrs,
293                        body
294                )
295        }
296
297        /**
298         * render a textFieldElement
299         * @param Map attrs
300         * @param Closure body  (help text)
301         */
302        def textFieldElement = {attrs, body ->
303                // set default size, or scale to max length if it is less than the default size
304                if (!attrs.get("size")) {
305                        if (attrs.get("maxlength")) {
306                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
307                        } else {
308                                attrs.size = defaultTextFieldSize
309                        }
310                }
311
312                // render template element
313                baseElement.call(
314                        'textField',
315                        attrs,
316                        body
317                )
318        }
319
320        /**
321         * render a select form element
322         * @param Map attrs
323         * @param Closure body  (help text)
324         */
325        def selectElement = {attrs, body ->
326                baseElement.call(
327                        'select',
328                        attrs,
329                        body
330                )
331        }
332
333        /**
334         * render a checkBox form element
335         * @param Map attrs
336         * @param Closure body  (help text)
337         */
338        def checkBoxElement = {attrs, body ->
339                baseElement.call(
340                        'checkBox',
341                        attrs,
342                        body
343                )
344        }
345
346        /**
347         * render a dateElement
348         * NOTE: datepicker is attached through wizard.js!
349         * @param Map attrs
350         * @param Closure body  (help text)
351         */
352        def dateElement = {attrs, body ->
353                // transform value?
354                if (attrs.value instanceof Date) {
355                        // transform date instance to formatted string (dd/mm/yyyy)
356                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
357                }
358
359                // add 'rel' field to identity the datefield using javascript
360                attrs.rel = 'date'
361
362                // set some textfield values
363                attrs.maxlength = (attrs.maxlength) ? attrs.maxlength : 10
364                attrs.addExampleElement = true
365
366                // render a normal text field
367                //out << textFieldElement(attrs,body)
368                textFieldElement.call(
369                        attrs,
370                        body
371                )
372        }
373
374        /**
375         * render a dateElement
376         * NOTE: datepicker is attached through wizard.js!
377         * @param Map attrs
378         * @param Closure body  (help text)
379         */
380        def timeElement = {attrs, body ->
381                // transform value?
382                if (attrs.value instanceof Date) {
383                        // transform date instance to formatted string (dd/mm/yyyy)
384                        attrs.value = String.format('%td/%<tm/%<tY %<tH:%<tM', attrs.value)
385                }
386
387                // add 'rel' field to identity the field using javascript
388                attrs.rel = 'datetime'
389
390                attrs.addExampleElement = true
391                attrs.addExample2Element = true
392                attrs.maxlength = 16
393
394                // render a normal text field
395                //out << textFieldElement(attrs,body)
396                textFieldElement.call(
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        /**
418         * Term form element
419         * @param Map attributes
420         * @param Closure help content
421         */
422        def termElement = { attrs, body ->
423                // render term element
424                baseElement.call(
425                        'termSelect',
426                        attrs,
427                        body
428                )
429        }
430
431        /**
432         * Term select element
433         * @param Map attributes
434         */
435        def termSelect = { attrs ->
436                def from = []
437
438                // got ontologies?
439                if (attrs.ontology) {
440                        attrs.ontology.split(/\,/).each() { ncboId ->
441                                // trim the id
442                                ncboId.trim()
443                               
444                                // fetch all terms for this ontology
445                                def ontology = Ontology.findAllByNcboId(ncboId)
446
447                                // does this ontology exist?
448                                if (ontology) {
449                                        ontology.each() {
450                                                Term.findAllByOntology(it).each() {
451                                                        // key = ncboId:concept-id
452                                                        from[ from.size() ] = it.name
453                                                }
454                                        }
455                                }
456                        }
457
458                        // sort alphabetically
459                        from.sort()
460                       
461                        // define 'from'
462                        attrs.from = from
463
464                        // add 'rel' attribute
465                        attrs.rel = 'term'
466
467                        out << select(attrs)
468                } else {
469                        out << "you should specify: <i>ontology=\"id\"</i> or <i>ontology=\"id1,id2,...,idN\"</i>"
470                }
471        }
472
473        /**
474         * Ontology form element
475         * @param Map attributes
476         * @param Closure help content
477         */
478        def ontologyElement = { attrs, body ->
479                // @see http://www.bioontology.org/wiki/index.php/NCBO_Widgets#Term-selection_field_on_a_form
480                // @see ontology-chooser.js, table-editor.js
481                baseElement.call(
482                        'textField',
483                        [
484                            name: attrs.name,
485                                value: attrs.value,
486                                description: attrs.description,
487                                rel: 'ontology-' + ((attrs.ontology) ? attrs.ontology : 'all') + '-name',
488                                size: 25
489                        ],
490                        body
491                )
492                out << hiddenField(
493                        name: attrs.name + '-concept_id'
494                )
495                out << hiddenField(
496                        name: attrs.name + '-ontology_id'
497                )
498                out << hiddenField(
499                        name: attrs.name + '-full_id'
500                )
501        }
502
503        /**
504         * Study form element
505         * @param Map attributes
506         * @param Closure help content
507         */
508        def studyElement = { attrs, body ->
509                // render study element
510                baseElement.call(
511                        'studySelect',
512                        attrs,
513                        body
514                )
515        }
516
517        /**
518         * render a study select element
519         * @param Map attrs
520         */
521        def studySelect = { attrs ->
522                // for now, just fetch all studies
523                attrs.from = Study.findAll()
524
525                // got a name?
526                if (!attrs.name) {
527                        attrs.name = "study"
528                }
529
530                // got result?
531                if (attrs.from.size() > 0) {
532                        out << select(attrs)
533                } else {
534                        // no, return false to make sure this element
535                        // is not rendered in the template
536                        return false
537                }
538        }
539
540        /**
541         * Template form element
542         * @param Map attributes
543         * @param Closure help content
544         */
545        def templateElement = {attrs, body ->
546                // add a rel element if it does not exist
547                if (!attrs.rel) {
548                        attrs.rel = 'template'
549                }
550               
551                // render template element
552                baseElement.call(
553                        'templateSelect',
554                        attrs,
555                        body
556                )
557        }
558
559        /**
560         * render a template select element
561         * @param Map attrs
562         */
563        def templateSelect = {attrs ->
564                def entity = attrs.remove('entity')
565
566                // fetch templates
567                if (attrs.remove('addDummy')) {
568                        attrs.from = ['']
569                        if (entity && entity instanceof Class) {
570                                Template.findAllByEntity(entity).each() {
571                                        attrs.from[attrs.from.size()] = it
572                                }
573                        }
574                } else {
575                        attrs.from = (entity) ? Template.findAllByEntity(entity) : Template.findAll()
576                }
577
578                // got a name?
579                if (!attrs.name) {
580                        attrs.name = 'template'
581                }
582
583                // got result?
584                if (attrs.from.size() > 0) {
585                        out << select(attrs)
586                } else {
587                        // no, return false to make sure this element
588                        // is not rendered in the template
589                        return false
590                }
591        }
592
593        /**
594         * Protocol form element
595         * @param Map attributes
596         * @param Closure help content
597         */
598        def protocolElement = {attrs, body ->
599                // render protocol element
600                baseElement.call(
601                        'protocolSelect',
602                        attrs,
603                        body
604                )
605        }
606
607        /**
608         * render a protocol select element
609         * @param Map attrs
610         */
611        def protocolSelect = {attrs ->
612                // fetch all protocold
613                attrs.from = Protocol.findAll() // for now, all protocols
614
615                // got a name?
616                if (!attrs.name) {
617                        attrs.name = 'protocol'
618                }
619
620                out << select(attrs)
621        }
622
623        def show = {attrs ->
624                // is object parameter set?
625                def o = attrs.object
626
627                println o.getProperties();
628                o.getProperties().each {
629                        println it
630                }
631
632                out << "!! test version of 'show' tag !!"
633        }
634
635        /**
636         * render table headers for all subjectFields in a template
637         * @param Map attributes
638         */
639        def templateColumnHeaders = {attrs ->
640                def template = attrs.remove('template')
641
642                // output table headers for template fields
643                template.fields.each() {
644                        out << '<div class="' + attrs.get('class') + '">' + it + '</div>'
645                }
646        }
647
648        /**
649         * render table input elements for all subjectFields in a template
650         * @param Map attributes
651         */
652        def templateColumns = {attrs, body ->
653                def subject = attrs.remove('subject')
654                def subjectId = attrs.remove('id')
655                def template = attrs.remove('template')
656                def intFields = subject.templateIntegerFields
657                def stringFields = subject.templateStringFields
658                def floatFields = subject.templateFloatFields
659                def termFields = subject.templateTermFields
660
661                // output columns for these subjectFields
662                template.fields.each() {
663                        def fieldValue = subject.getFieldValue(it.name)
664
665                        // output div
666                        out << '<div class="' + attrs.get('class') + '">'
667
668                        // handle field types
669                        switch (it.type.toString()) {
670                                case ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'DOUBLE']:
671                                        out << textField(
672                                                name: attrs.name + '_' + it.escapedName(),
673                                                value: fieldValue
674                                        )
675                                        break
676                                case 'STRINGLIST':
677                                        // render stringlist subjectfield
678                                        if (!it.listEntries.isEmpty()) {
679                                                out << select(
680                                                        name: attrs.name + '_' + it.escapedName(),
681                                                        from: it.listEntries,
682                                                        value: fieldValue
683                                                )
684                                        } else {
685                                                out << '<span class="warning">no values!!</span>'
686                                        }
687                                        break
688                                case 'DATE':
689                                        // transform value?
690                                        if (fieldValue instanceof Date) {
691                                                if (fieldValue.getHours() == 0 && fieldValue.getMinutes() == 0) {
692                                                        // transform date instance to formatted string (dd/mm/yyyy)
693                                                        fieldValue = String.format('%td/%<tm/%<tY', fieldValue)
694                                                } else {
695                                                        // transform to date + time
696                                                        fieldValue = String.format('%td/%<tm/%<tY %<tH:%<tM', fieldValue)
697                                                }
698                                        }
699
700                                        // output a date field (not the 'rel' which makes the
701                                        // javascript front-end bind the jquery-ui datepicker)
702                                        out << textField(
703                                                name: attrs.name + '_' + it.escapedName(),
704                                                value: fieldValue,
705                                                rel: 'date'
706                                        )
707                                        break
708                                case 'ONTOLOGYTERM':
709                                        // @see http://www.bioontology.org/wiki/index.php/NCBO_Widgets#Term-selection_field_on_a_form
710                                        // @see ontology-chooser.js, table-editor.js
711                                        //out << it.getClass()
712                                        out << textField(
713                                                name: attrs.name + '_' + it.escapedName(),
714                                                value: fieldValue,
715                                                rel: 'ontology-all-name',
716                                                size: 100
717                                        )
718                                        out << hiddenField(
719                                                name: attrs.name + '_' + it.escapedName() + '-concept_id'
720                                        )
721                                        out << hiddenField(
722                                                name: attrs.name + '_' + it.escapedName() + '-ontology_id'
723                                        )
724                                        out << hiddenField(
725                                                name: attrs.name + '_' + it.escapedName() + '-full_id'
726                                        )
727                                        break
728                                default:
729                                        // unsupported field type
730                                        out << '<span class="warning">!' + it.type + '</span>'
731                                        break
732                        }
733
734                        out << '</div>'
735                }
736        }
737
738        /**
739         * render form elements based on an entity's template
740         * @param Map attributes
741         * @param String body
742         */
743        def templateElements = {attrs ->
744                def entity = (attrs.get('entity'))
745                def template = (entity && entity instanceof TemplateEntity) ? entity.template : null
746
747                // got a template?
748                if (template) {
749                        // render template fields
750                        template.fields.each() {
751                                def fieldValue = entity.getFieldValue(it.name)
752
753                                switch (it.type.toString()) {
754                                        case ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'DOUBLE']:
755                                                out << textFieldElement(
756                                                        description: it.name,
757                                                        name: it.escapedName(),
758                                                        value: fieldValue
759                                                )
760                                                break
761                                        case 'STRINGLIST':
762                                                if (!it.listEntries.isEmpty()) {
763                                                        out << selectElement(
764                                                                description: it.name,
765                                                                name: it.escapedName(),
766                                                                from: it.listEntries,
767                                                                value: fieldValue
768                                                        )
769                                                } else {
770                                                        out << '<span class="warning">no values!!</span>'
771                                                }
772                                                break
773                                        case 'ONTOLOGYTERM':
774                                                // @see http://www.bioontology.org/wiki/index.php/NCBO_Widgets#Term-selection_field_on_a_form
775                                                // @see ontology-chooser.js
776                                                out << textFieldElement(
777                                                        name: it.escapedName(),
778                                                        value: fieldValue,
779                                                        rel: 'ontology-all-name',
780                                                        size: 100
781                                                )
782                                                out << hiddenField(
783                                                        name: it.name + '-concept_id',
784                                                        value: fieldValue
785                                                )
786                                                out << hiddenField(
787                                                        name: it.escapedName() + '-ontology_id',
788                                                        value: fieldValue
789                                                )
790                                                out << hiddenField(
791                                                        name: it.escapedName() + '-full_id',
792                                                        value: fieldValue
793                                                )
794                                                break
795                                        case 'DATE':
796                                                out << dateElement(
797                                                        description: it.name,
798                                                        name: it.escapedName(),
799                                                        value: fieldValue
800                                                )
801                                                break
802                                        default:
803                                                out << "unkown field type '" + it.type + "'<br/>"
804                                                break
805                                }
806                        }
807                }
808        }
809}
Note: See TracBrowser for help on using the browser.