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]
endGive 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")
...
endand <.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
@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 withinsert_node/4(with the entry'sref, 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 withremove_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/2before 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 isKotoba.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, ornil: 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.
@spec focus(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
Moves the focus to the editor id (focus).
@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
@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.
@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.
@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.
@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.
@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).
@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.
@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.
@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.
@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".
@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 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 thekotoba:assistrequest, 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".