lib/formtastic.rb in ShadowBelmolve-formtastic-0.1.6 vs lib/formtastic.rb in ShadowBelmolve-formtastic-0.2.1

- old
+ new

@@ -16,17 +16,22 @@ @@inline_errors = :sentence @@label_str_method = :humanize @@collection_label_methods = %w[to_label display_name full_name name title username login value to_s] @@inline_order = [ :input, :hints, :errors ] @@file_methods = [ :file?, :public_filename ] + @@priority_countries = ["Australia", "Canada", "United Kingdom", "United States"] + @@i18n_lookups_by_default = false cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string, :optional_string, :inline_errors, :label_str_method, :collection_label_methods, - :inline_order, :file_methods + :inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default + I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}', + '{{model}}.{{attribute}}', + '{{attribute}}'] + # Keeps simple mappings in a hash - # INPUT_MAPPINGS = { :string => :text_field, :password => :password_field, :numeric => :text_field, :text => :text_area, @@ -40,32 +45,36 @@ # and other factors (like the method name) to figure out what you probably want. # # Options: # # * :as - override the input type (eg force a :string to render as a :password field) - # * :label - use something other than the method name as the label (or fieldset legend) text + # * :label - use something other than the method name as the label text, when false no label is printed # * :required - specify if the column is required (true) or not (false) # * :hint - provide some text to hint or help the user provide the correct information for a field # * :input_html - provide options that will be passed down to the generated input + # * :wrapper_html - provide options that will be passed down to the li wrapper # # Input Types: # # Most inputs map directly to one of ActiveRecord's column types by default (eg string_input), # but there are a few special cases and some simplification (:integer, :float and :decimal # columns all map to a single numeric_input, for example). # # * :select (a select menu for associations) - default to association names + # * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations + # * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations # * :time_zone (a select menu with time zones) - # * :radio (a set of radio inputs for associations) - default to association names # * :password (a password input) - default for :string column types with 'password' in the method name # * :text (a textarea) - default for :text column types # * :date (a date select) - default for :date column types # * :datetime (a date and time select) - default for :datetime and :timestamp column types # * :time (a time select) - default for :time column types # * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio) # * :string (a text field) - default for :string column types # * :numeric (a text field, like string) - default for :integer, :float and :decimal column types + # * :country (a select menu of country names) - requires a country_select plugin to be installed + # * :hidden (a hidden field) - creates a hidden field (added for compatibility) # # Example: # # <% semantic_form_for @employee do |form| %> # <% form.inputs do -%> @@ -75,33 +84,34 @@ # <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %> # <% end %> # <% end %> # def input(method, options = {}) - options[:required] = method_required?(method, options[:required]) + options[:required] = method_required?(method) unless options.key?(:required) options[:as] ||= default_input_type(method) - options[:label] ||= if @object && @object.class.respond_to?(:human_attribute_name) - @object.class.human_attribute_name(method.to_s) - else - method.to_s.send(@@label_str_method) - end + html_class = [ options[:as], (options[:required] ? :required : :optional) ] + html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors[method.to_sym] + wrapper_html = options.delete(:wrapper_html) || {} + wrapper_html[:id] ||= generate_html_id(method) + wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ') + if [:boolean_select, :boolean_radio].include?(options[:as]) ::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1]) end - html_class = [ options[:as], (options[:required] ? :required : :optional) ].join(' ') - html_class << ' error' if @object && @object.respond_to?(:errors) && @object.errors.on(method.to_s) + if options[:input_html] && options[:input_html][:id] + options[:label_html] ||= {} + options[:label_html][:for] ||= options[:input_html][:id] + end - html_id = generate_html_id(method) - list_item_content = @@inline_order.map do |type| send(:"inline_#{type}_for", method, options) end.compact.join("\n") - return template.content_tag(:li, list_item_content, { :id => html_id, :class => html_class }) + return template.content_tag(:li, list_item_content, wrapper_html) end # Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be # called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc), # or with a list of fields. These two examples are functionally equivalent: @@ -239,11 +249,11 @@ field_set_and_list_wrapping(html_options, &block) else if @object && args.empty? args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to } args += @object.class.content_columns.map(&:name) - args -= %w[created_at updated_at created_on updated_on] + args -= %w[created_at updated_at created_on updated_on lock_version] args.compact! end contents = args.map { |method| input(method.to_sym) } field_set_and_list_wrapping(html_options, contents) @@ -277,12 +287,18 @@ # # The value of the button text can be overridden: # # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" /> # - def commit_button(value=nil, options={}) - value ||= save_or_create_button_text + # And you can pass html atributes down to the input, with or without the button text: + # + # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" /> + # <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty" /> + + def commit_button(*args) + value = args.first.is_a?(String) ? args.shift : save_or_create_button_text + options = args.shift || {} button_html = options.delete(:button_html) || {} template.content_tag(:li, self.submit(value, button_html), :class => "commit") end # A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder @@ -309,11 +325,73 @@ opts.merge!(:builder => Formtastic::SemanticFormBuilder) args.push(opts) fields_for(record_or_name_or_array, *args, &block) end + # Generates the label for the input. It also accepts the same arguments as + # Rails label method. It has three options that are not supported by Rails + # label method: # + # * :required - Appends an abbr tag if :required is true + # * :label - An alternative form to give the label content. Whenever label + # is false, a blank string is returned. + # * :as_span - When true returns a span tag with class label instead of a label element + # * :input_name - Gives the input to match for. This is needed when you want to + # to call f.label :authors but it should match :author_ids. + # + # == Examples + # + # f.label :title # like in rails, except that it searches the label on I18n API too + # + # f.label :title, "Your post title" + # f.label :title, :label => "Your post title" # Added for formtastic API + # + # f.label :title, :required => true # Returns <label>Title<abbr title="required">*</abbr></label> + # + def label(method, options_or_text=nil, options=nil) + if options_or_text.is_a?(Hash) + return "" if options_or_text[:label] == false + options = options_or_text + text = options.delete(:label) + else + text = options_or_text + options ||= {} + end + + text = localized_attribute_string(method, text, :label) || humanized_attribute_name(method) + text += required_or_optional_string(options.delete(:required)) + + input_name = options.delete(:input_name) || method + if options.delete(:as_span) + options[:class] ||= 'label' + template.content_tag(:span, text, options) + else + super(input_name, text, options) + end + end + + # Generates error messages for the given method. Errors can be shown as list + # or as sentence. If :none is set, no error is shown. + # + # This method is also aliased as errors_on, so you can call on your custom + # inputs as well: + # + # semantic_form_for :post do |f| + # f.text_field(:body) + # f.errors_on(:body) + # end + # + def inline_errors_for(method, options=nil) #:nodoc: + return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors) + + errors = @object.errors[method.to_sym] + send("error_#{@@inline_errors}", Array(errors)) unless errors.blank? + end + alias :errors_on :inline_errors_for + + # + # # Stolen from Attribute_fu (http://github.com/giraffesoft/attribute_fu) # Rails 2.3 Patches from http://github.com/odadata/attribute_fu # # Dinamically add and remove nested forms for a has_many relation. # @@ -497,22 +575,25 @@ end output.join end end - private + protected def association_name(class_name) @object.respond_to?("#{class_name}_attributes=") ? class_name : class_name.pluralize end def extract_option_or_class_name(hash, option, object) (hash.delete(option) || object.class.name.split('::').last.underscore) end - # End attribute_fu magic # - protected + # Prepare options to be sent to label + # + def options_for_label(options) + options.slice(:label, :required).merge!(options.fetch(:label_html, {})) + end # Deals with :for option when it's supplied to inputs methods. Additional # options to be passed down to :for should be supplied using :for_options # key. # @@ -569,13 +650,11 @@ # otherwise. # # * if the :required option isn't provided, and the plugin isn't available, the value of the # configuration option @@all_fields_required_by_default is used. # - def method_required?(attribute, required_option) #:nodoc: - return required_option unless required_option.nil? - + def method_required?(attribute) #:nodoc: if @object && @object.class.respond_to?(:reflect_on_all_validations) attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym @object.class.reflect_on_all_validations.any? do |validation| validation.macro == :validates_presence_of && validation.name == attribute_sym @@ -591,15 +670,25 @@ # # If input_html is given as option, it's passed down to the input. # def input_simple(type, method, options) html_options = options.delete(:input_html) || {} - html_options = default_string_options(method).merge(html_options) if STRING_MAPPINGS.include?(type) + html_options = default_string_options(method, type).merge(html_options) if STRING_MAPPINGS.include?(type) - input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options) + self.label(method, options_for_label(options)) + + self.send(INPUT_MAPPINGS[type], method, html_options) end + # Outputs a hidden field inside the wrapper, which should be hidden with CSS. + # Additionals options can be given and will be sent straight to hidden input + # element. + # + def hidden_input(method, options) + self.hidden_field(method, set_options(options)) + end + + # Outputs a label and a select box containing options from the parent # (belongs_to, has_many, has_and_belongs_to_many) association. If an association # is has_many or has_and_belongs_to_many the select box will be set as multi-select # and size = 5 # @@ -607,46 +696,50 @@ # # f.input :author # # <label for="book_author_id">Author</label> # <select id="book_author_id" name="book[author_id]"> + # <option value=""></option> # <option value="1">Justin French</option> # <option value="2">Jane Doe</option> # </select> # # Example (has_many): # # f.input :chapters # # <label for="book_chapter_ids">Chapters</label> # <select id="book_chapter_ids" name="book[chapter_ids]"> + # <option value=""></option> # <option value="1">Chapter 1</option> # <option value="2">Chapter 2</option> # </select> # # Example (has_and_belongs_to_many): # # f.input :authors # # <label for="book_author_ids">Authors</label> # <select id="book_author_ids" name="book[author_ids]"> + # <option value=""></option> # <option value="1">Justin French</option> # <option value="2">Jane Doe</option> # </select> # # - # You can customize the options available in the select by passing in a collection (Array) of - # ActiveRecord objects through the :collection option. If not provided, the choices are found - # by inferring the parent's class name from the method name and simply calling find(:all) on - # it (VehicleOwner.find(:all) in the example above). + # You can customize the options available in the select by passing in a collection (an Array or + # Hash) through the :collection option. If not provided, the choices are found by inferring the + # parent's class name from the method name and simply calling find(:all) on it + # (VehicleOwner.find(:all) in the example above). # # Examples: # # f.input :author, :collection => @authors # f.input :author, :collection => Author.find(:all) # f.input :author, :collection => [@justin, @kate] # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id} + # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"] # # Note: This input looks for a label method in the parent association. # # You can customize the text label inside each option tag, by naming the correct method # (:full_name, :display_name, :account_number, etc) to call on each object in the collection @@ -674,33 +767,46 @@ # # Examples: # # f.input :authors, :input_html => {:size => 20, :multiple => true} # + # By default, all select inputs will have a blank option at the top of the list. You can add + # a prompt with the :prompt option, or disable the blank option with :include_blank => false. + # def select_input(method, options) collection = find_collection_for_column(method, options) html_options = options.delete(:input_html) || {} + unless options.key?(:include_blank) || options.key?(:prompt) + options[:include_blank] = true + end + reflection = find_reflection(method) if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro) + options[:include_blank] = false html_options[:multiple] ||= true html_options[:size] ||= 5 end input_name = generate_association_input_name(method) - input_label(input_name, options.delete(:label), options.slice(:required)) + + self.label(method, options_for_label(options).merge(:input_name => input_name)) + self.select(input_name, collection, set_options(options), html_options) end alias :boolean_select_input :select_input - # Output ... + # Outputs a timezone select input as Rails' time_zone_select helper. You + # can give priority zones as option. + # + # Examples: + # + # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/ + # def time_zone_input(method, options) html_options = options.delete(:input_html) || {} - input_name = generate_association_input_name(method) - input_label(input_name, options.delete(:label), options.slice(:required)) + - self.time_zone_select(input_name, options.delete(:priority_zones), set_options(options), html_options) + self.label(method, options_for_label(options)) + + self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options) end # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list # items, one for each possible choice in the belongs_to association. Each li contains a # label and a radio input. @@ -721,20 +827,21 @@ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label> # </li> # </ol> # </fieldset> # - # You can customize the options available in the set by passing in a collection (Array) of - # ActiveRecord objects through the :collection option. If not provided, the choices are found - # by inferring the parent's class name from the method name and simply calling find(:all) on - # it (VehicleOwner.find(:all) in the example above). + # You can customize the options available in the select by passing in a collection (an Array or + # Hash) through the :collection option. If not provided, the choices are found by inferring the + # parent's class name from the method name and simply calling find(:all) on it + # (Author.find(:all) in the example above). # # Examples: # # f.input :author, :as => :radio, :collection => @authors # f.input :author, :as => :radio, :collection => Author.find(:all) # f.input :author, :as => :radio, :collection => [@justin, @kate] + # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"] # # You can also customize the text label inside each option tag, by naming the correct method # (:full_name, :display_name, :account_number, etc) to call on each object in the collection # by passing in the :label_method option. By default the :label_method is whichever element of # Formtastic::SemanticFormBuilder.collection_label_methods is found first. @@ -771,13 +878,10 @@ field_set_and_list_wrapping_for_method(method, options, list_item_content) end alias :boolean_radio_input :radio_input - - - # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list # items (li), one for each fragment for the date (year, month, day). Each li contains a label # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example. # # Some of Rails' options for select_date are supported, but not everything yet. @@ -845,123 +949,160 @@ time_inputs = [:hour, :minute] time_inputs << [:second] if options[:include_seconds] list_items_capture = "" + hidden_fields_capture = "" # Gets the datetime object. It can be a Fixnum, Date or Time, or nil. datetime = @object ? @object.send(method) : nil html_options = options.delete(:input_html) || {} (inputs + time_inputs).each do |input| html_id = generate_html_id(method, "#{position[input]}i") field_name = "#{method}(#{position[input]}i)" - - list_items_capture << if options["discard_#{input}".intern] + if options["discard_#{input}".intern] break if time_inputs.include?(input) - + hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime - template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id) + hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id) else opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name) item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts]) - template.content_tag(:li, + list_items_capture << template.content_tag(:li, template.content_tag(:label, item_label_text, :for => html_id) + template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id)) ) end end - field_set_and_list_wrapping_for_method(method, options, list_items_capture) + hidden_fields_capture + field_set_and_list_wrapping_for_method(method, options, list_items_capture) end - # Add a list of checkboxes, like radio - # - # Example: - # Link - # belongs_to :author - # belongs_to :post - # - # Author - # has_many :links - # has_many :posts, :through => :links - # - # Post - # has_many :links - # has_many :authors, :through => :links - # - # # posts/new.html.erb - # <% semantic_form_for @post do |post| %> - # <%= post.input :title %> - # <h3>This post belongs to<h3> - # <%= post.input :author, :as => :check_boxes %> - # <% end %> - # - # # Output - # ... - # <li class="check_boxes required" id="post_authors_input"> - # <fieldset><legend><span class="label">Authors</span></legend> - # <ol><li> - # <label for="post_author_ids_1"> - # <input name="post[author_ids][]" type="hidden" value="" /> - # <input checked="checked" id="post_author_ids" name="post[author_ids][]" - # type="checkbox" value="1" /> Renan T. Fernandes - # </label> - # </li> - # .... - # </ol> - # </fieldset> - # </li> - # .... - # - # NOTE : Accept all radio options, plus :checked_value and :unchecked_value - # NOTEĀ²: Don't change :(un)checkd_value unless you know what you're doing ! - # + + # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list + # items, one for each possible choice in the belongs_to association. Each li contains a + # label and a check_box input. + # + # This is an alternative for has many and has and belongs to many associations. + # + # Example: + # + # f.input :author, :as => :check_boxes + # + # Output: + # + # <fieldset> + # <legend><span>Authors</span></legend> + # <ol> + # <li> + # <input type="hidden" name="book[author_id][1]" value=""> + # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label> + # </li> + # <li> + # <input type="hidden" name="book[author_id][2]" value=""> + # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label> + # </li> + # </ol> + # </fieldset> + # + # Notice that the value of the checkbox is the same as the id and the hidden + # field has empty value. You can override the hidden field value using the + # unchecked_value option. + # + # You can customize the options available in the set by passing in a collection (Array) of + # ActiveRecord objects through the :collection option. If not provided, the choices are found + # by inferring the parent's class name from the method name and simply calling find(:all) on + # it (Author.find(:all) in the example above). + # + # Examples: + # + # f.input :author, :as => :check_boxes, :collection => @authors + # f.input :author, :as => :check_boxes, :collection => Author.find(:all) + # f.input :author, :as => :check_boxes, :collection => [@justin, @kate] + # + # You can also customize the text label inside each option tag, by naming the correct method + # (:full_name, :display_name, :account_number, etc) to call on each object in the collection + # by passing in the :label_method option. By default the :label_method is whichever element of + # Formtastic::SemanticFormBuilder.collection_label_methods is found first. + # + # Examples: + # + # f.input :author, :as => :check_boxes, :label_method => :full_name + # f.input :author, :as => :check_boxes, :label_method => :display_name + # f.input :author, :as => :check_boxes, :label_method => :to_s + # f.input :author, :as => :check_boxes, :label_method => :label + # + # You can set :value_as_class => true if you want that LI wrappers contains + # a class with the wrapped checkbox input value. + # def check_boxes_input(method, options) - collection = find_collection_for_column(method, options) - html_options = set_options(options).merge(options.delete(:input_html) || {}) + collection = find_collection_for_column(method, options) + html_options = options.delete(:input_html) || {} - input_name = generate_association_input_name(method) - html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options) - value_as_class = options.delete(:value_as_class) + input_name = generate_association_input_name(method) + value_as_class = options.delete(:value_as_class) + unchecked_value = options.delete(:unchecked_value) || '' + html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options) list_item_content = collection.map do |c| label = c.is_a?(Array) ? c.first : c - value = c.is_a?(Array) ? c.last : c + value = c.is_a?(Array) ? c.last : c + html_options.merge!(:id => generate_html_id(input_name, value.to_s.downcase)) + li_content = template.content_tag(:label, - "#{self.check_box(input_name, html_options, - options.delete(:checked_value) || value, options.delete(:unchecked_value) || '') - } #{label}", :for => generate_html_id(input_name, value.to_s.downcase) + "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}", + :for => html_options[:id] ) li_options = value_as_class ? { :class => value.to_s.downcase } : {} template.content_tag(:li, li_content, li_options) end field_set_and_list_wrapping_for_method(method, options, list_item_content) end - alias :checkboxes_input :check_boxes_input - alias :check_box_input :check_boxes_input - alias :checkbox_input :check_boxes_input + + + # Outputs a country select input, wrapping around a regular country_select helper. + # Rails doesn't come with a country_select helper by default any more, so you'll need to install + # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the + # same way. + # + # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select. + # + # By default, Formtastic includes a handfull of english-speaking countries as "priority counties", + # which you can change to suit your market and user base (see README for more info on config). + # + # Examples: + # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries + # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own + # + def country_input(method, options) + raise "To use the :country input, please install a country_select plugin, like this one: http://github.com/rails/iso-3166-country-select" unless self.respond_to?(:country_select) + + html_options = options.delete(:input_html) || {} + priority_countries = options.delete(:priority_countries) || @@priority_countries + self.label(method, options_for_label(options)) + + self.country_select(method, priority_countries, set_options(options), html_options) + end + # Outputs a label containing a checkbox and the label text. The label defaults # to the column name (method name) and can be altered with the :label option. - # - # Different from other inputs, :required options has no effect here and # :checked_value and :unchecked_value options are also available. # def boolean_input(method, options) html_options = options.delete(:input_html) || {} - content = self.check_box(method, set_options(options).merge(html_options), - options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0') + input = self.check_box(method, set_options(options).merge(html_options), + options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0') - # required does not make sense in check box - input_label(method, content + options.delete(:label), :skip_required => true) + label = options.delete(:label) || humanized_attribute_name(method) + self.label(method, input + label, options_for_label(options)) end # Generates an input for the given method using the type supplied with :as. # # If the input is included in INPUT_MAPPINGS, it uses input_simple @@ -977,74 +1118,51 @@ else send("#{input_type}_input", method, options) end end - # Generates error messages for the given method. Errors can be shown as list - # or as sentence. If :none is set, no error is shown. - # - def inline_errors_for(method, options) #:nodoc: - return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors) - - # Ruby 1.9: Strings are not Enumerable, ie no String#to_a - errors = @object.errors.on(method.to_s) - unless errors.respond_to?(:to_a) - errors = [errors] - else - errors = errors.to_a - end - - send("error_#{@@inline_errors}", errors) unless errors.empty? - end - # Generates hints for the given method using the text supplied in :hint. # def inline_hints_for(method, options) #:nodoc: - options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints') + options[:hint] = localized_attribute_string(method, options[:hint], :hint) + return if options[:hint].blank? + template.content_tag(:p, options[:hint], :class => 'inline-hints') end # Creates an error sentence by calling to_sentence on the errors array. # def error_sentence(errors) #:nodoc: - template.content_tag(:p, errors.to_sentence, :class => 'inline-errors') + template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors') end # Creates an error li list. # def error_list(errors) #:nodoc: list_elements = [] errors.each do |error| - list_elements << template.content_tag(:li, error) + list_elements << template.content_tag(:li, error.untaint) end template.content_tag(:ul, list_elements.join("\n"), :class => 'errors') end - # Generates the label for the input. Accepts the same options as Rails label - # method and a fourth option that allows the label to be generated as span - # with class label. - # - def input_label(method, text, options={}, as_span=false) #:nodoc: - text << required_or_optional_string(options.delete(:required)) unless options.delete(:skip_required) - - if as_span - options[:class] ||= 'label' - template.content_tag(:span, text, options) - else - self.label(method, text, options) - end - end - # Generates the required or optional string. If the value set is a proc, # it evaluates the proc first. # def required_or_optional_string(required) #:nodoc: - string_or_proc = required ? @@required_string : @@optional_string + string_or_proc = case required + when true + @@required_string + when false + @@optional_string + else + required + end - if string_or_proc.is_a? Proc + if string_or_proc.is_a?(Proc) string_or_proc.call else - string_or_proc + string_or_proc.to_s end end # Generates a fieldset and wraps the content in an ordered list. When working # with nested attributes (in Rails 2.3), it allows %i as interpolation option @@ -1075,45 +1193,48 @@ # Also generates a fieldset and an ordered list but with label based in # method. This methods is currently used by radio and datetime inputs. # def field_set_and_list_wrapping_for_method(method, options, contents) + contents = contents.join if contents.respond_to?(:join) + template.content_tag(:fieldset, - %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} + + %{<legend>#{self.label(method, options_for_label(options).merge!(:as_span => true))}</legend>} + template.content_tag(:ol, contents) ) end - # For methods that have a database column, take a best guess as to what the inout method + # For methods that have a database column, take a best guess as to what the input method # should be. In most cases, it will just return the column type (eg :string), but for special # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do # something different (like :password and :select). # # If there is no column for the method (eg "virtual columns" with an attr_accessor), the # default is a :string, a similar behaviour to Rails' scaffolding. # def default_input_type(method) #:nodoc: - return :string if @object.nil? - - # Find the column object by attribute column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute) - # Associations map by default to a select - return :select if column.nil? && find_reflection(method) - if column # handle the special cases where the column type doesn't map to an input method - return :select if column.type == :integer && method.to_s =~ /_id$/ - return :datetime if column.type == :timestamp - return :numeric if [:integer, :float, :decimal].include?(column.type) - return :password if column.type == :string && method.to_s =~ /password/ + return :time_zone if column.type == :string && method.to_s =~ /time_zone/ + return :select if column.type == :integer && method.to_s =~ /_id$/ + return :datetime if column.type == :timestamp + return :numeric if [:integer, :float, :decimal].include?(column.type) + return :password if column.type == :string && method.to_s =~ /password/ + return :country if column.type == :string && method.to_s =~ /country/ + # otherwise assume the input name will be the same as the column type (eg string_input) return column.type else - obj = @object.send(method) if @object.respond_to?(method) + if @object + return :select if find_reflection(method) - return :file if obj && @@file_methods.any? { |m| obj.respond_to?(m) } + file = @object.send(method) if @object.respond_to?(method) + return :file if file && @@file_methods.any? { |m| file.respond_to?(m) } + end + return :password if method.to_s =~ /password/ return :string end end @@ -1171,42 +1292,47 @@ def create_boolean_collection(options) options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic]) options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic]) options[:value_as_class] = true unless options.key?(:value_as_class) - { options.delete(:true) => true, options.delete(:false) => false } + [ [ options.delete(:true), true], [ options.delete(:false), false ] ] end # Used by association inputs (select, radio) to generate the name that should # be used for the input # # belongs_to :author; f.input :author; will generate 'author_id' + # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id' # has_many :authors; f.input :authors; will generate 'author_ids' # has_and_belongs_to_many will act like has_many # def generate_association_input_name(method) if reflection = find_reflection(method) - method = "#{method.to_s.singularize}_id" - method = method.pluralize if [:has_and_belongs_to_many, :has_many].include?(reflection.macro) + if [:has_and_belongs_to_many, :has_many].include?(reflection.macro) + "#{method.to_s.singularize}_ids" + else + reflection.options[:foreign_key] || "#{method}_id" + end + else + method end - method end # If an association method is passed in (f.input :author) try to find the # reflection object. # def find_reflection(method) - object.class.reflect_on_association(method) if object.class.respond_to?(:reflect_on_association) + @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association) end # Generates default_string_options by retrieving column information from # the database. # - def default_string_options(method) #:nodoc: + def default_string_options(method, type) #:nodoc: column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute) - if column.nil? || column.limit.nil? + if type == :numeric || column.nil? || column.limit.nil? { :size => @@default_text_field_size } else { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min } end end @@ -1222,12 +1348,12 @@ elsif defined?(@auto_index) index = "_#{@auto_index}" else index = "" end - sanitized_method_name = method_name.to_s.sub(/\?$/,"") - + sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '') + "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}" end # Gets the nested_child_index value from the parent builder. In Rails 2.3 # it always returns a fixnum. In next versions it returns a hash with each @@ -1247,11 +1373,66 @@ def sanitized_object_name @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") end - end + def humanized_attribute_name(method) + if @object && @object.class.respond_to?(:human_attribute_name) + @object.class.human_attribute_name(method.to_s) + else + method.to_s.send(@@label_str_method) + end + end + # Internal generic method for looking up localized values within Formtastic + # using I18n, if no explicit value is set and I18n-lookups are enabled. + # + # Enabled/Disable this by setting: + # + # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false + # + # Lookup priority: + # + # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}' + # 'formtastic.{{type}}.{{model}}.{{attribute}}' + # 'formtastic.{{type}}.{{attribute}}' + # + # Example: + # + # 'formtastic.labels.post.edit.title' + # 'formtastic.labels.post.title' + # 'formtastic.labels.title' + # + # NOTE: Generic, but only used for form input labels/hints. + # + def localized_attribute_string(attr_name, attr_value, i18n_key) + if attr_value.is_a?(String) + attr_value + else + use_i18n = attr_value.nil? ? @@i18n_lookups_by_default : attr_value + if use_i18n + model_name = @object.class.name.underscore + action_name = template.params[:action].to_s rescue '' + attribute_name = attr_name.to_s + + defaults = I18N_SCOPES.collect do |i18n_scope| + i18n_path = i18n_scope.dup + i18n_path.gsub!('{{action}}', action_name) + i18n_path.gsub!('{{model}}', model_name) + i18n_path.gsub!('{{attribute}}', attribute_name) + i18n_path.gsub!('..', '.') + i18n_path.to_sym + end + defaults << '' + + i18n_value = ::I18n.t(defaults.shift, :default => defaults, + :scope => "formtastic.#{i18n_key.to_s.pluralize}") + i18n_value.blank? ? nil : i18n_value + end + end + end + + end # Wrappers around form_for (etc) with :builder => SemanticFormBuilder. # # * semantic_form_for(@post) # * semantic_fields_for(@post)