feat: simplify ContentEditable handling

This commit is contained in:
Simon
2026-03-10 19:40:49 +08:00
parent ddcfa5b499
commit 7f9f0d1589

View File

@@ -114,62 +114,40 @@ export async function inputTextElement(element: HTMLElement, text: string) {
await clickElement(element) await clickElement(element)
if (isContentEditable) { if (isContentEditable) {
// For contenteditable elements (like LinkedIn editor, rich text editors), // (keydown) -> beforeinput -> mutation -> input -> (keyup) -> change
// we need to dispatch proper events to trigger framework listeners.
// Many frameworks (React, Vue, etc.) listen to specific events.
const editableElement = element as HTMLElement & { innerText: string }
try { // Dispatch beforeinput + mutation + input for clearing
// Focus the element first if (
editableElement.focus() element.dispatchEvent(
new InputEvent('beforeinput', {
// Dispatch keydown first (typical event order: keydown -> beforeinput -> mutation -> input -> keyup)
// Only for single-character input to maintain semantic consistency
if (text.length === 1) {
const keydownEvent = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: text,
})
editableElement.dispatchEvent(keydownEvent)
}
// Dispatch beforeinput for clearing (deleteContent)
const deleteEvent = new InputEvent('beforeinput', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
inputType: 'deleteContent', inputType: 'deleteContent',
}) })
editableElement.dispatchEvent(deleteEvent) )
) {
// Clear existing content (first mutation) element.innerText = ''
editableElement.innerText = '' element.dispatchEvent(
// Dispatch input event for the deletion
editableElement.dispatchEvent(
new InputEvent('input', { new InputEvent('input', {
bubbles: true, bubbles: true,
inputType: 'deleteContent', inputType: 'deleteContent',
}) })
) )
}
// Dispatch beforeinput event for insertion (important for React apps) // Dispatch beforeinput + mutation + input for insertion (important for React apps)
// Check if canceled - if so, skip the mutation but continue cleanup if (
const beforeInputEvent = new InputEvent('beforeinput', { element.dispatchEvent(
new InputEvent('beforeinput', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
inputType: 'insertText', inputType: 'insertText',
data: text, data: text,
}) })
const notCanceled = editableElement.dispatchEvent(beforeInputEvent) )
const shouldInsert = notCanceled && !beforeInputEvent.defaultPrevented ) {
element.innerText = text
// Set the text content (DOM mutation) - only if not canceled element.dispatchEvent(
if (shouldInsert) {
editableElement.innerText = text
// Dispatch input event for the insertion
editableElement.dispatchEvent(
new InputEvent('input', { new InputEvent('input', {
bubbles: true, bubbles: true,
inputType: 'insertText', inputType: 'insertText',
@@ -178,27 +156,18 @@ export async function inputTextElement(element: HTMLElement, text: string) {
) )
} }
// Dispatch keyup after input (completing the typical event sequence)
if (text.length === 1) {
const keyupEvent = new KeyboardEvent('keyup', {
bubbles: true,
cancelable: true,
key: text,
})
editableElement.dispatchEvent(keyupEvent)
}
// Dispatch change event (for good measure) // Dispatch change event (for good measure)
editableElement.dispatchEvent(new Event('change', { bubbles: true })) element.dispatchEvent(new Event('change', { bubbles: true }))
// Trigger blur for validation, then refocus // Trigger blur for validation, then refocus
// blur() dispatches its own focusout event, so we don't need a duplicate // blur() dispatches its own focusout event, so we don't need a duplicate
editableElement.blur() element.blur()
editableElement.focus()
} finally { // // Plan B: execCommand — triggers trusted native events for rich text editors
// Ensure cleanup always runs, even if early return above // element.focus()
// This is handled by the common cleanup below // document.execCommand('selectAll')
} // document.execCommand('delete')
// document.execCommand('insertText', false, text)
} else if (element instanceof HTMLTextAreaElement) { } else if (element instanceof HTMLTextAreaElement) {
nativeTextAreaValueSetter.call(element, text) nativeTextAreaValueSetter.call(element, text)
} else { } else {