GRAILS-1019: Allowing expressions to be used with the 'disabled' attribute for g...
[grails.git] / src / groovy / org / codehaus / groovy / grails / plugins / web / taglib / JavascriptTagLib.groovy
blob107b9f21a4b98d1a678c9ca78a53af6d5527eb4f
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;
22 /**
23 * A tag lib that provides tags for developing javascript and ajax applications
25 * @author Graeme Rocher
26 * @since 17-Jan-2006
28 class JavascriptTagLib {
30 /**
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']
40 static {
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
51 /**
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'
66 **/
67 def javascript = { attrs, body ->
68 setUpRequestAttributes();
69 def requestPluginContext = request[CONTROLLER]?.pluginContextPath
70 if(attrs.src) {
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
88 else {
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
98 else {
99 out.println '<script type="text/javascript">'
100 out.println body()
101 out.println '</script>'
105 private javascriptInclude(attrs) {
106 def requestPluginContext = request[CONTROLLER]?.pluginContextPath
107 out << '<script type="text/javascript" src="'
108 if (!attrs.base) {
109 def baseUri = grailsAttributes.getApplicationUri(request)
110 out << baseUri
111 out << (baseUri.endsWith('/') ? '' : '/')
112 if (requestPluginContext) {
113 out << (requestPluginContext.startsWith("/") ? requestPluginContext.substring(1) : requestPluginContext)
114 out << "/"
116 out << 'js/'
117 } else {
118 out << attrs.base
120 out << attrs.src
121 out.println '"></script>'
125 * Creates a remote function call using the prototype library
127 def remoteFunction = { attrs ->
128 // before remote function
129 def after = ''
130 if(attrs["before"])
131 out << "${attrs.remove('before')};"
132 if(attrs["after"])
133 after = "${attrs.remove('after')};"
135 def p = getProvider()
136 p.doRemoteFunction(owner,attrs,out)
137 attrs.remove('update')
138 // after remote function
139 if(after)
140 out << after
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
149 * using recursion
151 private deepClone(Map map) {
152 def cloned = [:]
153 map?.each { k,v ->
154 if(v instanceof Map) {
155 cloned[k] = deepClone(v)
157 else {
158 cloned[k] = v
161 return cloned
164 * A link to a remote uri that used the prototype library to invoke the link via ajax
166 def remoteLink = { attrs, body ->
167 out << "<a href=\""
169 def cloned = deepClone(attrs)
170 out << createLink(cloned)
172 out << "\" onclick=\""
173 // create remote function
174 out << remoteFunction(attrs)
175 attrs.remove('url')
176 out << "return false;\""
177 // process remaining attributes
178 attrs.each { k,v ->
179 out << ' ' << k << "=\"" << v << "\""
181 out << ">"
182 // output the body
183 out << body()
185 // close tag
186 out << "</a>"
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=\""
199 if(attrs.params) {
200 if(attrs.params instanceof Map) {
201 attrs.params.put(paramName, new JavascriptValue('this.value'))
203 else {
204 attrs.params += "+'${paramName}='+this.value"
207 else {
208 attrs.params = "'${paramName}='+this.value"
210 out << remoteFunction(attrs)
211 attrs.remove('params')
212 out << "\""
213 attrs.remove('url')
214 attrs.each { k,v->
215 out << " $k=\"$v\""
217 out <<" />"
221 * A form which used prototype to serialize its parameters and submit via an asynchronous ajax call
223 def formRemote = { attrs, body ->
224 if(!attrs.name) {
225 throwTagError("Tag [formRemote] is missing required attribute [name]")
227 if(!attrs.url) {
228 throwTagError("Tag [formRemote] is missing required attribute [url]")
231 // 'formRemote' does not support the 'params' attribute.
232 if(attrs.params != null) {
233 throwTagError("""\
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))
249 attrs.remove('url')
250 params.putAll(attrs)
251 if(params.name && !params.id)
252 params.id = params.name
253 out << withTag(name:'form',attrs:params) {
254 out << body()
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',
268 type: 'button',
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) {
276 out << body()
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 ->
286 def js = ''
287 if(body instanceof Closure) {
288 def tmp = out
289 def sw = new StringWriter()
290 out = new PrintWriter(out)
291 // invoke body
292 out << body()
293 // restore out
294 out = tmp
295 js = sw.toString()
298 else if(body instanceof String) {
299 js = body
301 else if(attrs instanceof String) {
302 js = attrs
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 {
347 def value
349 public JavascriptValue(value) {
350 this.value = value
353 public String toString() {
354 return "'+$value+'"
360 * Prototype implementation of JavaScript provider
362 * @author Graeme Rocher
364 class PrototypeProvider implements JavascriptProvider {
365 def doRemoteFunction(taglib,attrs, out) {
366 out << 'new Ajax.'
367 if(attrs.update) {
368 out << 'Updater('
369 if(attrs.update instanceof Map) {
370 out << "{"
371 def update = []
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(',')
379 out << "},"
381 else {
382 out << "'" << attrs.update << "',"
384 attrs.remove("update")
386 else {
387 out << "Request("
389 out << "'"
391 //def pms = attrs.remove('params')
392 def url
393 def jsParams = attrs.params?.findAll { it.value instanceof JavascriptValue }
395 jsParams?.each { attrs.params?.remove(it.key) }
399 if(attrs.url) {
400 url = taglib.createLink(attrs.url)
402 else {
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('?')
412 if(i >-1) {
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()}'"
420 else {
421 attrs.params = "'${url[i+1..-1].encodeAsJavaScript()}'"
423 out << url[0..i-1]
425 else {
426 out << url
428 out << "',"
429 /* We have removed these currently and are using full URLs to prevent duplication of parameters
430 as per GRAILS-2045
431 if(pms)
432 attrs.params = pms
434 // process options
435 out << getAjaxOptions(attrs)
436 // close
437 out << ');'
438 attrs.remove('params')
441 private String createQueryString(params) {
442 def allParams = []
443 for (entry in params) {
444 def value = entry.value
445 def key = entry.key
446 if (value instanceof JavascriptValue) {
447 allParams << "${key.encodeAsURL()}='+${value.value}+'"
449 else {
450 allParams << "${key.encodeAsURL()}=${value.encodeAsURL()}".encodeAsJavaScript()
453 if(allParams.size() == 1) {
454 return allParams[0]
456 else {
457 return allParams.join('&')
461 // helper function to build ajax options
462 def getAjaxOptions(options) {
463 def ajaxOptions = []
465 // necessary defaults
466 def optionsAttr = options.remove('options')
467 def async = optionsAttr?.remove('asynchronous')
468 if( async != null)
469 ajaxOptions << "asynchronous:${async}"
470 else
471 ajaxOptions << "asynchronous:true"
473 def eval = optionsAttr?.remove('evalScripts')
474 if(eval != null)
475 ajaxOptions << "evalScripts:${eval}"
476 else
477 ajaxOptions << "evalScripts:true"
479 if(options) {
480 // process callbacks
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}}"
486 options.remove(k)
488 if(options.params) {
489 def params = options.remove('params')
490 if (params instanceof Map) {
491 params = createQueryString(params)
493 ajaxOptions << "parameters:${params}"
496 // remaining options
497 optionsAttr?.each { k, v ->
498 if(k!='url') {
499 switch(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()