actionpack 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (39) hide show
  1. data/CHANGELOG +94 -0
  2. data/README +24 -0
  3. data/lib/action_controller.rb +2 -0
  4. data/lib/action_controller/assertions/action_pack_assertions.rb +1 -1
  5. data/lib/action_controller/base.rb +15 -2
  6. data/lib/action_controller/caching.rb +6 -16
  7. data/lib/action_controller/components.rb +1 -1
  8. data/lib/action_controller/flash.rb +125 -29
  9. data/lib/action_controller/pagination.rb +378 -0
  10. data/lib/action_controller/request.rb +13 -6
  11. data/lib/action_controller/routing.rb +37 -3
  12. data/lib/action_controller/test_process.rb +7 -3
  13. data/lib/action_controller/url_rewriter.rb +5 -4
  14. data/lib/action_view/helpers/asset_tag_helper.rb +35 -4
  15. data/lib/action_view/helpers/capture_helper.rb +95 -0
  16. data/lib/action_view/helpers/form_helper.rb +1 -1
  17. data/lib/action_view/helpers/form_options_helper.rb +2 -0
  18. data/lib/action_view/helpers/form_tag_helper.rb +28 -10
  19. data/lib/action_view/helpers/javascript_helper.rb +192 -0
  20. data/lib/action_view/helpers/javascripts/prototype.js +336 -0
  21. data/lib/action_view/helpers/pagination_helper.rb +71 -0
  22. data/lib/action_view/helpers/tag_helper.rb +2 -1
  23. data/lib/action_view/helpers/text_helper.rb +15 -2
  24. data/lib/action_view/helpers/url_helper.rb +3 -20
  25. data/lib/action_view/partials.rb +4 -2
  26. data/rakefile +2 -2
  27. data/test/controller/action_pack_assertions_test.rb +1 -2
  28. data/test/controller/flash_test.rb +30 -5
  29. data/test/controller/request_test.rb +33 -10
  30. data/test/controller/routing_tests.rb +26 -0
  31. data/test/template/asset_tag_helper_test.rb +87 -2
  32. data/test/template/form_helper_test.rb +1 -0
  33. data/test/template/form_options_helper_test.rb +11 -0
  34. data/test/template/form_tag_helper_test.rb +84 -16
  35. data/test/template/tag_helper_test.rb +2 -15
  36. data/test/template/text_helper_test.rb +6 -0
  37. data/test/template/url_helper_test.rb +13 -18
  38. metadata +10 -5
  39. data/test/controller/url_obsolete.rb.rej +0 -747
@@ -77,10 +77,6 @@ module ActionController
77
77
  (%r{^\w+\://[^/]+(/.*|$)$} =~ env['REQUEST_URI']) ? $1 : env['REQUEST_URI'] # Remove domain, which webrick puts into the request_uri.
78
78
  end
79
79
 
80
- def path_info
81
- (/^(.*)\.html$/ =~ env['PATH_INFO']) ? $1 : env['PATH_INFO']
82
- end
83
-
84
80
  def protocol
85
81
  env["HTTPS"] == "on" ? 'https://' : 'http://'
86
82
  end
@@ -88,9 +84,20 @@ module ActionController
88
84
  def ssl?
89
85
  protocol == 'https://'
90
86
  end
91
-
87
+
88
+ # returns the interpreted path to requested resource after
89
+ # all the installation directory of this application was taken into account
92
90
  def path
93
- (path_info && !path_info.empty?) ? path_info : (request_uri ? request_uri.split('?').first : '')
91
+ path = request_uri ? request_uri.split('?').first : ''
92
+
93
+ # cut off the part of the url which leads to the installation directory of this app
94
+ path[relative_url_root.length..-1]
95
+ end
96
+
97
+ # returns the path minus the web server relative
98
+ # installation directory
99
+ def relative_url_root
100
+ File.dirname(env["SCRIPT_NAME"].to_s).gsub /(^\.$|^\/$)/, ''
94
101
  end
95
102
 
96
103
  def port
@@ -47,13 +47,35 @@ module ActionController
47
47
 
48
48
  used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash} # Mark requirements as used so they don't get put in the query params
49
49
  components = @items.collect do |item|
50
+
50
51
  if item.kind_of? Symbol
52
+ collection = false
53
+
54
+ if /^\*/ =~ item.to_s
55
+ collection = true
56
+ item = item.to_s.sub(/^\*/,"").intern
57
+ end
58
+
51
59
  used_names[item] = true
52
60
  value = options[item] || defaults[item] || @defaults[item]
53
61
  return nil, requirements_for(item) unless passes_requirements?(item, value)
62
+
54
63
  defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
55
- (value.nil? || item == :controller) ? value : CGI.escape(value.to_s)
56
- else item
64
+
65
+ if value.nil? || item == :controller
66
+ value
67
+ elsif collection
68
+ if value.kind_of?(Array)
69
+ value = value.collect {|v| Routing.extract_parameter_value(v)}.join('/')
70
+ else
71
+ value = Routing.extract_parameter_value(value).gsub(/%2F/, "/")
72
+ end
73
+ value
74
+ else
75
+ Routing.extract_parameter_value(value)
76
+ end
77
+ else
78
+ item
57
79
  end
58
80
  end
59
81
 
@@ -96,6 +118,12 @@ module ActionController
96
118
  end
97
119
  options[:controller] = controller_class.controller_path
98
120
  return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller])
121
+ elsif /^\*/ =~ item.to_s
122
+ value = components.empty? ? @defaults[item].clone : components.clone
123
+ value.collect! {|c| CGI.unescape c}
124
+ components = []
125
+ def value.to_s() self.join('/') end
126
+ options[item.to_s.sub(/^\*/,"").intern] = value
99
127
  elsif item.kind_of? Symbol
100
128
  value = components.shift || @defaults[item]
101
129
  return nil, requirements_for(item) unless passes_requirements?(item, value)
@@ -142,7 +170,7 @@ module ActionController
142
170
  end
143
171
 
144
172
  def items=(path)
145
- items = path.split('/').collect {|c| (/^:(\w+)$/ =~ c) ? $1.intern : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
173
+ items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
146
174
  items.shift if items.first == ""
147
175
  items.pop if items.last == ""
148
176
  @items = items
@@ -172,6 +200,7 @@ module ActionController
172
200
  end
173
201
  end
174
202
  def requirements_for(name)
203
+ name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
175
204
  presence = (@defaults.key?(name) && @defaults[name].nil?)
176
205
  requirement = case @requirements[name]
177
206
  when nil then nil
@@ -295,6 +324,11 @@ module ActionController
295
324
  end
296
325
  end
297
326
 
327
+ def self.extract_parameter_value(parameter) #:nodoc:
328
+ value = (parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s
329
+ CGI.escape(value)
330
+ end
331
+
298
332
  def self.draw(*args, &block) #:nodoc:
299
333
  Routes.draw(*args) {|*args| block.call(*args)}
300
334
  end
@@ -168,7 +168,7 @@ module ActionController #:nodoc:
168
168
 
169
169
  # do we have a flash?
170
170
  def has_flash?
171
- !session['flash'].nil?
171
+ !session['flash'].empty?
172
172
  end
173
173
 
174
174
  # do we have a flash that has contents?
@@ -277,6 +277,10 @@ module Test
277
277
 
278
278
  get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
279
279
  end
280
- end
280
+
281
+ def assigns(name)
282
+ @response.template.assigns[name.to_s]
283
+ end
284
+ end
281
285
  end
282
- end
286
+ end
@@ -2,13 +2,13 @@ module ActionController
2
2
  # Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
3
3
 
4
4
  class UrlRewriter #:nodoc:
5
- RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol]
5
+ RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :application_prefix]
6
6
  def initialize(request, parameters)
7
7
  @request, @parameters = request, parameters
8
8
  @rewritten_path = @request.path ? @request.path.dup : ""
9
9
  end
10
10
 
11
- def rewrite(options = {})
11
+ def rewrite(options = {})
12
12
  rewrite_url(rewrite_path(options), options)
13
13
  end
14
14
 
@@ -20,11 +20,12 @@ module ActionController
20
20
 
21
21
  private
22
22
  def rewrite_url(path, options)
23
+
23
24
  rewritten_url = ""
24
25
  rewritten_url << (options[:protocol] || @request.protocol) unless options[:only_path]
25
26
  rewritten_url << (options[:host] || @request.host_with_port) unless options[:only_path]
26
27
 
27
- rewritten_url << options[:application_prefix] if options[:application_prefix]
28
+ rewritten_url << (options[:application_prefix] || @request.relative_url_root)
28
29
  rewritten_url << path
29
30
  rewritten_url << "##{options[:anchor]}" if options[:anchor]
30
31
 
@@ -95,7 +96,7 @@ module ActionController
95
96
  key = CGI.escape key
96
97
  key += '[]' if value.class == Array
97
98
  value = [ value ] unless value.class == Array
98
- value.each { |val| elements << "#{key}=#{CGI.escape(val.to_s)}" }
99
+ value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
99
100
  end
100
101
 
101
102
  query_string << ("?" + elements.join("&")) unless elements.empty?
@@ -33,8 +33,7 @@ module ActionView
33
33
  # <script language="JavaScript" type="text/javascript" src="/elsewhere/cools.js"></script>
34
34
  def javascript_include_tag(*sources)
35
35
  sources.collect { |source|
36
- source = "/javascripts/#{source}" unless source.include?("/")
37
- source = "#{source}.js" unless source.include?(".")
36
+ source = compute_public_path(source, 'javascripts', 'js')
38
37
  content_tag("script", "", "language" => "JavaScript", "type" => "text/javascript", "src" => source)
39
38
  }.join("\n")
40
39
  end
@@ -49,11 +48,43 @@ module ActionView
49
48
  # <link href="/css/stylish.css" media="screen" rel="Stylesheet" type="text/css" />
50
49
  def stylesheet_link_tag(*sources)
51
50
  sources.collect { |source|
52
- source = "/stylesheets/#{source}" unless source.include?("/")
53
- source = "#{source}.css" unless source.include?(".")
51
+ source = compute_public_path(source, 'stylesheets', 'css')
54
52
  tag("link", "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source)
55
53
  }.join("\n")
56
54
  end
55
+
56
+ # Returns an image tag converting the +options+ instead html options on the tag, but with these special cases:
57
+ #
58
+ # * <tt>:alt</tt> - If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension)
59
+ # * <tt>:size</tt> - Supplied as "XxY", so "30x45" becomes width="30" and height="45"
60
+ #
61
+ # The +src+ can be supplied as a...
62
+ # * full path, like "/my_images/image.gif"
63
+ # * file name, like "rss.gif", that gets expanded to "/images/rss.gif"
64
+ # * file name without extension, like "logo", that gets expanded to "/images/logo.png"
65
+ def image_tag(source, options = {})
66
+ options.symbolize_keys
67
+
68
+ options[:src] = compute_public_path(source, 'images', 'png')
69
+ options[:alt] ||= source.split("/").last.split(".").first.capitalize
70
+
71
+ if options[:size]
72
+ options[:width], options[:height] = options[:size].split("x")
73
+ options.delete :size
74
+ end
75
+
76
+ tag("img", options)
77
+ end
78
+
79
+ private
80
+
81
+ def compute_public_path(source, dir, ext)
82
+ source = "/#{dir}/#{source}" unless source.include?("/")
83
+ source = "#{source}.#{ext}" unless source.include?(".")
84
+ source = "#{@request.relative_url_root}#{source}"
85
+ source
86
+ end
87
+
57
88
  end
58
89
  end
59
90
  end
@@ -0,0 +1,95 @@
1
+ module ActionView
2
+ module Helpers
3
+ # Capture lets you extract parts of code into instance variables which
4
+ # can be used in other points of the template or even layout file.
5
+ #
6
+ # == Capturing a block into an instance variable
7
+ #
8
+ # <% @script = capture do %>
9
+ # [some html...]
10
+ # <% end %>
11
+ #
12
+ #
13
+ # == Add javascript to header using content_for
14
+ #
15
+ # content_for("name") is a wrapper for capture which will store the
16
+ # fragment in a instance variable similar to @content_for_layout.
17
+ #
18
+ # layout.rhtml:
19
+ #
20
+ # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
21
+ # <head>
22
+ # <title>layout with js</title>
23
+ # <script type="text/javascript">
24
+ # <%= @content_for_script %>
25
+ # </script>
26
+ # </head>
27
+ # <body>
28
+ # <%= @content_for_layout %>
29
+ # </body>
30
+ # </html>
31
+ #
32
+ # view.rhtml
33
+ #
34
+ # This page shows an alert box!
35
+ #
36
+ # <% content_for("script") do %>
37
+ # alert('hello world')
38
+ # <% end %>
39
+ #
40
+ # Normal view text
41
+ module CaptureHelper
42
+ # Capture allows you to extract a part of the template into an
43
+ # instance variable. You can use this instance variable anywhere
44
+ # in your templates and even in your layout.
45
+ #
46
+ # Example:
47
+ #
48
+ # <% @greeting = capture do %>
49
+ # Welcome To my shiny new web page!
50
+ # <% end %>
51
+ def capture(&block)
52
+ # execute the block
53
+ buffer = eval("_erbout", block.binding)
54
+ pos = buffer.length
55
+ block.call
56
+
57
+ # extract the block
58
+ data = buffer[pos..-1]
59
+
60
+ # replace it in the original with empty string
61
+ buffer[pos..-1] = ''
62
+
63
+ data
64
+ end
65
+
66
+ # Content_for will store the given block
67
+ # in an instance variable for later use in another template
68
+ # or in the layout.
69
+ #
70
+ # The name of the instance variable is content_for_<name>
71
+ # to stay consistent with @content_for_layout which is used
72
+ # by ActionView's layouts
73
+ #
74
+ # Example:
75
+ #
76
+ # <% content_for("header") do %>
77
+ # alert('hello world')
78
+ # <% end %>
79
+ #
80
+ # You can use @content_for_header anywhere in your templates
81
+ def content_for(name, &block)
82
+ base = controller.instance_variable_get(instance_var_name(name)) || ""
83
+ data = capture(&block)
84
+ controller.instance_variable_set(instance_var_name(name), base + data)
85
+ data
86
+ end
87
+
88
+ private
89
+
90
+ def instance_var_name(name) #:nodoc#
91
+ "@content_for_#{name}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -152,7 +152,7 @@ module ActionView
152
152
  @object_name, @method_name = object_name, method_name
153
153
  @template_object, @local_binding = template_object, local_binding
154
154
  if @object_name.sub!(/\[\]$/,"")
155
- @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id
155
+ @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast
156
156
  end
157
157
  end
158
158
 
@@ -83,6 +83,7 @@ module ActionView
83
83
  options_for_select = container.inject([]) do |options, element|
84
84
  if element.respond_to?(:first) && element.respond_to?(:last)
85
85
  is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) )
86
+ is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) )
86
87
  if is_selected
87
88
  options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>"
88
89
  else
@@ -90,6 +91,7 @@ module ActionView
90
91
  end
91
92
  else
92
93
  is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) )
94
+ is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) )
93
95
  options << ((is_selected) ? "<option selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option>#{html_escape(element.to_s)}</option>")
94
96
  end
95
97
  end
@@ -5,6 +5,9 @@ module ActionView
5
5
  module Helpers
6
6
  # Provides a number of methods for creating form tags that doesn't rely on conventions with an object assigned to the template like
7
7
  # FormHelper does. With the FormTagHelper, you provide the names and values yourself.
8
+ #
9
+ # NOTE: The html options disabled, readonly, and multiple can all be treated as booleans. So specifying <tt>disabled => :true</tt>
10
+ # will give <tt>disabled="disabled"</tt>.
8
11
  module FormTagHelper
9
12
  # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
10
13
  # ActionController::Base#url_for. The method for the form defaults to POST.
@@ -14,9 +17,9 @@ module ActionView
14
17
  def form_tag(url_for_options = {}, options = {}, *parameters_for_url)
15
18
  html_options = { "method" => "post" }.merge(options.stringify_keys)
16
19
 
17
- if html_options[:multipart]
20
+ if html_options["multipart"]
18
21
  html_options["enctype"] = "multipart/form-data"
19
- html_options.delete(:multipart)
22
+ html_options.delete("multipart")
20
23
  end
21
24
 
22
25
  html_options["action"] = url_for(url_for_options, *parameters_for_url)
@@ -31,11 +34,11 @@ module ActionView
31
34
  end
32
35
 
33
36
  def select_tag(name, option_tags = nil, options = {})
34
- content_tag("select", option_tags, { "name" => name, "id" => name }.update(options.stringify_keys))
37
+ content_tag("select", option_tags, { "name" => name, "id" => name }.update(convert_options(options)))
35
38
  end
36
39
 
37
40
  def text_field_tag(name, value = nil, options = {})
38
- tag("input", { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys))
41
+ tag("input", { "type" => "text", "name" => name, "id" => name, "value" => value }.update(convert_options(options)))
39
42
  end
40
43
 
41
44
  def hidden_field_tag(name, value = nil, options = {})
@@ -43,11 +46,11 @@ module ActionView
43
46
  end
44
47
 
45
48
  def file_field_tag(name, options = {})
46
- text_field_tag(name, nil, options.stringify_keys.update("type" => "file"))
49
+ text_field_tag(name, nil, convert_options(options).update("type" => "file"))
47
50
  end
48
51
 
49
52
  def password_field_tag(name = "password", value = nil, options = {})
50
- text_field_tag(name, value, options.stringify_keys.update("type" => "password"))
53
+ text_field_tag(name, value, convert_options(options).update("type" => "password"))
51
54
  end
52
55
 
53
56
  def text_area_tag(name, content = nil, options = {})
@@ -57,24 +60,39 @@ module ActionView
57
60
  options.delete("size")
58
61
  end
59
62
 
60
- content_tag("textarea", content, { "name" => name, "id" => name }.update(options.stringify_keys))
63
+ content_tag("textarea", content, { "name" => name, "id" => name }.update(convert_options(options)))
61
64
  end
62
65
 
63
66
  def check_box_tag(name, value = "1", checked = false, options = {})
64
- html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
67
+ html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(convert_options(options))
65
68
  html_options["checked"] = "checked" if checked
66
69
  tag("input", html_options)
67
70
  end
68
71
 
69
72
  def radio_button_tag(name, value, checked = false, options = {})
70
- html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
73
+ html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(convert_options(options))
71
74
  html_options["checked"] = "checked" if checked
72
75
  tag("input", html_options)
73
76
  end
74
77
 
75
78
  def submit_tag(value = "Save changes", options = {})
76
- tag("input", { "type" => "submit", "name" => "submit", "value" => value }.update(options.stringify_keys))
79
+ tag("input", { "type" => "submit", "name" => "submit", "value" => value }.update(convert_options(options)))
77
80
  end
81
+
82
+ private
83
+ def convert_options(options)
84
+ options = options.stringify_keys
85
+ %w( disabled readonly multiple ).each { |a| boolean_attribute(options, a) }
86
+ options
87
+ end
88
+
89
+ def boolean_attribute(options, attribute)
90
+ if options[attribute]
91
+ options[attribute] = attribute
92
+ else
93
+ options.delete attribute
94
+ end
95
+ end
78
96
  end
79
97
  end
80
98
  end
@@ -0,0 +1,192 @@
1
+ require File.dirname(__FILE__) + '/tag_helper'
2
+
3
+ module ActionView
4
+ module Helpers
5
+ # Provides a set of helpers for calling Javascript functions and, most importantly, to call remote methods using what has
6
+ # been labelled Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]. This means that you can call
7
+ # actions in your controllers without reloading the page, but still update certain parts of it using injections into the
8
+ # DOM. The common use case is having a form that adds a new element to a list without reloading the page.
9
+ #
10
+ # To be able to use the Javascript helpers, you must either call <tt><%= define_javascript_functions %></tt> (which returns all
11
+ # the Javascript support functions in a <script> block) or reference the Javascript library using
12
+ # <tt><%= javascript_include_tag "prototype" %></tt> (which looks for the library in /javascripts/prototype.js). The latter is
13
+ # recommended as the browser can then cache the library instead of fetching all the functions anew on every request.
14
+ #
15
+ # If you're the visual type, there's an Ajax movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating
16
+ # the use of form_remote_tag.
17
+ module JavascriptHelper
18
+ unless const_defined? :CALLBACKS
19
+ CALLBACKS = [:uninitialized, :loading, :loaded, :interactive, :complete]
20
+ JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
21
+ end
22
+
23
+ # Returns a link that'll trigger a javascript +function+ using the
24
+ # onclick handler and return false after the fact.
25
+ #
26
+ # Examples:
27
+ # link_to_function "Greeting", "alert('Hello world!')"
28
+ # link_to_function(image_tag("delete"), "if confirm('Really?'){ do_delete(); }")
29
+ def link_to_function(name, function, html_options = {})
30
+ content_tag(
31
+ "a", name,
32
+ html_options.symbolize_keys.merge(:href => "#", :onclick => "#{function}; return false;")
33
+ )
34
+ end
35
+
36
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
37
+ # (using the url_for format) that's called in the background using
38
+ # XMLHttpRequest. The result of that request can then be inserted into a
39
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
40
+ # Usually, the result would be a partial prepared by the controller with
41
+ # either render_partial or render_partial_collection.
42
+ #
43
+ # Examples:
44
+ # link_to_remote "Delete this post", :update => "posts", :url => { :action => "destroy", :id => post.id }
45
+ # link_to_remote(image_tag("refresh"), :update => "emails", :url => { :action => "list_emails" })
46
+ #
47
+ # By default, these remote requests are processed asynchronous during
48
+ # which various callbacks can be triggered (for progress indicators and
49
+ # the likes).
50
+ #
51
+ # Example:
52
+ # link_to_remote word,
53
+ # :url => { :action => "undo", :n => word_counter },
54
+ # :complete => "undoRequestCompleted(request)"
55
+ #
56
+ # The callbacks that may be specified are:
57
+ #
58
+ # <tt>:loading</tt>:: Called when the remote document is being
59
+ # loaded with data by the browser.
60
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
61
+ # the remote document.
62
+ # <tt>:interactive</tt>:: Called when the user can interact with the
63
+ # remote document, even though it has not
64
+ # finished loading.
65
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete.
66
+ #
67
+ # If you for some reason or another need synchronous processing (that'll
68
+ # block the browser while the request is happening), you can specify
69
+ # <tt>options[:type] = :synchronous</tt>.
70
+ def link_to_remote(name, options = {}, html_options = {})
71
+ link_to_function(name, remote_function(options), html_options)
72
+ end
73
+
74
+ # Returns a form tag that will submit using XMLHttpRequest in the background instead of the regular
75
+ # reloading POST arrangement. Even though it's using Javascript to serialize the form elements, the form submission
76
+ # will work just like a regular submission as viewed by the receiving side (all elements available in @params).
77
+ # The options for specifying the target with :url and defining callbacks is the same as link_to_remote.
78
+ def form_remote_tag(options = {})
79
+ options[:form] = true
80
+
81
+ options[:html] ||= { }
82
+ options[:html][:onsubmit] = "#{remote_function(options)}; return false;"
83
+
84
+ tag("form", options[:html], true)
85
+ end
86
+
87
+ def remote_function(options) #:nodoc: for now
88
+ javascript_options = options_for_ajax(options)
89
+
90
+ function = options[:update] ?
91
+ "new Ajax.Updater('#{options[:update]}', " :
92
+ "new Ajax.Request("
93
+
94
+ function << "'#{url_for(options[:url])}'"
95
+ function << ", #{javascript_options})"
96
+
97
+ function = "#{options[:before]}; #{function}" if options[:before]
98
+ function = "#{function}; #{options[:after]}" if options[:after]
99
+ function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
100
+
101
+ return function
102
+ end
103
+
104
+ # Includes the Action Pack Javascript library inside a single <script>
105
+ # tag.
106
+ #
107
+ # Note: The recommended approach is to copy the contents of
108
+ # lib/action_view/helpers/javascripts/ into your application's
109
+ # public/javascripts/ directory, and use +javascript_include_tag+ to
110
+ # create remote <script> links.
111
+ def define_javascript_functions
112
+ javascript = '<script type="text/javascript">'
113
+ Dir.glob(File.join(JAVASCRIPT_PATH, '*')).each do |filename|
114
+ javascript << "\n" << IO.read(filename)
115
+ end
116
+ javascript << '</script>'
117
+ end
118
+
119
+ # Observes the field with the DOM ID specified by +field_id+ and makes
120
+ # an Ajax when its contents have changed.
121
+ #
122
+ # Required +options+ are:
123
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
124
+ # this field will be detected.
125
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
126
+ # when the field has changed.
127
+ #
128
+ # Additional options are:
129
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
130
+ # innerHTML should be updated with the
131
+ # XMLHttpRequest response text.
132
+ # <tt>:with</tt>:: A Javascript expression specifying the
133
+ # parameters for the XMLHttpRequest. This defaults
134
+ # to 'value', which in the evaluated context
135
+ # refers to the new field value.
136
+ #
137
+ # Additionally, you may specify any of the options documented in
138
+ # +link_to_remote.
139
+ def observe_field(field_id, options = {})
140
+ build_observer('Form.Element.Observer', field_id, options)
141
+ end
142
+
143
+ # Like +observe_field+, but operates on an entire form identified by the
144
+ # DOM ID +form_id+. +options+ are the same as +observe_field+, except
145
+ # the default value of the <tt>:with</tt> option evaluates to the
146
+ # serialized (request string) value of the form.
147
+ def observe_form(form_id, options = {})
148
+ build_observer('Form.Observer', form_id, options)
149
+ end
150
+
151
+ private
152
+ def escape_javascript(javascript)
153
+ (javascript || '').gsub('"', '\"')
154
+ end
155
+
156
+ def options_for_ajax(options)
157
+ js_options = build_callbacks(options)
158
+
159
+ js_options['asynchronous'] = options[:type] != :synchronous
160
+ js_options['method'] = options[:method] if options[:method]
161
+
162
+ if options[:form]
163
+ js_options['parameters'] = 'Form.serialize(this)'
164
+ elsif options[:with]
165
+ js_options['parameters'] = options[:with]
166
+ end
167
+
168
+ '{' + js_options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
169
+ end
170
+
171
+ def build_observer(klass, name, options = {})
172
+ options[:with] ||= 'value' if options[:update]
173
+ callback = remote_function(options)
174
+ javascript = '<script type="text/javascript">'
175
+ javascript << "new #{klass}('#{name}', "
176
+ javascript << "#{options[:frequency]}, function(element, value) {"
177
+ javascript << "#{callback}})</script>"
178
+ end
179
+
180
+ def build_callbacks(options)
181
+ CALLBACKS.inject({}) do |callbacks, callback|
182
+ if options[callback]
183
+ name = 'on' + callback.to_s.capitalize
184
+ code = escape_javascript(options[callback])
185
+ callbacks[name] = "function(request){#{code}}"
186
+ end
187
+ callbacks
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end