eita-jrails 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -0
  3. data/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  4. data/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png +0 -0
  5. data/assets/images/jquery-ui/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  6. data/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png +0 -0
  7. data/assets/images/jquery-ui/ui-bg_glass_75_dadada_1x400.png +0 -0
  8. data/assets/images/jquery-ui/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  9. data/assets/images/jquery-ui/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  10. data/assets/images/jquery-ui/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  11. data/assets/images/jquery-ui/ui-icons_222222_256x240.png +0 -0
  12. data/assets/images/jquery-ui/ui-icons_2e83ff_256x240.png +0 -0
  13. data/assets/images/jquery-ui/ui-icons_454545_256x240.png +0 -0
  14. data/assets/images/jquery-ui/ui-icons_888888_256x240.png +0 -0
  15. data/assets/images/jquery-ui/ui-icons_cd0a0a_256x240.png +0 -0
  16. data/assets/javascripts/jquery/jquery-ui-i18n.js +1379 -0
  17. data/assets/javascripts/jquery/jquery-ui-i18n.min.js +33 -0
  18. data/assets/javascripts/jrails/jrails.js +0 -12
  19. data/assets/stylesheets/smoothness/jquery-ui.css +573 -0
  20. data/lib/action_view/helpers/generator.rb +336 -0
  21. data/lib/action_view/helpers/jquery_helper.rb +558 -0
  22. data/lib/action_view/helpers/jquery_ui_helper.rb +165 -0
  23. data/lib/action_view/helpers/proxies.rb +187 -0
  24. data/lib/action_view/helpers/scriptaculous_helper.rb +263 -0
  25. data/lib/action_view/template/handlers/rjs.rb +14 -0
  26. data/lib/jrails.rb +3 -7
  27. data/lib/jrails/engine.rb +26 -0
  28. data/lib/jrails/javascript_helper.rb +97 -0
  29. data/lib/jrails/on_load_action_controller.rb +2 -0
  30. data/lib/jrails/on_load_action_view.rb +26 -0
  31. data/lib/jrails/renderers.rb +12 -0
  32. data/lib/jrails/rendering.rb +13 -0
  33. data/lib/jrails/selector_assertions.rb +211 -0
  34. metadata +37 -12
  35. data/README.rdoc +0 -27
  36. data/init.rb +0 -3
  37. data/install.rb +0 -2
  38. data/lib/jrails/jquery_selector_assertions.rb +0 -60
@@ -0,0 +1,336 @@
1
+ module ActionView
2
+ module Helpers
3
+ module JqueryHelper
4
+
5
+ class JavaScriptGenerator
6
+
7
+ def initialize(context, &block) #:nodoc:
8
+ @context, @lines = context, []
9
+ def @lines.encoding() last.to_s.encoding end
10
+ include_helpers_from_context
11
+ @context.with_output_buffer(@lines) do
12
+ @context.instance_exec(self, &block)
13
+ end
14
+ end
15
+
16
+ private
17
+ def include_helpers_from_context
18
+ extend @context.controller._helpers if @context.controller.respond_to?(:_helpers) && @context.controller._helpers
19
+ extend GeneratorMethods
20
+ end
21
+
22
+ module GeneratorMethods
23
+ def to_s #:nodoc:
24
+ (@lines * $/).tap do |javascript|
25
+ if ActionView::Base.debug_rjs
26
+ source = javascript.dup
27
+ javascript.replace "try {\n#{source}\n} catch (e) "
28
+ javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
29
+ end
30
+ end
31
+ end
32
+
33
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
34
+ # used for further method calls. Examples:
35
+ #
36
+ # page['blank_slate'] # => $('blank_slate');
37
+ # page['blank_slate'].show # => $('blank_slate').show();
38
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
39
+ #
40
+ # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
41
+ # the correct id:
42
+ #
43
+ # page[@post] # => $('post_45')
44
+ # page[Post.new] # => $('new_post')
45
+ def [](id)
46
+ case id
47
+ when String, Symbol, NilClass
48
+ JavaScriptElementProxy.new(self, id)
49
+ else
50
+ JavaScriptElementProxy.new(self, RecordIdentifier.dom_id(id))
51
+ end
52
+ end
53
+
54
+ RecordIdentifier =
55
+ if defined? ActionView::RecordIdentifier
56
+ ActionView::RecordIdentifier
57
+ else
58
+ ActionController::RecordIdentifier
59
+ end
60
+
61
+ # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
62
+ # expression as an argument to another JavaScriptGenerator method.
63
+ def literal(code)
64
+ JsonLiteral.new(code.to_s)
65
+ end
66
+
67
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
68
+ # used for further method calls. Examples:
69
+ #
70
+ # page.select('p') # => $$('p');
71
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
72
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
73
+ #
74
+ # You can also use prototype enumerations with the collection. Observe:
75
+ #
76
+ # # Generates: $$('#items li').each(function(value) { value.hide(); });
77
+ # page.select('#items li').each do |value|
78
+ # value.hide
79
+ # end
80
+ #
81
+ # Though you can call the block param anything you want, they are always rendered in the
82
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
83
+ #
84
+ # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
85
+ # page.select('#items li').collect('hidden') do |item|
86
+ # item.hide
87
+ # end
88
+ #
89
+ def select(pattern)
90
+ JavaScriptElementCollectionProxy.new(self, pattern)
91
+ end
92
+
93
+ # Inserts HTML at the specified +position+ relative to the DOM element
94
+ # identified by the given +id+.
95
+ #
96
+ # +position+ may be one of:
97
+ #
98
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
99
+ # element's existing content.
100
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
101
+ # element's existing content.
102
+ # <tt>:before</tt>:: HTML is inserted immediately preceding the element.
103
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
104
+ #
105
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
106
+ # of options to be passed to ActionView::Base#render. For example:
107
+ #
108
+ # # Insert the rendered 'navigation' partial just before the DOM
109
+ # # element with ID 'content'.
110
+ # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
111
+ # page.insert_html :before, 'content', :partial => 'navigation'
112
+ #
113
+ # # Add a list item to the bottom of the <ul> with ID 'list'.
114
+ # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
115
+ # page.insert_html :bottom, 'list', '<li>Last item</li>'
116
+ #
117
+ def insert_html(position, id, *options_for_render)
118
+ insertion = position.to_s.downcase
119
+ insertion = 'append' if insertion == 'bottom'
120
+ insertion = 'prepend' if insertion == 'top'
121
+ call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").#{insertion}", render(*options_for_render)
122
+ end
123
+
124
+ # Replaces the inner HTML of the DOM element with the given +id+.
125
+ #
126
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
127
+ # of options to be passed to ActionView::Base#render. For example:
128
+ #
129
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
130
+ # # 'person' partial for the appropriate object.
131
+ # # Generates: Element.update("person-45", "-- Contents of 'person' partial --");
132
+ # page.replace_html 'person-45', :partial => 'person', :object => @person
133
+ #
134
+ def replace_html(id, *options_for_render)
135
+ insert_html(:html, id, *options_for_render)
136
+ end
137
+
138
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
139
+ # contents) of the DOM element with the given +id+.
140
+ #
141
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
142
+ # of options to be passed to ActionView::Base#render. For example:
143
+ #
144
+ # # Replace the DOM element having ID 'person-45' with the
145
+ # # 'person' partial for the appropriate object.
146
+ # page.replace 'person-45', :partial => 'person', :object => @person
147
+ #
148
+ # This allows the same partial that is used for the +insert_html+ to
149
+ # be also used for the input to +replace+ without resorting to
150
+ # the use of wrapper elements.
151
+ #
152
+ # Examples:
153
+ #
154
+ # <div id="people">
155
+ # <%= render :partial => 'person', :collection => @people %>
156
+ # </div>
157
+ #
158
+ # # Insert a new person
159
+ # #
160
+ # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
161
+ # page.insert_html :bottom, :partial => 'person', :object => @person
162
+ #
163
+ # # Replace an existing person
164
+ #
165
+ # # Generates: Element.replace("person_45", "-- Contents of partial --");
166
+ # page.replace 'person_45', :partial => 'person', :object => @person
167
+ #
168
+ def replace(id, *options_for_render)
169
+ call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").replaceWith", render(*options_for_render)
170
+ end
171
+
172
+ # Removes the DOM elements with the given +ids+ from the page.
173
+ #
174
+ # Example:
175
+ #
176
+ # # Remove a few people
177
+ # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
178
+ # page.remove 'person_23', 'person_9', 'person_2'
179
+ #
180
+ def remove(*ids)
181
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").remove"
182
+ end
183
+
184
+ # Shows hidden DOM elements with the given +ids+.
185
+ #
186
+ # Example:
187
+ #
188
+ # # Show a few people
189
+ # # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
190
+ # page.show 'person_6', 'person_13', 'person_223'
191
+ #
192
+ def show(*ids)
193
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").show"
194
+ end
195
+
196
+ # Hides the visible DOM elements with the given +ids+.
197
+ #
198
+ # Example:
199
+ #
200
+ # # Hide a few people
201
+ # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
202
+ # page.hide 'person_29', 'person_9', 'person_0'
203
+ #
204
+ def hide(*ids)
205
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").hide"
206
+ end
207
+
208
+ # Hides the visible DOM elements with the given +ids+.
209
+ #
210
+ # Example:
211
+ #
212
+ # # Hide a few people
213
+ # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
214
+ # page.hide 'person_29', 'person_9', 'person_0'
215
+ #
216
+ def toggle(*ids)
217
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").toggle"
218
+ end
219
+
220
+ # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
221
+ #
222
+ # Examples:
223
+ #
224
+ # # Generates: window.location.href = "/mycontroller";
225
+ # page.redirect_to(:action => 'index')
226
+ #
227
+ # # Generates: window.location.href = "/account/signup";
228
+ # page.redirect_to(:controller => 'account', :action => 'signup')
229
+ def redirect_to(location)
230
+ url = location.is_a?(String) ? location : @context.url_for(location)
231
+ record "window.location.href = #{url.inspect}"
232
+ end
233
+
234
+ # Reloads the browser's current +location+ using JavaScript
235
+ #
236
+ # Examples:
237
+ #
238
+ # # Generates: window.location.reload();
239
+ # page.reload
240
+ def reload
241
+ record 'window.location.reload()'
242
+ end
243
+
244
+ # Calls the JavaScript +function+, optionally with the given +arguments+.
245
+ #
246
+ # If a block is given, the block will be passed to a new JavaScriptGenerator;
247
+ # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
248
+ # and passed as the called function's final argument.
249
+ #
250
+ # Examples:
251
+ #
252
+ # # Generates: Element.replace(my_element, "My content to replace with.")
253
+ # page.call 'Element.replace', 'my_element', "My content to replace with."
254
+ #
255
+ # # Generates: alert('My message!')
256
+ # page.call 'alert', 'My message!'
257
+ #
258
+ # # Generates:
259
+ # # my_method(function() {
260
+ # # $("one").show();
261
+ # # $("two").hide();
262
+ # # });
263
+ # page.call(:my_method) do |p|
264
+ # p[:one].show
265
+ # p[:two].hide
266
+ # end
267
+ def call(function, *arguments, &block)
268
+ record "#{function}(#{arguments_for_call(arguments, block)})"
269
+ end
270
+
271
+ # Assigns the JavaScript +variable+ the given +value+.
272
+ #
273
+ # Examples:
274
+ #
275
+ # # Generates: my_string = "This is mine!";
276
+ # page.assign 'my_string', 'This is mine!'
277
+ #
278
+ # # Generates: record_count = 33;
279
+ # page.assign 'record_count', 33
280
+ #
281
+ # # Generates: tabulated_total = 47
282
+ # page.assign 'tabulated_total', @total_from_cart
283
+ #
284
+ def assign(variable, value)
285
+ record "#{variable} = #{javascript_object_for(value)}"
286
+ end
287
+
288
+ # Writes raw JavaScript to the page.
289
+ #
290
+ # Example:
291
+ #
292
+ # page << "alert('JavaScript with Prototype.');"
293
+ def <<(javascript)
294
+ @lines << javascript
295
+ end
296
+
297
+ # Executes the content of the block after a delay of +seconds+. Example:
298
+ #
299
+ # # Generates:
300
+ # # setTimeout(function() {
301
+ # # ;
302
+ # # new Effect.Fade("notice",{});
303
+ # # }, 20000);
304
+ # page.delay(20) do
305
+ # page.visual_effect :fade, 'notice'
306
+ # end
307
+ def delay(seconds = 1)
308
+ record "setTimeout(function() {\n\n"
309
+ yield
310
+ record "}, #{(seconds * 1000).to_i})"
311
+ end
312
+
313
+ def javascript_object_for(object)
314
+ ::ActiveSupport::JSON.encode(object)
315
+ end
316
+
317
+ def arguments_for_call(arguments, block = nil)
318
+ arguments << block_to_function(block) if block
319
+ arguments.map { |argument| javascript_object_for(argument) }.join ', '
320
+ end
321
+
322
+ private
323
+ def jquery_id(id)
324
+ id.to_s.count('#.*,>+~:[/ ') == 0 ? "##{id}" : id
325
+ end
326
+
327
+ def jquery_ids(ids)
328
+ Array(ids).map{|id| jquery_id(id)}.join(',')
329
+ end
330
+
331
+ end
332
+ end
333
+
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,558 @@
1
+ require 'action_view/helpers/proxies'
2
+ require 'action_view/helpers/generator'
3
+
4
+ module ActionView
5
+ module Helpers
6
+ module JqueryHelper
7
+
8
+ JQUERY_VAR = ::JRails::JQUERY_VAR
9
+
10
+ USE_PROTECTION = const_defined?(:DISABLE_JQUERY_FORGERY_PROTECTION) ? !DISABLE_JQUERY_FORGERY_PROTECTION : true
11
+
12
+ unless const_defined? :JQCALLBACKS
13
+ JQCALLBACKS = Set.new([ :beforeSend, :complete, :error, :success ] + (100..599).to_a)
14
+ #instance_eval { remove_const :AJAX_OPTIONS }
15
+ remove_const(:AJAX_OPTIONS) if const_defined?(:AJAX_OPTIONS)
16
+ AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
17
+ :asynchronous, :method, :insertion, :position,
18
+ :form, :with, :update, :script ]).merge(JQCALLBACKS)
19
+ end
20
+
21
+ # Periodically calls the specified url (<tt>options[:url]</tt>) every
22
+ # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
23
+ # update a specified div (<tt>options[:update]</tt>) with the results
24
+ # of the remote call. The options for specifying the target with <tt>:url</tt>
25
+ # and defining callbacks is the same as link_to_remote.
26
+ # Examples:
27
+ # # Call get_averages and put its results in 'avg' every 10 seconds
28
+ # # Generates:
29
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
30
+ # # {asynchronous:true, evalScripts:true})}, 10)
31
+ # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
32
+ #
33
+ # # Call invoice every 10 seconds with the id of the customer
34
+ # # If it succeeds, update the invoice DIV; if it fails, update the error DIV
35
+ # # Generates:
36
+ # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
37
+ # # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
38
+ # periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
39
+ # :update => { :success => "invoice", :failure => "error" }
40
+ #
41
+ # # Call update every 20 seconds and update the new_block DIV
42
+ # # Generates:
43
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20)
44
+ # periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block')
45
+ #
46
+ def periodically_call_remote(options = {})
47
+ frequency = options[:frequency] || 10 # every ten seconds by default
48
+ code = "setInterval(function() {#{remote_function(options)}}, #{frequency} * 1000)"
49
+ javascript_tag(code)
50
+ end
51
+
52
+ def remote_function(options)
53
+ javascript_options = options_for_ajax(options)
54
+
55
+ update = ''
56
+ if options[:update] && options[:update].is_a?(Hash)
57
+ update = []
58
+ update << "success:'#{options[:update][:success]}'" if options[:update][:success]
59
+ update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
60
+ update = '{' + update.join(',') + '}'
61
+ elsif options[:update]
62
+ update << "'#{options[:update]}'"
63
+ end
64
+
65
+ function = "#{JQUERY_VAR}.ajax(#{javascript_options})"
66
+
67
+ function = "#{options[:before]}; #{function}" if options[:before]
68
+ function = "#{function}; #{options[:after]}" if options[:after]
69
+ function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
70
+ function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
71
+ return function
72
+ end
73
+
74
+ # Creates a button with an onclick event which calls a remote action
75
+ # via XMLHttpRequest
76
+ # The options for specifying the target with :url
77
+ # and defining callbacks is the same as link_to_remote.
78
+ def button_to_remote(name, options = {}, html_options = {})
79
+ button_to_function(name, remote_function(options), html_options)
80
+ end
81
+
82
+ # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+
83
+ # that will submit form using XMLHttpRequest in the background instead of a regular POST request that
84
+ # reloads the page.
85
+ #
86
+ # # Create a button that submits to the create action
87
+ # #
88
+ # # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
89
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
90
+ # # return false;" type="button" value="Create" />
91
+ # <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %>
92
+ #
93
+ # # Submit to the remote action update and update the DIV succeed or fail based
94
+ # # on the success or failure of the request
95
+ # #
96
+ # # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
97
+ # # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
98
+ # # return false;" type="button" value="Update" />
99
+ # <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
100
+ # :update => { :success => "succeed", :failure => "fail" }
101
+ #
102
+ # <tt>options</tt> argument is the same as in form_remote_tag.
103
+ def submit_to_remote(name, value, options = {})
104
+ options[:with] ||= "#{JQUERY_VAR}(this.form).serialize()"
105
+
106
+ html_options = options.delete(:html) || {}
107
+ html_options[:name] = name
108
+
109
+ button_to_remote(value, options, html_options)
110
+ end
111
+
112
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
113
+ # (using the url_for format) that's called in the background using
114
+ # XMLHttpRequest. The result of that request can then be inserted into a
115
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
116
+ # Usually, the result would be a partial prepared by the controller with
117
+ # render :partial.
118
+ #
119
+ # Examples:
120
+ # # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
121
+ # # return false;">Delete this post</a>
122
+ # link_to_remote "Delete this post", :update => "posts",
123
+ # :url => { :action => "destroy", :id => post.id }
124
+ #
125
+ # # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
126
+ # # return false;"><img alt="Refresh" src="/images/refresh.png?" /></a>
127
+ # link_to_remote(image_tag("refresh"), :update => "emails",
128
+ # :url => { :action => "list_emails" })
129
+ #
130
+ # You can override the generated HTML options by specifying a hash in
131
+ # <tt>options[:html]</tt>.
132
+ #
133
+ # link_to_remote "Delete this post", :update => "posts",
134
+ # :url => post_url(@post), :method => :delete,
135
+ # :html => { :class => "destructive" }
136
+ #
137
+ # You can also specify a hash for <tt>options[:update]</tt> to allow for
138
+ # easy redirection of output to an other DOM element if a server-side
139
+ # error occurs:
140
+ #
141
+ # Example:
142
+ # # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
143
+ # # {asynchronous:true, evalScripts:true}); return false;">Delete this post</a>
144
+ # link_to_remote "Delete this post",
145
+ # :url => { :action => "destroy", :id => post.id },
146
+ # :update => { :success => "posts", :failure => "error" }
147
+ #
148
+ # Optionally, you can use the <tt>options[:position]</tt> parameter to
149
+ # influence how the target DOM element is updated. It must be one of
150
+ # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
151
+ #
152
+ # The method used is by default POST. You can also specify GET or you
153
+ # can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
154
+ #
155
+ # Example:
156
+ # # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
157
+ # # return false;">Destroy</a>
158
+ # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
159
+ #
160
+ # By default, these remote requests are processed asynchronous during
161
+ # which various JavaScript callbacks can be triggered (for progress
162
+ # indicators and the likes). All callbacks get access to the
163
+ # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
164
+ #
165
+ # To access the server response, use <tt>request.responseText</tt>, to
166
+ # find out the HTTP status, use <tt>request.status</tt>.
167
+ #
168
+ # Example:
169
+ # # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
170
+ # # onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a>
171
+ # word = 'hello'
172
+ # link_to_remote word,
173
+ # :url => { :action => "undo", :n => word_counter },
174
+ # :complete => "undoRequestCompleted(request)"
175
+ #
176
+ # The callbacks that may be specified are (in order):
177
+ #
178
+ # <tt>:loading</tt>:: Called when the remote document is being
179
+ # loaded with data by the browser.
180
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
181
+ # the remote document.
182
+ # <tt>:interactive</tt>:: Called when the user can interact with the
183
+ # remote document, even though it has not
184
+ # finished loading.
185
+ # <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
186
+ # and the HTTP status code is in the 2XX range.
187
+ # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
188
+ # and the HTTP status code is not in the 2XX
189
+ # range.
190
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
191
+ # (fires after success/failure if they are
192
+ # present).
193
+ #
194
+ # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
195
+ # adding additional callbacks for specific status codes.
196
+ #
197
+ # Example:
198
+ # # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
199
+ # # on404:function(request){alert('Not found...? Wrong URL...?')},
200
+ # # onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
201
+ # link_to_remote word,
202
+ # :url => { :action => "action" },
203
+ # 404 => "alert('Not found...? Wrong URL...?')",
204
+ # :failure => "alert('HTTP Error ' + request.status + '!')"
205
+ #
206
+ # A status code callback overrides the success/failure handlers if
207
+ # present.
208
+ #
209
+ # If you for some reason or another need synchronous processing (that'll
210
+ # block the browser while the request is happening), you can specify
211
+ # <tt>options[:type] = :synchronous</tt>.
212
+ #
213
+ # You can customize further browser side call logic by passing in
214
+ # JavaScript code snippets via some optional parameters. In their order
215
+ # of use these are:
216
+ #
217
+ # <tt>:confirm</tt>:: Adds confirmation dialog.
218
+ # <tt>:condition</tt>:: Perform remote request conditionally
219
+ # by this expression. Use this to
220
+ # describe browser-side conditions when
221
+ # request should not be initiated.
222
+ # <tt>:before</tt>:: Called before request is initiated.
223
+ # <tt>:after</tt>:: Called immediately after request was
224
+ # initiated and before <tt>:loading</tt>.
225
+ # <tt>:submit</tt>:: Specifies the DOM element ID that's used
226
+ # as the parent of the form elements. By
227
+ # default this is the current form, but
228
+ # it could just as well be the ID of a
229
+ # table row or any other DOM element.
230
+ # <tt>:with</tt>:: A JavaScript expression specifying
231
+ # the parameters for the XMLHttpRequest.
232
+ # Any expressions should return a valid
233
+ # URL query string.
234
+ #
235
+ # Example:
236
+ #
237
+ # :with => "'name=' + $('name').value"
238
+ #
239
+ # You can generate a link that uses AJAX in the general case, while
240
+ # degrading gracefully to plain link behavior in the absence of
241
+ # JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL.
242
+ # Note the extra curly braces around the <tt>options</tt> hash separate
243
+ # it as the second parameter from <tt>html_options</tt>, the third.
244
+ #
245
+ # Example:
246
+ # link_to_remote "Delete this post",
247
+ # { :update => "posts", :url => { :action => "destroy", :id => post.id } },
248
+ # :href => url_for(:action => "destroy", :id => post.id)
249
+ def link_to_remote(name, options = {}, html_options = nil)
250
+ link_to_function(name, remote_function(options), html_options || options.delete(:html))
251
+ end
252
+
253
+ # Returns a form tag that will submit using XMLHttpRequest in the
254
+ # background instead of the regular reloading POST arrangement. Even
255
+ # though it's using JavaScript to serialize the form elements, the form
256
+ # submission will work just like a regular submission as viewed by the
257
+ # receiving side (all elements available in <tt>params</tt>). The options for
258
+ # specifying the target with <tt>:url</tt> and defining callbacks is the same as
259
+ # +link_to_remote+.
260
+ #
261
+ # A "fall-through" target for browsers that doesn't do JavaScript can be
262
+ # specified with the <tt>:action</tt>/<tt>:method</tt> options on <tt>:html</tt>.
263
+ #
264
+ # Example:
265
+ # # Generates:
266
+ # # <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
267
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
268
+ # form_remote_tag :html => { :action =>
269
+ # url_for(:controller => "some", :action => "place") }
270
+ #
271
+ # The Hash passed to the <tt>:html</tt> key is equivalent to the options (2nd)
272
+ # argument in the FormTagHelper.form_tag method.
273
+ #
274
+ # By default the fall-through action is the same as the one specified in
275
+ # the <tt>:url</tt> (and the default method is <tt>:post</tt>).
276
+ #
277
+ # form_remote_tag also takes a block, like form_tag:
278
+ # # Generates:
279
+ # # <form action="/" method="post" onsubmit="new Ajax.Request('/',
280
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
281
+ # # return false;"> <div><input name="commit" type="submit" value="Save" /></div>
282
+ # # </form>
283
+ # <% form_remote_tag :url => '/posts' do -%>
284
+ # <div><%= submit_tag 'Save' %></div>
285
+ # <% end -%>
286
+ def form_remote_tag(options = {}, &block)
287
+ options[:form] = true
288
+
289
+ options[:html] ||= {}
290
+ options[:html][:onsubmit] =
291
+ (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
292
+ "#{remote_function(options)}; return false;"
293
+
294
+ form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
295
+ end
296
+
297
+ # Creates a form that will submit using XMLHttpRequest in the background
298
+ # instead of the regular reloading POST arrangement and a scope around a
299
+ # specific resource that is used as a base for questioning about
300
+ # values for the fields.
301
+ #
302
+ # === Resource
303
+ #
304
+ # Example:
305
+ # <% remote_form_for(@post) do |f| %>
306
+ # ...
307
+ # <% end %>
308
+ #
309
+ # This will expand to be the same as:
310
+ #
311
+ # <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
312
+ # ...
313
+ # <% end %>
314
+ #
315
+ # === Nested Resource
316
+ #
317
+ # Example:
318
+ # <% remote_form_for([@post, @comment]) do |f| %>
319
+ # ...
320
+ # <% end %>
321
+ #
322
+ # This will expand to be the same as:
323
+ #
324
+ # <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %>
325
+ # ...
326
+ # <% end %>
327
+ #
328
+ # If you don't need to attach a form to a resource, then check out form_remote_tag.
329
+ #
330
+ # See FormHelper#form_for for additional semantics.
331
+ def remote_form_for(record_or_name_or_array, *args, &proc)
332
+ options = args.extract_options!
333
+
334
+ case record_or_name_or_array
335
+ when String, Symbol
336
+ object_name = record_or_name_or_array
337
+ when Array
338
+ object = record_or_name_or_array.last
339
+ object_name = ActiveModel::Naming.singular(object)
340
+ apply_form_for_options!(record_or_name_or_array, options)
341
+ args.unshift object
342
+ else
343
+ object = record_or_name_or_array
344
+ object_name = ActiveModel::Naming.singular(record_or_name_or_array)
345
+ apply_form_for_options!(object, options)
346
+ args.unshift object
347
+ end
348
+
349
+ form_remote_tag options do
350
+ fields_for object_name, *(args << options), &proc
351
+ end
352
+ end
353
+ alias_method :form_remote_for, :remote_form_for
354
+
355
+ # Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
356
+ # that +form_remote_tag+ can call in <tt>:complete</tt> to evaluate a multiple
357
+ # update return document using +update_element_function+ calls.
358
+ def evaluate_remote_response
359
+ "#{JQUERY_VAR}.globalEval(request.responseText)"
360
+ end
361
+
362
+ # Observes the field with the DOM ID specified by +field_id+ and calls a
363
+ # callback when its contents have changed. The default callback is an
364
+ # Ajax call. By default the value of the observed field is sent as a
365
+ # parameter with the Ajax call.
366
+ #
367
+ # Example:
368
+ # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
369
+ # # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})})
370
+ # <%= observe_field :suggest, :url => { :action => :find_suggestion },
371
+ # :frequency => 0.25,
372
+ # :update => :suggest,
373
+ # :with => 'q'
374
+ # %>
375
+ #
376
+ # Required +options+ are either of:
377
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
378
+ # when the field has changed.
379
+ # <tt>:function</tt>:: Instead of making a remote call to a URL, you
380
+ # can specify javascript code to be called instead.
381
+ # Note that the value of this option is used as the
382
+ # *body* of the javascript function, a function definition
383
+ # with parameters named element and value will be generated for you
384
+ # for example:
385
+ # observe_field("glass", :frequency => 1, :function => "alert('Element changed')")
386
+ # will generate:
387
+ # new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')})
388
+ # The element parameter is the DOM element being observed, and the value is its value at the
389
+ # time the observer is triggered.
390
+ #
391
+ # Additional options are:
392
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
393
+ # this field will be detected. Not setting this
394
+ # option at all or to a value equal to or less than
395
+ # zero will use event based observation instead of
396
+ # time based observation.
397
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
398
+ # innerHTML should be updated with the
399
+ # XMLHttpRequest response text.
400
+ # <tt>:with</tt>:: A JavaScript expression specifying the parameters
401
+ # for the XMLHttpRequest. The default is to send the
402
+ # key and value of the observed field. Any custom
403
+ # expressions should return a valid URL query string.
404
+ # The value of the field is stored in the JavaScript
405
+ # variable +value+.
406
+ #
407
+ # Examples
408
+ #
409
+ # :with => "'my_custom_key=' + value"
410
+ # :with => "'person[name]=' + prompt('New name')"
411
+ # :with => "Form.Element.serialize('other-field')"
412
+ #
413
+ # Finally
414
+ # :with => 'name'
415
+ # is shorthand for
416
+ # :with => "'name=' + value"
417
+ # This essentially just changes the key of the parameter.
418
+ #
419
+ # Additionally, you may specify any of the options documented in the
420
+ # <em>Common options</em> section at the top of this document.
421
+ #
422
+ # Example:
423
+ #
424
+ # # Sends params: {:title => 'Title of the book'} when the book_title input
425
+ # # field is changed.
426
+ # observe_field 'book_title',
427
+ # :url => 'http://example.com/books/edit/1',
428
+ # :with => 'title'
429
+ #
430
+ #
431
+ def observe_field(field_id, options = {})
432
+ build_observer(field_id, options)
433
+ end
434
+
435
+ # Observes the form with the DOM ID specified by +form_id+ and calls a
436
+ # callback when its contents have changed. The default callback is an
437
+ # Ajax call. By default all fields of the observed field are sent as
438
+ # parameters with the Ajax call.
439
+ #
440
+ # The +options+ for +observe_form+ are the same as the options for
441
+ # +observe_field+. The JavaScript variable +value+ available to the
442
+ # <tt>:with</tt> option is set to the serialized form by default.
443
+ def observe_form(form_id, options = {})
444
+ build_observer(form_id, options)
445
+ end
446
+
447
+ protected
448
+ def options_for_ajax(options)
449
+ js_options = build_callbacks(options)
450
+
451
+ url_options = options[:url]
452
+ url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash)
453
+ js_options['url'] = "'#{url_for(url_options)}'"
454
+ js_options['async'] = false if options[:type] == :synchronous
455
+ js_options['type'] = options[:method] ? method_option_to_s(options[:method]) : ( options[:form] ? "'post'" : nil )
456
+ js_options['dataType'] = options[:datatype] ? "'#{options[:datatype]}'" : (options[:update] ? "'html'" : "'script'")
457
+
458
+ if options[:form]
459
+ js_options['data'] = "#{JQUERY_VAR}.param(#{JQUERY_VAR}(this).serializeArray())"
460
+ elsif options[:submit]
461
+ js_options['data'] = "#{JQUERY_VAR}(\"##{options[:submit]}:input\").serialize()"
462
+ elsif options[:with]
463
+ js_options['data'] = options[:with].gsub("Form.serialize(this.form)","#{JQUERY_VAR}.param(#{JQUERY_VAR}(this.form).serializeArray())")
464
+ end
465
+
466
+ js_options['type'] ||= "'post'"
467
+ if options[:method]
468
+ if method_option_to_s(options[:method]) == "'put'" || method_option_to_s(options[:method]) == "'delete'"
469
+ js_options['type'] = "'post'"
470
+ if js_options['data']
471
+ js_options['data'] << " + '&"
472
+ else
473
+ js_options['data'] = "'"
474
+ end
475
+ js_options['data'] << "_method=#{options[:method]}'"
476
+ end
477
+ end
478
+
479
+ if USE_PROTECTION && respond_to?('protect_against_forgery?') && protect_against_forgery?
480
+ if js_options['data']
481
+ js_options['data'] << " + '&"
482
+ else
483
+ js_options['data'] = "'"
484
+ end
485
+ js_options['data'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')"
486
+ end
487
+ js_options['data'] = "''" if js_options['type'] == "'post'" && js_options['data'].nil?
488
+ options_for_javascript(js_options.reject {|key, value| value.nil?})
489
+ end
490
+
491
+ def build_update_for_success(html_id, insertion=nil)
492
+ insertion = build_insertion(insertion)
493
+ "#{JQUERY_VAR}('#{jquery_id(html_id)}').#{insertion}(request);"
494
+ end
495
+
496
+ def build_update_for_error(html_id, insertion=nil)
497
+ insertion = build_insertion(insertion)
498
+ "#{JQUERY_VAR}('#{jquery_id(html_id)}').#{insertion}(request.responseText);"
499
+ end
500
+
501
+ def build_insertion(insertion)
502
+ insertion = insertion ? insertion.to_s.downcase : 'html'
503
+ insertion = 'append' if insertion == 'bottom'
504
+ insertion = 'prepend' if insertion == 'top'
505
+ insertion
506
+ end
507
+
508
+ def build_observer(name, options = {})
509
+ if options[:with] && (options[:with] !~ /[\{=(.]/)
510
+ options[:with] = "'#{options[:with]}=' + value"
511
+ else
512
+ options[:with] ||= 'value' unless options[:function]
513
+ end
514
+
515
+ callback = options[:function] || remote_function(options)
516
+ javascript = "#{JQUERY_VAR}('#{jquery_id(name)}').delayedObserver("
517
+ javascript << "#{options[:frequency] || 0}, "
518
+ javascript << "function(element, value) {"
519
+ javascript << "#{callback}}"
520
+ #javascript << ", '#{options[:on]}'" if options[:on]
521
+ javascript << ")"
522
+ javascript_tag(javascript)
523
+ end
524
+
525
+ def build_callbacks(options)
526
+ callbacks = {}
527
+ options[:beforeSend] = '';
528
+ [:uninitialized,:loading].each do |key|
529
+ options[:beforeSend] << (options[key].last == ';' ? options.delete(key) : options.delete(key) << ';') if options[key]
530
+ end
531
+ options.delete(:beforeSend) if options[:beforeSend].blank?
532
+ options[:complete] = options.delete(:loaded) if options[:loaded]
533
+ options[:error] = options.delete(:failure) if options[:failure]
534
+ if options[:update]
535
+ if options[:update].is_a?(Hash)
536
+ options[:update][:error] = options[:update].delete(:failure) if options[:update][:failure]
537
+ if options[:update][:success]
538
+ options[:success] = build_update_for_success(options[:update][:success], options[:position]) << (options[:success] ? options[:success] : '')
539
+ end
540
+ if options[:update][:error]
541
+ options[:error] = build_update_for_error(options[:update][:error], options[:position]) << (options[:error] ? options[:error] : '')
542
+ end
543
+ else
544
+ options[:success] = build_update_for_success(options[:update], options[:position]) << (options[:success] ? options[:success] : '')
545
+ end
546
+ end
547
+ options.each do |callback, code|
548
+ if JQCALLBACKS.include?(callback)
549
+ callbacks[callback] = "function(request){#{code}}"
550
+ end
551
+ end
552
+ callbacks
553
+ end
554
+
555
+ end
556
+
557
+ end
558
+ end