source: trunk/grails-app/taglib/CustomTagLib.groovy @ 911

Last change on this file since 911 was 829, checked in by gav, 14 years ago

Create custom taglib of helpBalloon to allow their usage in ajax rendered templates.

File size: 14.0 KB
Line 
1//import org.apache.commons.validator.UrlValidator
2import org.codehaus.groovy.grails.validation.routines.UrlValidator
3import org.codehaus.groovy.grails.validation.routines.RegexValidator
4
5/**
6* General use custom tags.
7* Some are taken from http://www.grails.org/Contribute+a+Tag#checkBoxList
8*/
9class CustomTagLib {
10    static namespace = 'custom'
11
12    private static final Object helpBalloonLockable = new Object();
13    private static long helpBalloonCount = 0L;
14
15    def resources = { attrs ->
16        ///@todo: should include our javascript and do setup here.
17    }
18
19    /**
20    * Checkbox list that can be used as a more user-friendly alternative to a multiselect list box.
21     * Usage:
22     * To map the selected ids to corresponding domain objects,
23     * an additional set method is required in the containing domain class:
24     *       //  This additional setter is used to convert the checkBoxList string or string array
25     *       //  of ids selected to the corresponding domain objects.
26     *       public void setAssetSubItemsFromCheckBoxList(ids) {
27     *           def idList = []
28     *           if(ids instanceof String) {
29     *                   if(ids.isInteger())
30     *                       idList << ids.toInteger()
31     *           }
32     *           else {
33     *               ids.each() {
34     *                   if(it.isInteger())
35     *                       idList << it.toInteger()
36     *               }
37     *           }
38     *           this.assetSubItems = idList.collect { AssetSubItem.get( it ) }
39     *       }
40     *
41     * Then a line in the controller:
42     *      assetInstance.setAssetSubItemsFromCheckBoxList(params.assetSubItems)
43     *
44     * Fields:
45     *    name - the property name.
46     *    from - the list to select from.
47     *    value - the current value.
48     *    optionKey - the key to use.
49     *    sortBy - (optional) the attribute to sort the from list by.
50     *    displayFields - (optional) defaults to the objects toString()
51     *    displayFieldsSeparator - (optional) defaults to a space.
52     *    linkController - (optional, requires linkAction.) the controller to use for a link to the objects in the checkBoxList.
53     *    linkAction - (optional, requires linkController.) the action to use for a link to the objects in the checkBoxList.
54     *
55     * Example:
56     *    <!--
57     *    <custom:checkBoxList name="assetSubItems"
58     *                                    from="${AssetSubItem.list()}"
59     *                                    value="${assetInstance?.assetSubItems.collect{it.id}}"
60     *                                    optionKey="id"
61     *                                    sortBy="description"
62     *                                    displayFields="['id', 'name']"
63     *                                    displayFieldsSeparator=', '
64     *                                    linkController="assetSubItemDetailed"
65     *                                    linkAction="show"/>
66     *    -->
67     *
68     */
69
70    def checkBoxList = {attrs, body ->
71
72        def from = attrs.from
73        def value = attrs.value
74        def cname = attrs.name
75        def isChecked, ht, wd, style, html
76
77        def sortBy = attrs.sortBy
78        def displayFields = attrs.displayFields
79        def displayFieldsSeparator = attrs.displayFieldsSeparator ?: ' '
80        def linkController = attrs.linkController
81        def linkAction = attrs.linkAction
82
83        def displayValue = " "
84
85        // sets the style to override height and/or width if either of them
86        // is specified, else the default from the CSS is taken
87        style = "style='"
88        if(attrs.height)
89            style += "height:${attrs.height};"
90        if(attrs.width)
91            style += "width:${attrs.width};"
92        if(style.length() == "style='".length())
93            style = ""
94        else
95            style += "'" // closing single quote
96
97        html = "<ul class='CheckBoxList' " + style + ">"
98
99        out << html
100
101        if(sortBy)
102            from.sort { p1, p2 -> p1[sortBy].compareToIgnoreCase(p2[sortBy]) }
103
104        from.each { obj ->
105
106            displayValue = " "
107
108            if(linkController && linkAction)
109                   displayValue += "<a href=\"${createLink(controller: linkController, action: linkAction, id: obj.id).encodeAsHTML()}\">"
110
111            if(displayFields) {
112                displayValue += displayFields.collect { obj[it] }.join(displayFieldsSeparator)
113            }
114            else displayValue += obj // use the obj's default toString()
115
116            if(linkController && linkAction)
117                displayValue += "</a>"
118
119            // if we wanted to select the checkbox using a click anywhere on the label (also hover effect)
120            // but grails does not recognize index suffix in the name as an array:
121            // cname = "${attrs.name}[${idx++}]"
122            // and put this inside the li: <label for='$cname'>...</label>
123
124            isChecked = (value?.contains(obj."${attrs.optionKey}"))? true: false
125
126            out << "<li>" << checkBox(name:cname, value:obj."${attrs.optionKey}", checked: isChecked) << displayValue << "</li>"
127        }
128
129        out << "</ul>"
130
131    } // checkBoxList
132
133    def sortableColumnWithImg = { attrs, body ->
134        def writer = out
135        if(!attrs.property)
136            throwTagError("Tag [sortableColumn] is missing required attribute [property]")
137
138//         if(!attrs.title && !attrs.titleKey)
139//             throwTagError("Tag [sortableColumn] is missing required attribute [title] or [titleKey]")
140
141        def property = attrs.remove("property")
142        def action = attrs.action ? attrs.remove("action") : (actionName ?: "list")
143
144        def defaultOrder = attrs.remove("defaultOrder")
145        if(defaultOrder != "desc") defaultOrder = "asc"
146
147        // current sorting property and order
148        def sort = params.sort
149        def order = params.order
150
151        // add sorting property and params to link params
152        def linkParams = [:]
153        if(params.id) linkParams.put("id",params.id)
154        if(attrs.params) linkParams.putAll(attrs.remove("params"))
155        linkParams.sort = property
156
157        // determine and add sorting order for this column to link params
158        attrs.class = (attrs.class ? "${attrs.class} sortable" : "sortable")
159        if(property == sort) {
160            attrs.class = attrs.class + " sorted " + order
161            if(order == "asc")
162                linkParams.order = "desc"
163            else
164                linkParams.order = "asc"
165        }
166        else
167            linkParams.order = defaultOrder
168
169        // determine column title
170//         def title = attrs.remove("title")
171//         def titleKey = attrs.remove("titleKey")
172//         if(titleKey) {
173//             if(!title) title = titleKey
174//             def messageSource = grailsAttributes.getApplicationContext().getBean("messageSource")
175//             def locale = RCU.getLocale(request)
176//
177//             title = messageSource.getMessage(titleKey, null, title, locale)
178//         }
179
180        // Image.
181        def img = "<img "
182        def imgAttrs = [:]
183        imgAttrs.src = attrs.remove("imgSrc")
184        imgAttrs.alt = attrs.remove("imgAlt")
185        imgAttrs.title = attrs.remove("imgTitle")
186        imgAttrs.each { k, v ->
187            if(v)
188                img += "${k}=\"${v.encodeAsHTML()}\" "
189        }
190        img += "/>"
191
192        writer << "<th "
193
194        // process remaining attributes
195        attrs.each { k, v ->
196            writer << "${k}=\"${v.encodeAsHTML()}\" "
197        }
198        writer << ">${link(action:action, params:linkParams) { img } }"
199        writer << "</th>"
200
201    } // sortableColumnWithImg
202
203    /**
204    * Customised version of jasperButton as found in jaser plugin.
205     * custom:jasperButtons is intended to be wrapped by g:jasperForm
206     */
207    def jasperButtons = {attrs ->
208        if(!attrs['format']){throw new Exception(message(code:"jasper.taglib.missingAttribute", args:'format'))}
209        if(!attrs['jasper']){throw new Exception(message(code:"jasper.taglib.missingAttribute", args:'jasper'))}
210        String jasper = attrs['jasper']
211        String buttonClass = attrs['class'] ?:  "jasperButton"
212        String format = attrs['format'].toUpperCase()
213        String text = attrs['text']
214        String heightAttr = attrs['height'] ? ' height="' + attrs['height'] + '"' : '' // leading space on purpose
215        String imgSrc = ''
216        String delimiter = attrs['delimiter'] ?: "|"
217        String delimiterBefore = attrs['delimiterBefore'] ?: delimiter
218        String delimiterAfter = attrs['delimiterAfter'] ?: delimiter
219
220        out << '''
221                    <script type="text/javascript">
222                        function submit_jasperForm(name, fmt) {
223                            var jasperForm = document.getElementsByName(name).item(0)
224                            jasperForm._format.value = fmt;
225                            jasperForm.submit();
226                            return false;
227                        }
228                    </script>
229                    '''
230
231        out << delimiterBefore
232
233        attrs['format'].toUpperCase().split(",").eachWithIndex { it, i ->
234            if (i > 0) out << delimiter
235            imgSrc = g.resource(plugin:"jasper", dir:'images/icons', file:"${it.trim()}.gif")
236            def fmt = it.trim()
237            out << """
238                        <a href="#" class="${buttonClass}" title="${it.trim()}" onClick="return submit_jasperForm('${jasper}', '${fmt}')">
239                        <img border="0" src="${imgSrc}"${heightAttr} /></a>
240                        """
241        }
242
243        out << delimiterAfter
244    } // jasperButtons
245
246    /**
247    * Easily create a link from a hopeful url string.
248    * If the supplied url is not considered a valid url the string is simply displayed.
249    *
250    * Fields:
251    *  url - String url to use.
252    *  body - If no body is supplied in the GSP then url.encodeAsHTML() is displayed.
253    *
254    * Example:
255    * <!--
256    * <custom:easyUrl url="${docRef.location}" />
257    * -->
258    */
259    def easyUrl = {attrs, body ->
260
261        def url = attrs.url
262
263        def html
264
265        body = body()
266        if(!body)
267            body = url.encodeAsHTML()
268
269        html = "${body}"
270
271        if(isURLValid(url)) {
272            html = """
273                        <a href="${url}">
274                            ${html}
275                        </a>
276                        """
277        }
278
279        out << html
280    }
281
282    /**
283    * Get a list of the machines assigned on a taskProcedureRevision.
284    *
285    * Fields:
286    *  taskProcedureRevision - TaskProcedureRevision to use.
287    *
288    * Example:
289    * <!--
290    * <custom:taskProcedureMachines taskProcedureRevision="${taskInstance.taskProcedureRevision}" />
291    * -->
292    */
293    def taskProcedureMachines = {attrs ->
294        def taskProcedureRevision = attrs.taskProcedureRevision
295        def machines = taskProcedureRevision.maintenanceActions.collect {it.assetSubItem.parentItem}.unique()
296        out << machines.encodeAsHTML()
297    }
298
299    /**
300    * Customised version of helpBalloon as found in help-balloon plugin.
301    * This version can be used in ajax rendered templates where the original fails.
302    *
303    * Fields added:
304    * iconId - Optional to specify the anchor elements id, by default an id is generated if iconSrc is supplied.
305    * iconSrc - Optional to specify anchor image src, if not supplied no anchor image is output by the taglib.
306    *
307    * Example:
308    * <!--
309    * <custom:helpBalloon code="entry.date.done" iconSrc="${resource(plugin:'help-balloons', dir:'images', file:'balloon-icon.gif')}"/>
310    * or
311    * <a href="#" id="mynewanchor" onclick="return false;">this</a>
312    * <custom:helpBalloon code="entry.date.done" iconId="mynewanchor" />
313    * -->
314    */
315    def helpBalloon = {attrs, body ->
316        def mkp = new groovy.xml.MarkupBuilder(out) //this line will be unnecessary in versions of Grails after version 1.2
317
318        def title = attrs["title"]
319        def content = attrs["content"]
320        def code = attrs["code"]
321        def suffix = attrs["suffix"] ?: ".help"
322        def encodeAs = attrs["encodeAs"]
323        def iconId = attrs["iconId"]
324        def iconSrc =  attrs["iconSrc"]
325
326        if (!title && code) title = g.message(code: code)
327        if (!content && code) content = g.message(code: code + suffix)
328
329        title = title ?: ""
330        content = content ?: ""
331
332        if (encodeAs) {
333            switch (encodeAs.toUpperCase()) {
334
335                case "HTML":
336                    title = title.encodeAsHTML()
337                    content = content.encodeAsHTML()
338                    break
339
340                case "XML":
341                    title = title.encodeAsXML()
342                    content = content.encodeAsXML()
343                    break
344            }
345        }
346
347        def num
348        synchronized (helpBalloonLockable) {
349            num = helpBalloonCount++;
350        }
351
352        if(iconSrc) {
353            iconId = iconId ?: "customHb$num"
354            mkp.img( id: iconId, src: iconSrc)
355        }
356
357        def javascript = """var customHb$num = new HelpBalloon({
358    title: '${title.encodeAsJavaScript()}',
359    content: '${content.encodeAsJavaScript()}'"""
360
361        if(iconId) {
362        javascript +=""",
363    icon: \$('$iconId')"""
364        }
365
366        javascript += """
367});"""
368
369        mkp.script(type: "text/javascript") {
370            yieldUnescaped(javascript)
371        }
372
373    }
374
375    /**
376    * Determine if a supplied string is considered a url or not.
377    * The scheme/protocol can be adjusted, file:// has been excluded here.
378    * A domainValidator regex is used to allow localhost domain.
379    * A Grails branched version of commons.validator is used, this should
380    * be compatible with the apache 1.4 version release.
381    * See: http://jira.codehaus.org/browse/GRAILS-1692 for more on Grails url validation.
382    */
383    private Boolean isURLValid(url) {
384
385        def schemes = ["http","https", "ftp"] as String[]
386        //def domainValidator = new RegexValidator("localhost(:(\\d{1,5}))?")
387        def domainValidator = new RegexValidator(".*(:(\\d{1,5}))?") // Any domain, incl user@host:port
388        def validator = new UrlValidator(schemes, domainValidator, UrlValidator.ALLOW_2_SLASHES)
389        return validator.isValid(url)
390
391    }
392
393} // end class
Note: See TracBrowser for help on using the repository browser.