# This module does not have a dependency with phoenix_html.
# It doesn't have any dependency.
#
defmodule PhoenixFormAwesomplete.GenJS do
@moduledoc ~S"""
Generate the javascript code for PhoenixFormAwesomplete.
## Example
iex> PhoenixFormAwesomplete.GenJS.awesomplete_js("user_drinks", %{ minChars: 1, prepop: true } )
"AwesompleteUtil.start('#user_drinks', {prepop: true}, {minChars: 1});"
"""
# @util & @awe refer to the default javascript libraries.
@util Application.compile_env(:phoenix_form_awesomplete, :util) || "AwesompleteUtil"
@awe Application.compile_env(:phoenix_form_awesomplete, :awesomplete) || "Awesomplete"
@doc ~S"""
Create javascript that listens to `awesomplete-prepop` and `awesomplete-match` events,
and copies the `data_field` to the DOM element with the given target id.
The `target_id` can also be a javascript function.
## Example
iex> PhoenixFormAwesomplete.GenJS.copy_to_id_js("#user_color", "label", "#awe-color-result")
"AwesompleteUtil.startCopy('#user_color', 'label', '#awe-color-result');"
"""
def copy_to_id_js(source_id, data_field \\ nil, target_id)
when (is_nil(data_field) or is_binary(data_field)) and is_binary(target_id) do
# In Javascript strings must be quoted, but functions not.
# Assume it is a function if it starts with an alfanumeric characters and doesn't contain space
# or it can be an inlined function or it can be a function-name with bind
is_function = String.contains?(target_id, "{") || target_id =~ ~r/^\w[^\s]*$/
target = if is_function, do: target_id, else: "'#{target_id}'"
"#{@util}.startCopy('#{source_id}', '#{data_field}', #{target});"
end
# Converts map with options to a comma separated string with key:value pairs..
defp opts_to_string(opts) do
opts
|> Enum.map_join(", ", fn{k, v} -> "#{k}: #{v}" end)
end
# convert string to integer
defp to_integer!(val) when is_nil(val) or is_integer(val), do: val
defp to_integer!(val) when is_binary(val), do: String.to_integer(val)
# convert string to boolean
defp to_bool!(true), do: true
defp to_bool!("true"), do: true
defp to_bool!(_), do: false
# returns filter_opts with added item
defp add_item(filter_opts, item_fun, starts_with, multiple_char, filter_str, descr_search) do
cond do
is_nil(item_fun) and is_nil(multiple_char) and descr_search -> filter_opts ++ [item: "#{@util}.itemMarkAll"]
is_nil(item_fun) and is_nil(multiple_char) -> filter_opts
is_nil(item_fun) and descr_search -> filter_opts ++ [item: "function(text, input) { return #{@util}.itemMarkAll(text, #{filter_str}); }"]
is_nil(item_fun) and starts_with -> filter_opts ++ [item: "function(text, input) { return #{@util}.itemStartsWith(text, #{filter_str}); }"]
is_nil(item_fun) -> filter_opts ++ [item: "function(text, input) { return #{@util}.itemContains(text, #{filter_str}); }"]
is_nil(multiple_char) -> filter_opts ++ [item: "#{item_fun}"]
true -> filter_opts ++ [item: "function(text, input) { return (#{item_fun}).call(this, text, #{filter_str}); }"]
end
end
#
# Be permissive about all kind of parameter combinations, except for these:
#
defp parameter_checks(fld_name, label_fld, descr_fld, descr_search) do
# We could take the default of 'value' for the 'value' parameter (a.k.a. fld_name,) but it's more clear to be explicit.
if is_nil(fld_name) and descr_fld != nil, do: raise(ArgumentError, "'descr' without 'value' parameter.")
if is_nil(fld_name) and label_fld != nil, do: raise(ArgumentError, "'label' without 'value' parameter.")
if is_nil(descr_fld) and descr_search, do: raise(ArgumentError, "Cannot search description texts without knowing the description field. Please supply descr parameter.")
end
#
# Determine which characters are used to separate multiple items
# For example in multiple color selection: red,blue,yellow the multiple_char is the comma
# Assume space as separator if multiple=true,
#
defp construct_multiple_char(multiple) do
cond do
is_nil(multiple) or multiple == false -> nil
multiple == true -> " "
true -> multiple
end
end
# return list with convertInput function
defp construct_conv_input_opts(multiple_char, conv_input_fun, conv_input_str) do
cond do
is_nil(multiple_char) and is_nil(conv_input_fun) -> []
is_nil(multiple_char) -> [convertInput: conv_input_fun]
is_nil(conv_input_fun) -> [convertInput: "function(input) { return #{conv_input_str}.trim().toLowerCase(); }"]
true -> [convertInput: "function(input) { return (#{conv_input_fun}).call(this, #{conv_input_str}.trim().toLowerCase()); }"]
end
end
# define the replacement text, mainly used in the replace function
defp construct_assign_replace_text(multiple_char) do
if is_nil(multiple_char) do
"text"
else
optional_space = if String.at(multiple_char, 0) == " ", do: "", else: " "
"this.input.value.match(/^.+[#{multiple_char}]\\s*|/)[0] + text + '" <> String.at(multiple_char, 0) <> optional_space <> "'"
end
end
# return list with replace function
defp construct_multiple_replace_opts(multiple_char, replace_fun, assign_replace_text) do
cond do
is_nil(multiple_char) -> []
is_nil(replace_fun) -> [replace: "function(data) { var text=data.value; this.input.value = #{assign_replace_text}; }"]
true -> [replace: "function(data) { var text=data.value; (#{replace_fun}).call(this, #{assign_replace_text}); }"]
end
end
# return javascript code for the result part of the data function
defp construct_data_fun_result(fld_name, label_fld, descr_fld, descr_search) do
label_str =
if label_fld do
"(rec['#{label_fld}'] || '').replace('<p>', '<p >')"
else
"rec['#{fld_name}']"
end
cond do
is_nil(descr_fld) and is_nil(label_fld) -> "rec['#{fld_name}']"
is_nil(descr_fld) -> "{ label:#{label_str}, value:rec['#{fld_name}'] }"
!descr_search -> "{ label: #{label_str}+'<p>'+(rec['#{descr_fld}'] || ''), value: rec['#{fld_name}'] }"
true -> "{ label: #{label_str}+'<p>'+(rec['#{descr_fld}'] || ''), value: rec['#{fld_name}']+'|'+(rec['#{descr_fld}'] || '').replace('|', ' ') }"
end
end
# return javascript of all options
defp construct_awe_script(element_id, util_opts_str, awe_opts_str, assign, combobox) do
assign_var = if assign == true or assign == "true", do: "awe_#{element_id}", else: "#{assign}"
assign_text = if assign == false or assign == "false", do: "", else: "var #{assign_var}="
awe_script = "#{assign_text}#{@util}.start('##{element_id}', #{util_opts_str}, #{awe_opts_str})"
# id of the combo button. Assume awe_btn_<awesomplete element id> if combobox=true. Or take the combobox supplied value.
combo_btn_id = if combobox == true or combobox == "true", do: "awe_btn_#{element_id}", else: "#{combobox}"
# awe_script, combobox, assign, combo_btn_id, assign_var
cond do
combobox == false or combobox == "false" -> "#{awe_script};"
assign == false or assign == "false" -> "#{@util}.startClick('##{combo_btn_id}', #{awe_script});"
true -> "#{awe_script};\n#{@util}.startClick('##{combo_btn_id}', #{assign_var});"
end
end
@doc ~S"""
This method generates javascript code for using Awesomplete(Util).
## Example
iex> PhoenixFormAwesomplete.GenJS.awesomplete_js("user_hobby", %{ minChars: 1 } )
"AwesompleteUtil.start('#user_hobby', {}, {minChars: 1});"
"""
def awesomplete_js(element_id, awesomplete_opts) do
awesomplete_opts = Enum.to_list(awesomplete_opts)
#
# Some of the options (data, filter, item, replace) are popped to be added later again.
# Unrecognized options are passed on to Awesomplete.
#
{ajax_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :ajax)
{assign, awesomplete_opts} = Keyword.pop(awesomplete_opts, :assign, false)
{combobox, awesomplete_opts} = Keyword.pop(awesomplete_opts, :combobox, false)
{conv_input_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :convertInput)
{conv_response_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :convertResponse)
{data_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :data)
{debounce, awesomplete_opts} = Keyword.pop(awesomplete_opts, :debounce)
{descr_fld, awesomplete_opts} = Keyword.pop(awesomplete_opts, :descr)
{descr_search, awesomplete_opts} = Keyword.pop(awesomplete_opts, :descrSearch, false)
{filter_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :filter)
{fld_name, awesomplete_opts} = Keyword.pop(awesomplete_opts, :value)
{item_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :item)
{label_fld, awesomplete_opts} = Keyword.pop(awesomplete_opts, :label)
{loadall, awesomplete_opts} = Keyword.pop(awesomplete_opts, :loadall, false)
{limit, awesomplete_opts} = Keyword.pop(awesomplete_opts, :limit)
{multiple, awesomplete_opts} = Keyword.pop(awesomplete_opts, :multiple)
{prepop, awesomplete_opts} = Keyword.pop(awesomplete_opts, :prepop, false)
{replace_fun, awesomplete_opts} = Keyword.pop(awesomplete_opts, :replace)
{url, awesomplete_opts} = Keyword.pop(awesomplete_opts, :url)
{url_end, awesomplete_opts} = Keyword.pop(awesomplete_opts, :urlEnd)
#
# Convert descr_search to boolean
#
descr_search = to_bool!(descr_search)
loadall = to_bool!(loadall)
prepop = to_bool!(prepop)
parameter_checks(fld_name, label_fld, descr_fld, descr_search)
# generated js code uses @util & @awe but in the input we expect the standard Awesomplete & AwesompleteUtil names.
starts_with = filter_fun == "Awesomplete.FILTER_STARTSWITH" or filter_fun == "AwesompleteUtil.filterStartsWith"
#
# Convert limit and debounce to integer
#
limit = to_integer!(limit)
debounce = to_integer!(debounce)
multiple_char = construct_multiple_char(multiple)
#
# For multiple items, if the separator is typed, the last field is considered complete.
# The separator is included in the search in the filter,
# and because the items do not contain the separator, the suggestion list will close.
# However, for the combobox, closing the list and waiting for the first characters is not desirable,
# because the combo button is there to be able to show all items.
#
filter_match_sep = if combobox != "false" and combobox, do: "", else: "([#{multiple_char}]\\s*)?"
#
# Select the text that should be considered as the current input.
# Will be used in the convertInput function and in the filter.
# For multiple items, only the last item is considered to be input for the suggestion list lookup.
#
conv_input_str =
if is_nil(multiple_char) do
"input"
else
"input.replace(/[#{multiple_char}]\\s*$/, '').match(/[^#{multiple_char}]*$/)[0]"
end
filter_str =
if is_nil(multiple_char) do
"input"
else
"input.match(/[^#{multiple_char}]*#{filter_match_sep}$/)[0]"
end
# when there is no descr_fld or label_fld we let the data function just return one string instead of a value and label
data_val =
if is_nil(fld_name) or (is_nil(descr_fld) and is_nil(label_fld)) do
"data"
else
"data.value"
end
starts_with_filter_fun = cond do
is_nil(descr_fld) and is_nil(label_fld) and filter_fun == "Awesomplete.FILTER_STARTSWITH" -> "#{@awe}.FILTER_STARTSWITH"
descr_search -> "function(data, input) { return #{@util}.filterStartsWith(data, #{filter_str}) || #{@awe}.FILTER_STARTSWITH(data.value.substring(data.value.lastIndexOf('|')+1), #{filter_str}); }"
true -> "#{@util}.filterStartsWith"
end
filter_opts = cond do
is_nil(multiple_char) and is_nil(filter_fun) and data_val == "data" -> []
is_nil(multiple_char) and is_nil(descr_fld) and is_nil(label_fld) and (is_nil(filter_fun) or filter_fun == "Awesomplete.FILTER_CONTAINS") -> []
is_nil(multiple_char) and (is_nil(filter_fun) or filter_fun == "Awesomplete.FILTER_CONTAINS" or filter_fun == "AwesompleteUtil.filterContains") -> [filter: "#{@util}.filterContains"]
is_nil(multiple_char) and data_val == "data" -> [filter: filter_fun]
is_nil(multiple_char) and starts_with and is_nil(item_fun) -> [filter: starts_with_filter_fun , item: "#{@util}.itemStartsWith"]
is_nil(multiple_char) and starts_with -> [filter: starts_with_filter_fun]
is_nil(multiple_char) -> [filter: "function(data, input) { return (#{filter_fun})(data.value, input); }"]
starts_with and descr_search -> [filter: starts_with_filter_fun]
starts_with -> [filter: "function(data, input) { return #{@awe}.FILTER_STARTSWITH(#{data_val}, #{filter_str}); }"]
is_nil(filter_fun) -> [filter: "function(data, input) { return #{@awe}.FILTER_CONTAINS(#{data_val}, #{filter_str}); }"]
true -> [filter: "function(data, input) { return (#{filter_fun}).call(this, #{data_val}, #{filter_str}); }"]
end
# add item: in filter_opts
filter_opts = add_item(filter_opts, item_fun, starts_with, multiple_char, filter_str, descr_search)
conv_input_opts = construct_conv_input_opts(multiple_char, conv_input_fun, conv_input_str)
assign_replace_text = construct_assign_replace_text(multiple_char)
multiple_replace_opts = construct_multiple_replace_opts(multiple_char, replace_fun, assign_replace_text)
data_fun_result = construct_data_fun_result(fld_name, label_fld, descr_fld, descr_search)
data_fun_str =
if is_nil(data_fun) do
"function(rec, input) { return #{data_fun_result}; }"
else
"function(rec, input) { return (#{data_fun}).call(this, #{data_fun_result}, input); }"
end
awesomplete_opts = cond do
is_nil(fld_name) and is_nil(data_fun) -> awesomplete_opts ++ multiple_replace_opts
is_nil(fld_name) -> awesomplete_opts ++ [data: data_fun] ++ multiple_replace_opts
!descr_search -> awesomplete_opts ++ [data: data_fun_str] ++ multiple_replace_opts
is_nil(replace_fun) -> awesomplete_opts ++ [data: data_fun_str, replace: "function(data) { var text = data.value.substring(0, data.value.lastIndexOf('|')); this.input.value = #{assign_replace_text}; }"]
true -> awesomplete_opts ++ [data: data_fun_str, replace: "function(data) { var text = data.value.substring(0, data.value.lastIndexOf('|')); (#{replace_fun}).call(this, #{assign_replace_text}); }"]
end
awesomplete_opts = awesomplete_opts ++ filter_opts
awesomplete_opts =
if is_nil(replace_fun) or descr_search or multiple_char != nil do
awesomplete_opts
else
awesomplete_opts ++ [replace: replace_fun]
end
util_opts = if is_nil(url), do: [], else: [url: "'#{url}'"]
util_opts = if is_nil(url_end), do: util_opts, else: util_opts ++ [urlEnd: "'#{url_end}'"]
util_opts = if is_nil(limit), do: util_opts, else: util_opts ++ [limit: limit]
util_opts = if is_nil(debounce), do: util_opts, else: util_opts ++ [debounce: debounce]
util_opts = if is_nil(ajax_fun), do: util_opts, else: util_opts ++ [ajax: ajax_fun]
util_opts = if is_nil(conv_response_fun), do: util_opts, else: util_opts ++ [convertResponse: conv_response_fun]
util_opts = if loadall, do: util_opts ++ [loadall: true], else: util_opts
util_opts = if prepop, do: util_opts ++ [prepop: true], else: util_opts
util_opts = util_opts ++ conv_input_opts
util_opts_str = "{" <> opts_to_string(util_opts) <> "}"
awe_opts_str = "{" <> opts_to_string(awesomplete_opts) <> "}"
construct_awe_script(element_id, util_opts_str, awe_opts_str, assign, combobox)
end
end