actionpack 1.8.1 → 1.9.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 +309 -16
- data/README +1 -1
- data/lib/action_controller.rb +5 -0
- data/lib/action_controller/assertions.rb +57 -12
- data/lib/action_controller/auto_complete.rb +47 -0
- data/lib/action_controller/base.rb +288 -258
- data/lib/action_controller/benchmarking.rb +8 -3
- data/lib/action_controller/caching.rb +88 -42
- data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
- data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
- data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
- data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
- data/lib/action_controller/cgi_process.rb +39 -11
- data/lib/action_controller/code_generation.rb +235 -0
- data/lib/action_controller/cookies.rb +14 -8
- data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
- data/lib/action_controller/filters.rb +8 -7
- data/lib/action_controller/helpers.rb +41 -6
- data/lib/action_controller/layout.rb +45 -16
- data/lib/action_controller/request.rb +86 -23
- data/lib/action_controller/rescue.rb +1 -0
- data/lib/action_controller/response.rb +1 -1
- data/lib/action_controller/routing.rb +536 -272
- data/lib/action_controller/scaffolding.rb +30 -25
- data/lib/action_controller/session/active_record_store.rb +251 -50
- data/lib/action_controller/streaming.rb +133 -0
- data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
- data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
- data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
- data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
- data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
- data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
- data/lib/action_controller/test_process.rb +68 -47
- data/lib/action_controller/upload_progress.rb +421 -0
- data/lib/action_controller/url_rewriter.rb +8 -11
- data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
- data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
- data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
- data/lib/action_controller/vendor/xml_simple.rb +1019 -0
- data/lib/action_controller/verification.rb +36 -30
- data/lib/action_view/base.rb +21 -14
- data/lib/action_view/helpers/active_record_helper.rb +15 -13
- data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
- data/lib/action_view/helpers/benchmark_helper.rb +24 -0
- data/lib/action_view/helpers/capture_helper.rb +7 -5
- data/lib/action_view/helpers/date_helper.rb +63 -46
- data/lib/action_view/helpers/form_helper.rb +7 -1
- data/lib/action_view/helpers/form_options_helper.rb +19 -11
- data/lib/action_view/helpers/form_tag_helper.rb +5 -1
- data/lib/action_view/helpers/javascript_helper.rb +403 -35
- data/lib/action_view/helpers/javascripts/controls.js +261 -0
- data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
- data/lib/action_view/helpers/javascripts/effects.js +570 -0
- data/lib/action_view/helpers/javascripts/prototype.js +633 -371
- data/lib/action_view/helpers/number_helper.rb +11 -13
- data/lib/action_view/helpers/tag_helper.rb +1 -2
- data/lib/action_view/helpers/text_helper.rb +69 -6
- data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
- data/lib/action_view/helpers/url_helper.rb +98 -3
- data/lib/action_view/partials.rb +14 -8
- data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
- data/rakefile +13 -5
- data/test/abstract_unit.rb +1 -1
- data/test/controller/action_pack_assertions_test.rb +52 -9
- data/test/controller/active_record_assertions_test.rb +119 -120
- data/test/controller/active_record_store_test.rb +111 -0
- data/test/controller/addresses_render_test.rb +45 -0
- data/test/controller/caching_filestore.rb +92 -0
- data/test/controller/capture_test.rb +39 -0
- data/test/controller/cgi_test.rb +40 -3
- data/test/controller/helper_test.rb +65 -13
- data/test/controller/multipart_progress_testx.rb +365 -0
- data/test/controller/new_render_test.rb +263 -0
- data/test/controller/redirect_test.rb +64 -0
- data/test/controller/render_test.rb +20 -21
- data/test/controller/request_test.rb +83 -3
- data/test/controller/routing_test.rb +702 -0
- data/test/controller/send_file_test.rb +2 -0
- data/test/controller/test_test.rb +44 -8
- data/test/controller/upload_progress_testx.rb +89 -0
- data/test/controller/verification_test.rb +94 -29
- data/test/fixtures/addresses/list.rhtml +1 -0
- data/test/fixtures/test/capturing.rhtml +4 -0
- data/test/fixtures/test/list.rhtml +1 -1
- data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
- data/test/template/active_record_helper_test.rb +30 -15
- data/test/template/asset_tag_helper_test.rb +12 -5
- data/test/template/benchmark_helper_test.rb +72 -0
- data/test/template/date_helper_test.rb +69 -0
- data/test/template/form_helper_test.rb +18 -10
- data/test/template/form_options_helper_test.rb +40 -5
- data/test/template/javascript_helper.rb +149 -2
- data/test/template/number_helper_test.rb +2 -0
- data/test/template/tag_helper_test.rb +4 -0
- data/test/template/text_helper_test.rb +36 -0
- data/test/template/upload_progress_helper_testx.rb +272 -0
- data/test/template/url_helper_test.rb +30 -0
- metadata +30 -6
- data/test/controller/layout_test.rb +0 -49
- data/test/controller/routing_tests.rb +0 -543
@@ -146,7 +146,7 @@ module ActionView
|
|
146
146
|
|
147
147
|
DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS)
|
148
148
|
DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS)
|
149
|
-
DEFAULT_TEXT_AREA_OPTIONS = { "
|
149
|
+
DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS)
|
150
150
|
DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS)
|
151
151
|
|
152
152
|
def initialize(object_name, method_name, template_object, local_binding = nil)
|
@@ -175,6 +175,10 @@ module ActionView
|
|
175
175
|
options["type"] = "radio"
|
176
176
|
options["value"] = tag_value
|
177
177
|
options["checked"] = "checked" if value == tag_value
|
178
|
+
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
|
179
|
+
options["id"] = @auto_index ?
|
180
|
+
"#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
|
181
|
+
"#{@object_name}_#{@method_name}_#{pretty_tag_value}"
|
178
182
|
add_default_name_and_id(options)
|
179
183
|
tag("input", options)
|
180
184
|
end
|
@@ -196,6 +200,8 @@ module ActionView
|
|
196
200
|
false
|
197
201
|
when Integer
|
198
202
|
value != 0
|
203
|
+
when String
|
204
|
+
value == checked_value
|
199
205
|
else
|
200
206
|
value.to_i != 0
|
201
207
|
end
|
@@ -22,6 +22,8 @@ module ActionView
|
|
22
22
|
# <option>poem</option>
|
23
23
|
# </select>
|
24
24
|
#
|
25
|
+
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
|
26
|
+
#
|
25
27
|
# Another common case is a select tag for an <tt>belongs_to</tt>-associated object. For example,
|
26
28
|
#
|
27
29
|
# select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] })
|
@@ -251,7 +253,7 @@ module ActionView
|
|
251
253
|
"Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic",
|
252
254
|
"Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia",
|
253
255
|
"Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands",
|
254
|
-
"Costa Rica", "Cote d'Ivoire", "Croatia", "Cyprus", "Czech Republic", "Denmark",
|
256
|
+
"Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark",
|
255
257
|
"Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt",
|
256
258
|
"El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia",
|
257
259
|
"Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France",
|
@@ -259,7 +261,7 @@ module ActionView
|
|
259
261
|
"Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland",
|
260
262
|
"Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana",
|
261
263
|
"Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland",
|
262
|
-
"India", "Indonesia", "Ireland", "Israel", "Italy", "Iran", "
|
264
|
+
"India", "Indonesia", "Ireland", "Israel", "Italy", "Iran", "Iraq", "Jamaica", "Japan", "Jordan",
|
263
265
|
"Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait",
|
264
266
|
"Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho",
|
265
267
|
"Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia",
|
@@ -274,8 +276,8 @@ module ActionView
|
|
274
276
|
"Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda",
|
275
277
|
"Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines",
|
276
278
|
"Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia",
|
277
|
-
"Scotland", "Senegal", "Seychelles", "Sierra Leone", "Singapore",
|
278
|
-
"Slovenia", "Solomon Islands", "Somalia", "South Africa",
|
279
|
+
"Scotland", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore",
|
280
|
+
"Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
|
279
281
|
"South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka",
|
280
282
|
"St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands",
|
281
283
|
"Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand",
|
@@ -294,37 +296,43 @@ module ActionView
|
|
294
296
|
def to_select_tag(choices, options, html_options)
|
295
297
|
html_options = html_options.stringify_keys
|
296
298
|
add_default_name_and_id(html_options)
|
297
|
-
content_tag("select",
|
299
|
+
content_tag("select", add_options(options_for_select(choices, value), options, value), html_options)
|
298
300
|
end
|
299
301
|
|
300
302
|
def to_collection_select_tag(collection, value_method, text_method, options, html_options)
|
301
303
|
html_options = html_options.stringify_keys
|
302
304
|
add_default_name_and_id(html_options)
|
303
305
|
content_tag(
|
304
|
-
"select",
|
306
|
+
"select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options
|
305
307
|
)
|
306
308
|
end
|
307
309
|
|
308
310
|
def to_country_select_tag(priority_countries, options, html_options)
|
309
311
|
html_options = html_options.stringify_keys
|
310
312
|
add_default_name_and_id(html_options)
|
311
|
-
content_tag("select",
|
313
|
+
content_tag("select", add_options(country_options_for_select(value, priority_countries), options, value), html_options)
|
312
314
|
end
|
313
315
|
|
314
316
|
def to_time_zone_select_tag(priority_zones, options, html_options)
|
315
317
|
html_options = html_options.stringify_keys
|
316
318
|
add_default_name_and_id(html_options)
|
317
319
|
content_tag("select",
|
318
|
-
|
320
|
+
add_options(
|
319
321
|
time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone),
|
320
|
-
options
|
322
|
+
options, value
|
321
323
|
), html_options
|
322
324
|
)
|
323
325
|
end
|
324
326
|
|
325
327
|
private
|
326
|
-
def
|
327
|
-
|
328
|
+
def add_options(option_tags, options, value = nil)
|
329
|
+
option_tags = "<option value=\"\"></option>\n" + option_tags if options[:include_blank]
|
330
|
+
|
331
|
+
if value.blank? && options[:prompt]
|
332
|
+
("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags
|
333
|
+
else
|
334
|
+
option_tags
|
335
|
+
end
|
328
336
|
end
|
329
337
|
end
|
330
338
|
end
|
@@ -76,7 +76,11 @@ module ActionView
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def submit_tag(value = "Save changes", options = {})
|
79
|
-
tag("input", { "type" => "submit", "name" => "
|
79
|
+
tag("input", { "type" => "submit", "name" => "commit", "value" => value }.update(convert_options(options)))
|
80
|
+
end
|
81
|
+
|
82
|
+
def image_submit_tag(source, options = {})
|
83
|
+
tag("input", { "type" => "image", "src" => image_path(source) }.update(convert_options(options)))
|
80
84
|
end
|
81
85
|
|
82
86
|
private
|
@@ -2,21 +2,24 @@ require File.dirname(__FILE__) + '/tag_helper'
|
|
2
2
|
|
3
3
|
module ActionView
|
4
4
|
module Helpers
|
5
|
-
# Provides a set of helpers for calling
|
6
|
-
# been labelled
|
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
7
|
# actions in your controllers without reloading the page, but still update certain parts of it using injections into the
|
8
8
|
# DOM. The common use case is having a form that adds a new element to a list without reloading the page.
|
9
9
|
#
|
10
|
-
# To be able to use the
|
11
|
-
# the
|
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
12
|
# <tt><%= javascript_include_tag "prototype" %></tt> (which looks for the library in /javascripts/prototype.js). The latter is
|
13
13
|
# recommended as the browser can then cache the library instead of fetching all the functions anew on every request.
|
14
14
|
#
|
15
|
-
# If you're the visual type, there's an
|
15
|
+
# If you're the visual type, there's an AJAX movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating
|
16
16
|
# the use of form_remote_tag.
|
17
|
-
module
|
17
|
+
module JavaScriptHelper
|
18
18
|
unless const_defined? :CALLBACKS
|
19
|
-
CALLBACKS
|
19
|
+
CALLBACKS =
|
20
|
+
[:uninitialized, :loading, :loaded, :interactive, :complete, :failure].push((100..599).to_a).flatten
|
21
|
+
AJAX_OPTIONS = [ :before, :after, :condition, :url, :asynchronous, :method,
|
22
|
+
:insertion, :position, :form, :with, :update, :script ].concat(CALLBACKS)
|
20
23
|
JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
|
21
24
|
end
|
22
25
|
|
@@ -44,9 +47,25 @@ module ActionView
|
|
44
47
|
# link_to_remote "Delete this post", :update => "posts", :url => { :action => "destroy", :id => post.id }
|
45
48
|
# link_to_remote(image_tag("refresh"), :update => "emails", :url => { :action => "list_emails" })
|
46
49
|
#
|
50
|
+
# You can also specify a hash for <tt>options[:update]</tt> to allow for
|
51
|
+
# easy redirection of output to an other DOM element if a server-side error occurs:
|
52
|
+
#
|
53
|
+
# Example:
|
54
|
+
# link_to_remote "Delete this post",
|
55
|
+
# :url => { :action => "destroy", :id => post.id },
|
56
|
+
# :update => { :success => "posts", :failure => "error" }
|
57
|
+
#
|
58
|
+
# Optionally, you can use the <tt>options[:position]</tt> parameter to influence
|
59
|
+
# how the target DOM element is updated. It must be one of
|
60
|
+
# <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
|
61
|
+
#
|
47
62
|
# By default, these remote requests are processed asynchronous during
|
48
|
-
# which various callbacks can be triggered (for progress indicators and
|
49
|
-
# the likes).
|
63
|
+
# which various JavaScript callbacks can be triggered (for progress indicators and
|
64
|
+
# the likes). All callbacks get access to the <tt>request</tt> object,
|
65
|
+
# which holds the underlying XMLHttpRequest.
|
66
|
+
#
|
67
|
+
# To access the server response, use <tt>request.responseText</tt>, to
|
68
|
+
# find out the HTTP status, use <tt>request.status</tt>.
|
50
69
|
#
|
51
70
|
# Example:
|
52
71
|
# link_to_remote word,
|
@@ -62,11 +81,38 @@ module ActionView
|
|
62
81
|
# <tt>:interactive</tt>:: Called when the user can interact with the
|
63
82
|
# remote document, even though it has not
|
64
83
|
# finished loading.
|
65
|
-
# <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
|
84
|
+
# <tt>:complete</tt>:: Called when the XMLHttpRequest is complete,
|
85
|
+
# and the HTTP status code is 200 OK.
|
86
|
+
# <tt>:failure</tt>:: Called when the XMLHttpRequest is complete,
|
87
|
+
# and the HTTP status code is anything other than
|
88
|
+
# 200 OK.
|
89
|
+
#
|
90
|
+
# You can further refine <tt>:failure</tt> by adding additional
|
91
|
+
# callbacks for specific status codes:
|
92
|
+
#
|
93
|
+
# Example:
|
94
|
+
# link_to_remote word,
|
95
|
+
# :url => { :action => "action" },
|
96
|
+
# 404 => "alert('Not found...? Wrong URL...?')",
|
97
|
+
# :failure => "alert('HTTP Error ' + request.status + '!')"
|
98
|
+
#
|
66
99
|
#
|
67
100
|
# If you for some reason or another need synchronous processing (that'll
|
68
101
|
# block the browser while the request is happening), you can specify
|
69
102
|
# <tt>options[:type] = :synchronous</tt>.
|
103
|
+
#
|
104
|
+
# You can customize further browser side call logic by passing
|
105
|
+
# in JavaScript code snippets via some optional parameters. In
|
106
|
+
# their order of use these are:
|
107
|
+
#
|
108
|
+
# <tt>:confirm</tt>:: Adds confirmation dialog.
|
109
|
+
# <tt>:condition</tt>:: Perform remote request conditionally
|
110
|
+
# by this expression. Use this to
|
111
|
+
# describe browser-side conditions when
|
112
|
+
# request should not be initiated.
|
113
|
+
# <tt>:before</tt>:: Called before request is initiated.
|
114
|
+
# <tt>:after</tt>:: Called immediately after request was
|
115
|
+
# initiated and before <tt>:loading</tt>.
|
70
116
|
def link_to_remote(name, options = {}, html_options = {})
|
71
117
|
link_to_function(name, remote_function(options), html_options)
|
72
118
|
end
|
@@ -79,16 +125,24 @@ module ActionView
|
|
79
125
|
code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
|
80
126
|
content_tag("script", code, options[:html_options] || {})
|
81
127
|
end
|
82
|
-
|
128
|
+
|
83
129
|
# Returns a form tag that will submit using XMLHttpRequest in the background instead of the regular
|
84
|
-
# reloading POST arrangement. Even though it's using
|
130
|
+
# reloading POST arrangement. Even though it's using JavaScript to serialize the form elements, the form submission
|
85
131
|
# will work just like a regular submission as viewed by the receiving side (all elements available in @params).
|
86
132
|
# The options for specifying the target with :url and defining callbacks is the same as link_to_remote.
|
133
|
+
#
|
134
|
+
# A "fall-through" target for browsers that doesn't do JavaScript can be specified with the :action/:method options on :html
|
135
|
+
#
|
136
|
+
# form_remote_tag :html => { :action => url_for(:controller => "some", :action => "place") }
|
137
|
+
#
|
138
|
+
# By default the fall-through action is the same as the one specified in the :url (and the default method is :post).
|
87
139
|
def form_remote_tag(options = {})
|
88
140
|
options[:form] = true
|
89
141
|
|
90
142
|
options[:html] ||= {}
|
91
143
|
options[:html][:onsubmit] = "#{remote_function(options)}; return false;"
|
144
|
+
options[:html][:action] = options[:html][:action] || url_for(options[:url])
|
145
|
+
options[:html][:method] = options[:html][:method] || "post"
|
92
146
|
|
93
147
|
tag("form", options[:html], true)
|
94
148
|
end
|
@@ -106,27 +160,110 @@ module ActionView
|
|
106
160
|
|
107
161
|
tag("input", options[:html], false)
|
108
162
|
end
|
163
|
+
|
164
|
+
# Returns a Javascript function (or expression) that'll update a DOM element according to the options passed.
|
165
|
+
#
|
166
|
+
# * <tt>:content</tt>: The content to use for updating. Can be left out if using block, see example.
|
167
|
+
# * <tt>:action</tt>: Valid options are :update (assumed by default), :empty, :remove
|
168
|
+
# * <tt>:position</tt> If the :action is :update, you can optionally specify one of the following positions: :before, :top, :bottom, :after.
|
169
|
+
#
|
170
|
+
# Examples:
|
171
|
+
# <%= javascript_tag(update_element_function(
|
172
|
+
# "products", :position => :bottom, :content => "<p>New product!</p>")) %>
|
173
|
+
#
|
174
|
+
# <% replacement_function = update_element_function("products") do %>
|
175
|
+
# <p>Product 1</p>
|
176
|
+
# <p>Product 2</p>
|
177
|
+
# <% end %>
|
178
|
+
# <%= javascript_tag(replacement_function) %>
|
179
|
+
#
|
180
|
+
# This method can also be used in combination with remote method call where the result is evaluated afterwards to cause
|
181
|
+
# multiple updates on a page. Example:
|
182
|
+
#
|
183
|
+
# # Calling view
|
184
|
+
# <%= form_remote_tag :url => { :action => "buy" }, :complete => evaluate_remote_response %>
|
185
|
+
# all the inputs here...
|
186
|
+
#
|
187
|
+
# # Controller action
|
188
|
+
# def buy
|
189
|
+
# @product = Product.find(1)
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# # Returning view
|
193
|
+
# <%= update_element_function(
|
194
|
+
# "cart", :action => :update, :position => :bottom,
|
195
|
+
# :content => "<p>New Product: #{@product.name}</p>")) %>
|
196
|
+
# <% update_element_function("status", :binding => binding) do %>
|
197
|
+
# You've bought a new product!
|
198
|
+
# <% end %>
|
199
|
+
#
|
200
|
+
# Notice how the second call doesn't need to be in an ERb output block since it uses a block and passes in the binding
|
201
|
+
# to render directly. This trick will however only work in ERb (not Builder or other template forms).
|
202
|
+
def update_element_function(element_id, options = {}, &block)
|
203
|
+
|
204
|
+
content = escape_javascript(options[:content] || '')
|
205
|
+
content = escape_javascript(capture(&block)) if block
|
206
|
+
|
207
|
+
javascript_function = case (options[:action] || :update)
|
208
|
+
when :update
|
209
|
+
if options[:position]
|
210
|
+
"new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')"
|
211
|
+
else
|
212
|
+
"$('#{element_id}').innerHTML = '#{content}'"
|
213
|
+
end
|
214
|
+
|
215
|
+
when :empty
|
216
|
+
"$('#{element_id}').innerHTML = ''"
|
217
|
+
|
218
|
+
when :remove
|
219
|
+
"Element.remove('#{element_id}')"
|
220
|
+
|
221
|
+
else
|
222
|
+
raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty"
|
223
|
+
end
|
224
|
+
|
225
|
+
javascript_function << ";\n"
|
226
|
+
options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns 'eval(request.responseText)' which is the Javascript function that form_remote_tag can call in :complete to
|
230
|
+
# evaluate a multiple update return document using update_element_function calls.
|
231
|
+
def evaluate_remote_response
|
232
|
+
"eval(request.responseText)"
|
233
|
+
end
|
109
234
|
|
110
235
|
def remote_function(options) #:nodoc: for now
|
111
236
|
javascript_options = options_for_ajax(options)
|
112
237
|
|
113
|
-
|
114
|
-
|
115
|
-
|
238
|
+
update = ''
|
239
|
+
if options[:update] and options[:update].is_a?Hash
|
240
|
+
update = []
|
241
|
+
update << "success:'#{options[:update][:success]}'" if options[:update][:success]
|
242
|
+
update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
|
243
|
+
update = '{' + update.join(',') + '}'
|
244
|
+
elsif options[:update]
|
245
|
+
update << "'#{options[:update]}'"
|
246
|
+
end
|
247
|
+
|
248
|
+
function = update.empty? ?
|
249
|
+
"new Ajax.Request(" :
|
250
|
+
"new Ajax.Updater(#{update}, "
|
116
251
|
|
117
252
|
function << "'#{url_for(options[:url])}'"
|
118
253
|
function << ", #{javascript_options})"
|
119
|
-
|
254
|
+
|
120
255
|
function = "#{options[:before]}; #{function}" if options[:before]
|
121
256
|
function = "#{function}; #{options[:after]}" if options[:after]
|
122
257
|
function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
|
123
258
|
function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
|
124
|
-
|
259
|
+
|
125
260
|
return function
|
126
261
|
end
|
127
262
|
|
128
|
-
# Includes the Action Pack
|
129
|
-
# tag.
|
263
|
+
# Includes the Action Pack JavaScript libraries inside a single <script>
|
264
|
+
# tag. The function first includes prototype.js and then its core extensions,
|
265
|
+
# (determined by filenames starting with "prototype").
|
266
|
+
# Afterwards, any additional scripts will be included in random order.
|
130
267
|
#
|
131
268
|
# Note: The recommended approach is to copy the contents of
|
132
269
|
# lib/action_view/helpers/javascripts/ into your application's
|
@@ -134,24 +271,36 @@ module ActionView
|
|
134
271
|
# create remote <script> links.
|
135
272
|
def define_javascript_functions
|
136
273
|
javascript = '<script type="text/javascript">'
|
137
|
-
|
274
|
+
|
275
|
+
# load prototype.js and its extensions first
|
276
|
+
prototype_libs = Dir.glob(File.join(JAVASCRIPT_PATH, 'prototype*')).sort.reverse
|
277
|
+
prototype_libs.each do |filename|
|
278
|
+
javascript << "\n" << IO.read(filename)
|
279
|
+
end
|
280
|
+
|
281
|
+
# load other librairies
|
282
|
+
(Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename|
|
283
|
+
javascript << "\n" << IO.read(filename)
|
284
|
+
end
|
138
285
|
javascript << '</script>'
|
139
286
|
end
|
140
287
|
|
141
288
|
# Observes the field with the DOM ID specified by +field_id+ and makes
|
142
|
-
# an
|
289
|
+
# an AJAX call when its contents have changed.
|
143
290
|
#
|
144
291
|
# Required +options+ are:
|
145
|
-
# <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
|
146
|
-
# this field will be detected.
|
147
292
|
# <tt>:url</tt>:: +url_for+-style options for the action to call
|
148
293
|
# when the field has changed.
|
149
294
|
#
|
150
295
|
# Additional options are:
|
296
|
+
# <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
|
297
|
+
# this field will be detected. Set this to a value
|
298
|
+
# greater than zero to use time based observation
|
299
|
+
# instead of event based observation.
|
151
300
|
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
|
152
301
|
# innerHTML should be updated with the
|
153
302
|
# XMLHttpRequest response text.
|
154
|
-
# <tt>:with</tt>:: A
|
303
|
+
# <tt>:with</tt>:: A JavaScript expression specifying the
|
155
304
|
# parameters for the XMLHttpRequest. This defaults
|
156
305
|
# to 'value', which in the evaluated context
|
157
306
|
# refers to the new field value.
|
@@ -159,7 +308,11 @@ module ActionView
|
|
159
308
|
# Additionally, you may specify any of the options documented in
|
160
309
|
# +link_to_remote.
|
161
310
|
def observe_field(field_id, options = {})
|
162
|
-
|
311
|
+
if options[:frequency]
|
312
|
+
build_observer('Form.Element.Observer', field_id, options)
|
313
|
+
else
|
314
|
+
build_observer('Form.Element.EventObserver', field_id, options)
|
315
|
+
end
|
163
316
|
end
|
164
317
|
|
165
318
|
# Like +observe_field+, but operates on an entire form identified by the
|
@@ -167,29 +320,212 @@ module ActionView
|
|
167
320
|
# the default value of the <tt>:with</tt> option evaluates to the
|
168
321
|
# serialized (request string) value of the form.
|
169
322
|
def observe_form(form_id, options = {})
|
170
|
-
|
323
|
+
if options[:frequency]
|
324
|
+
build_observer('Form.Observer', form_id, options)
|
325
|
+
else
|
326
|
+
build_observer('Form.EventObserver', form_id, options)
|
327
|
+
end
|
171
328
|
end
|
329
|
+
|
330
|
+
|
331
|
+
# Adds AJAX autocomplete functionality to the text input field with the
|
332
|
+
# DOM ID specified by +field_id+.
|
333
|
+
#
|
334
|
+
# This function expects that the called action returns a HTML <ul> list,
|
335
|
+
# or nothing if no entries should be displayed for autocompletion.
|
336
|
+
#
|
337
|
+
# You'll probably want to turn the browser's built-in autocompletion off,
|
338
|
+
# su be sure to include a autocomplete="off" attribute with your text
|
339
|
+
# input field.
|
340
|
+
#
|
341
|
+
# Required +options+ are:
|
342
|
+
# <tt>:url</tt>:: Specifies the DOM ID of the element whose
|
343
|
+
# innerHTML should be updated with the autocomplete
|
344
|
+
# entries returned by XMLHttpRequest.
|
345
|
+
#
|
346
|
+
# Addtional +options+ are:
|
347
|
+
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
|
348
|
+
# innerHTML should be updated with the autocomplete
|
349
|
+
# entries returned by the AJAX request.
|
350
|
+
# Defaults to field_id + '_auto_complete'
|
351
|
+
# <tt>:with</tt>:: A JavaScript expression specifying the
|
352
|
+
# parameters for the XMLHttpRequest. This defaults
|
353
|
+
# to 'fieldname=value'.
|
354
|
+
# <tt>:indicator</tt>:: Specifies the DOM ID of an elment which will be
|
355
|
+
# displayed while autocomplete is running.
|
356
|
+
def auto_complete_field(field_id, options = {})
|
357
|
+
function = "new Ajax.Autocompleter("
|
358
|
+
function << "'#{field_id}', "
|
359
|
+
function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', "
|
360
|
+
function << "'#{url_for(options[:url])}'"
|
172
361
|
|
173
|
-
|
362
|
+
js_options = {}
|
363
|
+
js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
|
364
|
+
js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
|
365
|
+
function << (', ' + options_for_javascript(js_options) + ')')
|
366
|
+
|
367
|
+
javascript_tag(function)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Use this method in your view to generate a return for the AJAX automplete requests.
|
371
|
+
#
|
372
|
+
# Example action:
|
373
|
+
#
|
374
|
+
# def auto_complete_for_item_title
|
375
|
+
# @items = Item.find(:all,
|
376
|
+
# :conditions => [ 'LOWER(description) LIKE ?',
|
377
|
+
# '%' + request.raw_post.downcase + '%' ])
|
378
|
+
# render :inline => '<%= auto_complete_result(@items, 'description') %>'
|
379
|
+
# end
|
380
|
+
#
|
381
|
+
# The auto_complete_result can of course also be called from a view belonging to the
|
382
|
+
# auto_complete action if you need to decorate it further.
|
383
|
+
def auto_complete_result(entries, field, phrase = nil)
|
384
|
+
return unless entries
|
385
|
+
items = entries.map { |entry| content_tag("li", phrase ? highlight(entry[field], phrase) : h(entry[field])) }
|
386
|
+
content_tag("ul", items)
|
387
|
+
end
|
388
|
+
|
389
|
+
# Wrapper for text_field with added AJAX autocompletion functionality.
|
390
|
+
#
|
391
|
+
# In your controller, you'll need to define an action called
|
392
|
+
# auto_complete_for_object_method to respond the AJAX calls,
|
393
|
+
#
|
394
|
+
# See the RDoc on ActionController::AutoComplete to learn more about this.
|
395
|
+
def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})
|
396
|
+
(completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
|
397
|
+
text_field(object, method, { :autocomplete => "off" }.merge!(tag_options)) +
|
398
|
+
content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") +
|
399
|
+
auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options))
|
400
|
+
end
|
401
|
+
|
402
|
+
# Returns a JavaScript snippet to be used on the AJAX callbacks for starting
|
403
|
+
# visual effects.
|
404
|
+
#
|
405
|
+
# Example:
|
406
|
+
# <%= link_to_remote "Reload", :update => "posts",
|
407
|
+
# :url => { :action => "reload" },
|
408
|
+
# :complete => visual_effect(:highlight, "posts", :duration => 0.5 )
|
409
|
+
#
|
410
|
+
# If no element_id is given, it assumes "element" which should be a local
|
411
|
+
# variable in the generated JavaScript execution context. This can be used
|
412
|
+
# for example with drop_receiving_element:
|
413
|
+
#
|
414
|
+
# <%= drop_receving_element (...), :loading => visual_effect(:fade) %>
|
415
|
+
#
|
416
|
+
# This would fade the element that was dropped on the drop receiving element.
|
417
|
+
#
|
418
|
+
# You can change the behaviour with various options, see
|
419
|
+
# http://script.aculo.us for more documentation.
|
420
|
+
def visual_effect(name, element_id = false, js_options = {})
|
421
|
+
element = element_id ? "'#{element_id}'" : "element"
|
422
|
+
"new Effect.#{name.to_s.capitalize}(#{element},#{options_for_javascript(js_options)});"
|
423
|
+
end
|
424
|
+
|
425
|
+
# Makes the element with the DOM ID specified by +element_id+ sortable
|
426
|
+
# by drag-and-drop and make an AJAX call whenever the sort order has
|
427
|
+
# changed. By default, the action called gets the serialized sortable
|
428
|
+
# element as parameters.
|
429
|
+
#
|
430
|
+
# Example:
|
431
|
+
# <%= sortable_element("my_list", :url => { :action => "order" }) %>
|
432
|
+
#
|
433
|
+
# In the example, the action gets a "my_list" array parameter
|
434
|
+
# containing the values of the ids of elements the sortable consists
|
435
|
+
# of, in the current order.
|
436
|
+
#
|
437
|
+
# You can change the behaviour with various options, see
|
438
|
+
# http://script.aculo.us for more documentation.
|
439
|
+
def sortable_element(element_id, options = {})
|
440
|
+
options[:with] ||= "Sortable.serialize('#{element_id}')"
|
441
|
+
options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
|
442
|
+
options.delete_if { |key, value| AJAX_OPTIONS.include?(key) }
|
443
|
+
|
444
|
+
[:tag, :overlap, :constraint].each do |option|
|
445
|
+
options[option] = "'#{options[option]}'" if options[option]
|
446
|
+
end
|
447
|
+
|
448
|
+
if options[:containment] and options[:containment].kind_of?(Array)
|
449
|
+
options[:containment] = "['#{options[:containment].join('\',\'')}']"
|
450
|
+
elsif options[:containment]
|
451
|
+
options[:containment] = "'#{options[:containment]}'" if options[:containment]
|
452
|
+
end
|
453
|
+
|
454
|
+
javascript_tag("Sortable.create('#{element_id}', #{options_for_javascript(options)})")
|
455
|
+
end
|
456
|
+
|
457
|
+
# Makes the element with the DOM ID specified by +element_id+ draggable.
|
458
|
+
#
|
459
|
+
# Example:
|
460
|
+
# <%= draggable_element("my_image", :revert => true)
|
461
|
+
#
|
462
|
+
# You can change the behaviour with various options, see
|
463
|
+
# http://script.aculo.us for more documentation.
|
464
|
+
def draggable_element(element_id, options = {})
|
465
|
+
javascript_tag("new Draggable('#{element_id}', #{options_for_javascript(options)})")
|
466
|
+
end
|
467
|
+
|
468
|
+
# Makes the element with the DOM ID specified by +element_id+ receive
|
469
|
+
# dropped draggable elements (created by draggable_element).
|
470
|
+
# and make an AJAX call By default, the action called gets the DOM ID of the
|
471
|
+
# element as parameter.
|
472
|
+
#
|
473
|
+
# Example:
|
474
|
+
# <%= drop_receiving_element("my_cart", :url => { :controller => "cart", :action => "add" }) %>
|
475
|
+
#
|
476
|
+
# You can change the behaviour with various options, see
|
477
|
+
# http://script.aculo.us for more documentation.
|
478
|
+
def drop_receiving_element(element_id, options = {})
|
479
|
+
options[:with] ||= "'id=' + encodeURIComponent(element.id)"
|
480
|
+
options[:onDrop] ||= "function(element){" + remote_function(options) + "}"
|
481
|
+
options.delete_if { |key, value| AJAX_OPTIONS.include?(key) }
|
482
|
+
|
483
|
+
if options[:accept] and options[:accept].kind_of?(Array)
|
484
|
+
options[:accept] = "['#{options[:accept].join('\',\'')}']"
|
485
|
+
elsif options[:accept]
|
486
|
+
options[:accept] = "'#{options[:accept]}'" if options[:accept]
|
487
|
+
end
|
488
|
+
|
489
|
+
options[:hoverclass] = "'#{options[:hoverclass]}'" if options[:hoverclass]
|
490
|
+
|
491
|
+
javascript_tag("Droppables.add('#{element_id}', #{options_for_javascript(options)})")
|
492
|
+
end
|
493
|
+
|
494
|
+
# Escape carrier returns and single and double quotes for JavaScript segments.
|
174
495
|
def escape_javascript(javascript)
|
175
496
|
(javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
|
176
497
|
end
|
177
498
|
|
499
|
+
# Returns a JavaScript tag with the +content+ inside. Example:
|
500
|
+
# javascript_tag "alert('All is good')" # => <script type="text/javascript">alert('All is good')</script>
|
501
|
+
def javascript_tag(content)
|
502
|
+
content_tag("script", content, :type => "text/javascript")
|
503
|
+
end
|
504
|
+
|
178
505
|
private
|
506
|
+
def options_for_javascript(options)
|
507
|
+
'{' + options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
|
508
|
+
end
|
509
|
+
|
179
510
|
def options_for_ajax(options)
|
180
511
|
js_options = build_callbacks(options)
|
181
512
|
|
182
513
|
js_options['asynchronous'] = options[:type] != :synchronous
|
183
|
-
js_options['method'] = options[:method] if options[:method]
|
514
|
+
js_options['method'] = method_option_to_s(options[:method]) if options[:method]
|
184
515
|
js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
|
185
|
-
|
516
|
+
js_options['evalScripts'] = options[:script].nil? || options[:script]
|
517
|
+
|
186
518
|
if options[:form]
|
187
519
|
js_options['parameters'] = 'Form.serialize(this)'
|
188
520
|
elsif options[:with]
|
189
521
|
js_options['parameters'] = options[:with]
|
190
522
|
end
|
191
523
|
|
192
|
-
|
524
|
+
options_for_javascript(js_options)
|
525
|
+
end
|
526
|
+
|
527
|
+
def method_option_to_s(method)
|
528
|
+
(method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
|
193
529
|
end
|
194
530
|
|
195
531
|
def build_observer(klass, name, options = {})
|
@@ -197,20 +533,52 @@ module ActionView
|
|
197
533
|
callback = remote_function(options)
|
198
534
|
javascript = '<script type="text/javascript">'
|
199
535
|
javascript << "new #{klass}('#{name}', "
|
200
|
-
javascript << "#{options[:frequency]},
|
536
|
+
javascript << "#{options[:frequency]}, " if options[:frequency]
|
537
|
+
javascript << "function(element, value) {"
|
201
538
|
javascript << "#{callback}})</script>"
|
202
539
|
end
|
203
540
|
|
204
541
|
def build_callbacks(options)
|
205
|
-
|
206
|
-
|
542
|
+
callbacks = {}
|
543
|
+
options.each do |callback, code|
|
544
|
+
if CALLBACKS.include?(callback)
|
207
545
|
name = 'on' + callback.to_s.capitalize
|
208
|
-
code = options[callback]
|
209
546
|
callbacks[name] = "function(request){#{code}}"
|
210
547
|
end
|
211
|
-
callbacks
|
212
548
|
end
|
549
|
+
callbacks
|
550
|
+
end
|
551
|
+
|
552
|
+
def auto_complete_stylesheet
|
553
|
+
content_tag("style", <<-EOT
|
554
|
+
div.auto_complete {
|
555
|
+
width: 350px;
|
556
|
+
background: #fff;
|
557
|
+
}
|
558
|
+
div.auto_complete ul {
|
559
|
+
border:1px solid #888;
|
560
|
+
margin:0;
|
561
|
+
padding:0;
|
562
|
+
width:100%;
|
563
|
+
list-style-type:none;
|
564
|
+
}
|
565
|
+
div.auto_complete ul li {
|
566
|
+
margin:0;
|
567
|
+
padding:3px;
|
568
|
+
}
|
569
|
+
div.auto_complete ul li.selected {
|
570
|
+
background-color: #ffb;
|
571
|
+
}
|
572
|
+
div.auto_complete ul strong.highlight {
|
573
|
+
color: #800;
|
574
|
+
margin:0;
|
575
|
+
padding:0;
|
576
|
+
}
|
577
|
+
EOT
|
578
|
+
)
|
213
579
|
end
|
214
580
|
end
|
581
|
+
|
582
|
+
JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper
|
215
583
|
end
|
216
584
|
end
|