PhiaUi.Components.Editor (phia_ui v0.1.17)

Copy Markdown View Source

Editor Suite — 19 components for building rich text editing experiences.

Inspired by TipTap — the most popular headless rich text editor (2.5M+ weekly npm downloads, built on ProseMirror, used by Notion, Linear, Vercel, and more) — this module provides all the building blocks needed to assemble a full-featured editor.

EditorWeekly DLsFoundationUsed by
TipTap2.5M+ProseMirrorNotion clones, Linear, Vercel
Quill v21.8M+CustomSlack, LinkedIn, Figma, Airtable
Lexical1.2M+Custom (Meta)Facebook, WhatsApp Web
ProseMirror900K+Base for TipTap/Remirror
CKEditor 5600K+CustomEnterprise / CMS

TipTap wins on DX: headless, framework-agnostic, tree-shakable, collaborative. advanced_editor/1 is a PhiaUI transpilation of TipTap's editor design.

Components

Group A — Toolbar Primitives (no JS hooks)

Group B — Floating / Contextual (JS hooks)

Group C — Enhanced Inline Editing

Group D — Rich Text Utilities

Bonus — TipTap-Inspired Full Editor

Summary

Functions

Full-featured WYSIWYG editor inspired by TipTap's design.

Renders a floating toolbar that appears above the user's text selection.

Renders a server-side character/word counter with optional progress bar.

Renders a styled code block with a header bar (language badge, filename, copy button).

Renders a color palette picker toolbar dropdown for text/background colors.

Renders a slide-in search-and-replace bar for a contenteditable editor.

Renders a modal dialog for inserting or editing a hyperlink.

Renders a role="toolbar" container for editor controls.

Renders a generic dropdown inside a toolbar (heading level, font size, etc.).

Renders a standalone words / characters / reading-time display widget.

Renders a block-insertion menu that floats at the empty cursor line.

Enhanced inline editable field. Extends editable/1 with input type variants, validation errors, loading state, and optional confirm/cancel buttons.

Wrapper for grouping multiple inline_edit/1 fields that share a bulk save/cancel flow.

Renders a split-pane Markdown editor: textarea (write) + server-rendered preview.

Renders a read-only Phoenix.HTML.raw container with prose sizing.

Renders a / trigger command palette for block-level actions.

Renders a single toolbar action button.

Renders a role="group" semantic group for related toolbar buttons.

Renders a thin vertical role="separator" divider between toolbar groups.

Functions

advanced_editor(assigns)

Full-featured WYSIWYG editor inspired by TipTap's design.

TipTap is the most popular headless rich text editor (built on ProseMirror, 2.5M+ weekly npm downloads). This component transpiles TipTap's UX patterns to PhiaUI: same toolbar layout, bubble menus, slash commands, and inline SVG icons — powered entirely by vanilla JS + execCommand via the PhiaAdvancedEditor hook.

Features:

  • Full toolbar: undo/redo, heading dropdown, marks (bold/italic/underline/strike/code), lists (bullet/ordered), links (insert/remove), text color picker, find & replace toggle
  • Bubble menu for quick inline formatting of selected text
  • Floating menu at empty cursor for block insertion
  • Find & replace bar (Ctrl/Cmd+F)
  • Live word/character count footer
  • Phoenix.HTML.FormField integration via hidden <input>
  • Dark mode support via .dark class

Example

<%!-- Standalone --%>
<.advanced_editor id="blog-editor" placeholder="Write your post..." min_height="400px" />

<%!-- Form-integrated --%>
<.form for={@form} phx-submit="publish">
  <.advanced_editor id="post-editor" field={@form[:body]} show_word_count />
</.form>

Hook registration

import PhiaAdvancedEditor from "./hooks/advanced_editor"
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { PhiaAdvancedEditor, PhiaBubbleMenu, PhiaEditorColorPicker, PhiaEditorFindReplace }
})

Attributes

  • id (:string) (required)
  • field (Phoenix.HTML.FormField) - Defaults to nil.
  • value (:string) - Defaults to nil.
  • placeholder (:string) - Defaults to "Type '/' for commands, or start writing...".
  • min_height (:string) - Defaults to "300px".
  • toolbar_variant (:atom) - Defaults to :default. Must be one of :default, :compact, or :floating.
  • show_word_count (:boolean) - Defaults to true.
  • show_find_replace (:boolean) - Defaults to false.
  • class (:string) - Defaults to nil.

bubble_menu(assigns)

Renders a floating toolbar that appears above the user's text selection.

The PhiaBubbleMenu hook listens to selectionchange, computes the selection bounding rect, and positions this div above the selected text. Clamps to viewport.

Example

<.bubble_menu id="bubble" editor_id="my-editor">
  <.toolbar_button action="bold" aria_label="Bold" size={:xs}>
    <svg .../>
  </.toolbar_button>
</.bubble_menu>

Attributes

  • id (:string) (required)
  • editor_id (:string) (required)
  • class (:string) - Defaults to nil.

Slots

  • inner_block (required)

editor_character_count(assigns)

Renders a server-side character/word counter with optional progress bar.

Drives via phx-change on the editor: the LiveView counts characters/words and assigns :count. The progress bar fills and changes colour as count nears max.

Example

<.editor_character_count count={@char_count} max={500} mode={:characters} show_bar />
<.editor_character_count count={@word_count} max={100} mode={:words} />
<.editor_character_count count={@char_count} mode={:both} />

Attributes

  • count (:integer) - Defaults to 0.
  • max (:integer) - Defaults to nil.
  • mode (:atom) - Defaults to :characters. Must be one of :characters, :words, or :both.
  • show_bar (:boolean) - Defaults to false.
  • class (:string) - Defaults to nil.

editor_code_block(assigns)

Renders a styled code block with a header bar (language badge, filename, copy button).

The copy button reuses PhiaCopyButton via data-value={copy_value}. Pass copy_value with the raw code string for clipboard support.

Example

<.editor_code_block id="my-snippet" language="elixir" filename="hello.ex"
                    copy_value={@raw_code}>
  def hello, do: IO.puts("Hello, world!")
</.editor_code_block>

Attributes

  • id (:string) - Defaults to "editor-code-block".
  • language (:string) - Defaults to nil.
  • filename (:string) - Defaults to nil.
  • show_copy (:boolean) - Defaults to true.
  • copy_value (:string) - Defaults to nil.
  • show_line_numbers (:boolean) - Defaults to false.
  • class (:string) - Defaults to nil.

Slots

  • inner_block (required)

editor_color_picker(assigns)

Renders a color palette picker toolbar dropdown for text/background colors.

The PhiaEditorColorPicker hook dispatches execCommand(action, false, hex) on swatch click and tracks the current color via queryCommandValue on selection change.

Example

<.editor_color_picker action="foreColor" label="Text color" />
<.editor_color_picker action="hiliteColor" label="Highlight" value="#EAB308" />

Attributes

  • id (:string) - Defaults to nil.
  • action (:string) - Defaults to "foreColor".
  • label (:string) - Defaults to "Text color".
  • colors (:list) - Defaults to [].
  • value (:string) - Defaults to nil.
  • class (:string) - Defaults to nil.

editor_find_replace(assigns)

Renders a slide-in search-and-replace bar for a contenteditable editor.

The PhiaEditorFindReplace hook opens on Ctrl/Cmd+F inside the editor, injects <mark> elements for matches, and provides find prev/next and replace.

Example

<.editor_find_replace id="find-bar" editor_id="my-editor" />

Attributes

  • id (:string) (required)
  • editor_id (:string) (required)
  • class (:string) - Defaults to nil.

editor_toolbar(assigns)

Renders a role="toolbar" container for editor controls.

Variants:

  • :default — bordered bar, wraps to multiple lines
  • :floating — elevated popover-style (shadow, backdrop blur)
  • :compact — minimal single-line strip

Example

<.editor_toolbar aria_label="Formatting toolbar">
  <.toolbar_button action="bold" aria_label="Bold">
    <svg .../>
  </.toolbar_button>
</.editor_toolbar>

Attributes

  • id (:string) - Defaults to nil.
  • variant (:atom) - Defaults to :default. Must be one of :default, :floating, or :compact.
  • aria_label (:string) - Defaults to "Editor toolbar".
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

editor_toolbar_dropdown(assigns)

Renders a generic dropdown inside a toolbar (heading level, font size, etc.).

The PhiaEditorDropdown hook toggles the panel on trigger click and dispatches the selected item's data-action to the linked editor.

Example

<.editor_toolbar_dropdown id="heading-dd" label="Paragraph">
  <:item value="paragraph" action="paragraph" label="Paragraph" />
  <:item value="h1" action="h1" label="Heading 1" />
  <:item value="h2" action="h2" label="Heading 2" />
  <:item value="h3" action="h3" label="Heading 3" />
</.editor_toolbar_dropdown>

Attributes

  • id (:string) (required)
  • label (:string) (required)
  • value (:string) - Defaults to nil.
  • class (:string) - Defaults to nil.

Slots

  • item - Accepts attributes:
    • value (:string) (required)
    • action (:string)
    • label (:string) (required)

editor_word_count(assigns)

Renders a standalone words / characters / reading-time display widget.

All computation is server-side — pass the raw text content.

Example

<.editor_word_count content={@body_text} show_reading_time words_per_minute={250} />

Attributes

  • content (:string) - Defaults to "".
  • show_reading_time (:boolean) - Defaults to true.
  • words_per_minute (:integer) - Defaults to 200.
  • class (:string) - Defaults to nil.

floating_menu(assigns)

Renders a block-insertion menu that floats at the empty cursor line.

The PhiaFloatingMenu hook shows this panel when the cursor is on an empty block, and hides it on any text input or Escape.

Example

<.floating_menu id="float-menu" editor_id="my-editor" trigger={:empty_line}>
  <.toolbar_button action="bulletList" aria_label="Bullet list">...</.toolbar_button>
  <.toolbar_button action="h1" aria_label="Heading 1">...</.toolbar_button>
</.floating_menu>

Attributes

  • id (:string) (required)
  • editor_id (:string) (required)
  • trigger (:atom) - Defaults to :empty_line. Must be one of :empty_line, or :slash.
  • class (:string) - Defaults to nil.

Slots

  • inner_block (required)

inline_edit(assigns)

Enhanced inline editable field. Extends editable/1 with input type variants, validation errors, loading state, and optional confirm/cancel buttons.

Uses the PhiaEditable hook (same as editable/1).

Example

<.inline_edit id="title-edit" value={@title} type={:text} on_submit="update_title"
              show_buttons placeholder="Enter a title...">
  <:preview>{@title}</:preview>
</.inline_edit>

Attributes

  • id (:string) (required)
  • value (:string) - Defaults to nil.
  • type (:atom) - Defaults to :text. Must be one of :text, :number, :textarea, or :select.
  • placeholder (:string) - Defaults to "Click to edit".
  • on_submit (:string) - Defaults to nil.
  • on_cancel (:string) - Defaults to nil.
  • show_buttons (:boolean) - Defaults to false.
  • loading (:boolean) - Defaults to false.
  • error (:string) - Defaults to nil.
  • required (:boolean) - Defaults to false.
  • min (:any) - Defaults to nil.
  • max (:any) - Defaults to nil.
  • options (:list) - Defaults to [].
  • class (:string) - Defaults to nil.

Slots

  • preview

inline_edit_group(assigns)

Wrapper for grouping multiple inline_edit/1 fields that share a bulk save/cancel flow.

Pass custom action buttons via the :actions slot, or use on_save_all/on_cancel_all to auto-render default Save all / Cancel buttons.

Example

<.inline_edit_group id="profile-edit" on_save_all="save_profile" on_cancel_all="reset_profile">
  <.inline_edit id="name-edit" value={@name} on_submit="update_name" />
  <.inline_edit id="email-edit" value={@email} on_submit="update_email" />
</.inline_edit_group>

Attributes

  • id (:string) (required)
  • on_save_all (:string) - Defaults to nil.
  • on_cancel_all (:string) - Defaults to nil.
  • class (:string) - Defaults to nil.

Slots

  • inner_block (required)
  • actions

markdown_editor(assigns)

Renders a split-pane Markdown editor: textarea (write) + server-rendered preview.

The PhiaMarkdownEditor hook debounces textarea input events (300 ms) and calls pushEvent(on_change, {raw, length, words}). The LiveView re-renders with the updated preview_html after server-side Markdown parsing.

Example

<.markdown_editor id="body-editor" value={@draft} preview_html={@preview}
                  on_change="md_changed" label="Article body" />

Attributes

  • id (:string) (required)
  • value (:string) - Defaults to nil.
  • preview_html (:string) - Defaults to nil.
  • on_change (:string) - Defaults to nil.
  • label (:string) - Defaults to nil.
  • placeholder (:string) - Defaults to "Write in Markdown...".
  • min_height (:string) - Defaults to "200px".
  • class (:string) - Defaults to nil.

Slots

  • toolbar

rich_text_viewer(assigns)

Renders a read-only Phoenix.HTML.raw container with prose sizing.

Security: Always sanitize content server-side before passing to this component. Use HtmlSanitizeEx or equivalent.

Example

<.rich_text_viewer content={@post.body} prose_size={:lg} />

Attributes

  • content (:string) (required)
  • prose_size (:atom) - Defaults to :base. Must be one of :sm, :base, or :lg.
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

slash_command_menu(assigns)

Renders a / trigger command palette for block-level actions.

The PhiaSlashCommand hook activates when the user types / at the start of an empty block. It fuzzy-filters items, supports arrow-key navigation and Enter to select.

Example

<.slash_command_menu id="slash" editor_id="my-editor" on_select="block_inserted">
  <:item value="h1" label="Heading 1" icon="H1" description="Large section heading" shortcut="/h1" />
  <:item value="bulletList" label="Bullet List" icon="•" description="Simple unordered list" />
  <:item value="codeBlock" label="Code Block" icon="<>" description="Insert code snippet" />
</.slash_command_menu>

Attributes

  • id (:string) (required)
  • editor_id (:string) (required)
  • on_select (:string) (required)
  • class (:string) - Defaults to nil.

Slots

  • item - Accepts attributes:
    • value (:string) (required)
    • label (:string) (required)
    • icon (:string)
    • description (:string)
    • shortcut (:string)

toolbar_button(assigns)

Renders a single toolbar action button.

The hook reads data-action to dispatch formatting commands and toggles aria-pressed and the is-active CSS class automatically.

Example

<.toolbar_button action="bold" aria_label="Bold" tooltip="Bold (Ctrl+B)" active={@bold_active}>
  <svg class="h-4 w-4" viewBox="0 0 16 16" fill="none">
    <path d="M5 3h4.5a2.5 2.5 0 010 5H5V3zM5 8h5a2.5 2.5 0 010 5H5V8z"
          stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  </svg>
</.toolbar_button>

Attributes

  • action (:string) (required)
  • aria_label (:string) (required)
  • active (:boolean) - Defaults to false.
  • disabled (:boolean) - Defaults to false.
  • tooltip (:string) - Defaults to nil.
  • size (:atom) - Defaults to :sm. Must be one of :xs, :sm, or :md.
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

toolbar_group(assigns)

Renders a role="group" semantic group for related toolbar buttons.

Example

<.toolbar_group label="Text formatting">
  <.toolbar_button action="bold" aria_label="Bold">...</.toolbar_button>
  <.toolbar_button action="italic" aria_label="Italic">...</.toolbar_button>
</.toolbar_group>

Attributes

  • label (:string) - Defaults to nil.
  • class (:string) - Defaults to nil.

Slots

  • inner_block (required)

toolbar_separator(assigns)

Renders a thin vertical role="separator" divider between toolbar groups.

Example

<.toolbar_group label="Marks">...</.toolbar_group>
<.toolbar_separator />
<.toolbar_group label="Blocks">...</.toolbar_group>

Attributes

  • class (:string) - Defaults to nil.