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.

Files changed (101) hide show
  1. data/CHANGELOG +309 -16
  2. data/README +1 -1
  3. data/lib/action_controller.rb +5 -0
  4. data/lib/action_controller/assertions.rb +57 -12
  5. data/lib/action_controller/auto_complete.rb +47 -0
  6. data/lib/action_controller/base.rb +288 -258
  7. data/lib/action_controller/benchmarking.rb +8 -3
  8. data/lib/action_controller/caching.rb +88 -42
  9. data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
  10. data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
  11. data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
  12. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
  13. data/lib/action_controller/cgi_process.rb +39 -11
  14. data/lib/action_controller/code_generation.rb +235 -0
  15. data/lib/action_controller/cookies.rb +14 -8
  16. data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
  17. data/lib/action_controller/filters.rb +8 -7
  18. data/lib/action_controller/helpers.rb +41 -6
  19. data/lib/action_controller/layout.rb +45 -16
  20. data/lib/action_controller/request.rb +86 -23
  21. data/lib/action_controller/rescue.rb +1 -0
  22. data/lib/action_controller/response.rb +1 -1
  23. data/lib/action_controller/routing.rb +536 -272
  24. data/lib/action_controller/scaffolding.rb +30 -25
  25. data/lib/action_controller/session/active_record_store.rb +251 -50
  26. data/lib/action_controller/streaming.rb +133 -0
  27. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
  28. data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
  29. data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
  30. data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
  31. data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
  32. data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
  33. data/lib/action_controller/test_process.rb +68 -47
  34. data/lib/action_controller/upload_progress.rb +421 -0
  35. data/lib/action_controller/url_rewriter.rb +8 -11
  36. data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
  37. data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
  38. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
  39. data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
  40. data/lib/action_controller/vendor/xml_simple.rb +1019 -0
  41. data/lib/action_controller/verification.rb +36 -30
  42. data/lib/action_view/base.rb +21 -14
  43. data/lib/action_view/helpers/active_record_helper.rb +15 -13
  44. data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
  45. data/lib/action_view/helpers/benchmark_helper.rb +24 -0
  46. data/lib/action_view/helpers/capture_helper.rb +7 -5
  47. data/lib/action_view/helpers/date_helper.rb +63 -46
  48. data/lib/action_view/helpers/form_helper.rb +7 -1
  49. data/lib/action_view/helpers/form_options_helper.rb +19 -11
  50. data/lib/action_view/helpers/form_tag_helper.rb +5 -1
  51. data/lib/action_view/helpers/javascript_helper.rb +403 -35
  52. data/lib/action_view/helpers/javascripts/controls.js +261 -0
  53. data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
  54. data/lib/action_view/helpers/javascripts/effects.js +570 -0
  55. data/lib/action_view/helpers/javascripts/prototype.js +633 -371
  56. data/lib/action_view/helpers/number_helper.rb +11 -13
  57. data/lib/action_view/helpers/tag_helper.rb +1 -2
  58. data/lib/action_view/helpers/text_helper.rb +69 -6
  59. data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
  60. data/lib/action_view/helpers/url_helper.rb +98 -3
  61. data/lib/action_view/partials.rb +14 -8
  62. data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
  63. data/rakefile +13 -5
  64. data/test/abstract_unit.rb +1 -1
  65. data/test/controller/action_pack_assertions_test.rb +52 -9
  66. data/test/controller/active_record_assertions_test.rb +119 -120
  67. data/test/controller/active_record_store_test.rb +111 -0
  68. data/test/controller/addresses_render_test.rb +45 -0
  69. data/test/controller/caching_filestore.rb +92 -0
  70. data/test/controller/capture_test.rb +39 -0
  71. data/test/controller/cgi_test.rb +40 -3
  72. data/test/controller/helper_test.rb +65 -13
  73. data/test/controller/multipart_progress_testx.rb +365 -0
  74. data/test/controller/new_render_test.rb +263 -0
  75. data/test/controller/redirect_test.rb +64 -0
  76. data/test/controller/render_test.rb +20 -21
  77. data/test/controller/request_test.rb +83 -3
  78. data/test/controller/routing_test.rb +702 -0
  79. data/test/controller/send_file_test.rb +2 -0
  80. data/test/controller/test_test.rb +44 -8
  81. data/test/controller/upload_progress_testx.rb +89 -0
  82. data/test/controller/verification_test.rb +94 -29
  83. data/test/fixtures/addresses/list.rhtml +1 -0
  84. data/test/fixtures/test/capturing.rhtml +4 -0
  85. data/test/fixtures/test/list.rhtml +1 -1
  86. data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
  87. data/test/template/active_record_helper_test.rb +30 -15
  88. data/test/template/asset_tag_helper_test.rb +12 -5
  89. data/test/template/benchmark_helper_test.rb +72 -0
  90. data/test/template/date_helper_test.rb +69 -0
  91. data/test/template/form_helper_test.rb +18 -10
  92. data/test/template/form_options_helper_test.rb +40 -5
  93. data/test/template/javascript_helper.rb +149 -2
  94. data/test/template/number_helper_test.rb +2 -0
  95. data/test/template/tag_helper_test.rb +4 -0
  96. data/test/template/text_helper_test.rb +36 -0
  97. data/test/template/upload_progress_helper_testx.rb +272 -0
  98. data/test/template/url_helper_test.rb +30 -0
  99. metadata +30 -6
  100. data/test/controller/layout_test.rb +0 -49
  101. 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 = { "wrap" => "virtual", "cols" => 40, "rows" => 20 }.freeze unless const_defined?(: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", "Irak", "Jamaica", "Japan", "Jordan",
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", "Slovakia",
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", add_blank_option(options_for_select(choices, value), options[:include_blank]), html_options)
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", add_blank_option(options_from_collection_for_select(collection, value_method, text_method, value), options[:include_blank]), html_options
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", add_blank_option(country_options_for_select(value, priority_countries), options[:include_blank]), html_options)
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
- add_blank_option(
320
+ add_options(
319
321
  time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone),
320
- options[:include_blank]
322
+ options, value
321
323
  ), html_options
322
324
  )
323
325
  end
324
326
 
325
327
  private
326
- def add_blank_option(option_tags, add_blank)
327
- add_blank ? "<option value=\"\"></option>\n" + option_tags : option_tags
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" => "submit", "value" => value }.update(convert_options(options)))
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 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
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 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
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 Ajax movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating
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 JavascriptHelper
17
+ module JavaScriptHelper
18
18
  unless const_defined? :CALLBACKS
19
- CALLBACKS = [:uninitialized, :loading, :loaded, :interactive, :complete]
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 Javascript to serialize the form elements, the form submission
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
- function = options[:update] ?
114
- "new Ajax.Updater('#{options[:update]}', " :
115
- "new Ajax.Request("
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 Javascript library inside a single <script>
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
- Dir.glob(File.join(JAVASCRIPT_PATH, '*')).each { |filename| javascript << "\n" << IO.read(filename) }
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 Ajax when its contents have changed.
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 Javascript expression specifying the
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
- build_observer('Form.Element.Observer', field_id, options)
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
- build_observer('Form.Observer', form_id, options)
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
- # Escape carrier returns and single and double quotes for Javascript segments.
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
- '{' + js_options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
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]}, function(element, value) {"
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
- CALLBACKS.inject({}) do |callbacks, callback|
206
- if options[callback]
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