Kotoba.Prompts (Kotoba v0.5.0)

Copy Markdown View Source

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 - true to let the query have spaces ("Ada Lovelace"). A query ends at two spaces in a row, and cannot start with a space. The default is false: 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 - true to run the search in a task of the LiveView, so that a slow search does not block it. The LiveView then needs a handle_async/3 clause, see Kotoba.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 an insert: :text prompt inserts (the label when there is none);
  • :attrs - the attributes of the node that an insert: {: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

callback()

@type callback() ::
  (String.t() -> list()) | (String.t(), Phoenix.LiveView.Socket.t() -> list())

A prompt callback.

insert()

@type insert() :: :mention | :text | {:node, String.t()}

What a result inserts.

item()

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

options()

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

prompt()

@type prompt() :: {String.t(), String.t(), callback()}

A prompt, normalized: {trigger, name, callback}.

prompts()

@type prompts() :: [
  {atom() | String.t(), spec()} | {String.t(), atom() | String.t(), spec()}
]

A prompt list, in one of the two forms of the module doc.

spec()

@type spec() :: callback() | {callback(), keyword()} | keyword()

A prompt: a callback, a callback with options, or a keyword list.

Functions

config(prompts)

@spec config(prompts() | nil) :: %{required(String.t()) => map()}

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}
}

filter(items, query)

@spec filter([item()], String.t()) :: [item()]

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"}]

find(prompts, name)

@spec find(prompts() | nil, String.t()) :: prompt() | nil

Finds the prompt with a name, in a prompt list.

find_spec(prompts, name)

@spec find_spec(prompts() | nil, String.t()) :: options() | nil

Finds the prompt with a name, with its options, in a prompt list.

item(map)

@spec item(term()) :: [item()]

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"})
[]

labels(prompts)

@spec labels(prompts() | nil) :: %{required(String.t()) => String.t()}

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

normalize(prompts)

@spec normalize(prompts() | nil) :: [prompt()]

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

run(prompt, query, socket \\ nil)

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

search(prompt, query, socket \\ nil)

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

specs(prompts)

@spec specs(prompts() | nil) :: [options()]

Normalizes a prompt list to its prompts with their options (see normalize/1 for the errors).

triggers(prompts)

@spec triggers(prompts() | nil) :: %{required(String.t()) => String.t()}

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