actionpack 1.11.2 → 1.12.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 (149) hide show
  1. data/CHANGELOG +392 -5
  2. data/lib/action_controller.rb +8 -4
  3. data/lib/action_controller/assertions.rb +9 -10
  4. data/lib/action_controller/base.rb +177 -88
  5. data/lib/action_controller/benchmarking.rb +5 -5
  6. data/lib/action_controller/caching.rb +44 -36
  7. data/lib/action_controller/cgi_ext/cgi_methods.rb +71 -6
  8. data/lib/action_controller/cgi_ext/cookie_performance_fix.rb +1 -1
  9. data/lib/action_controller/cgi_process.rb +36 -24
  10. data/lib/action_controller/components.rb +152 -52
  11. data/lib/action_controller/dependencies.rb +1 -1
  12. data/lib/action_controller/deprecated_redirects.rb +2 -2
  13. data/lib/action_controller/deprecated_request_methods.rb +34 -0
  14. data/lib/action_controller/filters.rb +59 -19
  15. data/lib/action_controller/flash.rb +53 -47
  16. data/lib/action_controller/helpers.rb +2 -2
  17. data/lib/action_controller/integration.rb +524 -0
  18. data/lib/action_controller/layout.rb +58 -23
  19. data/lib/action_controller/mime_responds.rb +163 -0
  20. data/lib/action_controller/mime_type.rb +142 -0
  21. data/lib/action_controller/pagination.rb +13 -7
  22. data/lib/action_controller/request.rb +59 -56
  23. data/lib/action_controller/rescue.rb +1 -1
  24. data/lib/action_controller/routing.rb +29 -10
  25. data/lib/action_controller/scaffolding.rb +8 -0
  26. data/lib/action_controller/session/active_record_store.rb +21 -10
  27. data/lib/action_controller/session/mem_cache_store.rb +18 -12
  28. data/lib/action_controller/session_management.rb +30 -11
  29. data/lib/action_controller/templates/rescues/_trace.rhtml +1 -1
  30. data/lib/action_controller/templates/scaffolds/layout.rhtml +4 -4
  31. data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
  32. data/lib/action_controller/test_process.rb +189 -118
  33. data/lib/action_controller/vendor/html-scanner/html/node.rb +20 -1
  34. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +3 -0
  35. data/lib/action_controller/vendor/html-scanner/html/version.rb +1 -1
  36. data/lib/action_controller/vendor/xml_node.rb +97 -0
  37. data/lib/action_controller/verification.rb +2 -0
  38. data/lib/action_pack/version.rb +3 -3
  39. data/lib/action_view.rb +0 -2
  40. data/lib/action_view/base.rb +109 -36
  41. data/lib/action_view/compiled_templates.rb +1 -1
  42. data/lib/action_view/helpers/active_record_helper.rb +4 -2
  43. data/lib/action_view/helpers/asset_tag_helper.rb +6 -7
  44. data/lib/action_view/helpers/capture_helper.rb +49 -12
  45. data/lib/action_view/helpers/date_helper.rb +14 -4
  46. data/lib/action_view/helpers/form_helper.rb +136 -20
  47. data/lib/action_view/helpers/form_options_helper.rb +29 -7
  48. data/lib/action_view/helpers/form_tag_helper.rb +22 -20
  49. data/lib/action_view/helpers/java_script_macros_helper.rb +29 -9
  50. data/lib/action_view/helpers/javascript_helper.rb +50 -446
  51. data/lib/action_view/helpers/javascripts/controls.js +95 -30
  52. data/lib/action_view/helpers/javascripts/dragdrop.js +161 -21
  53. data/lib/action_view/helpers/javascripts/effects.js +310 -211
  54. data/lib/action_view/helpers/javascripts/prototype.js +228 -28
  55. data/lib/action_view/helpers/number_helper.rb +9 -9
  56. data/lib/action_view/helpers/pagination_helper.rb +1 -1
  57. data/lib/action_view/helpers/prototype_helper.rb +900 -0
  58. data/lib/action_view/helpers/scriptaculous_helper.rb +135 -0
  59. data/lib/action_view/helpers/text_helper.rb +7 -6
  60. data/lib/action_view/helpers/url_helper.rb +23 -14
  61. data/lib/action_view/partials.rb +12 -4
  62. data/rakefile +13 -5
  63. data/test/abstract_unit.rb +4 -3
  64. data/test/active_record_unit.rb +88 -0
  65. data/test/{controller → activerecord}/active_record_assertions_test.rb +7 -50
  66. data/test/{controller → activerecord}/active_record_store_test.rb +27 -4
  67. data/test/activerecord/pagination_test.rb +161 -0
  68. data/test/controller/action_pack_assertions_test.rb +18 -15
  69. data/test/controller/base_test.rb +31 -42
  70. data/test/controller/benchmark_test.rb +8 -11
  71. data/test/controller/capture_test.rb +33 -1
  72. data/test/controller/cgi_test.rb +33 -0
  73. data/test/controller/custom_handler_test.rb +8 -0
  74. data/test/controller/fake_controllers.rb +9 -17
  75. data/test/controller/filters_test.rb +32 -3
  76. data/test/controller/flash_test.rb +26 -41
  77. data/test/controller/fragment_store_setting_test.rb +1 -1
  78. data/test/controller/layout_test.rb +73 -0
  79. data/test/controller/mime_responds_test.rb +257 -0
  80. data/test/controller/mime_type_test.rb +24 -0
  81. data/test/controller/new_render_test.rb +157 -1
  82. data/test/controller/redirect_test.rb +23 -0
  83. data/test/controller/render_test.rb +54 -56
  84. data/test/controller/request_test.rb +25 -0
  85. data/test/controller/routing_test.rb +74 -66
  86. data/test/controller/test_test.rb +66 -1
  87. data/test/controller/verification_test.rb +3 -1
  88. data/test/controller/webservice_test.rb +255 -0
  89. data/test/fixtures/companies.yml +24 -0
  90. data/test/fixtures/company.rb +9 -0
  91. data/test/fixtures/db_definitions/sqlite.sql +42 -0
  92. data/test/fixtures/developer.rb +7 -0
  93. data/test/fixtures/developers.yml +21 -0
  94. data/test/fixtures/developers_projects.yml +13 -0
  95. data/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml +1 -0
  96. data/test/fixtures/layout_tests/layouts/item.rhtml +1 -0
  97. data/test/fixtures/layout_tests/layouts/layout_test.rhtml +1 -0
  98. data/test/fixtures/layout_tests/layouts/third_party_template_library.mab +1 -0
  99. data/test/fixtures/layout_tests/views/hello.rhtml +1 -0
  100. data/test/fixtures/multipart/mona_lisa.jpg +0 -0
  101. data/test/fixtures/project.rb +3 -0
  102. data/test/fixtures/projects.yml +7 -0
  103. data/test/fixtures/replies.yml +13 -0
  104. data/test/fixtures/reply.rb +5 -0
  105. data/test/fixtures/respond_to/all_types_with_layout.rhtml +1 -0
  106. data/test/fixtures/respond_to/all_types_with_layout.rjs +1 -0
  107. data/test/fixtures/respond_to/layouts/standard.rhtml +1 -0
  108. data/test/fixtures/respond_to/using_defaults.rhtml +1 -0
  109. data/test/fixtures/respond_to/using_defaults.rjs +1 -0
  110. data/test/fixtures/respond_to/using_defaults.rxml +1 -0
  111. data/test/fixtures/respond_to/using_defaults_with_type_list.rhtml +1 -0
  112. data/test/fixtures/respond_to/using_defaults_with_type_list.rjs +1 -0
  113. data/test/fixtures/respond_to/using_defaults_with_type_list.rxml +1 -0
  114. data/test/fixtures/test/block_content_for.rhtml +2 -0
  115. data/test/fixtures/test/delete_with_js.rjs +2 -0
  116. data/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml +1 -0
  117. data/test/fixtures/test/enum_rjs_test.rjs +6 -0
  118. data/test/fixtures/test/erb_content_for.rhtml +2 -0
  119. data/test/fixtures/test/hello_world.rxml +3 -0
  120. data/test/fixtures/test/hello_world_with_layout_false.rhtml +1 -0
  121. data/test/fixtures/test/non_erb_block_content_for.rxml +4 -0
  122. data/test/fixtures/topic.rb +3 -0
  123. data/test/fixtures/topics.yml +22 -0
  124. data/test/template/active_record_helper_test.rb +4 -0
  125. data/test/template/asset_tag_helper_test.rb +7 -2
  126. data/test/template/date_helper_test.rb +39 -2
  127. data/test/template/form_helper_test.rb +238 -5
  128. data/test/template/form_options_helper_test.rb +78 -0
  129. data/test/template/form_tag_helper_test.rb +11 -0
  130. data/test/template/java_script_macros_helper_test.rb +51 -6
  131. data/test/template/javascript_helper_test.rb +7 -153
  132. data/test/template/number_helper_test.rb +14 -13
  133. data/test/template/prototype_helper_test.rb +423 -0
  134. data/test/template/scriptaculous_helper_test.rb +90 -0
  135. data/test/template/text_helper_test.rb +12 -9
  136. data/test/template/url_helper_test.rb +31 -15
  137. metadata +291 -246
  138. data/lib/action_controller/cgi_ext/multipart_progress.rb +0 -169
  139. data/lib/action_controller/upload_progress.rb +0 -473
  140. data/lib/action_controller/vendor/html-scanner/html/node.rb.rej +0 -17
  141. data/lib/action_view/helpers/upload_progress_helper.rb +0 -433
  142. data/lib/action_view/vendor/builder.rb +0 -13
  143. data/lib/action_view/vendor/builder/blankslate.rb +0 -53
  144. data/lib/action_view/vendor/builder/xmlbase.rb +0 -143
  145. data/lib/action_view/vendor/builder/xmlevents.rb +0 -63
  146. data/lib/action_view/vendor/builder/xmlmarkup.rb +0 -308
  147. data/test/controller/multipart_progress_testx.rb +0 -365
  148. data/test/controller/upload_progress_testx.rb +0 -89
  149. data/test/template/upload_progress_helper_testx.rb +0 -136
@@ -15,16 +15,11 @@ module ActionView
15
15
  # Options:
16
16
  # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
17
17
  # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
18
- def form_tag(url_for_options = {}, options = {}, *parameters_for_url)
18
+ def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc)
19
19
  html_options = { "method" => "post" }.merge(options.stringify_keys)
20
-
21
- if html_options["multipart"]
22
- html_options["enctype"] = "multipart/form-data"
23
- html_options.delete("multipart")
24
- end
25
-
20
+ html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
26
21
  html_options["action"] = url_for(url_for_options, *parameters_for_url)
27
- tag("form", html_options, true)
22
+ tag :form, html_options, true
28
23
  end
29
24
 
30
25
  alias_method :start_form_tag, :form_tag
@@ -47,7 +42,7 @@ module ActionView
47
42
  # Options:
48
43
  # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices.
49
44
  def select_tag(name, option_tags = nil, options = {})
50
- content_tag("select", option_tags, { "name" => name, "id" => name }.update(options.stringify_keys))
45
+ content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys)
51
46
  end
52
47
 
53
48
  # Creates a standard text field.
@@ -59,7 +54,7 @@ module ActionView
59
54
  #
60
55
  # A hash of standard HTML options for the tag.
61
56
  def text_field_tag(name, value = nil, options = {})
62
- tag("input", { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys))
57
+ tag :input, { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
63
58
  end
64
59
 
65
60
  # Creates a hidden field.
@@ -97,39 +92,46 @@ module ActionView
97
92
  # # Outputs <textarea name="body" id="body" cols="25" rows="10"></textarea>
98
93
  # <%= text_area_tag "body", nil, :size => "25x10" %>
99
94
  def text_area_tag(name, content = nil, options = {})
100
- options = options.stringify_keys
101
- if options["size"]
102
- options["cols"], options["rows"] = options["size"].split("x")
103
- options.delete("size")
95
+ options.stringify_keys!
96
+
97
+ if size = options.delete("size")
98
+ options["cols"], options["rows"] = size.split("x")
104
99
  end
105
100
 
106
- content_tag("textarea", content, { "name" => name, "id" => name }.update(options.stringify_keys))
101
+ content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys)
107
102
  end
108
103
 
109
104
  # Creates a check box.
110
105
  def check_box_tag(name, value = "1", checked = false, options = {})
111
106
  html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
112
107
  html_options["checked"] = "checked" if checked
113
- tag("input", html_options)
108
+ tag :input, html_options
114
109
  end
115
110
 
116
111
  # Creates a radio button.
117
112
  def radio_button_tag(name, value, checked = false, options = {})
118
113
  html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
119
114
  html_options["checked"] = "checked" if checked
120
- tag("input", html_options)
115
+ tag :input, html_options
121
116
  end
122
117
 
123
- # Creates a submit button with the text <tt>value</tt> as the caption.
118
+ # Creates a submit button with the text <tt>value</tt> as the caption. If options contains a pair with the key of "disable_with",
119
+ # then the value will be used to rename a disabled version of the submit button.
124
120
  def submit_tag(value = "Save changes", options = {})
125
- tag("input", { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys))
121
+ options.stringify_keys!
122
+
123
+ if disable_with = options.delete("disable_with")
124
+ options["onclick"] = "this.disabled=true;this.value='#{disable_with}';this.form.submit();#{options["onclick"]}"
125
+ end
126
+
127
+ tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys)
126
128
  end
127
129
 
128
130
  # Displays an image which when clicked will submit the form.
129
131
  #
130
132
  # <tt>source</tt> is passed to AssetTagHelper#image_path
131
133
  def image_submit_tag(source, options = {})
132
- tag("input", { "type" => "image", "src" => image_path(source) }.update(options.stringify_keys))
134
+ tag :input, { "type" => "image", "src" => image_path(source) }.update(options.stringify_keys)
133
135
  end
134
136
  end
135
137
  end
@@ -12,11 +12,11 @@ module ActionView
12
12
  #
13
13
  # A form is automatically created and displayed when the user clicks the element,
14
14
  # something like this:
15
- # <form id="myElement-in-place-edit-form" target="specified url">
16
- # <input name="value" text="The content of myElement"/>
17
- # <input type="submit" value="ok"/>
18
- # <a onclick="javascript to cancel the editing">cancel</a>
19
- # </form>
15
+ # <form id="myElement-in-place-edit-form" target="specified url">
16
+ # <input name="value" text="The content of myElement"/>
17
+ # <input type="submit" value="ok"/>
18
+ # <a onclick="javascript to cancel the editing">cancel</a>
19
+ # </form>
20
20
  #
21
21
  # The form is serialized and sent to the server using an AJAX call, the action on
22
22
  # the server should process the value and return the updated value in the body of
@@ -29,12 +29,17 @@ module ActionView
29
29
  #
30
30
  # Addtional +options+ are:
31
31
  # <tt>:rows</tt>:: Number of rows (more than 1 will use a TEXTAREA)
32
+ # <tt>:cols</tt>:: Number of characters the text input should span (works for both INPUT and TEXTAREA)
33
+ # <tt>:size</tt>:: Synonym for :cols when using a single line text input.
32
34
  # <tt>:cancel_text</tt>:: The text on the cancel link. (default: "cancel")
33
35
  # <tt>:save_text</tt>:: The text on the save link. (default: "ok")
36
+ # <tt>:loading_text</tt>:: The text to display when submitting to the server (default: "Saving...")
34
37
  # <tt>:external_control</tt>:: The id of an external control used to enter edit mode.
38
+ # <tt>:load_text_url</tt>:: URL where initial value of editor (content) is retrieved.
35
39
  # <tt>:options</tt>:: Pass through options to the AJAX call (see prototype's Ajax.Updater)
36
40
  # <tt>:with</tt>:: JavaScript snippet that should return what is to be sent
37
41
  # in the AJAX call, +form+ is an implicit parameter
42
+ # <tt>:script</tt>:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false)
38
43
  def in_place_editor(field_id, options = {})
39
44
  function = "new Ajax.InPlaceEditor("
40
45
  function << "'#{field_id}', "
@@ -43,9 +48,14 @@ module ActionView
43
48
  js_options = {}
44
49
  js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text]
45
50
  js_options['okText'] = %('#{options[:save_text]}') if options[:save_text]
51
+ js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text]
46
52
  js_options['rows'] = options[:rows] if options[:rows]
47
- js_options['externalControl'] = options[:external_control] if options[:external_control]
53
+ js_options['cols'] = options[:cols] if options[:cols]
54
+ js_options['size'] = options[:size] if options[:size]
55
+ js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control]
56
+ js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url]
48
57
  js_options['ajaxOptions'] = options[:options] if options[:options]
58
+ js_options['evalScripts'] = options[:script] if options[:script]
49
59
  js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with]
50
60
  function << (', ' + options_for_javascript(js_options)) unless js_options.empty?
51
61
 
@@ -72,8 +82,12 @@ module ActionView
72
82
  # or nothing if no entries should be displayed for autocompletion.
73
83
  #
74
84
  # You'll probably want to turn the browser's built-in autocompletion off,
75
- # su be sure to include a autocomplete="off" attribute with your text
85
+ # so be sure to include a autocomplete="off" attribute with your text
76
86
  # input field.
87
+ #
88
+ # The autocompleter object is assigned to a Javascript variable named <tt>field_id</tt>_auto_completer.
89
+ # This object is useful if you for example want to trigger the auto-complete suggestions through
90
+ # other means than user input (for that specific case, call the <tt>activate</tt> method on that object).
77
91
  #
78
92
  # Required +options+ are:
79
93
  # <tt>:url</tt>:: URL to call for autocompletion results
@@ -105,8 +119,11 @@ module ActionView
105
119
  # innerHTML is replaced.
106
120
  # <tt>:on_show</tt>:: Like on_hide, only now the expression is called
107
121
  # then the div is shown.
122
+ # <tt>:select</tt>:: Pick the class of the element from which the value for
123
+ # insertion should be extracted. If this is not specified,
124
+ # the entire element is used.
108
125
  def auto_complete_field(field_id, options = {})
109
- function = "new Ajax.Autocompleter("
126
+ function = "var #{field_id}_auto_completer = new Ajax.Autocompleter("
110
127
  function << "'#{field_id}', "
111
128
  function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', "
112
129
  function << "'#{url_for(options[:url])}'"
@@ -115,9 +132,12 @@ module ActionView
115
132
  js_options[:tokens] = array_or_string_for_javascript(options[:tokens]) if options[:tokens]
116
133
  js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
117
134
  js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
118
- {:on_show => :onShow, :on_hide => :onHide, :min_chars => :min_chars}.each do |k,v|
135
+ js_options[:select] = "'#{options[:select]}'" if options[:select]
136
+
137
+ { :on_show => :onShow, :on_hide => :onHide, :min_chars => :minChars }.each do |k,v|
119
138
  js_options[v] = options[k] if options[k]
120
139
  end
140
+
121
141
  function << (', ' + options_for_javascript(js_options) + ')')
122
142
 
123
143
  javascript_tag(function)
@@ -2,295 +2,80 @@ 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
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.
5
+ # Provides functionality for working with JavaScript in your views.
6
+ #
7
+ # == Ajax, controls and visual effects
8
+ #
9
+ # * For information on using Ajax, see
10
+ # ActionView::Helpers::PrototypeHelper.
11
+ # * For information on using controls and visual effects, see
12
+ # ActionView::Helpers::ScriptaculousHelper.
9
13
  #
10
- # To be able to use the JavaScript helpers, you must include the Prototype JavaScript Framework and for some functions
11
- # script.aculo.us (which both come with Rails) on your pages. Choose one of these options:
14
+ # == Including the JavaScript libraries into your pages
12
15
  #
13
- # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD section of your page (recommended):
14
- # The function will return references to the JavaScript files created by the +rails+ command in your
15
- # <tt>public/javascripts</tt> directory. Using it is recommended as the browser can then cache the libraries
16
- # instead of fetching all the functions anew on every request.
17
- # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but will only include the Prototype core library,
18
- # which means you are able to use all basic AJAX functionality. For the script.aculo.us-based JavaScript helpers,
19
- # like visual effects, autocompletion, drag and drop and so on, you should use the method described above.
20
- # * Use <tt><%= define_javascript_functions %></tt>: this will copy all the JavaScript support functions within a single
21
- # script block.
16
+ # Rails includes the Prototype JavaScript framework and the Scriptaculous
17
+ # JavaScript controls and visual effects library. If you wish to use
18
+ # these libraries and their helpers (ActionView::Helpers::PrototypeHelper
19
+ # and ActionView::Helpers::ScriptaculousHelper), you must do one of the
20
+ # following:
22
21
  #
23
- # For documentation on +javascript_include_tag+ see ActionView::Helpers::AssetTagHelper.
22
+ # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD
23
+ # section of your page (recommended): This function will return
24
+ # references to the JavaScript files created by the +rails+ command in
25
+ # your <tt>public/javascripts</tt> directory. Using it is recommended as
26
+ # the browser can then cache the libraries instead of fetching all the
27
+ # functions anew on every request.
28
+ # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but
29
+ # will only include the Prototype core library, which means you are able
30
+ # to use all basic AJAX functionality. For the Scriptaculous-based
31
+ # JavaScript helpers, like visual effects, autocompletion, drag and drop
32
+ # and so on, you should use the method described above.
33
+ # * Use <tt><%= define_javascript_functions %></tt>: this will copy all the
34
+ # JavaScript support functions within a single script block. Not
35
+ # recommended.
24
36
  #
25
- # If you're the visual type, there's an AJAX movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating
26
- # the use of form_remote_tag.
27
- module JavaScriptHelper
28
- unless const_defined? :CALLBACKS
29
- CALLBACKS =
30
- [:uninitialized, :loading, :loaded, :interactive, :complete, :failure, :success].push((100..599).to_a).flatten
31
- AJAX_OPTIONS = [ :before, :after, :condition, :url, :asynchronous, :method,
32
- :insertion, :position, :form, :with, :update, :script ].concat(CALLBACKS)
37
+ # For documentation on +javascript_include_tag+ see
38
+ # ActionView::Helpers::AssetTagHelper.
39
+ module JavaScriptHelper
40
+ unless const_defined? :JAVASCRIPT_PATH
33
41
  JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
34
42
  end
35
43
 
36
- # Returns a link that'll trigger a javascript +function+ using the
44
+ # Returns a link that'll trigger a JavaScript +function+ using the
37
45
  # onclick handler and return false after the fact.
38
46
  #
39
47
  # Examples:
40
48
  # link_to_function "Greeting", "alert('Hello world!')"
41
49
  # link_to_function(image_tag("delete"), "if confirm('Really?'){ do_delete(); }")
42
50
  def link_to_function(name, function, html_options = {})
51
+ html_options.symbolize_keys!
43
52
  content_tag(
44
53
  "a", name,
45
- {:href => "#", :onclick => "#{function}; return false;"}.merge(html_options.symbolize_keys)
54
+ html_options.merge({
55
+ :href => html_options[:href] || "#",
56
+ :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;"
57
+ })
46
58
  )
47
59
  end
48
-
49
- # Returns a link to a remote action defined by <tt>options[:url]</tt>
50
- # (using the url_for format) that's called in the background using
51
- # XMLHttpRequest. The result of that request can then be inserted into a
52
- # DOM object whose id can be specified with <tt>options[:update]</tt>.
53
- # Usually, the result would be a partial prepared by the controller with
54
- # either render_partial or render_partial_collection.
55
- #
56
- # Examples:
57
- # link_to_remote "Delete this post", :update => "posts", :url => { :action => "destroy", :id => post.id }
58
- # link_to_remote(image_tag("refresh"), :update => "emails", :url => { :action => "list_emails" })
59
- #
60
- # You can also specify a hash for <tt>options[:update]</tt> to allow for
61
- # easy redirection of output to an other DOM element if a server-side error occurs:
62
- #
63
- # Example:
64
- # link_to_remote "Delete this post",
65
- # :url => { :action => "destroy", :id => post.id },
66
- # :update => { :success => "posts", :failure => "error" }
67
- #
68
- # Optionally, you can use the <tt>options[:position]</tt> parameter to influence
69
- # how the target DOM element is updated. It must be one of
70
- # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
71
- #
72
- # By default, these remote requests are processed asynchronous during
73
- # which various JavaScript callbacks can be triggered (for progress indicators and
74
- # the likes). All callbacks get access to the <tt>request</tt> object,
75
- # which holds the underlying XMLHttpRequest.
76
- #
77
- # To access the server response, use <tt>request.responseText</tt>, to
78
- # find out the HTTP status, use <tt>request.status</tt>.
79
- #
80
- # Example:
81
- # link_to_remote word,
82
- # :url => { :action => "undo", :n => word_counter },
83
- # :complete => "undoRequestCompleted(request)"
84
- #
85
- # The callbacks that may be specified are (in order):
86
- #
87
- # <tt>:loading</tt>:: Called when the remote document is being
88
- # loaded with data by the browser.
89
- # <tt>:loaded</tt>:: Called when the browser has finished loading
90
- # the remote document.
91
- # <tt>:interactive</tt>:: Called when the user can interact with the
92
- # remote document, even though it has not
93
- # finished loading.
94
- # <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
95
- # and the HTTP status code is in the 2XX range.
96
- # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
97
- # and the HTTP status code is not in the 2XX
98
- # range.
99
- # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
100
- # (fires after success/failure if they are present).,
101
- #
102
- # You can further refine <tt>:success</tt> and <tt>:failure</tt> by adding additional
103
- # callbacks for specific status codes:
104
- #
105
- # Example:
106
- # link_to_remote word,
107
- # :url => { :action => "action" },
108
- # 404 => "alert('Not found...? Wrong URL...?')",
109
- # :failure => "alert('HTTP Error ' + request.status + '!')"
110
- #
111
- # A status code callback overrides the success/failure handlers if present.
112
- #
113
- # If you for some reason or another need synchronous processing (that'll
114
- # block the browser while the request is happening), you can specify
115
- # <tt>options[:type] = :synchronous</tt>.
116
- #
117
- # You can customize further browser side call logic by passing
118
- # in JavaScript code snippets via some optional parameters. In
119
- # their order of use these are:
120
- #
121
- # <tt>:confirm</tt>:: Adds confirmation dialog.
122
- # <tt>:condition</tt>:: Perform remote request conditionally
123
- # by this expression. Use this to
124
- # describe browser-side conditions when
125
- # request should not be initiated.
126
- # <tt>:before</tt>:: Called before request is initiated.
127
- # <tt>:after</tt>:: Called immediately after request was
128
- # initiated and before <tt>:loading</tt>.
129
- # <tt>:submit</tt>:: Specifies the DOM element ID that's used
130
- # as the parent of the form elements. By
131
- # default this is the current form, but
132
- # it could just as well be the ID of a
133
- # table row or any other DOM element.
134
- def link_to_remote(name, options = {}, html_options = {})
135
- link_to_function(name, remote_function(options), html_options)
136
- end
137
-
138
- # Periodically calls the specified url (<tt>options[:url]</tt>) every <tt>options[:frequency]</tt> seconds (default is 10).
139
- # Usually used to update a specified div (<tt>options[:update]</tt>) with the results of the remote call.
140
- # The options for specifying the target with :url and defining callbacks is the same as link_to_remote.
141
- def periodically_call_remote(options = {})
142
- frequency = options[:frequency] || 10 # every ten seconds by default
143
- code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
144
- javascript_tag(code)
145
- end
146
-
147
- # Returns a form tag that will submit using XMLHttpRequest in the background instead of the regular
148
- # reloading POST arrangement. Even though it's using JavaScript to serialize the form elements, the form submission
149
- # will work just like a regular submission as viewed by the receiving side (all elements available in @params).
150
- # The options for specifying the target with :url and defining callbacks is the same as link_to_remote.
151
- #
152
- # A "fall-through" target for browsers that doesn't do JavaScript can be specified with the :action/:method options on :html
153
- #
154
- # form_remote_tag :html => { :action => url_for(:controller => "some", :action => "place") }
155
- # The Hash passed to the :html key is equivalent to the options (2nd) argument in the FormTagHelper.form_tag method.
156
- #
157
- # By default the fall-through action is the same as the one specified in the :url (and the default method is :post).
158
- def form_remote_tag(options = {})
159
- options[:form] = true
160
-
161
- options[:html] ||= {}
162
- options[:html][:onsubmit] = "#{remote_function(options)}; return false;"
163
- options[:html][:action] = options[:html][:action] || url_for(options[:url])
164
- options[:html][:method] = options[:html][:method] || "post"
165
-
166
- tag("form", options[:html], true)
167
- end
168
60
 
169
- # Returns a button input tag that will submit form using XMLHttpRequest in the background instead of regular
170
- # reloading POST arrangement. <tt>options</tt> argument is the same as in <tt>form_remote_tag</tt>
171
- def submit_to_remote(name, value, options = {})
172
- options[:with] ||= 'Form.serialize(this.form)'
173
-
174
- options[:html] ||= {}
175
- options[:html][:type] = 'button'
176
- options[:html][:onclick] = "#{remote_function(options)}; return false;"
177
- options[:html][:name] = name
178
- options[:html][:value] = value
179
-
180
- tag("input", options[:html], false)
181
- end
182
-
183
- # Returns a Javascript function (or expression) that'll update a DOM element according to the options passed.
184
- #
185
- # * <tt>:content</tt>: The content to use for updating. Can be left out if using block, see example.
186
- # * <tt>:action</tt>: Valid options are :update (assumed by default), :empty, :remove
187
- # * <tt>:position</tt> If the :action is :update, you can optionally specify one of the following positions: :before, :top, :bottom, :after.
61
+ # Returns a link that'll trigger a JavaScript +function+ using the
62
+ # onclick handler.
188
63
  #
189
64
  # Examples:
190
- # <%= javascript_tag(update_element_function(
191
- # "products", :position => :bottom, :content => "<p>New product!</p>")) %>
192
- #
193
- # <% replacement_function = update_element_function("products") do %>
194
- # <p>Product 1</p>
195
- # <p>Product 2</p>
196
- # <% end %>
197
- # <%= javascript_tag(replacement_function) %>
198
- #
199
- # This method can also be used in combination with remote method call where the result is evaluated afterwards to cause
200
- # multiple updates on a page. Example:
201
- #
202
- # # Calling view
203
- # <%= form_remote_tag :url => { :action => "buy" }, :complete => evaluate_remote_response %>
204
- # all the inputs here...
205
- #
206
- # # Controller action
207
- # def buy
208
- # @product = Product.find(1)
209
- # end
210
- #
211
- # # Returning view
212
- # <%= update_element_function(
213
- # "cart", :action => :update, :position => :bottom,
214
- # :content => "<p>New Product: #{@product.name}</p>")) %>
215
- # <% update_element_function("status", :binding => binding) do %>
216
- # You've bought a new product!
217
- # <% end %>
218
- #
219
- # 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
220
- # to render directly. This trick will however only work in ERb (not Builder or other template forms).
221
- def update_element_function(element_id, options = {}, &block)
222
-
223
- content = escape_javascript(options[:content] || '')
224
- content = escape_javascript(capture(&block)) if block
225
-
226
- javascript_function = case (options[:action] || :update)
227
- when :update
228
- if options[:position]
229
- "new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')"
230
- else
231
- "$('#{element_id}').innerHTML = '#{content}'"
232
- end
233
-
234
- when :empty
235
- "$('#{element_id}').innerHTML = ''"
236
-
237
- when :remove
238
- "Element.remove('#{element_id}')"
239
-
240
- else
241
- raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty"
242
- end
243
-
244
- javascript_function << ";\n"
245
- options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function
246
- end
247
-
248
- # Returns 'eval(request.responseText)' which is the Javascript function that form_remote_tag can call in :complete to
249
- # evaluate a multiple update return document using update_element_function calls.
250
- def evaluate_remote_response
251
- "eval(request.responseText)"
252
- end
253
-
254
- # Returns the javascript needed for a remote function.
255
- # Takes the same arguments as link_to_remote.
256
- #
257
- # Example:
258
- # <select id="options" onchange="<%= remote_function(:update => "options", :url => { :action => :update_options }) %>">
259
- # <option value="0">Hello</option>
260
- # <option value="1">World</option>
261
- # </select>
262
- def remote_function(options)
263
- javascript_options = options_for_ajax(options)
264
-
265
- update = ''
266
- if options[:update] and options[:update].is_a?Hash
267
- update = []
268
- update << "success:'#{options[:update][:success]}'" if options[:update][:success]
269
- update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
270
- update = '{' + update.join(',') + '}'
271
- elsif options[:update]
272
- update << "'#{options[:update]}'"
273
- end
274
-
275
- function = update.empty? ?
276
- "new Ajax.Request(" :
277
- "new Ajax.Updater(#{update}, "
278
-
279
- function << "'#{url_for(options[:url])}'"
280
- function << ", #{javascript_options})"
281
-
282
- function = "#{options[:before]}; #{function}" if options[:before]
283
- function = "#{function}; #{options[:after]}" if options[:after]
284
- function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
285
- function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
286
-
287
- return function
65
+ # button_to_function "Greeting", "alert('Hello world!')"
66
+ # button_to_function "Delete", "if confirm('Really?'){ do_delete(); }")
67
+ def button_to_function(name, function, html_options = {})
68
+ html_options.symbolize_keys!
69
+ tag(:input, html_options.merge({
70
+ :type => "button", :value => name,
71
+ :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
72
+ }))
288
73
  end
289
74
 
290
75
  # Includes the Action Pack JavaScript libraries inside a single <script>
291
76
  # tag. The function first includes prototype.js and then its core extensions,
292
77
  # (determined by filenames starting with "prototype").
293
- # Afterwards, any additional scripts will be included in random order.
78
+ # Afterwards, any additional scripts will be included in undefined order.
294
79
  #
295
80
  # Note: The recommended approach is to copy the contents of
296
81
  # lib/action_view/helpers/javascripts/ into your application's
@@ -312,142 +97,6 @@ module ActionView
312
97
  javascript << '</script>'
313
98
  end
314
99
 
315
- # Observes the field with the DOM ID specified by +field_id+ and makes
316
- # an AJAX call when its contents have changed.
317
- #
318
- # Required +options+ are:
319
- # <tt>:url</tt>:: +url_for+-style options for the action to call
320
- # when the field has changed.
321
- #
322
- # Additional options are:
323
- # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
324
- # this field will be detected. Not setting this
325
- # option at all or to a value equal to or less than
326
- # zero will use event based observation instead of
327
- # time based observation.
328
- # <tt>:update</tt>:: Specifies the DOM ID of the element whose
329
- # innerHTML should be updated with the
330
- # XMLHttpRequest response text.
331
- # <tt>:with</tt>:: A JavaScript expression specifying the
332
- # parameters for the XMLHttpRequest. This defaults
333
- # to 'value', which in the evaluated context
334
- # refers to the new field value.
335
- #
336
- # Additionally, you may specify any of the options documented in
337
- # link_to_remote.
338
- def observe_field(field_id, options = {})
339
- if options[:frequency] and options[:frequency] > 0
340
- build_observer('Form.Element.Observer', field_id, options)
341
- else
342
- build_observer('Form.Element.EventObserver', field_id, options)
343
- end
344
- end
345
-
346
- # Like +observe_field+, but operates on an entire form identified by the
347
- # DOM ID +form_id+. +options+ are the same as +observe_field+, except
348
- # the default value of the <tt>:with</tt> option evaluates to the
349
- # serialized (request string) value of the form.
350
- def observe_form(form_id, options = {})
351
- if options[:frequency]
352
- build_observer('Form.Observer', form_id, options)
353
- else
354
- build_observer('Form.EventObserver', form_id, options)
355
- end
356
- end
357
-
358
- # Returns a JavaScript snippet to be used on the AJAX callbacks for starting
359
- # visual effects.
360
- #
361
- # This method requires the inclusion of the script.aculo.us JavaScript library.
362
- #
363
- # Example:
364
- # <%= link_to_remote "Reload", :update => "posts",
365
- # :url => { :action => "reload" },
366
- # :complete => visual_effect(:highlight, "posts", :duration => 0.5 )
367
- #
368
- # If no element_id is given, it assumes "element" which should be a local
369
- # variable in the generated JavaScript execution context. This can be used
370
- # for example with drop_receiving_element:
371
- #
372
- # <%= drop_receving_element (...), :loading => visual_effect(:fade) %>
373
- #
374
- # This would fade the element that was dropped on the drop receiving element.
375
- #
376
- # You can change the behaviour with various options, see
377
- # http://script.aculo.us for more documentation.
378
- def visual_effect(name, element_id = false, js_options = {})
379
- element = element_id ? "'#{element_id}'" : "element"
380
- js_options[:queue] = "'#{js_options[:queue]}'" if js_options[:queue]
381
- "new Effect.#{name.to_s.camelize}(#{element},#{options_for_javascript(js_options)});"
382
- end
383
-
384
- # Makes the element with the DOM ID specified by +element_id+ sortable
385
- # by drag-and-drop and make an AJAX call whenever the sort order has
386
- # changed. By default, the action called gets the serialized sortable
387
- # element as parameters.
388
- #
389
- # This method requires the inclusion of the script.aculo.us JavaScript library.
390
- #
391
- # Example:
392
- # <%= sortable_element("my_list", :url => { :action => "order" }) %>
393
- #
394
- # In the example, the action gets a "my_list" array parameter
395
- # containing the values of the ids of elements the sortable consists
396
- # of, in the current order.
397
- #
398
- # You can change the behaviour with various options, see
399
- # http://script.aculo.us for more documentation.
400
- def sortable_element(element_id, options = {})
401
- options[:with] ||= "Sortable.serialize('#{element_id}')"
402
- options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
403
- options.delete_if { |key, value| AJAX_OPTIONS.include?(key) }
404
-
405
- [:tag, :overlap, :constraint, :handle].each do |option|
406
- options[option] = "'#{options[option]}'" if options[option]
407
- end
408
-
409
- options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment]
410
- options[:only] = array_or_string_for_javascript(options[:only]) if options[:only]
411
-
412
- javascript_tag("Sortable.create('#{element_id}', #{options_for_javascript(options)})")
413
- end
414
-
415
- # Makes the element with the DOM ID specified by +element_id+ draggable.
416
- #
417
- # This method requires the inclusion of the script.aculo.us JavaScript library.
418
- #
419
- # Example:
420
- # <%= draggable_element("my_image", :revert => true)
421
- #
422
- # You can change the behaviour with various options, see
423
- # http://script.aculo.us for more documentation.
424
- def draggable_element(element_id, options = {})
425
- javascript_tag("new Draggable('#{element_id}', #{options_for_javascript(options)})")
426
- end
427
-
428
- # Makes the element with the DOM ID specified by +element_id+ receive
429
- # dropped draggable elements (created by draggable_element).
430
- # and make an AJAX call By default, the action called gets the DOM ID of the
431
- # element as parameter.
432
- #
433
- # This method requires the inclusion of the script.aculo.us JavaScript library.
434
- #
435
- # Example:
436
- # <%= drop_receiving_element("my_cart", :url => { :controller => "cart", :action => "add" }) %>
437
- #
438
- # You can change the behaviour with various options, see
439
- # http://script.aculo.us for more documentation.
440
- def drop_receiving_element(element_id, options = {})
441
- options[:with] ||= "'id=' + encodeURIComponent(element.id)"
442
- options[:onDrop] ||= "function(element){" + remote_function(options) + "}"
443
- options.delete_if { |key, value| AJAX_OPTIONS.include?(key) }
444
-
445
- options[:accept] = array_or_string_for_javascript(options[:accept]) if options[:accept]
446
- options[:hoverclass] = "'#{options[:hoverclass]}'" if options[:hoverclass]
447
-
448
- javascript_tag("Droppables.add('#{element_id}', #{options_for_javascript(options)})")
449
- end
450
-
451
100
  # Escape carrier returns and single and double quotes for JavaScript segments.
452
101
  def escape_javascript(javascript)
453
102
  (javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
@@ -463,7 +112,7 @@ module ActionView
463
112
  "\n//#{cdata_section("\n#{content}\n//")}\n"
464
113
  end
465
114
 
466
- private
115
+ protected
467
116
  def options_for_javascript(options)
468
117
  '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}'
469
118
  end
@@ -476,51 +125,6 @@ module ActionView
476
125
  end
477
126
  js_option
478
127
  end
479
-
480
- def options_for_ajax(options)
481
- js_options = build_callbacks(options)
482
-
483
- js_options['asynchronous'] = options[:type] != :synchronous
484
- js_options['method'] = method_option_to_s(options[:method]) if options[:method]
485
- js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
486
- js_options['evalScripts'] = options[:script].nil? || options[:script]
487
-
488
- if options[:form]
489
- js_options['parameters'] = 'Form.serialize(this)'
490
- elsif options[:submit]
491
- js_options['parameters'] = "Form.serialize(document.getElementById('#{options[:submit]}'))"
492
- elsif options[:with]
493
- js_options['parameters'] = options[:with]
494
- end
495
-
496
- options_for_javascript(js_options)
497
- end
498
-
499
- def method_option_to_s(method)
500
- (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
501
- end
502
-
503
- def build_observer(klass, name, options = {})
504
- options[:with] ||= 'value' if options[:update]
505
- callback = remote_function(options)
506
- javascript = "new #{klass}('#{name}', "
507
- javascript << "#{options[:frequency]}, " if options[:frequency]
508
- javascript << "function(element, value) {"
509
- javascript << "#{callback}})"
510
- javascript_tag(javascript)
511
- end
512
-
513
- def build_callbacks(options)
514
- callbacks = {}
515
- options.each do |callback, code|
516
- if CALLBACKS.include?(callback)
517
- name = 'on' + callback.to_s.capitalize
518
- callbacks[name] = "function(request){#{code}}"
519
- end
520
- end
521
- callbacks
522
- end
523
-
524
128
  end
525
129
 
526
130
  JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper