defmodule LivebookWeb.Confirm do use Phoenix.Component import LivebookWeb.CoreComponents import LivebookWeb.FormComponents import Phoenix.LiveView alias Phoenix.LiveView.JS @doc """ Shows a confirmation modal. On confirmation runs `on_confirm`. The function receives a socket and should return the socket. Note that this socket always comes from the root LV level, keep this in mind when using in a component and dealing with assigns. Make sure to render `confirm_root/1` in the layout. ## Options * `:title` - title of the confirmation modal. Defaults to `"Are you sure?"` * `:description` - content of the confirmation modal. Required * `:confirm_text` - text of the confirm button. Defaults to `"Yes"` * `:confirm_icon` - icon in the confirm button. Optional * `:danger` - whether the action is destructive or regular. Defaults to `true` * `:opt_out_id` - enables the "Don't show this message again" checkbox. Once checked by the user, the confirmation with this id is never shown again. Optional * `:options` - a list of togglable options to present alongside the confirmation message. Each option must be a map: ``` %{ name: String.t(), label: String.t(), default: boolean(), disabled: boolean() } ``` The option values are passed to the `on_confirm` function as the second argument """ def confirm(socket, on_confirm, opts) do opts = Keyword.validate!( opts, title: "Are you sure?", description: nil, confirm_text: "Yes", confirm_icon: nil, danger: true, opt_out_id: nil, options: [] ) send(self(), {:confirm, on_confirm, opts}) socket end @doc """ Renders the confirmation modal for `confirm/3`. """ attr :confirm_state, :map, required: true def confirm_root(assigns) do ~H""" <.confirm_modal :if={@confirm_state} id={"confirm-#{@confirm_state.id}"} {@confirm_state.attrs} /> """ end defp confirm_modal(assigns) do ~H""" <.modal id={@id} width={:medium} show={true}>
""" end def on_mount(:default, _params, _session, socket) do connect_params = get_connect_params(socket) || %{} # Opting out is a per-user preference, so we store it on the client # and send in the connect params confirm_opt_out_ids = connect_params["confirm_opt_out_ids"] || [] socket = socket |> assign(confirm_state: nil, confirm_opt_out_ids: MapSet.new(confirm_opt_out_ids)) |> attach_hook(:confirm, :handle_event, &handle_event/3) |> attach_hook(:confirm, :handle_info, &handle_info/2) {:cont, socket} end defp handle_event("confirm", params, socket) do socket = if opt_out_id = params["opt_out_id"] do socket |> update(:confirm_opt_out_ids, &MapSet.put(&1, opt_out_id)) |> push_event("add_confirm_opt_out_id", %{opt_out_id: opt_out_id}) else socket end options = for {name, value} <- params["options"] || [], into: %{}, do: {name, value == "true"} on_confirm = socket.assigns.confirm_state.on_confirm socket = cond do is_function(on_confirm, 1) -> on_confirm.(socket) is_function(on_confirm, 2) -> on_confirm.(socket, options) end {:halt, socket} end defp handle_event(_event, _params, socket), do: {:cont, socket} defp handle_info({:confirm, on_confirm, opts}, socket) do socket = if opts[:opt_out_id] && opts[:opt_out_id] in socket.assigns.confirm_opt_out_ids do on_confirm.(socket) else assign(socket, confirm_state: %{ id: Livebook.Utils.random_short_id(), on_confirm: on_confirm, attrs: opts } ) end {:cont, socket} end defp handle_info(_message, socket), do: {:cont, socket} end