Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the intended/recommended approach to hijack composition events? #137

Open
Azmisov opened this issue Oct 6, 2022 · 2 comments
Open

Comments

@Azmisov
Copy link

Azmisov commented Oct 6, 2022

Neither specifications nor various online documentation are especially clear on how the events are intended to be used to take over composition editing control from the browser. Additionally, digging through the issues on github and testing various browsers, it is clear that the interpretation of the events has been in flux, especially considering the division into Level 1 and Level 2 specs. Here is what I have gathered so far:

Per my interpretation of the specs, originally the level 2 beforeinput composition events were, in order:

  • deleteByComposition: Signal what DOM range will initially be deleted in preparation for composition. Canceling this event could cancel IME composition (What should happen when "deleteByComposition" is canceled? #42) and will prevent the delete. An IME may alter the range it is composing on-the-fly, e.g. it has been stated that MacOS Japanese IME will expand the composition range to include the previous/next word. It is not clear whether additional deleteByComposition events are supposed to fire in this case or what canceling them would do.
  • insertCompositionText: Fired repeatedly during composition with the full text that will be inserted. Not cancelable, since IME's take control of the text editing area and so interfering with the text area would cause them to malfunction.
  • deleteCompositionText: Discards the text created during insertCompositionText events, to "cleanup" the DOM. Not cancelable, as its purpose is simply to revert the insertCompositionText changes.
  • insertFromComposition: Insert finalized text into the DOM. Canceling this event will cancel this final insertion.

There is no way to completely hijack the composition events, mainly because it will ruin compatibility with existing IME's. As a compromise, while you won't have control of the actual composition events, the spec lets you track what parts of the DOM were removed (deleteByComposition) and what would be added (insertFromComposition) so you can undo the initial deletion if desired, and add the final composition text yourself in some way. It has been suggested by some (#122) that these events could be used to redirect the composition to occur in a separate, possibly hidden element, and port over the changes on the fly to the original element. However, it was observed that the IME's (or maybe it is the browsers) do not like getting redirected and will cancel the composition or otherwise exhibit quirky behavior. So it seems the only viable method for dealing with composition events presently is to wait for the composition to end and then cleanup/incorporate any changes. From what I gather, more deeply interfacing with IME's and composition is not possible with current API's, and would require something new such as the proposed EditContext (which also covers other deep editing integration cases, like controlling cursor/selection).

Observed behavior for various browsers, from my tests:

  • Firefox 105: On desktop/mobile, only insertCompositionText is fired. On desktop, with some composition text, a final empty insertCompositionText seems to fire, followed by an insertText with the actual text; those seem to mimic the deleteCompositionText and insertFromComposition events. Searching the Firefox source code, I found some defines for the other events, but they don't appear to be used in the code anywhere.
  • Chromium 105: On desktop/mobile, only insertCompositionText is fired; I can find no references to the other events in the Chromium source code either.
  • Safari 15.5: On desktop, insertCompositionText, deleteCompositionText, and insertFromComposition all fire; I cannot trigger deleteByComposition however. Of note, for Japanese input I was able to trigger a scenario where only insertCompositionText fires if I change the cursor position during composition (compositionend still fires in this case).

So for composition events, it seems the only guarantee you have is that insertCompositionText will fire, even with the supposed Level 2 spec'ed implementation of Safari/Webkit. Further, it seems from the discussion in #122, that at least the Chromium team has abandoned the other three composition events, so there are no plans for their implementation in the future.

It seems the current recommended approach to emulate the missing events is to use the composition[start,update,end] events. (As a side note, the order of composition events can be different, e.g. compositionupdate occurs before beforeinput for Firefox, but after for Chromium/Safari). Here's my take on emulating the events:

  • compositionstart: Set a flag to indicate composition has begun; beforeinput's isComposing flag could be used for this, but is inconsistent among browsers (see #202). You can try to cancel the composition here by preventDefault, but there are no guarantees it will actually cancel.
  • insertCompositionText: Use the first call after composition has started to check what range (affected_range) will be modified/deleted. You can cache the DOM in this range to restore later if needed (deleted_dom). Optionally, you could just cache the entire contenteditable area to restore later. Cache the last range seen for all insertCompositionText calls... this is the range for the inserted composition text (inserted_range). Cache the data from the event as well (inserted_data).
  • (Optional: you can check for insertFromComposition event here; if it fires, then you can skip all the compositionend logic and do it there instead)
  • compositionend: Delete the inserted composition text using inserted_range. Restore deleted_dom if desired. Call your own handler handle_composition(affected_range,inserted_data).

A bit more work, but seems like it is possible to do without the events after all. As it does require a fair bit of boilerplate just to replicate the Level 2 spec, it was proposed that insertCompositionText event be modified to allow some cancelation (see #134), letting the browser handle the deletion of inserted_range. However, as I commented there, it seems like a hacky solution when one could just implement the missing Level 2 events instead. Additionally, it wouldn't handle deleted_dom or affected_range.

Is this a fair assessment of the state of the Input Events spec currently? I surmise composition events are the only real stumbling block that has been run into for implementing the spec. Is the method I outlined for emulating the events what was intended/recommended by the spec writers, or is there a better way to go about this?

@johanneswilm
Copy link
Contributor

@Azmisov AFAIK it's not possible to emulate the missing events as the composition buffer may be filled at which time part of the composition string will be committed to the DOM permanently and trying to remove it at that stage will cancel the entire composition. I have not tested it in practice though.

@Azmisov
Copy link
Author

Azmisov commented Oct 23, 2022

Okay, interesting. The idea was to only repair the DOM on compositionend, but you're saying even that would not be possible? I was also going to post an update, since I did some testing as well and the results were not great:

  • In Chrome, simply changing the cursor position during composition will change the affected range (getTargetRanges), without emitting a compositionend. The idea in my original post was to use composition end as the trigger to sanitize the DOM yourself, but this test shows that a user could just keep switching the cursor manually to indefinitely delay the compositionend event. Probably not common in practice, but perhaps it is common for some IME's. Of note perhaps is speech-to-text, which at least on Android will emit entire sentences at once. Additionally, this means composition ranges could be nested, e.g. first you compose, then cursor changes and you do a composition edit on that newly composed text; so reverting the DOM and applying the changes yourself becomes a nightmare, since some of the affected ranges may not have existed in the original DOM.
  • I tried sanitizing the DOM and repositioning the cursor after each insertCompositionText event. This works in Chrome, but in Android, there is a janky refresh in the IME as it cancels and then starts again. In Firefox, it seems to track the original DOM nodes internally, so when you sanitize (possibly removing elements), it continues doing composition on the disconnected DOM elements now.

Also related, I have made a site to test composition events: https://azmisov.github.io/Input-Events-Tester/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants