fix: add execCommand fallback for contenteditable input (#168)

When typing into contenteditable elements (e.g. LinkedIn post editor),
the synthetic event approach (Plan A) may fail silently — the events
fire but the editor's internal state doesn't update, leaving the
element empty.

This adds an automatic fallback: after Plan A, we verify the text was
actually inserted by checking element.innerText. If it wasn't, we
fall back to execCommand('insertText') which integrates natively with
most rich-text editors including LinkedIn, Quill, and Slate.js.

The fallback uses proper Selection/Range API to select-all before
replacing, and preserves the undo stack since execCommand is handled
by the browser natively.

Fixes #168
This commit is contained in:
d 🔹
2026-03-11 17:07:22 +00:00
parent 025cb3391e
commit 2f92a9cb32

View File

@@ -119,11 +119,16 @@ export async function inputTextElement(element: HTMLElement, text: string) {
// - Monaco/CodeMirror: Require direct JS instance access. No universal way to obtain.
// - Draft.js: Not responsive to synthetic/execCommand/Range/DataTransfer. Unmaintained.
//
// Strategy: Try Plan A (synthetic events) first, then verify and fall back
// to Plan B (execCommand) if the text wasn't actually inserted.
//
// Plan A: Dispatch synthetic events
// Works: LinkedIn, React contenteditable, Quill.
// Fails: Slate.js
// Works: React contenteditable, Quill, some LinkedIn versions.
// Fails: Slate.js, some contenteditable editors that ignore synthetic events.
// Sequence: beforeinput -> mutation -> input -> change -> blur
let planASucceeded = false
// Dispatch beforeinput + mutation + input for clearing
if (
element.dispatchEvent(
@@ -164,18 +169,33 @@ export async function inputTextElement(element: HTMLElement, text: string) {
)
}
// Verify Plan A worked by checking if the text was actually inserted
const currentText = element.innerText.trim()
planASucceeded = currentText === text.trim()
if (!planASucceeded) {
// Plan B: execCommand fallback (deprecated but widely supported)
// Works: LinkedIn, Quill, Slate.js, react contenteditable components.
// This approach integrates with the browser's undo stack and is handled
// natively by most rich-text editors.
element.focus()
// Select all existing content and delete it
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element)
selection?.removeAllRanges()
selection?.addRange(range)
document.execCommand('delete', false)
document.execCommand('insertText', false, text)
}
// Dispatch change event (for good measure)
element.dispatchEvent(new Event('change', { bubbles: true }))
// Trigger blur for validation
element.blur()
// Plan B: execCommand (deprecated but works better for some editors)
// Works: LinkedIn, Quill, Slate.js, react contenteditable components
//
// document.execCommand('selectAll')
// document.execCommand('delete')
// document.execCommand('insertText', false, text)
} else if (element instanceof HTMLTextAreaElement) {
nativeTextAreaValueSetter.call(element, text)
} else {