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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user