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.
- data/CHANGELOG +94 -0
- data/README +24 -0
- data/lib/action_controller.rb +2 -0
- data/lib/action_controller/assertions/action_pack_assertions.rb +1 -1
- data/lib/action_controller/base.rb +15 -2
- data/lib/action_controller/caching.rb +6 -16
- data/lib/action_controller/components.rb +1 -1
- data/lib/action_controller/flash.rb +125 -29
- data/lib/action_controller/pagination.rb +378 -0
- data/lib/action_controller/request.rb +13 -6
- data/lib/action_controller/routing.rb +37 -3
- data/lib/action_controller/test_process.rb +7 -3
- data/lib/action_controller/url_rewriter.rb +5 -4
- data/lib/action_view/helpers/asset_tag_helper.rb +35 -4
- data/lib/action_view/helpers/capture_helper.rb +95 -0
- data/lib/action_view/helpers/form_helper.rb +1 -1
- data/lib/action_view/helpers/form_options_helper.rb +2 -0
- data/lib/action_view/helpers/form_tag_helper.rb +28 -10
- data/lib/action_view/helpers/javascript_helper.rb +192 -0
- data/lib/action_view/helpers/javascripts/prototype.js +336 -0
- data/lib/action_view/helpers/pagination_helper.rb +71 -0
- data/lib/action_view/helpers/tag_helper.rb +2 -1
- data/lib/action_view/helpers/text_helper.rb +15 -2
- data/lib/action_view/helpers/url_helper.rb +3 -20
- data/lib/action_view/partials.rb +4 -2
- data/rakefile +2 -2
- data/test/controller/action_pack_assertions_test.rb +1 -2
- data/test/controller/flash_test.rb +30 -5
- data/test/controller/request_test.rb +33 -10
- data/test/controller/routing_tests.rb +26 -0
- data/test/template/asset_tag_helper_test.rb +87 -2
- data/test/template/form_helper_test.rb +1 -0
- data/test/template/form_options_helper_test.rb +11 -0
- data/test/template/form_tag_helper_test.rb +84 -16
- data/test/template/tag_helper_test.rb +2 -15
- data/test/template/text_helper_test.rb +6 -0
- data/test/template/url_helper_test.rb +13 -18
- metadata +10 -5
- 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
|
-
|
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
|
-
|
56
|
-
|
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| (
|
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'].
|
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
|
-
|
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]
|
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}=#{
|
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 =
|
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 =
|
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}").
|
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[
|
20
|
+
if html_options["multipart"]
|
18
21
|
html_options["enctype"] = "multipart/form-data"
|
19
|
-
html_options.delete(
|
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
|
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
|
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.
|
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.
|
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
|
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
|
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
|
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
|
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
|