diff options
Diffstat (limited to 'actionpack/lib')
-rw-r--r-- | actionpack/lib/action_view/helpers/prototype_helper.rb | 554 |
1 files changed, 294 insertions, 260 deletions
diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 9a58f41821..113ddc8739 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -360,267 +360,287 @@ module ActionView end 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 <script> 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: - # - # 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 - # - # generates the following JavaScript: - # - # new Insertion.Bottom("list", "<li>Some item</li>"); - # new Effect.Highlight("list"); - # ["status-indicator", "cancel-link"].each(Element.hide); - # - # 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 - # - # You can also use PrototypeHelper#update_page_tag instead of - # PrototypeHelper#update_page to wrap the generated JavaScript in a - # <script> tag. - class JavaScriptGenerator + # 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, [] - # removed because those methods were overriding valid generator methods - # include_helpers_from_context + include_helpers_from_context @context.instance_exec(self, &block) end - - def to_s #:nodoc: - @lines * $/ - 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(); - def [](id) - JavaScriptElementProxy.new(self, id) - 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(); - 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 preceeding 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'. - # insert_html :before, 'content', :partial => 'navigation' - # - # # Add a list item to the bottom of the <ul> with ID 'list'. - # insert_html :bottom, 'list', '<li>Last item</li>' - # - def insert_html(position, id, *options_for_render) - insertion = position.to_s.camelize - call "new Insertion.#{insertion}", id, render(*options_for_render) - 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. - # replace_html 'person-45', :partial => 'person', :object => @person - # - def replace_html(id, *options_for_render) - call 'Element.update', id, render(*options_for_render) + + private + def include_helpers_from_context + @context.extended_by.each do |mod| + extend mod unless mod.name =~ /^ActionView::Helpers/ + end + extend GeneratorMethods 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. - # replace_html 'person-45', :partial => 'person', :object => @person + + # 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 <script> tag or as plain + # JavaScript sent with a Content-type of "text/javascript". # - # 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. + # 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. # - # Examples: + # Example: # - # <div id="people"> - # <%= render :partial => 'person', :collection => @people %> - # </div> + # 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 + # + # generates the following JavaScript: # - # # Insert a new person - # page.insert_html :bottom, :partial => 'person', :object => @person + # new Insertion.Bottom("list", "<li>Some item</li>"); + # new Effect.Highlight("list"); + # ["status-indicator", "cancel-link"].each(Element.hide); # - # # Replace an existing person - # page.replace 'person_45', :partial => 'person', :object => @person + # 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: # - 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. - def remove(*ids) - record "#{javascript_object_for(ids)}.each(Element.remove)" - end - - # Shows hidden DOM elements with the given +ids+. - def show(*ids) - call 'Element.show', *ids - end - - # Hides the visible DOM elements with the given +ids+. - def hide(*ids) - call 'Element.hide', *ids - end - - # Toggles the visibility of the DOM elements with the given +ids+. - def toggle(*ids) - call 'Element.toggle', *ids - end - - # Displays an alert dialog with the given +message+. - def alert(message) - call 'alert', message - end - - # Redirects the browser to the given +location+, in the same form as - # +url_for+. - def redirect_to(location) - assign 'window.location.href', @context.url_for(location) - end - - # Calls the JavaScript +function+, optionally with the given - # +arguments+. - def call(function, *arguments) - record "#{function}(#{arguments_for_call(arguments)})" - end - - # Assigns the JavaScript +variable+ the given +value+. - def assign(variable, value) - record "#{variable} = #{javascript_object_for(value)}" - end - - # Writes raw JavaScript to the page. - def <<(javascript) - @lines << javascript - end - - # Executes the content of the block after a delay of +seconds+. Example: + # module ApplicationHelper + # def update_time + # page.replace_html 'time', Time.now.to_s(:db) + # page.visual_effect :highlight, 'time' + # end + # end # - # page.delay(20) do - # page.visual_effect :fade, 'notice' + # # Controller action + # def poll + # render :update { |page| page.update_time } # end - def delay(seconds = 1) - record "setTimeout(function() {\n\n" - yield - record "}, #{(seconds * 1000).to_i})" - end - - # 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 - - private - def include_helpers_from_context - @context.extended_by.each do |mod| - extend mod unless mod.name =~ /^ActionView::Helpers/ + # + # You can also use PrototypeHelper#update_page_tag instead of + # PrototypeHelper#update_page to wrap the generated JavaScript in a + # <script> tag. + module GeneratorMethods + def to_s #:nodoc: + @lines * $/ end - end - - def page - self - end - - def record(line) - returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do - self << line + + # 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(); + def [](id) + JavaScriptElementProxy.new(self, id) + 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: + # + # page.select('#items li').each do |value| + # value.hide + # end + # # => $$('#items li').each(function(value) { value.hide(); }); + # + # 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: + # + # page.select('#items li').collect('hidden') do |item| + # item.hide + # end + # # => var hidden = $$('#items li').collect(function(value, index) { return value.hide(); }); + 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 preceeding 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'. + # insert_html :before, 'content', :partial => 'navigation' + # + # # Add a list item to the bottom of the <ul> with ID 'list'. + # insert_html :bottom, 'list', '<li>Last item</li>' + # + def insert_html(position, id, *options_for_render) + insertion = position.to_s.camelize + call "new Insertion.#{insertion}", id, render(*options_for_render) + 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. + # 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. + # replace_html '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 + # page.insert_html :bottom, :partial => 'person', :object => @person + # + # # Replace an existing person + # 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. + def remove(*ids) + record "#{javascript_object_for(ids)}.each(Element.remove)" + end + + # Shows hidden DOM elements with the given +ids+. + def show(*ids) + call 'Element.show', *ids + end + + # Hides the visible DOM elements with the given +ids+. + def hide(*ids) + call 'Element.hide', *ids + end + + # Toggles the visibility of the DOM elements with the given +ids+. + def toggle(*ids) + call 'Element.toggle', *ids + end + + # Displays an alert dialog with the given +message+. + def alert(message) + call 'alert', message + end + + # Redirects the browser to the given +location+, in the same form as + # +url_for+. + def redirect_to(location) + assign 'window.location.href', @context.url_for(location) + end + + # Calls the JavaScript +function+, optionally with the given + # +arguments+. + def call(function, *arguments) + record "#{function}(#{arguments_for_call(arguments)})" + end + + # Assigns the JavaScript +variable+ the given +value+. + def assign(variable, value) + record "#{variable} = #{javascript_object_for(value)}" + end + + # Writes raw JavaScript to the page. + def <<(javascript) + @lines << javascript + end + + # Executes the content of the block after a delay of +seconds+. Example: + # + # 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 + + # 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 + + private + def page + self + end + + def record(line) + returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do + self << line + end + end + + def render(*options_for_render) + Hash === options_for_render.first ? + @context.render(*options_for_render) : + options_for_render.first.to_s + end + + def javascript_object_for(object) + object.respond_to?(:to_json) ? object.to_json : object.inspect + end + + def arguments_for_call(arguments) + arguments.map { |argument| javascript_object_for(argument) }.join ', ' end - end - - def render(*options_for_render) - Hash === options_for_render.first ? - @context.render(*options_for_render) : - options_for_render.first.to_s - end - - def javascript_object_for(object) - object.respond_to?(:to_json) ? object.to_json : object.inspect - end - - def arguments_for_call(arguments) - arguments.map { |argument| javascript_object_for(argument) }.join ', ' end end @@ -769,17 +789,19 @@ module ActionView class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc: ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :findAll, :select, :max, :min, :partition, :reject, :sortBy] ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] + attr_reader :generator + delegate :arguments_for_call, :to => :generator def initialize(generator, pattern) super(generator, @pattern = pattern) end def grep(variable, pattern, &block) - enumerable_method("grep(#{pattern.to_json}, function(value, index) {", variable, %w(value index), &block) + enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block end def inject(variable, memo, &block) - enumerable_method("inject(#{memo.to_json}, function(memo, value, index) {", variable, %w(memo value index), &block) + enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block end def pluck(variable, property) @@ -789,7 +811,7 @@ module ActionView def zip(variable, *arguments, &block) add_variable_assignment!(variable) - append_enumerable_function!("zip(#{arguments.collect { |a| a.to_json } * ', '}") + append_enumerable_function!("zip(#{arguments_for_call arguments}") if block function_chain[-1] += ", function(array) {" yield ActiveSupport::JSON::Variable.new('array') @@ -802,24 +824,36 @@ module ActionView private def method_missing(method, *arguments, &block) - ENUMERABLE_METHODS.include?(method) ? enumerate(method, ENUMERABLE_METHODS_WITH_RETURN.include?(method), &block) : super - end - - def enumerate(enumerable, variable = nil, &block) - enumerable_method("#{enumerable}(function(value, index) {", variable, %w(value index), &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 - def enumerable_method(enumerable, variable, yield_params, &block) - add_variable_assignment!(variable) if variable - append_enumerable_function!(enumerable) + # 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}(#{method_args}function(#{yield_args}) {") # only yield as many params as were passed in the block - yield *yield_params.collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1] - add_return_statement! if variable + 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("#{variable} = #{function_chain.pop}") + function_chain.push("var #{variable} = #{function_chain.pop}") end def add_return_statement! |