Direct methods for text formatting - no exec() needed
Inline Formatting
Toggle
bold()
Toggle bold on selection
italic()
Toggle italic on selection
underline()
Toggle underline on selection
strike()
Toggle strikethrough
code()
Toggle inline code
subscript()
Toggle subscript
superscript()
Toggle superscript
clearFormat()
Remove all formatting
Block Formatting
heading(level: 1 | 2 | 3 | 4 | 5 | 6)
Set heading level
paragraph()
Remove block formatting
blockquote()
Toggle blockquote
codeBlock()
Insert code block
orderedList()
Toggle numbered list
unorderedList()
Toggle bullet list
Alignment
alignLeft()
Align text left
alignCenter()
Center text
alignRight()
Align text right
alignJustify()
Justify text
indent()
Increase indentation
outdent()
Decrease indentation
Links & Insert
link(url: string)
Wrap selection in link
unlink()
Remove link from selection
insertHR()
Insert horizontal rule
insertHTML(html: string)
Insert sanitized HTML at cursor
insertText(text: string)
Insert plain text at cursor
Usage Example
const editor = Scribe.init('#editor');
// Format selected text
editor.bold(); // Toggle bold
editor.italic(); // Toggle italic
editor.heading(2); // Make it an H2
editor.link('https://example.com');
// These methods work on the current selection
// No need to save/restore selection
// Apply styling to selection
editor.setFontSize(18);
editor.setFontFamily('Georgia');
editor.setColor('#ff0000');
editor.setBackgroundColor('#ffff00');
// Note: These use inline styles
// For better maintainability, consider using classes via plugins
Real-time formatting state introspection for toolbar synchronization
Key Concept
Scribe provides three levels of state introspection: FormatState for raw formatting booleans, CommandMetadata for rich command info, andformatChange events for reactive UI updates. Together they enable toolbars that always reflect the true state of the selection.
// FormatState — raw formatting state of the current selection
interface FormatState {
bold: boolean;
italic: boolean;
underline: boolean;
strike: boolean;
code: boolean;
subscript: boolean;
superscript: boolean;
link: string | null; // URL if cursor is inside a link
heading: number | null; // 1-6 or null
list: 'ordered' | 'unordered' | null;
blockquote: boolean;
align: 'left' | 'center' | 'right' | 'justify';
fontSize: string | null;
fontFamily: string | null;
color: string | null;
backgroundColor: string | null;
}
// Get current format state at any time
const state = editor.getFormatState();
console.log(state.bold); // true if selection is bold
console.log(state.link); // 'https://...' or null
console.log(state.fontSize); // '16px' or null
// Works with partial selections and nested formatting
// e.g., selecting across <strong>bold <em>and italic</em></strong>
// → { bold: true, italic: true, ... }
Real-World Examples
// Vanilla JS toolbar with full state sync
const editor = Scribe.init('#editor');
const toolbar = document.getElementById('toolbar');
// Build toolbar buttons from command metadata
const commands = editor.getAllCommandStates();
commands
.filter(cmd => ['bold','italic','underline','strike','link'].includes(cmd.name))
.forEach(cmd => {
const btn = document.createElement('button');
btn.textContent = cmd.label;
btn.title = cmd.shortcut || cmd.label;
btn.dataset.command = cmd.name;
toolbar.appendChild(btn);
});
// Click handler — use direct methods
toolbar.addEventListener('click', (e) => {
const cmd = e.target.dataset.command;
if (cmd && editor[cmd]) {
editor[cmd]();
}
});
// Sync active state reactively
editor.on('formatChange', ({ current, changed }) => {
toolbar.querySelectorAll('button').forEach(btn => {
const cmd = btn.dataset.command;
const meta = editor.getCommandState(cmd);
if (meta) {
btn.classList.toggle('active', meta.active);
btn.disabled = !meta.supported;
}
});
});
Cross-Browser Handling Strategy
Scribe uses a multi-layered approach to ensure toolbars remain in sync across all major browsers (Chrome, Firefox, Safari, Edge), overcoming known Selection API inconsistencies.
Reactive Triggers
Prioritizes formatChange events which fire immediately after any formatting command (bold, link, etc.) modifies the DOM.
Selection Guard
Subscribes to selectionchange on the document, plus mouseupand keyup fallbacks for Firefox/Safari.
Polling Fallback
Uses 100ms polling while the editor is focused to catch programmatic changes that browsers fail to report via standard events.
Troubleshooting & Edge Cases
Toolbar doesn't update
Ensure you're subscribing to formatChange, not selectionChange, for active state. selectionChange fires too frequently and may miss format updates after commands.
// ❌ Don't use selectionChange for active state
editor.on('selectionChange', (sel) => {
// This misses command-triggered changes
});
// ✅ Use formatChange for toolbar state
editor.on('formatChange', ({ current }) => {
updateToolbar(current);
});
Partial selection detection
When selecting across multiple format nodes (e.g., bold + non-bold text), Scribe reports the format of the common ancestor container.
// Selection: "bold and normal"
// in: <strong>bold</strong> and normal
// → formats.bold = false (common ancestor is parent)
// Selection: "bold text"
// in: <strong>bold text</strong>
// → formats.bold = true
Iframe focus issues
Clicking toolbar buttons in the parent steals focus from the iframe. Always use preventDefault on mousedown.
Use formatChange's changed array to only update buttons whose state actually changed.
editor.on('formatChange', ({ changed, current }) => {
// Only update what changed
for (const key of changed) {
const btn = toolbar.querySelector(`[data-format="${key}"]`);
if (btn) btn.classList.toggle('active', !!current[key]);
}
});