howdy/ui/rich_text
A rich text editor: bold, italic, links, headings, quotes, lists and code, with mentions of people or anything else, and files sent straight to storage, posted as a document your server can trust.
html.form([attribute.method("post"), attribute.action("/posts")], [
rich_text.new(name: "body")
|> rich_text.value(post.body)
|> rich_text.placeholder("Write something…")
|> rich_text.mentions(trigger: "@", kind: "person", search: "/people/search")
|> rich_text.attachments(start: "/posts/uploads", url: fn(file) {
Some(files.url(file.key))
})
|> rich_text.required
|> rich_text.view,
ui.submit_button(Primary, [], [text("Post")]),
])
The form posts the document as JSON under name. Read it with field
in a form validator, or document.decode, which refuses anything the
editor could not have made: decoding is the safety check, and
document.view draws the result through Lustre, so its text is always
escaped. Store it with document.to_string.
Typing a mention’s trigger, such as @, asks your search endpoint
for suggestions as the person types: GET /people/search?q=ad answers
with JSON made by suggestions_to_json:
[{"id": "42", "label": "Ada Lovelace", "detail": "ada@example.com", "image": "/avatars/42.png"}]
The ids come back from the browser, so check each mention with
document.mentions before acting on it, such as sending a
notification. Show mentions from your records as they are now with
document.view_with.
Attachments use the same endpoint as howdy/ui/uploader: the browser
posts each file’s name, type and size to start, which answers with a
key and where to send the file. The document keeps the key. Check each
one with document.attachments before you keep it.
The editor runs on ProseMirror, loaded the first time an editor is on
a page from the ui.rich_text() controller, which every app that uses
it must mount; see howdy/ui/rich_text/script. It looks like the rest of the theme: the toolbar,
menus and text use the same tokens and prose styles. Markdown-like
shortcuts work as you type: # for a heading, > for a quote, - or
1. for a list, three backticks for code, and **bold**, _italic_,
~~strike~~ and `code`.
In a live view, live.on_value(name, message) hears the document each
time the editor loses focus. Setting a different value from the
server, such as clearing it after sending, replaces what is in the
editor.
Types
Something a mention search found. label is what the mention shows
after its trigger; detail, such as an email address, and image,
such as an avatar, appear in the list only.
pub type Suggestion {
Suggestion(
id: String,
label: String,
detail: option.Option(String),
image: option.Option(String),
)
}
Constructors
-
Suggestion( id: String, label: String, detail: option.Option(String), image: option.Option(String), )
A button on the toolbar.
pub type Tool {
Bold
Italic
Strike
Code
Link
Heading(level: Int)
Quote
BulletList
NumberedList
CodeBlock
Divider
Attach
Undo
Redo
}
Constructors
-
Bold -
Italic -
Strike -
CodeCode within a line.
-
Link -
Heading(level: Int)A heading of this level, from 1 to 6. The button turns a paragraph into the heading, and back.
-
Quote -
BulletList -
NumberedList -
CodeBlock -
DividerA horizontal rule.
-
AttachChoose files to attach. Shown only with
attachments. -
Undo -
Redo
Values
pub fn accept(editor: Editor, types: List(String)) -> Editor
Only attach files of these types: content types such as
"image/png", families such as "image/*", or extensions such as
".pdf". Use with attachments.
pub fn attached_class() -> css.Class
A file in the editor: a picture, or a card while it uploads and for other files.
pub fn attachments(
editor: Editor,
start start: String,
url url: fn(document.Attachment) -> option.Option(String),
) -> Editor
Let people attach files, by the toolbar, pasting or dropping them. The
browser asks the endpoint at start where to send each one, as
howdy/ui/uploader does. url gives the address to show an attached
file by, such as a signed storage URL, or None to show only its name.
pub fn content_class() -> css.Class
The text, set as prose, whether drawn by the server or the editor.
pub fn default_tools() -> List(List(Tool))
The toolbar an editor has unless given tools.
pub fn disabled(editor: Editor) -> Editor
Show the document without letting it be edited. Nothing is posted.
pub fn field(
form: form.Form,
name: String,
rules: List(
fn(document.Document) -> Result(
document.Document,
problem.Problem,
),
),
next: fn(document.Document) -> Result(
b,
List(service.FieldError),
),
) -> Result(b, List(service.FieldError))
A required rich text field of a form, checked with rules. A value
that is missing or is not a document the editor could have made is an
error, so rules only see real documents.
use body <- rich_text.field(fields, "body", [rich_text.not_empty()])
pub fn heading_icon_class() -> css.Class
pub fn id(editor: Editor, id: String) -> Editor
An id for a <label for>, such as a field’s. Clicking the label
focuses the editor, and the label names it. Without one, the id is
made from the name.
pub fn invalid(editor: Editor) -> Editor
Mark the editor as holding a value that was refused, such as when its field has an error.
pub fn link_form_class() -> css.Class
pub fn link_input_class() -> css.Class
pub fn max_length(
length: Int,
) -> fn(document.Document) -> Result(
document.Document,
problem.Problem,
)
Refuse a document whose plain text is longer than this many characters.
pub fn max_size(editor: Editor, bytes: Int) -> Editor
Refuse attachments over this many bytes before they are sent. Your
start endpoint must check too. Use with attachments.
pub fn mentions(
editor: Editor,
trigger trigger: String,
kind kind: String,
search search: String,
) -> Editor
Typing trigger, such as "@" or "#", suggests things of kind to
mention, from the endpoint at search. It is sent what follows the
trigger as ?q= and answers with suggestions_to_json. Call this once
for each trigger.
pub fn menu_class() -> css.Class
The list of suggestions, placed under the text being typed.
pub fn not_empty() -> fn(document.Document) -> Result(
document.Document,
problem.Problem,
)
Refuse a document with no text, mentions, attachments or dividers.
pub fn option_avatar_class() -> css.Class
pub fn option_detail_class() -> css.Class
pub fn option_label_class() -> css.Class
pub fn placeholder(editor: Editor, text: String) -> Editor
Shown, faintly, while the editor is empty.
pub fn separator_class() -> css.Class
pub fn suggestion(
id id: String,
label label: String,
) -> Suggestion
A suggestion with an id and a label alone.
pub fn suggestions_to_json(
suggestions: List(Suggestion),
) -> json.Json
The answer to a mention search, as the editor expects it. Send at most ten or so, the best first.
pub fn tool_class() -> css.Class
pub fn toolbar_class() -> css.Class
pub fn tools(editor: Editor, groups: List(List(Tool))) -> Editor
The toolbar’s buttons, in groups with a line between each. The default
is default_tools(), with Attach when there are attachments. An
empty list hides the toolbar; the keyboard shortcuts still work.
pub fn unavailable_class() -> css.Class
pub fn value(editor: Editor, value: document.Document) -> Editor
The document to start with.
pub fn words(editor: Editor, words: words.Words) -> Editor
Say things in other words, or another language: the editor uses the
words’ rich_text.