A rich text editor for delivery notes, scope and long comments: a toolbar over a TipTap (ProseMirror)
surface, the way the canvas draws it (.editor, .editor-toolbar, .editor-btn, .editor-content).
The text lives in the browser (DevactionUI.Editor hook) and goes out as HTML and as Markdown in two hidden
inputs, <name>[html] and <name>[markdown], which fire input on every change so a form with phx-change
hears it. The initial content is value, as HTML. The root is ignored by LiveView patches, so the text is
never lost to a re-render; to replace it, give the editor a new id.
The server must treat what comes back as untrusted: sanitize the HTML before it is stored or shown.
Typing / at the start of a line opens a menu of blocks (slash, each %{key, label} with a key of
paragraph, h2, h3, bulletList, orderedList, taskList, blockquote, codeBlock or divider),
and @ opens the members (mentions, each %{id, label}); a chosen member stays in the text as
<span data-type="mention" data-id="...">, which is what the server reads to know who was mentioned.
Each slash and mentions entry may also carry an icon (a glyph) and a hint (what is written at the end
of the row, such as #). compact is the comment form: a short surface, usually without a toolbar, and with
send_on_enter the command (or control) and Enter submit the form the editor is in; its footer slot is for
the buttons under the surface. With toolbar_position={:bottom} the toolbar goes under the surface, in one bar
with the footer (tools on the left, the send button on the right), as the canvas draws a comment box.
The hook is tied to the host by names the caller can change: the image is sent with the CSRF token of the
<meta> called csrf_meta in the header csrf_header, and the ai tool sends the event improve_event and
listens for replace_event and failed_event (the defaults are Phoenix's csrf-token and x-csrf-token, and
improve, editor:replace and editor:failed).
The field has a visible label (show_label), a hint slot beside the word count, an error and a disabled
state. The label and the error sit outside the ignored root and the flags are data-* attributes, so they
follow the server; the toolbar, the hint and the footer are drawn once.
TipTap is loaded by the hook only when an editor is mounted (a dynamic import()), so an app that never
renders one does not need its npm packages: see assets/package.json for the list and the versions.
Buttons come from the :tool slot, each with the command it runs (bold, italic, strike, code,
bulletList, orderedList, taskList, blockquote, indent, outdent, alignLeft, alignCenter,
alignRight, undo, redo, link, image (which sends a file to upload_url) or ai (which sends the Markdown as the event
improve to the component the editor is in, and takes back the text it answers with in editor:replace)), its label and its icon as the body. A tool with separator is
preceded by a divider. The library holds no icons and no language: the caller gives both.
suggest/1 is the popup of those two menus as a component of its own, for a page that draws it itself (a
mock, a server-rendered mention list, the docs): a listbox with a group label (PEOPLE, COMMANDS), rows of
a 26px mark (the initials of a person, or an icon), a title and a hint, the active row highlighted and
carrying the Enter key cap.
Summary
Functions
The popup of the @ and / menus: a listbox with a group label and rows of a mark, a title and a hint.
Functions
Attributes
id(:string) (required)name(:string) (required) - prefix of the two inputs:name[html]andname[markdown].value(:string) - the initial content, as HTML. Defaults to"".label(:string) (required) - the name of the text area, shown whenshow_label.toolbar_label(:string) (required) - the name of the toolbar, distinct from the text area's (e.g.Formatting).show_label(:boolean) - drawlabelabove the editor. Defaults tofalse.error(:string) - what is wrong; marks the field invalid. Defaults tonil.disabled(:boolean) - the text cannot be edited and the tools are off. Defaults tofalse.placeholder(:string) - Defaults to"".styles(:list) -[{label, value}]with valuesparagraph,h2,h3; empty hides the select. Defaults to[].style_label(:string) (required)words_label(:string) (required)slash(:list) -[%{key, label}]for the/menu; empty turns it off. Defaults to[].mentions(:list) -[%{id, label}]for the@menu; empty turns it off. Defaults to[].upload_url(:string) - where theimagetool sends a file (the image is the body of the request; it answers{url}); nil turns the tool off. Defaults tonil.upload_max(:integer) - the largest image, in bytes. Defaults to5242880.upload_errors(:map) - what to say when an upload fails:unsupported,too_large,failed. Defaults to%{}.ai_labels(:map) - texts of theaitool:working,doneandfailed; the tool sendsimprovewith the Markdown to the component the editor is in. Defaults to%{}.compact(:boolean) - the short comment form. Defaults tofalse.toolbar_position(:atom) -:bottomputs the toolbar under the surface, in one bar with thefooter. Defaults to:top. Must be one of:top, or:bottom.csrf_meta(:string) - the name of the<meta>that holds the CSRF token sent with an image. Defaults to"csrf-token".csrf_header(:string) - the header that carries the token with an image. Defaults to"x-csrf-token".improve_event(:string) - the event theaitool sends, with the Markdown. Defaults to"improve".replace_event(:string) - the event the server answersimprove_eventwith:%{id, markdown}. Defaults to"editor:replace".failed_event(:string) - the event the server answers with when it could not improve the text:%{id}. Defaults to"editor:failed".send_on_enter(:boolean) - ⌘↵ or Ctrl↵ submits the form. Defaults tofalse.link_label(:string) (required)class(:any) - Defaults tonil.- Global attributes are accepted.
phx-target: where theaitool sendsimprove. Supports all globals plus:["phx-target"].
Slots
hint- text on the left of the word count, e.g. what Markdown is understood.footer- a bar under the surface, e.g. the send button of a comment.tool- Accepts attributes:name(:string) (required)label(:string) (required)separator(:boolean)
@spec suggest(map()) :: Phoenix.LiveView.Rendered.t()
The popup of the @ and / menus: a listbox with a group label and rows of a mark, a title and a hint.
Attributes
id(:string) (required)label(:string) (required) - the name of the list, for assistive technology.heading(:string) - the label of the group:PEOPLE,COMMANDS. Defaults tonil.active(:integer) - the index of the highlighted row; -1 for none. Defaults to0.enter(:string) - the key cap at the end of the active row. Defaults to"↵".class(:any) - Defaults tonil.target(:any) - aphx-target(a@myself) when used inside a LiveComponent. Defaults tonil.- Global attributes are accepted.
Slots
option(required) - a row; its body is the mark before the title: initials or an icon. Accepts attributes:title(:string) (required)hint(:string) - the second line, such as the role of a person or what a command does.tone(:atom) -:ink(the default) or aDevactionUI.Tone: the colour of the mark.event(:string) - sent when the row is chosen, with itsvalue.value(:string) - sent aschoice.