diff --git a/lib/live_select.ex b/lib/live_select.ex
index 51f0b0d..dc47185 100644
--- a/lib/live_select.ex
+++ b/lib/live_select.ex
@@ -370,9 +370,10 @@ defmodule LiveSelect do
~S(an id to assign to the component. If none is provided, `#{form_name}_#{field}_live_select_component` will be used)
attr :mode, :atom,
- values: [:single, :tags],
+ values: [:single, :tags, :quick_tags],
default: Component.default_opts()[:mode],
- doc: "either `:single` (for single selection), or `:tags` (for multiple selection using tags)"
+ doc:
+ "either `:single` (for single selection), `:tags` (for multiple selection using tags), or :quick_tags (tags mode but can select multiple at a time)"
attr :options, :list,
doc:
diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex
index 86a715a..3e93c32 100644
--- a/lib/live_select/component.ex
+++ b/lib/live_select/component.ex
@@ -78,7 +78,7 @@ defmodule LiveSelect.Component do
none: []
]
- @modes ~w(single tags)a
+ @modes ~w(single tags quick_tags)a
@impl true
def mount(socket) do
@@ -427,25 +427,48 @@ defmodule LiveSelect.Component do
defp maybe_select(%{assigns: %{active_option: -1}} = socket, _extra_params), do: socket
+ defp maybe_select(
+ %{assigns: %{active_option: active_option, options: options, selection: selection}} =
+ socket,
+ extra_params
+ )
+ when active_option >= 0 do
+ option = Enum.at(socket.assigns.options, active_option)
+
+ if already_selected?(option, selection) do
+ pos = get_selection_index(option, selection)
+ unselect(socket, pos)
+ else
+ select(socket, Enum.at(socket.assigns.options, socket.assigns.active_option), extra_params)
+ end
+ end
+
defp maybe_select(socket, extra_params) do
select(socket, Enum.at(socket.assigns.options, socket.assigns.active_option), extra_params)
end
+ defp get_selection_index(option, selection) do
+ Enum.find_index(selection, fn %{label: label} -> label == option.label end)
+ end
+
defp select(socket, selected, extra_params) do
selection =
case socket.assigns.mode do
:tags ->
socket.assigns.selection ++ [selected]
+ :quick_tags ->
+ socket.assigns.selection ++ [selected]
+
_ ->
[selected]
end
socket
|> assign(
- active_option: -1,
+ active_option: if(quick_tags_mode?(socket), do: socket.assigns.active_option, else: -1),
selection: selection,
- hide_dropdown: true
+ hide_dropdown: not quick_tags_mode?(socket)
)
|> maybe_save_selection()
|> client_select(Map.merge(%{input_event: true}, extra_params))
@@ -519,7 +542,7 @@ defmodule LiveSelect.Component do
List.wrap(normalize_selection_value(value, options ++ current_selection, value_mapper))
end
- defp update_selection(value, current_selection, options, :tags, value_mapper) do
+ defp update_selection(value, current_selection, options, _mode, value_mapper) do
value = if Enumerable.impl_for(value), do: value, else: [value]
Enum.map(value, &normalize_selection_value(&1, options ++ current_selection, value_mapper))
@@ -662,8 +685,16 @@ defmodule LiveSelect.Component do
defp encode(value), do: Jason.encode!(value)
- defp already_selected?(option, selection) do
- option.label in Enum.map(selection, & &1.label)
+ def already_selected?(idx, selection) when is_integer(idx) do
+ Enum.at(selection, idx) != nil
+ end
+
+ def already_selected?(option, selection) do
+ Enum.any?(selection, fn item -> item.label == option.label end)
+ end
+
+ defp quick_tags_mode?(socket) do
+ socket.assigns.mode == :quick_tags
end
defp next_selectable(%{
@@ -674,6 +705,19 @@ defmodule LiveSelect.Component do
when max_selectable > 0 and length(selection) >= max_selectable,
do: active_option
+ defp next_selectable(%{
+ options: options,
+ active_option: active_option,
+ selection: selection,
+ mode: :quick_tags
+ }) do
+ options
+ |> Enum.with_index()
+ |> Enum.reject(fn {opt, _} -> active_option == opt end)
+ |> Enum.map(fn {_, idx} -> idx end)
+ |> Enum.find(active_option, &(&1 > active_option))
+ end
+
defp next_selectable(%{options: options, active_option: active_option, selection: selection}) do
options
|> Enum.with_index()
@@ -690,6 +734,20 @@ defmodule LiveSelect.Component do
when max_selectable > 0 and length(selection) >= max_selectable,
do: active_option
+ defp prev_selectable(%{
+ options: options,
+ active_option: active_option,
+ selection: selection,
+ mode: :quick_tags
+ }) do
+ options
+ |> Enum.with_index()
+ |> Enum.reverse()
+ |> Enum.reject(fn {opt, _} -> active_option == opt end)
+ |> Enum.map(fn {_, idx} -> idx end)
+ |> Enum.find(active_option, &(&1 < active_option || active_option == -1))
+ end
+
defp prev_selectable(%{options: options, active_option: active_option, selection: selection}) do
options
|> Enum.with_index()
diff --git a/lib/live_select/component.html.heex b/lib/live_select/component.html.heex
index a4ecd3a..a55681a 100644
--- a/lib/live_select/component.html.heex
+++ b/lib/live_select/component.html.heex
@@ -8,10 +8,11 @@
data-field={@field.id}
data-debounce={@debounce}
>
- <%= if @mode == :tags && Enum.any?(@selection) do %>
+
+ <%= if (@mode == :tags || @mode == :quick_tags) && Enum.any?(@selection) do %>
<%= for {option, idx} <- Enum.with_index(@selection) do %>
<%= if @tag == [] do %>
@@ -40,8 +41,9 @@
<% end %>
+ <% end %>
- <% end %>
+
<%= text_input(@field.form, @text_input_field,
class:
@@ -122,12 +124,12 @@
)
)
}
- data-idx={unless already_selected?(option, @selection), do: idx}
+ data-idx={idx}
>
<%= if @option == [] do %>
<%= option.label %>
<% else %>
- <%= render_slot(@option, option) %>
+ <%= render_slot(@option, Map.merge(option, %{selected: already_selected?(option, @selection)})) %>
<% end %>
diff --git a/lib/support/live_select_web/live/showcase_live.ex b/lib/support/live_select_web/live/showcase_live.ex
index 30a44c5..2ccc710 100644
--- a/lib/support/live_select_web/live/showcase_live.ex
+++ b/lib/support/live_select_web/live/showcase_live.ex
@@ -67,9 +67,15 @@ defmodule LiveSelectWeb.ShowcaseLive do
field(:allow_clear, :boolean)
field(:debounce, :integer, default: Component.default_opts()[:debounce])
field(:disabled, :boolean)
+ field(:custom_option_html, :boolean)
field(:max_selectable, :integer, default: Component.default_opts()[:max_selectable])
field(:user_defined_options, :boolean)
- field(:mode, Ecto.Enum, values: [:single, :tags], default: Component.default_opts()[:mode])
+
+ field(:mode, Ecto.Enum,
+ values: [:single, :tags, :quick_tags],
+ default: Component.default_opts()[:mode]
+ )
+
field(:new, :boolean, default: true)
field(:placeholder, :string, default: "Search for a city")
field(:search_delay, :integer, default: 10)
@@ -93,6 +99,7 @@ defmodule LiveSelectWeb.ShowcaseLive do
:allow_clear,
:debounce,
:disabled,
+ :custom_option_html,
:max_selectable,
:user_defined_options,
:mode,
@@ -119,7 +126,7 @@ defmodule LiveSelectWeb.ShowcaseLive do
default_opts = Component.default_opts()
settings
- |> Map.drop([:search_delay, :new, :selection])
+ |> Map.drop([:search_delay, :new, :selection, :custom_option_html])
|> Map.from_struct()
|> then(
&if is_nil(&1.style) do
@@ -134,6 +141,19 @@ defmodule LiveSelectWeb.ShowcaseLive do
(settings.mode != :single && option == :allow_clear)
end)
|> Keyword.new()
+ |> then(&maybe_set_classes_for_multiselect/1)
+ end
+
+ defp maybe_set_classes_for_multiselect(opts) do
+ if LiveSelectWeb.ShowcaseLive.quick_tags?(opts[:mode]) do
+ Keyword.put(
+ opts,
+ :selected_option_class,
+ "cursor-pointer font-bold hover:bg-gray-400 rounded"
+ )
+ else
+ opts
+ end
end
def has_style_errors?(%Ecto.Changeset{errors: errors}) do
@@ -239,20 +259,55 @@ defmodule LiveSelectWeb.ShowcaseLive do
assigns = assign(assigns, opts: opts, format_value: format_value)
~H"""
-
-
<.live_select
-
field=
{my_form[:city_search]}
- <%= for {key, value} <- @opts, !is_nil(value) do %>
- <%= if value == true do %>
-
<%= key %>
- <% else %>
-
<%= key %>=
<%= @format_value.(value) %>
+ <%= if @custom_option_html do %>
+
+ <.live_select
+
field={my_form[:city_search]}
+ <%= for {key, value} <- @opts, !is_nil(value) do %>
+ <%= if value == true do %>
+
<%= key %>
+ <% else %>
+
<%= key %>=<%= @format_value.(value) %>
+ <% end %>
<% end %>
- <% end %>
- />
-
+
>
+
+
+ <:option :let=
{%{label: label, value: value, selected: selected}}
+
+ >
+
+
<%= indent(2) %>
<div class=
"flex justify-content items-center">
+
<%= indent(3) %>
<input
+
<%= indent(4) %>
class=
"rounded w-4 h-4 mr-3 border border-border"
+
<%= indent(4) %>
type=
"checkbox"
+
<%= indent(4) %>
checked=
{selected}
+
<%= indent(3) %>
/>
+
<%= indent(4) %>
<span class=
"text-sm"><%= label %>
+
<%= indent(2) %>
</div>
+
<%= indent(1) %>
</:option>
+
<.live_select/>
+
+ <% else %>
+
+ <.live_select
+
field={my_form[:city_search]}
+ <%= for {key, value} <- @opts, !is_nil(value) do %>
+ <%= if value == true do %>
+
<%= key %>
+ <% else %>
+
<%= key %>=<%= @format_value.(value) %>
+ <% end %>
+ <% end %>
+ />
+
+ <% end %>
"""
end
+
+ defp indent(amount) do
+ raw(for _ <- 1..amount, do: " ")
+ end
end
@impl true
@@ -299,9 +354,16 @@ defmodule LiveSelectWeb.ShowcaseLive do
{:ok, settings} ->
socket.assigns
+ attrs =
+ if settings.mode == :quick_select do
+ %{selected_option_class: "cursor-pointer font-bold hover:bg-gray-400 rounded"}
+ else
+ %{}
+ end
+
socket =
socket
- |> assign(:settings_form, Settings.changeset(settings, %{}) |> to_form)
+ |> assign(:settings_form, Settings.changeset(settings, attrs) |> to_form)
|> update(:schema_module, fn _, %{settings_form: settings_form} ->
if settings_form[:mode].value == :single, do: CitySearchSingle, else: CitySearchMany
end)
@@ -488,6 +550,10 @@ defmodule LiveSelectWeb.ShowcaseLive do
{:noreply, socket}
end
+ def quick_tags?(mode) do
+ mode == :quick_tags
+ end
+
defp value_mapper(%City{name: name} = value), do: %{label: name, value: Map.from_struct(value)}
defp value_mapper(value), do: value
diff --git a/lib/support/live_select_web/live/showcase_live.html.heex b/lib/support/live_select_web/live/showcase_live.html.heex
index c0a36e8..9b20cde 100644
--- a/lib/support/live_select_web/live/showcase_live.html.heex
+++ b/lib/support/live_select_web/live/showcase_live.html.heex
@@ -40,7 +40,7 @@
<%= label(@settings_form, :mode, "Mode:", class: "label label-text font-semibold") %>
- <%= select(@settings_form, :mode, [:single, :tags],
+ <%= select(@settings_form, :mode, [:single, :tags, :quick_tags],
class: "select select-sm select-bordered text-xs"
) %>
@@ -71,6 +71,10 @@
Disabled:
<%= checkbox(@settings_form, :disabled, class: "toggle") %>
<% end %>
+ <%= label class: "label cursor-pointer" do %>
+
Custom option HTML:
+ <%= checkbox(@settings_form, :custom_option_html, class: "toggle") %>
+ <% end %>
<%= submit("Submit",
@@ -310,7 +329,7 @@
-
+
diff --git a/test/live_select/component_test.exs b/test/live_select/component_test.exs
index b6a8090..c36a49e 100644
--- a/test/live_select/component_test.exs
+++ b/test/live_select/component_test.exs
@@ -427,7 +427,7 @@ defmodule LiveSelect.ComponentTest do
test "raises if unknown mode is given", %{form: form} do
assert_raise(
RuntimeError,
- ~s(Invalid mode: "not_a_valid_mode". Mode must be one of: [:single, :tags]),
+ ~s(Invalid mode: "not_a_valid_mode". Mode must be one of: [:single, :tags, :quick_tags]),
fn ->
render_component(&LiveSelect.live_select/1,
field: form[:input],
@@ -539,6 +539,11 @@ defmodule LiveSelect.ComponentTest do
]
end
+ describe "in quick_tags mode" do
+ test "" do
+ end
+ end
+
for style <- [:daisyui, :tailwind, :none, nil] do
@style style