Phenomenon
In Desktop Safari with Korean IME, insertFromComposition fires with targetRanges that may be collapsed but accurately represent the insertion point. Recalculating ranges based on current selection causes incorrect positioning. The targetRanges should be trusted as-is, even when collapsed.
Reproduction example
- Focus a
contenteditableelement in Desktop Safari (macOS). - Activate Korean IME.
- Start composing Korean text (e.g., type “ㅎ” then “ㅏ” then “ㄴ” to compose “한”).
- Continue typing to update composition (e.g., type “ㄱ” then “ㅡ” then “ㄹ” to update to “한글”).
- Observe
beforeinputevents withinputType: 'insertFromComposition'. - Check
getTargetRanges()- they may be collapsed but represent the correct insertion point. - If handlers recalculate ranges based on current selection, text will be inserted at wrong position.
Observed behavior
When updating Korean composition text:
-
beforeinput event:
inputType: 'insertFromComposition'isComposing: truedata: '한글'(the new composition text)getTargetRanges()returns ranges that may be collapsed (startOffset === endOffset)- The collapsed ranges accurately represent where the composition text should be inserted
-
If ranges are recalculated:
- Handlers that recalculate based on
window.getSelection()get incorrect position - Text may be inserted at wrong location (e.g., duplicated or misplaced)
- Composition text may appear in unexpected positions
- Handlers that recalculate based on
-
If ranges are trusted as-is:
- Text is inserted at correct position
- Composition updates work correctly
- No positioning issues occur
Expected behavior
targetRangesfrominsertFromCompositionshould be trusted as-is, even when collapsed- Recalculating ranges based on current selection should NOT be necessary
- The collapsed
targetRangesaccurately represent the insertion point - Handlers should use
targetRangesdirectly without modification
Impact
- Incorrect positioning: Handlers that recalculate collapsed
targetRangeswill insert text at wrong positions - Duplicated text: Text may be inserted multiple times or in wrong locations
- Composition breakage: Composition updates may fail or produce incorrect results
- Platform-specific bugs: Code that works on other browsers may fail on Desktop Safari
Browser Comparison
- Desktop Safari:
insertFromCompositionfires with accurate collapsedtargetRanges(trust as-is) - iOS Safari (Japanese/Kanji):
insertFromCompositionfires with collapsedtargetRangesthat need recalculation - iOS Safari (Korean):
insertFromCompositiondoes NOT fire - Chrome/Edge: Generally uses
insertCompositionTextinstead ofinsertFromComposition - Firefox: Behavior varies but generally more consistent with Chrome
Notes and possible direction for workarounds
-
Trust targetRanges as-is: Do not recalculate collapsed
targetRangesin Desktop Safari:element.addEventListener('beforeinput', (e) => { if (e.inputType === 'insertFromComposition') { const targetRanges = e.getTargetRanges?.() || []; if (targetRanges.length > 0) { // Trust targetRanges even if collapsed // Do NOT recalculate based on current selection const range = targetRanges[0]; // Use range.startContainer and range.startOffset directly handleCompositionInsertion(range); } } }); -
Platform detection: Detect Desktop Safari vs iOS Safari to apply correct strategy:
const isDesktopSafari = /Macintosh/.test(navigator.userAgent) && /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); if (isDesktopSafari && e.inputType === 'insertFromComposition') { // Trust targetRanges as-is } -
Avoid selection-based recalculation: Do not use
window.getSelection()to recalculate ranges in Desktop Safari