Text Edit
A rich text editor built from your own markup, storing markdown.
Overview
x-text-edit has two roles. Alone, it makes an element an editable area bound to a value. With a command modifier, it makes any element a control for that area. It adds no toolbar of its own.
<div x-data="{ post: '# Hello\n\nSelect a word, then press **Bold**.' }">
<button x-text-edit.strong>Bold</button>
<button x-text-edit.em>Italic</button>
<button x-text-edit.h2>Heading</button>
<button x-text-edit.ul>List</button>
<div x-text-edit="post" aria-label="Post"></div>
<pre x-text="post"></pre>
</div>
Setup
Text Edit is included in manifest.js with all core plugins, or can be selectively loaded.
<script src="https://cdn.jsdelivr.net/npm/mnfst@latest/lib/manifest.min.js"></script>
Editor styles are included in Manifest CSS or as a standalone stylesheet.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mnfst@latest/lib/manifest.min.css">
Editable Area
Point x-text-edit at a value. The element becomes editable in place. placeholder shows while it is empty. Markdown renders the value straight back.
<div x-data="{ note: 'A **markdown** note.' }">
<div x-text-edit="note" placeholder="Write a note…" aria-label="Note"></div>
<div x-markdown="note"></div>
</div>
| Modifier | Effect |
|---|---|
| (none) | Stores markdown |
.html |
Stores sanitized HTML; every command available |
.plain |
Stores text; no commands |
.minimal |
Inline marks only |
.literal |
No typed-markdown shortcuts |
.autofocus |
Focus on load |
Without a value, the element's existing markup is the initial content. Do not combine with x-html.
Commands
Commands are named for the tag they produce. A <button> runs on click and never takes the caret. A <select> or <input> runs on change and reflects the state at the caret. An argument is the next modifier or an expression: .align.center, .img="url".
<div x-data="{ doc: '<p>Select a word, then pick a command.</p>' }">
<select x-text-edit.block>
<option value="p">Paragraph</option>
<option value="h2">Heading</option>
<option value="blockquote">Quote</option>
</select>
<button x-text-edit.strong>Bold</button>
<button x-text-edit.code>Code</button>
<button x-text-edit.align.center>Center</button>
<input type="color" x-text-edit.color aria-label="Colour">
<button x-text-edit.undo>Undo</button>
<div x-text-edit.html="doc" aria-label="Document"></div>
</div>
| Group | Commands |
|---|---|
| Inline | strong b em i s del code and, in .html, u mark small sub sup kbd samp var abbr cite q ins dfn time span |
| Block | p h1–h6 blockquote pre block and, in .html, address figure figcaption dl dt dd |
| Lists, inserts | ul ol checklist indent outdent hr br img a |
Style (.html) |
align color background font size leading |
Table (.html) |
table row-before row-after row-remove column-before column-after column-remove merge split table-header table-remove |
| Other | clear undo redo |
A command markdown cannot store is disabled in markdown mode. A disabled control gets aria-disabled="true", never disabled. An inline command with nothing selected arms for the next text typed.
Links
One <input> defines, edits and clears a link. Select text and enter a URL to link it; the field shows the href at the caret; empty it to unlink.
<div x-data="{ post: 'Visit [Manifest](https://manifestx.dev) today.' }">
<input type="url" x-text-edit.a placeholder="https://" aria-label="Link">
<div x-text-edit="post" aria-label="Post"></div>
</div>
Typing Markdown
A line converts when you press Enter. Block rules apply at the start of a line; .literal turns conversion off.
<div x-data="{ draft: '' }">
<div x-text-edit="draft" placeholder="Type ## Title and press Enter" aria-label="Draft"></div>
</div>
| Type | Get |
|---|---|
# … ###### |
Heading |
- * + / 1. |
Bulleted / numbered list |
[ ] [x] |
Task list |
> |
Blockquote |
``` / --- |
Code block / rule |
**bold** *italic* ~~struck~~ `code` |
Inline marks |
[text](url)  |
Link, image |
Tables
Tables are .html only. Every operation is a command and is disabled while the caret is outside a table. Drag a cell border to resize.
<div x-data="{ doc: '<table><tr><td>Stem</td><td>Price</td></tr><tr><td>Rose</td><td>4</td></tr></table>' }">
<button x-text-edit.table.3x2>Insert 3×2</button>
<button x-text-edit.row-after>Row below</button>
<button x-text-edit.column-after>Column after</button>
<button x-text-edit.merge>Merge</button>
<button x-text-edit.table-header>Header row</button>
<button x-text-edit.table-remove>Remove</button>
<div x-text-edit.html="doc" aria-label="Document"></div>
</div>
merge joins the selected cells, or the caret's cell with the one to its right. Tab walks cell to cell and adds a row past the last one; arrows leave a cell only at its edge.
Keyboard
| Key | Action |
|---|---|
| Enter | New paragraph. In a list: next item; from an empty item, leave the list. In a code block: new line; from an empty last line, leave the block |
| Shift + Enter | Line break |
| Tab / Shift + Tab | Next / previous cell; nest / unnest a list item; indent / outdent a block (.html) |
| Backspace at line start | Join the line above; leave a list; demote a heading |
| Escape | Leave the editor |
| Cmd/Ctrl + Z / Shift + Z | Undo / redo (the editor keeps its own history) |
| Cmd/Ctrl + B / I / U | Bold / italic / underline (.html) |
| Cmd/Ctrl + K | Toggle a link: removes the one at the caret, otherwise asks for a URL (the area's x-text-edit.a field where there is one) and links the selection |
Pasted content keeps its text and marks, not the source's styling.
Scattered Controls
A control finds its area in this order: the nearest ancestor with x-text-edit-for="selector"; else the nearest ancestor holding exactly one area; else the last focused area. A shared toolbar is disabled until an area has been focused.
<div x-data="{ a: 'First **draft**', b: 'Second draft' }">
<button x-text-edit.strong>Bold (last focused)</button>
<span x-text-edit-for="#note-b">
<button x-text-edit.em>Italic in B</button>
</span>
<div x-text-edit="a" aria-label="A"></div>
<div x-text-edit="b" id="note-b" aria-label="B"></div>
</div>
Controls go quiet when the caret moves into any other editable element.
Selection Menu
The area fires text-edit:selection with { collapsed, text, x, y, width, height, top, right, bottom, left }, or null when it lets go. The same box is written as --text-edit-selection-x, -y, -width, -height and -center on the area and on :root, so a popover positions itself without script.
<div x-data="{ post: 'Select some of this text.',
bubble(d) {
const el = this.$refs.menu, open = el.matches(':popover-open'), want = !!d && !d.collapsed;
if (want && !open) el.showPopover();
if (!want && open) el.hidePopover();
} }"
@text-edit:selection="bubble($event.detail)">
<div popover="manual" x-ref="menu" style="position: fixed; margin: 0; inset: auto;
transform: translate(-50%, -125%); left: var(--text-edit-selection-center); top: var(--text-edit-selection-y)">
<button x-text-edit.strong>Bold</button>
<button x-text-edit.em>Italic</button>
</div>
<div x-text-edit="post" aria-label="Post" @focusout="bubble(null)"></div>
</div>
Use popover="manual": an auto popover closes on the outside click that selects the text. focusout covers clicks that land outside the editor.
Editing a Data Value
Bind the editor to a field of a data source row and the row stores the markup.
<template x-for="p in $x.example.products" :key="p.name">
<div x-text-edit.html.minimal="p.name" aria-label="Name"></div>
</template>
Page Styling
Add .page to font, size, leading, align, color or background to style the whole area. Page styles are CSS variables on the area, not content: read or assign them with $text.page, and text-edit:page fires with the current set.
<div x-data="{ doc: '<p>Page styles live outside the document.</p>', look: {} }">
<select x-text-edit.font.page>
<option value="">Default</option>
<option value="Georgia">Georgia</option>
<option value="ui-monospace">Mono</option>
</select>
<select x-text-edit.leading.page>
<option value="">Default</option>
<option value="2">Double</option>
</select>
<div x-text-edit.html="doc" aria-label="Document" @text-edit:page="look = $event.detail"></div>
<code x-text="JSON.stringify(look)"></code>
</div>
$text
$text is the area the element sits in, otherwise the last focused area.
| Member | Description |
|---|---|
value |
Stored value; assigning resets undo history |
page |
Page styles object; assign {} to clear |
link |
href at the caret; assign to set, empty to unlink |
selection |
The last reported selection, or null |
run(cmd, arg) active(cmd) can(cmd) |
Apply, test, or check availability of a command |
markdown() html() |
Content as markdown or sanitized HTML, whatever the mode |
focus() selectAll() |
Focus the area; select everything |
<div x-data="{ doc: '<p>Hello <strong>world</strong></p>', md: '' }">
<button @click="$text.selectAll()">Select all</button>
<button @click="doc = ''">New page</button>
<div x-text-edit.html="doc" aria-label="Document" @input="md = $text.markdown()"></div>
<pre x-text="md"></pre>
</div>
Styles
The area uses --color-surface-1, --color-line, --color-content-subtle, --color-brand-content and --radius from the theme. Set these per instance:
| Variable | Default | Sets |
|---|---|---|
--text-edit-min-height |
8rem |
Minimum height |
--text-edit-max-height |
none |
Scrolls past this |
--text-edit-padding |
0.75rem |
Content padding |
--text-edit-font -size -leading -align -color -background |
inherit | Page styles; .page controls override |
Style by attribute: [data-text-edit] (value is the mode), [data-text-edit-empty], [data-text-edit-selected], [data-text-edit-control] (value is the command), [data-text-edit-active], and [aria-disabled="true"], which the reset already dims.
Related
Article does not exist
There is no documentation at this path.