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