defmodule Oban.Web.SearchComponent do use Oban.Web, :live_component @spinner_timeout 100 @impl Phoenix.LiveComponent def mount(socket) do {:ok, assign(socket, buffer: "", loading: false)} end @impl Phoenix.LiveComponent def update(assigns, socket) do suggestions = Map.get_lazy(assigns, :suggestions, fn -> assigns.queryable.suggest(socket.assigns.buffer, assigns.conf, resolver: assigns.resolver) end) socket = socket |> assign(assigns) |> assign(suggestions: suggestions) {:ok, socket} end @impl Phoenix.LiveComponent def render(assigns) do ~H""" """ end attr :param, :any, required: true attr :terms, :any, required: true defp filter(assigns) do ~H"""
{format_filter(@param, @terms)}
""" end defp format_filter(param, [path, term]) when is_list(path) do "#{param}.#{Enum.join(path, ".")}:#{term}" end defp format_filter(param, term) when is_list(term) do "#{param}:#{Enum.join(term, ",")}" end defp format_filter(param, term) do "#{param}:#{term}" end attr :buff, :string attr :name, :string attr :desc, :string attr :exmp, :string defp option(assigns) do ~H""" """ end defp clearable?(buffer, params, queryable) do String.length(buffer) > 0 or map_size(filterable(params, queryable)) > 0 end defp filterable(params, queryable), do: Map.take(params, queryable.filterable()) defp highlight(value, substr) do match = substr |> String.split([":", "."], trim: true) |> List.last() if is_binary(value) and is_binary(match) do pattern = Regex.compile!("(#{Regex.escape(match)})", "i") value |> String.replace(pattern, "\\1") |> raw() else value end end def show_focus do %JS{} |> JS.remove_class("ring-gray-300 dark:ring-gray-700", to: "#search-wrapper") |> JS.add_class("shadow-blue-100 dark:shadow-blue-950", to: "#search-wrapper") |> JS.add_class("ring-blue-500 dark:ring-blue-700", to: "#search-wrapper") |> JS.add_class("bg-blue-100/30 dark:bg-blue-900/30", to: "#search-wrapper") |> JS.show(to: "#search-suggest") end # Closing the suggest menu is done with push events to allow clicking on a suggestion to fire # before the menu closes. def hide_focus do %JS{} |> JS.add_class("ring-gray-300 dark:ring-gray-700", to: "#search-wrapper") |> JS.remove_class("shadow-blue-100 dark:shadow-blue-950", to: "#search-wrapper") |> JS.remove_class("ring-blue-500 dark:ring-blue-700", to: "#search-wrapper") |> JS.remove_class("bg-blue-100/30 dark:bg-blue-900/30", to: "#search-wrapper") end # Events @impl Phoenix.LiveComponent def handle_event("change", %{"terms" => buffer}, socket) do socket = socket |> assign(buffer: buffer) |> async_suggest(buffer) {:noreply, socket} end def handle_event("clear", _params, socket) do suggestions = socket.assigns.queryable.suggest("", socket.assigns.conf) {:noreply, socket |> assign(buffer: "", loading: false, suggestions: suggestions) |> push_patch(to: oban_path(socket.assigns.page))} end def handle_event("append", %{"choice" => choice}, socket) do buffer = socket.assigns.queryable.append(socket.assigns.buffer, choice) socket |> assign(buffer: buffer) |> handle_submit() end def handle_event("complete", _params, socket) do buffer = socket.assigns.queryable.complete(socket.assigns.buffer, socket.assigns.conf) socket = socket |> assign(buffer: buffer) |> push_event("completed", %{buffer: buffer}) |> async_suggest(buffer) {:noreply, socket} end def handle_event("search", _, socket) do handle_submit(socket) end def handle_event("remove-filter", %{"param" => param, "terms" => _}, socket) do params = Map.delete(socket.assigns.params, String.to_existing_atom(param)) {:noreply, push_patch(socket, to: oban_path(socket.assigns.page, params))} end defp handle_submit(socket) do %{buffer: buffer, queryable: queryable} = socket.assigns if String.ends_with?(buffer, ":") or String.ends_with?(buffer, ".") do {:noreply, async_suggest(socket, buffer)} else parsed = queryable.parse(buffer) params = Map.merge(socket.assigns.params, parsed, fn _key, old, new -> old ++ new end) suggestions = queryable.suggest("", socket.assigns.conf) {:noreply, socket |> assign(buffer: "", loading: false, suggestions: suggestions) |> push_patch(to: oban_path(socket.assigns.page, params))} end end def handle_info({:DOWN, _ref, :process, _pid, _reason}, socket) do {:noreply, socket} end def handle_info({ref, suggestions}, socket) when is_reference(ref) do {:noreply, assign(socket, loading: false, suggestions: suggestions)} end defp async_suggest(socket, buffer) do self = self() fun = fn -> suggestions = socket.assigns.queryable.suggest(buffer, socket.assigns.conf, resolver: socket.assigns.resolver ) assigns = socket.assigns |> Map.take(~w(id conf params queryable resolver)a) |> Map.put(:loading, false) |> Map.put(:suggestions, suggestions) send_update(self, __MODULE__, assigns) end fun |> Task.async() |> Task.yield(@spinner_timeout) |> case do nil -> assign(socket, loading: true) _ok -> socket end end end