1 package org
.codehaus
.groovy
.grails
.plugins
.web
.taglib
3 /* Copyright 2004-2005 the original author or authors.
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT c;pWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 import org
.springframework
.validation
.Errors
;
18 import org
.springframework
.context
.NoSuchMessageException
;
19 import org
.springframework
.web
.servlet
.support
.RequestContextUtils as RCU
;
20 import org
.codehaus
.groovy
.grails
.commons
.GrailsClassUtils as GCU
;
23 * A tag lib that provides tags for developing javascript and ajax applications
25 * @author Graeme Rocher
28 class JavascriptTagLib
{
31 * Mappings to the relevant files to be included for each library
33 static final INCLUDED_LIBRARIES
= "org.codehaus.grails.INCLUDED_JS_LIBRARIES"
34 static final INCLUDED_JS
= "org.codehaus.grails.INCLUDED_JS"
35 static final CONTROLLER
= "org.codehaus.groovy.grails.CONTROLLER"
36 static final LIBRARY_MAPPINGS
= [
37 prototype
: ['prototype/prototype']
41 LIBRARY_MAPPINGS
.scriptaculous
= LIBRARY_MAPPINGS
.prototype
+ ['prototype/scriptaculous','prototype/builder','prototype/controls','prototype/effects','prototype/slider','prototype/dragdrop']
42 LIBRARY_MAPPINGS
.rico
= LIBRARY_MAPPINGS
.prototype
+ ['prototype/rico']
45 static final PROVIDER_MAPPINGS
= [
46 prototype
: PrototypeProvider
.class,
47 scriptaculous
: PrototypeProvider
.class,
48 rico
: PrototypeProvider
.class
52 * Includes a javascript src file, library or inline script
53 * if the tag has no 'src' or 'library' attributes its assumed to be an inline script:
55 * <g:javascript>alert('hello')</g:javascript>
57 * The 'library' attribute will attempt to use the library mappings defined above to import the
58 * right js files and not duplicate imports eg.
60 * <g:javascript library="scripaculous" /> // imports all the necessary js for the scriptaculous library
62 * The 'src' attribute will merely import the js file but within the right context (ie inside the /js/ directory of
63 * the Grails application:
65 * <g:javascript src="myscript.js" /> // actually imports '/app/js/myscript.js'
67 def javascript
= { attrs
, body
->
68 setUpRequestAttributes();
69 def requestPluginContext
= request
[CONTROLLER
]?
.pluginContextPath
71 javascriptInclude(attrs
)
73 else if(attrs
.library
) {
75 if(LIBRARY_MAPPINGS
.containsKey(attrs
.library
)) {
76 if(!request
[INCLUDED_LIBRARIES
].contains(attrs
.library
)) {
77 LIBRARY_MAPPINGS
[attrs
.library
].each
{
78 if(!request
[INCLUDED_JS
].contains(it
)) {
79 request
[INCLUDED_JS
] << it
80 def newattrs
= [:] + attrs
81 newattrs
.src
= it
+'.js'
82 javascriptInclude(newattrs
)
85 request
[INCLUDED_LIBRARIES
] << attrs
.library
89 if(!request
[INCLUDED_LIBRARIES
].contains(attrs
.library
)) {
90 def newattrs
= [:] + attrs
91 newattrs
.src
= newattrs
.remove('library')+'.js'
92 javascriptInclude(newattrs
)
93 request
[INCLUDED_LIBRARIES
] << attrs
.library
94 request
[INCLUDED_JS
] << attrs
.library
99 out
.println
'<script type="text/javascript">'
101 out
.println
'</script>'
105 private javascriptInclude(attrs
) {
106 def requestPluginContext
= request
[CONTROLLER
]?
.pluginContextPath
107 out
<< '<script type="text/javascript" src="'
109 def baseUri
= grailsAttributes
.getApplicationUri(request
)
111 out
<< (baseUri
.endsWith('/') ?
'' : '/')
112 if (requestPluginContext
) {
113 out
<< (requestPluginContext
.startsWith("/") ? requestPluginContext
.substring(1) : requestPluginContext
)
121 out
.println
'"></script>'
125 * Creates a remote function call using the prototype library
127 def remoteFunction
= { attrs
->
128 // before remote function
131 out
<< "${attrs.remove('before')};"
133 after
= "${attrs.remove('after')};"
135 def p
= getProvider()
136 p
.doRemoteFunction(owner
,attrs
,out
)
137 attrs
.remove('update')
138 // after remote function
143 private setUpRequestAttributes(){
144 if(!request
[INCLUDED_JS
]) request
[INCLUDED_JS
] = []
145 if(!request
[INCLUDED_LIBRARIES
]) request
[INCLUDED_LIBRARIES
] = []
148 * Normal map implementation does a shallow clone. This implements a deep clone for maps
151 private deepClone(Map map
) {
154 if(v
instanceof Map
) {
155 cloned
[k
] = deepClone(v
)
164 * A link to a remote uri that used the prototype library to invoke the link via ajax
166 def remoteLink
= { attrs
, body
->
169 def cloned
= deepClone(attrs
)
170 out
<< createLink(cloned
)
172 out
<< "\" onclick=\""
173 // create remote function
174 out
<< remoteFunction(attrs
)
176 out
<< "return false;\""
177 // process remaining attributes
179 out
<< ' ' << k
<< "=\"" << v
<< "\""
190 * A field that sends its value to a remote link
192 def remoteField
= { attrs
, body
->
193 def paramName
= attrs
.paramName ? attrs
.remove('paramName') : 'value'
194 def value
= attrs
.remove('value')
195 if(!value
) value
= ''
197 out
<< "<input type=\"text\" name=\"${attrs.remove('name')}\" value=\"${value}\" onkeyup=\""
200 if(attrs
.params
instanceof Map
) {
201 attrs
.params
.put(paramName
, new JavascriptValue('this.value'))
204 attrs
.params
+= "+'${paramName}='+this.value"
208 attrs
.params
= "'${paramName}='+this.value"
210 out
<< remoteFunction(attrs
)
211 attrs
.remove('params')
221 * A form which used prototype to serialize its parameters and submit via an asynchronous ajax call
223 def formRemote
= { attrs
, body
->
225 throwTagError("Tag [formRemote] is missing required attribute [name]")
228 throwTagError("Tag [formRemote] is missing required attribute [url]")
231 // 'formRemote' does not support the 'params' attribute.
232 if(attrs
.params
!= null) {
234 Tag [formRemote] does not support the [params] attribute - add\
235 a 'params' key to the [url] attribute instead.""")
238 // get javascript provider
239 def p
= getProvider()
240 def url
= deepClone(attrs
.url
)
242 // prepare form settings
243 p
.prepareAjaxForm(attrs
)
245 def params
= [ onsubmit
:remoteFunction(attrs
) + 'return false',
246 method
: (attrs
.method? attrs
.method
: 'POST' ),
247 action
: (attrs
.action? attrs
.action
: createLink(url
))
251 if(params
.name
&& !params
.id
)
252 params
.id
= params
.name
253 out
<< withTag(name
:'form',attrs
:params
) {
259 * Creates a form submit button that submits the current form to a remote ajax call
261 def submitToRemote
= { attrs
, body
->
262 // get javascript provider
263 def p
= getProvider()
264 // prepare form settings
265 attrs
.forSubmitTag
= ".form"
266 p
.prepareAjaxForm(attrs
)
267 def params
= [ onclick
:remoteFunction(attrs
) + 'return false',
269 name
: attrs
.remove('name'),
270 value
: attrs
.remove('value'),
271 id
: attrs
.remove('id'),
272 'class':attrs
.remove('class')
275 out
<< withTag(name
:'input', attrs
:params
) {
281 * Escapes a javasacript string replacing single/double quotes and new lines
283 * <g:escapeJavascript>This is some "text" to be escaped</g:escapeJavascript>
285 def escapeJavascript
= { attrs
,body
->
287 if(body
instanceof Closure
) {
289 def sw
= new StringWriter()
290 out
= new PrintWriter(out
)
298 else if(body
instanceof String
) {
301 else if(attrs
instanceof String
) {
304 out
<< js
.replaceAll(/\r\n|\n|\r/, '\\\\n')
305 .replaceAll('"','\\\\"')
306 .replaceAll("'","\\\\'")
309 def setProvider
= { attrs
, body
->
310 if (request
[JavascriptTagLib
.INCLUDED_LIBRARIES
] == null) {
311 request
[JavascriptTagLib
.INCLUDED_LIBRARIES
] = []
313 request
[JavascriptTagLib
.INCLUDED_LIBRARIES
] << attrs
.library
317 * Returns the provider of the necessary function calls to perform Javascript functions
320 private JavascriptProvider
getProvider() {
321 setUpRequestAttributes()
322 def providerClass
= PROVIDER_MAPPINGS
.find
{ request
[JavascriptTagLib
.INCLUDED_LIBRARIES
]?
.contains(it
.key
) }?
.value
323 if (providerClass
== null) {
324 providerClass
= PrototypeProvider
.class
326 return providerClass
.newInstance()
330 * Interface defining methods that a JavaScript provider should implement
332 * @author Graeme Rocher
334 interface JavascriptProvider
{
336 * Creates a remote function call
338 * @param The attributes to use
339 * @param The output to write to
341 def
doRemoteFunction(taglib
,attrs
, out
)
343 def
prepareAjaxForm(attrs
)
346 class JavascriptValue
{
349 public JavascriptValue(value
) {
353 public String
toString() {
360 * Prototype implementation of JavaScript provider
362 * @author Graeme Rocher
364 class PrototypeProvider
implements JavascriptProvider
{
365 def
doRemoteFunction(taglib
,attrs
, out
) {
369 if(attrs
.update
instanceof Map
) {
372 if(attrs
.update?
.success
) {
373 update
<< "success:'${attrs['update']['success']}'"
375 if(attrs
.update?
.failure
) {
376 update
<< "failure:'${attrs['update']['failure']}'"
378 out
<< update
.join(',')
382 out
<< "'" << attrs
.update
<< "',"
384 attrs
.remove("update")
391 //def pms = attrs.remove('params')
393 def jsParams
= attrs
.params?
.findAll
{ it
.value
instanceof JavascriptValue
}
395 jsParams?
.each
{ attrs
.params?
.remove(it
.key
) }
400 url
= taglib
.createLink(attrs
.url
)
403 url
= taglib
.createLink(attrs
)
406 if(!attrs
.params
) attrs
.params
= [:]
407 jsParams?
.each
{ attrs
.params
[it
.key
] = it
.value
}
410 def i
= url?
.indexOf('?')
413 if(attrs
.params
instanceof String
) {
414 attrs
.params
+= "+'&${url[i+1..-1].encodeAsJavaScript()}'"
416 else if(attrs
.params
instanceof Map
) {
417 def params
= createQueryString(attrs
.params
)
418 attrs
.params
= "'${params}${params ? '&' : ''}${url[i+1..-1].encodeAsJavaScript()}'"
421 attrs
.params
= "'${url[i+1..-1].encodeAsJavaScript()}'"
429 /* We have removed these currently and are using full URLs to prevent duplication of parameters
435 out
<< getAjaxOptions(attrs
)
438 attrs
.remove('params')
441 private String
createQueryString(params
) {
443 for (entry in params
) {
444 def value
= entry
.value
446 if (value
instanceof JavascriptValue
) {
447 allParams
<< "${key.encodeAsURL()}='+${value.value}+'"
450 allParams
<< "${key.encodeAsURL()}=${value.encodeAsURL()}".encodeAsJavaScript()
453 if(allParams
.size() == 1) {
457 return allParams
.join('&')
461 // helper function to build ajax options
462 def
getAjaxOptions(options
) {
465 // necessary defaults
466 def optionsAttr
= options
.remove('options')
467 def async
= optionsAttr?
.remove('asynchronous')
469 ajaxOptions
<< "asynchronous:${async}"
471 ajaxOptions
<< "asynchronous:true"
473 def eval
= optionsAttr?
.remove('evalScripts')
475 ajaxOptions
<< "evalScripts:${eval}"
477 ajaxOptions
<< "evalScripts:true"
481 def callbacks
= options
.findAll
{ k
,v
->
482 k
==~
/on(\p{Upper
}|\d){1}\w+/
484 callbacks
.each
{ k
,v
->
485 ajaxOptions
<< "${k}:function(e){${v}}"
489 def params
= options
.remove('params')
490 if (params
instanceof Map
) {
491 params
= createQueryString(params
)
493 ajaxOptions
<< "parameters:${params}"
497 optionsAttr?
.each
{ k
, v
->
500 case 'true': ajaxOptions
<< "${k}:${v}"; break;
501 case 'false': ajaxOptions
<< "${k}:${v}"; break;
502 case ~
/\s*function(\w*)\s*/
: ajaxOptions
<< "${k}:${v}"; break;
503 case ~
/Insertion\
..*/
: ajaxOptions
<< "${k}:${v}"; break;
504 default:ajaxOptions
<< "${k}:'${v}'"; break;
509 return "{${ajaxOptions.join(',')}}"
512 def
prepareAjaxForm(attrs
) {
513 if(!attrs
.forSubmitTag
) attrs
.forSubmitTag
= ""
515 attrs
.params
= "Form.serialize(this${attrs.remove('forSubmitTag')})".toString()