Features
Features
A complete overview of everything Scribe Editor supports out of the box — from basic formatting to advanced features like inline widgets, variables, and iframe editing.
Feature Matrix
Complete list of supported formatting features
Inline
Bold
.bold()Italic
.italic()Underline
.underline()Strikethrough
.strike()Inline Code
.code()Subscript
.subscript()Superscript
.superscript()Block
Headings (H1-H6)
.heading(1-6)Paragraph
.paragraph()Blockquote
.blockquote()Code Block
.codeBlock()Horizontal Rule
.insertHR()Lists
Ordered List
.orderedList()Unordered List
.unorderedList()Nested Lists
Tab/Shift+TabAlignment
Left
.alignLeft()Center
.alignCenter()Right
.alignRight()Justify
.alignJustify()Indent
.indent()Outdent
.outdent()Links & Media
Links
.link(url)Unlink
.unlink()Images
.insertHTML()Embeds
PluginLinks
Creating and managing hyperlinks
// Link handling
const editor = Scribe.init('#editor');
// Add link to selected text
editor.link('https://example.com');
// Remove link from selection
editor.unlink();
// Check if selection is a link
editor.on('selectionChange', (selection) => {
if (selection?.formats.link) {
console.log('Current link:', selection.formats.link);
}
});Images & Embeds
Inserting and handling media content
Images are inserted via
insertHTML(). For file uploads, handle the upload separately and insert the resulting URL.// Image insertion
const editor = Scribe.init('#editor');
// Insert image HTML
editor.insertHTML('<img src="image.jpg" alt="Description" />');
// With full attributes
editor.insertHTML(`
<figure>
<img src="photo.jpg" alt="A beautiful sunset" width="800" height="600" />
<figcaption>Photo by John Doe</figcaption>
</figure>
`);
// Handle image upload
async function handleImageUpload(file: File) {
const formData = new FormData();
formData.append('image', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const { url } = await response.json();
editor.insertHTML(`<img src="${url}" alt="" />`);
}
// Drag and drop
const content = editor.getContentElement();
content.addEventListener('drop', async (e) => {
e.preventDefault();
const files = Array.from(e.dataTransfer?.files || []);
for (const file of files) {
if (file.type.startsWith('image/')) {
await handleImageUpload(file);
}
}
});Variables / Tokens
Insertable placeholders for mail merge and templates
// Variable/Token insertion (mail merge, templates)
const editor = Scribe.init('#editor');
// Insert a non-editable variable token
function insertVariable(name: string, value: string) {
const token = `<span
class="variable-token"
contenteditable="false"
data-variable="${name}"
>${value}</span>`;
editor.insertHTML(token);
editor.focus();
}
// Usage
insertVariable('first_name', '{{first_name}}');
insertVariable('company', '{{company}}');
// Style in CSS
// .variable-token {
// background: #e3f2fd;
// border-radius: 3px;
// padding: 0 4px;
// color: #1565c0;
// cursor: default;
// }
// Extract variables from content
function extractVariables(html: string): string[] {
const matches = html.match(/data-variable="([^"]+)"/g) || [];
return matches.map(m => m.replace(/data-variable="|"/g, ''));
}Inline Widgets
Custom non-editable elements (mentions, hashtags, etc.)
Widget Structure
<span class="widget widget-mention"
contenteditable="false"
data-type="mention"
data-payload='{"id":"123","name":"John"}'>
<span class="mention">@John</span>
</span>// Inline widgets (mentions, hashtags, custom blocks)
class InlineWidget {
constructor(private editor: EditorInstance) {}
insert(type: string, data: Record<string, unknown>) {
const widget = document.createElement('span');
widget.className = `widget widget-${type}`;
widget.contentEditable = 'false';
widget.dataset.type = type;
widget.dataset.payload = JSON.stringify(data);
// Render based on type
switch (type) {
case 'mention':
widget.innerHTML = `<span class="mention">@${data.name}</span>`;
break;
case 'hashtag':
widget.innerHTML = `<span class="hashtag">#${data.tag}</span>`;
break;
case 'emoji':
widget.innerHTML = data.emoji as string;
break;
}
this.editor.insertHTML(widget.outerHTML);
}
// Parse widgets from HTML
static parse(html: string): Array<{ type: string; data: unknown }> {
const doc = new DOMParser().parseFromString(html, 'text/html');
const widgets = doc.querySelectorAll('.widget');
return Array.from(widgets).map(w => ({
type: w.dataset.type!,
data: JSON.parse(w.dataset.payload || '{}')
}));
}
}
// Usage
const widgets = new InlineWidget(editor);
widgets.insert('mention', { id: '123', name: 'John' });
widgets.insert('hashtag', { tag: 'important' });Toolbar Positioning
Floating toolbar placement logic
Positioning Algorithm
┌─────────────────────────────────────────────────┐ │ Viewport │ │ │ │ ┌─ Toolbar ─┐ ← Centered on selection │ │ │ B I U L │ │ │ └───────────┘ │ │ │ │ │ ▼ 8px gap │ │ ┌─────────────────────────────────────────┐ │ │ │ Selected text in the editor content │ │ │ └─────────────────────────────────────────┘ │ │ │ │ If selection near top: │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Selected text near top of viewport │ │ │ └─────────────────────────────────────────┘ │ │ ▼ 8px gap │ │ ┌─ Toolbar ─┐ ← Flips below selection │ │ │ B I U L │ │ │ └───────────┘ │ └─────────────────────────────────────────────────┘
// Toolbar positioning logic
function positionFloatingToolbar(
toolbar: HTMLElement,
selectionRect: DOMRect
) {
const toolbarHeight = toolbar.offsetHeight;
const toolbarWidth = toolbar.offsetWidth;
const padding = 8;
// Calculate position above selection
let x = selectionRect.left + selectionRect.width / 2;
let y = selectionRect.top - toolbarHeight - padding;
// Flip below if too close to top
if (y < padding) {
y = selectionRect.bottom + padding;
}
// Keep within viewport horizontally
const maxX = window.innerWidth - toolbarWidth / 2 - padding;
const minX = toolbarWidth / 2 + padding;
x = Math.max(minX, Math.min(maxX, x));
// Apply position
toolbar.style.left = `${x}px`;
toolbar.style.top = `${y}px`;
toolbar.style.transform = 'translateX(-50%)';
}
// Usage with selection events
editor.on('selectionChange', (selection) => {
if (selection && !selection.collapsed && selection.rect) {
positionFloatingToolbar(toolbar, selection.rect);
toolbar.classList.add('visible');
} else {
toolbar.classList.remove('visible');
}
});Mobile Support
Touch devices and virtual keyboard handling
Touch Targets
Minimum 44×44px buttons for reliable touch interaction
Virtual Keyboard
Auto-scroll selection into view when keyboard opens
iOS Quirks
Delayed selection detection for reliable toolbar display
// Mobile support considerations
const editor = Scribe.init('#editor', {
// Consider touch events
});
// 1. Touch-friendly toolbar sizing
// CSS:
// .toolbar button { min-width: 44px; min-height: 44px; }
// 2. Virtual keyboard handling
const content = editor.getContentElement();
// Scroll selection into view when keyboard appears
content.addEventListener('focus', () => {
setTimeout(() => {
const selection = editor.getSelection();
if (selection?.rect) {
// Scroll into viewport center
window.scrollTo({
top: selection.rect.top - window.innerHeight / 3,
behavior: 'smooth'
});
}
}, 300); // Wait for keyboard
});
// 3. Handle iOS quirks
// iOS doesn't fire selectionchange reliably
let lastTouch: Touch | null = null;
content.addEventListener('touchend', (e) => {
lastTouch = e.changedTouches[0];
// Delayed selection check
setTimeout(() => {
const selection = editor.getSelection();
if (selection && !selection.collapsed) {
showToolbar(selection);
}
}, 10);
});
// 4. Responsive toolbar
function getToolbarMode(): 'full' | 'compact' {
return window.innerWidth < 768 ? 'compact' : 'full';
}
// Compact mode shows fewer buttons with overflow menuPaste Sanitization
Automatic cleanup of pasted content
Preserved
Bold, italic, links, lists, headings, paragraphs, semantic markup
Removed
Scripts, iframes, event handlers, Word styles, external fonts, tracking pixels
// Paste sanitization
const editor = Scribe.init('#editor', {
sanitize: {
allowedTags: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
allowedSchemes: ['https']
}
});
// Scribe automatically sanitizes pasted content
// Additional custom handling:
const content = editor.getContentElement();
content.addEventListener('paste', (e) => {
// Access raw clipboard data
const html = e.clipboardData?.getData('text/html');
const text = e.clipboardData?.getData('text/plain');
// Log what was pasted (for debugging)
console.log('Pasted HTML:', html);
console.log('Pasted text:', text);
// Note: Scribe handles sanitization automatically
// This event fires BEFORE Scribe processes the paste
});
// Word/Google Docs cleanup is automatic:
// - Removes mso-* styles (Microsoft Word)
// - Strips Google Docs wrapper classes
// - Normalizes nested lists
// - Preserves semantic formatting (bold, italic, links)
// - Removes font-family, font-size unless allowedKeyboard Shortcuts
Built-in shortcuts for power users
Formatting
BoldCtrl/Cmd + B
ItalicCtrl/Cmd + I
UnderlineCtrl/Cmd + U
LinkCtrl/Cmd + K
Clear FormatCtrl/Cmd + \
History & Navigation
UndoCtrl/Cmd + Z
RedoCtrl/Cmd + Y
Redo (Alt)Ctrl/Cmd + Shift + Z
IndentTab
OutdentShift + Tab