formtastic 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -446,7 +446,22 @@ $ ./script/generate form Post --partial
446
446
  create app/views/posts/_form.html.erb
447
447
  </pre>
448
448
 
449
- To generate *HAML* markup, just add the @--haml@ as argument: @./script/generate form Post --haml@
449
+ To generate *HAML* markup, just add the @--haml@ as argument:
450
+
451
+ <pre>
452
+ $ ./script/generate form Post --haml
453
+ exists app/views/admin/posts
454
+ create app/views/admin/posts/_form.html.haml
455
+ </pre>
456
+
457
+ To specify the controller in a namespace (eg admin/posts instead of posts), use the --controller argument:
458
+
459
+ <pre>
460
+ $ ./script/generate form Post --partial --controller admin/posts
461
+ exists app/views/admin/posts
462
+ create app/views/admin/posts/_form.html.erb
463
+ </pre>
464
+
450
465
 
451
466
  h2. Custom Inputs
452
467
 
@@ -455,6 +470,8 @@ If you want to add your own input types to encapsulate your own logic or interfa
455
470
  @Formtastic::SemanticFormHelper.builder = MyCustomBuilder@
456
471
 
457
472
 
473
+
474
+
458
475
  h2. Status
459
476
 
460
477
  Formtastic has been in active development for about a year. We've just recently jumped to an 0.9 version number, signaling that we consider this a 1.0 release candidate, and that the API won't change significantly for the 1.x series.
@@ -464,7 +481,7 @@ h2. Dependencies
464
481
 
465
482
  There are none, but...
466
483
 
467
- * if you have the "ValidationReflection":http://github.com/redinger/validation_reflection plugin is installed, you won't have to specify the :required option (it checks the validations on the model instead).
484
+ * if you have the "ValidationReflection":http://github.com/redinger/validation_reflection plugin is installed, you won't have to specify the @:required@ option (it checks the validations on the model instead).
468
485
  * if you want to use the @:country@ input, you'll need to install the "iso-3166-country-select plugin":http://github.com/rails/iso-3166-country-select (or any other country_select plugin with the same API).
469
486
  * "rspec":http://github.com/dchelimsky/rspec/, "rspec_hpricot_matchers":http://rubyforge.org/projects/rspec-hpricot/ and "rcov":http://github.com/relevance/rcov gems (plus any of their own dependencies) are required for the test suite.
470
487
 
data/Rakefile CHANGED
@@ -23,7 +23,7 @@ begin
23
23
  ========================================================================
24
24
  Thanks for installing Formtastic!
25
25
  ------------------------------------------------------------------------
26
- You can now (optionally) run the generater to copy some stylesheets and
26
+ You can now (optionally) run the generator to copy some stylesheets and
27
27
  a config initializer into your application:
28
28
  ./script/generate formtastic
29
29
 
@@ -59,13 +59,12 @@ begin
59
59
  # Development dependencies. Not installed by default.
60
60
  # Install with: sudo gem install formtastic --development
61
61
  s.add_development_dependency 'rspec-rails', '>= 1.2.6'
62
- s.add_development_dependency 'hpricot', '>= 0.6.1' # for: rspec_hpricot_matchers
63
- s.add_development_dependency 'rspec_hpricot_matchers', '>= 1.0.0'
62
+ s.add_development_dependency 'rspec_tag_matchers', '>= 1.0.0'
64
63
  end
65
64
 
66
65
  Jeweler::GemcutterTasks.new
67
66
  rescue LoadError
68
- puts "[formtastic:] Jeweler - or one of it's dependencies - is not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
67
+ puts "[formtastic:] Jeweler - or one of its dependencies - is not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
69
68
  end
70
69
 
71
70
  desc 'Default: run unit specs.'
@@ -39,10 +39,11 @@ class FormGenerator < Rails::Generator::NamedBase
39
39
  def manifest
40
40
  record do |m|
41
41
  if options[:partial]
42
+ controller_and_view_path = options[:controller] || File.join(controller_class_path, controller_file_name)
42
43
  # Ensure directory exists.
43
- m.directory File.join(VIEWS_PATH, controller_class_path, controller_file_name)
44
+ m.directory File.join(VIEWS_PATH, controller_and_view_path)
44
45
  # Create a form partial for the model as "_form" in it's views path.
45
- m.template "view__form.html.#{template_type}", File.join(VIEWS_PATH, controller_file_name, "_form.html.#{template_type}")
46
+ m.template "view__form.html.#{template_type}", File.join(VIEWS_PATH, controller_and_view_path, "_form.html.#{template_type}")
46
47
  else
47
48
  # Load template file, and render without saving to file
48
49
  template = File.read(File.join(source_root, "view__form.html.#{template_type}"))
@@ -85,7 +86,7 @@ class FormGenerator < Rails::Generator::NamedBase
85
86
 
86
87
  # Add additional model attributes if specified in args - probably not that common scenario.
87
88
  def attributes
88
- # Get columns for the requested model
89
+ # Get columns for the requested model.
89
90
  existing_attributes = @class_name.constantize.content_columns.reject { |column| IGNORED_COLUMNS.include?(column.name.to_sym) }
90
91
  @args = super + existing_attributes
91
92
  end
@@ -105,6 +106,11 @@ class FormGenerator < Rails::Generator::NamedBase
105
106
  "Save generated output directly to a form partial (app/views/{resource}/_form.html.*).") do |v|
106
107
  options[:partial] = v
107
108
  end
109
+
110
+ opt.on('--controller CONTROLLER_PATH',
111
+ "Specify a non-standard controller for the specified model (e.g. admin/posts).") do |v|
112
+ options[:controller] = v if v.present?
113
+ end
108
114
  end
109
115
 
110
116
  def banner
@@ -27,10 +27,6 @@ module Formtastic #:nodoc:
27
27
 
28
28
  INLINE_ERROR_TYPES = [:sentence, :list, :first]
29
29
 
30
- I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
31
- '{{model}}.{{attribute}}',
32
- '{{attribute}}']
33
-
34
30
  attr_accessor :template
35
31
 
36
32
  # Returns a suitable form input for the given +method+, using the database column information
@@ -80,7 +76,7 @@ module Formtastic #:nodoc:
80
76
  #
81
77
  def input(method, options = {})
82
78
  options[:required] = method_required?(method) unless options.key?(:required)
83
- options[:as] ||= default_input_type(method)
79
+ options[:as] ||= default_input_type(method, options)
84
80
 
85
81
  html_class = [ options[:as], (options[:required] ? :required : :optional) ]
86
82
  html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
@@ -96,7 +92,7 @@ module Formtastic #:nodoc:
96
92
 
97
93
  input_parts = @@inline_order.dup
98
94
  input_parts.delete(:errors) if options[:as] == :hidden
99
-
95
+
100
96
  list_item_content = input_parts.map do |type|
101
97
  send(:"inline_#{type}_for", method, options)
102
98
  end.compact.join("\n")
@@ -251,11 +247,13 @@ module Formtastic #:nodoc:
251
247
  # instead (just as you would do with Rails' form builder).
252
248
  #
253
249
  def inputs(*args, &block)
250
+ title = field_set_title_from_args(*args)
254
251
  html_options = args.extract_options!
255
252
  html_options[:class] ||= "inputs"
256
-
257
- if html_options[:for]
258
- inputs_for_nested_attributes(args, html_options, &block)
253
+ html_options[:name] = title
254
+
255
+ if html_options[:for] # Nested form
256
+ inputs_for_nested_attributes(*(args << html_options), &block)
259
257
  elsif block_given?
260
258
  field_set_and_list_wrapping(*(args << html_options), &block)
261
259
  else
@@ -319,7 +317,6 @@ module Formtastic #:nodoc:
319
317
  key = :submit
320
318
  object_name = @object_name.to_s.send(@@label_str_method)
321
319
  end
322
- fallback_text ||= "#{key.to_s.humanize} {{model}}"
323
320
 
324
321
  text = (self.localized_string(key, text, :action, :model => object_name) ||
325
322
  ::Formtastic::I18n.t(key, :model => object_name)) unless text.is_a?(::String)
@@ -353,7 +350,7 @@ module Formtastic #:nodoc:
353
350
  #
354
351
  def semantic_fields_for(record_or_name_or_array, *args, &block)
355
352
  opts = args.extract_options!
356
- opts.merge!(:builder => ::Formtastic::SemanticFormHelper.builder)
353
+ opts[:builder] ||= Formtastic::SemanticFormHelper.builder
357
354
  args.push(opts)
358
355
  fields_for(record_or_name_or_array, *args, &block)
359
356
  end
@@ -407,1018 +404,1150 @@ module Formtastic #:nodoc:
407
404
  # f.errors_on(:body)
408
405
  # end
409
406
  #
410
- def inline_errors_for(method, options=nil) #:nodoc:
411
- return nil unless @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(@@inline_errors)
412
-
413
- errors = @object.errors[method.to_sym]
414
- send(:"error_#{@@inline_errors}", Array(errors)) unless errors.blank?
407
+ def inline_errors_for(method, options = nil) #:nodoc:
408
+ if render_inline_errors?
409
+ errors = @object.errors[method.to_sym]
410
+ send(:"error_#{@@inline_errors}", [*errors]) if errors.present?
411
+ else
412
+ nil
413
+ end
415
414
  end
416
415
  alias :errors_on :inline_errors_for
417
416
 
418
417
  protected
419
418
 
420
- # Collects content columns (non-relation columns) for the current form object class.
421
- #
422
- def content_columns
423
- if @object.present?
424
- @object.class.name.constantize.content_columns.collect { |c| c.name.to_sym }.compact
425
- else
426
- @object_name.to_s.classify.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
419
+ def render_inline_errors?
420
+ @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(@@inline_errors)
427
421
  end
428
- end
429
422
 
430
- # Collects association columns (relation columns) for the current form object class.
431
- #
432
- def association_columns(*by_associations)
433
- if @object.present?
434
- @object.class.reflections.collect do |name, _|
435
- if by_associations.present?
436
- name if by_associations.include?(_.macro)
437
- else
438
- name
439
- end
440
- end.compact
441
- else
442
- []
423
+ # Collects content columns (non-relation columns) for the current form object class.
424
+ #
425
+ def content_columns #:nodoc:
426
+ self.model_name.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
443
427
  end
444
- end
445
428
 
446
- # Prepare options to be sent to label
447
- #
448
- def options_for_label(options)
449
- options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
450
- end
429
+ # Collects association columns (relation columns) for the current form object class.
430
+ #
431
+ def association_columns(*by_associations) #:nodoc:
432
+ if @object.present?
433
+ @object.class.reflections.collect do |name, _|
434
+ if by_associations.present?
435
+ name if by_associations.include?(_.macro)
436
+ else
437
+ name
438
+ end
439
+ end.compact
440
+ else
441
+ []
442
+ end
443
+ end
451
444
 
452
- # Deals with :for option when it's supplied to inputs methods. Additional
453
- # options to be passed down to :for should be supplied using :for_options
454
- # key.
455
- #
456
- # It should raise an error if a block with arity zero is given.
457
- #
458
- def inputs_for_nested_attributes(args, options, &block)
459
- args << options.merge!(:parent => { :builder => self, :for => options[:for] })
445
+ # Prepare options to be sent to label
446
+ #
447
+ def options_for_label(options) #:nodoc:
448
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
449
+ end
460
450
 
461
- fields_for_block = if block_given?
462
- raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
463
- 'but the block does not accept any argument.' if block.arity <= 0
451
+ # Deals with :for option when it's supplied to inputs methods. Additional
452
+ # options to be passed down to :for should be supplied using :for_options
453
+ # key.
454
+ #
455
+ # It should raise an error if a block with arity zero is given.
456
+ #
457
+ def inputs_for_nested_attributes(*args, &block) #:nodoc:
458
+ options = args.extract_options!
459
+ args << options.merge!(:parent => { :builder => self, :for => options[:for] })
460
+
461
+ fields_for_block = if block_given?
462
+ raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
463
+ 'but the block does not accept any argument.' if block.arity <= 0
464
+
465
+ proc { |f| f.inputs(*args){ block.call(f) } }
466
+ else
467
+ proc { |f| f.inputs(*args) }
468
+ end
464
469
 
465
- proc { |f| f.inputs(*args){ block.call(f) } }
466
- else
467
- proc { |f| f.inputs(*args) }
470
+ fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
471
+ semantic_fields_for(*fields_for_args, &fields_for_block)
468
472
  end
469
473
 
470
- fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
471
- semantic_fields_for(*fields_for_args, &fields_for_block)
472
- end
473
-
474
- # Remove any Formtastic-specific options before passing the down options.
475
- #
476
- def set_options(options)
477
- options.except(:value_method, :label_method, :collection, :required, :label,
478
- :as, :hint, :input_html, :label_html, :value_as_class)
479
- end
474
+ # Remove any Formtastic-specific options before passing the down options.
475
+ #
476
+ def strip_formtastic_options(options) #:nodoc:
477
+ options.except(:value_method, :label_method, :collection, :required, :label,
478
+ :as, :hint, :input_html, :label_html, :value_as_class)
479
+ end
480
480
 
481
- # Determins if the attribute (eg :title) should be considered required or not.
482
- #
483
- # * if the :required option was provided in the options hash, the true/false value will be
484
- # returned immediately, allowing the view to override any guesswork that follows:
485
- #
486
- # * if the :required option isn't provided in the options hash, and the ValidationReflection
487
- # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
488
- # if the validates_presence_of macro has been used in the class for this attribute, or false
489
- # otherwise.
490
- #
491
- # * if the :required option isn't provided, and the plugin isn't available, the value of the
492
- # configuration option @@all_fields_required_by_default is used.
493
- #
494
- def method_required?(attribute) #:nodoc:
495
- if @object && @object.class.respond_to?(:reflect_on_validations_for)
496
- attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
497
-
498
- @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
499
- validation.macro == :validates_presence_of &&
500
- validation.name == attribute_sym &&
501
- (validation.options.present? ? options_require_validation?(validation.options) : true)
481
+ # Determins if the attribute (eg :title) should be considered required or not.
482
+ #
483
+ # * if the :required option was provided in the options hash, the true/false value will be
484
+ # returned immediately, allowing the view to override any guesswork that follows:
485
+ #
486
+ # * if the :required option isn't provided in the options hash, and the ValidationReflection
487
+ # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
488
+ # if the validates_presence_of macro has been used in the class for this attribute, or false
489
+ # otherwise.
490
+ #
491
+ # * if the :required option isn't provided, and the plugin isn't available, the value of the
492
+ # configuration option @@all_fields_required_by_default is used.
493
+ #
494
+ def method_required?(attribute) #:nodoc:
495
+ if @object && @object.class.respond_to?(:reflect_on_validations_for)
496
+ attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
497
+
498
+ @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
499
+ validation.macro == :validates_presence_of &&
500
+ validation.name == attribute_sym &&
501
+ (validation.options.present? ? options_require_validation?(validation.options) : true)
502
+ end
503
+ else
504
+ @@all_fields_required_by_default
502
505
  end
503
- else
504
- @@all_fields_required_by_default
505
506
  end
506
- end
507
507
 
508
- # Determines whether the given options evaluate to true
509
- def options_require_validation?(options) #nodoc
510
- if_condition = !options[:if].nil?
511
- condition = if_condition ? options[:if] : options[:unless]
508
+ # Determines whether the given options evaluate to true
509
+ def options_require_validation?(options) #nodoc
510
+ if_condition = !options[:if].nil?
511
+ condition = if_condition ? options[:if] : options[:unless]
512
512
 
513
- condition = if condition.respond_to?(:call)
514
- condition.call(@object)
515
- elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
516
- @object.send(condition)
517
- else
518
- condition
519
- end
513
+ condition = if condition.respond_to?(:call)
514
+ condition.call(@object)
515
+ elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
516
+ @object.send(condition)
517
+ else
518
+ condition
519
+ end
520
520
 
521
- if_condition ? !!condition : !condition
522
- end
521
+ if_condition ? !!condition : !condition
522
+ end
523
523
 
524
- def basic_input_helper(form_helper_method, type, method, options)
525
- html_options = options.delete(:input_html) || {}
526
- html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password].include?(type)
524
+ def basic_input_helper(form_helper_method, type, method, options) #:nodoc:
525
+ html_options = options.delete(:input_html) || {}
526
+ html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password].include?(type)
527
527
 
528
- self.label(method, options_for_label(options)) <<
529
- self.send(form_helper_method, method, html_options)
530
- end
531
-
532
- # Outputs a label and standard Rails text field inside the wrapper.
533
- def string_input(method, options)
534
- basic_input_helper(:text_field, :string, method, options)
535
- end
528
+ self.label(method, options_for_label(options)) <<
529
+ self.send(form_helper_method, method, html_options)
530
+ end
536
531
 
537
- # Outputs a label and standard Rails password field inside the wrapper.
538
- def password_input(method, options)
539
- basic_input_helper(:password_field, :password, method, options)
540
- end
532
+ # Outputs a label and standard Rails text field inside the wrapper.
533
+ def string_input(method, options)
534
+ basic_input_helper(:text_field, :string, method, options)
535
+ end
541
536
 
542
- # Outputs a label and standard Rails text field inside the wrapper.
543
- def numeric_input(method, options)
544
- basic_input_helper(:text_field, :numeric, method, options)
545
- end
537
+ # Outputs a label and standard Rails password field inside the wrapper.
538
+ def password_input(method, options)
539
+ basic_input_helper(:password_field, :password, method, options)
540
+ end
546
541
 
547
- # Ouputs a label and standard Rails text area inside the wrapper.
548
- def text_input(method, options)
549
- basic_input_helper(:text_area, :text, method, options)
550
- end
542
+ # Outputs a label and standard Rails text field inside the wrapper.
543
+ def numeric_input(method, options)
544
+ basic_input_helper(:text_field, :numeric, method, options)
545
+ end
551
546
 
552
- # Outputs a label and a standard Rails file field inside the wrapper.
553
- def file_input(method, options)
554
- basic_input_helper(:file_field, :file, method, options)
555
- end
547
+ # Ouputs a label and standard Rails text area inside the wrapper.
548
+ def text_input(method, options)
549
+ basic_input_helper(:text_area, :text, method, options)
550
+ end
556
551
 
557
- # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
558
- # Additionals options can be given and will be sent straight to hidden input
559
- # element.
560
- #
561
- def hidden_input(method, options)
562
- options ||= {}
563
- if options[:input_html].present?
564
- options[:value] = options[:input_html][:value] if options[:input_html][:value].present?
552
+ # Outputs a label and a standard Rails file field inside the wrapper.
553
+ def file_input(method, options)
554
+ basic_input_helper(:file_field, :file, method, options)
565
555
  end
566
- self.hidden_field(method, set_options(options))
567
- end
568
556
 
569
- # Outputs a label and a select box containing options from the parent
570
- # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
571
- # is has_many or has_and_belongs_to_many the select box will be set as multi-select
572
- # and size = 5
573
- #
574
- # Example (belongs_to):
575
- #
576
- # f.input :author
577
- #
578
- # <label for="book_author_id">Author</label>
579
- # <select id="book_author_id" name="book[author_id]">
580
- # <option value=""></option>
581
- # <option value="1">Justin French</option>
582
- # <option value="2">Jane Doe</option>
583
- # </select>
584
- #
585
- # Example (has_many):
586
- #
587
- # f.input :chapters
588
- #
589
- # <label for="book_chapter_ids">Chapters</label>
590
- # <select id="book_chapter_ids" name="book[chapter_ids]">
591
- # <option value=""></option>
592
- # <option value="1">Chapter 1</option>
593
- # <option value="2">Chapter 2</option>
594
- # </select>
595
- #
596
- # Example (has_and_belongs_to_many):
597
- #
598
- # f.input :authors
599
- #
600
- # <label for="book_author_ids">Authors</label>
601
- # <select id="book_author_ids" name="book[author_ids]">
602
- # <option value=""></option>
603
- # <option value="1">Justin French</option>
604
- # <option value="2">Jane Doe</option>
605
- # </select>
606
- #
607
- #
608
- # You can customize the options available in the select by passing in a collection (an Array or
609
- # Hash) through the :collection option. If not provided, the choices are found by inferring the
610
- # parent's class name from the method name and simply calling find(:all) on it
611
- # (VehicleOwner.find(:all) in the example above).
612
- #
613
- # Examples:
614
- #
615
- # f.input :author, :collection => @authors
616
- # f.input :author, :collection => Author.find(:all)
617
- # f.input :author, :collection => [@justin, @kate]
618
- # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
619
- # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
620
- #
621
- # The :label_method option allows you to customize the text label inside each option tag two ways:
622
- #
623
- # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
624
- # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
625
- #
626
- # Examples:
627
- #
628
- # f.input :author, :label_method => :full_name
629
- # f.input :author, :label_method => :login
630
- # f.input :author, :label_method => :full_name_with_post_count
631
- # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
632
- #
633
- # The :value_method option provides the same customization of the value attribute of each option tag.
634
- #
635
- # Examples:
636
- #
637
- # f.input :author, :value_method => :full_name
638
- # f.input :author, :value_method => :login
639
- # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
640
- #
641
- # You can pre-select a specific option value by passing in the :selected option.
642
- #
643
- # Examples:
644
- #
645
- # f.input :author, :selected => current_user.id
646
- # f.input :author, :value_method => :login, :selected => current_user.login
647
- #
648
- # You can pass html_options to the select tag using :input_html => {}
649
- #
650
- # Examples:
651
- #
652
- # f.input :authors, :input_html => {:size => 20, :multiple => true}
653
- #
654
- # By default, all select inputs will have a blank option at the top of the list. You can add
655
- # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
656
- #
657
- #
658
- # You can group the options in optgroup elements by passing the :group_by option
659
- # (Note: only tested for belongs_to relations)
660
- #
661
- # Examples:
662
- #
663
- # f.input :author, :group_by => :continent
664
- #
665
- # All the other options should work as expected. If you want to call a custom method on the
666
- # group item. You can include the option:group_label_method
667
- # Examples:
668
- #
669
- # f.input :author, :group_by => :continents, :group_label_method => :something_different
670
- #
671
- def select_input(method, options)
672
- html_options = options.delete(:input_html) || {}
673
- options = set_include_blank(options)
674
-
675
- reflection = find_reflection(method)
676
- if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
677
- options[:include_blank] = false
678
- html_options[:multiple] ||= true
679
- html_options[:size] ||= 5
557
+ # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
558
+ # Additionals options can be given and will be sent straight to hidden input
559
+ # element.
560
+ #
561
+ def hidden_input(method, options)
562
+ options ||= {}
563
+ if options[:input_html].present?
564
+ options[:value] = options[:input_html][:value] if options[:input_html][:value].present?
565
+ end
566
+ self.hidden_field(method, strip_formtastic_options(options))
680
567
  end
681
- input_name = generate_association_input_name(method)
682
-
683
- select_html = if options[:group_by]
684
- # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
685
- # The formtastic user however shouldn't notice this too much.
686
- raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
687
- label, value = detect_label_and_value_method!(raw_collection)
688
- group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
689
- group_label_method = options[:group_label_method] || detect_label_method(group_collection)
690
- group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
691
-
692
- # Here comes the monster with 8 arguments
693
- self.grouped_collection_select(input_name, group_collection,
694
- method.to_s.pluralize, group_label_method,
695
- value, label,
696
- set_options(options), html_options)
697
- else
698
- collection = find_collection_for_column(method, options)
699
- self.select(input_name, collection, set_options(options), html_options)
568
+
569
+ # Outputs a label and a select box containing options from the parent
570
+ # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
571
+ # is has_many or has_and_belongs_to_many the select box will be set as multi-select
572
+ # and size = 5
573
+ #
574
+ # Example (belongs_to):
575
+ #
576
+ # f.input :author
577
+ #
578
+ # <label for="book_author_id">Author</label>
579
+ # <select id="book_author_id" name="book[author_id]">
580
+ # <option value=""></option>
581
+ # <option value="1">Justin French</option>
582
+ # <option value="2">Jane Doe</option>
583
+ # </select>
584
+ #
585
+ # Example (has_many):
586
+ #
587
+ # f.input :chapters
588
+ #
589
+ # <label for="book_chapter_ids">Chapters</label>
590
+ # <select id="book_chapter_ids" name="book[chapter_ids]">
591
+ # <option value=""></option>
592
+ # <option value="1">Chapter 1</option>
593
+ # <option value="2">Chapter 2</option>
594
+ # </select>
595
+ #
596
+ # Example (has_and_belongs_to_many):
597
+ #
598
+ # f.input :authors
599
+ #
600
+ # <label for="book_author_ids">Authors</label>
601
+ # <select id="book_author_ids" name="book[author_ids]">
602
+ # <option value=""></option>
603
+ # <option value="1">Justin French</option>
604
+ # <option value="2">Jane Doe</option>
605
+ # </select>
606
+ #
607
+ #
608
+ # You can customize the options available in the select by passing in a collection (an Array or
609
+ # Hash) through the :collection option. If not provided, the choices are found by inferring the
610
+ # parent's class name from the method name and simply calling find(:all) on it
611
+ # (VehicleOwner.find(:all) in the example above).
612
+ #
613
+ # Examples:
614
+ #
615
+ # f.input :author, :collection => @authors
616
+ # f.input :author, :collection => Author.find(:all)
617
+ # f.input :author, :collection => [@justin, @kate]
618
+ # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
619
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
620
+ #
621
+ # The :label_method option allows you to customize the text label inside each option tag two ways:
622
+ #
623
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
624
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
625
+ #
626
+ # Examples:
627
+ #
628
+ # f.input :author, :label_method => :full_name
629
+ # f.input :author, :label_method => :login
630
+ # f.input :author, :label_method => :full_name_with_post_count
631
+ # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
632
+ #
633
+ # The :value_method option provides the same customization of the value attribute of each option tag.
634
+ #
635
+ # Examples:
636
+ #
637
+ # f.input :author, :value_method => :full_name
638
+ # f.input :author, :value_method => :login
639
+ # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
640
+ #
641
+ # You can pre-select a specific option value by passing in the :selected option.
642
+ #
643
+ # Examples:
644
+ #
645
+ # f.input :author, :selected => current_user.id
646
+ # f.input :author, :value_method => :login, :selected => current_user.login
647
+ # f.input :authors, :value_method => :login, :selected => Author.most_popular.collect(&:id)
648
+ # f.input :authors, :value_method => :login, :selected => nil # override any defaults: select none
649
+ #
650
+ # You can pass html_options to the select tag using :input_html => {}
651
+ #
652
+ # Examples:
653
+ #
654
+ # f.input :authors, :input_html => {:size => 20, :multiple => true}
655
+ #
656
+ # By default, all select inputs will have a blank option at the top of the list. You can add
657
+ # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
658
+ #
659
+ #
660
+ # You can group the options in optgroup elements by passing the :group_by option
661
+ # (Note: only tested for belongs_to relations)
662
+ #
663
+ # Examples:
664
+ #
665
+ # f.input :author, :group_by => :continent
666
+ #
667
+ # All the other options should work as expected. If you want to call a custom method on the
668
+ # group item. You can include the option:group_label_method
669
+ # Examples:
670
+ #
671
+ # f.input :author, :group_by => :continents, :group_label_method => :something_different
672
+ #
673
+ def select_input(method, options)
674
+ html_options = options.delete(:input_html) || {}
675
+ options = set_include_blank(options)
676
+ html_options[:multiple] = options.delete(:multiple) if html_options[:multiple].nil?
677
+
678
+ reflection = self.reflection_for(method)
679
+ if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
680
+ options[:include_blank] = false
681
+ html_options[:multiple] = true if html_options[:multiple].nil?
682
+ html_options[:size] ||= 5
683
+ end
684
+ options[:selected] = options[:selected].first if options[:selected].present? && html_options[:multiple] == false
685
+ input_name = generate_association_input_name(method)
686
+
687
+ select_html = if options[:group_by]
688
+ # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
689
+ # The formtastic user however shouldn't notice this too much.
690
+ raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
691
+ label, value = detect_label_and_value_method!(raw_collection)
692
+ group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
693
+ group_label_method = options[:group_label_method] || detect_label_method(group_collection)
694
+ group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
695
+ group_association = options[:group_association] || detect_group_association(method, options[:group_by])
696
+
697
+ # Here comes the monster with 8 arguments
698
+ self.grouped_collection_select(input_name, group_collection,
699
+ group_association, group_label_method,
700
+ value, label,
701
+ strip_formtastic_options(options), html_options)
702
+ else
703
+ collection = find_collection_for_column(method, options)
704
+ self.select(input_name, collection, strip_formtastic_options(options), html_options)
705
+ end
706
+
707
+ self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
708
+ end
709
+ alias :boolean_select_input :select_input
710
+
711
+ # Outputs a timezone select input as Rails' time_zone_select helper. You
712
+ # can give priority zones as option.
713
+ #
714
+ # Examples:
715
+ #
716
+ # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
717
+ #
718
+ # You can pre-select a specific option value by passing in the :selected option.
719
+ # Note: Right now only works if the form object attribute value is not set (nil),
720
+ # because of how the core helper is implemented.
721
+ #
722
+ # Examples:
723
+ #
724
+ # f.input :my_favorite_time_zone, :as => :time_zone, :selected => 'Singapore'
725
+ #
726
+ def time_zone_input(method, options)
727
+ html_options = options.delete(:input_html) || {}
728
+ selected_value = options.delete(:selected)
729
+
730
+ self.label(method, options_for_label(options)) <<
731
+ self.time_zone_select(method, options.delete(:priority_zones),
732
+ strip_formtastic_options(options).merge(:default => selected_value), html_options)
700
733
  end
701
-
702
- self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
703
- end
704
- alias :boolean_select_input :select_input
705
734
 
706
- # Outputs a timezone select input as Rails' time_zone_select helper. You
707
- # can give priority zones as option.
708
- #
709
- # Examples:
710
- #
711
- # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
712
- #
713
- def time_zone_input(method, options)
714
- html_options = options.delete(:input_html) || {}
735
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
736
+ # items, one for each possible choice in the belongs_to association. Each li contains a
737
+ # label and a radio input.
738
+ #
739
+ # Example:
740
+ #
741
+ # f.input :author, :as => :radio
742
+ #
743
+ # Output:
744
+ #
745
+ # <fieldset>
746
+ # <legend><span>Author</span></legend>
747
+ # <ol>
748
+ # <li>
749
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
750
+ # </li>
751
+ # <li>
752
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
753
+ # </li>
754
+ # </ol>
755
+ # </fieldset>
756
+ #
757
+ # You can customize the choices available in the radio button set by passing in a collection (an Array or
758
+ # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
759
+ # (Author.find(:all) in the example above).
760
+ #
761
+ # Examples:
762
+ #
763
+ # f.input :author, :as => :radio, :collection => @authors
764
+ # f.input :author, :as => :radio, :collection => Author.find(:all)
765
+ # f.input :author, :as => :radio, :collection => [@justin, @kate]
766
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
767
+ #
768
+ # The :label_method option allows you to customize the label for each radio button two ways:
769
+ #
770
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
771
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
772
+ #
773
+ # Examples:
774
+ #
775
+ # f.input :author, :as => :radio, :label_method => :full_name
776
+ # f.input :author, :as => :radio, :label_method => :login
777
+ # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
778
+ # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
779
+ #
780
+ # The :value_method option provides the same customization of the value attribute of each option tag.
781
+ #
782
+ # Examples:
783
+ #
784
+ # f.input :author, :as => :radio, :value_method => :full_name
785
+ # f.input :author, :as => :radio, :value_method => :login
786
+ # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
787
+ #
788
+ # You can force a particular radio button in the collection to be checked with the :selected option.
789
+ #
790
+ # Examples:
791
+ #
792
+ # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
793
+ # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
794
+ #
795
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
796
+ # button / label combination to contain a class with the value of the radio button (useful for
797
+ # applying specific CSS or Javascript to a particular radio button).
798
+ #
799
+ def radio_input(method, options)
800
+ collection = find_collection_for_column(method, options)
801
+ html_options = strip_formtastic_options(options).merge(options.delete(:input_html) || {})
802
+
803
+ input_name = generate_association_input_name(method)
804
+ value_as_class = options.delete(:value_as_class)
805
+ input_ids = []
806
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
807
+ selected_value = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
808
+
809
+ list_item_content = collection.map do |c|
810
+ label = c.is_a?(Array) ? c.first : c
811
+ value = c.is_a?(Array) ? c.last : c
812
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
813
+ input_ids << input_id
814
+
815
+ html_options[:checked] = selected_value == value if selected_option_is_present
816
+
817
+ li_content = template.content_tag(:label,
818
+ "#{self.radio_button(input_name, value, html_options)} #{label}",
819
+ :for => input_id
820
+ )
715
821
 
716
- self.label(method, options_for_label(options)) <<
717
- self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options)
718
- end
822
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
823
+ template.content_tag(:li, li_content, li_options)
824
+ end
719
825
 
720
- # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
721
- # items, one for each possible choice in the belongs_to association. Each li contains a
722
- # label and a radio input.
723
- #
724
- # Example:
725
- #
726
- # f.input :author, :as => :radio
727
- #
728
- # Output:
729
- #
730
- # <fieldset>
731
- # <legend><span>Author</span></legend>
732
- # <ol>
733
- # <li>
734
- # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
735
- # </li>
736
- # <li>
737
- # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
738
- # </li>
739
- # </ol>
740
- # </fieldset>
741
- #
742
- # You can customize the choices available in the radio button set by passing in a collection (an Array or
743
- # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
744
- # (Author.find(:all) in the example above).
745
- #
746
- # Examples:
747
- #
748
- # f.input :author, :as => :radio, :collection => @authors
749
- # f.input :author, :as => :radio, :collection => Author.find(:all)
750
- # f.input :author, :as => :radio, :collection => [@justin, @kate]
751
- # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
752
- #
753
- # The :label_method option allows you to customize the label for each radio button two ways:
754
- #
755
- # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
756
- # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
757
- #
758
- # Examples:
759
- #
760
- # f.input :author, :as => :radio, :label_method => :full_name
761
- # f.input :author, :as => :radio, :label_method => :login
762
- # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
763
- # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
764
- #
765
- # The :value_method option provides the same customization of the value attribute of each option tag.
766
- #
767
- # Examples:
768
- #
769
- # f.input :author, :as => :radio, :value_method => :full_name
770
- # f.input :author, :as => :radio, :value_method => :login
771
- # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
772
- #
773
- # You can force a particular radio button in the collection to be checked with the :selected option. Example:
774
- #
775
- # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
776
- # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
777
- #
778
- # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
779
- # button / label combination to contain a class with the value of the radio button (useful for
780
- # applying specific CSS or Javascript to a particular radio button).
781
- def radio_input(method, options)
782
- collection = find_collection_for_column(method, options)
783
- html_options = set_options(options).merge(options.delete(:input_html) || {})
784
-
785
- input_name = generate_association_input_name(method)
786
- value_as_class = options.delete(:value_as_class)
787
- input_ids = []
788
-
789
- list_item_content = collection.map do |c|
790
- label = c.is_a?(Array) ? c.first : c
791
- value = c.is_a?(Array) ? c.last : c
792
- html_options[:checked] = options.delete(:selected) unless options[:selected].blank?
793
-
794
- input_ids << input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
795
- li_content = template.content_tag(:label,
796
- "#{self.radio_button(input_name, value, html_options)} #{label}",
797
- :for => input_id
798
- )
826
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
827
+ end
828
+ alias :boolean_radio_input :radio_input
829
+
830
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
831
+ # items (li), one for each fragment for the date (year, month, day). Each li contains a label
832
+ # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
833
+ #
834
+ # You can pre-select a specific option value by passing in the :selected option.
835
+ #
836
+ # Examples:
837
+ #
838
+ # f.input :created_at, :as => :date, :selected => 1.day.ago
839
+ # f.input :created_at, :as => :date, :selected => nil # override any defaults: select none
840
+ #
841
+ # Some of Rails' options for select_date are supported, but not everything yet.
842
+ #
843
+ def date_input(method, options)
844
+ options = set_include_blank(options)
845
+ date_or_datetime_input(method, options.merge(:discard_hour => true))
846
+ end
799
847
 
800
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
801
- template.content_tag(:li, li_content, li_options)
848
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
849
+ # items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
850
+ # contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
851
+ # detailed output example.
852
+ #
853
+ # You can pre-select a specific option value by passing in the :selected option.
854
+ #
855
+ # Examples:
856
+ #
857
+ # f.input :created_at, :as => :datetime, :selected => 1.day.ago
858
+ # f.input :created_at, :as => :datetime, :selected => nil # override any defaults: select none
859
+ #
860
+ # Some of Rails' options for select_date are supported, but not everything yet.
861
+ #
862
+ def datetime_input(method, options)
863
+ options = set_include_blank(options)
864
+ date_or_datetime_input(method, options)
802
865
  end
803
866
 
804
- field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
805
- end
806
- alias :boolean_radio_input :radio_input
867
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
868
+ # items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
869
+ # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
870
+ #
871
+ # You can pre-select a specific option value by passing in the :selected option.
872
+ #
873
+ # Examples:
874
+ #
875
+ # f.input :created_at, :as => :time, :selected => 1.hour.ago
876
+ # f.input :created_at, :as => :time, :selected => nil # override any defaults: select none
877
+ #
878
+ # Some of Rails' options for select_time are supported, but not everything yet.
879
+ #
880
+ def time_input(method, options)
881
+ options = set_include_blank(options)
882
+ date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
883
+ end
807
884
 
808
- # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
809
- # items (li), one for each fragment for the date (year, month, day). Each li contains a label
810
- # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
811
- #
812
- # Some of Rails' options for select_date are supported, but not everything yet.
813
- def date_input(method, options)
814
- options = set_include_blank(options)
815
- date_or_datetime_input(method, options.merge(:discard_hour => true))
816
- end
885
+ # <fieldset>
886
+ # <legend>Created At</legend>
887
+ # <ol>
888
+ # <li>
889
+ # <label for="user_created_at_1i">Year</label>
890
+ # <select id="user_created_at_1i" name="user[created_at(1i)]">
891
+ # <option value="2003">2003</option>
892
+ # ...
893
+ # <option value="2013">2013</option>
894
+ # </select>
895
+ # </li>
896
+ # <li>
897
+ # <label for="user_created_at_2i">Month</label>
898
+ # <select id="user_created_at_2i" name="user[created_at(2i)]">
899
+ # <option value="1">January</option>
900
+ # ...
901
+ # <option value="12">December</option>
902
+ # </select>
903
+ # </li>
904
+ # <li>
905
+ # <label for="user_created_at_3i">Day</label>
906
+ # <select id="user_created_at_3i" name="user[created_at(3i)]">
907
+ # <option value="1">1</option>
908
+ # ...
909
+ # <option value="31">31</option>
910
+ # </select>
911
+ # </li>
912
+ # </ol>
913
+ # </fieldset>
914
+ #
915
+ # This is an absolute abomination, but so is the official Rails select_date().
916
+ #
917
+ def date_or_datetime_input(method, options)
918
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
919
+ i18n_date_order = ::I18n.t(:order, :scope => [:date])
920
+ i18n_date_order = nil unless i18n_date_order.is_a?(Array)
921
+ inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
922
+
923
+ time_inputs = [:hour, :minute]
924
+ time_inputs << [:second] if options[:include_seconds]
925
+
926
+ list_items_capture = ""
927
+ hidden_fields_capture = ""
928
+
929
+ default_time = ::Time.now
930
+
931
+ # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
932
+ datetime = options[:selected] || (@object ? @object.send(method) : default_time) || default_time
933
+
934
+ html_options = options.delete(:input_html) || {}
935
+ input_ids = []
817
936
 
818
- # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
819
- # items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
820
- # contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
821
- # detailed output example.
822
- #
823
- # Some of Rails' options for select_date are supported, but not everything yet.
824
- def datetime_input(method, options)
825
- options = set_include_blank(options)
826
- date_or_datetime_input(method, options)
827
- end
937
+ (inputs + time_inputs).each do |input|
938
+ input_ids << input_id = generate_html_id(method, "#{position[input]}i")
828
939
 
829
- # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
830
- # items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
831
- # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
832
- #
833
- # Some of Rails' options for select_time are supported, but not everything yet.
834
- def time_input(method, options)
835
- options = set_include_blank(options)
836
- date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
837
- end
940
+ field_name = "#{method}(#{position[input]}i)"
941
+ if options[:"discard_#{input}"]
942
+ break if time_inputs.include?(input)
838
943
 
839
- # <fieldset>
840
- # <legend>Created At</legend>
841
- # <ol>
842
- # <li>
843
- # <label for="user_created_at_1i">Year</label>
844
- # <select id="user_created_at_1i" name="user[created_at(1i)]">
845
- # <option value="2003">2003</option>
846
- # ...
847
- # <option value="2013">2013</option>
848
- # </select>
849
- # </li>
850
- # <li>
851
- # <label for="user_created_at_2i">Month</label>
852
- # <select id="user_created_at_2i" name="user[created_at(2i)]">
853
- # <option value="1">January</option>
854
- # ...
855
- # <option value="12">December</option>
856
- # </select>
857
- # </li>
858
- # <li>
859
- # <label for="user_created_at_3i">Day</label>
860
- # <select id="user_created_at_3i" name="user[created_at(3i)]">
861
- # <option value="1">1</option>
862
- # ...
863
- # <option value="31">31</option>
864
- # </select>
865
- # </li>
866
- # </ol>
867
- # </fieldset>
868
- #
869
- # This is an absolute abomination, but so is the official Rails select_date().
870
- #
871
- def date_or_datetime_input(method, options)
872
- position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
873
- i18n_date_order = ::I18n.t(:order, :scope => [:date])
874
- i18n_date_order = nil unless i18n_date_order.is_a?(Array)
875
- inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
944
+ hidden_value = datetime.respond_to?(input) ? datetime.send(input.to_sym) : datetime
945
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
946
+ else
947
+ opts = strip_formtastic_options(options).merge(:prefix => @object_name, :field_name => field_name, :default => datetime)
948
+ item_label_text = ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
876
949
 
877
- time_inputs = [:hour, :minute]
878
- time_inputs << [:second] if options[:include_seconds]
950
+ list_items_capture << template.content_tag(:li,
951
+ template.content_tag(:label, item_label_text, :for => input_id) <<
952
+ template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
953
+ )
954
+ end
955
+ end
879
956
 
880
- list_items_capture = ""
881
- hidden_fields_capture = ""
957
+ hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
958
+ end
882
959
 
883
- # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
884
- datetime = @object ? @object.send(method) : nil
885
- html_options = options.delete(:input_html) || {}
886
- input_ids = []
960
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
961
+ # items, one for each possible choice in the belongs_to association. Each li contains a
962
+ # label and a check_box input.
963
+ #
964
+ # This is an alternative for has many and has and belongs to many associations.
965
+ #
966
+ # Example:
967
+ #
968
+ # f.input :author, :as => :check_boxes
969
+ #
970
+ # Output:
971
+ #
972
+ # <fieldset>
973
+ # <legend class="label"><label>Authors</label></legend>
974
+ # <ol>
975
+ # <li>
976
+ # <input type="hidden" name="book[author_id][1]" value="">
977
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
978
+ # </li>
979
+ # <li>
980
+ # <input type="hidden" name="book[author_id][2]" value="">
981
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
982
+ # </li>
983
+ # </ol>
984
+ # </fieldset>
985
+ #
986
+ # Notice that the value of the checkbox is the same as the id and the hidden
987
+ # field has empty value. You can override the hidden field value using the
988
+ # unchecked_value option.
989
+ #
990
+ # You can customize the options available in the set by passing in a collection (Array) of
991
+ # ActiveRecord objects through the :collection option. If not provided, the choices are found
992
+ # by inferring the parent's class name from the method name and simply calling find(:all) on
993
+ # it (Author.find(:all) in the example above).
994
+ #
995
+ # Examples:
996
+ #
997
+ # f.input :author, :as => :check_boxes, :collection => @authors
998
+ # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
999
+ # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
1000
+ #
1001
+ # The :label_method option allows you to customize the label for each checkbox two ways:
1002
+ #
1003
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
1004
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
1005
+ #
1006
+ # Examples:
1007
+ #
1008
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
1009
+ # f.input :author, :as => :check_boxes, :label_method => :login
1010
+ # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
1011
+ # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
1012
+ #
1013
+ # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
1014
+ #
1015
+ # Examples:
1016
+ #
1017
+ # f.input :author, :as => :check_boxes, :value_method => :full_name
1018
+ # f.input :author, :as => :check_boxes, :value_method => :login
1019
+ # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
1020
+ #
1021
+ # You can pre-select/check a specific checkbox value by passing in the :selected option (alias :checked works as well).
1022
+ #
1023
+ # Examples:
1024
+ #
1025
+ # f.input :authors, :as => :check_boxes, :selected => @justin
1026
+ # f.input :authors, :as => :check_boxes, :selected => Author.most_popular.collect(&:id)
1027
+ # f.input :authors, :as => :check_boxes, :selected => nil # override any defaults: select none
1028
+ #
1029
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
1030
+ # combination to contain a class with the value of the radio button (useful for applying specific
1031
+ # CSS or Javascript to a particular checkbox).
1032
+ #
1033
+ def check_boxes_input(method, options)
1034
+ collection = find_collection_for_column(method, options)
1035
+ html_options = options.delete(:input_html) || {}
887
1036
 
888
- (inputs + time_inputs).each do |input|
889
- input_ids << input_id = generate_html_id(method, "#{position[input]}i")
890
-
891
- field_name = "#{method}(#{position[input]}i)"
892
- if options[:"discard_#{input}"]
893
- break if time_inputs.include?(input)
894
-
895
- hidden_value = datetime.respond_to?(input) ? datetime.send(input.to_sym) : datetime
896
- hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
897
- else
898
- opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
899
- item_label_text = ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
1037
+ input_name = generate_association_input_name(method)
1038
+ value_as_class = options.delete(:value_as_class)
1039
+ unchecked_value = options.delete(:unchecked_value) || ''
1040
+ html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
1041
+ input_ids = []
900
1042
 
901
- list_items_capture << template.content_tag(:li,
902
- template.content_tag(:label, item_label_text, :for => input_id) <<
903
- template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
1043
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
1044
+ selected_values = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
1045
+ selected_values = [*selected_values].compact
1046
+
1047
+ list_item_content = collection.map do |c|
1048
+ label = c.is_a?(Array) ? c.first : c
1049
+ value = c.is_a?(Array) ? c.last : c
1050
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
1051
+ input_ids << input_id
1052
+
1053
+ html_options[:checked] = selected_values.include?(value) if selected_option_is_present
1054
+ html_options[:id] = input_id
1055
+
1056
+ li_content = template.content_tag(:label,
1057
+ "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
1058
+ :for => input_id
904
1059
  )
905
- end
906
- end
907
1060
 
908
- hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
909
- end
910
-
911
- # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
912
- # items, one for each possible choice in the belongs_to association. Each li contains a
913
- # label and a check_box input.
914
- #
915
- # This is an alternative for has many and has and belongs to many associations.
916
- #
917
- # Example:
918
- #
919
- # f.input :author, :as => :check_boxes
920
- #
921
- # Output:
922
- #
923
- # <fieldset>
924
- # <legend class="label"><label>Authors</label></legend>
925
- # <ol>
926
- # <li>
927
- # <input type="hidden" name="book[author_id][1]" value="">
928
- # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
929
- # </li>
930
- # <li>
931
- # <input type="hidden" name="book[author_id][2]" value="">
932
- # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
933
- # </li>
934
- # </ol>
935
- # </fieldset>
936
- #
937
- # Notice that the value of the checkbox is the same as the id and the hidden
938
- # field has empty value. You can override the hidden field value using the
939
- # unchecked_value option.
940
- #
941
- # You can customize the options available in the set by passing in a collection (Array) of
942
- # ActiveRecord objects through the :collection option. If not provided, the choices are found
943
- # by inferring the parent's class name from the method name and simply calling find(:all) on
944
- # it (Author.find(:all) in the example above).
945
- #
946
- # Examples:
947
- #
948
- # f.input :author, :as => :check_boxes, :collection => @authors
949
- # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
950
- # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
951
- #
952
- # The :label_method option allows you to customize the label for each checkbox two ways:
953
- #
954
- # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
955
- # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
956
- #
957
- # Examples:
958
- #
959
- # f.input :author, :as => :check_boxes, :label_method => :full_name
960
- # f.input :author, :as => :check_boxes, :label_method => :login
961
- # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
962
- # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
963
- #
964
- # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
965
- #
966
- # Examples:
967
- #
968
- # f.input :author, :as => :check_boxes, :value_method => :full_name
969
- # f.input :author, :as => :check_boxes, :value_method => :login
970
- # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
971
- #
972
- # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
973
- # combination to contain a class with the value of the radio button (useful for applying specific
974
- # CSS or Javascript to a particular checkbox).
975
- def check_boxes_input(method, options)
976
- collection = find_collection_for_column(method, options)
977
- html_options = options.delete(:input_html) || {}
978
-
979
- input_name = generate_association_input_name(method)
980
- value_as_class = options.delete(:value_as_class)
981
- unchecked_value = options.delete(:unchecked_value) || ''
982
- html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
983
- input_ids = []
984
-
985
- list_item_content = collection.map do |c|
986
- label = c.is_a?(Array) ? c.first : c
987
- value = c.is_a?(Array) ? c.last : c
988
-
989
- input_ids << input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
990
- html_options.merge!(:id => input_id)
991
-
992
- li_content = template.content_tag(:label,
993
- "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
994
- :for => input_id
995
- )
1061
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
1062
+ template.content_tag(:li, li_content, li_options)
1063
+ end
996
1064
 
997
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
998
- template.content_tag(:li, li_content, li_options)
1065
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
999
1066
  end
1000
1067
 
1001
- field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
1002
- end
1003
-
1004
- # Outputs a country select input, wrapping around a regular country_select helper.
1005
- # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1006
- # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1007
- # same way.
1008
- #
1009
- # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1010
- #
1011
- # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1012
- # which you can change to suit your market and user base (see README for more info on config).
1013
- #
1014
- # Examples:
1015
- # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1016
- # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1017
- #
1018
- def country_input(method, options)
1019
- 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)
1068
+ # Outputs a country select input, wrapping around a regular country_select helper.
1069
+ # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1070
+ # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1071
+ # same way.
1072
+ #
1073
+ # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1074
+ #
1075
+ # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1076
+ # which you can change to suit your market and user base (see README for more info on config).
1077
+ #
1078
+ # Examples:
1079
+ # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1080
+ # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1081
+ #
1082
+ def country_input(method, options)
1083
+ 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)
1020
1084
 
1021
- html_options = options.delete(:input_html) || {}
1022
- priority_countries = options.delete(:priority_countries) || @@priority_countries
1085
+ html_options = options.delete(:input_html) || {}
1086
+ priority_countries = options.delete(:priority_countries) || @@priority_countries
1023
1087
 
1024
- self.label(method, options_for_label(options)) <<
1025
- self.country_select(method, priority_countries, set_options(options), html_options)
1026
- end
1088
+ self.label(method, options_for_label(options)) <<
1089
+ self.country_select(method, priority_countries, strip_formtastic_options(options), html_options)
1090
+ end
1027
1091
 
1028
- # Outputs a label containing a checkbox and the label text. The label defaults
1029
- # to the column name (method name) and can be altered with the :label option.
1030
- # :checked_value and :unchecked_value options are also available.
1031
- #
1032
- def boolean_input(method, options)
1033
- html_options = options.delete(:input_html) || {}
1092
+ # Outputs a label containing a checkbox and the label text. The label defaults
1093
+ # to the column name (method name) and can be altered with the :label option.
1094
+ # :checked_value and :unchecked_value options are also available.
1095
+ #
1096
+ # You can pre-select/check the boolean checkbox by passing in the :selected option (alias :checked works as well).
1097
+ #
1098
+ # Examples:
1099
+ #
1100
+ # f.input :allow_comments, :as => :boolean, :selected => true # override any default value: selected/checked
1101
+ #
1102
+ def boolean_input(method, options)
1103
+ html_options = options.delete(:input_html) || {}
1104
+ checked = options.key?(:checked) ? options[:checked] : options[:selected]
1105
+ html_options[:checked] = checked == true if [:selected, :checked].any? { |k| options.key?(k) }
1106
+
1107
+ input = self.check_box(method, strip_formtastic_options(options).merge(html_options),
1108
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
1109
+ options = options_for_label(options)
1110
+
1111
+ # the label() method will insert this nested input into the label at the last minute
1112
+ options[:label_prefix_for_nested_input] = input
1113
+
1114
+ self.label(method, options)
1115
+ end
1034
1116
 
1035
- input = self.check_box(method, set_options(options).merge(html_options),
1036
- options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
1037
- options = options_for_label(options)
1038
-
1039
- # the label() method will insert this nested input into the label at the last minute
1040
- options[:label_prefix_for_nested_input] = input
1041
-
1042
- self.label(method, options)
1043
- end
1117
+ # Generates an input for the given method using the type supplied with :as.
1118
+ def inline_input_for(method, options)
1119
+ send(:"#{options.delete(:as)}_input", method, options)
1120
+ end
1044
1121
 
1045
- # Generates an input for the given method using the type supplied with :as.
1046
- def inline_input_for(method, options)
1047
- send(:"#{options.delete(:as)}_input", method, options)
1048
- end
1122
+ # Generates hints for the given method using the text supplied in :hint.
1123
+ #
1124
+ def inline_hints_for(method, options) #:nodoc:
1125
+ options[:hint] = localized_string(method, options[:hint], :hint)
1126
+ return if options[:hint].blank?
1127
+ template.content_tag(:p, options[:hint], :class => 'inline-hints')
1128
+ end
1049
1129
 
1050
- # Generates hints for the given method using the text supplied in :hint.
1051
- #
1052
- def inline_hints_for(method, options) #:nodoc:
1053
- options[:hint] = localized_string(method, options[:hint], :hint)
1054
- return if options[:hint].blank?
1055
- template.content_tag(:p, options[:hint], :class => 'inline-hints')
1056
- end
1130
+ # Creates an error sentence by calling to_sentence on the errors array.
1131
+ #
1132
+ def error_sentence(errors) #:nodoc:
1133
+ template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
1134
+ end
1057
1135
 
1058
- # Creates an error sentence by calling to_sentence on the errors array.
1059
- #
1060
- def error_sentence(errors) #:nodoc:
1061
- template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
1062
- end
1136
+ # Creates an error li list.
1137
+ #
1138
+ def error_list(errors) #:nodoc:
1139
+ list_elements = []
1140
+ errors.each do |error|
1141
+ list_elements << template.content_tag(:li, error.untaint)
1142
+ end
1143
+ template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
1144
+ end
1063
1145
 
1064
- # Creates an error li list.
1065
- #
1066
- def error_list(errors) #:nodoc:
1067
- list_elements = []
1068
- errors.each do |error|
1069
- list_elements << template.content_tag(:li, error.untaint)
1146
+ # Creates an error sentence containing only the first error
1147
+ #
1148
+ def error_first(errors) #:nodoc:
1149
+ template.content_tag(:p, errors.first.untaint, :class => 'inline-errors')
1070
1150
  end
1071
- template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
1072
- end
1073
1151
 
1074
- # Creates an error sentence containing only the first error
1075
- #
1076
- def error_first(errors) #:nodoc:
1077
- template.content_tag(:p, errors.first.untaint, :class => 'inline-errors')
1078
- end
1152
+ # Generates the required or optional string. If the value set is a proc,
1153
+ # it evaluates the proc first.
1154
+ #
1155
+ def required_or_optional_string(required) #:nodoc:
1156
+ string_or_proc = case required
1157
+ when true
1158
+ @@required_string
1159
+ when false
1160
+ @@optional_string
1161
+ else
1162
+ required
1163
+ end
1079
1164
 
1080
- # Generates the required or optional string. If the value set is a proc,
1081
- # it evaluates the proc first.
1082
- #
1083
- def required_or_optional_string(required) #:nodoc:
1084
- string_or_proc = case required
1085
- when true
1086
- @@required_string
1087
- when false
1088
- @@optional_string
1165
+ if string_or_proc.is_a?(Proc)
1166
+ string_or_proc.call
1089
1167
  else
1090
- required
1168
+ string_or_proc.to_s
1169
+ end
1091
1170
  end
1092
1171
 
1093
- if string_or_proc.is_a?(Proc)
1094
- string_or_proc.call
1095
- else
1096
- string_or_proc.to_s
1097
- end
1098
- end
1172
+ # Generates a fieldset and wraps the content in an ordered list. When working
1173
+ # with nested attributes (in Rails 2.3), it allows %i as interpolation option
1174
+ # in :name. So you can do:
1175
+ #
1176
+ # f.inputs :name => 'Task #%i', :for => :tasks
1177
+ #
1178
+ # or the shorter equivalent:
1179
+ #
1180
+ # f.inputs 'Task #%i', :for => :tasks
1181
+ #
1182
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
1183
+ # 'Task #3' and so on.
1184
+ #
1185
+ # Note: Special case for the inline inputs (non-block):
1186
+ # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
1187
+ # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
1188
+ # f.inputs :title, :body, :author # First argument is a column => (no legend)
1189
+ #
1190
+ def field_set_and_list_wrapping(*args, &block) #:nodoc:
1191
+ contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
1192
+ html_options = args.extract_options!
1193
+
1194
+ legend = html_options.delete(:name).to_s
1195
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
1196
+ legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
1197
+
1198
+ if block_given?
1199
+ contents = if template.respond_to?(:is_haml?) && template.is_haml?
1200
+ template.capture_haml(&block)
1201
+ else
1202
+ template.capture(&block)
1203
+ end
1204
+ end
1099
1205
 
1100
- # Generates a fieldset and wraps the content in an ordered list. When working
1101
- # with nested attributes (in Rails 2.3), it allows %i as interpolation option
1102
- # in :name. So you can do:
1103
- #
1104
- # f.inputs :name => 'Task #%i', :for => :tasks
1105
- #
1106
- # or the shorter equivalent:
1107
- #
1108
- # f.inputs 'Task #%i', :for => :tasks
1109
- #
1110
- # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
1111
- # 'Task #3' and so on.
1112
- #
1113
- # Note: Special case for the inline inputs (non-block):
1114
- # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
1115
- # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
1116
- # f.inputs :title, :body, :author # First argument is a column => (no legend)
1117
- #
1118
- def field_set_and_list_wrapping(*args, &block) #:nodoc:
1119
- contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
1120
- html_options = args.extract_options!
1206
+ # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
1207
+ contents = contents.join if contents.respond_to?(:join)
1208
+ fieldset = template.content_tag(:fieldset,
1209
+ legend << template.content_tag(:ol, contents),
1210
+ html_options.except(:builder, :parent)
1211
+ )
1121
1212
 
1122
- html_options[:name] ||= html_options.delete(:title)
1123
- if html_options[:name].blank?
1124
- valid_name_classes = [::String, ::Symbol]
1125
- valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
1126
- html_options[:name] = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
1213
+ template.concat(fieldset) if block_given?
1214
+ fieldset
1127
1215
  end
1128
- html_options[:name] = localized_string(html_options[:name], html_options[:name], :title) if html_options[:name].is_a?(::Symbol)
1129
1216
 
1130
- legend = html_options.delete(:name).to_s
1131
- legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
1132
- legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
1217
+ def field_set_title_from_args(*args) #:nodoc:
1218
+ options = args.extract_options!
1219
+ options[:name] ||= options.delete(:title)
1220
+ title = options[:name]
1133
1221
 
1134
- if block_given?
1135
- contents = if template.respond_to?(:is_haml?) && template.is_haml?
1136
- template.capture_haml(&block)
1137
- else
1138
- template.capture(&block)
1222
+ if title.blank?
1223
+ valid_name_classes = [::String, ::Symbol]
1224
+ valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
1225
+ title = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
1139
1226
  end
1227
+ title = localized_string(title, title, :title) if title.is_a?(::Symbol)
1228
+ title
1140
1229
  end
1141
1230
 
1142
- # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
1143
- contents = contents.join if contents.respond_to?(:join)
1144
- fieldset = template.content_tag(:fieldset,
1145
- legend << template.content_tag(:ol, contents),
1146
- html_options.except(:builder, :parent)
1147
- )
1148
-
1149
- template.concat(fieldset) if block_given?
1150
- fieldset
1151
- end
1231
+ # Also generates a fieldset and an ordered list but with label based in
1232
+ # method. This methods is currently used by radio and datetime inputs.
1233
+ #
1234
+ def field_set_and_list_wrapping_for_method(method, options, contents) #:nodoc:
1235
+ contents = contents.join if contents.respond_to?(:join)
1236
+
1237
+ template.content_tag(:fieldset,
1238
+ template.content_tag(:legend,
1239
+ self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
1240
+ ) <<
1241
+ template.content_tag(:ol, contents)
1242
+ )
1243
+ end
1152
1244
 
1153
- # Also generates a fieldset and an ordered list but with label based in
1154
- # method. This methods is currently used by radio and datetime inputs.
1155
- #
1156
- def field_set_and_list_wrapping_for_method(method, options, contents)
1157
- contents = contents.join if contents.respond_to?(:join)
1158
-
1159
- template.content_tag(:fieldset,
1160
- template.content_tag(:legend,
1161
- self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
1162
- ) <<
1163
- template.content_tag(:ol, contents)
1164
- )
1165
- end
1245
+ # For methods that have a database column, take a best guess as to what the input method
1246
+ # should be. In most cases, it will just return the column type (eg :string), but for special
1247
+ # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1248
+ # something different (like :password and :select).
1249
+ #
1250
+ # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
1251
+ # default is a :string, a similar behaviour to Rails' scaffolding.
1252
+ #
1253
+ def default_input_type(method, options = {}) #:nodoc:
1254
+ if column = self.column_for(method)
1255
+ # Special cases where the column type doesn't map to an input method.
1256
+ case column.type
1257
+ when :string
1258
+ return :password if method.to_s =~ /password/
1259
+ return :country if method.to_s =~ /country/
1260
+ return :time_zone if method.to_s =~ /time_zone/
1261
+ when :integer
1262
+ return :select if method.to_s =~ /_id$/
1263
+ return :numeric
1264
+ when :float, :decimal
1265
+ return :numeric
1266
+ when :timestamp
1267
+ return :datetime
1268
+ end
1269
+
1270
+ # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
1271
+ return :select if column.type == :string && options.key?(:collection)
1272
+ # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
1273
+ return column.type
1274
+ else
1275
+ if @object
1276
+ return :select if self.reflection_for(method)
1166
1277
 
1167
- # For methods that have a database column, take a best guess as to what the input method
1168
- # should be. In most cases, it will just return the column type (eg :string), but for special
1169
- # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1170
- # something different (like :password and :select).
1171
- #
1172
- # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
1173
- # default is a :string, a similar behaviour to Rails' scaffolding.
1174
- #
1175
- def default_input_type(method) #:nodoc:
1176
- column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1177
-
1178
- if column
1179
- # handle the special cases where the column type doesn't map to an input method
1180
- return :time_zone if column.type == :string && method.to_s =~ /time_zone/
1181
- return :select if column.type == :integer && method.to_s =~ /_id$/
1182
- return :datetime if column.type == :timestamp
1183
- return :numeric if [:integer, :float, :decimal].include?(column.type)
1184
- return :password if column.type == :string && method.to_s =~ /password/
1185
- return :country if column.type == :string && method.to_s =~ /country/
1186
-
1187
- # otherwise assume the input name will be the same as the column type (eg string_input)
1188
- return column.type
1189
- else
1190
- if @object
1191
- return :select if find_reflection(method)
1278
+ file = @object.send(method) if @object.respond_to?(method)
1279
+ return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
1280
+ end
1192
1281
 
1193
- file = @object.send(method) if @object.respond_to?(method)
1194
- return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
1282
+ return :select if options.key?(:collection)
1283
+ return :password if method.to_s =~ /password/
1284
+ return :string
1195
1285
  end
1286
+ end
1196
1287
 
1197
- return :password if method.to_s =~ /password/
1198
- return :string
1288
+ # Used by select and radio inputs. The collection can be retrieved by
1289
+ # three ways:
1290
+ #
1291
+ # * Explicitly provided through :collection
1292
+ # * Retrivied through an association
1293
+ # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
1294
+ #
1295
+ # If the collection is not a hash or an array of strings, fixnums or arrays,
1296
+ # we use label_method and value_method to retreive an array with the
1297
+ # appropriate label and value.
1298
+ #
1299
+ def find_collection_for_column(column, options) #:nodoc:
1300
+ collection = find_raw_collection_for_column(column, options)
1301
+
1302
+ # Return if we have an Array of strings, fixnums or arrays
1303
+ return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
1304
+ [Array, Fixnum, String, Symbol].include?(collection.first.class)
1305
+
1306
+ label, value = detect_label_and_value_method!(collection, options)
1307
+ collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
1199
1308
  end
1200
- end
1201
1309
 
1202
- # Used by select and radio inputs. The collection can be retrieved by
1203
- # three ways:
1204
- #
1205
- # * Explicitly provided through :collection
1206
- # * Retrivied through an association
1207
- # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
1208
- #
1209
- # If the collection is not a hash or an array of strings, fixnums or arrays,
1210
- # we use label_method and value_method to retreive an array with the
1211
- # appropriate label and value.
1212
- #
1213
- def find_collection_for_column(column, options)
1214
- collection = find_raw_collection_for_column(column, options)
1310
+ # As #find_collection_for_column but returns the collection without mapping the label and value
1311
+ #
1312
+ def find_raw_collection_for_column(column, options) #:nodoc:
1313
+ collection = if options[:collection]
1314
+ options.delete(:collection)
1315
+ elsif reflection = self.reflection_for(column)
1316
+ reflection.klass.find(:all, options[:find_options] || {})
1317
+ else
1318
+ create_boolean_collection(options)
1319
+ end
1215
1320
 
1216
- # Return if we have an Array of strings, fixnums or arrays
1217
- return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
1218
- [Array, Fixnum, String, Symbol].include?(collection.first.class)
1321
+ collection = collection.to_a if collection.is_a?(Hash)
1322
+ collection
1323
+ end
1219
1324
 
1220
- label, value = detect_label_and_value_method!(collection, options)
1325
+ # Detects the label and value methods from a collection values set in
1326
+ # @@collection_label_methods. It will use and delete
1327
+ # the options :label_method and :value_methods when present
1328
+ #
1329
+ def detect_label_and_value_method!(collection_or_instance, options = {}) #:nodoc
1330
+ label = options.delete(:label_method) || detect_label_method(collection_or_instance)
1331
+ value = options.delete(:value_method) || :id
1332
+ [label, value]
1333
+ end
1221
1334
 
1222
- collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
1223
- end
1224
-
1225
- # As #find_collection_for_column but returns the collection without mapping the label and value
1226
- #
1227
- def find_raw_collection_for_column(column, options) #:nodoc:
1228
- reflection = find_reflection(column)
1335
+ # Detected the label collection method when none is supplied using the
1336
+ # values set in @@collection_label_methods.
1337
+ #
1338
+ def detect_label_method(collection) #:nodoc:
1339
+ @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
1340
+ end
1229
1341
 
1230
- collection = if options[:collection]
1231
- options.delete(:collection)
1232
- elsif reflection
1233
- reflection.klass.find(:all, options[:find_options] || {})
1234
- else
1235
- create_boolean_collection(options)
1342
+ # Detects the method to call for fetching group members from the groups when grouping select options
1343
+ #
1344
+ def detect_group_association(method, group_by)
1345
+ object_to_method_reflection = self.reflection_for(method)
1346
+ method_class = object_to_method_reflection.klass
1347
+
1348
+ method_to_group_association = method_class.reflect_on_association(group_by)
1349
+ group_class = method_to_group_association.klass
1350
+
1351
+ # This will return in the normal case
1352
+ return method.to_s.pluralize.to_sym if group_class.reflect_on_association(method.to_s.pluralize)
1353
+
1354
+ # This is for belongs_to associations named differently than their class
1355
+ # form.input :parent, :group_by => :customer
1356
+ # eg.
1357
+ # class Project
1358
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1359
+ # belongs_to :customer
1360
+ # end
1361
+ # class Customer
1362
+ # has_many :projects
1363
+ # end
1364
+ group_method = method_class.to_s.underscore.pluralize.to_sym
1365
+ return group_method if group_class.reflect_on_association(group_method) # :projects
1366
+
1367
+ # This is for has_many associations named differently than their class
1368
+ # eg.
1369
+ # class Project
1370
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1371
+ # belongs_to :customer
1372
+ # end
1373
+ # class Customer
1374
+ # has_many :tasks, :class_name => 'Project', :foreign_key => 'customer_id'
1375
+ # end
1376
+ possible_associations = group_class.reflect_on_all_associations(:has_many).find_all{|assoc| assoc.klass == object_class}
1377
+ return possible_associations.first.name.to_sym if possible_associations.count == 1
1378
+
1379
+ raise "Cannot infer group association for #{method} grouped by #{group_by}, there were #{possible_associations.empty? ? 'no' : possible_associations.size} possible associations. Please specify using :group_association"
1380
+
1236
1381
  end
1237
1382
 
1238
- collection = collection.to_a if collection.is_a?(Hash)
1383
+ # Returns a hash to be used by radio and select inputs when a boolean field
1384
+ # is provided.
1385
+ #
1386
+ def create_boolean_collection(options) #:nodoc:
1387
+ options[:true] ||= ::Formtastic::I18n.t(:yes)
1388
+ options[:false] ||= ::Formtastic::I18n.t(:no)
1389
+ options[:value_as_class] = true unless options.key?(:value_as_class)
1239
1390
 
1240
- collection
1241
- end
1242
-
1243
- # Detects the label and value methods from a collection values set in
1244
- # @@collection_label_methods. It will use and delete
1245
- # the options :label_method and :value_methods when present
1246
- #
1247
- def detect_label_and_value_method!(collection_or_instance, options = {}) #:nodoc
1248
- label = options.delete(:label_method) || detect_label_method(collection_or_instance)
1249
- value = options.delete(:value_method) || :id
1250
- [label, value]
1251
- end
1391
+ [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1392
+ end
1252
1393
 
1253
- # Detected the label collection method when none is supplied using the
1254
- # values set in @@collection_label_methods.
1255
- #
1256
- def detect_label_method(collection) #:nodoc:
1257
- @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
1258
- end
1394
+ # Used by association inputs (select, radio) to generate the name that should
1395
+ # be used for the input
1396
+ #
1397
+ # belongs_to :author; f.input :author; will generate 'author_id'
1398
+ # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1399
+ # has_many :authors; f.input :authors; will generate 'author_ids'
1400
+ # has_and_belongs_to_many will act like has_many
1401
+ #
1402
+ def generate_association_input_name(method) #:nodoc:
1403
+ if reflection = self.reflection_for(method)
1404
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1405
+ "#{method.to_s.singularize}_ids"
1406
+ else
1407
+ reflection.options[:foreign_key] || "#{method}_id"
1408
+ end
1409
+ else
1410
+ method
1411
+ end.to_sym
1412
+ end
1259
1413
 
1260
- # Returns a hash to be used by radio and select inputs when a boolean field
1261
- # is provided.
1262
- #
1263
- def create_boolean_collection(options)
1264
- options[:true] ||= ::Formtastic::I18n.t(:yes)
1265
- options[:false] ||= ::Formtastic::I18n.t(:no)
1266
- options[:value_as_class] = true unless options.key?(:value_as_class)
1414
+ # If an association method is passed in (f.input :author) try to find the
1415
+ # reflection object.
1416
+ #
1417
+ def reflection_for(method) #:nodoc:
1418
+ @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1419
+ end
1267
1420
 
1268
- [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1269
- end
1421
+ # Get a column object for a specified attribute method - if possible.
1422
+ #
1423
+ def column_for(method) #:nodoc:
1424
+ @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1425
+ end
1270
1426
 
1271
- # Used by association inputs (select, radio) to generate the name that should
1272
- # be used for the input
1273
- #
1274
- # belongs_to :author; f.input :author; will generate 'author_id'
1275
- # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1276
- # has_many :authors; f.input :authors; will generate 'author_ids'
1277
- # has_and_belongs_to_many will act like has_many
1278
- #
1279
- def generate_association_input_name(method)
1280
- if reflection = find_reflection(method)
1281
- if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1282
- "#{method.to_s.singularize}_ids"
1427
+ # Generates default_string_options by retrieving column information from
1428
+ # the database.
1429
+ #
1430
+ def default_string_options(method, type) #:nodoc:
1431
+ column = self.column_for(method)
1432
+
1433
+ if type == :numeric || column.nil? || column.limit.nil?
1434
+ { :size => @@default_text_field_size }
1283
1435
  else
1284
- reflection.options[:foreign_key] || "#{method}_id"
1436
+ { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
1285
1437
  end
1286
- else
1287
- method
1288
- end.to_sym
1289
- end
1290
-
1291
- # If an association method is passed in (f.input :author) try to find the
1292
- # reflection object.
1293
- #
1294
- def find_reflection(method)
1295
- @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1296
- end
1438
+ end
1297
1439
 
1298
- # Generates default_string_options by retrieving column information from
1299
- # the database.
1300
- #
1301
- def default_string_options(method, type) #:nodoc:
1302
- column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1440
+ # Generate the html id for the li tag.
1441
+ # It takes into account options[:index] and @auto_index to generate li
1442
+ # elements with appropriate index scope. It also sanitizes the object
1443
+ # and method names.
1444
+ #
1445
+ def generate_html_id(method_name, value='input') #:nodoc:
1446
+ if options.has_key?(:index)
1447
+ index = "_#{options[:index]}"
1448
+ elsif defined?(@auto_index)
1449
+ index = "_#{@auto_index}"
1450
+ else
1451
+ index = ""
1452
+ end
1453
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1303
1454
 
1304
- if type == :numeric || column.nil? || column.limit.nil?
1305
- { :size => @@default_text_field_size }
1306
- else
1307
- { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
1455
+ "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1308
1456
  end
1309
- end
1310
1457
 
1311
- # Generate the html id for the li tag.
1312
- # It takes into account options[:index] and @auto_index to generate li
1313
- # elements with appropriate index scope. It also sanitizes the object
1314
- # and method names.
1315
- #
1316
- def generate_html_id(method_name, value='input')
1317
- if options.has_key?(:index)
1318
- index = "_#{options[:index]}"
1319
- elsif defined?(@auto_index)
1320
- index = "_#{@auto_index}"
1321
- else
1322
- index = ""
1458
+ # Gets the nested_child_index value from the parent builder. In Rails 2.3
1459
+ # it always returns a fixnum. In next versions it returns a hash with each
1460
+ # association that the parent builds.
1461
+ #
1462
+ def parent_child_index(parent) #:nodoc:
1463
+ duck = parent[:builder].instance_variable_get('@nested_child_index')
1464
+
1465
+ if duck.is_a?(Hash)
1466
+ child = parent[:for]
1467
+ child = child.first if child.respond_to?(:first)
1468
+ duck[child].to_i + 1
1469
+ else
1470
+ duck.to_i + 1
1471
+ end
1323
1472
  end
1324
- sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1325
-
1326
- "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1327
- end
1328
1473
 
1329
- # Gets the nested_child_index value from the parent builder. In Rails 2.3
1330
- # it always returns a fixnum. In next versions it returns a hash with each
1331
- # association that the parent builds.
1332
- #
1333
- def parent_child_index(parent)
1334
- duck = parent[:builder].instance_variable_get('@nested_child_index')
1335
-
1336
- if duck.is_a?(Hash)
1337
- child = parent[:for]
1338
- child = child.first if child.respond_to?(:first)
1339
- duck[child].to_i + 1
1340
- else
1341
- duck.to_i + 1
1474
+ def sanitized_object_name #:nodoc:
1475
+ @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1342
1476
  end
1343
- end
1344
-
1345
- def sanitized_object_name
1346
- @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1347
- end
1348
1477
 
1349
- def humanized_attribute_name(method)
1350
- if @object && @object.class.respond_to?(:human_attribute_name)
1351
- @object.class.human_attribute_name(method.to_s)
1352
- else
1478
+ def humanized_attribute_name(method) #:nodoc:
1353
1479
  method.to_s.send(@@label_str_method)
1354
1480
  end
1355
- end
1356
-
1357
- # Internal generic method for looking up localized values within Formtastic
1358
- # using I18n, if no explicit value is set and I18n-lookups are enabled.
1359
- #
1360
- # Enabled/Disable this by setting:
1361
- #
1362
- # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1363
- #
1364
- # Lookup priority:
1365
- #
1366
- # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
1367
- # 'formtastic.{{type}}.{{model}}.{{attribute}}'
1368
- # 'formtastic.{{type}}.{{attribute}}'
1369
- #
1370
- # Example:
1371
- #
1372
- # 'formtastic.labels.post.edit.title'
1373
- # 'formtastic.labels.post.title'
1374
- # 'formtastic.labels.title'
1375
- #
1376
- # NOTE: Generic, but only used for form input titles/labels/hints/actions (titles = legends, actions = buttons).
1377
- #
1378
- def localized_string(key, value, type, options = {})
1379
- key = value if value.is_a?(::Symbol)
1380
1481
 
1381
- if value.is_a?(::String)
1382
- value
1383
- else
1384
- use_i18n = value.nil? ? @@i18n_lookups_by_default : (value != false)
1385
-
1386
- if use_i18n
1387
- model_name = (@object ? @object.class.name : @object_name.to_s.send(@@label_str_method)).underscore
1388
- action_name = template.params[:action].to_s rescue ''
1389
- attribute_name = key.to_s
1390
-
1391
- defaults = I18N_SCOPES.collect do |i18n_scope|
1392
- i18n_path = i18n_scope.dup
1393
- i18n_path.gsub!('{{action}}', action_name)
1394
- i18n_path.gsub!('{{model}}', model_name)
1395
- i18n_path.gsub!('{{attribute}}', attribute_name)
1396
- i18n_path.gsub!('..', '.')
1397
- i18n_path.to_sym
1482
+ # Internal generic method for looking up localized values within Formtastic
1483
+ # using I18n, if no explicit value is set and I18n-lookups are enabled.
1484
+ #
1485
+ # Enabled/Disable this by setting:
1486
+ #
1487
+ # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1488
+ #
1489
+ # Lookup priority:
1490
+ #
1491
+ # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
1492
+ # 'formtastic.{{type}}.{{model}}.{{attribute}}'
1493
+ # 'formtastic.{{type}}.{{attribute}}'
1494
+ #
1495
+ # Example:
1496
+ #
1497
+ # 'formtastic.labels.post.edit.title'
1498
+ # 'formtastic.labels.post.title'
1499
+ # 'formtastic.labels.title'
1500
+ #
1501
+ # NOTE: Generic, but only used for form input titles/labels/hints/actions (titles = legends, actions = buttons).
1502
+ #
1503
+ def localized_string(key, value, type, options = {}) #:nodoc:
1504
+ key = value if value.is_a?(::Symbol)
1505
+
1506
+ if value.is_a?(::String)
1507
+ value
1508
+ else
1509
+ use_i18n = value.nil? ? @@i18n_lookups_by_default : (value != false)
1510
+
1511
+ if use_i18n
1512
+ model_name = self.model_name.underscore
1513
+ action_name = template.params[:action].to_s rescue ''
1514
+ attribute_name = key.to_s
1515
+
1516
+ defaults = ::Formtastic::I18n::SCOPES.collect do |i18n_scope|
1517
+ i18n_path = i18n_scope.dup
1518
+ i18n_path.gsub!('{{action}}', action_name)
1519
+ i18n_path.gsub!('{{model}}', model_name)
1520
+ i18n_path.gsub!('{{attribute}}', attribute_name)
1521
+ i18n_path.gsub!('..', '.')
1522
+ i18n_path.to_sym
1523
+ end
1524
+ defaults << ''
1525
+
1526
+ i18n_value = ::Formtastic::I18n.t(defaults.shift,
1527
+ options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
1528
+ i18n_value.blank? ? nil : i18n_value
1398
1529
  end
1399
- defaults << ''
1400
-
1401
- i18n_value = ::Formtastic::I18n.t(defaults.shift,
1402
- options.merge(:default => defaults,:scope => type.to_s.pluralize.to_sym))
1403
- i18n_value.blank? ? nil : i18n_value
1404
1530
  end
1405
1531
  end
1406
- end
1407
1532
 
1408
- def send_or_call(duck, object)
1409
- if duck.is_a?(Proc)
1410
- duck.call(object)
1411
- else
1412
- object.send(duck)
1533
+ def model_name
1534
+ @object.present? ? @object.class.name : @object_name.to_s.classify
1413
1535
  end
1414
- end
1415
1536
 
1416
- def set_include_blank(options)
1417
- unless options.key?(:include_blank) || options.key?(:prompt)
1418
- options[:include_blank] = @@include_blank_for_select_by_default
1537
+ def send_or_call(duck, object)
1538
+ if duck.is_a?(Proc)
1539
+ duck.call(object)
1540
+ else
1541
+ object.send(duck)
1542
+ end
1543
+ end
1544
+
1545
+ def set_include_blank(options)
1546
+ unless options.key?(:include_blank) || options.key?(:prompt)
1547
+ options[:include_blank] = @@include_blank_for_select_by_default
1548
+ end
1549
+ options
1419
1550
  end
1420
- options
1421
- end
1422
1551
 
1423
1552
  end
1424
1553
 
@@ -1480,7 +1609,7 @@ module Formtastic #:nodoc:
1480
1609
  src = <<-END_SRC
1481
1610
  def semantic_#{meth}(record_or_name_or_array, *args, &proc)
1482
1611
  options = args.extract_options!
1483
- options[:builder] = @@builder
1612
+ options[:builder] ||= @@builder
1484
1613
  options[:html] ||= {}
1485
1614
 
1486
1615
  class_names = options[:html][:class] ? options[:html][:class].split(" ") : []