lib/ex_teal/fields/select.ex

defmodule ExTeal.Fields.Select do
  @moduledoc """
  The `Select` field may be used to generate a drop-down select menu.

  The select menu's options may be defined using the `Select.options/2` function:

      Select.make(:size)
      |> Select.options(["Small", "Medium"])
  """

  use ExTeal.Field
  alias ExTeal.Field

  def component, do: "select"

  @doc """
  Define the options for the select field.  The function accepts a list of
  options that are either strings or maps with of `value` and `label` keys.  If
  the list members are strings, the value will be used for both the value and label
  of the `<option>` element it represents.

  `options` are expected to be an enumerable which will be used to generate
  each respective `option`.  The enumerable may have:

    * keyword lists - each keyword list is expected to have the keys `:key` and
      `:value`.  Additional keys such as `:disabled` may be given to customize
      the option

    * two-item tuples - where the first element is an atom, string or integer to
      be used as the option label and the second element is an atom, string or
      integer to be used as the option value

    * atom, string or integer - which will be used as both label and value for
      the generated select

  ## Optgroups

  If `options` is a map or keyword list where the firs element is a string, atom,
  or integer and the second element is a list or a map, it is assumed the key
  will be wrapped in an `<optgroup>` and teh value will be used to generate
  `<options>` nested under the group.

  This functionality is equivalent to `Phoenix.HTML.Form.select/3`
  """
  def options(field, options) do
    %{
      field
      | options: Map.put_new(field.options, :field_options, Field.transform_options(options))
    }
  end

  @doc """
  At times it's convenient to be able to search or filter the list of options in a
  select field.  You can enable this by calling `Select.searchable` on the field:

      Select.make(:type) |> Select.options(~w(foo bar)) |> Select.searchable()

  When using this field, Teal will display an input field which allows you to filter
  the list based on it's key.
  """
  @spec searchable(Field.t()) :: Field.t()
  def searchable(field) do
    %{field | options: Map.put_new(field.options, :searchable, true)}
  end

  def with_options(field, options) when is_map(options) do
    IO.warn("with_options/2 is depreciated.  See `ExTeal.Fields.Select.options/2`")
    options(field, options)
  end

  def with_options(field, options_fn) when is_function(options_fn) do
    IO.warn("with_options/2 is depreciated.  See `ExTeal.Fields.Select.options/2`")
    option_value = options_fn.()
    options(field, option_value)
  end

  def display_using_labels(field) do
    IO.warn("display_using_labels/1 is depreciated.  See `ExTeal.Fields.Select.options/2`")
    field
  end

  @impl true
  def value_for(field, model, view) when view in [:show, :index] do
    value = ExTeal.Field.value_for(field, model, view)

    if field_represents_an_enum?(field, model) do
      value
    else
      option_values = all_options_for(field)
      option = Enum.find(option_values, &(&1.value == value)) || %{}
      Map.get(option, :key, nil)
    end
  end

  def value_for(field, model, view), do: Field.value_for(field, model, view)

  def all_options_for(field) do
    field.options
    |> Map.get(:field_options, [])
    |> Enum.map(fn
      %{group: _group, options: group_options} -> group_options
      option -> option
    end)
    |> List.flatten()
  end

  @impl true
  def filterable_as, do: ExTeal.FieldFilter.Select

  @impl true
  def apply_options_for(%Field{options: options} = field, model, _conn, _type) do
    if field_represents_an_enum?(field, model) and Map.fetch(options, :field_options) == :error do
      {:parameterized, Ecto.Enum, details} = schema_field_type(field, model)
      enum_options = Field.transform_options(details.mappings)
      %{field | options: Map.put(field.options, :field_options, enum_options)}
    else
      field
    end
  end

  defp field_represents_an_enum?(_field, model) when not is_struct(model), do: false

  defp field_represents_an_enum?(field, model) do
    case schema_field_type(field, model) do
      {:parameterized, Ecto.Enum, _} ->
        true

      _ ->
        false
    end
  end

  defp schema_field_type(field, model) do
    model.__struct__.__schema__(:type, field.field)
  end
end