Kotoba.Live (Kotoba v0.5.0)

Copy Markdown View Source

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).

Summary

Functions

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

Moves the focus to the editor id (focus).

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

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.

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

Replaces the document of the editor id (set_content).

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

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

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.

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.

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

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".

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 guide.

Functions

consume_uploads(socket, upload_name, editor_id, opts \\ [])

@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(socket, id)

Moves the focus to the editor id (focus).

handle_prompt(socket, params, prompts)

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(socket, arg2, result)

@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(socket, id, node, opts \\ [])

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(socket, id, content)

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(socket, id, ref)

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

set_readonly(socket, id, readonly)

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

stream_cancel(socket, id, ref)

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(socket, id, ref, text)

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(socket, id, ref)

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

stream_ref()

@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(socket, id, ref, opts \\ [])

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 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".