Prompts: a trigger character in the editor opens a menu of results, for
example @ for people. A result inserts a mention, text (an emoji, a
snippet) or a node of the app. See the Prompts guide.
A prompt list has one of two forms:
# The first prompt has the trigger "@", the second has "#".
[people: &MyApp.People.search/1, work: &MyApp.Work.search/1]
# Each trigger given explicitly.
[{"@", :people, &MyApp.People.search/1}, {"#", :work, &MyApp.Work.search/1}]A trigger is exactly one character that is not white space. The name of a
prompt (an atom or a string) is the kind of the mentions that it
inserts.
A prompt
A prompt is a search callback, a callback with options, or a keyword list
with the callback (:search) or a list of items (:items):
people: &MyApp.People.search/1
people: {&MyApp.People.search/1, label: "People in the workshop"}
people: [search: &MyApp.People.search/1, spaces: true, min_length: 2]
emoji: [items: MyApp.Emoji.items(), insert: :text]The options:
:label- the accessible name of the menu, "<name> suggestions" otherwise. A non-empty string of at most 200 characters.:spaces-trueto let the query have spaces ("Ada Lovelace"). A query ends at two spaces in a row, and cannot start with a space. The default isfalse: a space closes the menu.:min_length- the characters of the query before the first search (0 to the:max_length, default 0). A shorter query shows "Keep typing".:max_length- the longest query, 1 to 200 characters (default 64). A longer one closes the menu.:insert- what a result inserts::mention(the default),:text(the item's:text, or its label), or{:node, type}: a node of the editor of that Lexical type, from the item's:attrs.:async-trueto run the search in a task of the LiveView, so that a slow search does not block it. The LiveView then needs ahandle_async/3clause, seeKotoba.Live.handle_prompt_async/3. The callback of an async prompt takes only the query (arity 1).
Searches
A :search callback gets the query (the text after the trigger) and
returns a list of items. A callback with arity 2 also gets the socket,
for example to scope the search to the current user:
[people: fn query, socket -> MyApp.People.search(socket.assigns.scope, query) end]A callback that raises, exits or throws, or that returns anything but a
list, fails: search/3 logs a warning with the prompt name, and the
editor says "Results did not load". A callback runs in the LiveView
process, so a slow one blocks the LiveView while it runs (see :async).
Items
With :items, the editor has the items (at most 500) and filters them as
the person types, with no request to the server: each word of the query
must start a word of the label or the hint, with no regard to case or
accents.
An item is a map with an :id (a string or an integer), a :label (a
string) and these optional keys, with atom or string keys, or an
{id, label} tuple:
:hint- a string shown next to the label;:text- the text that aninsert: :textprompt inserts (the label when there is none);:attrs- the attributes of the node that aninsert: {:node, type}prompt inserts: a map that encodes to JSON in at most 2048 bytes.
Items that do not have this shape are dropped, and at most 50 results go to the menu. Control characters (such as a tab or a line break) are removed from the id, the label, the hint and the text.
The component Kotoba.Components.kotoba/1 sends the triggers, the names,
the options and the items to the editor; the search callbacks stay in the
LiveView. Kotoba.Live.handle_prompt/3 runs them.
Summary
Types
A prompt callback.
What a result inserts.
An item as the editor receives it.
A prompt with its options, normalized.
A prompt, normalized: {trigger, name, callback}.
A prompt list, in one of the two forms of the module doc.
A prompt: a callback, a callback with options, or a keyword list.
Functions
Returns the options that the editor reads (data-prompt-config): for each
prompt with an option that is not the default, a map of the options that
are not, and the items of an :items prompt.
Filters items for a query as the editor does for an :items prompt: each
word of the query must start a word of the label or the hint, with no
regard to case or accents. The items whose label starts with the query
come first.
Finds the prompt with a name, in a prompt list.
Finds the prompt with a name, with its options, in a prompt list.
Normalizes one result item. Returns [item], or [] when the item is
not valid. An optional key that is not valid is dropped.
Returns the map from name to label, for the prompts that have a label,
that the editor reads (data-prompt-labels).
Normalizes a prompt list to {trigger, name, callback} tuples. The
callback of an :items prompt filters its items.
Runs the callback of a prompt for a query, and returns the items for the
editor, or [] when the callback fails. See search/3.
Runs the callback of a prompt for a query. Returns {:ok, items}, or
:error when the callback fails.
Normalizes a prompt list to its prompts with their options (see
normalize/1 for the errors).
Returns the map from trigger to name that the editor reads
(data-prompts).
Types
@type callback() :: (String.t() -> list()) | (String.t(), Phoenix.LiveView.Socket.t() -> list())
A prompt callback.
@type insert() :: :mention | :text | {:node, String.t()}
What a result inserts.
@type item() :: %{ :id => String.t(), :label => String.t(), optional(:hint) => String.t(), optional(:text) => String.t(), optional(:attrs) => map() }
An item as the editor receives it.
@type options() :: %{ trigger: String.t(), name: String.t(), search: callback(), items: [item()] | nil, label: String.t() | nil, spaces: boolean(), min_length: non_neg_integer(), max_length: pos_integer(), insert: insert(), async: boolean() }
A prompt with its options, normalized.
A prompt, normalized: {trigger, name, callback}.
A prompt list, in one of the two forms of the module doc.
A prompt: a callback, a callback with options, or a keyword list.
Functions
Returns the options that the editor reads (data-prompt-config): for each
prompt with an option that is not the default, a map of the options that
are not, and the items of an :items prompt.
Examples
iex> Kotoba.Prompts.config(
...> people: [search: fn _ -> [] end, spaces: true],
...> emoji: [items: [%{id: "tada", label: "tada", text: "🎉"}], insert: :text]
...> )
%{
"emoji" => %{insert: "text", items: [%{id: "tada", label: "tada", text: "🎉"}]},
"people" => %{spaces: true}
}
Filters items for a query as the editor does for an :items prompt: each
word of the query must start a word of the label or the hint, with no
regard to case or accents. The items whose label starts with the query
come first.
Examples
iex> items = [%{id: "1", label: "Ada Lovelace"}, %{id: "2", label: "Grace Hopper"}]
iex> Kotoba.Prompts.filter(items, "lov ad")
[%{id: "1", label: "Ada Lovelace"}]
Finds the prompt with a name, in a prompt list.
Finds the prompt with a name, with its options, in a prompt list.
Normalizes one result item. Returns [item], or [] when the item is
not valid. An optional key that is not valid is dropped.
Examples
iex> Kotoba.Prompts.item(%{id: 7, label: "Ada", hint: "Engineering"})
[%{id: "7", label: "Ada", hint: "Engineering"}]
iex> Kotoba.Prompts.item({"u1", "Grace"})
[%{id: "u1", label: "Grace"}]
iex> Kotoba.Prompts.item(%{id: "tada", label: "tada", text: "🎉"})
[%{id: "tada", label: "tada", text: "🎉"}]
iex> Kotoba.Prompts.item(%{id: 1, label: "urgent", attrs: %{label: "#urgent"}})
[%{id: "1", label: "urgent", attrs: %{"label" => "#urgent"}}]
iex> Kotoba.Prompts.item(%{label: "No id"})
[]
Returns the map from name to label, for the prompts that have a label,
that the editor reads (data-prompt-labels).
Examples
iex> Kotoba.Prompts.labels(people: {fn _ -> [] end, label: "People"}, work: fn _ -> [] end)
%{"people" => "People"}
Normalizes a prompt list to {trigger, name, callback} tuples. The
callback of an :items prompt filters its items.
Raises ArgumentError when the list is not valid: a trigger that is not
one character, a name or a trigger that is given twice, a callback that
is not a function of arity 1 or 2, an option that is not one of the
module doc or that has a value that is not valid, more than two prompts
without explicit triggers, or the two forms mixed.
Examples
iex> [{trigger, name, _fun}] = Kotoba.Prompts.normalize(people: fn _query -> [] end)
iex> {trigger, name}
{"@", "people"}
@spec run(prompt() | options(), String.t(), Phoenix.LiveView.Socket.t() | nil) :: [ item() ]
Runs the callback of a prompt for a query, and returns the items for the
editor, or [] when the callback fails. See search/3.
@spec search(prompt() | options(), String.t(), Phoenix.LiveView.Socket.t() | nil) :: {:ok, [item()]} | :error
Runs the callback of a prompt for a query. Returns {:ok, items}, or
:error when the callback fails.
A query that is longer than the prompt's :max_length gives no items,
and the callback is not called.
A callback that raises, exits or throws, or that does not return a list, fails, and a warning with the prompt name goes to the log.
Normalizes a prompt list to its prompts with their options (see
normalize/1 for the errors).
Returns the map from trigger to name that the editor reads
(data-prompts).
Examples
iex> Kotoba.Prompts.triggers(people: fn _ -> [] end, work: fn _ -> [] end)
%{"#" => "work", "@" => "people"}