defmodule LivebookWeb.Output.InputComponent do use LivebookWeb, :live_component @impl true def mount(socket) do {:ok, assign(socket, local: false, counter: 0)} end @impl true def update(%{event: :change, value: value}, socket) do {:ok, handle_change(socket, value)} end def update(assigns, socket) do %{value: value, changed: changed} = assigns.input_views[assigns.input.id] socket = socket |> assign(assigns) |> assign(value: value, changed: changed) {:ok, socket} end @impl true def render(assigns) when assigns.input.attrs.type == :image do ~H"""
<.input_label label={@input.attrs.label} changed={@changed} /> <.live_component module={LivebookWeb.Output.ImageInputComponent} id={"#{@id}-input"} input_component_id={@id} value={@value} height={@input.attrs.size && elem(@input.attrs.size, 0)} width={@input.attrs.size && elem(@input.attrs.size, 1)} format={@input.attrs.format} fit={@input.attrs.fit} input_id={@input.id} session_pid={@session_pid} client_id={@client_id} local={@local} />
""" end def render(assigns) when assigns.input.attrs.type == :audio do ~H"""
<.input_label label={@input.attrs.label} changed={@changed} /> <.live_component module={LivebookWeb.Output.AudioInputComponent} id={"#{@id}-input"} input_component_id={@id} value={@value} format={@input.attrs.format} sampling_rate={@input.attrs.sampling_rate} input_id={@input.id} session_pid={@session_pid} client_id={@client_id} local={@local} />
""" end def render(assigns) when assigns.input.attrs.type == :file do ~H"""
<.input_label label={@input.attrs.label} changed={@changed} /> <.live_component module={LivebookWeb.Output.FileInputComponent} id={"#{@id}-input"} input_component_id={@id} value={@value} accept={@input.attrs.accept} input_id={@input.id} session_pid={@session_pid} client_id={@client_id} local={@local} />
""" end def render(assigns) when assigns.input.attrs.type == :utc_datetime do ~H"""
<.input_label label={@input.attrs.label} changed={@changed} help="Choose the time in your local time zone" />
""" end def render(assigns) when assigns.input.attrs.type == :utc_time do ~H"""
<.input_label label={@input.attrs.label} changed={@changed} help="Choose the time in your local time zone" />
""" end def render(assigns) do ~H"""
<.input_label label={@input.attrs.label} changed={@changed} /> <.input_output id={"#{@id}-input"} attrs={@input.attrs} value={@value} myself={@myself} />
""" end defp input_output(%{attrs: %{type: :select}} = assigns) do ~H""" """ end defp input_output(%{attrs: %{type: :checkbox}} = assigns) do ~H"""
<.switch_field data-el-input name="html_value" value={@value} id={@id} />
""" end defp input_output(%{attrs: %{type: :range}} = assigns) do ~H"""
<%= @attrs.min %>
<%= @attrs.max %>
""" end defp input_output(%{attrs: %{type: :textarea}} = assigns) do ~H""" """ end defp input_output(%{attrs: %{type: :password}} = assigns) do ~H""" <.with_password_toggle id={"#{@id}-password-toggle"}> """ end defp input_output(%{attrs: %{type: :date}} = assigns) do ~H""" """ end defp input_output(%{attrs: %{type: type}} = assigns) when type in [:number, :color, :url, :text] do ~H""" """ end defp input_output(assigns) do ~H"""
Unknown input type <%= @input.attrs.type %>
""" end attr :label, :string, required: true attr :changed, :boolean, required: true attr :help, :string, default: nil defp input_label(assigns) do ~H""" <.label help={@help}>
<%= @label %> <.remix_icon icon="error-warning-line text-gray-500" />
""" end defp html_input_type(:number), do: "number" defp html_input_type(:color), do: "color" defp html_input_type(:url), do: "url" defp html_input_type(:text), do: "text" @impl true def handle_event("change", %{"html_value" => html_value}, socket) do case parse(html_value, socket.assigns.input.attrs) do {:ok, value} -> {:noreply, handle_change(socket, value)} :error -> # Force the current value {:noreply, update(socket, :counter, &(&1 + 1))} end end def handle_event("submit", %{"html_value" => html_value}, socket) do case parse(html_value, socket.assigns.input.attrs) do {:ok, value} -> socket = handle_change(socket, value) send(self(), {:queue_bound_cells_evaluation, socket.assigns.input.id}) {:noreply, socket} :error -> {:noreply, socket} end end defp handle_change(socket, value) do prev_value = socket.assigns.value socket = assign(socket, value: value) if value != prev_value do report_change(socket) end socket end defp report_change(%{assigns: assigns} = socket) do send(self(), {:set_input_values, [{assigns.input.id, assigns.value}], assigns.local}) unless assigns.local do report_event(socket, assigns.value) end end defp parse(html_value, %{type: :text}) do {:ok, html_value} end defp parse(html_value, %{type: :textarea}) do # The browser may normalize newlines to \r\n, but we prefer just \n value = String.replace(html_value, "\r\n", "\n") {:ok, value} end defp parse(html_value, %{type: :password}) do {:ok, html_value} end defp parse(html_value, %{type: :number}) do if html_value == "" do {:ok, nil} else case Integer.parse(html_value) do {number, ""} -> {:ok, number} _ -> {number, ""} = Float.parse(html_value) {:ok, number} end end end defp parse(html_value, %{type: :url}) do cond do html_value == "" -> {:ok, nil} Livebook.Utils.valid_url?(html_value) -> {:ok, html_value} true -> :error end end defp parse(html_value, %{type: :select, options: options}) do selected_idx = String.to_integer(html_value) options |> Enum.with_index() |> Enum.find_value(fn {{key, _label}, idx} -> idx == selected_idx && {:ok, key} end) end defp parse(html_value, %{type: :checkbox}) do {:ok, html_value == "true"} end defp parse(html_value, %{type: :range}) do {number, ""} = Float.parse(html_value) {:ok, number} end defp parse(html_value, %{type: :color}) do {:ok, html_value} end defp parse(html_value, %{type: :utc_datetime} = attrs) do if html_value do with {:ok, datetime} <- NaiveDateTime.from_iso8601(html_value), datetime <- truncate_datetime(datetime), true <- in_range?(datetime, attrs.min, attrs.max) do {:ok, datetime} else _ -> :error end else {:ok, nil} end end defp parse(html_value, %{type: :utc_time} = attrs) do if html_value do with {:ok, time} <- Time.from_iso8601(html_value), time <- truncate_time(time), true <- in_range?(time, attrs.min, attrs.max) do {:ok, time} else _ -> :error end else {:ok, nil} end end defp parse(html_value, %{type: :date} = attrs) do if html_value == "" do {:ok, nil} else with {:ok, date} <- Date.from_iso8601(html_value), true <- in_range?(date, attrs.min, attrs.max) do {:ok, date} else _ -> :error end end end defp truncate_datetime(datetime) do datetime |> NaiveDateTime.truncate(:second) |> Map.replace!(:second, 0) end defp truncate_time(time) do time |> Time.truncate(:second) |> Map.replace!(:second, 0) end defp in_range?(%struct{} = datetime, min, max) when struct in [NaiveDateTime, Time, Date] do (min == nil or struct.compare(datetime, min) != :lt) and (max == nil or struct.compare(datetime, max) != :gt) end defp report_event(socket, value) do topic = socket.assigns.input.ref event = %{value: value, origin: socket.assigns.client_id, type: :change} send(socket.assigns.input.destination, {:event, topic, event}) end end