JavaScript Rich Text Editor

@lemonadejs/editor · ✓ 19 contract checks · framework-agnostic · zero dependencies

<Editor /> — a rich text editor built ON the Toolbar block (the bar is a dependency, not a fork: the editor drives it through live item mutations + api.refresh()). Contenteditable engine, semantic HTML out.

  • formatting: block styles (paragraph, headings, quote, code), bold / italic / underline / strikethrough, sub / superscript, text and highlight colors (the Color block panel, hosted by the Toolbar’s color items), alignment, ordered / unordered lists, indent / outdent, links (inline balloon), inline images (file picker, paste and drop — stored as data URLs), horizontal rule, clear formatting, fullscreen
  • images are objects: click one (or Tab to it — images are focusable, focusing selects) for drag-resize — corner handles keep the aspect ratio, edge handles stretch width or height independently — and an alignment menu: wrap left / center / wrap right / inline, full width, remove; Delete removes the selection
  • tables, CKEditor-style: a hover grid picker inserts (keyboard too: arrows size the grid, Enter inserts, Escape closes); a floating balloon over the active table carries Row / Column / Cell menus (insert above/below/left/right, header row/column, delete), cell merge (drag across cells to select, or merge right/down), split, cell background color and table delete. Column boundaries, row boundaries and the table edge grow DRAG HANDLES for resizing — widths land on the colgroup, so they survive as plain HTML.
  • own undo/redo stack (snapshots, typing coalesced) — structural table surgery is undoable, which native execCommand undo never was
  • paste is sanitized to a semantic subset (Word/Docs noise dropped, script vectors removed) — raw paste with filterpaste=“false”
  • output extensions, all local to the browser: DIRECT PDF generation (api.exportPDF — the built-in zero-dependency writer lays the content out and downloads real selectable-text PDF bytes), print through a staged print document (api.print), and Word export as an MHTML .doc download (api.exportWord)

Two-way value: bind carries the HTML (onchange fires per input); the value prop seeds an unbound editor.

Example

live
import { html } from 'lemonadejs';
import Editor from '@lemonadejs/editor';

const NOTE = [
    '<h2>Release notes - September</h2>',
    '<p>The <b>Editor</b> block sits on the Toolbar block: headings, lists, links, ',
    'images and <i>real tables</i> with resizable columns.</p>',
    '<ul><li>Undo and redo with structural table changes</li>',
    '<li>Sanitized paste from Word and Google Docs</li>',
    '<li>Print, PDF and Word export, all in the browser</li></ul>',
].join('');

const App = (props, { state }) => {
    const content = state(NOTE);
    let editor;

    return html`<div>
        <${Editor} bind="${content}" height="260px" placeholder="Write something..."
            ref="${(api) => (editor = api)}" />
        <p style="margin-top:8px">
            <button onclick="${() => editor.insertTable(3, 3)}">Insert table</button>
            <button onclick="${() => editor.undo()}">Undo</button>
            ${() => ' ' + content.value.replace(/<[^>]+>/g, ' ').trim().split(/\s+/).length + ' words'}
        </p>
    </div>`;
};

Installation

npm install @lemonadejs/editor
import Editor from '@lemonadejs/editor';
import '@lemonadejs/editor/style.css';

The icons come from Google Material Symbols. Load the font once per page:

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">

Three deployment forms, one component:

html`<${Editor} />`                       // by value (no registration)
setComponents({ Editor });               // then <Editor /> by name anywhere
createWebComponent(Editor);              // <lm-editor> in plain HTML/any framework

Props

Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.

PropTypeDefaultDescription
bindstringTwo-way bound value. .set() fires onchange; plain assignment is silent. two-way HTML content (the value prop seeds it when unbound)
valuestring''initial HTML when unbound
placeholderstring''hint shown while the editor is empty
toolbarbooleantruefalse hides the formatting bar
heightstring''CSS height of the writing area (” grows with content)
readonlybooleanfalsetrue locks editing (toolbar disabled, content selectable)
filterpastebooleantruesanitize pasted HTML to the semantic subset
acceptimagesbooleantruepaste / drop / pick images as inline data URLs

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onfocus — (e) the writing area gained focus
  • onblur — (e) the writing area lost focus

API

import { ref } from 'lemonadejs';
const editor = ref();
html`<${Editor} ref="${editor}" />`;
// editor.current.getData(...)  ·  editor.current.setData(...)  ·  editor.current.getText(...)  ·  editor.current.exec(...)  ·  editor.current.focus(...)  ·  editor.current.insertTable(...)  ·  editor.current.undo(...)  ·  editor.current.redo(...)  ·  editor.current.toggleSource(...)  ·  editor.current.print(...)  ·  editor.current.exportPDF(...)  ·  editor.current.exportWord(...)
  • getData()
  • setData()
  • getText()
  • exec()
  • focus()
  • insertTable()
  • undo()
  • redo()
  • toggleSource()
  • print()
  • exportPDF()
  • exportWord()

Styling

All classes follow the lm-editor-* convention; visual variants are data-* attributes on the root. Override freely — there is no styling engine to fight.

Contract

The machine-readable schema ships with the package:

import contract from '@lemonadejs/editor/contract.json';

verify.json carries the conformance proof produced by verify(Editor).

Looking for the v5 plugin? See the archived v5 documentation.