1 | //import org.apache.commons.validator.UrlValidator |
---|
2 | import org.codehaus.groovy.grails.validation.routines.UrlValidator |
---|
3 | import 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 | */ |
---|
9 | class 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 mb = new groovy.xml.MarkupBuilder(out) |
---|
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 | mb.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 | mb.script(type: "text/javascript") { |
---|
370 | mkp.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 |
---|