Rich text
<xo-richtext> is a contenteditable area with an accessible toolbar, and its value is sanitised HTML. It covers what most admin forms need and hands the rest (media libraries, embeds, custom blocks) to your app.
import '@xoong/richtext';<link rel="stylesheet" href="/@xoong/richtext/richtext.css">
<xo-richtext name="body" label="Body" placeholder="Write something" media></xo-richtext>Try it
Drag the handle in the corner of the box to change its width: under 580px the editor switches to the compact toolbar and the rest of the tools move into a More menu. The toolbar is chosen by the editor's own width, not the screen's, so an editor in a narrow side panel gets the compact one on a wide screen. Click the image tool to see the media hook, and paste something from a web page: only what the editor allows is kept.
Value
<div class="resizable">
<xo-richtext id="demo-editor" name="body" label="Body" placeholder="Write something" media></xo-richtext>
</div>
<p class="toolbar"><button id="expand" type="button">Expand</button><button id="reset" type="button">Reset</button></p>
<h3>Value</h3>
<pre class="value" tabindex="0"><code id="rt-out"></code></pre>// The rich text page: a start value, the media hook and a live view of the sanitised value.
const editor = document.querySelector('#demo-editor');
const out = document.querySelector('#rt-out');
const START = '<h2>Release notes</h2><p>Write <strong>bold</strong>, <em>italic</em> and <u>underlined</u> text, add a <a href="https://example.org">link</a> or a list:</p><ul><li>one</li><li>two</li></ul>';
const show = () => { out.textContent = editor.value; };
customElements.whenDefined('xo-richtext').then(() => {
editor.value = START;
show();
});
editor.addEventListener('input', show);
// The image tool asks the app: open your own library, then call insert() now or later.
editor.addEventListener('xo-pick-media', (event) => {
event.preventDefault();
const src = new URL('../img/landscape.svg', import.meta.url).pathname;
event.detail.insert({ src, alt: 'A landscape' });
});
document.querySelector('#expand').addEventListener('click', () => editor.expand());
document.querySelector('#reset').addEventListener('click', () => { editor.value = START; show(); });What the value may contain
sanitizeHtml() runs on every input, paste, drop and value set. Everything else is unwrapped (its text stays); scripts, styles, frames and the like are removed with their content, and no attribute outside this table survives (no style, class, on*).
| Allowed | Notes |
|---|---|
p h2 h3 blockquote ul ol li br | h1 becomes h2, h4 to h6 become h3, div becomes p |
strong em u | b and i are mapped |
a with href | Only http, https, mailto, tel; rel="noopener noreferrer" is added |
img with src, alt | src must be http(s) or a relative URL; no data:, no javascript: |
Media
The image tool fires a cancelable xo-pick-media event. Open your own library, then call detail.insert({ src, alt }) now or later. With the caret in a heading the image goes in a paragraph after it, and nothing is written inline, so it works under a strict CSP.
Keys and sizes
- Toolbar: one tab stop; Left, Right, Home and End move inside it. Ctrl or Cmd with B, I, U and K work in the editor.
- Sizes:
toolbar="full"or"compact"forces one; the default follows the editor's width. - Expand:
el.expand()moves the same editor, with its caret and undo history, into a dialog; Done and Escape close it. The host places its own Expand button. - Props:
name,label,placeholder,required,readonly,disabled,maxlength,tools="bold italic link".