diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index 080eb87445..3d088678fc 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -3,7 +3,7 @@ require 'active_support/benchmarkable' module ActionView #:nodoc: module Helpers #:nodoc: autoload :ActiveModelHelper, 'action_view/helpers/active_model_helper' - autoload :AjaxHelperCompat, 'action_view/helpers/ajax_helper' + autoload :AjaxHelper, 'action_view/helpers/ajax_helper' autoload :AssetTagHelper, 'action_view/helpers/asset_tag_helper' autoload :AtomFeedHelper, 'action_view/helpers/atom_feed_helper' autoload :CacheHelper, 'action_view/helpers/cache_helper' @@ -15,11 +15,12 @@ module ActionView #:nodoc: autoload :FormTagHelper, 'action_view/helpers/form_tag_helper' autoload :JavaScriptHelper, 'action_view/helpers/javascript_helper' autoload :NumberHelper, 'action_view/helpers/number_helper' - autoload :AjaxHelper, 'action_view/helpers/ajax_helper' + autoload :PrototypeHelper, 'action_view/helpers/prototype_helper' autoload :RawOutputHelper, 'action_view/helpers/raw_output_helper' autoload :RecordIdentificationHelper, 'action_view/helpers/record_identification_helper' autoload :RecordTagHelper, 'action_view/helpers/record_tag_helper' autoload :SanitizeHelper, 'action_view/helpers/sanitize_helper' + autoload :ScriptaculousHelper, 'action_view/helpers/scriptaculous_helper' autoload :TagHelper, 'action_view/helpers/tag_helper' autoload :TextHelper, 'action_view/helpers/text_helper' autoload :TranslationHelper, 'action_view/helpers/translation_helper' @@ -47,11 +48,12 @@ module ActionView #:nodoc: include FormTagHelper include JavaScriptHelper include NumberHelper - include AjaxHelperCompat + include PrototypeHelper include RawOutputHelper include RecordIdentificationHelper include RecordTagHelper include SanitizeHelper + include ScriptaculousHelper include TagHelper include TextHelper include TranslationHelper diff --git a/actionpack/lib/action_view/helpers/ajax_helper.rb b/actionpack/lib/action_view/helpers/ajax_helper.rb index 169a803848..9cc2acc239 100644 --- a/actionpack/lib/action_view/helpers/ajax_helper.rb +++ b/actionpack/lib/action_view/helpers/ajax_helper.rb @@ -1,713 +1,68 @@ module ActionView module Helpers module AjaxHelper - # Included for backwards compatibility / RJS functionality - # Rails classes should not be aware of individual JS frameworks - include PrototypeHelper - - # Returns a form that will allow the unobtrusive JavaScript drivers to submit the - # form dynamically. The default driver behaviour is an XMLHttpRequest in the background - # instead of the regular POST arrangement. Even though it's using JavaScript to serialize - # the form elements, the form submission will work just like a regular submission as - # viewed by the receiving side (all elements available in params). The options - # for specifying the target with :url anddefining callbacks is the same as +link_to_remote+. - # - # === Resource - # - # Example: - # - # # Generates: - # #
- # # - # <% remote_form_for(@record, {:html => { :id => 'create-author' }}) do |f| %> - # ... - # <% end %> - # - # This will expand to be the same as: - # - # <% remote_form_for :post, @post, :url => post_path(@post), - # :html => { :method => :put, - # :class => "edit_post", - # :id => "edit_post_45" } do |f| %> - # ... - # <% end %> - # - # === Nested Resource - # - # Example: - # # Generates: - # # - # # - # <% remote_form_for([@author, @article]) do |f| %> - # ... - # <% end %> - # - # This will expand to be the same as: - # - # <% remote_form_for :article, @article, :url => author_article_path(@author, @article), - # :html => { :method => :put, - # :class => "new_article", - # :id => "new_comment" } do |f| %> - # ... - # <% end %> - # - # If you don't need to attach a form to a resource, then check out form_remote_tag. - # - # See FormHelper#form_for for additional semantics. - def remote_form_for(record_or_name_or_array, *args, &proc) - options = args.extract_options! - - if confirm = options.delete(:confirm) - add_confirm_to_attributes!(options, confirm) - end - - object_name = extract_object_name_for_form!(args, options, record_or_name_or_array) - - concat(form_remote_tag(options)) - fields_for(object_name, *(args << options), &proc) - concat(''.html_safe!) - end - alias_method :form_remote_for, :remote_form_for - - # Returns a form tag that will allow the unobtrusive JavaScript drivers to submit the - # form dynamically. The default JavaScript driver behaviour is an XMLHttpRequest - # in the background instead of the regular POST arrangement. Even though it's using - # JavaScript to serialize the form elements, the form submission will work just like - # a regular submission as viewed by the receiving side (all elements available in - # params). The options for specifying the target with :url and - # defining callbacks is the same as +link_to_remote+. - # - # A "fall-through" target for browsers that doesn't do JavaScript can be - # specified with the :action/:method options on :html. - # - # Example: - # - # # Generates: - # # - # # - # form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }) {} - # - # The Hash passed to the :html key is equivalent to the options (2nd) - # argument in the FormTagHelper.form_tag method. - # - # By default the fall-through action is the same as the one specified in - # the :url (and the default method is :post). - # - # form_remote_tag also takes a block, like form_tag: - # # Generates: - # # - # # - # <% form_remote_tag :url => '/posts' do -%> - # <%= submit_tag 'Save' %> - # <% end -%> - # - # # Generates: - # # - # # - # <% form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }) do -%> - # "Hello world!" - # <% end -%> - # - def form_remote_tag(options = {}, &block) - html_options = options.delete(:callbacks) - - attributes = {} - attributes.merge!(extract_remote_attributes!(options)) - attributes.merge!(html_options) if html_options - attributes.merge!(options) - attributes.delete(:builder) - - form_tag(attributes.delete(:action) || attributes.delete("data-url"), attributes, &block) - end - - # Returns a link that will allow unobtrusive JavaScript to dynamical adjust its - # behaviour. The default behaviour is an XMLHttpRequest in the background instead - # of the regular GET arrangement. The result of that request can then be inserted - # into a DOM object whose id can be specified with options[:update]. Usually, - # the result would be a partial prepared by the controller with render :partial. - # - # Examples: - # - # # Generates: - # # Remove Author - # # - # link_to_remote("Remove Author", { :url => { :action => "whatnot" }, - # :method => "delete"}) - # - # - # You can override the generated HTML options by specifying a hash in - # options[:html]. - # - # # Generates: - # # Remove Author - # # - # link_to_remote("Remove Author", { :url => { :action => "whatnot" }, - # :method => "delete", - # :html => { :class => "fine" }}) - # - # - # You can also specify a hash for options[:update] to allow for - # easy redirection of output to an other DOM element if a server-side - # error occurs: - # - # Example: - # # Generates: - # # - # # Delete this Post' - # # - # link_to_remote "Delete this post", - # :url => { :action => "destroy"}, - # :update => { :success => "posts", :failure => "error" } - # - # Optionally, you can use the options[:position] parameter to - # influence how the target DOM element is updated. It must be one of - # :before, :top, :bottom, or :after. - # - # Example: - # # Generates: - # # Remove Author - # # - # link_to_remote("Remove Author", :url => { :action => "whatnot" }, :position => :bottom) - # - # - # The method used is by default POST. You can also specify GET or you - # can simulate PUT or DELETE over POST. All specified with options[:method] - # - # Example: - # # Generates: - # # Destroy - # # - # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete - # - # By default, these remote requests are processed asynchronous during - # which various JavaScript callbacks can be triggered (for progress - # indicators and the likes). All callbacks get access to the - # request object, which holds the underlying XMLHttpRequest. - # - # To access the server response, use request.responseText, to - # find out the HTTP status, use request.status. - # - # Example: - # # Generates: - # # - # # undo - # # - # link_to_remote "undo", - # :url => { :controller => "words", :action => "undo", :n => word_counter }, - # :complete => "undoRequestCompleted(request)" - # - # The callbacks that may be specified are (in order): - # - # :loading:: Called when the remote document is being - # loaded with data by the browser. - # :loaded:: Called when the browser has finished loading - # the remote document. - # :interactive:: Called when the user can interact with the - # remote document, even though it has not - # finished loading. - # :success:: Called when the XMLHttpRequest is completed, - # and the HTTP status code is in the 2XX range. - # :failure:: Called when the XMLHttpRequest is completed, - # and the HTTP status code is not in the 2XX - # range. - # :complete:: Called when the XMLHttpRequest is complete - # (fires after success/failure if they are - # present). - # - # You can further refine :success and :failure by - # adding additional callbacks for specific status codes. - # - # Example: - # - # # Generates: - # # Hello - # # - # link_to_remote word, - # :url => { :action => "action" }, - # 404 => "alert('Not found...? Wrong URL...?')", - # :failure => "alert('HTTP Error ' + request.status + '!')" - # - # A status code callback overrides the success/failure handlers if - # present. - # - # If you for some reason or another need synchronous processing (that'll - # block the browser while the request is happening), you can specify - # options[:type] = :synchronous. - # - # You can customize further browser side call logic by passing in - # JavaScript code snippets via some optional parameters. In their order - # of use these are: - # - # :confirm:: Adds confirmation dialog. - # :condition:: Perform remote request conditionally - # by this expression. Use this to - # describe browser-side conditions when - # request should not be initiated. - # :before:: Called before request is initiated. - # :after:: Called immediately after request was - # initiated and before :loading. - # :submit:: Specifies the DOM element ID that's used - # as the parent of the form elements. By - # default this is the current form, but - # it could just as well be the ID of a - # table row or any other DOM element. - # :with:: A JavaScript expression specifying - # the parameters for the XMLHttpRequest. - # Any expressions should return a valid - # URL query string. - # - # Example: - # - # :with => "'name=' + $('name').value" - # - # You can generate a link that uses the UJS drivers in the general case, while - # degrading gracefully to plain link behavior in the absence of - # JavaScript by setting html_options[:href] to an alternate URL. - # Note the extra curly braces around the options hash separate - # it as the second parameter from html_options, the third. - # - # Example: - # - # # Generates: - # # Delete this post - # # - # link_to_remote "Delete this post", - # { :update => "posts", :url => { :action => "destroy", :id => post.id } } - # - def link_to_remote(name, options, html_options = {}) - attributes = {} - - attributes.merge!(:rel => "nofollow") if options[:method] && options[:method].to_s.downcase == "delete" - attributes.merge!(extract_remote_attributes!(options)) + include UrlHelper + + def link_to_remote(name, url, options = {}) + html = options.delete(:html) || {} - if confirm = options.delete(:confirm) - add_confirm_to_attributes!(attributes, confirm) + update = options.delete(:update) + if update.is_a?(Hash) + html["data-update-success"] = update[:success] + html["data-update-failure"] = update[:failure] + else + html["data-update-success"] = update end - attributes.merge!(html_options) - href = html_options[:href].nil? ? "#" : html_options[:href] - attributes.merge!(:href => href) - - content_tag(:a, name, attributes) - end - - # Returns an input of type button, which allows the unobtrusive JavaScript driver - # to dynamically adjust its behaviour. The default driver behaviour is to call a - # remote action via XMLHttpRequest in the background. - # The options for specifying the target with :url and defining callbacks is the same - # as link_to_remote. - # - # Example: - # - # # Generates: - # # - # # - # button_to_remote("Remote outpost", { :url => { :action => "whatnot" }}, { :class => "fine" }) - # - def button_to_remote(name, options = {}, html_options = {}) - attributes = html_options.merge!(:type => "button", :value => name) - - if confirm = options.delete(:confirm) - add_confirm_to_attributes!(attributes, confirm) - end - - if disable_with = options.delete(:disable_with) - add_disable_with_to_attributes!(attributes, disable_with) - end - - attributes.merge!(extract_remote_attributes!(options)) - - tag(:input, attributes) - end - - # Returns an input tag of type button, with the element name of +name+ and a value (i.e., display text) - # of +value+ which will allow the unobtrusive JavaScript driver to dynamically adjust its behaviour - # The default behaviour is to call a remote action via XMLHttpRequest in the background. - # - # request that reloads the page. - # - # # Create a button that submits to the create action - # # - # # Generates: - # # - # # - # <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %> - # - # # Submit to the remote action update and update the DIV succeed or fail based - # # on the success or failure of the request - # # - # # Generates: - # # - # # - # <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' }, - # :update => { :success => "succeed", :failure => "fail" } - # - # options argument is the same as in form_remote_tag. - def submit_to_remote(name, value, options = {}) - html_options = options.delete(:html) || {} - html_options.merge!(:name => name, :value => value, :type => "button") - - attributes = extract_remote_attributes!(options) - attributes.merge!(html_options) - attributes["data-remote-submit"] = true - attributes.delete("data-remote") - - tag(:input, attributes) - end - - # Periodically provides the UJS driver with the information to call the specified - # url (options[:url]) every options[:frequency] seconds (default is 10). Usually used to - # update a specified div (options[:update]) with the results - # of the remote call. The options for specifying the target with :url - # and defining callbacks is the same as link_to_remote. - # Examples: - # # Call get_averages and put its results in 'avg' every 10 seconds - # # Generates: - # # - # # - # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg') - # - # # Call invoice every 10 seconds with the id of the customer - # # If it succeeds, update the invoice DIV; if it fails, update the error DIV - # # Generates: - # # " - # # - # periodically_call_remote(:url => { :action => 'invoice', :id => 1 }, - # :update => { :success => "invoice", :failure => "error" } - # - # # Call update every 20 seconds and update the new_block DIV - # # Generates: - # # - # # - # periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block') - # - def periodically_call_remote(options = {}) - attributes = extract_observer_attributes!(options) - attributes["data-periodical"] = true - attributes["data-frequency"] ||= 10 - - # periodically_call_remote does not need data-observe=true - attributes.delete('data-observe') - - script_decorator(attributes).html_safe! - end - - # Observes the field with the DOM ID specified by +field_id+ and calls a - # callback when its contents have changed. The default callback is an - # Ajax call. By default the value of the observed field is sent as a - # parameter with the Ajax call. - # - # Example: - # # Generates: - # # "" - # # - # <%= observe_field :suggest, :url => { :action => :find_suggestion }, - # :frequency => 0.25, - # :update => :suggest, - # :with => 'q' - # %> - # - # Required +options+ are either of: - # :url:: +url_for+-style options for the action to call - # when the field has changed. - # :function:: Instead of making a remote call to a URL, you - # can specify javascript code to be called instead. - # Note that the value of this option is used as the - # *body* of the javascript function, a function definition - # with parameters named element and value will be generated for you - # for example: - # observe_field("glass", :frequency => 1, :function => "alert('Element changed')") - # will generate: - # new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')}) - # The element parameter is the DOM element being observed, and the value is its value at the - # time the observer is triggered. - # - # Additional options are: - # :frequency:: The frequency (in seconds) at which changes to - # this field will be detected. Not setting this - # option at all or to a value equal to or less than - # zero will use event based observation instead of - # time based observation. - # :update:: Specifies the DOM ID of the element whose - # innerHTML should be updated with the - # XMLHttpRequest response text. - # :with:: A JavaScript expression specifying the parameters - # for the XMLHttpRequest. The default is to send the - # key and value of the observed field. Any custom - # expressions should return a valid URL query string. - # The value of the field is stored in the JavaScript - # variable +value+. - # - # Examples - # - # :with => "'my_custom_key=' + value" - # :with => "'person[name]=' + prompt('New name')" - # :with => "Form.Element.serialize('other-field')" - # - # Finally - # :with => 'name' - # is shorthand for - # :with => "'name=' + value" - # This essentially just changes the key of the parameter. - # - # Additionally, you may specify any of the options documented in the - # Common options section at the top of this document. - # - # Example: - # - # # Sends params: {:title => 'Title of the book'} when the book_title input - # # field is changed. - # observe_field 'book_title', - # :url => 'http://example.com/books/edit/1', - # :with => 'title' - # - # - def observe_field(name, options = {}) - html_options = options.delete(:callbacks) - - options[:observed] = name - attributes = extract_observer_attributes!(options) - attributes.merge!(html_options) if html_options - - script_decorator(attributes).html_safe! - end - - # Observes the form with the DOM ID specified by +form_id+ and calls a - # callback when its contents have changed. The default callback is an - # Ajax call. By default all fields of the observed field are sent as - # parameters with the Ajax call. - # - # The +options+ for +observe_form+ are the same as the options for - # +observe_field+. The JavaScript variable +value+ available to the - # :with option is set to the serialized form by default. - def observe_form(name, options = {}) - html_options = options.delete(:callbacks) - - options[:observed] = name - attributes = extract_observer_attributes!(options) - attributes.merge!(html_options) if html_options - - script_decorator(attributes).html_safe! - end - - def script_decorator(options) - attributes = %w(type="application/json") - attributes += options.map{|k, v| k + '="' + v.to_s + '"'} - "" - end - - private - - def extract_remote_attributes!(options) - attributes = options.delete(:html) || {} - - attributes.merge!(extract_update_attributes!(options)) - attributes.merge!(extract_request_attributes!(options)) - attributes["data-remote"] = true - - if submit = options.delete(:submit) - attributes["data-submit"] = submit - end - - attributes - end - - def extract_request_attributes!(options) - attributes = {} - if method = options.delete(:method) - attributes["data-method"] = method.to_s - end - - if type = options.delete(:type) - attributes["data-remote-type"] = type.to_s - end - - url_options = options.delete(:url) - url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash) - attributes["data-url"] = escape_javascript(url_for(url_options)) if url_options - - purge_unused_attributes!(attributes) - end - - def extract_update_attributes!(options) - attributes = {} - update = options.delete(:update) - if update.is_a?(Hash) - attributes["data-update-success"] = update[:success] - attributes["data-update-failure"] = update[:failure] - else - attributes["data-update-success"] = update - end - - if position = options.delete(:position) - attributes["data-update-position"] = position.to_s - end - - purge_unused_attributes!(attributes) - end - - def extract_observer_attributes!(options) - callback = options.delete(:function) - frequency = options.delete(:frequency) || 10 - - - attributes = extract_remote_attributes!(options) - attributes["data-observe"] = true - attributes["data-observed"] = options.delete(:observed) - attributes["data-onobserve"] = callback if callback - attributes["data-frequency"] = frequency if frequency && frequency.to_f != 0 - attributes.delete("data-remote") - - purge_unused_attributes!(attributes) - end - - def purge_unused_attributes!(attributes) - attributes.delete_if {|key, value| value.nil? } - attributes - end - end - - # TODO: All evaled goes here per wycat - module AjaxHelperCompat - include AjaxHelper - - def link_to_remote(name, options, html_options = {}) - set_callbacks(options, html_options) - set_with_and_condition_attributes(options, html_options) - super + html["data-update-position"] = options.delete(:position) + html["data-method"] = options.delete(:method) + html["data-remote"] = "true" + + html.merge!(options) + + url = url_for(url) if url.is_a?(Hash) + link_to(name, url, html) end def button_to_remote(name, options = {}, html_options = {}) - set_callbacks(options, html_options) - set_with_and_condition_attributes(options, html_options) - super - end - - def form_remote_tag(options, &block) - html = {} - set_callbacks(options, html) - set_with_and_condition_attributes(options, html) - options.merge!(:callbacks => html) - super - end - - def observe_field(name, options = {}) - html = {} - set_with_and_condition_attributes(options, html) - options.merge!(:callbacks => html) - super + url = options.delete(:url) + url = url_for(url) if url.is_a?(Hash) + + html_options.merge!(:type => "button", :value => name, + :"data-url" => url) + + tag(:input, html_options) end - def observe_form(name, options = {}) - html = {} - set_with_and_condition_attributes(options, html) - options.merge!(:callbacks => html) - super - end - - private + module Rails2Compatibility def set_callbacks(options, html) - [:before, :after, :uninitialized, :complete, :failure, :success, :interactive, :loaded, :loading].each do |type| - html["data-on#{type}"] = options.delete(type.to_sym) + [:complete, :failure, :success, :interactive, :loaded, :loading].each do |type| + html["data-#{type}-code"] = options.delete(type.to_sym) end options.each do |option, value| if option.is_a?(Integer) - html["data-on#{option}"] = options.delete(option) + html["data-#{option}-code"] = options.delete(option) end end end - - def set_with_and_condition_attributes(options, html) - if with = options.delete(:with) - html["data-with"] = with - end - - if condition = options.delete(:condition) - html["data-condition"] = condition + + def link_to_remote(name, url, options = nil) + if !options && url.is_a?(Hash) && url.key?(:url) + url, options = url.delete(:url), url end + + set_callbacks(options, options[:html] ||= {}) + + super end + + def button_to_remote(name, options = {}, html_options = {}) + set_callbacks(options, html_options) + super + end + end + end end -end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 1615f135b4..20e9916d62 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -262,8 +262,23 @@ module ActionView # FormTagHelper#form_tag. def form_for(record_or_name_or_array, *args, &proc) raise ArgumentError, "Missing block" unless block_given? + options = args.extract_options! - object_name = extract_object_name_for_form!(args, options, record_or_name_or_array) + + case record_or_name_or_array + when String, Symbol + object_name = record_or_name_or_array + when Array + object = record_or_name_or_array.last + object_name = ActionController::RecordIdentifier.singular_class_name(object) + apply_form_for_options!(record_or_name_or_array, options) + args.unshift object + else + object = record_or_name_or_array + object_name = ActionController::RecordIdentifier.singular_class_name(object) + apply_form_for_options!([object], options) + args.unshift object + end concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {})) fields_for(object_name, *(args << options), &proc) @@ -727,25 +742,6 @@ module ActionView def radio_button(object_name, method, tag_value, options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options) end - - private - def extract_object_name_for_form!(args, options, record_or_name_or_array) - case record_or_name_or_array - when String, Symbol - object_name = record_or_name_or_array - when Array - object = record_or_name_or_array.last - object_name = ActionController::RecordIdentifier.singular_class_name(object) - apply_form_for_options!(record_or_name_or_array, options) - args.unshift object - else - object = record_or_name_or_array - object_name = ActionController::RecordIdentifier.singular_class_name(object) - apply_form_for_options!([object], options) - args.unshift object - end - object_name - end end module InstanceTagMethods #:nodoc: diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index ebce5c1513..048bedc7ba 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -352,24 +352,33 @@ module ActionView # # => # # submit_tag "Complete sale", :disable_with => "Please wait..." - # # => # # submit_tag nil, :class => "form_submit" # # => # # submit_tag "Edit", :disable_with => "Editing...", :class => "edit-button" - # # => def submit_tag(value = "Save changes", options = {}) options.stringify_keys! if disable_with = options.delete("disable_with") - add_disable_with_to_attributes!(options, disable_with) + disable_with = "this.value='#{disable_with}'" + disable_with << ";#{options.delete('onclick')}" if options['onclick'] + + options["onclick"] = "if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }" + options["onclick"] << "else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';" + options["onclick"] << "hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }" + options["onclick"] << "this.setAttribute('originalValue', this.value);this.disabled = true;#{disable_with};" + options["onclick"] << "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());" + options["onclick"] << "if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" end if confirm = options.delete("confirm") - add_confirm_to_attributes!(options, confirm) + options["onclick"] ||= 'return true;' + options["onclick"] = "if (!#{confirm_javascript_function(confirm)}) return false; #{options['onclick']}" end tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) @@ -402,7 +411,8 @@ module ActionView options.stringify_keys! if confirm = options.delete("confirm") - add_confirm_to_attributes!(options, confirm) + options["onclick"] ||= '' + options["onclick"] += "return #{confirm_javascript_function(confirm)};" end tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 02d0a88189..06a9d3405a 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -39,7 +39,7 @@ module ActionView JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts') end - include AjaxHelperCompat + include PrototypeHelper # Returns a link of the given +name+ that will trigger a JavaScript +function+ using the # onclick handler and return false after the fact. @@ -187,57 +187,22 @@ module ActionView "\n//#{cdata_section("\n#{content}\n//")}\n" end - protected - def convert_options_to_javascript!(html_options, url = '') - confirm = html_options.delete("confirm") - - if html_options.key?("popup") - ActiveSupport::Deprecation.warn(":popup has been deprecated", caller) + protected + def options_for_javascript(options) + if options.empty? + '{}' + else + "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}" + end end - method, href = html_options.delete("method"), html_options['href'] - - if confirm && method - add_confirm_to_attributes!(html_options, confirm) - add_method_to_attributes!(html_options, method, url) - elsif confirm - add_confirm_to_attributes!(html_options, confirm) - elsif method - add_method_to_attributes!(html_options, method, url) + def array_or_string_for_javascript(option) + if option.kind_of?(Array) + "['#{option.join('\',\'')}']" + elsif !option.nil? + "'#{option}'" + end end - end - - def add_confirm_to_attributes!(html_options, confirm) - html_options["data-confirm"] = confirm if confirm - end - - def add_method_to_attributes!(html_options, method, url = nil) - html_options["rel"] = "nofollow" if method.to_s.downcase == "delete" - html_options["data-method"] = method - if url.size > 0 - html_options["data-url"] = url - end - end - - def add_disable_with_to_attributes!(html_options, disable_with) - html_options["data-disable-with"] = disable_with if disable_with - end - - def options_for_javascript(options) - if options.empty? - '{}' - else - "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}" - end - end - - def array_or_string_for_javascript(option) - if option.kind_of?(Array) - "['#{option.join('\',\'')}']" - elsif !option.nil? - "'#{option}'" - end - end end end end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index d861810f19..bef93dd0f8 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -1,7 +1,6 @@ require 'set' require 'active_support/json' require 'active_support/core_ext/object/returning' -require 'action_view/helpers/scriptaculous_helper' module ActionView module Helpers @@ -28,6 +27,40 @@ module ActionView # ActionView::Helpers::JavaScriptHelper for more information on including # this and other JavaScript files in your Rails templates.) # + # Now you're ready to call a remote action either through a link... + # + # link_to_remote "Add to cart", + # :url => { :action => "add", :id => product.id }, + # :update => { :success => "cart", :failure => "error" } + # + # ...through a form... + # + # <% form_remote_tag :url => '/shipping' do -%> + #
+ # link_to_remote(image_tag("refresh"), :update => "emails",
+ # :url => { :action => "list_emails" })
+ #
+ # You can override the generated HTML options by specifying a hash in
+ # options[:html].
+ #
+ # link_to_remote "Delete this post", :update => "posts",
+ # :url => post_url(@post), :method => :delete,
+ # :html => { :class => "destructive" }
+ #
+ # You can also specify a hash for options[:update] to allow for
+ # easy redirection of output to an other DOM element if a server-side
+ # error occurs:
+ #
+ # Example:
+ # # Generates: Delete this post
+ # link_to_remote "Delete this post",
+ # :url => { :action => "destroy", :id => post.id },
+ # :update => { :success => "posts", :failure => "error" }
+ #
+ # Optionally, you can use the options[:position] parameter to
+ # influence how the target DOM element is updated. It must be one of
+ # :before, :top, :bottom, or :after.
+ #
+ # The method used is by default POST. You can also specify GET or you
+ # can simulate PUT or DELETE over POST. All specified with options[:method]
+ #
+ # Example:
+ # # Generates: Destroy
+ # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
+ #
+ # By default, these remote requests are processed asynchronous during
+ # which various JavaScript callbacks can be triggered (for progress
+ # indicators and the likes). All callbacks get access to the
+ # request object, which holds the underlying XMLHttpRequest.
+ #
+ # To access the server response, use request.responseText, to
+ # find out the HTTP status, use request.status.
+ #
+ # Example:
+ # # Generates: hello
+ # word = 'hello'
+ # link_to_remote word,
+ # :url => { :action => "undo", :n => word_counter },
+ # :complete => "undoRequestCompleted(request)"
+ #
+ # The callbacks that may be specified are (in order):
+ #
+ # :loading:: Called when the remote document is being
+ # loaded with data by the browser.
+ # :loaded:: Called when the browser has finished loading
+ # the remote document.
+ # :interactive:: Called when the user can interact with the
+ # remote document, even though it has not
+ # finished loading.
+ # :success:: Called when the XMLHttpRequest is completed,
+ # and the HTTP status code is in the 2XX range.
+ # :failure:: Called when the XMLHttpRequest is completed,
+ # and the HTTP status code is not in the 2XX
+ # range.
+ # :complete:: Called when the XMLHttpRequest is complete
+ # (fires after success/failure if they are
+ # present).
+ #
+ # You can further refine :success and :failure by
+ # adding additional callbacks for specific status codes.
+ #
+ # Example:
+ # # Generates: hello
+ # link_to_remote word,
+ # :url => { :action => "action" },
+ # 404 => "alert('Not found...? Wrong URL...?')",
+ # :failure => "alert('HTTP Error ' + request.status + '!')"
+ #
+ # A status code callback overrides the success/failure handlers if
+ # present.
+ #
+ # If you for some reason or another need synchronous processing (that'll
+ # block the browser while the request is happening), you can specify
+ # options[:type] = :synchronous.
+ #
+ # You can customize further browser side call logic by passing in
+ # JavaScript code snippets via some optional parameters. In their order
+ # of use these are:
+ #
+ # :confirm:: Adds confirmation dialog.
+ # :condition:: Perform remote request conditionally
+ # by this expression. Use this to
+ # describe browser-side conditions when
+ # request should not be initiated.
+ # :before:: Called before request is initiated.
+ # :after:: Called immediately after request was
+ # initiated and before :loading.
+ # :submit:: Specifies the DOM element ID that's used
+ # as the parent of the form elements. By
+ # default this is the current form, but
+ # it could just as well be the ID of a
+ # table row or any other DOM element.
+ # :with:: A JavaScript expression specifying
+ # the parameters for the XMLHttpRequest.
+ # Any expressions should return a valid
+ # URL query string.
+ #
+ # Example:
+ #
+ # :with => "'name=' + $('name').value"
+ #
+ # You can generate a link that uses AJAX in the general case, while
+ # degrading gracefully to plain link behavior in the absence of
+ # JavaScript by setting html_options[:href] to an alternate URL.
+ # Note the extra curly braces around the options hash separate
+ # it as the second parameter from html_options, the third.
+ #
+ # Example:
+ # link_to_remote "Delete this post",
+ # { :update => "posts", :url => { :action => "destroy", :id => post.id } },
+ # :href => url_for(:action => "destroy", :id => post.id)
+ def link_to_remote(name, options = {}, html_options = nil)
+ link_to_function(name, remote_function(options), html_options || options.delete(:html))
+ end
+
+ # Creates a button with an onclick event which calls a remote action
+ # via XMLHttpRequest
+ # The options for specifying the target with :url
+ # and defining callbacks is the same as link_to_remote.
+ def button_to_remote(name, options = {}, html_options = {})
+ button_to_function(name, remote_function(options), html_options)
+ end
+
+ # Periodically calls the specified url (options[:url]) every
+ # options[:frequency] seconds (default is 10). Usually used to
+ # update a specified div (options[:update]) with the results
+ # of the remote call. The options for specifying the target with :url
+ # and defining callbacks is the same as link_to_remote.
+ # Examples:
+ # # Call get_averages and put its results in 'avg' every 10 seconds
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
+ # # {asynchronous:true, evalScripts:true})}, 10)
+ # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
+ #
+ # # Call invoice every 10 seconds with the id of the customer
+ # # If it succeeds, update the invoice DIV; if it fails, update the error DIV
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
+ # # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
+ # periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
+ # :update => { :success => "invoice", :failure => "error" }
+ #
+ # # Call update every 20 seconds and update the new_block DIV
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20)
+ # periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block')
+ #
+ def periodically_call_remote(options = {})
+ frequency = options[:frequency] || 10 # every ten seconds by default
+ code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
+ javascript_tag(code)
+ end
+
+ # Returns a form tag that will submit using XMLHttpRequest in the
+ # background instead of the regular reloading POST arrangement. Even
+ # though it's using JavaScript to serialize the form elements, the form
+ # submission will work just like a regular submission as viewed by the
+ # receiving side (all elements available in params). The options for
+ # specifying the target with :url and defining callbacks is the same as
+ # +link_to_remote+.
+ #
+ # A "fall-through" target for browsers that doesn't do JavaScript can be
+ # specified with the :action/:method options on :html.
+ #
+ # Example:
+ # # Generates:
+ # #
+ # <% form_remote_tag :url => '/posts' do -%>
+ # =0))k||l.push(u);else if(k)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&& -"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,l,q,p){h=g[1].replace(/\\/g,"");if(!p&&m.attrMap[h])g[1]=m.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,l,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=o(g[3],null,null,h);else{g=o.filter(g[3],h,k,true^q);k||l.push.apply(l,g);return false}else if(m.match.POS.test(g[0])||m.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true); -return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!o(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"=== -g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,h){return h===0},last:function(g,h,k,l){return h===l.length-1},even:function(g,h){return h%2=== -0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return hk[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,l){var q=h[1],p=m.filters[q];if(p)return p(g,k,h,l);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=h[3];k=0;for(l=h.length;k = -0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=m.attrHandle[k]?m.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var l=h[2];h=h[4];return g==null?l==="!=":l==="="?k===h:l==="*="?k.indexOf(h)>=0:l==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:l==="!="?k!==h:l==="^="? -k.indexOf(h)===0:l==="$="?k.substr(k.length-h.length)===h:l==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,l){var q=m.setFilters[h[2]];if(q)return q(g,k,h,l)}}},s=m.match.POS;for(var x in m.match){m.match[x]=new RegExp(m.match[x].source+/(?![^\[]*\])(?![^\(]*\))/.source);m.leftMatch[x]=new RegExp(/(^(?:.|\r|\n)*?)/.source+m.match[x].source.replace(/\\(\d+)/g,function(g,h){return"\\"+(h-0+1)}))}var A=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g}; -try{Array.prototype.slice.call(r.documentElement.childNodes,0)}catch(B){A=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,l=g.length;k ";var k=r.documentElement;k.insertBefore(g,k.firstChild);if(r.getElementById(h)){m.find.ID=function(l,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(l[1]))?q.id===l[1]||typeof q.getAttributeNode!=="undefined"&&q.getAttributeNode("id").nodeValue===l[1]?[q]:v:[]};m.filter.ID=function(l,q){var p=typeof l.getAttributeNode!=="undefined"&&l.getAttributeNode("id"); -return l.nodeType===1&&p&&p.nodeValue===q}}k.removeChild(g);k=g=null})();(function(){var g=r.createElement("div");g.appendChild(r.createComment(""));if(g.getElementsByTagName("*").length>0)m.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var l=0;k[l];l++)k[l].nodeType===1&&h.push(k[l]);k=h}return k};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")m.attrHandle.href=function(h){return h.getAttribute("href", -2)};g=null})();r.querySelectorAll&&function(){var g=o,h=r.createElement("div");h.innerHTML="";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){o=function(l,q,p,u){q=q||r;if(!u&&q.nodeType===9&&!w(q))try{return A(q.querySelectorAll(l),p)}catch(t){}return g(l,q,p,u)};for(var k in g)o[k]=g[k];h=null}}();(function(){var g=r.createElement("div");g.innerHTML="";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length=== -0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){m.order.splice(1,0,"CLASS");m.find.CLASS=function(h,k,l){if(typeof k.getElementsByClassName!=="undefined"&&!l)return k.getElementsByClassName(h[1])};g=null}}})();var E=r.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g,h){return g!==h&&(g.contains?g.contains(h):true)},w=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},fa=function(g,h){var k=[], -l="",q;for(h=h.nodeType?[h]:h;q=m.match.PSEUDO.exec(g);){l+=q[0];g=g.replace(m.match.PSEUDO,"")}g=m.relative[g]?g+"*":g;q=0;for(var p=h.length;q =0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f
0)for(var i=d;i 0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i={},j;if(f&&a.length){e=0;for(var n=a.length;e --1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var o=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(m,s){for(;s&&s.ownerDocument&&s!==b;){if(o?o.index(s)>-1:c(s).is(a))return s;s=s.parentNode}return null})},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(), -a);return this.pushStack(pa(a[0])||pa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")}, -nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);bb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e): -e;if((this.length>1||db.test(f))&&cb.test(a))e=e.reverse();return this.pushStack(e,a,Q.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===v||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!== -b&&d.push(a);return d}});var Fa=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ga=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/"+d+">"},F={option:[1,""],legend:[1,""],thead:[1," ","
"],tr:[2,"","
"],td:[3,""], -col:[2,"
"," "],area:[1,""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
"," ",""];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==v)return this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(a));return c.getText(this)}, -wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length? -d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments, -false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&& -!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Fa,"").replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){qa(this,b);qa(this.find("*"),b.find("*"))}return b},html:function(a){if(a===v)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Fa,""):null;else if(typeof a==="string"&&!/