Closed ianstormtaylor closed 4 years ago
@thesunny you're right, that sounds like a bug to me. It should use the passed in range, and never the value.selection
.
There is an Android issue that I'm not sure we can fix.
I edited "rich" to change to "Rich" using Android's auto-suggest and it returns this in the DOM:
It obliterates the <span data-offset-key="3:1">
and only keeps the <b>
tags.
Any ideas? I'm wondering if this is even fixable without some really hard work to rebuild the DOM manually.
I have to say, this is really really hard, most of all because the time to check a piece of code after writing it is about 30 seconds. Android studio is slow, requires a hard reload, and crashes often requiring restarts and reconnecting the debugger.
Wow it even changes it from a <strong>
tag inside to a <b>
tag huh?
To add context for anyone reading, this is the before state of the DOM:
This is definitely something that's fixable, but yeah, the only solutions I see on the face of it are really ugly. Hopefully @ianstormtaylor has some better thoughts on how to manage this?
@thesunny can you describe your current testing setup? What's the oldest API you're trying to cover, and are you testing with each API version from that one up to the present? I guess, in short, how many emulators do you need to run and how many physical devices do you have?
Is the trouble with Android Studio on Mac or Windows?
I don't necessarily have anything to offer, but it seems that it would help if you had more physical devices. Does that seem likely? I don't know how Android Studio behaves with multiple devices connected though.
If you think that would help, which APIs do you need physical versions of? We can try to get some unused Android devices together to cover that need.
@Slapbox
I have a few Android devices but I couldn't get the Chrome debugger on the desktop to connect so I've been using Android Studio and restarting often.
My proof of concept React Editor (not using Slate) was set up with 6 different versions and all the basics worked. With Slate, I'm stuck on two issues with API28 so haven't moved on to earlier APIs. I'm fighting with Slate which is great but doesn't have the necessary abstractions so I'm creating new ones along with unexpected Android behavior which really makes no sense whatsoever. Android Chrome is worse than fighting early versions of IE.
My two biggest problems right now are:
compositionEnd
when you move the selection in the middle of a compositionI had other problems which mostly boiled down to the incosistent state of the DOM and given events which I resolved by modifying the input tester which you can see here https://thesunny.github.io/input-methods/index.html which now shows both the DOM state at a given event as well as the selection position.
That compositionEnd
not being called is tricky because I cannot re-render the DOM while the selection is on it because it moves the selection to the beginning of the node. So, I tried to update Slate's state on the input
after compositionUpdate
(which is when the DOM is correct) but then the cursor gets forced into the wrong place by Android.
My solution which I will try is to wait for the cursor to move to its new location, remembering where the last cursor position is, updating the state in the place where the cursor previously was. Hopefully this won't reset the cursor position because it's in a different node... however, I haven't tested it yet so don't know if it will work. To get this to work I have to create another abstraction which will allow Slate to update itself from an arbitrary Slate node.
I feel like this wouldn't be that hard if it weren't for the fact that I'm working in molasses. It's hard to remain focused when everything takes so long.
@thesunny let me know if there's any way I can help. Tinkering in slow motion is exasperating. Are all the Chrome debugger/Android Studio issues on Mac or Windows? On Windows 7, I've found them to be somewhat buggy, but not terribly so.
It’s on the Mac. Maybe I should try windows. How is the page reload speed?
Update:
I solved the Android issue where the compositionEnd
won't fire if you move the selection after starting a composition. Essentially you can get a composition that spans several nodes. I solved it by keeping a Set
of all the nodes that were touched during the composition. When the input
after compositionEnd
fires, I then go and resolve all the nodes against the Slate DOM. Seems to work.
Now I'm at the issue (final issue that I've seen) of the React nodes being removed when you choose a suggestion for a word that is wholly within a node like <b>bold</b>
. In this case Android fires all the events outside of the composition so if your cursor was between o
and l
and you wanted to switch it to <b>Bold</b>
it would do something like compositionEnd, delete, delete, insert "B" insert "o". But it would do it all simultaneously with the events firing in a weird order (keydown, input, etc. are out of order for some reason). I have to keep trying things until it works but I have a few angles I'll try.
So, I said I'd put in 1-2 weeks and this is 2 so I'll keep plugging but won't be full-time. Sorry it's been slow but Android is slow to develop with and frankly the wait events fire doesn't make much sense. This only solves API28 but my feeling is that a lot of the abstractions necessary to fix the problem are there now so the other APIs should be easier to tackle.
We're currently having issues with the long-press keyboard behavior for diacritics on macOS and I was wondering if this could be a fix for that.
Our problem is with this feature:
When picking one of the options, the original letter that was typed doesn't get replaced, resulting in two letters which is not the desired behavior.
(Also, is there a simplified version of the code somewhere that works on major desktop browsers and that we could try to experiment with to see if we could help with the non-Android bugs that have been reported throughout the thread?).
Hi @quentez
This is a candidate to fix that problem.
These are the main concepts in understanding what problems it can solve:
It stays out of the way while the browser does its thing while composing content
At certain times, Slate will issue a command that will (a) update Slate's internal document
to match the current state of the contenteditable
div
(b) because the state is updated, it forces React to reconcile the DOM. Assuming Slate's document
is the same as the DOM, this should make no changes to the document.
The problems occur in figuring out when those moments are when we can sync the editor's document
up to the DOM. For example, in the middle of a composition, if we update the internal document forcing the re-render, the composition mode gets broken. This is trickier than it first seems because compositions can sometimes span multiple HTML elements (this in my opinion is a bug) and sometimes don't sync well at the compositionend
event. But I think this problem may be fairly straightforward (if time-consuming) now that the basics are in.
The problem you cite appears to fall into the category of solvable issues so long as you sync the document
after the modified character is inserted and not before.
I do have a proof of concept here:
https://github.com/thesunny/composition-editor
With a demo here:
https://composition-editor.now.sh/
Can't remember how far along it was though so not sure if it would be useful for you or not.
@thesunny, sorry I haven't had a chance to test your work still. Still having trouble updating my Android version to the proper API.
Mini Update:
I posted a short update in Slack which I'll summarize and expand on here.
Our project is prioritizing mobile which includes Android support. I will start again on Android within 2-8 weeks and I estimate 2-3 weeks of work which includes:
compositionbegin
, compositionchange
and compositionfinish
event and block all other events in between for the Android platform. These events roughly equate to (a) we guarantee that this is the state before the composition started (b) something might have changed within the composition so note the cursor position here (c) we guarantee that the composition has ended and that the results of the composition have been commited to the DOM so it is safe to update Slate State and re-render. These roughly equate to the idea of compositionstart
, compositionupdate
and compositionend
but abstracts away the problems and inconsistencies between them. Note: We may wish to implement this across all platforms.A few different resources
Here's one more that I built recently: https://input-inspector.now.sh/
It's inspired by https://github.com/danburzo/input-methods, and has a couple nice additions:
Source code is here, if you're interested. Hope you find it useful! ✌️
I want to shed some light on Android OS versions and how those relate to Chrome and embedded WebView
's in an app. I noticed some discussion here about testing and supporting particular OS versions, but I don't believe that's a great path for producing reliable input or tests.
The more relevant piece of information is the WebView
or Chrome version installed on the device. I've outlined below how the Android OS versions relate to their WebView
counterpart. (I've started at Android 4.4, because I don't believe it's worth anyone's time to support Android 4.3 and lower.)
Starting in Android 4.4, the system WebView
component is based on Chromium. https://developer.android.com/guide/webapps/migrating
WebView: shipped with and locked at version 30.0.0.0
Chrome: latest available (currently 70.x
)
WebView: shipped with and locked at version 33.0.0.0
Chrome: latest available (currently 70.x
)
Starting in 5.0, the WebView
component became unbundled from the OS and regular updates are provided via the Android System WebView app in the Play Store. https://play.google.com/store/apps/details?id=com.google.android.webview
WebView: latest System WebView available (currently 70.x
)
Chrome: latest available (currently 70.x
)
Starting in 7.0, the WebView
is no longer backed by the Android System WebView app, but instead uses the installed version of Chrome on the device to render embedded WebView
's. https://developer.android.com/about/versions/nougat/android-7.0#chrome--webview-together
WebView: latest Chrome available (currently 70.x
)
Chrome: latest available (currently 70.x
)
Given the self-updating nature of Chrome and embedded WebView
's on Android 5.x+, it'd make sense to focus on those devices. A (relatively new) minimum Chrome/WebView instance could be required as a baseline based on how far back you want to support.
With all the well known composition event issues on Android, Level 2 beforeinput
/ input
events seem like a good path forward, like @javan mentioned. These events are supported in Chrome 60+ and Android System WebView 60+ across all Android OS versions 5.0 and up.
Javan has started implementing a separate Level 2 events controller for mobile in Trix, and so far it's looking really promising. Here's the progress so far: https://github.com/basecamp/trix/compare/level-2-input. The basic gist is handle beforeinput
, and implement a method for each event.inputType
https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes
You can test if Level 2 events are supported in a particular browser/device by running this in the console:
(typeof InputEvent != "undefined" &&
"inputType" in InputEvent.prototype &&
"data" in InputEvent.prototype)
@jayohms thank you so much for that detailed write up, I really appreciate you taking the time to share your Trix learnings! Starting with only the versions that have auto-updating behavior sounds like an extremely reasonable approach.
Question for you, does Chrome really support Input Events Level 2? Everything I've read so far stated that Chrome supports Level 1, and Safari supports Level 2. (Although my testing revealed that some of the Chrome events were cancelable, so maybe they really are at Level 2 and just haven't said so?)
Yup, they're supported in Chrome: https://www.chromestatus.com/features/5656380006465536
@jayohms - Thanks a ton for dropping in with that knowledge! That's a huge help.
@javan - Perhaps you can shed some light - The issue you linked to links out to the W3C specification for Input Events Level 1, but if you go to that specification it shows this:
That's encouraging, but it doesn't seem to explicitly state that Level 2 is supported and the Chromium issue it links to doesn't provide any further insight. Is there any additional information you know of that we can reference to confirm Level 2 support? That would be a dream.
I think they just have an old link on https://www.chromestatus.com/features/5656380006465536
https://bugs.chromium.org/p/chromium/issues/detail?id=585875 links to
I'm pretty certain Level 2 input events are here to stay in Chrome. Also, Safari supports them and Firefox is workin' on it.
Chrome doesn't support level 2 yet. As Masayuki points out in the Firefox bug you linked to there is no support for "insertFromComposition", "deleteByComposition" or "deleteCompositionText".
Support for inputType
should land in Firefox very soon (there was a lot of work to get to this point) with work on beforeinput
coming after that. (There's still a number of things that need to be fixed in the spec first, however.)
Apologies, I think I have my terminology wrong. I was using level 2 to mean beforeinput
/input
events with inputType
, data
, etc. properties vs. the old generic input
event without those properties.
What's the difference between level 1 and 2? At a glance, the specs look similar.
Oh, I see:
According to the source code, Chrome implements Level 1. https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/events/InputEvent.h?l=26-72&rcl=d4c3823079fe41cd1cbef1ffe2e8b24a173f5559
Since there are not definition of "insertFromComposition", "deleteByComposition" and "deleteCompositionText". So, perhaps, we should follow Level 1. I don't know how much stable of the specs, though.
Chrome doesn't implement those newer inputType
s for compositions, but it does implement "insertCompositionText". Here's a composition in Chrome (Level 1) and Safari (Level 2): https://input-inspector.now.sh/profiles/KXHldfRK9AtSj19nRR3O,q1nsTzNNdb8927yUR1vW. So, it's entirely possible to handle compositions in both browsers by responding all four composition inputType
s. I have it working in Trix: https://trix-level-2.now.sh/
Last I checked I thought the big difference between Level 1 and Level 2 was that in Level 1 many of the events were not preventable using event.preventDefault()
? So you were required to let the event happen and then try to figure out what happened. But maybe this is no longer true?
@ianstormtaylor I think Level 2 encompassed quite a bit more than that. I wouldn't be surprised if some browsers implemented an ability to cancel the events without implementing the rest of the draft specification. That said, last I checked the Chrome team said they'd not be making any accommodations with regards to onBeforeInput
and preventDefault
. Last news I saw was about a year ago now though.
Regarding preventing events, see the first two notes in this section of the draft spec
Trix, for the most part, does not cancel events so the inability to event.preventDefault()
Level 1 input events isn't an issue. Generally, our approach is:
This way, we avoid touching/disrupting the editor element when contenteditable
does the "right" thing, which is fairly often when typing normally. When it doesn't do what we want, we step in. We never have to "try to figure out what happened" since the internal document model is always the source of truth.
With Level {1,2} input events, step 1 becomes a lot easier. We just listen for beforeinput
and translate the event's inputType
, data
, and getTargetRanges()
. In browsers that don't support them we handle a dozen or so key and composition events.
Firstly, sorry for missing the deadline for starting work on Slate Android support. Unfortunately, I've had more work to do on our app before I could start dedicated work on Slate Android. I will try and start around the new year. I've compiled everything I know and what my plan is and pasted them below in a kind of state of Slate Android support so you know where I am:
Sunny's State of Slate Android Support
I've tried to write this many times and failed because there are so many overlapping ideas. After many failed attempts, I think there's value in doing a brain dump even if it's imperfect. So, my apologies, if this comes out as a stream of consciousness post.
The following is a run-down on the current state of my attempts at supporting Android on Slate, what has failed or proved difficult and how I plan on implementing Android support.
I also felt the need to publish all of this because it informs the approach I'm taking at the end. Without this preamble, it would be hard to understand the approach.
1. The problem is hard to solve because of many incompatible Android versions.
Android support is hard. It is not hard like getting Chrome, or Safari or Firefox to work. It is more like it's hard like getting Chrome, Safari and Firefox to work. That is because there are at least 6 versions of Android we need to support and they all work differently. They fire different events in different order, in varying amounts and at different times. In essence, you aren't supporting one new browser, you are supporting 6. In fact, the differences between versions of Android are greater than the differences between actually different browsers in my opinion.
Also, unlike desktop browsers, Android browsers are tied to Android versions. It's easy to support the latest few versions of Chrome on desktop because you know they auto update. Mobile OSes, however, do not, so you end up having to support the last few versions in order to get any meaningful browser support.
Note that the most highest rate of usage of an Android version as of today is API 23 with 21.3% which is 4 versions away so even if you only wanted to support the most popular API version and newer, that's 5 versions. The next earlier version is API 22 with 14.4% and then after that is API 21 with 3.5% which I think is reasonable enough to cut off. The latest 6 versions encompasses 85.4% of Android API versions.
2. High number of input methods and situational edge cases
There are many different input methods that need to work to support Android propertly. International input methods, autocorrect, spellcheck correct, gesture writing and what I call word break insertion in which browser behaves differently when a word breaking character is entered.
Furthermore these scenarios may behave differently at the beginning of a word, in the middle of a word and at the end of the word as well as at the beginning of a line, end of a line and the beginning and end of a document.
Also, in some API versions a single composition can take place across multiple separate DOM nodes. That is, multiple words can be edited as part of a single composition which must be handled as a single composition or it fails. In my opinion this is a browser bug but never-the-less needs to be handled.
3. Inability to do partial support
Certain bugs appearing in Slate are inconvenient but acceptable even if annoying. For example, if a user's input fails in 10% of the input cases and that left the text a little garbled and then you go back and fix that garbled text, a case could be made that it would be acceptable. However, if 10% of the input cases left the editor in an inconsistent state in which errors compound and can never be recovered from without resetting the editor, that to me is unacceptable.
Our users get upset losing any amount of data. Imagine somebody composing a blog post and then losing their post. Android bugs tend to fall into the space of putting the editor in an inconsistent state which results in data loss and therefore, it's unfortunate but, partial support feels like a bad option unless the edge cases are truly edge (maybe 1%).
4. Don't construct Editor State from individual events
I've been advocating for this a long time, but for anybody else attempting otherwise, do not reconstruct Slate's internal state by individually reading events and trying to figure out what the user did in Android. It works in other browsers but it doesn't work in Android. Worse, it looks like it will work and in certain, very simple scenarios, it does work, but it always fails when some combination of different input methods or Android API versions are used and then you have to throw away all your Android compatibility work.
Just looking at the enormous differences from the events I logged between different input methods and Android API versions shows basically how hard it is. Honestly, I think it's actually impossible to make this work reliably because I believe there is not enough information provided in the events themselves to reconstruct in all scenarios.
5. Do Use DOM Reconciliation
Something I got working quite reliably (in one day actually) with a proof of concept editor I built was to use DOM reconciliation. Basically, you wait for a compositionStart event to happen, then you ignore everything else that happens until you get to a compositionEnd event. Then when you get one of those, you read the DOM, then you update the Editor's internal state using the DOM. If all goes well, React's DOM Reconciliation ends up being like a no op because the DOM matches the internal state. I did this successfully in my prototype also using transactions like Slate does. Then I also got this to work in almost all cases except one that I found in (I think) two versions of Android.
6. Nieve Reconciliation Fails
The version of DOM Reconciliation I built works under the idea that it is safe to ignore all the events after a compositionStart and that it is safe to read the DOM during a compositionEnd and to update the DOM immediately after a compositionEnd. Unfortunately, this is sometimes true but not always true. It is true and untrue depending on which Android version you are using and what you are doing. In some cases, it is true on an event that happens immediately after the compositionEnd but not on compositionEnd itself. Ultimately, it is unpredictable to know without testing every Android API version multiplied by every input scenario.
So one aspect of making Android work is to discover when what would be our ideal compositionStart and compositionEnd happens. We may wish to call these different events to distinguish them from the native compositionStart and compositionEnd. At any rate, as long as it follows our rules which is safe to ignore after start, safe to read at end and safe to do a React DOM refresh after then everything should work.
7. Must Support Multiple DOM Nodes in a Single Composition
Initially, I assumed that a composition must occur within a single DOM node. For example, if you started editing a word in a paragraph then clicked into another paragraph, I assumed that the first composition would end, then a second composition would begin in the second paragraph. In fact, this held true for the initial API version I tested against; however, in switching to a different API version, I found that a single composition could span multiple nodes.
So the solution must be able to know in which nodes the composition has taken place in, then reconcile all the changes across multiple nodes. I handled this by creating a JavaScript "Set" of nodes and adding the DOM node whenever a compositionUpdate event took place. Since a Set doesn't allow for duplicates, you end up with a nice clean list of DOM nodes in which you edited. I have gotten this to work in Slate.
8. Android Studio is hard to work with
There is no other way to put it than working on Android support in Android Studio is the most frustrating, slowest, and hardest development process I have ever gone through in my entire life. It executes an order of magnitude slower than an actual device, it gets slower as you work on it, then it crashes fairly soon. Sometimes you can't figure something out, and the virtual device just needed to be restarted.
In my opinion, it's more than just a matter of having more patience. When you make a code change and you have to make sure that code change didn't break the 20 other cases before it that you have already fixed, you need to be able to do those tests quickly.
Furthermore, the longer it takes to run each test, the more shortcuts you end up taking like ignoring some cases like maybe just doing a basic gesture edit and not testing at the end of a line and only in the middle. When you finally get to do the full test again, you're not sure which of the last changes broke Android support.
The slowness is compounded by the fact that these tests can't be automated short of using a robot finger (which I'm not about to build) and that these tests compound for each version of Android you need to support.
9. Use Native Devices
So use native devices where possible for the device testing. My initial two week dedicated work on Slate was done in Android Studio because I couldn't get the native device debugging to work with Chrome and I just needed to move forward. That problem has been solved now and I recommend not trying to implement Android support using Android Studio with one caveat which I will get to.
10. Regression testing
Given that (a) Android studio is hard to get running and when it is running we need 6 versions and those 6 versions can crash unreliably and (b) basically nobody has 6 Android native devices each having a different Android version that needs to be supported, then my biggest worry is regression testing. How can we possibly do regression testing when the only affordable way to test is prohibitive to set up and run and the only reliable way is expensive and would still take prohibitively long to test manually.
When somebody makes an update to Slate, there is a non-zero chance that it breaks Android support in one or more versions so there needs to be a way to test.
The only way I can see this working is to record Android events and DOM state during each event, save them in a test, replay them to Slate and see if we get what we expect out of it.
11. Recording Android Events
We need to create a web app that closely mirrors Slate's event system. Then we need to record the events into a JSON compatible format extracting the minimal amount of data for us to properly test. In addition to the events, we need to record the DOM state at the time of the event as that information is necessary to tell us if, for example, a change has been commited to the DOM at the time an event has been called. This can vary between versions of Android.
In the scenario of recording the events, this may be a good area where we can use Android Studio since we only need the recordings to execute once.
12. setTimeout and requestAnimationFrame
Another thing we need to track is setTimeout and requestAnimationFrame as a way to tell us what events get fired as a group. This can be useful in possibly simplifying code.
For example, if compositionEnd is unreliable to tell us when the edit has been commited to DOM (which it is for some verisons of Android) and in some cases the keyDown fires after it which is when the change is commited to DOM, we can't use compositionEnd. But if the keyDown is grouped with the compositionEnd, we could potentially use setTimeout or requestAnimationFrame from inside the compositionEnd with the minimum timeout and then fire the reconciliation code after that. So in order to test this, whenever an event fires, we run a setTimeout and a requestAnimationFrame. This gets logged. All the subsequent events that get fired do not launch a new setTimeout or requestAnimationFrame. When the callback is fired, we log that. This way we know which events get fired as a continuous group without space for a setTimeout.
13. Holy Grail
I think there is a possibility that there is a holy grail like using setTimeout/requestAnimationFrame. Like maybe we can wait for a compositionEnd, fire a setTimeout and wait for the callback before reconiling and maybe it works in 90% of the edge cases in all Android versions and then we only handle the other cases; however, we can't know without running all the test cases against all the Android versions so it seems likely we need to record all the android events with setTimeout/requestAnimationFrame for grouping at a minimum.
14. Why I haven't made PRs so far
Well, to put it simply, I wouldn't accept my own PRs. Yes, it's mostly working but the code is a mess. Because of how long it takes to retest a change, I kept refactoring to a minimum because it could inadvertently stop code from working which would in turn take a long time to fix and retest.
However, you can peek into my Slate repo.
15. Separating DOM Reconciliation and making it more efficient
I have a version of DOM reconciliation which is basically the code already in Slate extracted into a separate module. It works, but it is somewhat nieve in that it replaces the entire contents of the DOM node in Slate's State even if only one character changed.
A more efficient algorithm would look for characters that match at the beginning and match at the end and only the differences in the middle would make up the actual changes. I had a version of this working in my prototype Editor. I don't think this is necessary for a version 1 of Android support but it wasn't that much work in my prototype to build.
16. Modularize Composition Handling
Right now, Slate's before plugin handles preprocessing of events which includes handling of compositions including when they start and end. This has made it difficult for to build in the different methods that Android needs to handle compositions.
We need a way to separate composition handling into it's own component which can be customized per browser and possibly per API version. The alternative is to have potentially a large number of if/else/switch statements in the before plugin itself.
Steps to Move Forward
So here's a plan for working on Slate Android Support.
before before
plugin to handle Android input. It would handle all the Android stuff before it gets to the before
and after
plugins. Anything non-Android related will be let through to the before
and after
plugin. The reason this is necessary is due to the complexity of manipulating when events need to be handled in Android. For example, we may need to handle the end of a composition, not at the compositionEnd event but at some other event that happens immediately after it. This logic just makes the before
plugin messy and hard to understand for every other browser. I fear there would be so much Android specific logic that is hard to untie from the rest of the Slate logic. By placing it in a separate plugin that only handles Android logic, we keep the rest of the code clean. Also, this helps because we don't want the events in the middle of a composition to be handled through the plugin architecture. They need to be ignored (e.g. keydown, keyup, etc in a composition). That is, we don't want users handling them at all until the composition has fully ended. In Android, we can just cancel all the events that shouldn't go through the plugin system.@thesunny amazing write-up (from what I've gotten to read so far) - Have you tried using Expo as a way to bypass the nightmares of Android Studio? Not to nitpick at all, but I wanted to let you know you missed bolding items 15 & 16.
Thanks @Slapbox and fixed.
Also, I've updated with a Steps to Move Forward.
Hey @thesunny thanks for the writeup!
As for version support, can you reconcile what you said with what @jayohms mentioned above? I'm unclear about how the API xx
versions reconcile with the x.x.x
versions. Specifically it sounds like 7.x+
would guarantee we're working with the latest version of Chrome always.
Supporting back 6 versions sounds like a huge amount of work, and it doesn't sound like it should be necessary for the first stab at Android support. It would be more useful to get it working with the latest version or two, and then see what can be done about backwards compatibility.
Build an event logging system that can save data into a permanent format like JSON or into a database. May be ideal if we can store this in a database or something so that others can contribute use cases
FWIW, that's exactly what I built: https://github.com/ianstormtaylor/slate/issues/2062#issuecomment-438436799
@ianstormtaylor
7.0+ includes Nugat and Oreo or about 50% of the ecosystem which is pretty good.
However, what may not have been clear is that this already encompasses 5 API versions which is one less than my proposal. Nougat and Oreo each had 2 API changes each. Adding one more API version (API 23) adds the biggest number of supported users. API 23 (which is Marshmallow or Android 6.0) has the largest market share out of all API versions with 21.3% of all users using it.
I think what @jayohms is talking about is the chrome version in relation to Android versions but I think, most likely, we are seeing different behaviors in the event firing and their order due to GBoard. I'm guessing this because the Samsung keyboard I believe doesn't have the GBoard issues.
I think that the approach I'm taking will work almost identically between each Android version with the differences being when a composition starts, when it ends, and which events we need to cancel. In other words, I think the big work is actually getting any 2+ version of Android to work since it has to be abstracted. At that point, we just need to configure the handling and cancelling of events.
As I mentioned in an another post, I had Slate working in 2 versions of Android with (if I remember correctly) one issue. But it was getting really hard to add the second version because I was shoehorning each version on top of the other instead of building a proper framework around it like the before before
plugin which I mentioned. I think it will get deblitating going to 5+ versions! Also, I kept getting regressions which I wasn't aware of which is why I was thinking of the event capturing and replay framework (BTW, thanks @javan I was planning on talking to your more about your work in that).
@thesunny the benefit to working backwards from the latest versions though is that over time all but 8.x
is declining is usage, so depending on how long it takes to get it working, we can eliminate needing to serve versions like 5.x
and 6.x
.
But I'm still unclear on what the API changes we need to care about are... There's the Chrome version, which is sounds like as of Android 7+
will be the latest version of Chrome. So that shouldn't be an issue.
Are there other things that determine the event ordering? Is it not tied to the Chrome version that is being used?
@javan
Thank you so much for building that.
There are a few things missing that I need and I wonder if you might be able to help me by adding them. I know I can fork but having some help would be great and move things along faster. I can start another issue to see if we can hash things out if you are able to. We can go deeper if you are able to, but off the top of my head:
@ianstormtaylor
So, my hope initially was that we wouldn't have to handle so many versions. When I initially started recording the inputs, I was hoping that (a) API versions didn't matter and only Android versions did and (b) I'd find some patterns so that we wouldn't have to handle so many versions.
So for (a) unfortunately, each API versions is basically like handling another browser when it comes to input events. I might be missing something but I don't think we can get away from the fact that there are quite a few API versions we need to support. For example, you can buy brand new Nougat devices today which is still like 4-5 versions of support?
And for (b) there are similarities which is what I'm planning on abstracting against.
Basically, it boils down to this. (1) user starts to make an edit and then we need to lock Slate's document model from changes. (2) as the user is editing, we need to record where the edits took place in th DOM (3) we need to wait until the composition ends, reconcile Slate against the dom changes and unlock the editor .
So we need to write that no matter which version of Android and I think that's actually most of the work to handle all versions of Android. Each Android version only changes the logic of when composition starts and ends and which inputs that we need to blackhole (i.e. the events that need to be ignored lest Slate Documents gets updated which means React re-render the DOM in the middle of composition and puts editor in an incosistent state). But I think to put you at ease, I would probably release with 2 versions of Android first but I'd maybe try to keep working until we got all 6. The idea I was trying to convey is that most of the work has to be done if we are going to support at least 2 versions anyways.
I still have hope though that we can use requestAnimationFrame with an event and it might (hopefully) work for all or almost all versions of Android.
@thesunny I am really need of this android support. Because of this limitation, i have gone through other libraries likes quills js, proseMirror, but i don't get the comfortable that i got with slate. any ETA for this issue to be get fixed, Since i am building a minimal editor using plain contentEditable div. once this issue is got fixed, i will thrown this code and use the slate instead.
It may be worth considering building an abstraction library for Android devices' input (with Slate as primary candidate user) and maintaining it as a separate package.
This may make it a little easier to test and could be proven to be useful for other devs. 2¢
I was playing with this today... Not that this works well by any means, but it might be helpful to others:
componentDidMount () {
window.addEventListener('input', this.onInput)
}
componentWillUnmount () {
window.removeEventListener('input', this.onInput)
}
onInput = e => {
if (isAndroid && `editor-${this.id}` === e.target.attributes.id.value) {
e.preventDefault()
e.stopPropagation()
const endsWithSpace = /[ ]+$/
const focusKey = this.state.value.selection.focus.key
const block = this.state.value.document.getClosestBlock(focusKey)
const lastText = block.getLastText().text
const words = lastText ? lastText.split(' ') : ['']
const lastWord = words[words.length - 1]
console.log('=========================')
console.log(e.inputType)
console.log('key', focusKey)
console.log('data', e.data)
console.log('lastText', lastText.length, block.text.length)
console.log('empty', lastText.length === 0)
console.log('endsWithSpace:', endsWithSpace.test(lastText))
console.log('words', words)
switch (e.inputType) {
case 'deleteContentBackward':
if (e.data) { // If the soft keyboard has buffered data
this.editor
.deleteBackward(lastWord.length)
.insertText(e.data)
} else if (lastWord.length > 1 || words.length > 1) {
this.editor.deleteBackward(1)
} else {
console.log('crash!')
}
this.editor.focus()
break
case 'insertCompositionText':
if (lastWord.length > e.data.length) {
this.editor
.deleteBackward(lastWord.length)
.insertText(e.data)
} else if (lastText.length === 0 || endsWithSpace.test(lastText)) {
this.editor
.insertText(e.data)
} else if (lastWord !== e.data) {
this.editor
.insertText(e.data.replace(lastWord, ''))
}
break
default:
console.log('unhandled: ', e.inputType, e)
}
}
}
....
render = () => {
...
<Editor
id={`editor-${this.id}`}
key={`editor-${this.id}`}
placeholder='Send a message...'
ref={this.ref}
value={this.state.value}
onChange={this.onChange}
renderNode={this.renderNode}
renderMark={this.renderMark}
plugins={this.plugins}
/>
...
I think @dmitrizzle's idea is worth serious consideration.
@JuanValencia can you comment on the current state of that code and if it improves any of the behavior of Slate as it is?
I kept hacking a bit more. This is the current update:
onInput = e => {
if (isAndroid && `editor-${this.id}` === e.target.attributes.id.value && this.editor) {
e.preventDefault()
e.stopPropagation()
const endsWithSpace = /[ ]+$/
const focusKey = this.state.value.selection.focus.key
const block = this.state.value.document.getClosestBlock(focusKey)
const lastText = block && block.getLastText().text
const words = lastText ? lastText.split(' ') : ['']
const lastWord = words[words.length - 1]
console.log('=========================')
console.log(e.inputType)
console.log('key', focusKey)
console.log('data', e.data)
console.log('lastText', lastText.length, block.text.length)
console.log('empty', lastText.length === 0)
console.log('endsWithSpace:', endsWithSpace.test(lastText))
console.log('words', words)
switch (e.inputType) {
case 'deleteContentBackward':
console.log('---- could crash')
if (!lastWord.length > 1 && !words.length > 1) {
// The node with key doesn't exist when you backspace here in the DOM, but does in the value
}
break
case 'insertCompositionText':
if (lastWord.length > e.data.length) {
this.editor
.deleteBackward(lastWord.length)
.insertText(e.data)
} else if (lastText.length === 0 || endsWithSpace.test(lastText)) {
this.editor
.insertText(e.data)
} else if (lastWord !== e.data) {
this.editor
.insertText(e.data.replace(lastWord, ''))
}
break
default:
console.log('unhandled: ', e.inputType, e)
}
}
}
This improves the functionality on a nexus 6 and pixel 3 a great deal. There's oddities in edge cases and I really haven't tested it thoroughly. What's more, this is external -- not internal, so there's that.
I don't see why you couldn't do this logic internally -- with several iterations, I'm sure it could catch more and more people's edge cases.
-edit- : I'm not sure how you would abstract this to a separate library. I feel like I want more control over the events not less. What's more, you have to muck with the editor's value since this is more of a compensating solution, rather than a native one like Sunny was suggesting.
@JuanValencia Nice approach and a great way to test ideas before having to modify Slate code! I think I'll do that during prototyping.
@saravanan10393 I would say estimated time to completion will be January or February realistically.
@thesunny I am really hoping to see this as well. Having even "Inserting text" and "Deleting characters backwards" work would be a huge help!
@JuanValencia I tried to play around with your code but I can't get input
events to fire on the editor on desktop or mobile. They fire just fine if I input into a plain <input />
, <textarea />
, or <div contentEditable />
Any ideas what I might be overlooking? slate@0.43.7
& slate-react@0.20.8
Just a heads up that I've almost completed an events recorder to properly understand Android in ContentEditable. It was faster to build it from scratch because of some specific requirements I had for it that weren't present in existing events recorders.
I may be asking the community to contribute recordings for various API versions. I will then use that information to build the Slate Android feature.
Hey everyone, if you have time to make some event recordings from an Android device, please go to this URL with an Android browser:
https://editor-events.herokuapp.com/
The app will identify what API version you're on and also which scenarios don't have recordings for a particular API version. Please follow the instructions on each scenario carefully. Thank you!
Note: "View Recordings" doesn't work on mobile yet. You'll have to view from desktop. I wanted to deploy before I went home and didn't want to delay before the weekend. It shall be working soon enough...
Featuers:
If you think you can help in designing useful scenarios or editing existing ones as well as potentially curating the recordings (e.g. if a recording is bad) then let me know and I can show you how to enable admin access.
@thesunny, when I visit the project on a Google Pixel 2 with Chrome, I see "Note: You are not using an Android browser"🤔. Shall I still proceed and complete recordings, or is there an issue I'm overlooking?
@rbar2
Ahh... interesting. Do you think you can share your user agent with me? I might be parsing it wrong.
It works for me from Android Studio but I haven't tested it with any real devices yet.
You can get your user agent from here:
Note: Recording should be fine as I store the entire User Agent for analysis but you won't be informed which of the tests require a recording for your API version.
Sure thing: Mozilla/5.0 (Linux; Android 9; Pixel 2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36
Great, I'll do some recording for you👍
It also gives, "Note: You are not using an Android browser" for Firefox for Android (UA: Mozilla/5.0 (Android 9; Mobile; rv:65.0) Gecko/65.0 Firefox/65.0
for current beta).
(And I really hope draft doesn't end up adding UA sniffing here. UA sniffing in libraries like this makes it nearly impossible for us to update Firefox to behave more like Chrome/Safari.)
Thanks so much, @thesunny! I've recorded many using api-27 and the SwiftKey keyboard. I noted in some but not all the comments I was using SwiftKey rather than the default Gboard.
@KrisBraun Oh, I better add the ability to specify the keyboard. It's Gboard that typically gives us problems. That said, the more recordings we have the better so we know what's going on in all the browsers.
@birtles FYI, this is Slate not Draft but I'll try to use as little UA sniffing as possible. I'm hoping to find some generic solutions here. That said, Firefox will probably be easier than Android. Basically all the browsers except Android/Gboard work similarly. It's only Android and it's multiple API versions that work differently that is problematic. I suggest aiming for compatibility with Chrome/Safari and my guess is Slate will either already be working or it shouldn't take too much work to do so.
@rbar2 Yeah, I just used a bad regexp. Basically I search for /^9[.]0/ after Android when I didn't account for there not being a "." after the "9". I won't be able to fix it immediately but please continue the recordings. :)
@thesunny, no problem! I have completed all recordings other than the final, "hard keyboard" one :)
Do you want to request a feature or report a bug?
Bug.
What's the current behavior?
The issue with using Slate on Android is complex, and due to a difference in how its OS keyboard is designed. On mobile devices, keyboards are starting to move away from the "keys" concepts in many ways...
Because of this, it sounds like the Android team (reasonably from their point of view) decided to not reliably fire key-related events.
It sounds like the behavior is:
keypress
event is never triggered, which is fine for us.keydown
event but always with a key code of229
, indicating that the key is unidentifiable because the keyboard is still busy processing IME input, which may invalidate the actual key pressed.keydown
event as normal, with anevent.key
that can be recognized? (This is unclear whether this happens or not.)keydown
event.A few different resources:
What's the expected behavior?
The fix for this is also complicated. There are a handful of different, overlapping pieces of logic that need to change, to accommodate a handful of different input types...
The first stage is to handle basic insertions, and auto-suggestions...
preventDefault
inonBeforeInput
, so that the DOM is updated, and theonInput
logic will trigger, diffing the insertion and then "fixing" it.<Leaf>
(and other?) components to increment theirkey
such that React properly unmounts and reconciles the DOM, since it has changed out from under it.This is actually the same starting steps as is required for https://github.com/ianstormtaylor/slate/issues/2060, so I'd recommend we solve that issue in its entirety first, to work from a solid base.
This fixes the actual text insertion pieces, and probably deletions as well. Splitting blocks can still be handled by enter because it still provide proper key codes.
keydown
events.And then there's some selection issues, which apparently can mess up Android's IME (and potentially others) if the selection is manually changed during a composition.
compositionstart
andcompositionend
.I think this would solve the 90% case for soft keyboard input.
Separately, there's still another question of how to properly handle these kinds of behaviors other plugins. For example if a plugin uses backspace at the start of a block to reset that block, that won't work on Android. So after solving the input issues, we need to step back to an architectural level and solve this plugin handling problem. But that can wait.