This guide is the tour of what the editor does, and how to choose it for each field: the built-in features, their toolbar buttons, keyboard and Markdown shortcuts, colors, links, code blocks, tables, and the toolbar itself. To see them all at once, run the development server of this repository (mix dev, see the README).

The features

Each built-in feature is a part of the editor that a field can have or not (Kotoba.Features):

FeatureWhat it isToolbarKeyboardMarkdown
boldbold textBoldCmd/Ctrl+B**text**, __text__
italicitalic textItalicCmd/Ctrl+I*text*, _text_
underlineunderlined textUnderlineCmd/Ctrl+U
strikethroughstruck textStrikethrough~~text~~
highlighthighlights and text colorsHighlight (the palette)Cmd/Ctrl+Shift+H==text==
subscriptsubscript text(an app's toolbar)Cmd/Ctrl+,
superscriptsuperscript text(an app's toolbar)Cmd/Ctrl+.
inline_codecode in a lineInline code`code`
linkslinks and autolinksLinkCmd/Ctrl+K[text](url)
headingsheadings 1 to 4Heading 1 to 4# to ####
quotesblock quotesQuote>
listsbulleted and numbered listsBulleted list, Numbered listTab, Shift+Tab-, *, +, 1.
check_listscheck lists (needs lists)Check listSpace on a focused box[ ], [x], - [ ]
code_blockscode blocks, highlighted, with a language pickerCode block, Code languageTab inserts a tab```, ```elixir
horizontal_ruleshorizontal rulesHorizontal rule---, ***, ___
tablestablesTable, and the Table groupTab, Shift+Tab
attachmentsfiles, images and galleries (with uploads)Attach a file, and the Image groupdrop, paste; Alt+Left, Alt+Right in a gallery; Alt+F10 to the caption
mentionsprompts and mentions (with prompts)the trigger
imagesimages from a URL, in the text![alt](url) (pasted)
footnotesfootnotes and their references[^1] and [^1]: text (pasted)
embedslive things of the app's own: a canvas, a grid (with embeds, see Embeds)the app's button ("Canvas")Backspace after one deletes ita fenced block the app reads (pasted)
blocksa handle beside each block: drag it to move the block, click it for the block's menu (only when listed, see below)Alt+Shift+Up, Alt+Shift+Down, Cmd/Ctrl+/

Pasted Markdown is not a feature: plain text that reads as Markdown (a README, a note from a Markdown app) comes in as rich text, read on the server as Kotoba.Content.from_markdown/2 reads it, with the features of the editor. <.kotoba> renders the server's half, so the app adds no code; paste_markdown={false} turns it off. Images and footnotes come from pasted Markdown and from the server; the editor has no button for them yet.

Suggestions are not a feature: an editor with assist has an Assist button, and any editor shows the text that the server streams with Kotoba.Live.stream_start/4 (see Suggestions). A suggestion has only the editor's features.

Cmd is the key on a Mac, Ctrl elsewhere. A Markdown shortcut acts as you type it: ## at the start of a line makes a heading, and **bold** becomes bold when you type the last *. ***text*** makes bold and italic text.

Paragraphs, line breaks (Shift+Enter), tabs, undo (Cmd/Ctrl+Z) and redo (Cmd/Ctrl+Shift+Z) are always there. The case formats (lowercase, uppercase, capitalize) are not features: the editor keeps them when they come in a pasted text or a stored document.

Choose the features of a field

With no features, an editor has every feature but blocks, which a field has only when its list names it (features={Kotoba.Features.all()} gives every one). Give a list for fewer:

<.kotoba field={@form[:comment]} id="comment" features={~w(bold italic links lists mentions)} />
<.kotoba field={@form[:body]} id="body" />

A feature that is off is not in the editor at all:

  • its buttons are not in the default toolbar, and are hidden in a custom one;
  • its Markdown shortcut types the text, and its keyboard shortcut does nothing;
  • pasted content of it comes in as plain paragraphs and text: a pasted table, heading or code block is paragraphs, and a pasted format is dropped;
  • a stored node of it loads as an unknown node: the editor shows its type in a placeholder, and saves it with no change.

attachments needs uploads too (see the Uploads guide), and mentions needs prompts (see Prompts and mentions).

Check them on the server

The editor keeps out what it does not have, but a request can post any document. Check the field in its changeset:

def changeset(comment, attrs) do
  comment
  |> cast(attrs, [:body])
  |> Kotoba.Content.validate_features(:body, ~w(bold italic links lists mentions)a)
end

A document with another feature gets the error "has content that is not allowed: tables" (with validation: :features and the names in the error's keys). Kotoba.Features.used/1 and Kotoba.Features.check/2 read the features of a Kotoba.Document. App nodes are not features: the check allows them.

Text formats and colors

The Text group of the default toolbar has Bold, Italic, Underline, Strikethrough, Highlight and Inline code. A format button has aria-pressed, and the live region says "Bold on" or "Bold off".

Subscript and superscript are features with their keyboard shortcuts, but have no button in the default toolbar. An app's toolbar can have them (command="subscript", command="superscript", see below). Turning one on turns the other off.

The Highlight button opens the color palette: seven text colors, seven highlights (red, orange, yellow, green, blue, purple and gray) and "Remove color". Cmd/Ctrl+Shift+H and ==text== give the default highlight, yellow. Text colors are part of the highlight feature.

The document stores a color as its name, never as a CSS value, and the HTML has classes: span.kotoba-color-red and mark.kotoba-highlight-green (the default highlight is a plain mark). kotoba.css gives each name a color that reads on light and dark backgrounds; the Theming guide shows how to set your own. Kotoba.Nodes.Text.colors/1 reads the colors of a text node, and Kotoba.Nodes.Text.palette/0 gives the names.

The Link button or Cmd/Ctrl+K opens a small form for the URL of the selected text. Enter applies it, an empty URL removes the link, and Escape closes the form. Pasting a URL over selected text makes a link, and the editor links the URLs and email addresses that you type (autolinks).

A link can be relative (/docs, #part) or have an allowed scheme: http, https and mailto by default. To allow others:

config :kotoba, allowed_link_schemes: ~w(http https mailto tel)

A link with another scheme becomes its text, in the editor and in the HTML. The HTML of a link has rel="noopener nofollow".

Lists and check lists

Tab indents a list item and Shift+Tab outdents it (in an item that is not indented, Shift+Tab leaves the editor). In a check list, a click on the box, or Space when the box has the focus, checks the item. The HTML of a check list is ul.kotoba-check, with li.kotoba-checked and li.kotoba-unchecked items, which kotoba.css draws with a box.

Code blocks

In a code block, the toolbar shows the code language picker. It offers plain text and the 28 languages of Kotoba.CodeLanguages, and the editor highlights the code as you type. ```elixir at the start of a line makes a code block in that language. To offer fewer languages, give their ids or aliases:

<.kotoba field={@form[:body]} id="post-body" code_languages={~w(elixir erlang sql bash)} />

A name that is not a language, and an empty list, raise ArgumentError. The list chooses what the picker offers, not what the editor keeps: a pasted or stored block in another language keeps its language, is highlighted when the editor knows it, and shows in the picker.

The HTML of a code block is pre with code.language-<language>, and no highlighting. To highlight rendered code, use a highlighter of your own on the page, for example Prism with a Prism theme: the editor's own copy of Prism does not touch the page's window.Prism (see the Theming guide).

Galleries

Images uploaded together make a gallery, a grid of images. Select an image (a click, or the arrow keys from the text next to it) for the Image group of the toolbar: Gallery groups the image with the images next to it, or ungroups its gallery, and Move image left and Move image right (or Alt+Left and Alt+Right) reorder it. See the Uploads guide.

Block handles

With blocks in its features, the editor has a margin on the left of its blocks, as a block editor has. The pointer over a block (a paragraph, a heading, a list, a quote, a code block, a table, a gallery, an embed) shows two buttons there:

  • a grip (⋮⋮). Drag it to move the whole block: a line shows where it lands, and Escape calls the drag off. Click it for the block's menu;
  • a +, which adds an empty paragraph below the block, with the caret in it, and opens the insert menu there when the editor has one (see the Prompts guide).

The menu has:

  • Turn into, for a block of text: Paragraph, Heading 1 to 4, Quote, Bulleted list, Numbered list, Check list, Code block and the blocks of the app's extensions (a callout, see the Extensions guide), as the editor's features allow: an editor without headings offers no heading. The block's own kind is checked. A list turns into paragraphs as a whole, and a quote of paragraphs gives them up first;
  • Duplicate, a copy of the block after it;
  • Delete;
  • Copy a link to the block: the page's URL with a text fragment of the block's first words (#:~:text=Second%20paragraph), which scrolls a browser to them on the page where the document is rendered.

From the keyboard, Alt+Shift+Up and Alt+Shift+Down move the block at the caret, with the caret, and Cmd/Ctrl+/ opens its menu. In the menu, the arrow keys, Home and End move between the items, Enter or Space chooses one, and Escape closes it and gives the caret back; Tab leaves it. The live region says what each command did ("Block moved down", "Turned into Heading 2"). The handles are not tab stops, and a read-only editor has none.

A table or a list has one handle, for the whole block, and moves as a whole: a list item moves its list. A gallery and an embed keep their own dragging; only the grip moves the block.

Each change is one undo step. In a shared editor (see Collaboration) a move is a move of the block in the shared document: another person's caret in it goes with it, and the undo of the person who moved it takes it back. An agent moves a block with the move_block tool.

The handles are div.kotoba-block-handle (with button.kotoba-block-add and button.kotoba-block-grip), the line div.kotoba-block-drop, and the menu div.kotoba-block-menu. --kotoba-block-gutter is the width of the margin (3.25rem).

Tables

The Table button inserts a table of three rows and three columns, with a header row. In a table, the Table group of the toolbar has Insert row above, Insert row below, Insert column before, Insert column after, Header row, Header column, Delete row, Delete column and Delete table. Tab and Shift+Tab move between the cells; Alt+F10 goes to the toolbar.

A pasted HTML table becomes a table. The editor has no command to merge cells, but it keeps the merged cells of a pasted or stored table (colspan, rowspan).

The HTML is a table.kotoba-table in a div.kotoba-table-scroll (which scrolls a wide table sideways), with a thead for a header row and th scope="col" or th scope="row" for header cells. kotoba.css styles it. The Markdown of a table is a GitHub Flavored Markdown table, with the limits in the Known limits guide.

The toolbar

The default toolbar has the buttons of the editor's features, in groups: Text (formats and Link), Blocks (headings and Quote), Lists, Insert (Code block, Horizontal rule, Table, Attach a file), Assist (with assist), the extensions' buttons, and History (Undo, Redo). The Code group (the language picker) shows in a code block, the Table group in a table, and the Image group (Gallery, Move image left, Move image right) when an image is selected. The toolbar is one tab stop, with the arrow keys between the buttons (see the Accessibility guide).

A custom toolbar

To choose the buttons, their order or their icons, give a toolbar slot with Kotoba.Components.kotoba_toolbar/1:

<.kotoba field={@form[:body]} id="post-body">
  <:toolbar>
    <.kotoba_toolbar>
      <:button command="bold"><.icon name="hero-bold" /></:button>
      <:button command="italic"><.icon name="hero-italic" /></:button>
      <:button command="link" label="Add a link"><.icon name="hero-link" /></:button>
      <:button command="subscript" label="Subscript" class="my-button">x₂</:button>
      <:button command="undo"><.icon name="hero-arrow-uturn-left" /></:button>
    </.kotoba_toolbar>
  </:toolbar>
</.kotoba>

Each button slot has a command, an optional label (the accessible name and the title; the command's own label by default) and an optional class. With no slots, commands gives text buttons for a list of commands:

<.kotoba_toolbar commands={~w(bold italic underline highlight subscript superscript undo redo)} />

Kotoba.Components.toolbar_commands/0 lists the commands: bold, italic, underline, strikethrough, highlight, code, subscript, superscript, link, h1 to h4, quote, bullet, number, check, code-block, rule, table, upload, assist, code-language, the table-* commands, gallery, image-previous, image-next, undo and redo. An extension's control is <extension>:<command> (see the Extensions guide). Another command raises ArgumentError.

  • The editor adds role="toolbar", the roving tabindex, aria-pressed and aria-disabled; label names the toolbar ("Formatting" by default), and class adds classes.
  • The buttons of a feature that the editor does not have are hidden.
  • The table-* buttons are hidden when the selection is not in a table, and gallery, image-previous and image-next when no image is selected.
  • code-language renders a <select>, the code language picker: the editor fills it, and shows it in a code block.
  • highlight opens the color palette.
  • assist opens the Assist menu, and is hidden in an editor with no actions.

A toolbar can also be outside the editor, anywhere on the page, with for set to the editor's id:

<.kotoba_toolbar for="post-body" commands={~w(bold italic link)} />
<.kotoba field={@form[:body]} id="post-body" />

The selection menu

A menu can float over the selected text, as in Notion, Medium or Tiptap's bubble menu. Turn it on with selection_menu:

<.kotoba field={@form[:body]} id="post-body" selection_menu />

The default menu has Bold, Italic, Underline, Strikethrough, Inline code, Highlight and Link. A list chooses the commands, in order; they are the toolbar's commands, and an extension's control is <extension>:<command>:

<.kotoba field={@form[:body]} selection_menu={~w(bold italic link h2 quote callout:toggle)} />

The menu shows above a selection of text, or below it when there is no room above (in the editor or in the window). It waits for the mouse button to come up, so it does not follow a drag. It does not show for a caret, in a code block, in a read-only editor, or while the link form, the palette or a prompt menu is open. Its buttons work as the toolbar's buttons: aria-pressed follows the selection, and the buttons of a feature that the editor does not have are left out.

  • Alt+F10 moves the focus to the menu (to the toolbar when no menu shows); the arrow keys move between its buttons.
  • Escape closes it until the selection changes.

An app's own menu

For the app's own icons, labels or classes, give a selection slot with Kotoba.Components.kotoba_selection_menu/1. It takes button slots, as kotoba_toolbar/1 does:

<.kotoba field={@form[:body]} id="post-body">
  <:selection>
    <.kotoba_selection_menu class="my-menu">
      <:button command="bold"><.icon name="hero-bold" class="size-4" /></:button>
      <:button command="italic"><.icon name="hero-italic" class="size-4" /></:button>
      <:button command="link" label="Add a link"><.icon name="hero-link" class="size-4" /></:button>
    </.kotoba_selection_menu>
  </:selection>
</.kotoba>

Style the menu

The menu is a div.kotoba-selection-menu with data-placement="top" or "bottom"; its buttons are .kotoba-toolbar-button, in .kotoba-toolbar-group elements. These custom properties theme it and the link card, and each one falls back to the editor's own property:

PropertyDefault
--kotoba-floating-background--kotoba-background
--kotoba-floating-text--kotoba-text
--kotoba-floating-border--kotoba-border
--kotoba-floating-hover--kotoba-surface
--kotoba-floating-shadow--kotoba-shadow
--kotoba-floating-radius--kotoba-radius

For example, a dark menu over a light editor:

.kotoba-selection-menu,
.kotoba-link-card {
  --kotoba-floating-background: #1f2023;
  --kotoba-floating-text: #f4f4f5;
  --kotoba-floating-border: #34353a;
  --kotoba-floating-hover: #34353a;
  --kotoba-floating-radius: 10px;
}

.kotoba-selection-menu .kotoba-toolbar-button[aria-pressed="true"] {
  background: #e8590c;
  color: white;
}

The menu shows with a short animation, which is off with prefers-reduced-motion.

When the caret is in a link, a card shows under it: the URL, which opens in a new tab, and Edit link (the link form) and Remove link buttons. An automatic link (a URL typed in the text) has only the URL. Alt+F10 moves the focus to the card, and Escape closes it until the caret moves. Cmd/Ctrl+click on a link in the editor opens it in a new tab. The card is a div.kotoba-link-card, themed with the same properties as the selection menu.

The caption field

When an attachment is selected, a field shows under it for its caption (see Captions). Alt+F10 moves the focus to it, Enter keeps the caption, and Escape in the editor hides it until the selection changes. It is a div.kotoba-caption-field with an input.kotoba-caption-input, themed with the same properties as the selection menu.

More