Rich text editor web component — no contentEditable, no browser selection APIs, no execCommand.
All selection and editing is handled through direct DOM manipulation, giving full control
over editing behavior with none of the compatibility and accessibility nightmares of
contentEditable.
npm install tosijs-styled-editorPeer dependencies: tosijs, tosijs-ui
<tosi-editable>
<p>Edit this text!</p>
<p>It supports <b>bold</b>, <i>italic</i>, and more.</p>
</tosi-editable>tosi-editable {
background: white;
border: 1px solid #ccc;
min-height: 200px;
}
tosi-editable [part="toolbar"] {
background: #f8f8f8;
border-bottom: 1px solid #ccc;
}import { tosiEditable, type TosiEditable } from 'tosijs-styled-editor'
import { defaultToolbar, defaultMenubar } from 'tosijs-styled-editor'
const editor = tosiEditable() as TosiEditable
editor.value = '<p>Hello world</p>'
document.body.appendChild(editor)
// Add menus (slot="menubar")
for (const menu of defaultMenubar(editor)) {
editor.appendChild(menu)
}
// Add toolbar buttons (slot="toolbar")
for (const widget of defaultToolbar()) {
widget.setAttribute('slot', 'toolbar')
editor.appendChild(widget)
}The editor uses three layers:
- DOM utilities (
dom-utils.ts) — leaf-node traversal; nearly all operations work with leaf nodes - Selection (
selection.ts) — custom selection via "spanification" (wrapping characters in<span>elements to determine exact positions without browser APIs) - Commands (
commands.ts) — extensible command system for formatting and editing
The caret is an <input> element, so mobile browsers show their keyboard automatically.
| Key | Action |
|---|---|
| Typing | Inserts character at caret; replaces selection if any |
| Backspace | Deletes character before caret, or deletes selection |
| Delete | Deletes character after caret, or deletes selection |
| Enter | Splits the current block at the caret |
| ArrowLeft / ArrowRight | Moves caret one character |
| ArrowUp / ArrowDown | Moves caret one visual line, maintaining horizontal position |
| Alt+ArrowLeft / Alt+ArrowRight | Moves caret one word |
| Shift+Arrow | Extends selection |
| Action | Selects |
|---|---|
| Click | Places caret at character position |
| Shift+Click | Extends selection to click position |
| Double-click | Selects word |
| Triple-click | Selects block |
| Click-drag | Selects character range |
| Key | Action |
|---|---|
| Tab | Move to next cell; at last cell, creates a new row |
| Shift+Tab | Move to previous cell |
| ArrowDown | Move to cell below; at last row, exit table downward |
| ArrowUp | Move to cell above; at first row, exit table upward |
| Shift+Enter | Move to cell below (same column); at last row, creates a new row |
| Shift+ArrowDown | Same as Shift+Enter |
| Enter | Insert line break (<br>) within the cell |
| Backspace / Delete | Delete within cell only (won't escape the cell) |
| Key | Action |
|---|---|
| Enter | Split into a new list item; if item is empty, exit the list as a <p> |
| Backspace | Delete within item; at start, merge with previous item or exit list |
| Delete | Delete within item; at end, merge with next item |
Shortcuts are defined by toolbar buttons via data-shortcut attributes. The default toolbar provides:
| Shortcut | Action |
|---|---|
| Ctrl/Cmd+B | Bold |
| Ctrl/Cmd+I | Italic |
| Ctrl/Cmd+U | Underline |
| Ctrl/Cmd+Z | Undo |
| Ctrl/Cmd+Y | Redo |
Commands are invoked via editor.doCommand(commandString). Multiple commands
can be chained with semicolons.
Applies CSS to selected characters:
setText font-weight bold
setText font-style italic
setText text-decoration underline
setText background-color rgba(255,255,64,0.5)
setText font-family Helvetica
setText font-size 18px
Use + for spaces in values: setText font-family Times+New+Roman
Changes the element type of selected blocks:
setBlockType h1
setBlockType p
setBlockType pre
setBlockType blockquote
Applies CSS to selected blocks:
setBlocks text-align center
setBlocks margin-left 40px
setBlocks line-height 2.5
insertTable 3 # 3 columns, 2 rows (1 header)
insertTable 4 5 1 # 4 columns, 5 rows, 1 header row
insertTableRow after # Insert row after current
insertTableRow before # Insert row before current
insertTableCol after # Insert column after current
insertTableCol before # Insert column before current
deleteTableRow # Delete current row
deleteTableCol # Delete current column
toggleHeaderRow # Toggle header styling on current row
Column widths can be resized by dragging cell borders.
updateUndo undo
updateUndo redo
setDebug # Toggle debug visualization
annotate note # Insert annotation at caret
Tables use CSS Grid layout instead of <table> elements:
<ul class="editor-table" style="grid-template-columns: 1fr 1fr 1fr">
<li class="table-header">Name</li>
<li class="table-header">Role</li>
<li class="table-header">Location</li>
<li>Alice</li>
<li>Engineer</li>
<li>New York</li>
</ul>Add custom commands by extending editor.commands:
editor.commands.myCommand = (ctx, ...args) => {
// ctx provides: root, selectable, find(), findAll(),
// selectedLeafNodes(), selectedBlocks(), insertionPoint(),
// block(), normalize(), focus(), updateUndo()
const nodes = ctx.selectedLeafNodes()
// ... manipulate nodes ...
ctx.updateUndo('new')
}
editor.doCommand('myCommand arg1 arg2')| Property | Type | Description |
|---|---|---|
value |
string |
Get/set the editor content as HTML |
active |
boolean |
Enable/disable editing |
pastemode |
'merge' | 'remove' | 'preserve' | 'paragraphs' |
How pasted HTML is handled |
commands |
object |
Command registry (extend to add custom commands) |
widgets |
'none' | 'minimal' | 'default' |
Attribute — built-in toolbar preset |
| Method | Description |
|---|---|
doCommand(str) |
Execute a command string |
focus() |
Focus the caret |
Apache-2.0