prototype-rails 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/Gemfile +7 -0
  2. data/README +20 -0
  3. data/Rakefile +10 -0
  4. data/lib/action_view/helpers/prototype_helper.rb +852 -0
  5. data/lib/action_view/helpers/scriptaculous_helper.rb +263 -0
  6. data/lib/action_view/template/handlers/rjs.rb +14 -0
  7. data/lib/prototype-rails.rb +17 -0
  8. data/lib/prototype-rails/javascript_helper.rb +65 -0
  9. data/lib/prototype-rails/on_load_action_controller.rb +2 -0
  10. data/lib/prototype-rails/on_load_action_view.rb +22 -0
  11. data/lib/prototype-rails/renderers.rb +12 -0
  12. data/lib/prototype-rails/rendering.rb +13 -0
  13. data/lib/prototype-rails/selector_assertions.rb +208 -0
  14. data/test/abstract_unit.rb +235 -0
  15. data/test/assert_select_test.rb +452 -0
  16. data/test/controller/caching_test.rb +46 -0
  17. data/test/controller/content_type_test.rb +16 -0
  18. data/test/controller/mime_responds_test.rb +213 -0
  19. data/test/controller/new_base/content_type_test.rb +19 -0
  20. data/test/controller/new_base/render_rjs_test.rb +71 -0
  21. data/test/controller/render_js_test.rb +22 -0
  22. data/test/fixtures/functional_caching/_partial.erb +3 -0
  23. data/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs +6 -0
  24. data/test/fixtures/functional_caching/js_fragment_cached_with_partial.js.rjs +1 -0
  25. data/test/fixtures/old_content_type/render_default_for_rjs.rjs +1 -0
  26. data/test/fixtures/respond_to/all_types_with_layout.js.rjs +1 -0
  27. data/test/fixtures/respond_to/layouts/standard.html.erb +1 -0
  28. data/test/fixtures/respond_to/using_defaults.js.rjs +1 -0
  29. data/test/fixtures/respond_to/using_defaults_with_type_list.js.rjs +1 -0
  30. data/test/fixtures/respond_with/using_resource.js.rjs +1 -0
  31. data/test/fixtures/test/_one.html.erb +1 -0
  32. data/test/fixtures/test/_partial.html.erb +1 -0
  33. data/test/fixtures/test/_partial.js.erb +1 -0
  34. data/test/fixtures/test/_two.html.erb +1 -0
  35. data/test/fixtures/test/delete_with_js.rjs +2 -0
  36. data/test/fixtures/test/enum_rjs_test.rjs +6 -0
  37. data/test/fixtures/test/greeting.js.rjs +1 -0
  38. data/test/fixtures/test/render_explicit_html_template.js.rjs +1 -0
  39. data/test/fixtures/test/render_implicit_html_template.js.rjs +1 -0
  40. data/test/javascript_helper_test.rb +61 -0
  41. data/test/lib/controller/fake_models.rb +29 -0
  42. data/test/render_other_test.rb +257 -0
  43. data/test/template/prototype_helper_test.rb +476 -0
  44. data/test/template/render_test.rb +24 -0
  45. data/test/template/scriptaculous_helper_test.rb +86 -0
  46. data/vendor/assets/javascripts/controls.js +965 -0
  47. data/vendor/assets/javascripts/dragdrop.js +974 -0
  48. data/vendor/assets/javascripts/effects.js +1123 -0
  49. data/vendor/assets/javascripts/prototype.js +6082 -0
  50. data/vendor/assets/javascripts/prototype_ujs.js +208 -0
  51. metadata +127 -0
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gem 'rails', :git => 'git://github.com/rails/rails.git'
4
+ gem "rack", :git => "git://github.com/rack/rack.git"
5
+ gem "rack-test", :git => "git://github.com/brynary/rack-test.git"
6
+ gem 'mocha'
7
+
data/README ADDED
@@ -0,0 +1,20 @@
1
+ prototype-rails provides Prototype, Scriptaculous, and RJS for Rails 3.1.
2
+
3
+ Prototype and Scriptaculous are provided via the asset pipeline and you
4
+ do *not* need to copy their files into your application. Rails will get
5
+ them from prototype-rails automatically.
6
+
7
+ You may want to add them to you app/assets/javascripts/application.js:
8
+
9
+ //= require prototype
10
+ //= require prototype_ujs
11
+ //= require effects
12
+ //= require dragdrop
13
+ //= require controls
14
+
15
+ New applications using this may also want to add
16
+
17
+ config.action_view.debug_rjs = true
18
+
19
+ to their config/environments/development.rb.
20
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = Dir.glob('test/*_test.rb') + Dir.glob('test/{controller,template}/**/*_test.rb')
8
+ t.warning = true
9
+ t.verbose = true
10
+ end
@@ -0,0 +1,852 @@
1
+ require 'set'
2
+ require 'active_support/json'
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/core_ext/string/output_safety'
5
+
6
+ module ActionView
7
+ # = Action View Prototype Helpers
8
+ module Helpers
9
+ # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides
10
+ # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation,
11
+ # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]
12
+ # functionality, and more traditional object-oriented facilities for JavaScript.
13
+ # This module provides a set of helpers to make it more convenient to call
14
+ # functions from Prototype using Rails, including functionality to call remote
15
+ # Rails methods (that is, making a background request to a Rails action) using Ajax.
16
+ # This means that you can call actions in your controllers without
17
+ # reloading the page, but still update certain parts of it using
18
+ # injections into the DOM. A common use case is having a form that adds
19
+ # a new element to a list without reloading the page or updating a shopping
20
+ # cart total when a new item is added.
21
+ #
22
+ # == Usage
23
+ # To be able to use these helpers, you must first include the Prototype
24
+ # JavaScript framework in your pages.
25
+ #
26
+ # javascript_include_tag 'prototype'
27
+ #
28
+ # (See the documentation for
29
+ # ActionView::Helpers::JavaScriptHelper for more information on including
30
+ # this and other JavaScript files in your Rails templates.)
31
+ #
32
+ # Now you're ready to call a remote action either through a link...
33
+ #
34
+ # link_to_remote "Add to cart",
35
+ # :url => { :action => "add", :id => product.id },
36
+ # :update => { :success => "cart", :failure => "error" }
37
+ #
38
+ # ...through a form...
39
+ #
40
+ # <%= form_remote_tag :url => '/shipping' do -%>
41
+ # <div><%= submit_tag 'Recalculate Shipping' %></div>
42
+ # <% end -%>
43
+ #
44
+ # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than
45
+ # are listed here); check out the documentation for each method to find out more about its usage and options.
46
+ #
47
+ # === Common Options
48
+ # See link_to_remote for documentation of options common to all Ajax
49
+ # helpers; any of the options specified by link_to_remote can be used
50
+ # by the other helpers.
51
+ #
52
+ # == Designing your Rails actions for Ajax
53
+ # When building your action handlers (that is, the Rails actions that receive your background requests), it's
54
+ # important to remember a few things. First, whatever your action would normally return to the browser, it will
55
+ # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause
56
+ # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up.
57
+ # You can turn the layout off on particular actions by doing the following:
58
+ #
59
+ # class SiteController < ActionController::Base
60
+ # layout "standard", :except => [:ajax_method, :more_ajax, :another_ajax]
61
+ # end
62
+ #
63
+ # Optionally, you could do this in the method you wish to lack a layout:
64
+ #
65
+ # render :layout => false
66
+ #
67
+ # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the
68
+ # method that Ajax uses to make background requests) method.
69
+ # def name
70
+ # # Is this an XmlHttpRequest request?
71
+ # if (request.xhr?)
72
+ # render :text => @name.to_s
73
+ # else
74
+ # # No? Then render an action.
75
+ # render :action => 'view_attribute', :attr => @name
76
+ # end
77
+ # end
78
+ #
79
+ # The else clause can be left off and the current action will render with full layout and template. An extension
80
+ # to this solution was posted to Ryan Heneise's blog at ArtOfMission["http://www.artofmission.com/"].
81
+ #
82
+ # layout proc{ |c| c.request.xhr? ? false : "application" }
83
+ #
84
+ # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request.
85
+ #
86
+ # If you are just returning a little data or don't want to build a template for your output, you may opt to simply
87
+ # render text output, like this:
88
+ #
89
+ # render :text => 'Return this from my method!'
90
+ #
91
+ # Since whatever the method returns is injected into the DOM, this will simply inject some text (or HTML, if you
92
+ # tell it to). This is usually how small updates, such updating a cart total or a file count, are handled.
93
+ #
94
+ # == Updating multiple elements
95
+ # See JavaScriptGenerator for information on updating multiple elements
96
+ # on the page in an Ajax response.
97
+ module PrototypeHelper
98
+ CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded,
99
+ :interactive, :complete, :failure, :success ] +
100
+ (100..599).to_a)
101
+ AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
102
+ :asynchronous, :method, :insertion, :position,
103
+ :form, :with, :update, :script, :type ]).merge(CALLBACKS)
104
+
105
+ # Returns the JavaScript needed for a remote function.
106
+ # See the link_to_remote documentation at https://github.com/rails/prototype_legacy_helper as it takes the same arguments.
107
+ #
108
+ # Example:
109
+ # # Generates: <select id="options" onchange="new Ajax.Updater('options',
110
+ # # '/testing/update_options', {asynchronous:true, evalScripts:true})">
111
+ # <select id="options" onchange="<%= remote_function(:update => "options",
112
+ # :url => { :action => :update_options }) %>">
113
+ # <option value="0">Hello</option>
114
+ # <option value="1">World</option>
115
+ # </select>
116
+ def remote_function(options)
117
+ javascript_options = options_for_ajax(options)
118
+
119
+ update = ''
120
+ if options[:update] && options[:update].is_a?(Hash)
121
+ update = []
122
+ update << "success:'#{options[:update][:success]}'" if options[:update][:success]
123
+ update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
124
+ update = '{' + update.join(',') + '}'
125
+ elsif options[:update]
126
+ update << "'#{options[:update]}'"
127
+ end
128
+
129
+ function = update.empty? ?
130
+ "new Ajax.Request(" :
131
+ "new Ajax.Updater(#{update}, "
132
+
133
+ url_options = options[:url]
134
+ function << "'#{ERB::Util.html_escape(escape_javascript(url_for(url_options)))}'"
135
+ function << ", #{javascript_options})"
136
+
137
+ function = "#{options[:before]}; #{function}" if options[:before]
138
+ function = "#{function}; #{options[:after]}" if options[:after]
139
+ function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
140
+ function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
141
+
142
+ return function.html_safe
143
+ end
144
+
145
+ # All the methods were moved to GeneratorMethods so that
146
+ # #include_helpers_from_context has nothing to overwrite.
147
+ class JavaScriptGenerator #:nodoc:
148
+ def initialize(context, &block) #:nodoc:
149
+ @context, @lines = context, []
150
+ include_helpers_from_context
151
+ @context.with_output_buffer(@lines) do
152
+ @context.instance_exec(self, &block)
153
+ end
154
+ end
155
+
156
+ private
157
+ def include_helpers_from_context
158
+ extend @context.helpers if @context.respond_to?(:helpers)
159
+ extend GeneratorMethods
160
+ end
161
+
162
+ # JavaScriptGenerator generates blocks of JavaScript code that allow you
163
+ # to change the content and presentation of multiple DOM elements. Use
164
+ # this in your Ajax response bodies, either in a <tt>\<script></tt> tag
165
+ # or as plain JavaScript sent with a Content-type of "text/javascript".
166
+ #
167
+ # Create new instances with PrototypeHelper#update_page or with
168
+ # ActionController::Base#render, then call +insert_html+, +replace_html+,
169
+ # +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in
170
+ # methods on the yielded generator in any order you like to modify the
171
+ # content and appearance of the current page.
172
+ #
173
+ # Example:
174
+ #
175
+ # # Generates:
176
+ # # new Element.insert("list", { bottom: "<li>Some item</li>" });
177
+ # # new Effect.Highlight("list");
178
+ # # ["status-indicator", "cancel-link"].each(Element.hide);
179
+ # update_page do |page|
180
+ # page.insert_html :bottom, 'list', "<li>#{@item.name}</li>"
181
+ # page.visual_effect :highlight, 'list'
182
+ # page.hide 'status-indicator', 'cancel-link'
183
+ # end
184
+ #
185
+ #
186
+ # Helper methods can be used in conjunction with JavaScriptGenerator.
187
+ # When a helper method is called inside an update block on the +page+
188
+ # object, that method will also have access to a +page+ object.
189
+ #
190
+ # Example:
191
+ #
192
+ # module ApplicationHelper
193
+ # def update_time
194
+ # page.replace_html 'time', Time.now.to_s(:db)
195
+ # page.visual_effect :highlight, 'time'
196
+ # end
197
+ # end
198
+ #
199
+ # # Controller action
200
+ # def poll
201
+ # render(:update) { |page| page.update_time }
202
+ # end
203
+ #
204
+ # Calls to JavaScriptGenerator not matching a helper method below
205
+ # generate a proxy to the JavaScript Class named by the method called.
206
+ #
207
+ # Examples:
208
+ #
209
+ # # Generates:
210
+ # # Foo.init();
211
+ # update_page do |page|
212
+ # page.foo.init
213
+ # end
214
+ #
215
+ # # Generates:
216
+ # # Event.observe('one', 'click', function () {
217
+ # # $('two').show();
218
+ # # });
219
+ # update_page do |page|
220
+ # page.event.observe('one', 'click') do |p|
221
+ # p[:two].show
222
+ # end
223
+ # end
224
+ #
225
+ # You can also use PrototypeHelper#update_page_tag instead of
226
+ # PrototypeHelper#update_page to wrap the generated JavaScript in a
227
+ # <tt>\<script></tt> tag.
228
+ module GeneratorMethods
229
+ def to_s #:nodoc:
230
+ (@lines * $/).tap do |javascript|
231
+ if ActionView::Base.debug_rjs
232
+ source = javascript.dup
233
+ javascript.replace "try {\n#{source}\n} catch (e) "
234
+ javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
235
+ end
236
+ end
237
+ end
238
+
239
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
240
+ # used for further method calls. Examples:
241
+ #
242
+ # page['blank_slate'] # => $('blank_slate');
243
+ # page['blank_slate'].show # => $('blank_slate').show();
244
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
245
+ #
246
+ # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
247
+ # the correct id:
248
+ #
249
+ # page[@post] # => $('post_45')
250
+ # page[Post.new] # => $('new_post')
251
+ def [](id)
252
+ case id
253
+ when String, Symbol, NilClass
254
+ JavaScriptElementProxy.new(self, id)
255
+ else
256
+ JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
257
+ end
258
+ end
259
+
260
+ # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
261
+ # expression as an argument to another JavaScriptGenerator method.
262
+ def literal(code)
263
+ ::ActiveSupport::JSON::Variable.new(code.to_s)
264
+ end
265
+
266
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
267
+ # used for further method calls. Examples:
268
+ #
269
+ # page.select('p') # => $$('p');
270
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
271
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
272
+ #
273
+ # You can also use prototype enumerations with the collection. Observe:
274
+ #
275
+ # # Generates: $$('#items li').each(function(value) { value.hide(); });
276
+ # page.select('#items li').each do |value|
277
+ # value.hide
278
+ # end
279
+ #
280
+ # Though you can call the block param anything you want, they are always rendered in the
281
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
282
+ #
283
+ # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
284
+ # page.select('#items li').collect('hidden') do |item|
285
+ # item.hide
286
+ # end
287
+ #
288
+ def select(pattern)
289
+ JavaScriptElementCollectionProxy.new(self, pattern)
290
+ end
291
+
292
+ # Inserts HTML at the specified +position+ relative to the DOM element
293
+ # identified by the given +id+.
294
+ #
295
+ # +position+ may be one of:
296
+ #
297
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
298
+ # element's existing content.
299
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
300
+ # element's existing content.
301
+ # <tt>:before</tt>:: HTML is inserted immediately preceding the element.
302
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
303
+ #
304
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
305
+ # of options to be passed to ActionView::Base#render. For example:
306
+ #
307
+ # # Insert the rendered 'navigation' partial just before the DOM
308
+ # # element with ID 'content'.
309
+ # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
310
+ # page.insert_html :before, 'content', :partial => 'navigation'
311
+ #
312
+ # # Add a list item to the bottom of the <ul> with ID 'list'.
313
+ # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
314
+ # page.insert_html :bottom, 'list', '<li>Last item</li>'
315
+ #
316
+ def insert_html(position, id, *options_for_render)
317
+ content = javascript_object_for(render(*options_for_render))
318
+ record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
319
+ end
320
+
321
+ # Replaces the inner HTML of the DOM element with the given +id+.
322
+ #
323
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
324
+ # of options to be passed to ActionView::Base#render. For example:
325
+ #
326
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
327
+ # # 'person' partial for the appropriate object.
328
+ # # Generates: Element.update("person-45", "-- Contents of 'person' partial --");
329
+ # page.replace_html 'person-45', :partial => 'person', :object => @person
330
+ #
331
+ def replace_html(id, *options_for_render)
332
+ call 'Element.update', id, render(*options_for_render)
333
+ end
334
+
335
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
336
+ # contents) of the DOM element with the given +id+.
337
+ #
338
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
339
+ # of options to be passed to ActionView::Base#render. For example:
340
+ #
341
+ # # Replace the DOM element having ID 'person-45' with the
342
+ # # 'person' partial for the appropriate object.
343
+ # page.replace 'person-45', :partial => 'person', :object => @person
344
+ #
345
+ # This allows the same partial that is used for the +insert_html+ to
346
+ # be also used for the input to +replace+ without resorting to
347
+ # the use of wrapper elements.
348
+ #
349
+ # Examples:
350
+ #
351
+ # <div id="people">
352
+ # <%= render :partial => 'person', :collection => @people %>
353
+ # </div>
354
+ #
355
+ # # Insert a new person
356
+ # #
357
+ # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
358
+ # page.insert_html :bottom, :partial => 'person', :object => @person
359
+ #
360
+ # # Replace an existing person
361
+ #
362
+ # # Generates: Element.replace("person_45", "-- Contents of partial --");
363
+ # page.replace 'person_45', :partial => 'person', :object => @person
364
+ #
365
+ def replace(id, *options_for_render)
366
+ call 'Element.replace', id, render(*options_for_render)
367
+ end
368
+
369
+ # Removes the DOM elements with the given +ids+ from the page.
370
+ #
371
+ # Example:
372
+ #
373
+ # # Remove a few people
374
+ # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
375
+ # page.remove 'person_23', 'person_9', 'person_2'
376
+ #
377
+ def remove(*ids)
378
+ loop_on_multiple_args 'Element.remove', ids
379
+ end
380
+
381
+ # Shows hidden DOM elements with the given +ids+.
382
+ #
383
+ # Example:
384
+ #
385
+ # # Show a few people
386
+ # # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
387
+ # page.show 'person_6', 'person_13', 'person_223'
388
+ #
389
+ def show(*ids)
390
+ loop_on_multiple_args 'Element.show', ids
391
+ end
392
+
393
+ # Hides the visible DOM elements with the given +ids+.
394
+ #
395
+ # Example:
396
+ #
397
+ # # Hide a few people
398
+ # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
399
+ # page.hide 'person_29', 'person_9', 'person_0'
400
+ #
401
+ def hide(*ids)
402
+ loop_on_multiple_args 'Element.hide', ids
403
+ end
404
+
405
+ # Toggles the visibility of the DOM elements with the given +ids+.
406
+ # Example:
407
+ #
408
+ # # Show a few people
409
+ # # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle);
410
+ # page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements
411
+ # page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements
412
+ #
413
+ def toggle(*ids)
414
+ loop_on_multiple_args 'Element.toggle', ids
415
+ end
416
+
417
+ # Displays an alert dialog with the given +message+.
418
+ #
419
+ # Example:
420
+ #
421
+ # # Generates: alert('This message is from Rails!')
422
+ # page.alert('This message is from Rails!')
423
+ def alert(message)
424
+ call 'alert', message
425
+ end
426
+
427
+ # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
428
+ #
429
+ # Examples:
430
+ #
431
+ # # Generates: window.location.href = "/mycontroller";
432
+ # page.redirect_to(:action => 'index')
433
+ #
434
+ # # Generates: window.location.href = "/account/signup";
435
+ # page.redirect_to(:controller => 'account', :action => 'signup')
436
+ def redirect_to(location)
437
+ url = location.is_a?(String) ? location : @context.url_for(location)
438
+ record "window.location.href = #{url.inspect}"
439
+ end
440
+
441
+ # Reloads the browser's current +location+ using JavaScript
442
+ #
443
+ # Examples:
444
+ #
445
+ # # Generates: window.location.reload();
446
+ # page.reload
447
+ def reload
448
+ record 'window.location.reload()'
449
+ end
450
+
451
+ # Calls the JavaScript +function+, optionally with the given +arguments+.
452
+ #
453
+ # If a block is given, the block will be passed to a new JavaScriptGenerator;
454
+ # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
455
+ # and passed as the called function's final argument.
456
+ #
457
+ # Examples:
458
+ #
459
+ # # Generates: Element.replace(my_element, "My content to replace with.")
460
+ # page.call 'Element.replace', 'my_element', "My content to replace with."
461
+ #
462
+ # # Generates: alert('My message!')
463
+ # page.call 'alert', 'My message!'
464
+ #
465
+ # # Generates:
466
+ # # my_method(function() {
467
+ # # $("one").show();
468
+ # # $("two").hide();
469
+ # # });
470
+ # page.call(:my_method) do |p|
471
+ # p[:one].show
472
+ # p[:two].hide
473
+ # end
474
+ def call(function, *arguments, &block)
475
+ record "#{function}(#{arguments_for_call(arguments, block)})"
476
+ end
477
+
478
+ # Assigns the JavaScript +variable+ the given +value+.
479
+ #
480
+ # Examples:
481
+ #
482
+ # # Generates: my_string = "This is mine!";
483
+ # page.assign 'my_string', 'This is mine!'
484
+ #
485
+ # # Generates: record_count = 33;
486
+ # page.assign 'record_count', 33
487
+ #
488
+ # # Generates: tabulated_total = 47
489
+ # page.assign 'tabulated_total', @total_from_cart
490
+ #
491
+ def assign(variable, value)
492
+ record "#{variable} = #{javascript_object_for(value)}"
493
+ end
494
+
495
+ # Writes raw JavaScript to the page.
496
+ #
497
+ # Example:
498
+ #
499
+ # page << "alert('JavaScript with Prototype.');"
500
+ def <<(javascript)
501
+ @lines << javascript
502
+ end
503
+
504
+ # Executes the content of the block after a delay of +seconds+. Example:
505
+ #
506
+ # # Generates:
507
+ # # setTimeout(function() {
508
+ # # ;
509
+ # # new Effect.Fade("notice",{});
510
+ # # }, 20000);
511
+ # page.delay(20) do
512
+ # page.visual_effect :fade, 'notice'
513
+ # end
514
+ def delay(seconds = 1)
515
+ record "setTimeout(function() {\n\n"
516
+ yield
517
+ record "}, #{(seconds * 1000).to_i})"
518
+ end
519
+
520
+ private
521
+ def loop_on_multiple_args(method, ids)
522
+ record(ids.size>1 ?
523
+ "#{javascript_object_for(ids)}.each(#{method})" :
524
+ "#{method}(#{javascript_object_for(ids.first)})")
525
+ end
526
+
527
+ def page
528
+ self
529
+ end
530
+
531
+ def record(line)
532
+ line = "#{line.to_s.chomp.gsub(/\;\z/, '')};"
533
+ self << line
534
+ line
535
+ end
536
+
537
+ def render(*options)
538
+ with_formats(:html) do
539
+ case option = options.first
540
+ when Hash
541
+ @context.render(*options)
542
+ else
543
+ option.to_s
544
+ end
545
+ end
546
+ end
547
+
548
+ def with_formats(*args)
549
+ @context ? @context.lookup_context.update_details(:formats => args) { yield } : yield
550
+ end
551
+
552
+ def javascript_object_for(object)
553
+ ::ActiveSupport::JSON.encode(object)
554
+ end
555
+
556
+ def arguments_for_call(arguments, block = nil)
557
+ arguments << block_to_function(block) if block
558
+ arguments.map { |argument| javascript_object_for(argument) }.join ', '
559
+ end
560
+
561
+ def block_to_function(block)
562
+ generator = self.class.new(@context, &block)
563
+ literal("function() { #{generator.to_s} }")
564
+ end
565
+
566
+ def method_missing(method, *arguments)
567
+ JavaScriptProxy.new(self, method.to_s.camelize)
568
+ end
569
+ end
570
+ end
571
+
572
+ # Yields a JavaScriptGenerator and returns the generated JavaScript code.
573
+ # Use this to update multiple elements on a page in an Ajax response.
574
+ # See JavaScriptGenerator for more information.
575
+ #
576
+ # Example:
577
+ #
578
+ # update_page do |page|
579
+ # page.hide 'spinner'
580
+ # end
581
+ def update_page(&block)
582
+ JavaScriptGenerator.new(self, &block).to_s.html_safe
583
+ end
584
+
585
+ # Works like update_page but wraps the generated JavaScript in a
586
+ # <tt>\<script></tt> tag. Use this to include generated JavaScript in an
587
+ # ERb template. See JavaScriptGenerator for more information.
588
+ #
589
+ # +html_options+ may be a hash of <tt>\<script></tt> attributes to be
590
+ # passed to ActionView::Helpers::JavaScriptHelper#javascript_tag.
591
+ def update_page_tag(html_options = {}, &block)
592
+ javascript_tag update_page(&block), html_options
593
+ end
594
+
595
+ protected
596
+ def options_for_javascript(options)
597
+ if options.empty?
598
+ '{}'
599
+ else
600
+ "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}"
601
+ end
602
+ end
603
+
604
+ def options_for_ajax(options)
605
+ js_options = build_callbacks(options)
606
+
607
+ js_options['asynchronous'] = options[:type] != :synchronous
608
+ js_options['method'] = method_option_to_s(options[:method]) if options[:method]
609
+ js_options['insertion'] = "'#{options[:position].to_s.downcase}'" if options[:position]
610
+ js_options['evalScripts'] = options[:script].nil? || options[:script]
611
+
612
+ if options[:form]
613
+ js_options['parameters'] = 'Form.serialize(this)'
614
+ elsif options[:submit]
615
+ js_options['parameters'] = "Form.serialize('#{options[:submit]}')"
616
+ elsif options[:with]
617
+ js_options['parameters'] = options[:with]
618
+ end
619
+
620
+ if protect_against_forgery? && !options[:form]
621
+ if js_options['parameters']
622
+ js_options['parameters'] << " + '&"
623
+ else
624
+ js_options['parameters'] = "'"
625
+ end
626
+ js_options['parameters'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')"
627
+ end
628
+
629
+ options_for_javascript(js_options)
630
+ end
631
+
632
+ def method_option_to_s(method)
633
+ (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
634
+ end
635
+
636
+ def build_callbacks(options)
637
+ callbacks = {}
638
+ options.each do |callback, code|
639
+ if CALLBACKS.include?(callback)
640
+ name = 'on' + callback.to_s.capitalize
641
+ callbacks[name] = "function(request){#{code}}"
642
+ end
643
+ end
644
+ callbacks
645
+ end
646
+ end
647
+
648
+ # Converts chained method calls on DOM proxy elements into JavaScript chains
649
+ class JavaScriptProxy < ActiveSupport::BasicObject #:nodoc:
650
+
651
+ def initialize(generator, root = nil)
652
+ @generator = generator
653
+ @generator << root if root
654
+ end
655
+
656
+ def is_a?(klass)
657
+ klass == JavaScriptProxy
658
+ end
659
+
660
+ private
661
+ def method_missing(method, *arguments, &block)
662
+ if method.to_s =~ /(.*)=$/
663
+ assign($1, arguments.first)
664
+ else
665
+ call("#{method.to_s.camelize(:lower)}", *arguments, &block)
666
+ end
667
+ end
668
+
669
+ def call(function, *arguments, &block)
670
+ append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
671
+ self
672
+ end
673
+
674
+ def assign(variable, value)
675
+ append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
676
+ end
677
+
678
+ def function_chain
679
+ @function_chain ||= @generator.instance_variable_get(:@lines)
680
+ end
681
+
682
+ def append_to_function_chain!(call)
683
+ function_chain[-1].chomp!(';')
684
+ function_chain[-1] += ".#{call};"
685
+ end
686
+ end
687
+
688
+ class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
689
+ def initialize(generator, id)
690
+ @id = id
691
+ super(generator, "$(#{::ActiveSupport::JSON.encode(id)})")
692
+ end
693
+
694
+ # Allows access of element attributes through +attribute+. Examples:
695
+ #
696
+ # page['foo']['style'] # => $('foo').style;
697
+ # page['foo']['style']['color'] # => $('blank_slate').style.color;
698
+ # page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
699
+ # page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
700
+ def [](attribute)
701
+ append_to_function_chain!(attribute)
702
+ self
703
+ end
704
+
705
+ def []=(variable, value)
706
+ assign(variable, value)
707
+ end
708
+
709
+ def replace_html(*options_for_render)
710
+ call 'update', @generator.send(:render, *options_for_render)
711
+ end
712
+
713
+ def replace(*options_for_render)
714
+ call 'replace', @generator.send(:render, *options_for_render)
715
+ end
716
+
717
+ def reload(options_for_replace = {})
718
+ replace(options_for_replace.merge({ :partial => @id.to_s }))
719
+ end
720
+
721
+ end
722
+
723
+ class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
724
+ def initialize(generator, variable)
725
+ @variable = ::ActiveSupport::JSON::Variable.new(variable)
726
+ @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks
727
+ super(generator)
728
+ end
729
+
730
+ # The JSON Encoder calls this to check for the +to_json+ method
731
+ # Since it's a blank slate object, I suppose it responds to anything.
732
+ def respond_to?(*)
733
+ true
734
+ end
735
+
736
+ def as_json(options = nil)
737
+ @variable
738
+ end
739
+
740
+ private
741
+ def append_to_function_chain!(call)
742
+ @generator << @variable if @empty
743
+ @empty = false
744
+ super
745
+ end
746
+ end
747
+
748
+ class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
749
+ ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by, :in_groups_of, :each_slice] unless defined? ENUMERABLE_METHODS_WITH_RETURN
750
+ ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS
751
+ attr_reader :generator
752
+ delegate :arguments_for_call, :to => :generator
753
+
754
+ def initialize(generator, pattern)
755
+ super(generator, @pattern = pattern)
756
+ end
757
+
758
+ def each_slice(variable, number, &block)
759
+ if block
760
+ enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
761
+ else
762
+ add_variable_assignment!(variable)
763
+ append_enumerable_function!("eachSlice(#{::ActiveSupport::JSON.encode(number)});")
764
+ end
765
+ end
766
+
767
+ def grep(variable, pattern, &block)
768
+ enumerate :grep, :variable => variable, :return => true, :method_args => [::ActiveSupport::JSON::Variable.new(pattern.inspect)], :yield_args => %w(value index), &block
769
+ end
770
+
771
+ def in_groups_of(variable, number, fill_with = nil)
772
+ arguments = [number]
773
+ arguments << fill_with unless fill_with.nil?
774
+ add_variable_assignment!(variable)
775
+ append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});")
776
+ end
777
+
778
+ def inject(variable, memo, &block)
779
+ enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
780
+ end
781
+
782
+ def pluck(variable, property)
783
+ add_variable_assignment!(variable)
784
+ append_enumerable_function!("pluck(#{::ActiveSupport::JSON.encode(property)});")
785
+ end
786
+
787
+ def zip(variable, *arguments, &block)
788
+ add_variable_assignment!(variable)
789
+ append_enumerable_function!("zip(#{arguments_for_call arguments}")
790
+ if block
791
+ function_chain[-1] += ", function(array) {"
792
+ yield ::ActiveSupport::JSON::Variable.new('array')
793
+ add_return_statement!
794
+ @generator << '});'
795
+ else
796
+ function_chain[-1] += ');'
797
+ end
798
+ end
799
+
800
+ private
801
+ def method_missing(method, *arguments, &block)
802
+ if ENUMERABLE_METHODS.include?(method)
803
+ returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method)
804
+ variable = arguments.first if returnable
805
+ enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block)
806
+ else
807
+ super
808
+ end
809
+ end
810
+
811
+ # Options
812
+ # * variable - name of the variable to set the result of the enumeration to
813
+ # * method_args - array of the javascript enumeration method args that occur before the function
814
+ # * yield_args - array of the javascript yield args
815
+ # * return - true if the enumeration should return the last statement
816
+ def enumerate(enumerable, options = {}, &block)
817
+ options[:method_args] ||= []
818
+ options[:yield_args] ||= []
819
+ yield_args = options[:yield_args] * ', '
820
+ method_args = arguments_for_call options[:method_args] # foo, bar, function
821
+ method_args << ', ' unless method_args.blank?
822
+ add_variable_assignment!(options[:variable]) if options[:variable]
823
+ append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {")
824
+ # only yield as many params as were passed in the block
825
+ yield(*options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1])
826
+ add_return_statement! if options[:return]
827
+ @generator << '});'
828
+ end
829
+
830
+ def add_variable_assignment!(variable)
831
+ function_chain.push("var #{variable} = #{function_chain.pop}")
832
+ end
833
+
834
+ def add_return_statement!
835
+ unless function_chain.last =~ /return/
836
+ function_chain.push("return #{function_chain.pop.chomp(';')};")
837
+ end
838
+ end
839
+
840
+ def append_enumerable_function!(call)
841
+ function_chain[-1].chomp!(';')
842
+ function_chain[-1] += ".#{call}"
843
+ end
844
+ end
845
+
846
+ class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
847
+ def initialize(generator, pattern)
848
+ super(generator, "$$(#{::ActiveSupport::JSON.encode(pattern)})")
849
+ end
850
+ end
851
+ end
852
+ end