xoong
Menu · Rich text
DocsGuidesRich text

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*).

AllowedNotes
p h2 h3 blockquote ul ol li brh1 becomes h2, h4 to h6 become h3, div becomes p
strong em ub and i are mapped
a with hrefOnly http, https, mailto, tel; rel="noopener noreferrer" is added
img with src, altsrc 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