formtastic/formtastic

View on GitHub
lib/formtastic/inputs/base/timeish.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true
module Formtastic
  module Inputs
    module Base
      # Timeish inputs (`:date_select`, `:datetime_select`, `:time_select`) are similar to the Rails date and time 
      # helpers (`date_select`, `datetime_select`, `time_select`), rendering a series of `<select>`
      # tags for each fragment (year, month, day, hour, minute, seconds). The fragments are then 
      # re-combined to a date by ActiveRecord through multi-parameter assignment.
      #
      # The mark-up produced by Rails is simple but far from ideal, with no way to label the 
      # individual fragments for accessibility, no fieldset to group the related fields, and no
      # legend describing the group. Formtastic addresses this within the standard `<li>` wrapper 
      # with a `<fieldset>` with a `<legend>` as a label, followed by an ordered list (`<ol>`) of 
      # list items (`<li>`), one for each fragment (year, month, ...). Each `<li>` fragment contains
      # a `<label>` (eg "Year") for the fragment, and a `<select>` containing `<option>`s (eg a 
      # range of years).
      #
      # In the supplied formtastic.css file, the resulting mark-up is styled to appear a lot like a
      # standard Rails date time select by:
      #
      # * styling the legend to look like the other labels (to the left hand side of the selects)
      # * floating the `<li>` fragments against each other as a single line
      # * hiding the `<label>` of each fragment with `display:none`
      #
      # @example `:date_select` input with full form context and sample HTMl output
      #
      #   <%= semantic_form_for(@post) do |f| %>
      #     <%= f.inputs do %>
      #       ...
      #       <%= f.input :publish_at, :as => :date_select %>
      #     <% end %>
      #   <% end %>
      #
      #   <form...>
      #     <fieldset class="inputs">
      #       <ol>
      #         <li class="date">
      #           <fieldset class="fragments">
      #             <ol class="fragments-group">
      #               <li class="fragment">
      #                 <label for="post_publish_at_1i">Year</label>
      #                 <select id="post_publish_at_1i" name="post[publish_at_1i]">...</select>
      #               </li>
      #               <li class="fragment">
      #                 <label for="post_publish_at_2i">Month</label>
      #                 <select id="post_publish_at_2i" name="post[publish_at_2i]">...</select>
      #               </li>
      #               <li class="fragment">
      #                 <label for="post_publish_at_3i">Day</label>
      #                 <select id="post_publish_at_3i" name="post[publish_at_3i]">...</select>
      #               </li>
      #             </ol>
      #           </fieldset>
      #         </li>
      #       </ol>
      #     </fieldset>
      #   </form>
      #       
      #
      # @example `:time_select` input
      #   <%= f.input :publish_at, :as => :time_select %>
      #
      # @example `:datetime_select` input
      #   <%= f.input :publish_at, :as => :datetime_select %>
      #
      # @example Change the labels for each fragment
      #   <%= f.input :publish_at, :as => :date_select, :labels => { :year => "Y", :month => "M", :day => "D" }  %>
      #
      # @example Suppress the labels for all fragments
      #   <%= f.input :publish_at, :as => :date_select, :labels => false  %>
      #
      # @example Skip a fragment (defaults to 1, skips all following fragments)
      #   <%= f.input :publish_at, :as => :datetime_select, :discard_minute => true  %>
      #   <%= f.input :publish_at, :as => :datetime_select, :discard_hour => true  %>
      #   <%= f.input :publish_at, :as => :datetime_select, :discard_day => true  %>
      #   <%= f.input :publish_at, :as => :datetime_select, :discard_month => true  %>
      #   <%= f.input :publish_at, :as => :datetime_select, :discard_year => true  %>
      #
      # @example Change the order
      #   <%= f.input :publish_at, :as => :date_select, :order => [:month, :day, :year]  %>
      #
      # @example Include seconds with times (excluded by default)
      #   <%= f.input :publish_at, :as => :time_select, :include_seconds => true %>
      #
      # @example Specify if there should be a blank option at the start of each select or not. Note that, unlike select inputs, :include_blank does not accept a string value.
      #   <%= f.input :publish_at, :as => :time_select, :include_blank => true %>
      #   <%= f.input :publish_at, :as => :time_select, :include_blank => false %>
      #
      # @example Provide a value for the field via selected
      #   <%= f.input :publish_at, :as => :datetime_select, :selected => DateTime.new(2018, 10, 4, 12, 00)
      #
      # @todo Document i18n
      # @todo Check what other Rails options are supported (`start_year`, `end_year`, `use_month_numbers`, `use_short_month`, `add_month_numbers`, `prompt`), write tests for them, and otherwise support them
      # @todo Could we take the rendering from Rails' helpers and inject better HTML in and around it rather than re-inventing the whee?
      module Timeish
        
        def to_html
          input_wrapping do
            fragments_wrapping do
              hidden_fragments <<
              fragments_label <<
              template.content_tag(:ol,
                fragments.map do |fragment|
                  fragment_wrapping do
                    fragment_label_html(fragment) <<
                    fragment_input_html(fragment)
                  end
                end.join.html_safe, # TODO is this safe?
                { :class => 'fragments-group' } # TODO refactor to fragments_group_wrapping
              )
            end
          end
        end
        
        def fragments
          date_fragments + time_fragments
        end
        
        def time_fragments
          options[:include_seconds] ? [:hour, :minute, :second] : [:hour, :minute]
        end
        
        def date_fragments
          options[:order] || i18n_date_fragments || default_date_fragments
        end
        
        def default_date_fragments
          [:year, :month, :day]
        end
        
        def fragment_wrapping(&block)
          template.content_tag(:li, template.capture(&block), fragment_wrapping_html_options)
        end
        
        def fragment_wrapping_html_options
          { :class => 'fragment' }
        end
        
        def fragment_label(fragment)
          labels_from_options = options.key?(:labels) ? options[:labels] : {}
          if !labels_from_options
            ''
          elsif labels_from_options.key?(fragment)
            labels_from_options[fragment]
          else
            ::I18n.t(fragment.to_s, :default => fragment.to_s.humanize, :scope => [:datetime, :prompts])
          end
        end
        
        def fragment_id(fragment)
          "#{input_html_options[:id]}_#{position(fragment)}i"
        end
        
        def fragment_name(fragment)
          "#{method}(#{position(fragment)}i)"
        end
        
        def fragment_label_html(fragment)
          text = fragment_label(fragment)
          text.blank? ? +"".html_safe : template.content_tag(:label, text, :for => fragment_id(fragment))
        end
        
        def value
          return input_options[:selected] if options.key?(:selected)
          object.send(method) if object && object.respond_to?(method)
        end
        
        def fragment_input_html(fragment)
          opts = input_options.merge(:prefix => fragment_prefix, :field_name => fragment_name(fragment), :default => value, :include_blank => include_blank?)
          template.send(:"select_#{fragment}", value, opts, input_html_options.merge(:id => fragment_id(fragment)))
        end
        
        def fragment_prefix
          if builder.options.key?(:index)
            object_name + "[#{builder.options[:index]}]"
          else
            object_name
          end
        end
        
        # TODO extract to BlankOptions or similar -- Select uses similar code
        def include_blank?
          options.key?(:include_blank) ? options[:include_blank] : builder.include_blank_for_select_by_default
        end
        
        def positions
          { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
        end
        
        def position(fragment)
          positions[fragment]
        end
        
        def i18n_date_fragments
          order = ::I18n.t(:order, :scope => [:date])
          if order.is_a?(Array)
            order.map &:to_sym
          else
            nil
          end
        end
        
        def fragments_wrapping(&block)
          template.content_tag(:fieldset,
            template.capture(&block).html_safe, 
            fragments_wrapping_html_options
          )
        end
        
        def fragments_wrapping_html_options
          { :class => "fragments" }
        end
        
        def fragments_label
          if render_label?
            template.content_tag(:legend, 
              builder.label(method, label_text, :for => fragment_id(fragments.first)), 
              :class => "label"
            )
          else
            +"".html_safe
          end
        end
        
        def fragments_inner_wrapping(&block)
          template.content_tag(:ol,
            template.capture(&block)
          )
        end
        
        def hidden_fragments
          +"".html_safe
        end
        
        def hidden_field_name(fragment)
          if builder.options.key?(:index)
            "#{object_name}[#{builder.options[:index]}][#{fragment_name(fragment)}]"
          else
            "#{object_name}[#{fragment_name(fragment)}]"
          end
        end
        
      end
    end
  end
end