ShadowBelmolve-formtastic 0.1.6 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ /* -------------------------------------------------------------------------------------------------
2
+
3
+ Load this stylesheet after formtastic.css in your layouts to override the CSS to suit your needs.
4
+ This will allow you to update formtastic.css with new releases without clobbering your own changes.
5
+
6
+ For example, to make the inline hint paragraphs a little darker in color than the standard #666:
7
+
8
+ form.formtastic fieldset ol li p.inline-hints { color:#333; }
9
+
10
+ --------------------------------------------------------------------------------------------------*/
data/lib/formtastic.rb CHANGED
@@ -18,13 +18,18 @@ module Formtastic #:nodoc:
18
18
  @@collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
19
19
  @@inline_order = [ :input, :hints, :errors ]
20
20
  @@file_methods = [ :file?, :public_filename ]
21
+ @@priority_countries = ["Australia", "Canada", "United Kingdom", "United States"]
22
+ @@i18n_lookups_by_default = false
21
23
 
22
24
  cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string,
23
25
  :optional_string, :inline_errors, :label_str_method, :collection_label_methods,
24
- :inline_order, :file_methods
26
+ :inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default
27
+
28
+ I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
29
+ '{{model}}.{{attribute}}',
30
+ '{{attribute}}']
25
31
 
26
32
  # Keeps simple mappings in a hash
27
- #
28
33
  INPUT_MAPPINGS = {
29
34
  :string => :text_field,
30
35
  :password => :password_field,
@@ -42,10 +47,11 @@ module Formtastic #:nodoc:
42
47
  # Options:
43
48
  #
44
49
  # * :as - override the input type (eg force a :string to render as a :password field)
45
- # * :label - use something other than the method name as the label (or fieldset legend) text
50
+ # * :label - use something other than the method name as the label text, when false no label is printed
46
51
  # * :required - specify if the column is required (true) or not (false)
47
52
  # * :hint - provide some text to hint or help the user provide the correct information for a field
48
53
  # * :input_html - provide options that will be passed down to the generated input
54
+ # * :wrapper_html - provide options that will be passed down to the li wrapper
49
55
  #
50
56
  # Input Types:
51
57
  #
@@ -54,8 +60,9 @@ module Formtastic #:nodoc:
54
60
  # columns all map to a single numeric_input, for example).
55
61
  #
56
62
  # * :select (a select menu for associations) - default to association names
63
+ # * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations
64
+ # * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations
57
65
  # * :time_zone (a select menu with time zones)
58
- # * :radio (a set of radio inputs for associations) - default to association names
59
66
  # * :password (a password input) - default for :string column types with 'password' in the method name
60
67
  # * :text (a textarea) - default for :text column types
61
68
  # * :date (a date select) - default for :date column types
@@ -64,6 +71,8 @@ module Formtastic #:nodoc:
64
71
  # * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
65
72
  # * :string (a text field) - default for :string column types
66
73
  # * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
74
+ # * :country (a select menu of country names) - requires a country_select plugin to be installed
75
+ # * :hidden (a hidden field) - creates a hidden field (added for compatibility)
67
76
  #
68
77
  # Example:
69
78
  #
@@ -77,29 +86,30 @@ module Formtastic #:nodoc:
77
86
  # <% end %>
78
87
  #
79
88
  def input(method, options = {})
80
- options[:required] = method_required?(method, options[:required])
89
+ options[:required] = method_required?(method) unless options.key?(:required)
81
90
  options[:as] ||= default_input_type(method)
82
91
 
83
- options[:label] ||= if @object && @object.class.respond_to?(:human_attribute_name)
84
- @object.class.human_attribute_name(method.to_s)
85
- else
86
- method.to_s.send(@@label_str_method)
87
- end
92
+ html_class = [ options[:as], (options[:required] ? :required : :optional) ]
93
+ html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors[method.to_sym]
94
+
95
+ wrapper_html = options.delete(:wrapper_html) || {}
96
+ wrapper_html[:id] ||= generate_html_id(method)
97
+ wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
88
98
 
89
99
  if [:boolean_select, :boolean_radio].include?(options[:as])
90
100
  ::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1])
91
101
  end
92
102
 
93
- html_class = [ options[:as], (options[:required] ? :required : :optional) ].join(' ')
94
- html_class << ' error' if @object && @object.respond_to?(:errors) && @object.errors.on(method.to_s)
95
-
96
- html_id = generate_html_id(method)
103
+ if options[:input_html] && options[:input_html][:id]
104
+ options[:label_html] ||= {}
105
+ options[:label_html][:for] ||= options[:input_html][:id]
106
+ end
97
107
 
98
108
  list_item_content = @@inline_order.map do |type|
99
109
  send(:"inline_#{type}_for", method, options)
100
110
  end.compact.join("\n")
101
111
 
102
- return template.content_tag(:li, list_item_content, { :id => html_id, :class => html_class })
112
+ return template.content_tag(:li, list_item_content, wrapper_html)
103
113
  end
104
114
 
105
115
  # Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
@@ -241,7 +251,7 @@ module Formtastic #:nodoc:
241
251
  if @object && args.empty?
242
252
  args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to }
243
253
  args += @object.class.content_columns.map(&:name)
244
- args -= %w[created_at updated_at created_on updated_on]
254
+ args -= %w[created_at updated_at created_on updated_on lock_version]
245
255
  args.compact!
246
256
  end
247
257
  contents = args.map { |method| input(method.to_sym) }
@@ -279,8 +289,14 @@ module Formtastic #:nodoc:
279
289
  #
280
290
  # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
281
291
  #
282
- def commit_button(value=nil, options={})
283
- value ||= save_or_create_button_text
292
+ # And you can pass html atributes down to the input, with or without the button text:
293
+ #
294
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
295
+ # <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty" />
296
+
297
+ def commit_button(*args)
298
+ value = args.first.is_a?(String) ? args.shift : save_or_create_button_text
299
+ options = args.shift || {}
284
300
  button_html = options.delete(:button_html) || {}
285
301
  template.content_tag(:li, self.submit(value, button_html), :class => "commit")
286
302
  end
@@ -311,6 +327,68 @@ module Formtastic #:nodoc:
311
327
  fields_for(record_or_name_or_array, *args, &block)
312
328
  end
313
329
 
330
+ # Generates the label for the input. It also accepts the same arguments as
331
+ # Rails label method. It has three options that are not supported by Rails
332
+ # label method:
333
+ #
334
+ # * :required - Appends an abbr tag if :required is true
335
+ # * :label - An alternative form to give the label content. Whenever label
336
+ # is false, a blank string is returned.
337
+ # * :as_span - When true returns a span tag with class label instead of a label element
338
+ # * :input_name - Gives the input to match for. This is needed when you want to
339
+ # to call f.label :authors but it should match :author_ids.
340
+ #
341
+ # == Examples
342
+ #
343
+ # f.label :title # like in rails, except that it searches the label on I18n API too
344
+ #
345
+ # f.label :title, "Your post title"
346
+ # f.label :title, :label => "Your post title" # Added for formtastic API
347
+ #
348
+ # f.label :title, :required => true # Returns <label>Title<abbr title="required">*</abbr></label>
349
+ #
350
+ def label(method, options_or_text=nil, options=nil)
351
+ if options_or_text.is_a?(Hash)
352
+ return "" if options_or_text[:label] == false
353
+ options = options_or_text
354
+ text = options.delete(:label)
355
+ else
356
+ text = options_or_text
357
+ options ||= {}
358
+ end
359
+
360
+ text = localized_attribute_string(method, text, :label) || humanized_attribute_name(method)
361
+ text += required_or_optional_string(options.delete(:required))
362
+
363
+ input_name = options.delete(:input_name) || method
364
+ if options.delete(:as_span)
365
+ options[:class] ||= 'label'
366
+ template.content_tag(:span, text, options)
367
+ else
368
+ super(input_name, text, options)
369
+ end
370
+ end
371
+
372
+ # Generates error messages for the given method. Errors can be shown as list
373
+ # or as sentence. If :none is set, no error is shown.
374
+ #
375
+ # This method is also aliased as errors_on, so you can call on your custom
376
+ # inputs as well:
377
+ #
378
+ # semantic_form_for :post do |f|
379
+ # f.text_field(:body)
380
+ # f.errors_on(:body)
381
+ # end
382
+ #
383
+ def inline_errors_for(method, options=nil) #:nodoc:
384
+ return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors)
385
+
386
+ errors = @object.errors[method.to_sym]
387
+ send("error_#{@@inline_errors}", Array(errors)) unless errors.blank?
388
+ end
389
+ alias :errors_on :inline_errors_for
390
+
391
+ #
314
392
  #
315
393
  # Stolen from Attribute_fu (http://github.com/giraffesoft/attribute_fu)
316
394
  # Rails 2.3 Patches from http://github.com/odadata/attribute_fu
@@ -499,7 +577,7 @@ module Formtastic #:nodoc:
499
577
  end
500
578
  end
501
579
 
502
- private
580
+ protected
503
581
 
504
582
  def association_name(class_name)
505
583
  @object.respond_to?("#{class_name}_attributes=") ? class_name : class_name.pluralize
@@ -508,9 +586,12 @@ module Formtastic #:nodoc:
508
586
  def extract_option_or_class_name(hash, option, object)
509
587
  (hash.delete(option) || object.class.name.split('::').last.underscore)
510
588
  end
511
- # End attribute_fu magic #
512
589
 
513
- protected
590
+ # Prepare options to be sent to label
591
+ #
592
+ def options_for_label(options)
593
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
594
+ end
514
595
 
515
596
  # Deals with :for option when it's supplied to inputs methods. Additional
516
597
  # options to be passed down to :for should be supplied using :for_options
@@ -571,9 +652,7 @@ module Formtastic #:nodoc:
571
652
  # * if the :required option isn't provided, and the plugin isn't available, the value of the
572
653
  # configuration option @@all_fields_required_by_default is used.
573
654
  #
574
- def method_required?(attribute, required_option) #:nodoc:
575
- return required_option unless required_option.nil?
576
-
655
+ def method_required?(attribute) #:nodoc:
577
656
  if @object && @object.class.respond_to?(:reflect_on_all_validations)
578
657
  attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
579
658
 
@@ -593,11 +672,21 @@ module Formtastic #:nodoc:
593
672
  #
594
673
  def input_simple(type, method, options)
595
674
  html_options = options.delete(:input_html) || {}
596
- html_options = default_string_options(method).merge(html_options) if STRING_MAPPINGS.include?(type)
675
+ html_options = default_string_options(method, type).merge(html_options) if STRING_MAPPINGS.include?(type)
597
676
 
598
- input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options)
677
+ self.label(method, options_for_label(options)) +
678
+ self.send(INPUT_MAPPINGS[type], method, html_options)
599
679
  end
600
680
 
681
+ # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
682
+ # Additionals options can be given and will be sent straight to hidden input
683
+ # element.
684
+ #
685
+ def hidden_input(method, options)
686
+ self.hidden_field(method, set_options(options))
687
+ end
688
+
689
+
601
690
  # Outputs a label and a select box containing options from the parent
602
691
  # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
603
692
  # is has_many or has_and_belongs_to_many the select box will be set as multi-select
@@ -609,6 +698,7 @@ module Formtastic #:nodoc:
609
698
  #
610
699
  # <label for="book_author_id">Author</label>
611
700
  # <select id="book_author_id" name="book[author_id]">
701
+ # <option value=""></option>
612
702
  # <option value="1">Justin French</option>
613
703
  # <option value="2">Jane Doe</option>
614
704
  # </select>
@@ -619,6 +709,7 @@ module Formtastic #:nodoc:
619
709
  #
620
710
  # <label for="book_chapter_ids">Chapters</label>
621
711
  # <select id="book_chapter_ids" name="book[chapter_ids]">
712
+ # <option value=""></option>
622
713
  # <option value="1">Chapter 1</option>
623
714
  # <option value="2">Chapter 2</option>
624
715
  # </select>
@@ -629,15 +720,16 @@ module Formtastic #:nodoc:
629
720
  #
630
721
  # <label for="book_author_ids">Authors</label>
631
722
  # <select id="book_author_ids" name="book[author_ids]">
723
+ # <option value=""></option>
632
724
  # <option value="1">Justin French</option>
633
725
  # <option value="2">Jane Doe</option>
634
726
  # </select>
635
727
  #
636
728
  #
637
- # You can customize the options available in the select by passing in a collection (Array) of
638
- # ActiveRecord objects through the :collection option. If not provided, the choices are found
639
- # by inferring the parent's class name from the method name and simply calling find(:all) on
640
- # it (VehicleOwner.find(:all) in the example above).
729
+ # You can customize the options available in the select by passing in a collection (an Array or
730
+ # Hash) through the :collection option. If not provided, the choices are found by inferring the
731
+ # parent's class name from the method name and simply calling find(:all) on it
732
+ # (VehicleOwner.find(:all) in the example above).
641
733
  #
642
734
  # Examples:
643
735
  #
@@ -645,6 +737,7 @@ module Formtastic #:nodoc:
645
737
  # f.input :author, :collection => Author.find(:all)
646
738
  # f.input :author, :collection => [@justin, @kate]
647
739
  # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
740
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
648
741
  #
649
742
  # Note: This input looks for a label method in the parent association.
650
743
  #
@@ -676,29 +769,42 @@ module Formtastic #:nodoc:
676
769
  #
677
770
  # f.input :authors, :input_html => {:size => 20, :multiple => true}
678
771
  #
772
+ # By default, all select inputs will have a blank option at the top of the list. You can add
773
+ # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
774
+ #
679
775
  def select_input(method, options)
680
776
  collection = find_collection_for_column(method, options)
681
777
  html_options = options.delete(:input_html) || {}
682
778
 
779
+ unless options.key?(:include_blank) || options.key?(:prompt)
780
+ options[:include_blank] = true
781
+ end
782
+
683
783
  reflection = find_reflection(method)
684
784
  if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
785
+ options[:include_blank] = false
685
786
  html_options[:multiple] ||= true
686
787
  html_options[:size] ||= 5
687
788
  end
688
789
 
689
790
  input_name = generate_association_input_name(method)
690
- input_label(input_name, options.delete(:label), options.slice(:required)) +
791
+ self.label(method, options_for_label(options).merge(:input_name => input_name)) +
691
792
  self.select(input_name, collection, set_options(options), html_options)
692
793
  end
693
794
  alias :boolean_select_input :select_input
694
795
 
695
- # Output ...
796
+ # Outputs a timezone select input as Rails' time_zone_select helper. You
797
+ # can give priority zones as option.
798
+ #
799
+ # Examples:
800
+ #
801
+ # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
802
+ #
696
803
  def time_zone_input(method, options)
697
804
  html_options = options.delete(:input_html) || {}
698
805
 
699
- input_name = generate_association_input_name(method)
700
- input_label(input_name, options.delete(:label), options.slice(:required)) +
701
- self.time_zone_select(input_name, options.delete(:priority_zones), set_options(options), html_options)
806
+ self.label(method, options_for_label(options)) +
807
+ self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options)
702
808
  end
703
809
 
704
810
  # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
@@ -723,16 +829,17 @@ module Formtastic #:nodoc:
723
829
  # </ol>
724
830
  # </fieldset>
725
831
  #
726
- # You can customize the options available in the set by passing in a collection (Array) of
727
- # ActiveRecord objects through the :collection option. If not provided, the choices are found
728
- # by inferring the parent's class name from the method name and simply calling find(:all) on
729
- # it (VehicleOwner.find(:all) in the example above).
832
+ # You can customize the options available in the select by passing in a collection (an Array or
833
+ # Hash) through the :collection option. If not provided, the choices are found by inferring the
834
+ # parent's class name from the method name and simply calling find(:all) on it
835
+ # (Author.find(:all) in the example above).
730
836
  #
731
837
  # Examples:
732
838
  #
733
839
  # f.input :author, :as => :radio, :collection => @authors
734
840
  # f.input :author, :as => :radio, :collection => Author.find(:all)
735
841
  # f.input :author, :as => :radio, :collection => [@justin, @kate]
842
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
736
843
  #
737
844
  # You can also customize the text label inside each option tag, by naming the correct method
738
845
  # (:full_name, :display_name, :account_number, etc) to call on each object in the collection
@@ -773,9 +880,6 @@ module Formtastic #:nodoc:
773
880
  end
774
881
  alias :boolean_radio_input :radio_input
775
882
 
776
-
777
-
778
-
779
883
  # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
780
884
  # items (li), one for each fragment for the date (year, month, day). Each li contains a label
781
885
  # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
@@ -847,6 +951,7 @@ module Formtastic #:nodoc:
847
951
  time_inputs << [:second] if options[:include_seconds]
848
952
 
849
953
  list_items_capture = ""
954
+ hidden_fields_capture = ""
850
955
 
851
956
  # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
852
957
  datetime = @object ? @object.send(method) : nil
@@ -855,84 +960,100 @@ module Formtastic #:nodoc:
855
960
  (inputs + time_inputs).each do |input|
856
961
  html_id = generate_html_id(method, "#{position[input]}i")
857
962
  field_name = "#{method}(#{position[input]}i)"
858
-
859
- list_items_capture << if options["discard_#{input}".intern]
963
+ if options["discard_#{input}".intern]
860
964
  break if time_inputs.include?(input)
861
-
965
+
862
966
  hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
863
- template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
967
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
864
968
  else
865
969
  opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
866
970
  item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
867
971
 
868
- template.content_tag(:li,
972
+ list_items_capture << template.content_tag(:li,
869
973
  template.content_tag(:label, item_label_text, :for => html_id) +
870
974
  template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id))
871
975
  )
872
976
  end
873
977
  end
874
978
 
875
- field_set_and_list_wrapping_for_method(method, options, list_items_capture)
979
+ hidden_fields_capture + field_set_and_list_wrapping_for_method(method, options, list_items_capture)
876
980
  end
877
981
 
878
- # Add a list of checkboxes, like radio
879
- #
880
- # Example:
881
- # Link
882
- # belongs_to :author
883
- # belongs_to :post
884
- #
885
- # Author
886
- # has_many :links
887
- # has_many :posts, :through => :links
888
- #
889
- # Post
890
- # has_many :links
891
- # has_many :authors, :through => :links
892
- #
893
- # # posts/new.html.erb
894
- # <% semantic_form_for @post do |post| %>
895
- # <%= post.input :title %>
896
- # <h3>This post belongs to<h3>
897
- # <%= post.input :author, :as => :check_boxes %>
898
- # <% end %>
899
- #
900
- # # Output
901
- # ...
902
- # <li class="check_boxes required" id="post_authors_input">
903
- # <fieldset><legend><span class="label">Authors</span></legend>
904
- # <ol><li>
905
- # <label for="post_author_ids_1">
906
- # <input name="post[author_ids][]" type="hidden" value="" />
907
- # <input checked="checked" id="post_author_ids" name="post[author_ids][]"
908
- # type="checkbox" value="1" /> Renan T. Fernandes
909
- # </label>
910
- # </li>
911
- # ....
912
- # </ol>
913
- # </fieldset>
914
- # </li>
915
- # ....
916
- #
917
- # NOTE : Accept all radio options, plus :checked_value and :unchecked_value
918
- # NOTE²: Don't change :(un)checkd_value unless you know what you're doing !
919
- #
982
+
983
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
984
+ # items, one for each possible choice in the belongs_to association. Each li contains a
985
+ # label and a check_box input.
986
+ #
987
+ # This is an alternative for has many and has and belongs to many associations.
988
+ #
989
+ # Example:
990
+ #
991
+ # f.input :author, :as => :check_boxes
992
+ #
993
+ # Output:
994
+ #
995
+ # <fieldset>
996
+ # <legend><span>Authors</span></legend>
997
+ # <ol>
998
+ # <li>
999
+ # <input type="hidden" name="book[author_id][1]" value="">
1000
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
1001
+ # </li>
1002
+ # <li>
1003
+ # <input type="hidden" name="book[author_id][2]" value="">
1004
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
1005
+ # </li>
1006
+ # </ol>
1007
+ # </fieldset>
1008
+ #
1009
+ # Notice that the value of the checkbox is the same as the id and the hidden
1010
+ # field has empty value. You can override the hidden field value using the
1011
+ # unchecked_value option.
1012
+ #
1013
+ # You can customize the options available in the set by passing in a collection (Array) of
1014
+ # ActiveRecord objects through the :collection option. If not provided, the choices are found
1015
+ # by inferring the parent's class name from the method name and simply calling find(:all) on
1016
+ # it (Author.find(:all) in the example above).
1017
+ #
1018
+ # Examples:
1019
+ #
1020
+ # f.input :author, :as => :check_boxes, :collection => @authors
1021
+ # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
1022
+ # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
1023
+ #
1024
+ # You can also customize the text label inside each option tag, by naming the correct method
1025
+ # (:full_name, :display_name, :account_number, etc) to call on each object in the collection
1026
+ # by passing in the :label_method option. By default the :label_method is whichever element of
1027
+ # Formtastic::SemanticFormBuilder.collection_label_methods is found first.
1028
+ #
1029
+ # Examples:
1030
+ #
1031
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
1032
+ # f.input :author, :as => :check_boxes, :label_method => :display_name
1033
+ # f.input :author, :as => :check_boxes, :label_method => :to_s
1034
+ # f.input :author, :as => :check_boxes, :label_method => :label
1035
+ #
1036
+ # You can set :value_as_class => true if you want that LI wrappers contains
1037
+ # a class with the wrapped checkbox input value.
1038
+ #
920
1039
  def check_boxes_input(method, options)
921
- collection = find_collection_for_column(method, options)
922
- html_options = set_options(options).merge(options.delete(:input_html) || {})
1040
+ collection = find_collection_for_column(method, options)
1041
+ html_options = options.delete(:input_html) || {}
923
1042
 
924
- input_name = generate_association_input_name(method)
925
- html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
926
- value_as_class = options.delete(:value_as_class)
1043
+ input_name = generate_association_input_name(method)
1044
+ value_as_class = options.delete(:value_as_class)
1045
+ unchecked_value = options.delete(:unchecked_value) || ''
1046
+ html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
927
1047
 
928
1048
  list_item_content = collection.map do |c|
929
1049
  label = c.is_a?(Array) ? c.first : c
930
- value = c.is_a?(Array) ? c.last : c
1050
+ value = c.is_a?(Array) ? c.last : c
931
1051
 
1052
+ html_options.merge!(:id => generate_html_id(input_name, value.to_s.downcase))
1053
+
932
1054
  li_content = template.content_tag(:label,
933
- "#{self.check_box(input_name, html_options,
934
- options.delete(:checked_value) || value, options.delete(:unchecked_value) || '')
935
- } #{label}", :for => generate_html_id(input_name, value.to_s.downcase)
1055
+ "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
1056
+ :for => html_options[:id]
936
1057
  )
937
1058
 
938
1059
  li_options = value_as_class ? { :class => value.to_s.downcase } : {}
@@ -941,25 +1062,45 @@ module Formtastic #:nodoc:
941
1062
 
942
1063
  field_set_and_list_wrapping_for_method(method, options, list_item_content)
943
1064
  end
944
- alias :checkboxes_input :check_boxes_input
945
- alias :check_box_input :check_boxes_input
946
- alias :checkbox_input :check_boxes_input
1065
+
1066
+
1067
+ # Outputs a country select input, wrapping around a regular country_select helper.
1068
+ # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1069
+ # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1070
+ # same way.
1071
+ #
1072
+ # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1073
+ #
1074
+ # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1075
+ # which you can change to suit your market and user base (see README for more info on config).
1076
+ #
1077
+ # Examples:
1078
+ # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1079
+ # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1080
+ #
1081
+ def country_input(method, options)
1082
+ 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)
1083
+
1084
+ html_options = options.delete(:input_html) || {}
1085
+ priority_countries = options.delete(:priority_countries) || @@priority_countries
947
1086
 
1087
+ self.label(method, options_for_label(options)) +
1088
+ self.country_select(method, priority_countries, set_options(options), html_options)
1089
+ end
1090
+
948
1091
 
949
1092
  # Outputs a label containing a checkbox and the label text. The label defaults
950
1093
  # to the column name (method name) and can be altered with the :label option.
951
- #
952
- # Different from other inputs, :required options has no effect here and
953
1094
  # :checked_value and :unchecked_value options are also available.
954
1095
  #
955
1096
  def boolean_input(method, options)
956
1097
  html_options = options.delete(:input_html) || {}
957
1098
 
958
- content = self.check_box(method, set_options(options).merge(html_options),
959
- options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
1099
+ input = self.check_box(method, set_options(options).merge(html_options),
1100
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
960
1101
 
961
- # required does not make sense in check box
962
- input_label(method, content + options.delete(:label), :skip_required => true)
1102
+ label = options.delete(:label) || humanized_attribute_name(method)
1103
+ self.label(method, input + label, options_for_label(options))
963
1104
  end
964
1105
 
965
1106
  # Generates an input for the given method using the type supplied with :as.
@@ -979,33 +1120,18 @@ module Formtastic #:nodoc:
979
1120
  end
980
1121
  end
981
1122
 
982
- # Generates error messages for the given method. Errors can be shown as list
983
- # or as sentence. If :none is set, no error is shown.
984
- #
985
- def inline_errors_for(method, options) #:nodoc:
986
- return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors)
987
-
988
- # Ruby 1.9: Strings are not Enumerable, ie no String#to_a
989
- errors = @object.errors.on(method.to_s)
990
- unless errors.respond_to?(:to_a)
991
- errors = [errors]
992
- else
993
- errors = errors.to_a
994
- end
995
-
996
- send("error_#{@@inline_errors}", errors) unless errors.empty?
997
- end
998
-
999
1123
  # Generates hints for the given method using the text supplied in :hint.
1000
1124
  #
1001
1125
  def inline_hints_for(method, options) #:nodoc:
1002
- options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints')
1126
+ options[:hint] = localized_attribute_string(method, options[:hint], :hint)
1127
+ return if options[:hint].blank?
1128
+ template.content_tag(:p, options[:hint], :class => 'inline-hints')
1003
1129
  end
1004
1130
 
1005
1131
  # Creates an error sentence by calling to_sentence on the errors array.
1006
1132
  #
1007
1133
  def error_sentence(errors) #:nodoc:
1008
- template.content_tag(:p, errors.to_sentence, :class => 'inline-errors')
1134
+ template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
1009
1135
  end
1010
1136
 
1011
1137
  # Creates an error li list.
@@ -1013,36 +1139,28 @@ module Formtastic #:nodoc:
1013
1139
  def error_list(errors) #:nodoc:
1014
1140
  list_elements = []
1015
1141
  errors.each do |error|
1016
- list_elements << template.content_tag(:li, error)
1142
+ list_elements << template.content_tag(:li, error.untaint)
1017
1143
  end
1018
1144
  template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
1019
1145
  end
1020
1146
 
1021
- # Generates the label for the input. Accepts the same options as Rails label
1022
- # method and a fourth option that allows the label to be generated as span
1023
- # with class label.
1024
- #
1025
- def input_label(method, text, options={}, as_span=false) #:nodoc:
1026
- text << required_or_optional_string(options.delete(:required)) unless options.delete(:skip_required)
1027
-
1028
- if as_span
1029
- options[:class] ||= 'label'
1030
- template.content_tag(:span, text, options)
1031
- else
1032
- self.label(method, text, options)
1033
- end
1034
- end
1035
-
1036
1147
  # Generates the required or optional string. If the value set is a proc,
1037
1148
  # it evaluates the proc first.
1038
1149
  #
1039
1150
  def required_or_optional_string(required) #:nodoc:
1040
- string_or_proc = required ? @@required_string : @@optional_string
1151
+ string_or_proc = case required
1152
+ when true
1153
+ @@required_string
1154
+ when false
1155
+ @@optional_string
1156
+ else
1157
+ required
1158
+ end
1041
1159
 
1042
- if string_or_proc.is_a? Proc
1160
+ if string_or_proc.is_a?(Proc)
1043
1161
  string_or_proc.call
1044
1162
  else
1045
- string_or_proc
1163
+ string_or_proc.to_s
1046
1164
  end
1047
1165
  end
1048
1166
 
@@ -1077,13 +1195,15 @@ module Formtastic #:nodoc:
1077
1195
  # method. This methods is currently used by radio and datetime inputs.
1078
1196
  #
1079
1197
  def field_set_and_list_wrapping_for_method(method, options, contents)
1198
+ contents = contents.join if contents.respond_to?(:join)
1199
+
1080
1200
  template.content_tag(:fieldset,
1081
- %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} +
1201
+ %{<legend>#{self.label(method, options_for_label(options).merge!(:as_span => true))}</legend>} +
1082
1202
  template.content_tag(:ol, contents)
1083
1203
  )
1084
1204
  end
1085
1205
 
1086
- # For methods that have a database column, take a best guess as to what the inout method
1206
+ # For methods that have a database column, take a best guess as to what the input method
1087
1207
  # should be. In most cases, it will just return the column type (eg :string), but for special
1088
1208
  # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1089
1209
  # something different (like :password and :select).
@@ -1092,26 +1212,27 @@ module Formtastic #:nodoc:
1092
1212
  # default is a :string, a similar behaviour to Rails' scaffolding.
1093
1213
  #
1094
1214
  def default_input_type(method) #:nodoc:
1095
- return :string if @object.nil?
1096
-
1097
- # Find the column object by attribute
1098
1215
  column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1099
1216
 
1100
- # Associations map by default to a select
1101
- return :select if column.nil? && find_reflection(method)
1102
-
1103
1217
  if column
1104
1218
  # handle the special cases where the column type doesn't map to an input method
1105
- return :select if column.type == :integer && method.to_s =~ /_id$/
1106
- return :datetime if column.type == :timestamp
1107
- return :numeric if [:integer, :float, :decimal].include?(column.type)
1108
- return :password if column.type == :string && method.to_s =~ /password/
1219
+ return :time_zone if column.type == :string && method.to_s =~ /time_zone/
1220
+ return :select if column.type == :integer && method.to_s =~ /_id$/
1221
+ return :datetime if column.type == :timestamp
1222
+ return :numeric if [:integer, :float, :decimal].include?(column.type)
1223
+ return :password if column.type == :string && method.to_s =~ /password/
1224
+ return :country if column.type == :string && method.to_s =~ /country/
1225
+
1109
1226
  # otherwise assume the input name will be the same as the column type (eg string_input)
1110
1227
  return column.type
1111
1228
  else
1112
- obj = @object.send(method) if @object.respond_to?(method)
1229
+ if @object
1230
+ return :select if find_reflection(method)
1231
+
1232
+ file = @object.send(method) if @object.respond_to?(method)
1233
+ return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
1234
+ end
1113
1235
 
1114
- return :file if obj && @@file_methods.any? { |m| obj.respond_to?(m) }
1115
1236
  return :password if method.to_s =~ /password/
1116
1237
  return :string
1117
1238
  end
@@ -1173,38 +1294,43 @@ module Formtastic #:nodoc:
1173
1294
  options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic])
1174
1295
  options[:value_as_class] = true unless options.key?(:value_as_class)
1175
1296
 
1176
- { options.delete(:true) => true, options.delete(:false) => false }
1297
+ [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1177
1298
  end
1178
1299
 
1179
1300
  # Used by association inputs (select, radio) to generate the name that should
1180
1301
  # be used for the input
1181
1302
  #
1182
1303
  # belongs_to :author; f.input :author; will generate 'author_id'
1304
+ # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1183
1305
  # has_many :authors; f.input :authors; will generate 'author_ids'
1184
1306
  # has_and_belongs_to_many will act like has_many
1185
1307
  #
1186
1308
  def generate_association_input_name(method)
1187
1309
  if reflection = find_reflection(method)
1188
- method = "#{method.to_s.singularize}_id"
1189
- method = method.pluralize if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1310
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1311
+ "#{method.to_s.singularize}_ids"
1312
+ else
1313
+ reflection.options[:foreign_key] || "#{method}_id"
1314
+ end
1315
+ else
1316
+ method
1190
1317
  end
1191
- method
1192
1318
  end
1193
1319
 
1194
1320
  # If an association method is passed in (f.input :author) try to find the
1195
1321
  # reflection object.
1196
1322
  #
1197
1323
  def find_reflection(method)
1198
- object.class.reflect_on_association(method) if object.class.respond_to?(:reflect_on_association)
1324
+ @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1199
1325
  end
1200
1326
 
1201
1327
  # Generates default_string_options by retrieving column information from
1202
1328
  # the database.
1203
1329
  #
1204
- def default_string_options(method) #:nodoc:
1330
+ def default_string_options(method, type) #:nodoc:
1205
1331
  column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1206
1332
 
1207
- if column.nil? || column.limit.nil?
1333
+ if type == :numeric || column.nil? || column.limit.nil?
1208
1334
  { :size => @@default_text_field_size }
1209
1335
  else
1210
1336
  { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
@@ -1224,8 +1350,8 @@ module Formtastic #:nodoc:
1224
1350
  else
1225
1351
  index = ""
1226
1352
  end
1227
- sanitized_method_name = method_name.to_s.sub(/\?$/,"")
1228
-
1353
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1354
+
1229
1355
  "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1230
1356
  end
1231
1357
 
@@ -1249,8 +1375,63 @@ module Formtastic #:nodoc:
1249
1375
  @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1250
1376
  end
1251
1377
 
1252
- end
1378
+ def humanized_attribute_name(method)
1379
+ if @object && @object.class.respond_to?(:human_attribute_name)
1380
+ @object.class.human_attribute_name(method.to_s)
1381
+ else
1382
+ method.to_s.send(@@label_str_method)
1383
+ end
1384
+ end
1253
1385
 
1386
+ # Internal generic method for looking up localized values within Formtastic
1387
+ # using I18n, if no explicit value is set and I18n-lookups are enabled.
1388
+ #
1389
+ # Enabled/Disable this by setting:
1390
+ #
1391
+ # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1392
+ #
1393
+ # Lookup priority:
1394
+ #
1395
+ # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
1396
+ # 'formtastic.{{type}}.{{model}}.{{attribute}}'
1397
+ # 'formtastic.{{type}}.{{attribute}}'
1398
+ #
1399
+ # Example:
1400
+ #
1401
+ # 'formtastic.labels.post.edit.title'
1402
+ # 'formtastic.labels.post.title'
1403
+ # 'formtastic.labels.title'
1404
+ #
1405
+ # NOTE: Generic, but only used for form input labels/hints.
1406
+ #
1407
+ def localized_attribute_string(attr_name, attr_value, i18n_key)
1408
+ if attr_value.is_a?(String)
1409
+ attr_value
1410
+ else
1411
+ use_i18n = attr_value.nil? ? @@i18n_lookups_by_default : attr_value
1412
+ if use_i18n
1413
+ model_name = @object.class.name.underscore
1414
+ action_name = template.params[:action].to_s rescue ''
1415
+ attribute_name = attr_name.to_s
1416
+
1417
+ defaults = I18N_SCOPES.collect do |i18n_scope|
1418
+ i18n_path = i18n_scope.dup
1419
+ i18n_path.gsub!('{{action}}', action_name)
1420
+ i18n_path.gsub!('{{model}}', model_name)
1421
+ i18n_path.gsub!('{{attribute}}', attribute_name)
1422
+ i18n_path.gsub!('..', '.')
1423
+ i18n_path.to_sym
1424
+ end
1425
+ defaults << ''
1426
+
1427
+ i18n_value = ::I18n.t(defaults.shift, :default => defaults,
1428
+ :scope => "formtastic.#{i18n_key.to_s.pluralize}")
1429
+ i18n_value.blank? ? nil : i18n_value
1430
+ end
1431
+ end
1432
+ end
1433
+
1434
+ end
1254
1435
 
1255
1436
  # Wrappers around form_for (etc) with :builder => SemanticFormBuilder.
1256
1437
  #