diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2011-04-17 17:08:49 +0100 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2011-04-17 17:08:49 +0100 |
commit | 1c2b2233c3a7ec76c0a0eddf5b8be45c489be133 (patch) | |
tree | 56f2b767c3a4f1f14c51606bf2cbb714a98c5f89 /actionpack/lib/action_view | |
parent | 8d558cb1b069410c8f693295c9c4e2ffc9661e06 (diff) | |
parent | b6843f22ac42b503f6b8ac00105ca0679049be7d (diff) | |
download | rails-1c2b2233c3a7ec76c0a0eddf5b8be45c489be133.tar.gz rails-1c2b2233c3a7ec76c0a0eddf5b8be45c489be133.tar.bz2 rails-1c2b2233c3a7ec76c0a0eddf5b8be45c489be133.zip |
Merge branch 'master' of https://github.com/rails/rails into performance_test
Diffstat (limited to 'actionpack/lib/action_view')
30 files changed, 539 insertions, 1459 deletions
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index ab8c6259c5..9e8a3c51a3 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -8,13 +8,12 @@ require 'action_view/log_subscriber' module ActionView #:nodoc: # = Action View Base # - # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb + # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. - # If the template file has a <tt>.rjs</tt> extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator. # - # == ERb + # == ERB # - # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the # following loop for names: # # <b>Names of all the people</b> @@ -23,7 +22,7 @@ module ActionView #:nodoc: # <% end %> # # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this - # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong: + # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong: # # <%# WRONG %> # Hi, Mr. <% puts "Frodo" %> @@ -81,7 +80,7 @@ module ActionView #:nodoc: # # == Builder # - # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An XmlMarkup object + # Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension. # # Here are some basic examples: @@ -131,37 +130,9 @@ module ActionView #:nodoc: # end # # More builder documentation can be found at http://builder.rubyforge.org. - # - # == JavaScriptGenerator - # - # JavaScriptGenerator templates end in <tt>.rjs</tt>. Unlike conventional templates which are used to - # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to - # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax - # and make updates to the page where the request originated from. - # - # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block. - # - # When an <tt>.rjs</tt> action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example: - # - # link_to_remote :url => {:action => 'delete'} - # - # The subsequently rendered <tt>delete.rjs</tt> might look like: - # - # page.replace_html 'sidebar', :partial => 'sidebar' - # page.remove "person-#{@person.id}" - # page.visual_effect :highlight, 'user-list' - # - # This refreshes the sidebar, removes a person element and highlights the user list. - # - # See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods documentation for more details. class Base include Helpers, Rendering, Partials, ::ERB::Util, Context - # Specify whether RJS responses should be wrapped in a try/catch block - # that alert()s the caught exception (and then re-raises it). - cattr_accessor :debug_rjs - @@debug_rjs = false - # Specify the proc used to decorate input tags that refer to attributes with errors. cattr_accessor :field_error_proc @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } @@ -182,7 +153,7 @@ module ActionView #:nodoc: end end - attr_accessor :_template + attr_accessor :_template, :_view_flow attr_internal :request, :controller, :config, :assigns, :lookup_context delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context @@ -210,8 +181,8 @@ module ActionView #:nodoc: self.helpers = Module.new unless self.class.helpers @_config = {} - @_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } @_virtual_path = nil + @_view_flow = OutputFlow.new @output_buffer = nil if @_controller = controller @@ -224,10 +195,6 @@ module ActionView #:nodoc: @_lookup_context.formats = formats if formats end - def store_content_for(key, value) - @_content_for[key] = value - end - def controller_path @controller_path ||= controller && controller.controller_path end diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb new file mode 100644 index 0000000000..089fc68706 --- /dev/null +++ b/actionpack/lib/action_view/buffers.rb @@ -0,0 +1,43 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: + def initialize(*) + super + encode! if encoding_aware? + end + + def <<(value) + super(value.to_s) + end + alias :append= :<< + alias :safe_append= :safe_concat + end + + class StreamingBuffer #:nodoc: + def initialize(block) + @block = block + end + + def <<(value) + value = value.to_s + value = ERB::Util.h(value) unless value.html_safe? + @block.call(value) + end + alias :concat :<< + alias :append= :<< + + def safe_concat(value) + @block.call(value.to_s) + end + alias :safe_append= :safe_concat + + def html_safe? + true + end + + def html_safe + self + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb new file mode 100644 index 0000000000..386a06511f --- /dev/null +++ b/actionpack/lib/action_view/flows.rb @@ -0,0 +1,79 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputFlow #:nodoc: + attr_reader :content + + def initialize + @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } + end + + # Called by _layout_for to read stored values. + def get(key) + @content[key] + end + + # Called by each renderer object to set the layout contents. + def set(key, value) + @content[key] = value + end + + # Called by content_for + def append(key, value) + @content[key] << value + end + + # Called by provide + def append!(key, value) + @content[key] << value + end + end + + class StreamingFlow < OutputFlow #:nodoc: + def initialize(view, fiber) + @view = view + @parent = nil + @child = view.output_buffer + @content = view._view_flow.content + @fiber = fiber + @root = Fiber.current.object_id + end + + # Try to get an stored content. If the content + # is not available and we are inside the layout + # fiber, we set that we are waiting for the given + # key and yield. + def get(key) + return super if @content.key?(key) + + if inside_fiber? + view = @view + + begin + @waiting_for = key + view.output_buffer, @parent = @child, view.output_buffer + Fiber.yield + ensure + @waiting_for = nil + view.output_buffer, @child = @parent, view.output_buffer + end + end + + super + end + + # Appends the contents for the given key. This is called + # by provides and resumes back to the fiber if it is + # the key it is waiting for. + def append!(key, value) + super + @fiber.resume if @waiting_for == key + end + + private + + def inside_fiber? + Fiber.current.object_id != @root + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index d338ce616a..205116f610 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -17,11 +17,10 @@ module ActionView #:nodoc: autoload :FormTagHelper autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper - autoload :PrototypeHelper autoload :OutputSafetyHelper autoload :RecordTagHelper autoload :SanitizeHelper - autoload :ScriptaculousHelper + autoload :SprocketsHelper autoload :TagHelper autoload :TextHelper autoload :TranslationHelper @@ -47,11 +46,10 @@ module ActionView #:nodoc: include FormTagHelper include JavaScriptHelper include NumberHelper - include PrototypeHelper include OutputSafetyHelper include RecordTagHelper include SanitizeHelper - include ScriptaculousHelper + include SprocketsHelper include TagHelper include TextHelper include TranslationHelper diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb index 014a03c54d..1e00fd996b 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -30,9 +30,6 @@ module ActionView source = rewrite_extension(source, dir, ext) if ext source = "/#{dir}/#{source}" unless source[0] == ?/ - if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"] - source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"]) - end source = rewrite_asset_path(source, config.asset_path) has_request = controller.respond_to?(:request) diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb index 82bbfcc7d2..ce5a7dc2e5 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -86,99 +86,119 @@ module ActionView # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js def javascript_path(source) - asset_paths.compute_public_path(source, 'javascripts', 'js') + if config.use_sprockets + sprockets_javascript_path(source) + else + asset_paths.compute_public_path(source, 'javascripts', 'js') + end end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - # Returns an HTML script tag for each of the +sources+ provided. You - # can pass in the filename (.js extension is optional) of JavaScript files - # that exist in your <tt>public/javascripts</tt> directory for inclusion into the - # current page or you can pass the full path relative to your document - # root. To include the Prototype and Scriptaculous JavaScript libraries in - # your application, pass <tt>:defaults</tt> as the source. When using - # <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in - # <tt>public/javascripts</tt> it will be included as well. You can modify the - # HTML attributes of the script tag by passing a hash as the last argument. + # Returns an HTML script tag for each of the +sources+ provided. + # + # Sources may be paths to JavaScript files. Relative paths are assumed to be relative + # to <tt>public/javascripts</tt>, full paths are assumed to be relative to the document + # root. Relative paths are idiomatic, use absolute paths only when needed. + # + # When passing paths, the ".js" extension is optional. + # + # To include the default JavaScript expansion pass <tt>:defaults</tt> as source. + # By default, <tt>:defaults</tt> loads jQuery. If the application was generated + # with "-j prototype" the libraries Prototype and Scriptaculous are loaded instead. + # In any case, the defaults can be overridden in <tt>config/application.rb</tt>: + # + # config.action_view.javascript_expansions[:defaults] = %w(foo.js bar.js) + # + # When using <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in + # <tt>public/javascripts</tt> it will be included as well at the end. + # + # You can modify the HTML attributes of the script tag by passing a hash as the + # last argument. # # ==== Examples - # javascript_include_tag "xmlhr" # => - # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> + # javascript_include_tag "xmlhr" + # # => <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> # - # javascript_include_tag "xmlhr.js" # => - # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> + # javascript_include_tag "xmlhr.js" + # # => <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> # - # javascript_include_tag "common.javascript", "/elsewhere/cools" # => - # <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script> - # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> + # javascript_include_tag "common.javascript", "/elsewhere/cools" + # # => <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script> + # # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> # - # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => - # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # javascript_include_tag "http://www.railsapplication.com/xmlhr" + # # => <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> # - # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => - # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" + # # => <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> # - # javascript_include_tag :defaults # => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # javascript_include_tag :defaults + # # => <script type="text/javascript" src="/javascripts/jquery.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/rails.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> # # * = The application.js file is only referenced if it exists # - # You can also include all javascripts in the +javascripts+ directory using <tt>:all</tt> as the source: + # You can also include all JavaScripts in the +javascripts+ directory using <tt>:all</tt> as the source: # - # javascript_include_tag :all # => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> + # javascript_include_tag :all + # # => <script type="text/javascript" src="/javascripts/jquery.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/rails.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> # - # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to - # all subsequently included files. + # Note that your defaults of choice will be included first, so they will be available to all subsequently + # included files. # - # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>: + # If you want Rails to search in all the subdirectories under <tt>public/javascripts</tt>, you should + # explicitly set <tt>:recursive</tt>: # # javascript_include_tag :all, :recursive => true # - # == Caching multiple javascripts into one + # == Caching multiple JavaScripts into one # - # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development - # environment). + # You can also cache multiple JavaScripts into one file, which requires less HTTP connections to download + # and can better be compressed by gzip (leading to faster transfers). Caching will only happen if + # <tt>config.perform_caching</tt> is set to true (which is the case by default for the Rails + # production environment, but not for the development environment). # # ==== Examples - # javascript_include_tag :all, :cache => true # when config.perform_caching is false => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> # - # javascript_include_tag :all, :cache => true # when config.perform_caching is true => - # <script type="text/javascript" src="/javascripts/all.js?1344139789"></script> + # # assuming config.perform_caching is false + # javascript_include_tag :all, :cache => true + # # => <script type="text/javascript" src="/javascripts/jquery.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/rails.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> + # + # # assuming config.perform_caching is true + # javascript_include_tag :all, :cache => true + # # => <script type="text/javascript" src="/javascripts/all.js?1344139789"></script> # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/cart.js?1289139157"></script> - # <script type="text/javascript" src="/javascripts/checkout.js?1299139816"></script> + # # assuming config.perform_caching is false + # javascript_include_tag "jquery", "cart", "checkout", :cache => "shop" + # # => <script type="text/javascript" src="/javascripts/jquery.js?1284139606"></script> + # # <script type="text/javascript" src="/javascripts/cart.js?1289139157"></script> + # # <script type="text/javascript" src="/javascripts/checkout.js?1299139816"></script> # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => - # <script type="text/javascript" src="/javascripts/shop.js?1299139816"></script> + # # assuming config.perform_caching is true + # javascript_include_tag "jquery", "cart", "checkout", :cache => "shop" + # # => <script type="text/javascript" src="/javascripts/shop.js?1299139816"></script> # # The <tt>:recursive</tt> option is also available for caching: # # javascript_include_tag :all, :cache => true, :recursive => true def javascript_include_tag(*sources) - @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) - @javascript_include.include_tag(*sources) + if config.use_sprockets + sprockets_javascript_include_tag(*sources) + else + @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) + @javascript_include.include_tag(*sources) + end end - end - end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index a48c87b49a..a994afb65e 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -63,7 +63,11 @@ module ActionView # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css def stylesheet_path(source) - asset_paths.compute_public_path(source, 'stylesheets', 'css') + if config.use_sprockets + sprockets_stylesheet_path(source) + else + asset_paths.compute_public_path(source, 'stylesheets', 'css') + end end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route @@ -136,8 +140,12 @@ module ActionView # stylesheet_link_tag :all, :concat => true # def stylesheet_link_tag(*sources) - @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) - @stylesheet_include.include_tag(*sources) + if config.use_sprockets + sprockets_stylesheet_link_tag(*sources) + else + @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) + @stylesheet_include.include_tag(*sources) + end end end diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index db9d7a08ff..96e5722252 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -4,7 +4,7 @@ module ActionView # = Action View Atom Feed Helpers module Helpers #:nodoc: module AtomFeedHelper - # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other + # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other # template languages). # # Full usage example: diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index c88bd1efd5..0139714240 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -14,7 +14,7 @@ module ActionView # variable. You can then use this variable anywhere in your templates or layout. # # ==== Examples - # The capture method can be used in ERb templates... + # The capture method can be used in ERB templates... # # <% @greeting = capture do %> # Welcome to my shiny new web page! The date and time is @@ -107,8 +107,8 @@ module ActionView # <%= javascript_include_tag :defaults %> # <% end %> # - # That will place <tt>script</tt> tags for Prototype, Scriptaculous, and application.js (if it exists) - # on the page; this technique is useful if you'll only be using these scripts in a few views. + # That will place +script+ tags for your default set of JavaScript files on the page; + # this technique is useful if you'll only be using these scripts in a few views. # # Note that content_for concatenates the blocks it is given for a particular # identifier in order. For example: @@ -135,8 +135,19 @@ module ActionView # for elements that will be fragment cached. def content_for(name, content = nil, &block) content = capture(&block) if block_given? - @_content_for[name] << content if content - @_content_for[name] unless content + result = @_view_flow.append(name, content) if content + result unless content + end + + # The same as +content_for+ but when used with streaming flushes + # straight back to the layout. In other words, if you want to + # concatenate several times to the same buffer when rendering a given + # template, you should use +content_for+, if not, use +provide+ to tell + # the layout to stop looking for more contents. + def provide(name, content = nil, &block) + content = capture(&block) if block_given? + result = @_view_flow.append!(name, content) if content + result unless content end # content_for? simply checks whether any content has been captured yet using content_for @@ -158,7 +169,7 @@ module ActionView # </body> # </html> def content_for?(name) - @_content_for[name].present? + @_view_flow.get(name).present? end # Use an alternate output buffer for the duration of the block. diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb index 65c8debc76..1f2bc28cac 100644 --- a/actionpack/lib/action_view/helpers/csrf_helper.rb +++ b/actionpack/lib/action_view/helpers/csrf_helper.rb @@ -17,10 +17,12 @@ module ActionView # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted, # so they do not use these tags. def csrf_meta_tags - <<-METAS.strip_heredoc.chomp.html_safe if protect_against_forgery? - <meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/> - <meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/> - METAS + if protect_against_forgery? + [ + tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token), + tag('meta', :name => 'csrf-token', :content => form_authenticity_token) + ].join("\n").html_safe + end end # For backwards compatibility. diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 6cd1565031..313a2591bf 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -26,9 +26,9 @@ module ActionView # 30 secs <-> 1 min, 29 secs # => 1 minute # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour - # 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours - # 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day - # 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days + # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours + # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day + # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months # 1 yr <-> 1 yr, 3 months # => about 1 year @@ -89,8 +89,8 @@ module ActionView when 2..44 then locale.t :x_minutes, :count => distance_in_minutes when 45..89 then locale.t :about_x_hours, :count => 1 when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round - when 1440..2529 then locale.t :x_days, :count => 1 - when 2530..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round + when 1440..2519 then locale.t :x_days, :count => 1 + when 2520..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round when 43200..86399 then locale.t :about_x_months, :count => 1 when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round else diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 9025d9e24c..440acafa88 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -610,10 +610,11 @@ module ActionView # label(:post, :body) # # => <label for="post_body">Write your entire text here</label> # - # Localization can also be based purely on the translation of the attribute-name like this: + # Localization can also be based purely on the translation of the attribute-name + # (if you are using ActiveRecord): # - # activemodel: - # attribute: + # activerecord: + # attributes: # post: # cost: "Total cost" # diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index a19ba7a968..d7228bab67 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -1,42 +1,8 @@ require 'action_view/helpers/tag_helper' module ActionView - # = Action View JavaScript Helpers module Helpers - # Provides functionality for working with JavaScript in your views. - # - # == Ajax, controls and visual effects - # - # * For information on using Ajax, see - # ActionView::Helpers::PrototypeHelper. - # * For information on using controls and visual effects, see - # ActionView::Helpers::ScriptaculousHelper. - # - # == Including the JavaScript libraries into your pages - # - # Rails includes the Prototype JavaScript framework and the Scriptaculous - # JavaScript controls and visual effects library. If you wish to use - # these libraries and their helpers (ActionView::Helpers::PrototypeHelper - # and ActionView::Helpers::ScriptaculousHelper), you must do one of the - # following: - # - # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD - # section of your page (recommended): This function will return - # references to the JavaScript files created by the +rails+ command in - # your <tt>public/javascripts</tt> directory. Using it is recommended as - # the browser can then cache the libraries instead of fetching all the - # functions anew on every request. - # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but - # will only include the Prototype core library, which means you are able - # to use all basic AJAX functionality. For the Scriptaculous-based - # JavaScript helpers, like visual effects, autocompletion, drag and drop - # and so on, you should use the method described above. - # - # For documentation on +javascript_include_tag+ see - # ActionView::Helpers::AssetTagHelper. module JavaScriptHelper - include PrototypeHelper - JS_ESCAPE_MAP = { '\\' => '\\\\', '</' => '<\/', @@ -96,87 +62,34 @@ module ActionView "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe end - # Returns a button with the given +name+ text that'll trigger a JavaScript +function+ using the - # onclick handler. - # - # The first argument +name+ is used as the button's value or display text. - # - # The next arguments are optional and may include the javascript function definition and a hash of html_options. + # Returns a button whose +onclick+ handler triggers the passed JavaScript. # - # The +function+ argument can be omitted in favor of an +update_page+ - # block, which evaluates to a string when the template is rendered - # (instead of making an Ajax request first). + # The helper receives a name, JavaScript code, and an optional hash of HTML options. The + # name is used as button label and the JavaScript code goes into its +onclick+ attribute. + # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. # - # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" + # button_to_function "Greeting", "alert('Hello world!')", :class => "ok" + # # => <input class="ok" onclick="alert('Hello world!');" type="button" value="Greeting" /> # - # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil - # - # Examples: - # button_to_function "Greeting", "alert('Hello world!')" - # button_to_function "Delete", "if (confirm('Really?')) do_delete()" - # button_to_function "Details" do |page| - # page[:details].visual_effect :toggle_slide - # end - # button_to_function "Details", :class => "details_button" do |page| - # page[:details].visual_effect :toggle_slide - # end - def button_to_function(name, *args, &block) - html_options = args.extract_options!.symbolize_keys - - function = block_given? ? update_page(&block) : args[0] || '' + def button_to_function(name, function=nil, html_options={}) onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};" tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick)) end - # Returns a link of the given +name+ that will trigger a JavaScript +function+ using the - # onclick handler and return false after the fact. - # - # The first argument +name+ is used as the link text. + # Returns a link whose +onclick+ handler triggers the passed JavaScript. # - # The next arguments are optional and may include the javascript function definition and a hash of html_options. + # The helper receives a name, JavaScript code, and an optional hash of HTML options. The + # name is used as the link text and the JavaScript code goes into the +onclick+ attribute. + # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. Once all + # the JavaScript is set, the helper appends "; return false;". # - # The +function+ argument can be omitted in favor of an +update_page+ - # block, which evaluates to a string when the template is rendered - # (instead of making an Ajax request first). + # The +href+ attribute of the tag is set to "#" unles +html_options+ has one. # - # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" + # link_to_function "Greeting", "alert('Hello world!')", :class => "nav_link" + # # => <a class="nav_link" href="#" onclick="alert('Hello world!'); return false;">Greeting</a> # - # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil - # - # - # Examples: - # link_to_function "Greeting", "alert('Hello world!')" - # Produces: - # <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> - # - # link_to_function(image_tag("delete"), "if (confirm('Really?')) do_delete()") - # Produces: - # <a onclick="if (confirm('Really?')) do_delete(); return false;" href="#"> - # <img src="/images/delete.png?" alt="Delete"/> - # </a> - # - # link_to_function("Show me more", nil, :id => "more_link") do |page| - # page[:details].visual_effect :toggle_blind - # page[:more_link].replace_html "Show me less" - # end - # Produces: - # <a href="#" id="more_link" onclick="try { - # $("details").visualEffect("toggle_blind"); - # $("more_link").update("Show me less"); - # } - # catch (e) { - # alert('RJS error:\n\n' + e.toString()); - # alert('$(\"details\").visualEffect(\"toggle_blind\"); - # \n$(\"more_link\").update(\"Show me less\");'); - # throw e - # }; - # return false;">Show me more</a> - # - def link_to_function(name, *args, &block) - html_options = args.extract_options!.symbolize_keys - - function = block_given? ? update_page(&block) : args[0] || '' + def link_to_function(name, function, html_options={}) onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;" href = html_options[:href] || '#' diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb deleted file mode 100644 index 18e303778c..0000000000 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ /dev/null @@ -1,852 +0,0 @@ -require 'set' -require 'active_support/json' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/string/output_safety' - -module ActionView - # = Action View Prototype Helpers - module Helpers - # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides - # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation, - # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php] - # functionality, and more traditional object-oriented facilities for JavaScript. - # This module provides a set of helpers to make it more convenient to call - # functions from Prototype using Rails, including functionality to call remote - # Rails methods (that is, making a background request to a Rails action) using Ajax. - # This means that you can call actions in your controllers without - # reloading the page, but still update certain parts of it using - # injections into the DOM. A common use case is having a form that adds - # a new element to a list without reloading the page or updating a shopping - # cart total when a new item is added. - # - # == Usage - # To be able to use these helpers, you must first include the Prototype - # JavaScript framework in your pages. - # - # javascript_include_tag 'prototype' - # - # (See the documentation for - # 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 -%> - # <div><%= submit_tag 'Recalculate Shipping' %></div> - # <% end -%> - # - # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than - # are listed here); check out the documentation for each method to find out more about its usage and options. - # - # === Common Options - # See link_to_remote for documentation of options common to all Ajax - # helpers; any of the options specified by link_to_remote can be used - # by the other helpers. - # - # == Designing your Rails actions for Ajax - # When building your action handlers (that is, the Rails actions that receive your background requests), it's - # important to remember a few things. First, whatever your action would normally return to the browser, it will - # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause - # 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. - # You can turn the layout off on particular actions by doing the following: - # - # class SiteController < ActionController::Base - # layout "standard", :except => [:ajax_method, :more_ajax, :another_ajax] - # end - # - # Optionally, you could do this in the method you wish to lack a layout: - # - # render :layout => false - # - # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the - # method that Ajax uses to make background requests) method. - # def name - # # Is this an XmlHttpRequest request? - # if (request.xhr?) - # render :text => @name.to_s - # else - # # No? Then render an action. - # render :action => 'view_attribute', :attr => @name - # end - # end - # - # The else clause can be left off and the current action will render with full layout and template. An extension - # to this solution was posted to Ryan Heneise's blog at ArtOfMission["http://www.artofmission.com/"]. - # - # layout proc{ |c| c.request.xhr? ? false : "application" } - # - # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request. - # - # If you are just returning a little data or don't want to build a template for your output, you may opt to simply - # render text output, like this: - # - # render :text => 'Return this from my method!' - # - # Since whatever the method returns is injected into the DOM, this will simply inject some text (or HTML, if you - # tell it to). This is usually how small updates, such updating a cart total or a file count, are handled. - # - # == Updating multiple elements - # See JavaScriptGenerator for information on updating multiple elements - # on the page in an Ajax response. - module PrototypeHelper - CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded, - :interactive, :complete, :failure, :success ] + - (100..599).to_a) - AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, - :asynchronous, :method, :insertion, :position, - :form, :with, :update, :script, :type ]).merge(CALLBACKS) - - # Returns the JavaScript needed for a remote function. - # See the link_to_remote documentation at https://github.com/rails/prototype_legacy_helper as it takes the same arguments. - # - # Example: - # # Generates: <select id="options" onchange="new Ajax.Updater('options', - # # '/testing/update_options', {asynchronous:true, evalScripts:true})"> - # <select id="options" onchange="<%= remote_function(:update => "options", - # :url => { :action => :update_options }) %>"> - # <option value="0">Hello</option> - # <option value="1">World</option> - # </select> - def remote_function(options) - javascript_options = options_for_ajax(options) - - update = '' - if options[:update] && options[:update].is_a?(Hash) - update = [] - update << "success:'#{options[:update][:success]}'" if options[:update][:success] - update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure] - update = '{' + update.join(',') + '}' - elsif options[:update] - update << "'#{options[:update]}'" - end - - function = update.empty? ? - "new Ajax.Request(" : - "new Ajax.Updater(#{update}, " - - url_options = options[:url] - function << "'#{ERB::Util.html_escape(escape_javascript(url_for(url_options)))}'" - function << ", #{javascript_options})" - - function = "#{options[:before]}; #{function}" if options[:before] - function = "#{function}; #{options[:after]}" if options[:after] - function = "if (#{options[:condition]}) { #{function}; }" if options[:condition] - function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm] - - return function.html_safe - end - - # All the methods were moved to GeneratorMethods so that - # #include_helpers_from_context has nothing to overwrite. - class JavaScriptGenerator #:nodoc: - def initialize(context, &block) #:nodoc: - @context, @lines = context, [] - include_helpers_from_context - @context.with_output_buffer(@lines) do - @context.instance_exec(self, &block) - end - end - - private - def include_helpers_from_context - extend @context.helpers if @context.respond_to?(:helpers) - extend GeneratorMethods - end - - # JavaScriptGenerator generates blocks of JavaScript code that allow you - # to change the content and presentation of multiple DOM elements. Use - # this in your Ajax response bodies, either in a <tt>\<script></tt> tag - # or as plain JavaScript sent with a Content-type of "text/javascript". - # - # Create new instances with PrototypeHelper#update_page or with - # ActionController::Base#render, then call +insert_html+, +replace_html+, - # +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in - # methods on the yielded generator in any order you like to modify the - # content and appearance of the current page. - # - # Example: - # - # # Generates: - # # new Element.insert("list", { bottom: "<li>Some item</li>" }); - # # new Effect.Highlight("list"); - # # ["status-indicator", "cancel-link"].each(Element.hide); - # update_page do |page| - # page.insert_html :bottom, 'list', "<li>#{@item.name}</li>" - # page.visual_effect :highlight, 'list' - # page.hide 'status-indicator', 'cancel-link' - # end - # - # - # Helper methods can be used in conjunction with JavaScriptGenerator. - # When a helper method is called inside an update block on the +page+ - # object, that method will also have access to a +page+ object. - # - # Example: - # - # module ApplicationHelper - # def update_time - # page.replace_html 'time', Time.now.to_s(:db) - # page.visual_effect :highlight, 'time' - # end - # end - # - # # Controller action - # def poll - # render(:update) { |page| page.update_time } - # end - # - # Calls to JavaScriptGenerator not matching a helper method below - # generate a proxy to the JavaScript Class named by the method called. - # - # Examples: - # - # # Generates: - # # Foo.init(); - # update_page do |page| - # page.foo.init - # end - # - # # Generates: - # # Event.observe('one', 'click', function () { - # # $('two').show(); - # # }); - # update_page do |page| - # page.event.observe('one', 'click') do |p| - # p[:two].show - # end - # end - # - # You can also use PrototypeHelper#update_page_tag instead of - # PrototypeHelper#update_page to wrap the generated JavaScript in a - # <tt>\<script></tt> tag. - module GeneratorMethods - def to_s #:nodoc: - (@lines * $/).tap do |javascript| - if ActionView::Base.debug_rjs - source = javascript.dup - javascript.replace "try {\n#{source}\n} catch (e) " - javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }" - end - end - end - - # Returns a element reference by finding it through +id+ in the DOM. This element can then be - # used for further method calls. Examples: - # - # page['blank_slate'] # => $('blank_slate'); - # page['blank_slate'].show # => $('blank_slate').show(); - # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up(); - # - # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup - # the correct id: - # - # page[@post] # => $('post_45') - # page[Post.new] # => $('new_post') - def [](id) - case id - when String, Symbol, NilClass - JavaScriptElementProxy.new(self, id) - else - JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id)) - end - end - - # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript - # expression as an argument to another JavaScriptGenerator method. - def literal(code) - ::ActiveSupport::JSON::Variable.new(code.to_s) - end - - # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be - # used for further method calls. Examples: - # - # page.select('p') # => $$('p'); - # page.select('p.welcome b').first # => $$('p.welcome b').first(); - # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide(); - # - # You can also use prototype enumerations with the collection. Observe: - # - # # Generates: $$('#items li').each(function(value) { value.hide(); }); - # page.select('#items li').each do |value| - # value.hide - # end - # - # Though you can call the block param anything you want, they are always rendered in the - # javascript as 'value, index.' Other enumerations, like collect() return the last statement: - # - # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); }); - # page.select('#items li').collect('hidden') do |item| - # item.hide - # end - # - def select(pattern) - JavaScriptElementCollectionProxy.new(self, pattern) - end - - # Inserts HTML at the specified +position+ relative to the DOM element - # identified by the given +id+. - # - # +position+ may be one of: - # - # <tt>:top</tt>:: HTML is inserted inside the element, before the - # element's existing content. - # <tt>:bottom</tt>:: HTML is inserted inside the element, after the - # element's existing content. - # <tt>:before</tt>:: HTML is inserted immediately preceding the element. - # <tt>:after</tt>:: HTML is inserted immediately following the element. - # - # +options_for_render+ may be either a string of HTML to insert, or a hash - # of options to be passed to ActionView::Base#render. For example: - # - # # Insert the rendered 'navigation' partial just before the DOM - # # element with ID 'content'. - # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" }); - # page.insert_html :before, 'content', :partial => 'navigation' - # - # # Add a list item to the bottom of the <ul> with ID 'list'. - # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" }); - # page.insert_html :bottom, 'list', '<li>Last item</li>' - # - def insert_html(position, id, *options_for_render) - content = javascript_object_for(render(*options_for_render)) - record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });" - end - - # Replaces the inner HTML of the DOM element with the given +id+. - # - # +options_for_render+ may be either a string of HTML to insert, or a hash - # of options to be passed to ActionView::Base#render. For example: - # - # # Replace the HTML of the DOM element having ID 'person-45' with the - # # 'person' partial for the appropriate object. - # # Generates: Element.update("person-45", "-- Contents of 'person' partial --"); - # page.replace_html 'person-45', :partial => 'person', :object => @person - # - def replace_html(id, *options_for_render) - call 'Element.update', id, render(*options_for_render) - end - - # Replaces the "outer HTML" (i.e., the entire element, not just its - # contents) of the DOM element with the given +id+. - # - # +options_for_render+ may be either a string of HTML to insert, or a hash - # of options to be passed to ActionView::Base#render. For example: - # - # # Replace the DOM element having ID 'person-45' with the - # # 'person' partial for the appropriate object. - # page.replace 'person-45', :partial => 'person', :object => @person - # - # This allows the same partial that is used for the +insert_html+ to - # be also used for the input to +replace+ without resorting to - # the use of wrapper elements. - # - # Examples: - # - # <div id="people"> - # <%= render :partial => 'person', :collection => @people %> - # </div> - # - # # Insert a new person - # # - # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, ""); - # page.insert_html :bottom, :partial => 'person', :object => @person - # - # # Replace an existing person - # - # # Generates: Element.replace("person_45", "-- Contents of partial --"); - # page.replace 'person_45', :partial => 'person', :object => @person - # - def replace(id, *options_for_render) - call 'Element.replace', id, render(*options_for_render) - end - - # Removes the DOM elements with the given +ids+ from the page. - # - # Example: - # - # # Remove a few people - # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove); - # page.remove 'person_23', 'person_9', 'person_2' - # - def remove(*ids) - loop_on_multiple_args 'Element.remove', ids - end - - # Shows hidden DOM elements with the given +ids+. - # - # Example: - # - # # Show a few people - # # Generates: ["person_6", "person_13", "person_223"].each(Element.show); - # page.show 'person_6', 'person_13', 'person_223' - # - def show(*ids) - loop_on_multiple_args 'Element.show', ids - end - - # Hides the visible DOM elements with the given +ids+. - # - # Example: - # - # # Hide a few people - # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide); - # page.hide 'person_29', 'person_9', 'person_0' - # - def hide(*ids) - loop_on_multiple_args 'Element.hide', ids - end - - # Toggles the visibility of the DOM elements with the given +ids+. - # Example: - # - # # Show a few people - # # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle); - # page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements - # page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements - # - def toggle(*ids) - loop_on_multiple_args 'Element.toggle', ids - end - - # Displays an alert dialog with the given +message+. - # - # Example: - # - # # Generates: alert('This message is from Rails!') - # page.alert('This message is from Rails!') - def alert(message) - call 'alert', message - end - - # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+. - # - # Examples: - # - # # Generates: window.location.href = "/mycontroller"; - # page.redirect_to(:action => 'index') - # - # # Generates: window.location.href = "/account/signup"; - # page.redirect_to(:controller => 'account', :action => 'signup') - def redirect_to(location) - url = location.is_a?(String) ? location : @context.url_for(location) - record "window.location.href = #{url.inspect}" - end - - # Reloads the browser's current +location+ using JavaScript - # - # Examples: - # - # # Generates: window.location.reload(); - # page.reload - def reload - record 'window.location.reload()' - end - - # Calls the JavaScript +function+, optionally with the given +arguments+. - # - # If a block is given, the block will be passed to a new JavaScriptGenerator; - # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt> - # and passed as the called function's final argument. - # - # Examples: - # - # # Generates: Element.replace(my_element, "My content to replace with.") - # page.call 'Element.replace', 'my_element', "My content to replace with." - # - # # Generates: alert('My message!') - # page.call 'alert', 'My message!' - # - # # Generates: - # # my_method(function() { - # # $("one").show(); - # # $("two").hide(); - # # }); - # page.call(:my_method) do |p| - # p[:one].show - # p[:two].hide - # end - def call(function, *arguments, &block) - record "#{function}(#{arguments_for_call(arguments, block)})" - end - - # Assigns the JavaScript +variable+ the given +value+. - # - # Examples: - # - # # Generates: my_string = "This is mine!"; - # page.assign 'my_string', 'This is mine!' - # - # # Generates: record_count = 33; - # page.assign 'record_count', 33 - # - # # Generates: tabulated_total = 47 - # page.assign 'tabulated_total', @total_from_cart - # - def assign(variable, value) - record "#{variable} = #{javascript_object_for(value)}" - end - - # Writes raw JavaScript to the page. - # - # Example: - # - # page << "alert('JavaScript with Prototype.');" - def <<(javascript) - @lines << javascript - end - - # Executes the content of the block after a delay of +seconds+. Example: - # - # # Generates: - # # setTimeout(function() { - # # ; - # # new Effect.Fade("notice",{}); - # # }, 20000); - # page.delay(20) do - # page.visual_effect :fade, 'notice' - # end - def delay(seconds = 1) - record "setTimeout(function() {\n\n" - yield - record "}, #{(seconds * 1000).to_i})" - end - - private - def loop_on_multiple_args(method, ids) - record(ids.size>1 ? - "#{javascript_object_for(ids)}.each(#{method})" : - "#{method}(#{javascript_object_for(ids.first)})") - end - - def page - self - end - - def record(line) - line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" - self << line - line - end - - def render(*options) - with_formats(:html) do - case option = options.first - when Hash - @context.render(*options) - else - option.to_s - end - end - end - - def with_formats(*args) - @context ? @context.lookup_context.update_details(:formats => args) { yield } : yield - end - - def javascript_object_for(object) - ::ActiveSupport::JSON.encode(object) - end - - def arguments_for_call(arguments, block = nil) - arguments << block_to_function(block) if block - arguments.map { |argument| javascript_object_for(argument) }.join ', ' - end - - def block_to_function(block) - generator = self.class.new(@context, &block) - literal("function() { #{generator.to_s} }") - end - - def method_missing(method, *arguments) - JavaScriptProxy.new(self, method.to_s.camelize) - end - end - end - - # Yields a JavaScriptGenerator and returns the generated JavaScript code. - # Use this to update multiple elements on a page in an Ajax response. - # See JavaScriptGenerator for more information. - # - # Example: - # - # update_page do |page| - # page.hide 'spinner' - # end - def update_page(&block) - JavaScriptGenerator.new(self, &block).to_s.html_safe - end - - # Works like update_page but wraps the generated JavaScript in a - # <tt>\<script></tt> tag. Use this to include generated JavaScript in an - # ERb template. See JavaScriptGenerator for more information. - # - # +html_options+ may be a hash of <tt>\<script></tt> attributes to be - # passed to ActionView::Helpers::JavaScriptHelper#javascript_tag. - def update_page_tag(html_options = {}, &block) - javascript_tag update_page(&block), html_options - end - - protected - def options_for_javascript(options) - if options.empty? - '{}' - else - "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}" - end - end - - def options_for_ajax(options) - js_options = build_callbacks(options) - - js_options['asynchronous'] = options[:type] != :synchronous - js_options['method'] = method_option_to_s(options[:method]) if options[:method] - js_options['insertion'] = "'#{options[:position].to_s.downcase}'" if options[:position] - js_options['evalScripts'] = options[:script].nil? || options[:script] - - if options[:form] - js_options['parameters'] = 'Form.serialize(this)' - elsif options[:submit] - js_options['parameters'] = "Form.serialize('#{options[:submit]}')" - elsif options[:with] - js_options['parameters'] = options[:with] - end - - if protect_against_forgery? && !options[:form] - if js_options['parameters'] - js_options['parameters'] << " + '&" - else - js_options['parameters'] = "'" - end - js_options['parameters'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')" - end - - options_for_javascript(js_options) - end - - def method_option_to_s(method) - (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'" - end - - def build_callbacks(options) - callbacks = {} - options.each do |callback, code| - if CALLBACKS.include?(callback) - name = 'on' + callback.to_s.capitalize - callbacks[name] = "function(request){#{code}}" - end - end - callbacks - end - end - - # Converts chained method calls on DOM proxy elements into JavaScript chains - class JavaScriptProxy < ActiveSupport::BasicObject #:nodoc: - - def initialize(generator, root = nil) - @generator = generator - @generator << root if root - end - - def is_a?(klass) - klass == JavaScriptProxy - end - - private - def method_missing(method, *arguments, &block) - if method.to_s =~ /(.*)=$/ - assign($1, arguments.first) - else - call("#{method.to_s.camelize(:lower)}", *arguments, &block) - end - end - - def call(function, *arguments, &block) - append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})") - self - end - - def assign(variable, value) - append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}") - end - - def function_chain - @function_chain ||= @generator.instance_variable_get(:@lines) - end - - def append_to_function_chain!(call) - function_chain[-1].chomp!(';') - function_chain[-1] += ".#{call};" - end - end - - class JavaScriptElementProxy < JavaScriptProxy #:nodoc: - def initialize(generator, id) - @id = id - super(generator, "$(#{::ActiveSupport::JSON.encode(id)})") - end - - # Allows access of element attributes through +attribute+. Examples: - # - # page['foo']['style'] # => $('foo').style; - # page['foo']['style']['color'] # => $('blank_slate').style.color; - # page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red'; - # page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red'; - def [](attribute) - append_to_function_chain!(attribute) - self - end - - def []=(variable, value) - assign(variable, value) - end - - def replace_html(*options_for_render) - call 'update', @generator.send(:render, *options_for_render) - end - - def replace(*options_for_render) - call 'replace', @generator.send(:render, *options_for_render) - end - - def reload(options_for_replace = {}) - replace(options_for_replace.merge({ :partial => @id.to_s })) - end - - end - - class JavaScriptVariableProxy < JavaScriptProxy #:nodoc: - def initialize(generator, variable) - @variable = ::ActiveSupport::JSON::Variable.new(variable) - @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks - super(generator) - end - - # The JSON Encoder calls this to check for the +to_json+ method - # Since it's a blank slate object, I suppose it responds to anything. - def respond_to?(*) - true - end - - def as_json(options = nil) - @variable - end - - private - def append_to_function_chain!(call) - @generator << @variable if @empty - @empty = false - super - end - end - - class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc: - 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 - ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS - attr_reader :generator - delegate :arguments_for_call, :to => :generator - - def initialize(generator, pattern) - super(generator, @pattern = pattern) - end - - def each_slice(variable, number, &block) - if block - enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block - else - add_variable_assignment!(variable) - append_enumerable_function!("eachSlice(#{::ActiveSupport::JSON.encode(number)});") - end - end - - def grep(variable, pattern, &block) - enumerate :grep, :variable => variable, :return => true, :method_args => [::ActiveSupport::JSON::Variable.new(pattern.inspect)], :yield_args => %w(value index), &block - end - - def in_groups_of(variable, number, fill_with = nil) - arguments = [number] - arguments << fill_with unless fill_with.nil? - add_variable_assignment!(variable) - append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});") - end - - def inject(variable, memo, &block) - enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block - end - - def pluck(variable, property) - add_variable_assignment!(variable) - append_enumerable_function!("pluck(#{::ActiveSupport::JSON.encode(property)});") - end - - def zip(variable, *arguments, &block) - add_variable_assignment!(variable) - append_enumerable_function!("zip(#{arguments_for_call arguments}") - if block - function_chain[-1] += ", function(array) {" - yield ::ActiveSupport::JSON::Variable.new('array') - add_return_statement! - @generator << '});' - else - function_chain[-1] += ');' - end - end - - private - def method_missing(method, *arguments, &block) - if ENUMERABLE_METHODS.include?(method) - returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method) - variable = arguments.first if returnable - enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block) - else - super - end - end - - # Options - # * variable - name of the variable to set the result of the enumeration to - # * method_args - array of the javascript enumeration method args that occur before the function - # * yield_args - array of the javascript yield args - # * return - true if the enumeration should return the last statement - def enumerate(enumerable, options = {}, &block) - options[:method_args] ||= [] - options[:yield_args] ||= [] - yield_args = options[:yield_args] * ', ' - method_args = arguments_for_call options[:method_args] # foo, bar, function - method_args << ', ' unless method_args.blank? - add_variable_assignment!(options[:variable]) if options[:variable] - append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {") - # only yield as many params as were passed in the block - yield(*options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1]) - add_return_statement! if options[:return] - @generator << '});' - end - - def add_variable_assignment!(variable) - function_chain.push("var #{variable} = #{function_chain.pop}") - end - - def add_return_statement! - unless function_chain.last =~ /return/ - function_chain.push("return #{function_chain.pop.chomp(';')};") - end - end - - def append_enumerable_function!(call) - function_chain[-1].chomp!(';') - function_chain[-1] += ".#{call}" - end - end - - class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\ - def initialize(generator, pattern) - super(generator, "$$(#{::ActiveSupport::JSON.encode(pattern)})") - end - end - end -end diff --git a/actionpack/lib/action_view/helpers/scriptaculous_helper.rb b/actionpack/lib/action_view/helpers/scriptaculous_helper.rb deleted file mode 100644 index 8610c2469e..0000000000 --- a/actionpack/lib/action_view/helpers/scriptaculous_helper.rb +++ /dev/null @@ -1,263 +0,0 @@ -require 'action_view/helpers/javascript_helper' -require 'active_support/json' - -module ActionView - # = Action View Scriptaculous Helpers - module Helpers - # Provides a set of helpers for calling Scriptaculous[http://script.aculo.us/] - # JavaScript functions, including those which create Ajax controls and visual - # effects. - # - # To be able to use these helpers, you must include the Prototype - # JavaScript framework and the Scriptaculous JavaScript library in your - # pages. See the documentation for ActionView::Helpers::JavaScriptHelper - # for more information on including the necessary JavaScript. - # - # The Scriptaculous helpers' behavior can be tweaked with various options. - # - # See the documentation at http://script.aculo.us for more information on - # using these helpers in your application. - module ScriptaculousHelper - TOGGLE_EFFECTS = [:toggle_appear, :toggle_slide, :toggle_blind] - - # Returns a JavaScript snippet to be used on the Ajax callbacks for - # starting visual effects. - # - # If no +element_id+ is given, it assumes "element" which should be a local - # variable in the generated JavaScript execution context. This can be - # used for example with +drop_receiving_element+: - # - # <%= drop_receiving_element (...), :loading => visual_effect(:fade) %> - # - # This would fade the element that was dropped on the drop receiving - # element. - # - # For toggling visual effects, you can use <tt>:toggle_appear</tt>, <tt>:toggle_slide</tt>, and - # <tt>:toggle_blind</tt> which will alternate between appear/fade, slidedown/slideup, and - # blinddown/blindup respectively. - # - # You can change the behaviour with various options, see - # http://script.aculo.us for more documentation. - def visual_effect(name, element_id = false, js_options = {}) - element = element_id ? ActiveSupport::JSON.encode(element_id) : "element" - - js_options[:queue] = if js_options[:queue].is_a?(Hash) - '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}' - elsif js_options[:queue] - "'#{js_options[:queue]}'" - end if js_options[:queue] - - [:endcolor, :direction, :startcolor, :scaleMode, :restorecolor].each do |option| - js_options[option] = "'#{js_options[option]}'" if js_options[option] - end - - if TOGGLE_EFFECTS.include? name.to_sym - "Effect.toggle(#{element},'#{name.to_s.gsub(/^toggle_/,'')}',#{options_for_javascript(js_options)});" - else - "new Effect.#{name.to_s.camelize}(#{element},#{options_for_javascript(js_options)});" - end - end - - # Makes the element with the DOM ID specified by +element_id+ sortable - # by drag-and-drop and make an Ajax call whenever the sort order has - # changed. By default, the action called gets the serialized sortable - # element as parameters. - # - # Example: - # - # <%= sortable_element("my_list", :url => { :action => "order" }) %> - # - # In the example, the action gets a "my_list" array parameter - # containing the values of the ids of elements the sortable consists - # of, in the current order. - # - # Important: For this to work, the sortable elements must have id - # attributes in the form "string_identifier". For example, "item_1". Only - # the identifier part of the id attribute will be serialized. - # - # Additional +options+ are: - # - # * <tt>:format</tt> - A regular expression to determine what to send as the - # serialized id to the server (the default is <tt>/^[^_]*_(.*)$/</tt>). - # - # * <tt>:constraint</tt> - Whether to constrain the dragging to either - # <tt>:horizontal</tt> or <tt>:vertical</tt> (or false to make it unconstrained). - # - # * <tt>:overlap</tt> - Calculate the item overlap in the <tt>:horizontal</tt> - # or <tt>:vertical</tt> direction. - # - # * <tt>:tag</tt> - Which children of the container element to treat as - # sortable (default is <tt>li</tt>). - # - # * <tt>:containment</tt> - Takes an element or array of elements to treat as - # potential drop targets (defaults to the original target element). - # - # * <tt>:only</tt> - A CSS class name or array of class names used to filter - # out child elements as candidates. - # - # * <tt>:scroll</tt> - Determines whether to scroll the list during drag - # operations if the list runs past the visual border. - # - # * <tt>:tree</tt> - Determines whether to treat nested lists as part of the - # main sortable list. This means that you can create multi-layer lists, - # and not only sort items at the same level, but drag and sort items - # between levels. - # - # * <tt>:hoverclass</tt> - If set, the Droppable will have this additional CSS class - # when an accepted Draggable is hovered over it. - # - # * <tt>:handle</tt> - Sets whether the element should only be draggable by an - # embedded handle. The value may be a string referencing a CSS class value - # (as of script.aculo.us V1.5). The first child/grandchild/etc. element - # found within the element that has this CSS class value will be used as - # the handle. - # - # * <tt>:ghosting</tt> - Clones the element and drags the clone, leaving - # the original in place until the clone is dropped (default is <tt>false</tt>). - # - # * <tt>:dropOnEmpty</tt> - If true the Sortable container will be made into - # a Droppable, that can receive a Draggable (as according to the containment - # rules) as a child element when there are no more elements inside (default - # is <tt>false</tt>). - # - # * <tt>:onChange</tt> - Called whenever the sort order changes while dragging. When - # dragging from one Sortable to another, the callback is called once on each - # Sortable. Gets the affected element as its parameter. - # - # * <tt>:onUpdate</tt> - Called when the drag ends and the Sortable's order is - # changed in any way. When dragging from one Sortable to another, the callback - # is called once on each Sortable. Gets the container as its parameter. - # - # See http://script.aculo.us for more documentation. - def sortable_element(element_id, options = {}) - javascript_tag(sortable_element_js(element_id, options).chop!) - end - - def sortable_element_js(element_id, options = {}) #:nodoc: - options[:with] ||= "Sortable.serialize(#{ActiveSupport::JSON.encode(element_id)})" - options[:onUpdate] ||= "function(){" + remote_function(options) + "}" - options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) } - - [:tag, :overlap, :constraint, :handle].each do |option| - options[option] = "'#{options[option]}'" if options[option] - end - - options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment] - options[:only] = array_or_string_for_javascript(options[:only]) if options[:only] - - %(Sortable.create(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});) - end - - # Makes the element with the DOM ID specified by +element_id+ draggable. - # - # Example: - # <%= draggable_element("my_image", :revert => true) - # - # You can change the behaviour with various options, see - # http://script.aculo.us for more documentation. - def draggable_element(element_id, options = {}) - javascript_tag(draggable_element_js(element_id, options).chop!) - end - - def draggable_element_js(element_id, options = {}) #:nodoc: - %(new Draggable(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});) - end - - # Makes the element with the DOM ID specified by +element_id+ receive - # dropped draggable elements (created by +draggable_element+). - # and make an AJAX call. By default, the action called gets the DOM ID - # of the element as parameter. - # - # Example: - # <%= drop_receiving_element("my_cart", :url => - # { :controller => "cart", :action => "add" }) %> - # - # You can change the behaviour with various options, see - # http://script.aculo.us for more documentation. - # - # Some of these +options+ include: - # * <tt>:accept</tt> - Set this to a string or an array of strings describing the - # allowable CSS classes that the +draggable_element+ must have in order - # to be accepted by this +drop_receiving_element+. - # - # * <tt>:confirm</tt> - Adds a confirmation dialog. Example: - # - # :confirm => "Are you sure you want to do this?" - # - # * <tt>:hoverclass</tt> - If set, the +drop_receiving_element+ will have - # this additional CSS class when an accepted +draggable_element+ is - # hovered over it. - # - # * <tt>:onDrop</tt> - Called when a +draggable_element+ is dropped onto - # this element. Override this callback with a JavaScript expression to - # change the default drop behaviour. Example: - # - # :onDrop => "function(draggable_element, droppable_element, event) { alert('I like bananas') }" - # - # This callback gets three parameters: The Draggable element, the Droppable - # element and the Event object. You can extract additional information about - # the drop - like if the Ctrl or Shift keys were pressed - from the Event object. - # - # * <tt>:with</tt> - A JavaScript expression specifying the parameters for - # the XMLHttpRequest. Any expressions should return a valid URL query string. - def drop_receiving_element(element_id, options = {}) - javascript_tag(drop_receiving_element_js(element_id, options).chop!) - end - - def drop_receiving_element_js(element_id, options = {}) #:nodoc: - options[:with] ||= "'id=' + encodeURIComponent(element.id)" - options[:onDrop] ||= "function(element){" + remote_function(options) + "}" - options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) } - - options[:accept] = array_or_string_for_javascript(options[:accept]) if options[:accept] - options[:hoverclass] = "'#{options[:hoverclass]}'" if options[:hoverclass] - - # Confirmation happens during the onDrop callback, so it can be removed from the options - options.delete(:confirm) if options[:confirm] - - %(Droppables.add(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});) - end - - protected - def array_or_string_for_javascript(option) - if option.kind_of?(Array) - "['#{option.join('\',\'')}']" - elsif !option.nil? - "'#{option}'" - end - end - end - - module PrototypeHelper - class JavaScriptGenerator - module GeneratorMethods - # Starts a script.aculo.us visual effect. See - # ActionView::Helpers::ScriptaculousHelper for more information. - def visual_effect(name, id = nil, options = {}) - record @context.send(:visual_effect, name, id, options) - end - - # Creates a script.aculo.us sortable element. Useful - # to recreate sortable elements after items get added - # or deleted. - # See ActionView::Helpers::ScriptaculousHelper for more information. - def sortable(id, options = {}) - record @context.send(:sortable_element_js, id, options) - end - - # Creates a script.aculo.us draggable element. - # See ActionView::Helpers::ScriptaculousHelper for more information. - def draggable(id, options = {}) - record @context.send(:draggable_element_js, id, options) - end - - # Creates a script.aculo.us drop receiving element. - # See ActionView::Helpers::ScriptaculousHelper for more information. - def drop_receiving(id, options = {}) - record @context.send(:drop_receiving_element_js, id, options) - end - end - end - end - end -end diff --git a/actionpack/lib/action_view/helpers/sprockets_helper.rb b/actionpack/lib/action_view/helpers/sprockets_helper.rb new file mode 100644 index 0000000000..408a2030ab --- /dev/null +++ b/actionpack/lib/action_view/helpers/sprockets_helper.rb @@ -0,0 +1,85 @@ +require 'uri' + +module ActionView + module Helpers + module SprocketsHelper + def sprockets_javascript_path(source) + compute_sprockets_path source, 'assets', 'js' + end + + def sprockets_javascript_include_tag(source, options = {}) + options = { + 'type' => "text/javascript", + 'src' => sprockets_javascript_path(source) + }.merge(options.stringify_keys) + + content_tag 'script', "", options + end + + def sprockets_stylesheet_path(source) + compute_sprockets_path source, 'assets', 'css' + end + + def sprockets_stylesheet_link_tag(source, options = {}) + options = { + 'rel' => "stylesheet", + 'type' => "text/css", + 'media' => "screen", + 'href' => sprockets_stylesheet_path(source) + }.merge(options.stringify_keys) + + tag 'link', options + end + + private + def compute_sprockets_path(source, dir, default_ext) + source = source.to_s + + return source if URI.parse(source).host + + # Add /javscripts to relative paths + if source[0] != ?/ + source = "/#{dir}/#{source}" + end + + # Add default extension if there isn't one + if default_ext && File.extname(source).empty? + source = "#{source}.#{default_ext}" + end + + # Fingerprint url + if source =~ /^\/#{dir}\/(.+)/ + source = assets.path($1, config.perform_caching, dir) + end + + host = compute_asset_host(source) + + if controller.respond_to?(:request) && host && URI.parse(host).host + source = "#{controller.request.protocol}#{host}#{source}" + end + + source + end + + def compute_asset_host(source) + if host = config.asset_host + if host.is_a?(Proc) || host.respond_to?(:call) + case host.is_a?(Proc) ? host.arity : host.method(:call).arity + when 2 + request = controller.respond_to?(:request) && controller.request + host.call(source, request) + else + host.call(source) + end + else + (host =~ /%d/) ? host % (source.hash % 4) : host + end + end + end + + def assets + Rails.application.assets + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 2d3c5fe7e7..bdda1df437 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -303,7 +303,7 @@ module ActionView # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>. # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." def auto_link(text, *args, &block)#link = :all, html = {}, &block) - return ''.html_safe if text.blank? + return '' if text.blank? options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter unless args.empty? @@ -507,7 +507,7 @@ module ActionView end content_tag(:a, link_text, link_attributes.merge('href' => href), !!options[:sanitize]) + punctuation.reverse.join('') end - end.html_safe + end end # Turns all email addresses into clickable links. If a block is given, diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 2cd2dca711..de75488e72 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -81,9 +81,12 @@ module ActionView # # => /workshops # # <%= url_for(@workshop) %> - # # calls @workshop.to_s + # # calls @workshop.to_param which by default returns the id # # => /workshops/5 # + # # to_param can be re-defined in a model to provide different URL names: + # # => /workshops/1-workshop-name + # # <%= url_for("http://www.example.com") %> # # => http://www.example.com # @@ -183,7 +186,7 @@ module ActionView # link_to "Profiles", :controller => "profiles" # # => <a href="/profiles">Profiles</a> # - # You can use a block as well if your link target is hard to fit into the name parameter. ERb example: + # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: # # <%= link_to(@profile) do %> # <strong><%= @profile.name %></strong> -- <span>Check it out!</span> diff --git a/actionpack/lib/action_view/path_set.rb b/actionpack/lib/action_view/path_set.rb index e3de3e1eac..8b840a6463 100644 --- a/actionpack/lib/action_view/path_set.rb +++ b/actionpack/lib/action_view/path_set.rb @@ -15,6 +15,7 @@ module ActionView #:nodoc: end def find_all(path, prefixes = [], *args) + prefixes = [prefixes] if String === prefixes prefixes.each do |prefix| each do |resolver| templates = resolver.find_all(path, prefix, *args) diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 501ec07b09..f20ba7e6d3 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -6,7 +6,7 @@ module ActionView class Railtie < Rails::Railtie config.action_view = ActiveSupport::OrderedOptions.new config.action_view.stylesheet_expansions = {} - config.action_view.javascript_expansions = { :defaults => ['prototype', 'effects', 'dragdrop', 'controls', 'rails'] } + config.action_view.javascript_expansions = { :defaults => %w(jquery rails) } initializer "action_view.cache_asset_ids" do |app| unless app.config.cache_classes diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 94c0a8a8fb..10cd37d56f 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,3 @@ -require 'action_view/renderer/abstract_renderer' - module ActionView class PartialRenderer < AbstractRenderer #:nodoc: PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } @@ -79,7 +77,7 @@ module ActionView locals[as] = object content = @template.render(view, locals) do |*name| - view._layout_for(*name, &block) + view._block_layout_for(*name, &block) end content = layout.render(view, locals){ content } if layout diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb new file mode 100644 index 0000000000..52f0e9f5bd --- /dev/null +++ b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb @@ -0,0 +1,129 @@ +# 1.9 ships with Fibers but we need to require the extra +# methods explicitly. We only load those extra methods if +# Fiber is available in the first place. +require 'fiber' if defined?(Fiber) + +module ActionView + # Consider the following layout: + # + # <%= yield :header %> + # 2 + # <%= yield %> + # 5 + # <%= yield :footer %> + # + # And template: + # + # <%= provide :header, "1" %> + # 3 + # 4 + # <%= provide :footer, "6" %> + # + # It will stream: + # + # "1\n", "2\n", "3\n4\n", "5\n", "6\n" + # + # Notice that once you <%= yield %>, it will render the whole template + # before streaming again. In the future, we can also support streaming + # from the template and not only the layout. + # + # Also, notice we use +provide+ instead of +content_for+, as +provide+ + # gives the control back to the layout as soon as it is called. + # With +content_for+, it would render all the template to find all + # +content_for+ calls. For instance, consider this layout: + # + # <%= yield :header %> + # + # With this template: + # + # <%= content_for :header, "1" %> + # <%= provide :header, "2" %> + # <%= provide :header, "3" %> + # + # It will return "12\n" because +content_for+ continues rendering the + # template but it is returns back to the layout as soon as it sees the + # first +provide+. + # + # == TODO + # + # * Add streaming support in the controllers with no-cache settings + # * What should happen when an error happens? + # * Support streaming from child templates, partials and so on. + # * Support on sprockets async JS load? + # + class StreamingTemplateRenderer < TemplateRenderer #:nodoc: + # A valid Rack::Body (i.e. it responds to each). + # It is initialized with a block that, when called, starts + # rendering the template. + class Body #:nodoc: + def initialize(&start) + @start = start + end + + def each(&block) + @start.call(block) + self + end + end + + # For streaming, instead of rendering a given a template, we return a Body + # object that responds to each. This object is initialized with a block + # that knows how to render the template. + def render_template(template, layout_name = nil, locals = {}) #:nodoc: + return [super] unless layout_name && template.supports_streaming? + + locals ||= {} + layout = layout_name && find_layout(layout_name, locals.keys) + + Body.new do |buffer| + delayed_render(buffer, template, layout, @view, locals) + end + end + + private + + def delayed_render(buffer, template, layout, view, locals) + # Wrap the given buffer in the StreamingBuffer and pass it to the + # underlying template handler. Now, everytime something is concatenated + # to the buffer, it is not appended to an array, but streamed straight + # to the client. + output = ActionView::StreamingBuffer.new(buffer) + yielder = lambda { |*name| view._layout_for(*name) } + + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + fiber = Fiber.new do + if layout + layout.render(view, locals, output, &yielder) + else + # If you don't have a layout, just render the thing + # and concatenate the final result. This is the same + # as a layout with just <%= yield %> + output.safe_concat view._layout_for + end + end + + # Set the view flow to support streaming. It will be aware + # when to stop rendering the layout because it needs to search + # something in the template and vice-versa. + view._view_flow = StreamingFlow.new(view, fiber) + + # Yo! Start the fiber! + fiber.resume + + # If the fiber is still alive, it means we need something + # from the template, so start rendering it. If not, it means + # the layout exited without requiring anything from the template. + if fiber.alive? + content = template.render(view, locals, &yielder) + + # Once rendering the template is done, sets its content in the :layout key. + view._view_flow.set(:layout, content) + + # In case the layout continues yielding, we need to resume + # the fiber until all yields are handled. + fiber.resume while fiber.alive? + end + end + end + end +end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index 9ae1636131..6b5ead463f 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -1,41 +1,16 @@ -require 'set' require 'active_support/core_ext/object/try' require 'active_support/core_ext/array/wrap' -require 'action_view/renderer/abstract_renderer' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: - attr_reader :rendered - - def initialize(view) - super - @rendered = Set.new - end - def render(options) wrap_formats(options[:template] || options[:file]) do template = determine_template(options) + freeze_formats(template.formats, true) render_template(template, options[:layout], options[:locals]) end end - def render_once(options) - paths, locals = options[:once], options[:locals] || {} - layout, keys = options[:layout], locals.keys - prefixes = options.fetch(:prefixes, @view.controller_prefixes) - - raise "render :once expects a String or an Array to be given" unless paths - - render_with_layout(layout, locals) do - contents = [] - Array.wrap(paths).each do |path| - template = find_template(path, prefixes, false, keys) - contents << render_template(template, nil, locals) if @rendered.add?(template) - end - contents.join("\n") - end - end - # Determine the template to be rendered using the given options. def determine_template(options) #:nodoc: keys = options[:locals].try(:keys) || [] @@ -56,7 +31,6 @@ module ActionView # Renders the given template. An string representing the layout can be # supplied as well. def render_template(template, layout_name = nil, locals = {}) #:nodoc: - freeze_formats(template.formats, true) view, locals = @view, locals || {} render_with_layout(layout_name, locals) do |layout| @@ -72,7 +46,7 @@ module ActionView if layout view = @view - view.store_content_for(:layout, content) + view._view_flow.set(:layout, content) layout.render(view, locals){ |*name| view._layout_for(*name) } else content diff --git a/actionpack/lib/action_view/rendering.rb b/actionpack/lib/action_view/rendering.rb index baa5d2c3fd..2bce2fb045 100644 --- a/actionpack/lib/action_view/rendering.rb +++ b/actionpack/lib/action_view/rendering.rb @@ -6,11 +6,9 @@ module ActionView # Returns the result of a render that's dictated by the options hash. The primary options are: # # * <tt>:partial</tt> - See ActionView::Partials. - # * <tt>:update</tt> - Calls update_page with the block given. # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. - # * <tt>:once</tt> - Accepts a string or an array of strings and Rails will ensure they each of them are rendered just once. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. @@ -21,18 +19,27 @@ module ActionView _render_partial(options.merge(:partial => options[:layout]), &block) elsif options.key?(:partial) _render_partial(options) - elsif options.key?(:once) - _render_once(options) else _render_template(options) end - when :update - update_page(&block) else _render_partial(:partial => options, :locals => locals) end end + # Render but returns a valid Rack body. If fibers are defined, we return + # a streaming body that renders the template piece by piece. + # + # Note that partials are not supported to be rendered with streaming, + # so in such cases, we just wrap them in an array. + def render_body(options) + if options.key?(:partial) + [_render_partial(options)] + else + StreamingTemplateRenderer.new(self).render(options) + end + end + # Returns the contents that are yielded to a layout, given a name or a block. # # You can think of a layout as a method that is called with a block. If the user calls @@ -79,22 +86,23 @@ module ActionView # Hello David # </html> # - def _layout_for(*args, &block) + def _layout_for(*args) + name = args.first + name = :layout unless name.is_a?(Symbol) + @_view_flow.get(name).html_safe + end + + # Handle layout for calls from partials that supports blocks. + def _block_layout_for(*args, &block) name = args.first - if name.is_a?(Symbol) - @_content_for[name].html_safe - elsif block + if !name.is_a?(Symbol) && block capture(*args, &block) else - @_content_for[:layout].html_safe + _layout_for(*args) end end - def _render_once(options) #:nodoc: - _template_renderer.render_once(options) - end - def _render_template(options) #:nodoc: _template_renderer.render(options) end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 96d506fac5..6dfc4f68ae 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -126,17 +126,23 @@ module ActionView @formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f } end + # Returns if the underlying handler supports streaming. If so, + # a streaming buffer *may* be passed when it start rendering. + def supports_streaming? + handler.respond_to?(:supports_streaming?) && handler.supports_streaming? + end + # Render a template. If the template was not compiled yet, it is done # exactly before rendering. # # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, &block) + def render(view, locals, buffer=nil, &block) old_template, view._template = view._template, self ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do compile!(view) - view.send(method_name, locals, &block) + view.send(method_name, locals, buffer, &block) end rescue Exception => e handle_render_error(view, e) @@ -167,28 +173,6 @@ module ActionView end end - # Expires this template by setting his updated_at date to Jan 1st, 1970. - def expire! - @updated_at = Time.utc(1970) - end - - # Receives a view context and renders a template exactly like self by using - # the @virtual_path. It raises an error if no @virtual_path was given. - def rerender(view) - raise "A template needs to have a virtual path in order to be rerendered" unless @virtual_path - name = @virtual_path.dup - if name.sub!(/(^|\/)_([^\/]*)$/, '\1\2') - view.render :partial => name - else - view.render :template => @virtual_path - end - end - - # Used to store template data by template handlers. - def data - @data ||= {} - end - def inspect @inspect ||= if defined?(Rails.root) @@ -274,13 +258,12 @@ module ActionView end end - arity = @handler.respond_to?(:arity) ? @handler.arity : @handler.method(:call).arity - code = arity.abs == 1 ? @handler.call(self) : @handler.call(self, view) + code = @handler.call(self) # Make sure that the resulting String to be evalled is in the # encoding of the code source = <<-end_src - def #{method_name}(local_assigns) + def #{method_name}(local_assigns, output_buffer) _old_output_buffer = @output_buffer;#{locals_code};#{code} ensure @output_buffer = _old_output_buffer diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index 4438199497..959afa734e 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -3,12 +3,10 @@ module ActionView #:nodoc: class Template module Handlers #:nodoc: autoload :ERB, 'action_view/template/handlers/erb' - autoload :RJS, 'action_view/template/handlers/rjs' autoload :Builder, 'action_view/template/handlers/builder' def self.extended(base) base.register_default_template_handler :erb, ERB.new - base.register_template_handler :rjs, RJS.new base.register_template_handler :builder, Builder.new end diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index a36837afc8..7e9e4e518a 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,28 +1,14 @@ require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/string/output_safety' require 'action_view/template' require 'action_view/template/handler' require 'erubis' module ActionView - class OutputBuffer < ActiveSupport::SafeBuffer - def initialize(*) - super - encode! if encoding_aware? - end - - def <<(value) - super(value.to_s) - end - alias :append= :<< - alias :safe_append= :safe_concat - end - class Template module Handlers class Erubis < ::Erubis::Eruby def add_preamble(src) - src << "@output_buffer = ActionView::OutputBuffer.new;" + src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" end def add_text(src, text) @@ -55,7 +41,7 @@ module ActionView class ERB # Specify trim mode for the ERB compiler. Defaults to '-'. - # See ERb documentation for suitable values. + # See ERB documentation for suitable values. class_attribute :erb_trim_mode self.erb_trim_mode = '-' @@ -73,6 +59,10 @@ module ActionView new.call(template) end + def supports_streaming? + true + end + def handles_encoding? true end diff --git a/actionpack/lib/action_view/template/handlers/rjs.rb b/actionpack/lib/action_view/template/handlers/rjs.rb deleted file mode 100644 index 9d71059134..0000000000 --- a/actionpack/lib/action_view/template/handlers/rjs.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActionView - module Template::Handlers - class RJS - # Default format used by RJS. - class_attribute :default_format - self.default_format = Mime::JS - - def call(template) - "update_page do |page|;#{template.source}\nend" - end - end - end -end diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 41c6310ae2..870897958a 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -186,7 +186,7 @@ module ActionView # ==== Examples # # Default pattern, loads views the same way as previous versions of rails, eg. when you're - # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml,rjs},}` + # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}` # # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}") # diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 3e2ddffa16..5c74bf843a 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -121,7 +121,7 @@ module ActionView # Support the selector assertions # # Need to experiment if this priority is the best one: rendered => output_buffer - def response_from_page_or_rjs + def response_from_page HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root end |