~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_view/helpers/prototype_helper.rb

  • Committer: Michael Forrest
  • Date: 2010-10-15 16:28:50 UTC
  • Revision ID: michael.forrest@canonical.com-20101015162850-tj2vchanv0kr0dun
refrozeĀ gems

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require 'set'
 
2
require 'active_support/json'
 
3
 
 
4
module ActionView
 
5
  module Helpers
 
6
    # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides
 
7
    # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation,
 
8
    # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]
 
9
    # functionality, and more traditional object-oriented facilities for JavaScript.
 
10
    # This module provides a set of helpers to make it more convenient to call
 
11
    # functions from Prototype using Rails, including functionality to call remote
 
12
    # Rails methods (that is, making a background request to a Rails action) using Ajax.
 
13
    # This means that you can call actions in your controllers without
 
14
    # reloading the page, but still update certain parts of it using
 
15
    # injections into the DOM. A common use case is having a form that adds
 
16
    # a new element to a list without reloading the page or updating a shopping
 
17
    # cart total when a new item is added.
 
18
    #
 
19
    # == Usage
 
20
    # To be able to use these helpers, you must first include the Prototype
 
21
    # JavaScript framework in your pages.
 
22
    #
 
23
    #  javascript_include_tag 'prototype'
 
24
    #
 
25
    # (See the documentation for
 
26
    # ActionView::Helpers::JavaScriptHelper for more information on including
 
27
    # this and other JavaScript files in your Rails templates.)
 
28
    #
 
29
    # Now you're ready to call a remote action either through a link...
 
30
    #
 
31
    #  link_to_remote "Add to cart",
 
32
    #    :url => { :action => "add", :id => product.id },
 
33
    #    :update => { :success => "cart", :failure => "error" }
 
34
    #
 
35
    # ...through a form...
 
36
    #
 
37
    #  <% form_remote_tag :url => '/shipping' do -%>
 
38
    #    <div><%= submit_tag 'Recalculate Shipping' %></div>
 
39
    #  <% end -%>
 
40
    #
 
41
    # ...periodically...
 
42
    #
 
43
    #  periodically_call_remote(:url => 'update', :frequency => '5', :update => 'ticker')
 
44
    #
 
45
    # ...or through an observer (i.e., a form or field that is observed and calls a remote
 
46
    # action when changed).
 
47
    #
 
48
    #  <%= observe_field(:searchbox,
 
49
    #       :url => { :action => :live_search }),
 
50
    #       :frequency => 0.5,
 
51
    #       :update => :hits,
 
52
    #       :with => 'query'
 
53
    #       %>
 
54
    #
 
55
    # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than
 
56
    # are listed here); check out the documentation for each method to find out more about its usage and options.
 
57
    #
 
58
    # === Common Options
 
59
    # See link_to_remote for documentation of options common to all Ajax
 
60
    # helpers; any of the options specified by link_to_remote can be used
 
61
    # by the other helpers.
 
62
    #
 
63
    # == Designing your Rails actions for Ajax
 
64
    # When building your action handlers (that is, the Rails actions that receive your background requests), it's
 
65
    # important to remember a few things.  First, whatever your action would normally return to the browser, it will
 
66
    # return to the Ajax call.  As such, you typically don't want to render with a layout.  This call will cause
 
67
    # 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.
 
68
    # You can turn the layout off on particular actions by doing the following:
 
69
    #
 
70
    #  class SiteController < ActionController::Base
 
71
    #    layout "standard", :except => [:ajax_method, :more_ajax, :another_ajax]
 
72
    #  end
 
73
    #
 
74
    # Optionally, you could do this in the method you wish to lack a layout:
 
75
    #
 
76
    #  render :layout => false
 
77
    #
 
78
    # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the
 
79
    # method that Ajax uses to make background requests) method.
 
80
    #  def name
 
81
    #    # Is this an XmlHttpRequest request?
 
82
    #    if (request.xhr?)
 
83
    #      render :text => @name.to_s
 
84
    #    else
 
85
    #      # No?  Then render an action.
 
86
    #      render :action => 'view_attribute', :attr => @name
 
87
    #    end
 
88
    #  end
 
89
    #
 
90
    # The else clause can be left off and the current action will render with full layout and template. An extension
 
91
    # to this solution was posted to Ryan Heneise's blog at ArtOfMission["http://www.artofmission.com/"].
 
92
    #
 
93
    #  layout proc{ |c| c.request.xhr? ? false : "application" }
 
94
    #
 
95
    # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request.
 
96
    #
 
97
    # If you are just returning a little data or don't want to build a template for your output, you may opt to simply
 
98
    # render text output, like this:
 
99
    #
 
100
    #  render :text => 'Return this from my method!'
 
101
    #
 
102
    # Since whatever the method returns is injected into the DOM, this will simply inject some text (or HTML, if you
 
103
    # tell it to).  This is usually how small updates, such updating a cart total or a file count, are handled.
 
104
    #
 
105
    # == Updating multiple elements
 
106
    # See JavaScriptGenerator for information on updating multiple elements
 
107
    # on the page in an Ajax response.
 
108
    module PrototypeHelper
 
109
      unless const_defined? :CALLBACKS
 
110
        CALLBACKS    = Set.new([ :create, :uninitialized, :loading, :loaded,
 
111
                         :interactive, :complete, :failure, :success ] +
 
112
                         (100..599).to_a)
 
113
        AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
 
114
                         :asynchronous, :method, :insertion, :position,
 
115
                         :form, :with, :update, :script, :type ]).merge(CALLBACKS)
 
116
      end
 
117
 
 
118
      # Returns a link to a remote action defined by <tt>options[:url]</tt>
 
119
      # (using the url_for format) that's called in the background using
 
120
      # XMLHttpRequest. The result of that request can then be inserted into a
 
121
      # DOM object whose id can be specified with <tt>options[:update]</tt>.
 
122
      # Usually, the result would be a partial prepared by the controller with
 
123
      # render :partial.
 
124
      #
 
125
      # Examples:
 
126
      #   # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
 
127
      #   #            return false;">Delete this post</a>
 
128
      #   link_to_remote "Delete this post", :update => "posts",
 
129
      #     :url => { :action => "destroy", :id => post.id }
 
130
      #
 
131
      #   # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
 
132
      #   #            return false;"><img alt="Refresh" src="/images/refresh.png?" /></a>
 
133
      #   link_to_remote(image_tag("refresh"), :update => "emails",
 
134
      #     :url => { :action => "list_emails" })
 
135
      #
 
136
      # You can override the generated HTML options by specifying a hash in
 
137
      # <tt>options[:html]</tt>.
 
138
      #
 
139
      #   link_to_remote "Delete this post", :update => "posts",
 
140
      #     :url  => post_url(@post), :method => :delete,
 
141
      #     :html => { :class  => "destructive" }
 
142
      #
 
143
      # You can also specify a hash for <tt>options[:update]</tt> to allow for
 
144
      # easy redirection of output to an other DOM element if a server-side
 
145
      # error occurs:
 
146
      #
 
147
      # Example:
 
148
      #   # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
 
149
      #   #            {asynchronous:true, evalScripts:true}); return false;">Delete this post</a>
 
150
      #   link_to_remote "Delete this post",
 
151
      #     :url => { :action => "destroy", :id => post.id },
 
152
      #     :update => { :success => "posts", :failure => "error" }
 
153
      #
 
154
      # Optionally, you can use the <tt>options[:position]</tt> parameter to
 
155
      # influence how the target DOM element is updated. It must be one of
 
156
      # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
 
157
      #
 
158
      # The method used is by default POST. You can also specify GET or you
 
159
      # can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
 
160
      #
 
161
      # Example:
 
162
      #   # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
 
163
      #   #            return false;">Destroy</a>
 
164
      #   link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
 
165
      #
 
166
      # By default, these remote requests are processed asynchronous during
 
167
      # which various JavaScript callbacks can be triggered (for progress
 
168
      # indicators and the likes). All callbacks get access to the
 
169
      # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
 
170
      #
 
171
      # To access the server response, use <tt>request.responseText</tt>, to
 
172
      # find out the HTTP status, use <tt>request.status</tt>.
 
173
      #
 
174
      # Example:
 
175
      #   # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
 
176
      #   #            onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a>
 
177
      #   word = 'hello'
 
178
      #   link_to_remote word,
 
179
      #     :url => { :action => "undo", :n => word_counter },
 
180
      #     :complete => "undoRequestCompleted(request)"
 
181
      #
 
182
      # The callbacks that may be specified are (in order):
 
183
      #
 
184
      # <tt>:loading</tt>::       Called when the remote document is being
 
185
      #                           loaded with data by the browser.
 
186
      # <tt>:loaded</tt>::        Called when the browser has finished loading
 
187
      #                           the remote document.
 
188
      # <tt>:interactive</tt>::   Called when the user can interact with the
 
189
      #                           remote document, even though it has not
 
190
      #                           finished loading.
 
191
      # <tt>:success</tt>::       Called when the XMLHttpRequest is completed,
 
192
      #                           and the HTTP status code is in the 2XX range.
 
193
      # <tt>:failure</tt>::       Called when the XMLHttpRequest is completed,
 
194
      #                           and the HTTP status code is not in the 2XX
 
195
      #                           range.
 
196
      # <tt>:complete</tt>::      Called when the XMLHttpRequest is complete
 
197
      #                           (fires after success/failure if they are
 
198
      #                           present).
 
199
      #
 
200
      # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
 
201
      # adding additional callbacks for specific status codes.
 
202
      #
 
203
      # Example:
 
204
      #   # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
 
205
      #   #            on404:function(request){alert('Not found...? Wrong URL...?')},
 
206
      #   #            onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
 
207
      #   link_to_remote word,
 
208
      #     :url => { :action => "action" },
 
209
      #     404 => "alert('Not found...? Wrong URL...?')",
 
210
      #     :failure => "alert('HTTP Error ' + request.status + '!')"
 
211
      #
 
212
      # A status code callback overrides the success/failure handlers if
 
213
      # present.
 
214
      #
 
215
      # If you for some reason or another need synchronous processing (that'll
 
216
      # block the browser while the request is happening), you can specify
 
217
      # <tt>options[:type] = :synchronous</tt>.
 
218
      #
 
219
      # You can customize further browser side call logic by passing in
 
220
      # JavaScript code snippets via some optional parameters. In their order
 
221
      # of use these are:
 
222
      #
 
223
      # <tt>:confirm</tt>::      Adds confirmation dialog.
 
224
      # <tt>:condition</tt>::    Perform remote request conditionally
 
225
      #                          by this expression. Use this to
 
226
      #                          describe browser-side conditions when
 
227
      #                          request should not be initiated.
 
228
      # <tt>:before</tt>::       Called before request is initiated.
 
229
      # <tt>:after</tt>::        Called immediately after request was
 
230
      #                          initiated and before <tt>:loading</tt>.
 
231
      # <tt>:submit</tt>::       Specifies the DOM element ID that's used
 
232
      #                          as the parent of the form elements. By
 
233
      #                          default this is the current form, but
 
234
      #                          it could just as well be the ID of a
 
235
      #                          table row or any other DOM element.
 
236
      # <tt>:with</tt>::         A JavaScript expression specifying
 
237
      #                          the parameters for the XMLHttpRequest.
 
238
      #                          Any expressions should return a valid
 
239
      #                          URL query string.
 
240
      #
 
241
      #                          Example:
 
242
      #
 
243
      #                            :with => "'name=' + $('name').value"
 
244
      #
 
245
      # You can generate a link that uses AJAX in the general case, while
 
246
      # degrading gracefully to plain link behavior in the absence of
 
247
      # JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL.
 
248
      # Note the extra curly braces around the <tt>options</tt> hash separate
 
249
      # it as the second parameter from <tt>html_options</tt>, the third.
 
250
      #
 
251
      # Example:
 
252
      #   link_to_remote "Delete this post",
 
253
      #     { :update => "posts", :url => { :action => "destroy", :id => post.id } },
 
254
      #     :href => url_for(:action => "destroy", :id => post.id)
 
255
      def link_to_remote(name, options = {}, html_options = nil)
 
256
        link_to_function(name, remote_function(options), html_options || options.delete(:html))
 
257
      end
 
258
 
 
259
      # Creates a button with an onclick event which calls a remote action
 
260
      # via XMLHttpRequest
 
261
      # The options for specifying the target with :url
 
262
      # and defining callbacks is the same as link_to_remote.
 
263
      def button_to_remote(name, options = {}, html_options = {})
 
264
        button_to_function(name, remote_function(options), html_options)
 
265
      end
 
266
 
 
267
      # Periodically calls the specified url (<tt>options[:url]</tt>) every
 
268
      # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
 
269
      # update a specified div (<tt>options[:update]</tt>) with the results
 
270
      # of the remote call. The options for specifying the target with <tt>:url</tt>
 
271
      # and defining callbacks is the same as link_to_remote.
 
272
      # Examples:
 
273
      #  # Call get_averages and put its results in 'avg' every 10 seconds
 
274
      #  # Generates:
 
275
      #  #      new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
 
276
      #  #      {asynchronous:true, evalScripts:true})}, 10)
 
277
      #  periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
 
278
      #
 
279
      #  # Call invoice every 10 seconds with the id of the customer
 
280
      #  # If it succeeds, update the invoice DIV; if it fails, update the error DIV
 
281
      #  # Generates:
 
282
      #  #      new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
 
283
      #  #      '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
 
284
      #  periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
 
285
      #     :update => { :success => "invoice", :failure => "error" }
 
286
      #
 
287
      #  # Call update every 20 seconds and update the new_block DIV
 
288
      #  # Generates:
 
289
      #  # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20)
 
290
      #  periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block')
 
291
      #
 
292
      def periodically_call_remote(options = {})
 
293
         frequency = options[:frequency] || 10 # every ten seconds by default
 
294
         code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
 
295
         javascript_tag(code)
 
296
      end
 
297
 
 
298
      # Returns a form tag that will submit using XMLHttpRequest in the
 
299
      # background instead of the regular reloading POST arrangement. Even
 
300
      # though it's using JavaScript to serialize the form elements, the form
 
301
      # submission will work just like a regular submission as viewed by the
 
302
      # receiving side (all elements available in <tt>params</tt>). The options for
 
303
      # specifying the target with <tt>:url</tt> and defining callbacks is the same as
 
304
      # +link_to_remote+.
 
305
      #
 
306
      # A "fall-through" target for browsers that doesn't do JavaScript can be
 
307
      # specified with the <tt>:action</tt>/<tt>:method</tt> options on <tt>:html</tt>.
 
308
      #
 
309
      # Example:
 
310
      #   # Generates:
 
311
      #   #      <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
 
312
      #   #      {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
 
313
      #   form_remote_tag :html => { :action =>
 
314
      #     url_for(:controller => "some", :action => "place") }
 
315
      #
 
316
      # The Hash passed to the <tt>:html</tt> key is equivalent to the options (2nd)
 
317
      # argument in the FormTagHelper.form_tag method.
 
318
      #
 
319
      # By default the fall-through action is the same as the one specified in
 
320
      # the <tt>:url</tt> (and the default method is <tt>:post</tt>).
 
321
      #
 
322
      # form_remote_tag also takes a block, like form_tag:
 
323
      #   # Generates:
 
324
      #   #     <form action="/" method="post" onsubmit="new Ajax.Request('/',
 
325
      #   #     {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
 
326
      #   #     return false;"> <div><input name="commit" type="submit" value="Save" /></div>
 
327
      #   #     </form>
 
328
      #   <% form_remote_tag :url => '/posts' do -%>
 
329
      #     <div><%= submit_tag 'Save' %></div>
 
330
      #   <% end -%>
 
331
      def form_remote_tag(options = {}, &block)
 
332
        options[:form] = true
 
333
 
 
334
        options[:html] ||= {}
 
335
        options[:html][:onsubmit] =
 
336
          (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
 
337
          "#{remote_function(options)}; return false;"
 
338
 
 
339
        form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
 
340
      end
 
341
 
 
342
      # Creates a form that will submit using XMLHttpRequest in the background
 
343
      # instead of the regular reloading POST arrangement and a scope around a
 
344
      # specific resource that is used as a base for questioning about
 
345
      # values for the fields.
 
346
      #
 
347
      # === Resource
 
348
      #
 
349
      # Example:
 
350
      #   <% remote_form_for(@post) do |f| %>
 
351
      #     ...
 
352
      #   <% end %>
 
353
      #
 
354
      # This will expand to be the same as:
 
355
      #
 
356
      #   <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
 
357
      #     ...
 
358
      #   <% end %>
 
359
      #
 
360
      # === Nested Resource
 
361
      #
 
362
      # Example:
 
363
      #   <% remote_form_for([@post, @comment]) do |f| %>
 
364
      #     ...
 
365
      #   <% end %>
 
366
      #
 
367
      # This will expand to be the same as:
 
368
      #
 
369
      #   <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %>
 
370
      #     ...
 
371
      #   <% end %>
 
372
      #
 
373
      # If you don't need to attach a form to a resource, then check out form_remote_tag.
 
374
      #
 
375
      # See FormHelper#form_for for additional semantics.
 
376
      def remote_form_for(record_or_name_or_array, *args, &proc)
 
377
        options = args.extract_options!
 
378
 
 
379
        case record_or_name_or_array
 
380
        when String, Symbol
 
381
          object_name = record_or_name_or_array
 
382
        when Array
 
383
          object = record_or_name_or_array.last
 
384
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
 
385
          apply_form_for_options!(record_or_name_or_array, options)
 
386
          args.unshift object
 
387
        else
 
388
          object      = record_or_name_or_array
 
389
          object_name = ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array)
 
390
          apply_form_for_options!(object, options)
 
391
          args.unshift object
 
392
        end
 
393
 
 
394
        concat(form_remote_tag(options))
 
395
        fields_for(object_name, *(args << options), &proc)
 
396
        concat('</form>'.html_safe!)
 
397
      end
 
398
      alias_method :form_remote_for, :remote_form_for
 
399
 
 
400
      # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+
 
401
      # that will submit form using XMLHttpRequest in the background instead of a regular POST request that
 
402
      # reloads the page.
 
403
      #
 
404
      #  # Create a button that submits to the create action
 
405
      #  #
 
406
      #  # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
 
407
      #  #     {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
 
408
      #  #     return false;" type="button" value="Create" />
 
409
      #  <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %>
 
410
      #
 
411
      #  # Submit to the remote action update and update the DIV succeed or fail based
 
412
      #  # on the success or failure of the request
 
413
      #  #
 
414
      #  # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
 
415
      #  #      '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
 
416
      #  #      return false;" type="button" value="Update" />
 
417
      #  <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
 
418
      #     :update => { :success => "succeed", :failure => "fail" }
 
419
      #
 
420
      # <tt>options</tt> argument is the same as in form_remote_tag.
 
421
      def submit_to_remote(name, value, options = {})
 
422
        options[:with] ||= 'Form.serialize(this.form)'
 
423
 
 
424
        html_options = options.delete(:html) || {}
 
425
        html_options[:name] = name
 
426
 
 
427
        button_to_remote(value, options, html_options)
 
428
      end
 
429
 
 
430
      # Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
 
431
      # that +form_remote_tag+ can call in <tt>:complete</tt> to evaluate a multiple
 
432
      # update return document using +update_element_function+ calls.
 
433
      def evaluate_remote_response
 
434
        "eval(request.responseText)"
 
435
      end
 
436
 
 
437
      # Returns the JavaScript needed for a remote function.
 
438
      # Takes the same arguments as link_to_remote.
 
439
      #
 
440
      # Example:
 
441
      #   # Generates: <select id="options" onchange="new Ajax.Updater('options',
 
442
      #   # '/testing/update_options', {asynchronous:true, evalScripts:true})">
 
443
      #   <select id="options" onchange="<%= remote_function(:update => "options",
 
444
      #       :url => { :action => :update_options }) %>">
 
445
      #     <option value="0">Hello</option>
 
446
      #     <option value="1">World</option>
 
447
      #   </select>
 
448
      def remote_function(options)
 
449
        javascript_options = options_for_ajax(options)
 
450
 
 
451
        update = ''
 
452
        if options[:update] && options[:update].is_a?(Hash)
 
453
          update  = []
 
454
          update << "success:'#{options[:update][:success]}'" if options[:update][:success]
 
455
          update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
 
456
          update  = '{' + update.join(',') + '}'
 
457
        elsif options[:update]
 
458
          update << "'#{options[:update]}'"
 
459
        end
 
460
 
 
461
        function = update.empty? ?
 
462
          "new Ajax.Request(" :
 
463
          "new Ajax.Updater(#{update}, "
 
464
 
 
465
        url_options = options[:url]
 
466
        url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash)
 
467
        function << "'#{escape_javascript(url_for(url_options))}'"
 
468
        function << ", #{javascript_options})"
 
469
 
 
470
        function = "#{options[:before]}; #{function}" if options[:before]
 
471
        function = "#{function}; #{options[:after]}"  if options[:after]
 
472
        function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
 
473
        function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
 
474
 
 
475
        return function
 
476
      end
 
477
 
 
478
      # Observes the field with the DOM ID specified by +field_id+ and calls a
 
479
      # callback when its contents have changed. The default callback is an
 
480
      # Ajax call. By default the value of the observed field is sent as a
 
481
      # parameter with the Ajax call.
 
482
      #
 
483
      # Example:
 
484
      #  # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
 
485
      #  #         '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})})
 
486
      #  <%= observe_field :suggest, :url => { :action => :find_suggestion },
 
487
      #       :frequency => 0.25,
 
488
      #       :update => :suggest,
 
489
      #       :with => 'q'
 
490
      #       %>
 
491
      #
 
492
      # Required +options+ are either of:
 
493
      # <tt>:url</tt>::       +url_for+-style options for the action to call
 
494
      #                       when the field has changed.
 
495
      # <tt>:function</tt>::  Instead of making a remote call to a URL, you
 
496
      #                       can specify javascript code to be called instead.
 
497
      #                       Note that the value of this option is used as the
 
498
      #                       *body* of the javascript function, a function definition
 
499
      #                       with parameters named element and value will be generated for you
 
500
      #                       for example:
 
501
      #                         observe_field("glass", :frequency => 1, :function => "alert('Element changed')")
 
502
      #                       will generate:
 
503
      #                         new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')})
 
504
      #                       The element parameter is the DOM element being observed, and the value is its value at the
 
505
      #                       time the observer is triggered.
 
506
      #
 
507
      # Additional options are:
 
508
      # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
 
509
      #                       this field will be detected. Not setting this
 
510
      #                       option at all or to a value equal to or less than
 
511
      #                       zero will use event based observation instead of
 
512
      #                       time based observation.
 
513
      # <tt>:update</tt>::    Specifies the DOM ID of the element whose
 
514
      #                       innerHTML should be updated with the
 
515
      #                       XMLHttpRequest response text.
 
516
      # <tt>:with</tt>::      A JavaScript expression specifying the parameters
 
517
      #                       for the XMLHttpRequest. The default is to send the
 
518
      #                       key and value of the observed field. Any custom
 
519
      #                       expressions should return a valid URL query string.
 
520
      #                       The value of the field is stored in the JavaScript
 
521
      #                       variable +value+.
 
522
      #
 
523
      #                       Examples
 
524
      #
 
525
      #                         :with => "'my_custom_key=' + value"
 
526
      #                         :with => "'person[name]=' + prompt('New name')"
 
527
      #                         :with => "Form.Element.serialize('other-field')"
 
528
      #
 
529
      #                       Finally
 
530
      #                         :with => 'name'
 
531
      #                       is shorthand for
 
532
      #                         :with => "'name=' + value"
 
533
      #                       This essentially just changes the key of the parameter.
 
534
      #
 
535
      # Additionally, you may specify any of the options documented in the
 
536
      # <em>Common options</em> section at the top of this document.
 
537
      #
 
538
      # Example:
 
539
      #
 
540
      #   # Sends params: {:title => 'Title of the book'} when the book_title input
 
541
      #   # field is changed.
 
542
      #   observe_field 'book_title',
 
543
      #     :url => 'http://example.com/books/edit/1',
 
544
      #     :with => 'title'
 
545
      #
 
546
      #
 
547
      def observe_field(field_id, options = {})
 
548
        if options[:frequency] && options[:frequency] > 0
 
549
          build_observer('Form.Element.Observer', field_id, options)
 
550
        else
 
551
          build_observer('Form.Element.EventObserver', field_id, options)
 
552
        end
 
553
      end
 
554
 
 
555
      # Observes the form with the DOM ID specified by +form_id+ and calls a
 
556
      # callback when its contents have changed. The default callback is an
 
557
      # Ajax call. By default all fields of the observed field are sent as
 
558
      # parameters with the Ajax call.
 
559
      #
 
560
      # The +options+ for +observe_form+ are the same as the options for
 
561
      # +observe_field+. The JavaScript variable +value+ available to the
 
562
      # <tt>:with</tt> option is set to the serialized form by default.
 
563
      def observe_form(form_id, options = {})
 
564
        if options[:frequency]
 
565
          build_observer('Form.Observer', form_id, options)
 
566
        else
 
567
          build_observer('Form.EventObserver', form_id, options)
 
568
        end
 
569
      end
 
570
 
 
571
      # All the methods were moved to GeneratorMethods so that
 
572
      # #include_helpers_from_context has nothing to overwrite.
 
573
      class JavaScriptGenerator #:nodoc:
 
574
        def initialize(context, &block) #:nodoc:
 
575
          @context, @lines = context, []
 
576
          include_helpers_from_context
 
577
          @context.with_output_buffer(@lines) do
 
578
            @context.instance_exec(self, &block)
 
579
          end
 
580
        end
 
581
 
 
582
        private
 
583
          def include_helpers_from_context
 
584
            extend @context.helpers if @context.respond_to?(:helpers)
 
585
            extend GeneratorMethods
 
586
          end
 
587
 
 
588
        # JavaScriptGenerator generates blocks of JavaScript code that allow you
 
589
        # to change the content and presentation of multiple DOM elements.  Use
 
590
        # this in your Ajax response bodies, either in a <script> tag or as plain
 
591
        # JavaScript sent with a Content-type of "text/javascript".
 
592
        #
 
593
        # Create new instances with PrototypeHelper#update_page or with
 
594
        # ActionController::Base#render, then call +insert_html+, +replace_html+,
 
595
        # +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in
 
596
        # methods on the yielded generator in any order you like to modify the
 
597
        # content and appearance of the current page.
 
598
        #
 
599
        # Example:
 
600
        #
 
601
        #   # Generates:
 
602
        #   #     new Element.insert("list", { bottom: "<li>Some item</li>" });
 
603
        #   #     new Effect.Highlight("list");
 
604
        #   #     ["status-indicator", "cancel-link"].each(Element.hide);
 
605
        #   update_page do |page|
 
606
        #     page.insert_html :bottom, 'list', "<li>#{@item.name}</li>"
 
607
        #     page.visual_effect :highlight, 'list'
 
608
        #     page.hide 'status-indicator', 'cancel-link'
 
609
        #   end
 
610
        #
 
611
        #
 
612
        # Helper methods can be used in conjunction with JavaScriptGenerator.
 
613
        # When a helper method is called inside an update block on the +page+
 
614
        # object, that method will also have access to a +page+ object.
 
615
        #
 
616
        # Example:
 
617
        #
 
618
        #   module ApplicationHelper
 
619
        #     def update_time
 
620
        #       page.replace_html 'time', Time.now.to_s(:db)
 
621
        #       page.visual_effect :highlight, 'time'
 
622
        #     end
 
623
        #   end
 
624
        #
 
625
        #   # Controller action
 
626
        #   def poll
 
627
        #     render(:update) { |page| page.update_time }
 
628
        #   end
 
629
        #
 
630
        # Calls to JavaScriptGenerator not matching a helper method below
 
631
        # generate a proxy to the JavaScript Class named by the method called.
 
632
        #
 
633
        # Examples:
 
634
        #
 
635
        #   # Generates:
 
636
        #   #     Foo.init();
 
637
        #   update_page do |page|
 
638
        #     page.foo.init
 
639
        #   end
 
640
        #
 
641
        #   # Generates:
 
642
        #   #     Event.observe('one', 'click', function () {
 
643
        #   #       $('two').show();
 
644
        #   #     });
 
645
        #   update_page do |page|
 
646
        #     page.event.observe('one', 'click') do |p|
 
647
        #      p[:two].show
 
648
        #     end
 
649
        #   end
 
650
        #
 
651
        # You can also use PrototypeHelper#update_page_tag instead of
 
652
        # PrototypeHelper#update_page to wrap the generated JavaScript in a
 
653
        # <script> tag.
 
654
        module GeneratorMethods
 
655
          def to_s #:nodoc:
 
656
            returning javascript = @lines * $/ do
 
657
              if ActionView::Base.debug_rjs
 
658
                source = javascript.dup
 
659
                javascript.replace "try {\n#{source}\n} catch (e) "
 
660
                javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
 
661
              end
 
662
            end
 
663
          end
 
664
 
 
665
          # Returns a element reference by finding it through +id+ in the DOM. This element can then be
 
666
          # used for further method calls. Examples:
 
667
          #
 
668
          #   page['blank_slate']                  # => $('blank_slate');
 
669
          #   page['blank_slate'].show             # => $('blank_slate').show();
 
670
          #   page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
 
671
          #
 
672
          # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
 
673
          # the correct id:
 
674
          #
 
675
          #   page[@post]     # => $('post_45')
 
676
          #   page[Post.new]  # => $('new_post')
 
677
          def [](id)
 
678
            case id
 
679
              when String, Symbol, NilClass
 
680
                JavaScriptElementProxy.new(self, id)
 
681
              else
 
682
                JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
 
683
            end
 
684
          end
 
685
 
 
686
          # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
 
687
          # expression as an argument to another JavaScriptGenerator method.
 
688
          def literal(code)
 
689
            ::ActiveSupport::JSON::Variable.new(code.to_s)
 
690
          end
 
691
 
 
692
          # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
 
693
          # used for further method calls. Examples:
 
694
          #
 
695
          #   page.select('p')                      # => $$('p');
 
696
          #   page.select('p.welcome b').first      # => $$('p.welcome b').first();
 
697
          #   page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
 
698
          #
 
699
          # You can also use prototype enumerations with the collection.  Observe:
 
700
          #
 
701
          #   # Generates: $$('#items li').each(function(value) { value.hide(); });
 
702
          #   page.select('#items li').each do |value|
 
703
          #     value.hide
 
704
          #   end
 
705
          #
 
706
          # Though you can call the block param anything you want, they are always rendered in the
 
707
          # javascript as 'value, index.'  Other enumerations, like collect() return the last statement:
 
708
          #
 
709
          #   # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
 
710
          #   page.select('#items li').collect('hidden') do |item|
 
711
          #     item.hide
 
712
          #   end
 
713
          #
 
714
          def select(pattern)
 
715
            JavaScriptElementCollectionProxy.new(self, pattern)
 
716
          end
 
717
 
 
718
          # Inserts HTML at the specified +position+ relative to the DOM element
 
719
          # identified by the given +id+.
 
720
          #
 
721
          # +position+ may be one of:
 
722
          #
 
723
          # <tt>:top</tt>::    HTML is inserted inside the element, before the
 
724
          #                    element's existing content.
 
725
          # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
 
726
          #                    element's existing content.
 
727
          # <tt>:before</tt>:: HTML is inserted immediately preceding the element.
 
728
          # <tt>:after</tt>::  HTML is inserted immediately following the element.
 
729
          #
 
730
          # +options_for_render+ may be either a string of HTML to insert, or a hash
 
731
          # of options to be passed to ActionView::Base#render.  For example:
 
732
          #
 
733
          #   # Insert the rendered 'navigation' partial just before the DOM
 
734
          #   # element with ID 'content'.
 
735
          #   # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
 
736
          #   page.insert_html :before, 'content', :partial => 'navigation'
 
737
          #
 
738
          #   # Add a list item to the bottom of the <ul> with ID 'list'.
 
739
          #   # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
 
740
          #   page.insert_html :bottom, 'list', '<li>Last item</li>'
 
741
          #
 
742
          def insert_html(position, id, *options_for_render)
 
743
            content = javascript_object_for(render(*options_for_render))
 
744
            record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
 
745
          end
 
746
 
 
747
          # Replaces the inner HTML of the DOM element with the given +id+.
 
748
          #
 
749
          # +options_for_render+ may be either a string of HTML to insert, or a hash
 
750
          # of options to be passed to ActionView::Base#render.  For example:
 
751
          #
 
752
          #   # Replace the HTML of the DOM element having ID 'person-45' with the
 
753
          #   # 'person' partial for the appropriate object.
 
754
          #   # Generates:  Element.update("person-45", "-- Contents of 'person' partial --");
 
755
          #   page.replace_html 'person-45', :partial => 'person', :object => @person
 
756
          #
 
757
          def replace_html(id, *options_for_render)
 
758
            call 'Element.update', id, render(*options_for_render)
 
759
          end
 
760
 
 
761
          # Replaces the "outer HTML" (i.e., the entire element, not just its
 
762
          # contents) of the DOM element with the given +id+.
 
763
          #
 
764
          # +options_for_render+ may be either a string of HTML to insert, or a hash
 
765
          # of options to be passed to ActionView::Base#render.  For example:
 
766
          #
 
767
          #   # Replace the DOM element having ID 'person-45' with the
 
768
          #   # 'person' partial for the appropriate object.
 
769
          #   page.replace 'person-45', :partial => 'person', :object => @person
 
770
          #
 
771
          # This allows the same partial that is used for the +insert_html+ to
 
772
          # be also used for the input to +replace+ without resorting to
 
773
          # the use of wrapper elements.
 
774
          #
 
775
          # Examples:
 
776
          #
 
777
          #   <div id="people">
 
778
          #     <%= render :partial => 'person', :collection => @people %>
 
779
          #   </div>
 
780
          #
 
781
          #   # Insert a new person
 
782
          #   #
 
783
          #   # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
 
784
          #   page.insert_html :bottom, :partial => 'person', :object => @person
 
785
          #
 
786
          #   # Replace an existing person
 
787
          #
 
788
          #   # Generates: Element.replace("person_45", "-- Contents of partial --");
 
789
          #   page.replace 'person_45', :partial => 'person', :object => @person
 
790
          #
 
791
          def replace(id, *options_for_render)
 
792
            call 'Element.replace', id, render(*options_for_render)
 
793
          end
 
794
 
 
795
          # Removes the DOM elements with the given +ids+ from the page.
 
796
          #
 
797
          # Example:
 
798
          #
 
799
          #  # Remove a few people
 
800
          #  # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
 
801
          #  page.remove 'person_23', 'person_9', 'person_2'
 
802
          #
 
803
          def remove(*ids)
 
804
            loop_on_multiple_args 'Element.remove', ids
 
805
          end
 
806
 
 
807
          # Shows hidden DOM elements with the given +ids+.
 
808
          #
 
809
          # Example:
 
810
          #
 
811
          #  # Show a few people
 
812
          #  # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
 
813
          #  page.show 'person_6', 'person_13', 'person_223'
 
814
          #
 
815
          def show(*ids)
 
816
            loop_on_multiple_args 'Element.show', ids
 
817
          end
 
818
 
 
819
          # Hides the visible DOM elements with the given +ids+.
 
820
          #
 
821
          # Example:
 
822
          #
 
823
          #  # Hide a few people
 
824
          #  # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
 
825
          #  page.hide 'person_29', 'person_9', 'person_0'
 
826
          #
 
827
          def hide(*ids)
 
828
            loop_on_multiple_args 'Element.hide', ids
 
829
          end
 
830
 
 
831
          # Toggles the visibility of the DOM elements with the given +ids+.
 
832
          # Example:
 
833
          #
 
834
          #  # Show a few people
 
835
          #  # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle);
 
836
          #  page.toggle 'person_14', 'person_12', 'person_23'      # Hides the elements
 
837
          #  page.toggle 'person_14', 'person_12', 'person_23'      # Shows the previously hidden elements
 
838
          #
 
839
          def toggle(*ids)
 
840
            loop_on_multiple_args 'Element.toggle', ids
 
841
          end
 
842
 
 
843
          # Displays an alert dialog with the given +message+.
 
844
          #
 
845
          # Example:
 
846
          #
 
847
          #   # Generates: alert('This message is from Rails!')
 
848
          #   page.alert('This message is from Rails!')
 
849
          def alert(message)
 
850
            call 'alert', message
 
851
          end
 
852
 
 
853
          # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
 
854
          #
 
855
          # Examples:
 
856
          #
 
857
          #  # Generates: window.location.href = "/mycontroller";
 
858
          #  page.redirect_to(:action => 'index')
 
859
          #
 
860
          #  # Generates: window.location.href = "/account/signup";
 
861
          #  page.redirect_to(:controller => 'account', :action => 'signup')
 
862
          def redirect_to(location)
 
863
            url = location.is_a?(String) ? location : @context.url_for(location)
 
864
            record "window.location.href = #{url.inspect}"
 
865
          end
 
866
 
 
867
          # Reloads the browser's current +location+ using JavaScript
 
868
          #
 
869
          # Examples:
 
870
          #
 
871
          #  # Generates: window.location.reload();
 
872
          #  page.reload
 
873
          def reload
 
874
            record 'window.location.reload()'
 
875
          end
 
876
 
 
877
          # Calls the JavaScript +function+, optionally with the given +arguments+.
 
878
          #
 
879
          # If a block is given, the block will be passed to a new JavaScriptGenerator;
 
880
          # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
 
881
          # and passed as the called function's final argument.
 
882
          #
 
883
          # Examples:
 
884
          #
 
885
          #   # Generates: Element.replace(my_element, "My content to replace with.")
 
886
          #   page.call 'Element.replace', 'my_element', "My content to replace with."
 
887
          #
 
888
          #   # Generates: alert('My message!')
 
889
          #   page.call 'alert', 'My message!'
 
890
          #
 
891
          #   # Generates:
 
892
          #   #     my_method(function() {
 
893
          #   #       $("one").show();
 
894
          #   #       $("two").hide();
 
895
          #   #    });
 
896
          #   page.call(:my_method) do |p|
 
897
          #      p[:one].show
 
898
          #      p[:two].hide
 
899
          #   end
 
900
          def call(function, *arguments, &block)
 
901
            record "#{function}(#{arguments_for_call(arguments, block)})"
 
902
          end
 
903
 
 
904
          # Assigns the JavaScript +variable+ the given +value+.
 
905
          #
 
906
          # Examples:
 
907
          #
 
908
          #  # Generates: my_string = "This is mine!";
 
909
          #  page.assign 'my_string', 'This is mine!'
 
910
          #
 
911
          #  # Generates: record_count = 33;
 
912
          #  page.assign 'record_count', 33
 
913
          #
 
914
          #  # Generates: tabulated_total = 47
 
915
          #  page.assign 'tabulated_total', @total_from_cart
 
916
          #
 
917
          def assign(variable, value)
 
918
            record "#{variable} = #{javascript_object_for(value)}"
 
919
          end
 
920
 
 
921
          # Writes raw JavaScript to the page.
 
922
          #
 
923
          # Example:
 
924
          #
 
925
          #  page << "alert('JavaScript with Prototype.');"
 
926
          def <<(javascript)
 
927
            @lines << javascript
 
928
          end
 
929
 
 
930
          # Executes the content of the block after a delay of +seconds+. Example:
 
931
          #
 
932
          #   # Generates:
 
933
          #   #     setTimeout(function() {
 
934
          #   #     ;
 
935
          #   #     new Effect.Fade("notice",{});
 
936
          #   #     }, 20000);
 
937
          #   page.delay(20) do
 
938
          #     page.visual_effect :fade, 'notice'
 
939
          #   end
 
940
          def delay(seconds = 1)
 
941
            record "setTimeout(function() {\n\n"
 
942
            yield
 
943
            record "}, #{(seconds * 1000).to_i})"
 
944
          end
 
945
 
 
946
          # Starts a script.aculo.us visual effect. See
 
947
          # ActionView::Helpers::ScriptaculousHelper for more information.
 
948
          def visual_effect(name, id = nil, options = {})
 
949
            record @context.send(:visual_effect, name, id, options)
 
950
          end
 
951
 
 
952
          # Creates a script.aculo.us sortable element. Useful
 
953
          # to recreate sortable elements after items get added
 
954
          # or deleted.
 
955
          # See ActionView::Helpers::ScriptaculousHelper for more information.
 
956
          def sortable(id, options = {})
 
957
            record @context.send(:sortable_element_js, id, options)
 
958
          end
 
959
 
 
960
          # Creates a script.aculo.us draggable element.
 
961
          # See ActionView::Helpers::ScriptaculousHelper for more information.
 
962
          def draggable(id, options = {})
 
963
            record @context.send(:draggable_element_js, id, options)
 
964
          end
 
965
 
 
966
          # Creates a script.aculo.us drop receiving element.
 
967
          # See ActionView::Helpers::ScriptaculousHelper for more information.
 
968
          def drop_receiving(id, options = {})
 
969
            record @context.send(:drop_receiving_element_js, id, options)
 
970
          end
 
971
 
 
972
          private
 
973
            def loop_on_multiple_args(method, ids)
 
974
              record(ids.size>1 ?
 
975
                "#{javascript_object_for(ids)}.each(#{method})" :
 
976
                "#{method}(#{::ActiveSupport::JSON.encode(ids.first)})")
 
977
            end
 
978
 
 
979
            def page
 
980
              self
 
981
            end
 
982
 
 
983
            def record(line)
 
984
              returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
 
985
                self << line
 
986
              end
 
987
            end
 
988
 
 
989
            def render(*options_for_render)
 
990
              old_format = @context && @context.template_format
 
991
              @context.template_format = :html if @context
 
992
              Hash === options_for_render.first ?
 
993
                @context.render(*options_for_render) :
 
994
                  options_for_render.first.to_s
 
995
            ensure
 
996
              @context.template_format = old_format if @context
 
997
            end
 
998
 
 
999
            def javascript_object_for(object)
 
1000
              ::ActiveSupport::JSON.encode(object)
 
1001
            end
 
1002
 
 
1003
            def arguments_for_call(arguments, block = nil)
 
1004
              arguments << block_to_function(block) if block
 
1005
              arguments.map { |argument| javascript_object_for(argument) }.join ', '
 
1006
            end
 
1007
 
 
1008
            def block_to_function(block)
 
1009
              generator = self.class.new(@context, &block)
 
1010
              literal("function() { #{generator.to_s} }")
 
1011
            end
 
1012
 
 
1013
            def method_missing(method, *arguments)
 
1014
              JavaScriptProxy.new(self, method.to_s.camelize)
 
1015
            end
 
1016
        end
 
1017
      end
 
1018
 
 
1019
      # Yields a JavaScriptGenerator and returns the generated JavaScript code.
 
1020
      # Use this to update multiple elements on a page in an Ajax response.
 
1021
      # See JavaScriptGenerator for more information.
 
1022
      #
 
1023
      # Example:
 
1024
      #
 
1025
      #   update_page do |page|
 
1026
      #     page.hide 'spinner'
 
1027
      #   end
 
1028
      def update_page(&block)
 
1029
        JavaScriptGenerator.new(@template, &block).to_s
 
1030
      end
 
1031
 
 
1032
      # Works like update_page but wraps the generated JavaScript in a <script>
 
1033
      # tag. Use this to include generated JavaScript in an ERb template.
 
1034
      # See JavaScriptGenerator for more information.
 
1035
      #
 
1036
      # +html_options+ may be a hash of <script> attributes to be passed
 
1037
      # to ActionView::Helpers::JavaScriptHelper#javascript_tag.
 
1038
      def update_page_tag(html_options = {}, &block)
 
1039
        javascript_tag update_page(&block), html_options
 
1040
      end
 
1041
 
 
1042
    protected
 
1043
      def options_for_ajax(options)
 
1044
        js_options = build_callbacks(options)
 
1045
 
 
1046
        js_options['asynchronous'] = options[:type] != :synchronous
 
1047
        js_options['method']       = method_option_to_s(options[:method]) if options[:method]
 
1048
        js_options['insertion']    = "'#{options[:position].to_s.downcase}'" if options[:position]
 
1049
        js_options['evalScripts']  = options[:script].nil? || options[:script]
 
1050
 
 
1051
        if options[:form]
 
1052
          js_options['parameters'] = 'Form.serialize(this)'
 
1053
        elsif options[:submit]
 
1054
          js_options['parameters'] = "Form.serialize('#{options[:submit]}')"
 
1055
        elsif options[:with]
 
1056
          js_options['parameters'] = options[:with]
 
1057
        end
 
1058
 
 
1059
        if protect_against_forgery? && !options[:form]
 
1060
          if js_options['parameters']
 
1061
            js_options['parameters'] << " + '&"
 
1062
          else
 
1063
            js_options['parameters'] = "'"
 
1064
          end
 
1065
          js_options['parameters'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')"
 
1066
        end
 
1067
 
 
1068
        options_for_javascript(js_options)
 
1069
      end
 
1070
 
 
1071
      def method_option_to_s(method)
 
1072
        (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
 
1073
      end
 
1074
 
 
1075
      def build_observer(klass, name, options = {})
 
1076
        if options[:with] && (options[:with] !~ /[\{=(.]/)
 
1077
          options[:with] = "'#{options[:with]}=' + encodeURIComponent(value)"
 
1078
        else
 
1079
          options[:with] ||= 'value' unless options[:function]
 
1080
        end
 
1081
 
 
1082
        callback = options[:function] || remote_function(options)
 
1083
        javascript  = "new #{klass}('#{name}', "
 
1084
        javascript << "#{options[:frequency]}, " if options[:frequency]
 
1085
        javascript << "function(element, value) {"
 
1086
        javascript << "#{callback}}"
 
1087
        javascript << ")"
 
1088
        javascript_tag(javascript)
 
1089
      end
 
1090
 
 
1091
      def build_callbacks(options)
 
1092
        callbacks = {}
 
1093
        options.each do |callback, code|
 
1094
          if CALLBACKS.include?(callback)
 
1095
            name = 'on' + callback.to_s.capitalize
 
1096
            callbacks[name] = "function(request){#{code}}"
 
1097
          end
 
1098
        end
 
1099
        callbacks
 
1100
      end
 
1101
    end
 
1102
 
 
1103
    # Converts chained method calls on DOM proxy elements into JavaScript chains
 
1104
    class JavaScriptProxy < ActiveSupport::BasicObject #:nodoc:
 
1105
 
 
1106
      def initialize(generator, root = nil)
 
1107
        @generator = generator
 
1108
        @generator << root if root
 
1109
      end
 
1110
 
 
1111
      private
 
1112
        def method_missing(method, *arguments, &block)
 
1113
          if method.to_s =~ /(.*)=$/
 
1114
            assign($1, arguments.first)
 
1115
          else
 
1116
            call("#{method.to_s.camelize(:lower)}", *arguments, &block)
 
1117
          end
 
1118
        end
 
1119
 
 
1120
        def call(function, *arguments, &block)
 
1121
          append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
 
1122
          self
 
1123
        end
 
1124
 
 
1125
        def assign(variable, value)
 
1126
          append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
 
1127
        end
 
1128
 
 
1129
        def function_chain
 
1130
          @function_chain ||= @generator.instance_variable_get(:@lines)
 
1131
        end
 
1132
 
 
1133
        def append_to_function_chain!(call)
 
1134
          function_chain[-1].chomp!(';')
 
1135
          function_chain[-1] += ".#{call};"
 
1136
        end
 
1137
    end
 
1138
 
 
1139
    class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
 
1140
      def initialize(generator, id)
 
1141
        @id = id
 
1142
        super(generator, "$(#{::ActiveSupport::JSON.encode(id)})")
 
1143
      end
 
1144
 
 
1145
      # Allows access of element attributes through +attribute+. Examples:
 
1146
      #
 
1147
      #   page['foo']['style']                  # => $('foo').style;
 
1148
      #   page['foo']['style']['color']         # => $('blank_slate').style.color;
 
1149
      #   page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
 
1150
      #   page['foo']['style'].color = 'red'    # => $('blank_slate').style.color = 'red';
 
1151
      def [](attribute)
 
1152
        append_to_function_chain!(attribute)
 
1153
        self
 
1154
      end
 
1155
 
 
1156
      def []=(variable, value)
 
1157
        assign(variable, value)
 
1158
      end
 
1159
 
 
1160
      def replace_html(*options_for_render)
 
1161
        call 'update', @generator.send(:render, *options_for_render)
 
1162
      end
 
1163
 
 
1164
      def replace(*options_for_render)
 
1165
        call 'replace', @generator.send(:render, *options_for_render)
 
1166
      end
 
1167
 
 
1168
      def reload(options_for_replace = {})
 
1169
        replace(options_for_replace.merge({ :partial => @id.to_s }))
 
1170
      end
 
1171
 
 
1172
    end
 
1173
 
 
1174
    class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
 
1175
      def initialize(generator, variable)
 
1176
        @variable = variable
 
1177
        @empty    = true # only record lines if we have to.  gets rid of unnecessary linebreaks
 
1178
        super(generator)
 
1179
      end
 
1180
 
 
1181
      # The JSON Encoder calls this to check for the +to_json+ method
 
1182
      # Since it's a blank slate object, I suppose it responds to anything.
 
1183
      def respond_to?(method)
 
1184
        true
 
1185
      end
 
1186
 
 
1187
      def to_json(options = nil)
 
1188
        @variable
 
1189
      end
 
1190
 
 
1191
      private
 
1192
        def append_to_function_chain!(call)
 
1193
          @generator << @variable if @empty
 
1194
          @empty = false
 
1195
          super
 
1196
        end
 
1197
    end
 
1198
 
 
1199
    class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
 
1200
      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
 
1201
      ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS
 
1202
      attr_reader :generator
 
1203
      delegate :arguments_for_call, :to => :generator
 
1204
 
 
1205
      def initialize(generator, pattern)
 
1206
        super(generator, @pattern = pattern)
 
1207
      end
 
1208
 
 
1209
      def each_slice(variable, number, &block)
 
1210
        if block
 
1211
          enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
 
1212
        else
 
1213
          add_variable_assignment!(variable)
 
1214
          append_enumerable_function!("eachSlice(#{::ActiveSupport::JSON.encode(number)});")
 
1215
        end
 
1216
      end
 
1217
 
 
1218
      def grep(variable, pattern, &block)
 
1219
        enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block
 
1220
      end
 
1221
 
 
1222
      def in_groups_of(variable, number, fill_with = nil)
 
1223
        arguments = [number]
 
1224
        arguments << fill_with unless fill_with.nil?
 
1225
        add_variable_assignment!(variable)
 
1226
        append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});")
 
1227
      end
 
1228
 
 
1229
      def inject(variable, memo, &block)
 
1230
        enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
 
1231
      end
 
1232
 
 
1233
      def pluck(variable, property)
 
1234
        add_variable_assignment!(variable)
 
1235
        append_enumerable_function!("pluck(#{::ActiveSupport::JSON.encode(property)});")
 
1236
      end
 
1237
 
 
1238
      def zip(variable, *arguments, &block)
 
1239
        add_variable_assignment!(variable)
 
1240
        append_enumerable_function!("zip(#{arguments_for_call arguments}")
 
1241
        if block
 
1242
          function_chain[-1] += ", function(array) {"
 
1243
          yield ::ActiveSupport::JSON::Variable.new('array')
 
1244
          add_return_statement!
 
1245
          @generator << '});'
 
1246
        else
 
1247
          function_chain[-1] += ');'
 
1248
        end
 
1249
      end
 
1250
 
 
1251
      private
 
1252
        def method_missing(method, *arguments, &block)
 
1253
          if ENUMERABLE_METHODS.include?(method)
 
1254
            returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method)
 
1255
            variable   = arguments.first if returnable
 
1256
            enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block)
 
1257
          else
 
1258
            super
 
1259
          end
 
1260
        end
 
1261
 
 
1262
        # Options
 
1263
        #   * variable - name of the variable to set the result of the enumeration to
 
1264
        #   * method_args - array of the javascript enumeration method args that occur before the function
 
1265
        #   * yield_args - array of the javascript yield args
 
1266
        #   * return - true if the enumeration should return the last statement
 
1267
        def enumerate(enumerable, options = {}, &block)
 
1268
          options[:method_args] ||= []
 
1269
          options[:yield_args]  ||= []
 
1270
          yield_args  = options[:yield_args] * ', '
 
1271
          method_args = arguments_for_call options[:method_args] # foo, bar, function
 
1272
          method_args << ', ' unless method_args.blank?
 
1273
          add_variable_assignment!(options[:variable]) if options[:variable]
 
1274
          append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {")
 
1275
          # only yield as many params as were passed in the block
 
1276
          yield(*options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1])
 
1277
          add_return_statement! if options[:return]
 
1278
          @generator << '});'
 
1279
        end
 
1280
 
 
1281
        def add_variable_assignment!(variable)
 
1282
          function_chain.push("var #{variable} = #{function_chain.pop}")
 
1283
        end
 
1284
 
 
1285
        def add_return_statement!
 
1286
          unless function_chain.last =~ /return/
 
1287
            function_chain.push("return #{function_chain.pop.chomp(';')};")
 
1288
          end
 
1289
        end
 
1290
 
 
1291
        def append_enumerable_function!(call)
 
1292
          function_chain[-1].chomp!(';')
 
1293
          function_chain[-1] += ".#{call}"
 
1294
        end
 
1295
    end
 
1296
 
 
1297
    class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
 
1298
      def initialize(generator, pattern)
 
1299
        super(generator, "$$(#{::ActiveSupport::JSON.encode(pattern)})")
 
1300
      end
 
1301
    end
 
1302
  end
 
1303
end
 
1304
 
 
1305
require 'action_view/helpers/javascript_helper'