[798] | 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 |
---|
[286] | 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 | |
---|
[829] | 12 | private static final Object helpBalloonLockable = new Object(); |
---|
| 13 | private static long helpBalloonCount = 0L; |
---|
| 14 | |
---|
[286] | 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: |
---|
[343] | 24 | * // This additional setter is used to convert the checkBoxList string or string array |
---|
[286] | 25 | * // of ids selected to the corresponding domain objects. |
---|
| 26 | * public void setAssetSubItemsFromCheckBoxList(ids) { |
---|
| 27 | * def idList = [] |
---|
[343] | 28 | * if(ids instanceof String) { |
---|
| 29 | * if(ids.isInteger()) |
---|
| 30 | * idList << ids.toInteger() |
---|
[286] | 31 | * } |
---|
[343] | 32 | * else { |
---|
| 33 | * ids.each() { |
---|
| 34 | * if(it.isInteger()) |
---|
| 35 | * idList << it.toInteger() |
---|
| 36 | * } |
---|
| 37 | * } |
---|
[286] | 38 | * this.assetSubItems = idList.collect { AssetSubItem.get( it ) } |
---|
| 39 | * } |
---|
[303] | 40 | * |
---|
[286] | 41 | * Then a line in the controller: |
---|
| 42 | * assetInstance.setAssetSubItemsFromCheckBoxList(params.assetSubItems) |
---|
[303] | 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. |
---|
[343] | 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. |
---|
[303] | 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" |
---|
[343] | 61 | * sortBy="description" |
---|
[303] | 62 | * displayFields="['id', 'name']" |
---|
[343] | 63 | * displayFieldsSeparator=', ' |
---|
[303] | 64 | * linkController="assetSubItemDetailed" |
---|
| 65 | * linkAction="show"/> |
---|
| 66 | * --> |
---|
| 67 | * |
---|
[286] | 68 | */ |
---|
[303] | 69 | |
---|
[286] | 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 | |
---|
[343] | 77 | def sortBy = attrs.sortBy |
---|
[290] | 78 | def displayFields = attrs.displayFields |
---|
[343] | 79 | def displayFieldsSeparator = attrs.displayFieldsSeparator ?: ' ' |
---|
[303] | 80 | def linkController = attrs.linkController |
---|
| 81 | def linkAction = attrs.linkAction |
---|
[290] | 82 | |
---|
| 83 | def displayValue = " " |
---|
| 84 | |
---|
[286] | 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 | |
---|
[343] | 101 | if(sortBy) |
---|
| 102 | from.sort { p1, p2 -> p1[sortBy].compareToIgnoreCase(p2[sortBy]) } |
---|
| 103 | |
---|
[286] | 104 | from.each { obj -> |
---|
| 105 | |
---|
[290] | 106 | displayValue = " " |
---|
| 107 | |
---|
[303] | 108 | if(linkController && linkAction) |
---|
| 109 | displayValue += "<a href=\"${createLink(controller: linkController, action: linkAction, id: obj.id).encodeAsHTML()}\">" |
---|
| 110 | |
---|
[343] | 111 | if(displayFields) { |
---|
| 112 | displayValue += displayFields.collect { obj[it] }.join(displayFieldsSeparator) |
---|
[290] | 113 | } |
---|
[343] | 114 | else displayValue += obj // use the obj's default toString() |
---|
[290] | 115 | |
---|
[303] | 116 | if(linkController && linkAction) |
---|
[343] | 117 | displayValue += "</a>" |
---|
[303] | 118 | |
---|
[286] | 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 | |
---|
[290] | 126 | out << "<li>" << checkBox(name:cname, value:obj."${attrs.optionKey}", checked: isChecked) << displayValue << "</li>" |
---|
[286] | 127 | } |
---|
| 128 | |
---|
| 129 | out << "</ul>" |
---|
| 130 | |
---|
| 131 | } // checkBoxList |
---|
| 132 | |
---|
[417] | 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) |
---|
[932] | 176 | // |
---|
[417] | 177 | // title = messageSource.getMessage(titleKey, null, title, locale) |
---|
| 178 | // } |
---|
| 179 | |
---|
| 180 | // Image. |
---|
[582] | 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 += "/>" |
---|
[417] | 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 | |
---|
[582] | 201 | } // sortableColumnWithImg |
---|
| 202 | |
---|
[667] | 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'))} |
---|
[672] | 209 | if(!attrs['jasper']){throw new Exception(message(code:"jasper.taglib.missingAttribute", args:'jasper'))} |
---|
| 210 | String jasper = attrs['jasper'] |
---|
[667] | 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 << """ |
---|
[672] | 238 | <a href="#" class="${buttonClass}" title="${it.trim()}" onClick="return submit_jasperForm('${jasper}', '${fmt}')"> |
---|
[667] | 239 | <img border="0" src="${imgSrc}"${heightAttr} /></a> |
---|
| 240 | """ |
---|
| 241 | } |
---|
| 242 | |
---|
| 243 | out << delimiterAfter |
---|
| 244 | } // jasperButtons |
---|
| 245 | |
---|
[798] | 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 | /** |
---|
[817] | 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 | /** |
---|
[829] | 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 -> |
---|
[918] | 316 | def mb = new groovy.xml.MarkupBuilder(out) |
---|
[829] | 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" |
---|
[918] | 354 | mb.img(id: iconId, src: iconSrc) |
---|
[829] | 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 | |
---|
[918] | 369 | mb.script(type: "text/javascript") { |
---|
| 370 | mkp.yieldUnescaped(javascript) |
---|
[829] | 371 | } |
---|
| 372 | |
---|
| 373 | } |
---|
| 374 | |
---|
| 375 | /** |
---|
[932] | 376 | * Returns the correct headerId for the main header div. |
---|
| 377 | */ |
---|
| 378 | def headerId = { attrs, body -> |
---|
| 379 | def headerId = '' |
---|
| 380 | if(grails.util.Environment.isDevelopmentMode()) headerId = 'HeaderDev' |
---|
| 381 | else if(grailsApplication.config.demoMode.enabled) headerId = 'HeaderDemo' |
---|
| 382 | else headerId = 'Header' |
---|
| 383 | out << headerId |
---|
| 384 | } |
---|
| 385 | |
---|
| 386 | /** |
---|
[798] | 387 | * Determine if a supplied string is considered a url or not. |
---|
| 388 | * The scheme/protocol can be adjusted, file:// has been excluded here. |
---|
| 389 | * A domainValidator regex is used to allow localhost domain. |
---|
| 390 | * A Grails branched version of commons.validator is used, this should |
---|
| 391 | * be compatible with the apache 1.4 version release. |
---|
| 392 | * See: http://jira.codehaus.org/browse/GRAILS-1692 for more on Grails url validation. |
---|
| 393 | */ |
---|
| 394 | private Boolean isURLValid(url) { |
---|
| 395 | |
---|
| 396 | def schemes = ["http","https", "ftp"] as String[] |
---|
[808] | 397 | //def domainValidator = new RegexValidator("localhost(:(\\d{1,5}))?") |
---|
| 398 | def domainValidator = new RegexValidator(".*(:(\\d{1,5}))?") // Any domain, incl user@host:port |
---|
[798] | 399 | def validator = new UrlValidator(schemes, domainValidator, UrlValidator.ALLOW_2_SLASHES) |
---|
| 400 | return validator.isValid(url) |
---|
| 401 | |
---|
| 402 | } |
---|
| 403 | |
---|
[667] | 404 | } // end class |
---|