# `Kotoba.Live`
[🔗](https://github.com/hattoriai/kotoba/blob/v0.5.0/lib/kotoba/live.ex#L1)

Helpers for a LiveView (or a LiveComponent) with a Kotoba editor.

Each helper takes the socket and returns the socket, so that it fits in a
`handle_event/3`, a `handle_info/2` or an upload progress callback. Every
push carries the editor's `id` (the `id` of `Kotoba.Components.kotoba/1`),
so that only that editor acts on it when a page has more than one
editor.

The editor pushes `kotoba:change` only when the component has `change`
(see `Kotoba.Components.kotoba/1`); a form gets the document without it.

## Prompts

    def handle_event("kotoba:prompt", params, socket) do
      {:noreply, Kotoba.Live.handle_prompt(socket, params, prompts(socket))}
    end

    defp prompts(socket) do
      [people: fn query -> MyApp.People.search(socket.assigns.scope, query) end]
    end

Give the same prompt list to the component (`prompts={prompts(@socket)}`,
or build it once in `mount/3`). The component sends the triggers, the
names, the options and the items of a local prompt to the browser; the
search functions run here.

## Uploads

    def mount(_params, _session, socket) do
      {:ok,
       allow_upload(socket, :attachments,
         accept: ~w(.png .jpg .jpeg .gif .webp .pdf),
         max_file_size: 10_000_000,
         max_entries: 5,
         auto_upload: true,
         progress: &handle_progress/3
       )}
    end

    defp handle_progress(:attachments, entry, socket) do
      if entry.done? do
        {:noreply, Kotoba.Live.consume_uploads(socket, :attachments, "post-body")}
      else
        {:noreply, socket}
      end
    end

    # The form's phx-change handler, so that a file that fails validation
    # (too large, a type that is not accepted) loses its marker.
    def handle_event("validate", params, socket) do
      socket = Kotoba.Live.consume_uploads(socket, :attachments, "post-body")
      ...
    end

and `<.kotoba field={@form[:body]} id="post-body" uploads={@uploads.attachments} />`
in the form. The size limits and the accepted types are the ones of
`Phoenix.LiveView.allow_upload/3`; the content type of each file is
checked again on the server (see `Kotoba.Attachments`).

# `consume_uploads`

```elixir
@spec consume_uploads(
  Phoenix.LiveView.Socket.t(),
  atom() | String.t(),
  String.t(),
  keyword()
) ::
  Phoenix.LiveView.Socket.t()
```

Stores the finished uploads of `upload_name` and puts an attachment node
for each in the editor `editor_id`.

For each entry:

  * An entry that is done is consumed (`Phoenix.LiveView.consume_uploaded_entry/3`).
    Its file is checked (`Kotoba.Attachments.describe/2`), stored under a
    new key (`Kotoba.Storage.key/2`, with the extension of the checked
    type) through the storage adapter, and an attachment node with the
    URL from the adapter is pushed with `insert_node/4` (with the entry's
    `ref`, so it takes the place of that file's marker). When the check
    fails, or the adapter gives an error or a result that does not make
    a valid attachment (a URL that is not a safe link, say), a warning is
    logged and the marker is removed with `remove_marker/3`.
  * An entry with an error (too large, a type that is not accepted) is
    cancelled, and its marker is removed. Read `upload_errors/2` before
    this call to show the error.
  * An entry still in progress is left alone.

An entry is consumed once: a call before LiveView has dropped an entry
that an earlier call consumed leaves it alone. So the function can run
in the progress callback and in the form's change handler, with files
that finish at different times.

## Options

  * `:storage` - the storage adapter. The default is
    `Kotoba.Storage.adapter/0`.
  * `:key` - a function of the upload entry and the checked content
    type that returns a storage key. The default is
    `&Kotoba.Storage.key(&1.client_name, &2)`.
  * `:preview` - a function of the stored attachment
    (`Kotoba.Nodes.Attachment`) and the path of the uploaded file that
    returns the URL of a preview image, or `nil`: the first page of a
    PDF, the poster frame of a video. It runs after the file is stored,
    while its temporary file is still there; the app renders and stores
    the image. A result that is not a safe URL, or an exception, gives no
    preview and logs a warning. See the Uploads guide.

# `focus`

```elixir
@spec focus(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
```

Moves the focus to the editor `id` (`focus`).

# `handle_prompt`

```elixir
@spec handle_prompt(Phoenix.LiveView.Socket.t(), map(), Kotoba.Prompts.prompts()) ::
  Phoenix.LiveView.Socket.t()
```

Answers a `kotoba:prompt` event: runs the search of the prompt with the
query, and pushes `kotoba:prompt_results` to the editor.

`params` is the payload of the event (`%{"id", "prompt", "query"}`), and
`prompts` the prompt list (see `Kotoba.Prompts`). The pushed payload is
`%{id, prompt, query, items: [item]}`, with `error: true` when the search
failed. The editor uses `query` to drop results that arrive after a
newer query. A prompt name that is not in the list gives no items. A
payload without a prompt and a query is ignored.

The search of an `async: true` prompt runs in a task of the LiveView
(`Phoenix.LiveView.start_async/3`), so the LiveView goes on while it
runs. Its result comes to `handle_async/3`, which gives it to
`handle_prompt_async/3`:

    def handle_async({:kotoba_prompt, _id, _prompt, _query} = name, result, socket) do
      {:noreply, Kotoba.Live.handle_prompt_async(socket, name, result)}
    end

# `handle_prompt_async`

```elixir
@spec handle_prompt_async(
  Phoenix.LiveView.Socket.t(),
  term(),
  {:ok, term()} | {:exit, term()}
) ::
  Phoenix.LiveView.Socket.t()
```

Pushes the result of the search of an `async: true` prompt (see
`handle_prompt/3`) to the editor. `name` and `result` are the arguments
of `handle_async/3`. A search that exits (a crash or a timeout of the
task) gives `error: true`, as a search that fails.

# `insert_node`

```elixir
@spec insert_node(
  Phoenix.LiveView.Socket.t(),
  String.t(),
  Kotoba.Node.t() | map(),
  keyword()
) ::
  Phoenix.LiveView.Socket.t()
```

Inserts a node in the editor `id` (`insert_node`), at the selection.

`node` is a node struct (a module that uses `Kotoba.Node`), which is sent
in its JSON form (camelCase keys, as in Lexical), or a node JSON map. The
editor refuses a node of a type that it does not know.

## Options

  * `:ref` - the ref of a LiveView upload entry. The node takes the place
    of that upload's marker. Without it, the node takes the place of the
    oldest marker, if any.

Raises `ArgumentError` when a node struct is not valid.

# `push_content`

```elixir
@spec push_content(
  Phoenix.LiveView.Socket.t(),
  String.t(),
  Kotoba.Content.t() | Kotoba.Document.t() | map() | String.t()
) :: Phoenix.LiveView.Socket.t()
```

Replaces the document of the editor `id` (`set_content`).

`content` is a `Kotoba.Content`, a `Kotoba.Document`, or anything that
`Kotoba.Content.cast/1` accepts. The editor clears its undo history and
does not push `kotoba:change` for it.

Raises `ArgumentError` when `content` is not a valid document.

# `remove_marker`

```elixir
@spec remove_marker(Phoenix.LiveView.Socket.t(), String.t(), String.t()) ::
  Phoenix.LiveView.Socket.t()
```

Removes the upload marker of the LiveView upload entry `ref` from the
editor `id` (`remove_marker`), for an upload that failed.

# `set_readonly`

```elixir
@spec set_readonly(Phoenix.LiveView.Socket.t(), String.t(), boolean()) ::
  Phoenix.LiveView.Socket.t()
```

Makes the editor `id` read-only, or editable again (`set_readonly`).

# `stream_cancel`

```elixir
@spec stream_cancel(Phoenix.LiveView.Socket.t(), String.t(), String.t()) ::
  Phoenix.LiveView.Socket.t()
```

Cancels the suggestion `ref` of the editor `id`, for example when the
model failed: the editor closes it, and the document stays as it was.

# `stream_chunk`

```elixir
@spec stream_chunk(Phoenix.LiveView.Socket.t(), String.t(), String.t(), String.t()) ::
  Phoenix.LiveView.Socket.t()
```

Adds text to the suggestion `ref` of the editor `id`. The chunks can cut
the text anywhere, Markdown syntax included: the editor reads the whole
text again at each chunk.

# `stream_end`

```elixir
@spec stream_end(Phoenix.LiveView.Socket.t(), String.t(), String.t()) ::
  Phoenix.LiveView.Socket.t()
```

Ends the suggestion `ref` of the editor `id`: the person can accept or
reject it.

# `stream_ref`

```elixir
@spec stream_ref() :: String.t()
```

Returns a new ref for a stream that the server starts by itself. A
stream that answers a `kotoba:assist` event takes the event's `"ref"`.

# `stream_start`

```elixir
@spec stream_start(Phoenix.LiveView.Socket.t(), String.t(), String.t(), keyword()) ::
  Phoenix.LiveView.Socket.t()
```

Starts a suggestion in the editor `id`: text that the server streams
with `stream_chunk/4` and ends with `stream_end/3`. See the
[Suggestions](suggestions.md) guide.

The text is not in the document while it streams: the editor shows it in
a panel, and the person accepts it (it goes into the document, as one
undo step) or rejects it. A new stream in the same editor replaces the
open suggestion.

`ref` is the ref of the `kotoba:assist` event that asked for it, or a new
one from `stream_ref/0`.

## Options

  * `:at` - where the text goes: `:selection` (in place of the
    selection, the default), `:caret`, `:after` (after the block of the
    selection) or `:end` (at the end of the document). The selection is
    the one of the `kotoba:assist` request, or the editor's selection
    when the stream starts.
  * `:format` - `:markdown` (the default) or `:text`. Markdown has only
    the formats and blocks of the editor's features.
  * `:label` - the name of the suggestion, for example `"Rewrite"`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
