defmodule PrimerLive.Helpers.AttributeHelpers do @moduledoc false use Phoenix.Component alias Phoenix.LiveView.JS alias PrimerLive.Helpers.{ComponentHelpers, FormHelpers, PromptHelpers} @doc ~S""" Concatenates a list of classnames to a single string. - Ignores any nil or false entries - Removes duplicate entries - Trims whitespaces Returns nil if the resulting output is an empty string. ## Tests iex> PrimerLive.Helpers.AttributeHelpers.classnames([]) nil iex> PrimerLive.Helpers.AttributeHelpers.classnames([""]) nil iex> PrimerLive.Helpers.AttributeHelpers.classnames(["foo ", nil, " bar ", false]) "foo bar" iex> PrimerLive.Helpers.AttributeHelpers.classnames(["foo", nil, " foo "]) "foo" iex> is_foo = true iex> is_bar = false iex> PrimerLive.Helpers.AttributeHelpers.classnames([ ...> is_foo && "foo", ...> is_bar && "bar" ...> ]) "foo" iex> PrimerLive.Helpers.AttributeHelpers.classnames([nil, ["class1", "class2"], "class3"]) "class1 class2 class3" """ def classnames(input_classnames) do concat(input_classnames_normalize(input_classnames), " ") end def inline_styles(input_styles) do concat(input_styles, "; ") end defp concat(list, joiner) do result = list |> Enum.reject(&(is_nil(&1) || &1 == false || &1 == "")) |> Enum.map(&String.trim(&1)) |> Enum.uniq() |> Enum.join(joiner) if result == "" do nil else result end end # Make it works with HEEx and Surface # - [nil, "class1", "class2"] (HEEx | ~H\""" \""") # - [nil, ["class1", "class2"], "class3"] (.sface | ~F\""" \"""") defp input_classnames_normalize(input_classnames) do input_classnames |> Enum.map(&input_classnames_list_to_string/1) end defp input_classnames_list_to_string(class) when is_list(class), do: Enum.join(class, " ") defp input_classnames_list_to_string(class), do: class @doc ~S""" Concatenates 2 keyword lists of attributes. - Ignores any nil or false entries - Removes duplicate entries ## Tests iex> PrimerLive.Helpers.AttributeHelpers.append_attributes([], [[]]) [] iex> PrimerLive.Helpers.AttributeHelpers.append_attributes([[]]) [] iex> PrimerLive.Helpers.AttributeHelpers.append_attributes([dir: "rtl"], [[]]) [dir: "rtl"] iex> PrimerLive.Helpers.AttributeHelpers.append_attributes([dir: "rtl"], [[placeholder: "hello"]]) [dir: "rtl", placeholder: "hello"] iex> PrimerLive.Helpers.AttributeHelpers.append_attributes([dir: "rtl"], %{"placeholder" => "hello"}) [dir: "rtl", placeholder: "hello"] iex> PrimerLive.Helpers.AttributeHelpers.append_attributes([dir: "rtl"], [false, [placeholder: "hello"], [placeholder: "hello"], nil]) [dir: "rtl", placeholder: "hello"] iex> is_foo = true iex> is_bar = false iex> extra = [class: "x"] iex> PrimerLive.Helpers.AttributeHelpers.append_attributes(extra, [ ...> is_foo && [foo: "foo"], ...> is_bar && [bar: "bar"] ...> ]) [class: "x", foo: "foo"] iex> is_foo = true iex> is_bar = false iex> extra = %{class: "x"} iex> PrimerLive.Helpers.AttributeHelpers.append_attributes(extra, [ ...> is_foo && [foo: "foo"], ...> is_bar && [bar: "bar"] ...> ]) [class: "x", foo: "foo"] """ def append_attributes(attributes, input_attributes) when is_map(attributes) do attr_list = convert_map_to_keyword_list(attributes) append_attributes(attr_list, input_attributes) end def append_attributes(attributes, input_attributes) when is_map(input_attributes) do input_attributes_list = convert_map_to_keyword_list(input_attributes) append_attributes(attributes, input_attributes_list) end def append_attributes(attributes, input_attributes) when is_nil(attributes), do: append_attributes([], input_attributes) def append_attributes(attributes, input_attributes) do flat_input_attributes = input_attributes |> Enum.reject(&(&1 == false || is_nil(&1))) |> List.flatten() flat_attributes = attributes |> List.flatten() flat_attributes |> Keyword.merge(flat_input_attributes) |> Enum.uniq() |> Enum.sort() end def append_attributes(input_attributes), do: append_attributes([], input_attributes) @doc ~S""" Passes arguments to Phoenix.LiveView.Helpers.assigns_to_attributes and sorts the result. """ def assigns_to_attributes_sorted(assigns, exclude \\ []) do assigns_to_attributes(assigns, exclude) |> Enum.sort() end defp convert_map_to_keyword_list(map) when map == %{}, do: nil defp convert_map_to_keyword_list(map) do Keyword.new(map, fn {k, v} -> {to_atom(k), v} end) end defp to_atom(key) when is_atom(key), do: key defp to_atom(key) when is_binary(key), do: String.to_existing_atom(key) defp to_atom(key), do: key @doc ~S""" Converts user input to an integer, with optionally a default value ## Tests iex> PrimerLive.Helpers.AttributeHelpers.as_integer(nil) nil iex> PrimerLive.Helpers.AttributeHelpers.as_integer("1") 1 iex> PrimerLive.Helpers.AttributeHelpers.as_integer(2) 2 iex> PrimerLive.Helpers.AttributeHelpers.as_integer(2.0) 2 iex> PrimerLive.Helpers.AttributeHelpers.as_integer("x") nil iex> PrimerLive.Helpers.AttributeHelpers.as_integer("x", 42) 42 """ def as_integer(input, default_value \\ nil) def as_integer(input, _default_value) when is_integer(input), do: input def as_integer(input, _default_value) when is_float(input), do: trunc(input) def as_integer(input, default_value) when is_binary(input) do String.to_integer(input) rescue ArgumentError -> default_value end def as_integer(_input, default_value), do: default_value @doc ~S""" Converts user input to a boolean, with optionally a default value ## Tests iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(true) true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("true") true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("1") true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(1) true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(1.0) true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(false) false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("false") false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("0") false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(0) false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(0.0) false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("x") false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("100") true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("100.0") true iex> PrimerLive.Helpers.AttributeHelpers.as_boolean("0.1") false iex> PrimerLive.Helpers.AttributeHelpers.as_boolean(%{}) false """ def as_boolean(input, default_value \\ false) def as_boolean(input, _default_value) when is_boolean(input), do: input def as_boolean(input, _default_value) when is_binary(input) and input === "true", do: true def as_boolean(input, _default_value) when is_binary(input) and input === "1", do: true def as_boolean(input, _default_value) when is_binary(input) and input === "false", do: false def as_boolean(input, _default_value) when is_binary(input) and input === "0", do: false def as_boolean(input, default_value) when is_binary(input) do case Integer.parse(input) do {int, _rest} -> as_boolean(int, default_value) _ -> default_value end end def as_boolean(input, _default_value) when is_number(input) do if input > 0 do true else false end end def as_boolean(_input, default_value), do: default_value @doc """ Takes 2 lists and padds the shortest list with placeholder values. ## Tests iex> PrimerLive.Helpers.AttributeHelpers.pad_lists([], []) [[], []] iex> PrimerLive.Helpers.AttributeHelpers.pad_lists([1, 2, 3], []) [[1, 2, 3], [:placeholder, :placeholder, :placeholder]] iex> PrimerLive.Helpers.AttributeHelpers.pad_lists([], [1, 2, 3]) [[:placeholder, :placeholder, :placeholder], [1, 2, 3]] iex> PrimerLive.Helpers.AttributeHelpers.pad_lists([1], [1, 2]) [[1, :placeholder], [1, 2]] iex> PrimerLive.Helpers.AttributeHelpers.pad_lists([], [1, 2, 3], 0) [[0, 0, 0], [1, 2, 3]] iex> header_slot = [] iex> header_title_slot = [%{ ...> __slot__: :header_title, ...> inner_block: "Title content" ...> }] iex> Enum.zip(PrimerLive.Helpers.AttributeHelpers.pad_lists(header_slot, header_title_slot, [])) [{[], %{__slot__: :header_title, inner_block: "Title content"}}] """ def pad_lists(list1, list2, padding \\ :placeholder) do case Enum.count(list1) > Enum.count(list2) do true -> [ list1, pad_list(list1, list2, padding) ] false -> [ pad_list(list2, list1, padding), list2 ] end end defp pad_list(longest, shortest, padding) do longest |> Enum.with_index() |> Enum.map(fn {_, idx} -> Enum.at(shortest, idx) || padding end) end @doc """ Forces a value to be within a min and max value.any() ## Tests iex> PrimerLive.Helpers.AttributeHelpers.minmax(-1, 0, 2) 0 iex> PrimerLive.Helpers.AttributeHelpers.minmax(3, 0, 2) 2 iex> PrimerLive.Helpers.AttributeHelpers.minmax(0, 0, 0) 0 iex> PrimerLive.Helpers.AttributeHelpers.minmax(1, 0, 0) 0 """ def minmax(value, min, max) do max(min, min(value, max)) end @doc """ From: https://gist.github.com/ahmadshah/8d978bbc550128cca12dd917a09ddfb7?permalink_comment_id=4178225#gistcomment-4178225 A helper functions to generate a random (printable) string of given `len`. You could change the hard-coded ascii codes 32 and 126 to only uppercase letters, digits, etc... """ def random_string(len \\ 12) when is_integer(len) and len > 0 do for(_ <- 1..len, do: rand_uniform(98, 122)) |> List.to_string() end # Returns a random integer uniformly distributed in the range # `n <= X <= m`. # # If the random variable `X` is uniformly distributed in the range # `1 <= X <= m - n + 1`, then r.v `Y = X + n - 1` is uniformly # distributed in the range `n <= Y <= m`. # (Because we just shift X to the right). defp rand_uniform(n, m) do # random X :rand.uniform(m - n + 1) # shift X to the right to get Y |> Kernel.+(n - 1) end @doc """ Verifies if a slot should be handled as a link. ## Tests iex> PrimerLive.Helpers.AttributeHelpers.link?(%{href: "#url"}) true iex> PrimerLive.Helpers.AttributeHelpers.link?(%{navigate: "#url"}) true iex> PrimerLive.Helpers.AttributeHelpers.link?(%{patch: "#url"}) true iex> PrimerLive.Helpers.AttributeHelpers.link?(%{url: "#url"}) false """ def link?(assigns), do: !!link_url(assigns) defp link_url(assigns), do: assigns[:href] || assigns[:navigate] || assigns[:patch] @doc """ Verifies if a slot should be handled as a link. ## Tests iex> PrimerLive.Helpers.AttributeHelpers.anchor_link?(%{}) false iex> PrimerLive.Helpers.AttributeHelpers.anchor_link?(%{href: "/url"}) false iex> PrimerLive.Helpers.AttributeHelpers.anchor_link?(%{href: "url"}) false iex> PrimerLive.Helpers.AttributeHelpers.anchor_link?(%{href: "#url"}) true """ def anchor_link?(assigns) do link_url(assigns) |> anchor_link_url?() end defp anchor_link_url?(url) when is_nil(url), do: false defp anchor_link_url?(url) do String.match?(url, ~r/^#/) end @doc """ Creates a DOM-safe ID string from a label and optional prefix. iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id(nil) nil iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("") nil iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("hello") "hello" iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("hello", "pre") "pre-hello" iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("dark_high_contrast") "dark-high-contrast" iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("hello 123 bye (465) #$!", "pre") "pre-hello-123-bye-465" iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("hello 123 bye (465) #$!", "form-01910f69-5f44-7603-bef1-475485871486") "form-01910f69-5f44-7603-bef1-475485871486-hello-123-bye-465" iex> PrimerLive.Helpers.AttributeHelpers.create_dom_id("hello 123 bye (465) #$!", "999-form-01910f69-5f44-7603-bef1-475485871486") "form-01910f69-5f44-7603-bef1-475485871486-hello-123-bye-465" """ def create_dom_id(label, prefix \\ "") do prefix = String.replace(prefix, ~r/^[0-9]+/, "-") dom_id = [prefix, label] |> Enum.join("-") |> String.replace(~r/[^A-Za-z0-9]/, "-") |> String.replace(~r/[-]+/, "-") |> String.trim("-") |> String.downcase() if dom_id == "", do: nil, else: dom_id end @doc """ Generates the input name of a form field, using `Phoenix.HTML.Form.input_name/2` with support for multiple inputs (e.g. checkboxes). Param opts accepts - name: the field name - is_multiple ## Tests iex> PrimerLive.Helpers.AttributeHelpers.input_name(nil, nil) nil iex> PrimerLive.Helpers.AttributeHelpers.input_name("my-form", nil) "my-form" iex> PrimerLive.Helpers.AttributeHelpers.input_name("my-form", "field") "my-form[field]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(:form, nil) "form" iex> PrimerLive.Helpers.AttributeHelpers.input_name(nil, "my-field") "[my-field]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(nil, :first_name) "[first_name]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(:profile, :first_name) "profile[first_name]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(nil, nil, name: "my-first-name") "my-first-name" iex> PrimerLive.Helpers.AttributeHelpers.input_name(:profile, nil, name: "my-first-name") "profile[my-first-name]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(:profile, :first_name, name: "my-first-name") "profile[my-first-name]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(:profile, :first_name, is_multiple: true) "profile[first_name][]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(%Phoenix.HTML.Form{ ...> name: :profile, ...> source: %Ecto.Changeset{ ...> action: :update, ...> changes: %{first_name: "annette"}, ...> errors: [], ...> data: nil, ...> valid?: true ...> } ...> }, :first_name) "profile[first_name]" iex> PrimerLive.Helpers.AttributeHelpers.input_name(%PrimerLive.Helpers.TestForm{ ...> source: %PrimerLive.Helpers.TestForm{ ...> name: "test", ...> source: %Phoenix.HTML.Form{ ...> name: :profile, ...> } ...> } ...> }, :first_name) "first_name" """ def input_name(form_or_name, field, opts \\ []) def input_name(form, field, opts) do _input_name(form, field, opts[:name], !!opts[:is_multiple]) end defp _input_name(form, field, name, _is_multiple) when is_nil(form) and is_nil(field), do: name defp _input_name(form, field, name, is_multiple) do single_input_name = Phoenix.HTML.Form.input_name(form, name || field || "") |> String.replace(~r/(\[\])+$/, "") cond do is_multiple && single_input_name !== "" -> single_input_name <> "[]" single_input_name !== "" -> single_input_name true -> nil end end @doc """ Generates the input id of a form field. ## Tests iex> PrimerLive.Helpers.AttributeHelpers.input_id("my-input-id", nil, nil, "", nil, false) "my-input-id" iex> PrimerLive.Helpers.AttributeHelpers.input_id("my-input-id", nil, nil, "", nil, true) "my-input-id" iex> PrimerLive.Helpers.AttributeHelpers.input_id("my-input-id", nil, nil, "", "my-value", true) "my-input-id-my-value" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, "my-id", nil, "", nil, false) "my-id" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :select, "", nil, false) nil iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :select, "[my-input-name]", nil, false) "[my-input-name]" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :select, "[my-input-name][]", nil, false) "[my-input-name]" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :checkbox, "", nil, false) nil iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :checkbox, "my-input-name", nil, false) "my-input-name" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :checkbox, "my-input-name", "my-value", false) "my-input-name[my-value]" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :checkbox, "[my-input-name]", "my-value", false) "[my-input-name][my-value]" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :checkbox, "[my-input-name][]", "my-value", false) "[my-input-name][my-value]" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :checkbox, "[my-input-name][]", nil, false) "[my-input-name][]" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :radio_button, "", nil, false) nil iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :radio_button, "my-input-name", nil, false) "my-input-name" iex> PrimerLive.Helpers.AttributeHelpers.input_id(nil, nil, :radio_button, "my-input-name", "my-value", false) "my-input-name[my-value]" """ def input_id(input_id, _id, _input_type, _input_name, value_for_derived_label, is_multiple) when not is_nil(input_id) and is_multiple and is_binary(value_for_derived_label), do: "#{input_id}-#{value_for_derived_label}" def input_id(input_id, _id, _input_type, _input_name, _value_for_derived_label, _is_multiple) when not is_nil(input_id), do: input_id def input_id(_input_id, id, _input_type, _input_name, _value_for_derived_label, _is_multiple) when not is_nil(id), do: id def input_id(_input_id, _id, input_type, "", _value_for_derived_label, _is_multiple) when input_type === :select, do: nil def input_id(_input_id, _id, input_type, input_name, _value_for_derived_label, _is_multiple) when input_type === :select, do: input_name |> String.replace(~r/\[\]$/, "") def input_id(_input_id, _id, input_type, "", value_for_derived_label, _is_multiple) when input_type === :checkbox or input_type === :radio_button, do: value_for_derived_label def input_id(_input_id, _id, input_type, input_name, value_for_derived_label, _is_multiple) when (input_type === :checkbox or input_type === :radio_button) and is_binary(input_name) do cond do String.match?(input_name, ~r/\[\]$/) -> String.replace(input_name, ~r/(\[\]$)/, "[#{value_for_derived_label}]") !is_nil(value_for_derived_label) -> "#{input_name}[#{value_for_derived_label}]" true -> input_name end end def input_id(_input_id, _id, _input_type, "", _value_for_derived_label, _is_multiple), do: nil def input_id(_input_id, _id, _input_type, input_name, _value_for_derived_label, _is_multiple), do: input_name @spec cleanup_id(nil | binary) :: nil | binary def cleanup_id(id) when is_nil(id), do: nil def cleanup_id(id) do id |> String.replace(~r/(\]\[)|(\[\])|\[|\]/, "_") |> String.replace(~r/^(\_)*/, "") |> String.replace(~r/(\_)*$/, "") end @doc ~S""" Extracts common attributes for form inputs. Shared for consistency. """ def common_input_attrs(assigns, input_type \\ nil) do common_shared_attrs = common_shared_attrs(assigns) # ID and label id_attrs = common_id_attrs(assigns, input_type, common_shared_attrs) # Field state field_state_attrs = common_field_state_attrs( assigns, id_attrs, common_shared_attrs ) # Form control form_control_attrs = common_form_control_attrs(assigns, id_attrs.input_id, common_shared_attrs) |> Map.merge(field_state_attrs |> Map.drop([:caption])) [ common_shared_attrs, id_attrs, form_control_attrs, field_state_attrs ] |> Enum.reduce(&Map.merge/2) end defp common_shared_attrs(assigns) do rest = assigns[:rest] name = assigns[:name] || rest[:name] form = assigns[:form] field = assigns[:field] field_or_name = field || name || "" %{ rest: rest, name: name, form: form, field: field, field_or_name: field_or_name } end defp common_id_attrs( assigns, input_type, %{form: form, field: field, rest: rest, name: name} ) do id = cleanup_id(assigns[:id] || rest[:id]) is_multiple = !!assigns[:is_multiple] checked_value = assigns[:checked_value] input_name = if !is_nil(form) || !is_nil(field), do: input_name(form, field, name: name, is_multiple: is_multiple), else: name value = assigns[:value] || rest[:value] value_for_derived_label = checked_value || value input_id = assigns.input_id || input_id( assigns[:input_id], id, input_type, input_name, value_for_derived_label, is_multiple ) |> create_dom_id() derived_label = case input_type do :checkbox -> Phoenix.Naming.humanize(value_for_derived_label || field) :radio_button -> Phoenix.Naming.humanize(value_for_derived_label) _ -> nil end %{ derived_label: derived_label, input_id: input_id, id: id, input_name: input_name, value: value } end defp common_form_control_attrs(assigns, input_id, %{ form: form, field_or_name: field_or_name }) do form_control = assigns[:form_control] is_form_control = assigns[:is_form_control] || !!form_control has_form_control = is_form_control form_control_attrs = Map.merge(form_control || %{}, %{ form: form, field: field_or_name, input_id: input_id, for: input_id, is_full_width: assigns[:is_full_width] }) %{ form_control_attrs: form_control_attrs, has_form_control: has_form_control } end defp common_field_state_attrs(assigns, %{input_id: input_id, id: id}, %{ form: form, field_or_name: field_or_name }) do validation_message = assigns[:validation_message] caption = assigns[:caption] field_state = FormHelpers.field_state(form, field_or_name, validation_message, caption) %{ changeset: changeset, message: message, valid?: valid?, required?: required?, ignore_errors?: ignore_errors?, caption: caption } = field_state has_changeset? = !is_nil(changeset) show_message? = !!message && !ignore_errors? && assigns[:type] !== "hidden" validation_message_id = cond do !show_message? -> nil assigns[:validation_message_id] -> assigns[:validation_message_id] input_id -> "#{input_id}-validation" id -> "#{id}-validation" true -> random_string() end validation_marker_class = if has_changeset? do cond do valid? == false -> "pl-invalid" valid? && show_message? -> "pl-valid" valid? && !show_message? -> "pl-neutral" end end %{ message: message, valid?: valid?, required?: required?, ignore_errors?: ignore_errors?, show_message?: show_message?, validation_message_id: validation_message_id, validation_marker_class: validation_marker_class, caption: caption } end @doc ~S""" Extracts menu/dialog prompt attributes. """ def prompt_attrs(assigns, %{ component: component, toggle_slot: toggle_slot, toggle_class: toggle_class, menu_class: menu_class, is_menu: is_menu }) do id = assigns[:id] || assigns.rest[:id] || random_string() if is_nil(id), do: ComponentHelpers.missing_attribute(component, "'id'") focus_wrap_id = "focus-wrap-#{id}" focus_wrap_attrs = append_attributes([ [ "data-focuswrap": "", id: focus_wrap_id, "phx-key": "Escape", "phx-window-keydown": assigns.is_escapable && PromptHelpers.cancel_prompt(id) ] ]) toggle_id = if id, do: "#{id}-toggle", else: id toggle_rest = assigns_to_attributes_sorted(toggle_slot || [], [:class, :for, :options]) toggle_attrs = append_attributes(toggle_rest, [ [ "aria-haspopup": "true", "aria-owns": focus_wrap_id, class: toggle_class, "phx-click": Keyword.get(toggle_rest, :"phx-click", PromptHelpers.toggle_prompt(id)), type: "button" ] ]) id_selector = "##{id}" prompt_id = id || "menu-" <> toggle_id show_state = assigns.show_state || "default" is_show_on_mount = !!assigns.is_show_on_mount is_show = assigns.is_show || is_show_on_mount is_opening_transitions_disabled = show_state == "hold" || is_show_on_mount is_focus_first_disabled = show_state == "hold" is_push_drawer = !!assigns[:is_push] prompt_attrs = append_attributes(assigns.rest |> Map.drop([:id]), [ [ class: classnames([ menu_class, is_show_on_mount && "is-open is-showing is-show_on_mount", is_push_drawer && "is-show_on_mount" ]) ], ["data-prompt": ""], [id: prompt_id], ["phx-hook": assigns.rest[:"phx-hook"] || "Prompt"], is_show && ["phx-mounted": JS.exec("data-open", to: id_selector)], show_state == "default" && [ "phx-remove": assigns.on_cancel && PromptHelpers.close_prompt(id) ], ["data-cancel": JS.exec(assigns.on_cancel || %JS{}, "data-close")], [ "data-open": %JS{} |> maybe_focus_after_closing_selector(assigns.focus_after_closing_selector) |> maybe_set_transition_duration_style( if(is_opening_transitions_disabled, do: nil, else: assigns.transition_duration), id_selector, is_menu ) |> maybe_send_status_event( assigns.transition_duration, if(is_opening_transitions_disabled, do: nil, else: assigns.status_callback_selector), id_selector, is_menu, "prompt:open" ) |> JS.add_class("is-open", to: id_selector) |> maybe_focus_first(id_selector, is_focus_first_disabled) |> maybe_focus_after_opening_selector(assigns.focus_after_opening_selector) |> maybe_add_delay_to_show(id_selector, is_opening_transitions_disabled) |> maybe_remove_classes(id_selector, %{ is_show_on_mount: is_show_on_mount, is_push_drawer: is_push_drawer }) ], [ "data-close": %JS{} |> maybe_send_status_event( assigns.transition_duration, assigns.status_callback_selector, id_selector, is_menu, "prompt:close" ) |> maybe_set_transition_duration_style( assigns.transition_duration, id_selector, is_menu ) |> JS.remove_class("is-showing", to: id_selector) |> JS.remove_class("is-open", transition: {"duration-#{assigns.transition_duration}", "", ""}, time: assigns.transition_duration, to: id_selector ) |> JS.pop_focus() ], is_menu && ["data-ismenu": ""], assigns[:is_fast] && ["data-isfast": ""], assigns.is_escapable != false && ["data-isescapable": ""] ]) is_backdrop = !!assigns[:is_backdrop] backdrop_strength = cond do is_backdrop && assigns[:backdrop_strength] -> assigns[:backdrop_strength] is_backdrop && is_menu -> "light" true -> nil end backdrop_tint = if is_backdrop && assigns[:backdrop_tint] do assigns[:backdrop_tint] else nil end backdrop_attrs = append_attributes([ is_backdrop && ["data-backdrop": ""], backdrop_strength && ["data-backdrop-strength": backdrop_strength], backdrop_tint && ["data-backdrop-tint": backdrop_tint] ]) is_modal = assigns[:is_modal] || false touch_layer_attrs = append_attributes([ [ "data-touch": "", "phx-click": not is_modal && PromptHelpers.cancel_prompt(id) ] ]) %{ toggle_attrs: toggle_attrs, prompt_attrs: prompt_attrs, backdrop_attrs: backdrop_attrs, touch_layer_attrs: touch_layer_attrs, focus_wrap_attrs: focus_wrap_attrs } end defp maybe_focus_first(js, _id_selector, is_focus_first_disabled) when is_focus_first_disabled, do: js defp maybe_focus_first(js, id_selector, _is_focus_first_disabled) do JS.focus_first(js, to: "#{id_selector} [data-content]") end defp maybe_focus_after_opening_selector(js, selector) when is_nil(selector), do: js defp maybe_focus_after_opening_selector(js, selector) do JS.focus(js, to: selector) end defp maybe_focus_after_closing_selector(js, selector) when is_nil(selector), do: js defp maybe_focus_after_closing_selector(js, selector) do JS.push_focus(js, to: selector) end defp maybe_send_status_event( js, _transition_duration, status_callback_selector, _id_selector, _is_menu, _event_name ) when is_nil(status_callback_selector), do: js defp maybe_send_status_event( js, transition_duration, status_callback_selector, id_selector, is_menu, event_name ) when is_menu do dispatch_send_status_event( js, transition_duration || PromptHelpers.default_menu_transition_duration(), status_callback_selector, id_selector, event_name ) end defp maybe_send_status_event( js, transition_duration, status_callback_selector, id_selector, _is_menu, event_name ) do dispatch_send_status_event( js, transition_duration || PromptHelpers.default_dialog_transition_duration(), status_callback_selector, id_selector, event_name ) end defp maybe_add_delay_to_show(js, id_selector, is_opening_transitions_disabled) when is_opening_transitions_disabled do JS.add_class(js, "is-showing", to: id_selector) end defp maybe_add_delay_to_show(js, id_selector, _is_opening_transitions_disabled) do transition_delay = 30 JS.add_class(js, "is-showing", # Small delay to create a fade-in effect transition: {"duration-#{transition_delay}", "", ""}, time: transition_delay, to: id_selector ) end defp maybe_remove_classes(js, id_selector, %{ is_show_on_mount: is_show_on_mount, is_push_drawer: is_push_drawer }) when is_show_on_mount or is_push_drawer do JS.remove_class(js, "is-show_on_mount", to: id_selector) end defp maybe_remove_classes(js, _id_selector, _is_show_on_mount), do: js defp dispatch_send_status_event( js, transition_duration, status_callback_selector, id_selector, event_name ) do JS.dispatch(js, event_name, to: id_selector, detail: %{selector: status_callback_selector, transitionDuration: transition_duration} ) end defp maybe_set_transition_duration_style(js, transition_duration, _id_selector, _is_menu) when is_nil(transition_duration), do: js defp maybe_set_transition_duration_style(js, transition_duration, id_selector, is_menu) when is_menu do maybe_create_transition_duration_style_attribute( js, transition_duration, id_selector, transition_duration == PromptHelpers.default_menu_transition_duration() ) end defp maybe_set_transition_duration_style(js, transition_duration, id_selector, _is_menu) do maybe_create_transition_duration_style_attribute( js, transition_duration, id_selector, transition_duration == PromptHelpers.default_dialog_transition_duration() ) end defp maybe_create_transition_duration_style_attribute( js, _transition_duration, _id_selector, is_default_transition_duration ) when is_default_transition_duration do js end defp maybe_create_transition_duration_style_attribute( js, transition_duration, id_selector, _is_default_transition_duration ) do JS.set_attribute( js, {"style", "--prompt-transition-duration: #{transition_duration}ms; --prompt-fast-transition-duration: #{transition_duration}ms;"}, to: id_selector ) end end