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

Better Samsung keyboard for Android detection #1021

Merged
merged 2 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/trix/controllers/input_controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import BasicObject from "trix/core/basic_object"
import MutationObserver from "trix/observers/mutation_observer"
import FileVerificationOperation from "trix/operations/file_verification_operation"
import FlakyAndroidKeyboardDetector from "../models/flaky_android_keyboard_detector"

import { handleEvent, innerElementIsActive } from "trix/core/helpers"

Expand All @@ -13,6 +14,7 @@ export default class InputController extends BasicObject {
this.element = element
this.mutationObserver = new MutationObserver(this.element)
this.mutationObserver.delegate = this
this.flakyKeyboardDetector = new FlakyAndroidKeyboardDetector(this.element)
for (const eventName in this.constructor.events) {
handleEvent(eventName, { onElement: this.element, withCallback: this.handlerFor(eventName) })
}
Expand Down Expand Up @@ -55,6 +57,8 @@ export default class InputController extends BasicObject {
if (!event.defaultPrevented) {
this.handleInput(() => {
if (!innerElementIsActive(this.element)) {
if (this.flakyKeyboardDetector.shouldIgnore(event)) return

this.eventName = eventName
this.constructor.events[eventName].call(this, event)
}
Expand Down
14 changes: 1 addition & 13 deletions src/trix/controllers/level_2_input_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ export default class Level2InputController extends InputController {
},

beforeinput(event) {
if (guardAgainstSpuriousAndroidEvents(event)) return

const handler = this.constructor.inputTypes[event.inputType]

if (handler) {
Expand All @@ -82,7 +80,7 @@ export default class Level2InputController extends InputController {
},

input(event) {
if (!emittedBySamsungKeyboard(event)) selectionChangeObserver.reset()
selectionChangeObserver.reset()
},

dragstart(event) {
Expand Down Expand Up @@ -609,13 +607,3 @@ const pointFromEvent = (event) => ({
x: event.clientX,
y: event.clientY,
})

// Samsung keyboard running in a webview emits insertText events
// with composed true, in addition to composition events, let's ignore those
const guardAgainstSpuriousAndroidEvents = (event) => {
return emittedBySamsungKeyboard(event) && event.data !== ". "
}

const emittedBySamsungKeyboard = (event) => {
return config.browser.samsungAndroid && event.inputType === "insertText" && !event.sourceCapabilities
}
60 changes: 60 additions & 0 deletions src/trix/models/flaky_android_keyboard_detector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as config from "trix/config"

// Each software keyboard on Android emmits its own set of events and some of them can be buggy.
// This class detects when some buggy events are being emmitted and lets know the input controller
// that they should be ignored.
export default class FlakyAndroidKeyboardDetector {
constructor(element) {
this.element = element
}

shouldIgnore(event) {
if (!config.browser.samsungAndroid) return false

this.previousEvent = this.event
this.event = event

this.checkSamsungKeyboardBuggyModeStart()
this.checkSamsungKeyboardBuggyModeEnd()

return this.buggyMode
}

// private

// The Samsung keyboard on Android can enter a buggy state in which it emmits a flurry of confused events that,
// if processed, corrupts the editor. The buggy mode always starts with an insertText event, right after a
// keydown event with for an "Unidentified" key, with the same text as the editor element, except for an
// extra new line after the cursor.
checkSamsungKeyboardBuggyModeStart() {
if (this.insertingLongTextAfterUnidentifiedChar() && differsInWhitespace(this.element.innerText, this.event.data)) {
this.buggyMode = true
this.event.preventDefault()
}
}

// The flurry of buggy events are always insertText. If we see any other type, it means it's over.
checkSamsungKeyboardBuggyModeEnd() {
if (this.buggyMode && this.event.inputType !== "insertText") {
this.buggyMode = false
}
}

insertingLongTextAfterUnidentifiedChar() {
return this.isBeforeInputInsertText() && this.previousEventWasUnidentifiedKeydown() && this.event.data?.length > 100
}

isBeforeInputInsertText() {
return this.event.type === "beforeinput" && this.event.inputType === "insertText"
}

previousEventWasUnidentifiedKeydown() {
return this.previousEvent?.type === "keydown" && this.previousEvent?.key === "Unidentified"
}
}

const differsInWhitespace = (text1, text2) => {
return normalize(text1) === normalize(text2)
}

const normalize = (text) => text.replace(/\s+/g, " ").trim()