# Development Notes ## Spell Checking with beforeinput / input events Spell checking replacements come from: ```javascript InputEvent.inputType === 'insertReplacementText' ``` The replacement text comes from: ```javascript InputEvent.dataTransfer.items[0].readAsString(callback) ``` The ranges come from: ```javascript InputEvent.getTargetRanges() ``` There are browser differences here: * Chrome: * Older versions (even from 2024) didn't provide target ranges in this call, but newer versions (as of June 2025) do. * Chrome does not always perform an initial spell check on a `contenteditable` and may not even catch all words. Manual entry seems to refresh what it checks, but again, it may miss things. * As a test, repeat "This is a tset" over and over across lines and watch only some get highlighted. * Hey! Even in Obsidian it does this. Here's a test case as of June 24, 2025: * This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. This is a tset. * See how only some get marked sometimes? It's very inconsistent. * Chrome triggers a selection of the content before marking it. * Firefox: * Ranges seem consistent. * Firefox seems to perform an initial spell check and seems to notice every misspelled word. Ranges will be within the parent element, so it's necessary to calculate the character offset to that element: ```typescript _findOffsetsForRange( range: StaticRange, ): { endLineNum: number, endOffset: number, startLineNum: number, startOffset: number, } { let startLineNum = null; let endLineNum = null; const startParentEl = range.startContainer.parentElement; const endParentEl = range.endContainer.parentElement; const startCMLineEl = startParentEl.closest('.line'); const endCMLineEl = (range.endContainer === range.startContainer) ? startCMLineEl : endParentEl.closest('.line'); const cmLinesEl = startCMLineEl.parentElement; console.assert(cmLinesEl === endCMLineEl.parentElement); const children = cmLinesEl.children; const childrenLen = children.length; for (let i = 0; i < childrenLen; i++) { const child = children[i]; if (child === startCMLineEl) { startLineNum = i; } if (child === endCMLineEl) { endLineNum = i; break; } } const startOffset = this._findCharOffsetForNode(startParentEl); const endOffset = (startParentEl === endParentEl) ? startOffset : this._findCharOffsetForNode(endParentEl); console.log('Found lines'); return { endLineNum: endLineNum, endOffset: endOffset + range.endOffset, startLineNum: startLineNum, startOffset: startOffset + range.startOffset, }; } _findCharOffsetForNode( targetEl: Element, ): number { const lineEl = targetEl.closest('.line'); let offset = 0; let found = false; function walk(node: Node) { if (found || !node) { return; } if (node === targetEl) { found = true; return; } if (node.nodeType === Node.TEXT_NODE) { offset += (node as Text).data.length; } else if (node.nodeType === Node.ELEMENT_NODE) { const children = node.childNodes; for (let i = 0; i < children.length; i++) { walk(children[i]); if (found) { return; } } } } walk(lineEl); return offset; } ``` # Blog Posts * Piotrek Koszuliński: * [ContentEditable — The Good, the Bad and the Ugly](https://medium.com/content-uneditable/contenteditable-the-good-the-bad-and-the-ugly-261a38555e9c) — August 13, 2015 * [Fixing ContentEditable. Working with a system which is driving…](https://medium.com/content-uneditable/fixing-contenteditable-1a9a5073c35d) — August 21, 2015