module FightTheMelons #:nodoc: module Helpers #:nodoc: # Some of the most used variables so you can setup them per application or # per controller instead of everytime you use one of the helpers. module FormMultipleSelectHelperConfiguration # Class used in the ul tag that wraps all the checkboxes. attr_accessor :outer_class module_function :outer_class, :outer_class= # Class used in the li tag that wraps each checkbox. attr_accessor :inner_class module_function :inner_class, :inner_class= # Prefix of the level class added to the inner class in the tree methods. attr_accessor :level_class module_function :level_class, :level_class= # Class for the alternate elements. Added to inner class. attr_accessor :alternate_class module_function :alternate_class, :alternate_class= # Determines if the methods will alternate the classes of the checkboxes. attr_accessor :alternate module_function :alternate, :alternate= # Establish the position of the checkbox with respect the label. attr_accessor :position module_function :position, :position= # Establish the name of the tags used to wrap the elements. attr_accessor :list_tags module_function :list_tags, :list_tags= # Default value for alternate is false. self.alternate = false # Default value for alternate_class is 'alt'. self.alternate_class = 'alt' # Default value for position is ':right' self.position = :right # Default value for list_tags is ['ul', 'li'] self.list_tags = ['ul', 'li'] end # Provides a number of methods for turning different kinds of containers # into checkboxes lists. module FormMultipleSelectHelper include ERB::Util include ActionView::Helpers::FormTagHelper include ActionView::Helpers::TagHelper include ActionView::Helpers # There is another content_tag in ActionView::Helpers::InstanceTag, but # we want the one in ActionView::Helpers::TagHelper. alias av_content_tag content_tag # Returns a list of checkboxes using # checkboxes_from_collection_for_multiple_select to generate the list of # checkboxes. # # If a :selected_items option is provided it will be used as # selection. :selected_items array should be an array of values # to be matched with the ones provided by value_method. # # The option :outer_class specifies the HTML class of the ul # element that wraps the checkbox list. def collection_multiple_select( object, method, collection, value_method, text_method, options = {} ) InstanceTag.new( object, method, self, nil, options.delete(:object) ).to_collection_multiple_select_tag( "#{object}[#{method}]", collection, value_method, text_method, options ) end # Same as collection_multiple_select but without an associated object. def collection_multiple_select_tag( name, collection, value_method, text_method, options = {} ) multiple_select_with_path(name, options) do |selected_items| checkboxes_from_collection_for_multiple_select(name, collection, value_method, text_method, selected_items, options ) end end # Create a list of checkboxes. See checkboxes_for_multiple_select for the # required format of the choices parameter. # # If a :selected_items option is provided it will be used as # selection. :selected_items array should be an array of values # to be matched with the ones provided by method. # # The option :outer_class specifies the HTML class of the HTML # element that wraps the checkbox list. def multiple_select(object, method, container, options = {}) InstanceTag.new( object, method, self, nil, options.delete(:object) ).to_multiple_select_tag("#{object}[#{method}]", container, options) end # Same as multiple_select but without an associated object. def multiple_select_tag(name, container, options = {}) multiple_select_with_path(name, options) do |selected_items| checkboxes_for_multiple_select( name, container, selected_items, options ) end end # Create a list of hierarchical checkboxes for the provides object and # method. # The hierarchy must respond to :child_method to get the direct # children of the actual node. The default value is children. # # If a :selected_items option is provided it will be used as # selection. :selected_items array should be an array of values # to be matched with the ones provided by value_method. # # The option :outer_class specifies the HTML class of the ul # element that wraps the checkbox hierarchy. def tree_multiple_select(object, method, nodes, value_method, text_method, options = {}) InstanceTag.new( object, method, self, nil, options.delete(:object) ).to_tree_multiple_select_tag( "#{object}[#{method}]", nodes, value_method, text_method, options ) end # Same as tree_multiple_select but without an associated object. def tree_multiple_select_tag( name, nodes, value_method, text_method, options = {} ) multiple_select_with_path(name, options) do |selected_items| checkboxes_from_tree_for_multiple_select( name, nodes, value_method, text_method, selected_items, options ) end end # Returns a string of checkboxes that have been compiled iterating over # the collection and assigning the result of a call to the # value_method as the option value and the text_method # as the label text. If selected_items is specified, the element # returning a match on value_method will get the selected # attribute in its checkbox. See checkboxes_for_multiple_select for the # allowed options in the hash. def checkboxes_from_collection_for_multiple_select( name, collection, value_method, text_method, selected_items = [], options = {} ) coll = (collection or []) checkboxes_for_multiple_select( name, coll.map { |item| [item.send(text_method), item.send(value_method)] }, selected_items, options ) end # Returns a string of checkboxes that have been compiling descending the # node and assigning the result of a call to the # value_method as the option value and the text_method # as the label text. If selected_items is specified, the element # returning a match on value_method will get the selected # attribute in its checkbox. # # The option :depth specifies the maximum depth of the nodes to # show. It defaults to infinity. # # The option :level_class is a CSS class prefix that will be # applied to the checkbox li element suffixing it with the actual depth. # # The option :initial_level is the value that will be used as suffix # for level_class option. It defaults to 0. # # The option :child_method allows access to an array of direct # children of the actual method. It defaults to "children". # # See checkboxes_for_multiple_select for more allowed options. def checkboxes_from_tree_for_multiple_select( name, nodes, value_method, text_method, selected_items = [], options = {} ) depth = (options[:depth] or -1) level_class = (options[:level_class] or FormMultipleSelectHelperConfiguration.level_class) initial_level = (options[:initial_level] or 0) child_method = (options[:child_method] or :children) inner_class = (options[:inner_class] or FormMultipleSelectHelperConfiguration.inner_class) alternate = options[:alternate].nil? ? FormMultipleSelectHelperConfiguration.alternate : options[:alternate] alt = (options[:initial_alternate] or false) if alternate root_options = options.dup if level_class root_options[:inner_class] = "#{inner_class} #{level_class}#{initial_level}".strip end root_options[:initial_alternate] = alt if alternate child_options = { :depth => depth - 1, :initial_level => initial_level + 1, :inner_class => inner_class, } child_options = options.merge(child_options) child_options[:initial_alternate] = !alt if alternate checkboxes = nodes.map do |node| parent = checkbox_for_multiple_select( name, [node.send(text_method), node.send(value_method)], selected_items, alt, root_options ) do children = node.send(child_method) branch = if not (depth == 0 || children.size == 0) "\n" + av_content_tag( FormMultipleSelectHelperConfiguration.list_tags[0], checkboxes_from_tree_for_multiple_select( name, children, value_method, text_method, selected_items, child_options ) ) else '' end if alternate alt = alt ? (not children.size%2 == 0) : (children.size%2 == 0) root_options[:initial_alternate] = alt child_options[:initial_alternate] = !alt end branch end end checkboxes.join("\n") end # Accepts a container (hash, array, enumerable, your type) and returns a # string of checkbox tags. Given a container where the elements respond to # first and last (such a two-element array), the "last" serve as checkbox # values and the "first" as label text. Hashes are turned into this form # automatically, so the keys beceome "firsts" and the values become # "lasts". If selected_items is not empty, the matching # elements will get the selected attribute in its checkbox. # # The :alternate option determines if the classes of the items # should alternate. The default value for :alternate is # false. # # The :initial_alternate option specifies if the first element # should have the alternate style or not. By default the first element # does not have the alternate style. # # If the option :position is provided the specified # position is used (:left or :right), otherwise the # default :right position is used. # # The :inner_class option specifies the base class of the HTML # element that surrounds the checkbox and the label. # # The :disabled option specifies if the checkbox will be rendered # disabled or not. Disabled can be true, false or an # array of values that will be disabled. By default the checkbox will not # be disabled. def checkboxes_for_multiple_select( name, container, selected_items = [], options = {} ) container = container.to_a if Hash === container ialternate = options[:alternate].nil? ? FormMultipleSelectHelperConfiguration.alternate : options[:alternate] alt = (options[:initial_alternate] or false) checkboxes = container.map do |item| cbfms = checkbox_for_multiple_select( name, item, selected_items, alt, options ) alt = !alt if ialternate cbfms end checkboxes.join("\n") end protected # Common body for multiple_select, collection_multiple_select and # tree_multiple_select. def multiple_select_with_path(name, options, &block) val = value(object) if respond_to? :value selected_items = options.has_key?(:selected_items) ? options[:selected_items] : val selected_items ||= [] outer_class = (options[:outer_class] or FormMultipleSelectHelperConfiguration.outer_class) checkboxes = yield(selected_items) av_content_tag( FormMultipleSelectHelperConfiguration.list_tags[0], checkboxes, :class => outer_class ) << "\n" << hidden_field_tag("#{name}[]", '', :id => nil) end private # Accepts an item and returns a checkbox tag. If the item respond to first # and last (such a two element array), the "last" serve as checkbox value # and the "first" as label text. If the item is included in the # selected_items its checkbox will be selected. The # is_alternate determines if the checkbox will use the alternate # class name or not. # # If the option :position is provided the specified # position is used (:left or :right), otherwise the # default :right position is used. # # The :inner_class option specifies the base class of the li that # surrounds the checkbox and the label. # # The :alternate_class option allow to specify # a additional class that will be used in the element if # is_alternate is true. # # The :disabled option specifies if the checkbox will be rendered # disabled or not. Disabled can be true, false or an # array of values that will be disabled. By default the checkbox will not # be disabled. def checkbox_for_multiple_select( name, item, selected_items = [], is_alternate = false, options = {} ) position = (options[:position] or FormMultipleSelectHelperConfiguration.position) inner_class = (options[:inner_class] or FormMultipleSelectHelperConfiguration.inner_class) alternate_class = (options[:alternate_class] or FormMultipleSelectHelperConfiguration.alternate_class) is_disabled = (options[:disabled] or false) if !item.is_a?(String) and item.respond_to?(:first) and item.respond_to?(:last) is_selected = selected_items.include?(item.last) is_disabled = is_disabled.include?(item.last) if is_disabled.respond_to?(:include?) item_id = idfy("#{name}#{item.last}") cbt = check_box_tag("#{name}[]", html_escape(item.last.to_s), is_selected, :id => item_id, :disabled => is_disabled) lbt = av_content_tag('label', html_escape(item.first.to_s), :for => item_id) else is_selected = selected_items.include?(item) is_disabled = is_disabled.include?(item) if is_disabled.respond_to?(:include?) item_id = idfy("#{name}#{item.to_s}") cbt = check_box_tag("#{name}[]", html_escape(item.to_s), is_selected, :id => item_id, :disabled => is_disabled) lbt = av_content_tag('label', html_escape(item.to_s), :for => item_id) end item_class = is_alternate ? "#{inner_class} #{alternate_class}".strip : inner_class extra = block_given? ? yield : '' av_content_tag( FormMultipleSelectHelperConfiguration.list_tags[1], position == :left ? lbt + cbt + extra : cbt + lbt + extra, :class => item_class ) end # Convert the string name provided into a HTML 4.01 valid # identifier. If name does not start with a letter it will be # prepend an 'x' character. All characters outside the posible characters # for a HTML identifier are replaced by an underscore ("_"). The only # valid characters are letters, digits, hyphens ("-"), underscores ("_"), # colons (":") and periods ("."). See # http://www.w3.org/TR/html4/types.html#type-name for more information. def idfy(name) name = 'x' + name unless /^[a-zA-Z]/ =~ name name.scan(/[-a-zA-Z0-9_:.]+/).join('_') end end end end module ActionView #:nodoc: module Helpers #:nodoc: class InstanceTag #:nodoc: include FightTheMelons::Helpers::FormMultipleSelectHelper include FightTheMelons::Helpers def to_multiple_select_tag(name, container, options) multiple_select_with_path(name, options) do |selected_items| checkboxes_for_multiple_select( name, container, selected_items, options ) end end def to_collection_multiple_select_tag(name, collection, value_method, text_method, options) multiple_select_with_path(name, options) do |selected_items| checkboxes_from_collection_for_multiple_select( name, collection, value_method, text_method, selected_items, options ) end end def to_tree_multiple_select_tag(name, nodes, value_method, text_method, options) multiple_select_with_path(name, options) do |selected_items| checkboxes_from_tree_for_multiple_select( name, nodes, value_method, text_method, selected_items, options ) end end end end end