Overview
Debugging contenteditable editors is challenging due to browser differences, event timing, and DOM complexity. This guide covers effective debugging techniques.
Debugging Tools
Essential tools for debugging contenteditable editors.
Browser DevTools
Use browser DevTools effectively:
// Chrome DevTools: Emulate focused page
// Prevents blur events when inspecting elements
// Rendering panel → "Emulate a focused page"
// Break on DOM mutations
// Elements panel → Right-click → Break on → Subtree modifications
// Monitor events
monitorEvents(element, ['input', 'beforeinput', 'compositionstart']);
// Get event listeners
getEventListeners(element);Tip: Use "Emulate a focused page" in Chrome DevTools to prevent elements from disappearing when you click in DevTools.
Event Logging
class EventLogger {
#logs: EventLog[] = [];
log(event: Event) {
this.#logs.push({
type: event.type,
timestamp: Date.now(),
target: event.target,
data: this.#extractEventData(event)
});
}
#extractEventData(event: Event) {
if (event instanceof InputEvent) {
return {
inputType: event.inputType,
data: event.data,
isComposing: event.isComposing
};
}
if (event instanceof CompositionEvent) {
return {
data: event.data
};
}
return {};
}
getLogs() {
return this.#logs;
}
clear() {
this.#logs = [];
}
}
// Usage
const logger = new EventLogger();
editor.element.addEventListener('*', (e) => logger.log(e));Common Issues
Common debugging scenarios and solutions.
Selection Issues
// Debug selection problems
function debugSelection() {
const selection = window.getSelection();
console.log('Selection:', {
rangeCount: selection.rangeCount,
isCollapsed: selection.isCollapsed,
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset
});
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
console.log('Range:', {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset,
collapsed: range.collapsed
});
}
}
// Monitor selection changes
document.addEventListener('selectionchange', () => {
debugSelection();
});DOM Sync Issues
// Compare model and DOM
function compareModelAndDOM(editor) {
const modelContent = editor.getModelContent();
const domContent = editor.element.textContent;
if (modelContent !== domContent) {
console.error('Mismatch:', {
model: modelContent,
dom: domContent
});
}
}
// Monitor DOM mutations
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log('DOM mutation:', {
type: mutation.type,
target: mutation.target,
addedNodes: Array.from(mutation.addedNodes),
removedNodes: Array.from(mutation.removedNodes)
});
});
});
observer.observe(editor.element, {
childList: true,
subtree: true,
characterData: true
});IME Issues
// Debug IME composition
function debugIME() {
const element = editor.element;
element.addEventListener('compositionstart', (e) => {
console.log('Composition start:', e.data);
});
element.addEventListener('compositionupdate', (e) => {
console.log('Composition update:', e.data);
});
element.addEventListener('compositionend', (e) => {
console.log('Composition end:', e.data);
});
element.addEventListener('beforeinput', (e) => {
console.log('Before input:', {
inputType: e.inputType,
data: e.data,
isComposing: e.isComposing
});
});
}
// Check if composition is active
function isComposing() {
return document.activeElement === editor.element &&
(editor.element as any).isComposing;
}Debugging Techniques
Effective debugging techniques.
Breakpoints
// Conditional breakpoints
editor.on('operation', (operation) => {
if (operation.type === 'insert' && operation.data === 'problem') {
debugger; // Break here
}
});
// Break on specific conditions
function breakOnCondition(condition: () => boolean) {
if (condition()) {
debugger;
}
}
// Break on selection change
document.addEventListener('selectionchange', () => {
const selection = window.getSelection();
if (selection?.rangeCount === 0) {
debugger; // Break when selection is lost
}
});State Inspection
// Inspect editor state
function inspectEditor(editor) {
return {
model: editor.getModel(),
selection: editor.getSelection(),
history: editor.getHistory(),
isComposing: editor.isComposing,
dom: {
content: editor.element.innerHTML,
textContent: editor.element.textContent
}
};
}
// Make editor state available in console
(window as any).editor = editor;
(window as any).inspectEditor = () => console.log(inspectEditor(editor));Performance Debugging
Debug performance issues.
// Measure operation performance
function measurePerformance(fn: () => void) {
const start = performance.now();
fn();
const end = performance.now();
console.log('Operation took', end - start, 'ms');
}
// Profile rendering
function profileRender(editor: Editor) {
console.profile('render');
editor.render();
console.profileEnd('render');
}
// Monitor memory usage
function checkMemory() {
if ('memory' in performance) {
console.log('Memory:', (performance as any).memory);
}
}Best Practices
- Use event logging to track event sequences
- Compare model and DOM state when debugging sync issues
- Use conditional breakpoints to focus on specific scenarios
- Monitor selection changes to debug selection issues
- Test in multiple browsers to identify browser-specific issues
- Use performance profiling to identify bottlenecks
- Keep debugging code separate from production code