-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
The first character of a word is duplicated to the right of the caret using Samsung keyboard using Firefox Mobile on Android #14567
Comments
The issue is still reproducible, however it goes away when the predictive text option in the keyboard's settings is turned off. It's the same case as the one mentioned here: #13247 (comment) Issue not reproducible while using Gboard or SwiftKey. |
@Witoso We have received multiple reports from users that were affected by this issue. We suggested to them to disable the predictive text option and it was uniformly confirmed to resolve the issue for them. This means that this is less of an issue of Firefox but rather a problem of that specific option of the Samsung keyboard. Samsung owns a significant potion of the smartphone market and this issue appears to be exclusively / primarily an issue with CKEditor. |
@dtdesign we will be investigating this on our side in more depth (waiting for a proper hardware). |
Same issue for my users. Affected devices:
Wortverdoppelung.mp4Screen_Recording_20240116_123854_Firefox.Beta.mp4 |
Those duplicated characters bug happens even on my exotic smartphone OS: Sony Xperia 10II -- Sailfish OS 4.5 -- Browser Opera in an Android 11 Virtual Machine. (No Samsung or Samsung Keyboard involved here.) |
@Witoso So, how long until you get the hardware to pinpoint the issue and possibly find a solution? Waiting for a solution for months doesn't seem right, especially given Samsung's market share. And using a different keyboard as a workaround isn't winning users over. |
Do you have any news about a possible solution? We are also waiting desperately for a bugfix (Woltlab Customer)! |
We really appreciate all the comments, but we usually don't share details about the development stage. Typing is one of the hardest topics to debug, and as you see, even when we try to closely follow standards, browsers or keyboards break them. This won't be easy nor fast, also may not be able to provide a fix for all “exotic” or already unsupported OSes. Thank you for your patience :) |
Understandable, I appreciate the long waiting period 100%. I can also fully understand that an editor doesn't live by typing correctly, without any strange behaviors, alone, and that you'd rather devote yourself to other things. I'll light a candle of hope that we get a fix this year; 6 months ain't long enough. |
@Witoso However, we are talking about the standard Samsung keyboard here. The standard should be the least that is covered. |
This problem is a major issue and I spend half a day looking into it in order to find a pattern in the behavior. I was able to observe that the browser in some situations issues two I have documented by observations before and mostly typed them while trying out different things and sometimes there was a gap of an hour between two paragraphs because chased some wild ideas. Debugging Observed Behavior in the EditorWhen I type Next, I type The important bit is that I can backspace the second word, leaving only Comparing the Events Being Dispatched by the BrowserThere are essentially four events that can be used to determine what happens in the editor. The most notable is Monitoring
This confirms that whatever causes the character to be duplicated is the result from an extra Hypothesis: The Space Character Makes the DifferenceAt this point I had the suspicion that there must be something odd about the space being inserted, because the first character typed immediately after the space is not being underlined. As far as I understand it, the underlining is part of the composition process therefore its absence signals that no composition takes places. This causes an issue because of the assumptions made in ckeditor5/packages/ckeditor5-typing/src/input.ts Lines 63 to 84 in b1d801d
In order to prove my hypothesis I chose a slightly different text with two consecutive spaces, I will now type
Yikes! This is even worse than before, we now have two characters being duplicated. Let’s try the same using three spaces:
That’s odd, we’re back to a single duplicated letter! Do one more try, but this time using four consecutive spaces to prove that two spaces is an outlier.
If you think that this looks like a pattern then you are correct, an odd number of spaces will duplicate a single letter to the right while an even number of spaces duplicates the first two letters twice to the right. Also, for an odd number of spaces the first character will also appear on the left so that you only end up with an extra letter. This is different to the even number of spaces that cause the first two characters to never appear at the start. Cross-checking the HypothesisThere is definitely a pattern here but I want to know if this issue is caused by a preceding space in general or just trailing spaces. The obvious first thing to validate is to enter a single space first and then type a word (spoiler: It works just fine). Next was trying two spaces to see if that makes any difference and … yes, it does.
I repeated this with three leading spaces and we’re back to the double duplication bug. Four leading spaces and it results in the single character being duplicated, five leading spaces and we get the double duplication bug again. Six leading spaces … you get the idea. On the bright side, the behavior is identical to the previous observed issue but the number of spaces is consistently increased by one. My immediate thought was that this was caused by The Aforementioned Workaround for Android in DetailFor the whole time I was running a modified version of I chose
So we have three passes for – what should be – a single keystroke. To further investigate this I repeated the test with three leading spaces which results in the double duplication bug. Unfortunately, the value of
It continues from here on, I can type as many characters as I want, Final ThoughtsDuring my tests I used standalone test pages with I have a strong suspicion that the spaces play a crucial role here, the pattern is too obvious at this point. My wild guess is that the selection is being updated to a state that throws off the browser and causes its heuristics to misinterpret the caret position. I did check Maybe checking the state of the selection while |
I had a few other ideas that I tried out to further pin point the issue. Since the actual DOM presented in the editor is managed by CKEditor, I cross-checked the HTML with a plain Another idea was to check if blurring and focusing the editor again makes any difference, for example, typing The Duplication Takes Place after the First KeystrokeEverything points to an issue with the composition process and I was finally able to prove that in a slightly different way by typing the first character of the second word and then blurring the editor by moving the focus elsewhere. I’m using
This is remarkable because there was only one keystroke to produce the initial There is also this weird double duplication bug that kicks in when there are a multiple of two consecutive spaces. I tested the behavior for this case too and this is the result:
The double duplication bug immediately occurs but blurring the editor does not cause any further changes. This is to be expected because this is exactly what happened before. But this means that the initial duplication bug is the result of the end of a composition whereas the double duplication bug hits immediately. This could indicate that these two bugs are similar, yet may have slightly different causes. Probing the Composition EventsFrom now on I will only focus the first case, the single letter duplication bug. Everything points to the root cause being the composition event so I set up a tracking for these events in parallel to the existing logging. The output below starts with the editor being in the following state:
Once again I set up a small demo to cross-check my findings about the behavior of the composition events. The behavior is identical, the The This reinforces my previous impression that this is entirely an issue with the selection handling because that would reasonably explain this behavior. Verifying the Event OrderI did another check to see if the appearance and order of
There is no difference between both cases so the only variable left is where CKEditor decides to place the typed in data. Considering the way the composition is being processed, the correct selection handling is crucial for the computation of the changed bits. It still bugs me that the composition is visually terminated after the first keystroke of the second word because it means that something must be going on that I am note aware of yet. A malformed or in other way incorrectly modified selection could possibly explain them both. Well over an hour later my cup has ran out of coffee. Honestly, I didn’t expect the rabbit hole to be that deep … |
I’m quite confident that it indeed is an issue with the selection. Surprisingly the debugger breakpoints have been proven to be super reliable which I really don’t take for granted when dealing with
That range later matches the view selection that it passed to the The change to the DOM selection only takes place when I stop editing by moving the caret anywhere else and then place it at the end of a word. The first keystroke will briefly trigger a selection of the characters in front of the caret up until the left boundary. This is easy to stop with a debugger, but can also be observed by the “Select All” popup becoming visible for a short moment. It’s a bit annoying that the DOM selection constantly reports a collapsed selection but the behavior of the composition events suggest that there is some kind of hidden selection of the current word. In case CKEditor in some way is able to alter that selection and use it as its source of truth it would explain why both the browser and CKEditor produce the incorrect results. At this point I’m confident that the internal selection stored in CKEditor is actually correct but the source of truth is not. That same source of truth is used by the browser to indicate the composition by underlining the word but even the browser (visually) reports that no composition in that regard is taking place. |
I’ve found an interesting bit about the behavior of the spaces that has to do with how many spaces are in front of the caret and if the caret is at the boundary of the text node. (I had to use
Workaround for the Single Duplication BugOne common pattern I’ve noticed is that when you place the caret at the end of a word and start to type, the composition will pick up the entire word as confirmed by the data exposed in I build a naive workaround that fixes the single duplication bug by adjusting diff --git a/packages/ckeditor5-typing/src/input.ts b/packages/ckeditor5-typing/src/input.ts
index 7716904fa0..41eadfcc0b 100644
--- a/packages/ckeditor5-typing/src/input.ts
+++ b/packages/ckeditor5-typing/src/input.ts
@@ -63,10 +63,29 @@ export default class Input extends Plugin {
// Typing in English on Android is firing composition events for the whole typed word.
// We need to check the target range text to only apply the difference.
if ( env.isAndroid ) {
- const selectedText = Array.from( modelRanges[ 0 ].getItems() ).reduce( ( rangeText, node ) => {
+ let selectedText = Array.from( modelRanges[ 0 ].getItems() ).reduce( ( rangeText, node ) => {
return rangeText + ( node.is( '$textProxy' ) ? node.data : '' );
}, '' );
+ if ( selectedText === '' && viewSelection.isCollapsed && viewSelection.rangeCount === 1 ) {
+ const existingRange = viewSelection.getFirstRange()!;
+ if ( !existingRange.start.isAtStart ) {
+ const newPosition = view.createRange(
+ existingRange.start.getShiftedBy( -1 ),
+ existingRange.end
+ );
+
+ const newModelRange = editor.editing.mapper.toModelRange( newPosition );
+ selectedText = Array.from( newModelRange.getItems() ).reduce( ( rangeText, node ) => {
+ return rangeText + ( node.is( '$textProxy' ) ? node.data : '' );
+ }, '' );
+
+ if ( selectedText === ' ' || selectedText !== insertText ) {
+ selectedText = '';
+ }
+ }
+ }
+
if ( selectedText ) {
if ( selectedText.length <= insertText.length ) {
if ( insertText.startsWith( selectedText ) ) { Maybe this is the solution after all? In that case, please feel free to steal it, the diff above is hereby released into the public domain! 🙂 |
Unfortunately, some more test cases showed that while the above workaround worked well for words, it broke the ability to type in two consecutive dots or spaces, or generally anything that is not a word character. While I am a bit disappointed that my attempt eventually did not hold up, I’m still surprised at the effectiveness of such a small change. The primary weakness of my approach was that it was too simple. It simply shifted the selection by one character to the left and called it a day, which does not work if there is a non-word character. I’ve since iterated on my approach by tapping into It works remarkably well, supporting multiple spaces and dots, properly handling compositions. There is only one caveat: For some reason it casually throws a For now I simply ignored it because the editor gracefully recovers and just continues. The error is reproducible by typing
Maybe someone with more insight into the internals of CKEditor can figure out what causes this off-by-one error. There is a limit to how far I can dig into the code within a few days! ;-) (Mostly) Working Workaround / PoC ImplementationThe code is a bit messy, for example, the two extra event listeners at the top aren’t ideal and certainly do not follow your general code style for persisting data between events. You might come across the regex check for the word character which is required because I’m only targeting the specific issue that occurs when the composition of a word previously failed. Since it does not fix the original error but merely works around it, I wanted to keep my change as scoped as possible. Hard to believe it still took me 2-3 hours of testing and debugging to get here. It’s really annoying that Firefox does not allow me to edit the source on the go like Chrome does, having to wait 5 seconds for the debug build really adds up. diff --git a/packages/ckeditor5-typing/src/input.ts b/packages/ckeditor5-typing/src/input.ts
index 7716904fa0..c2aa76489e 100644
--- a/packages/ckeditor5-typing/src/input.ts
+++ b/packages/ckeditor5-typing/src/input.ts
@@ -13,7 +13,7 @@ import { env } from '@ckeditor/ckeditor5-utils';
import InsertTextCommand from './inserttextcommand.js';
import InsertTextObserver, { type ViewDocumentInsertTextEvent } from './inserttextobserver.js';
-import type { Model } from '@ckeditor/ckeditor5-engine';
+import type { ViewDocumentCompositionStartEvent, Model, ViewDocumentCompositionUpdateEvent } from '@ckeditor/ckeditor5-engine';
/**
* Handles text input coming from the keyboard or other input methods.
@@ -44,6 +44,18 @@ export default class Input extends Plugin {
editor.commands.add( 'insertText', insertTextCommand );
editor.commands.add( 'input', insertTextCommand );
+ let compositionStartData: string | undefined = undefined;
+ if ( env.isAndroid ) {
+ this.listenTo<ViewDocumentCompositionStartEvent>( view.document, 'compositionstart', ( evt, data ) => {
+ compositionStartData = data.domEvent.data;
+ } );
+ this.listenTo<ViewDocumentCompositionUpdateEvent>( view.document, 'compositionupdate', ( evt, data ) => {
+ if ( compositionStartData === '' ) {
+ compositionStartData = data.domEvent.data;
+ }
+ } );
+ }
+
this.listenTo<ViewDocumentInsertTextEvent>( view.document, 'insertText', ( evt, data ) => {
// Rendering is disabled while composing so prevent events that will be rendered by the engine
// and should not be applied by the browser.
@@ -63,10 +75,37 @@ export default class Input extends Plugin {
// Typing in English on Android is firing composition events for the whole typed word.
// We need to check the target range text to only apply the difference.
if ( env.isAndroid ) {
- const selectedText = Array.from( modelRanges[ 0 ].getItems() ).reduce( ( rangeText, node ) => {
+ let selectedText = Array.from( modelRanges[ 0 ].getItems() ).reduce( ( rangeText, node ) => {
return rangeText + ( node.is( '$textProxy' ) ? node.data : '' );
}, '' );
+ if ( typeof compositionStartData === 'string' && compositionStartData !== '' ) {
+ if (
+ selectedText === '' &&
+ viewSelection.isCollapsed &&
+ viewSelection.rangeCount === 1
+ ) {
+ const existingRange = viewSelection.getFirstRange()!;
+ if ( !existingRange.start.isAtStart ) {
+ const newPosition = view.createRange(
+ existingRange.start.getShiftedBy( compositionStartData.length * -1 ),
+ existingRange.end
+ );
+
+ const newModelRange = editor.editing.mapper.toModelRange( newPosition );
+ selectedText = Array.from( newModelRange.getItems() ).reduce( ( rangeText, node ) => {
+ return rangeText + ( node.is( '$textProxy' ) ? node.data : '' );
+ }, '' );
+
+ if ( /^\w+$/.test( selectedText ) && selectedText === insertText ) {
+ compositionStartData = undefined;
+ } else {
+ selectedText = '';
+ }
+ }
+ }
+ }
+
if ( selectedText ) {
if ( selectedText.length <= insertText.length ) {
if ( insertText.startsWith( selectedText ) ) { I hereby release this code into the public domain. |
I did some further digging to investigate the “double duplication bug” which takes place after entering two consecutive spaces. After diving deeper into the batch processing and the selection handling I noticed that CKEditor appears to do just fine until the DOM selection suddenly changes. Let me explain:
Whatever is causing the update to the DOM selection is responsible for this entire mess. I set up a logger in The problem that my workaround solves is also related to the selection, in particular that the internal selection does not keep track of the composition that started thus it assume the selection to be collapsed. This explains why my workaround above does not handle this case, because the selection is not where it should be. If the I added a switch to discard the I feel like I am missing some crucial here. When I start typing the first word, I get an underline from the very beginning (together with a Edit: There is one thing that I just noticed: |
The workaround in this issue is not required, it can be solved much easier with the fix outlined in #13994 (comment). |
@Witoso Is there going to be an official fix for this? I'm now experiencing this (or an extremely similar) issue in the latest Safari Technology Preview (Release 190), also related to predictive text. Here is a demo of it happening on the CKEditor5 demo page: screenshot.2024-03-26.at.15.16.07.mp4I thought perhaps it's a bug in the tech preview, but I tried to reproduce it with the Quill demo editor, and it works fine there (and every other field I've tried this in on this version of Safari, outside of CKEditor): screenshot.2024-03-26.at.15.14.41.mp4 |
@winzig thanks for the info! We will check this. |
I can reproduce this issue in Safari TP 190 and I’ve tested my fix from #13994 (comment) by changing the check for That means that while it is somewhat similar, this is in fact a different issue. Trying a few different things I believe that this is possibly similar to the behavior described in #15831 (which by the way lacks the proper labels). I did not perform any further investigations to confirm or disprove this hypothesis. @Witoso I recommend moving the Safari issue into a dedicated tracking issue. |
The work for this issue and others related is completed in the #16289, and we finalized the round of internal (successful) tests. I encourage everyone to test the PR if you have a chance. It should be merged in the following days, and will be a part of the next release. |
📝 Provide detailed reproduction steps (if any)
A user has reported that sometimes the first character of a word is duplicated and appears to the right of the caret. Any keypress is inserted to the left of the caret as expected.
The user has provided a screen recording of the issue and has explicitly confirmed that this is reproducible in the online demo of CKEditor. It was confirmed that the issue does not occur with Gboard or SwiftKey.
Screen_Recording_20230703_075830_Firefox.mp4
✔️ Expected result
Typing just works as expected.
❌ Actual result
The first character of a new word is duplicated and inserted to the right of the caret.
📃 Other details
If you'd like to see this fixed sooner, add a 👍 reaction to this post.
The text was updated successfully, but these errors were encountered: