lib/fluint_ui/api/classes.ex

defmodule FlintUI.API.Classes do
  @moduledoc """
  CSS class helpers.
  """

  @doc """
  Builds a class string, concatenates a class string, out of a list of:
  * Strings;
  * Atoms;
  * Keyword lists or maps where the keys are the CSS class and the
  values are treated as booleans for conditional inclusion of the class.

  All `nil`, `false` and duplicated values are filtered out.

  ## Examples

      iex> cx(["foo", "bar"])
      "foo bar"

      iex> cx([foo: false, bar: true])
      "bar"

      iex> cx([[a: true, b: false], [c: false, d: true]])
      "a d"

      iex> cx(["one", :two])
      "one two"

      iex> cx(yes: true, no: false)
      "yes"

      iex> cx(%{"a" => true, b: false})
      "a"

      iex> cx(t: "I'm truthy", f: nil)
      "t"

      iex> cx([[[nil, false, "hello", :world]]])
      "hello world"
  """
  def cx(classes) do
    [classes]
    |> flatten_classes()
    |> Enum.uniq()
    |> Enum.join(" ")
    |> String.trim()
    |> case do
      "" -> nil
      classes -> classes
    end
  end

  defguardp is_class(c) when is_binary(c) or is_atom(c)

  # Return a list of classes in string or atom form.
  defp classes(nil), do: []
  defp classes(false), do: []
  defp classes(s) when is_class(s), do: [to_string(s)]
  defp classes({k, false}) when is_class(k), do: []
  defp classes({k, nil}) when is_class(k), do: []
  defp classes({k, _}) when is_class(k), do: [to_string(k)]
  defp classes(%{} = map), do: Enum.flat_map(map, &classes/1)
  defp classes(_), do: []

  defp flatten_classes(class_list) do
    class_list
    |> List.flatten()
    |> Enum.flat_map(&classes/1)
    |> Enum.filter(&(!!&1 && &1 != ""))
  end
end