| 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 | * 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 | /** |
|---|
| 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[] |
|---|
| 397 | //def domainValidator = new RegexValidator("localhost(:(\\d{1,5}))?") |
|---|
| 398 | def domainValidator = new RegexValidator(".*(:(\\d{1,5}))?") // Any domain, incl user@host:port |
|---|
| 399 | def validator = new UrlValidator(schemes, domainValidator, UrlValidator.ALLOW_2_SLASHES) |
|---|
| 400 | return validator.isValid(url) |
|---|
| 401 | |
|---|
| 402 | } |
|---|
| 403 | |
|---|
| 404 | } // end class |
|---|