From 5c91e41bb6888b8a8ed377f6927afef92f45e015 Mon Sep 17 00:00:00 2001 From: Alberto Fernandez-Capel Date: Mon, 28 Nov 2022 15:16:57 +0000 Subject: [PATCH 1/2] Better Samsung Andorid keyboard detection --- src/trix/controllers/input_controller.js | 4 ++ .../controllers/level_2_input_controller.js | 14 +---- .../models/flaky_android_keyboard_detector.js | 57 +++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 src/trix/models/flaky_android_keyboard_detector.js diff --git a/src/trix/controllers/input_controller.js b/src/trix/controllers/input_controller.js index 29187b646..830db3001 100644 --- a/src/trix/controllers/input_controller.js +++ b/src/trix/controllers/input_controller.js @@ -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" @@ -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) }) } @@ -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) } diff --git a/src/trix/controllers/level_2_input_controller.js b/src/trix/controllers/level_2_input_controller.js index a8b9c4817..14707dffc 100644 --- a/src/trix/controllers/level_2_input_controller.js +++ b/src/trix/controllers/level_2_input_controller.js @@ -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) { @@ -82,7 +80,7 @@ export default class Level2InputController extends InputController { }, input(event) { - if (!emittedBySamsungKeyboard(event)) selectionChangeObserver.reset() + selectionChangeObserver.reset() }, dragstart(event) { @@ -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 -} diff --git a/src/trix/models/flaky_android_keyboard_detector.js b/src/trix/models/flaky_android_keyboard_detector.js new file mode 100644 index 000000000..73f8b9f87 --- /dev/null +++ b/src/trix/models/flaky_android_keyboard_detector.js @@ -0,0 +1,57 @@ +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.insertTextAfterUnidentifiedChar() && differsInOneSpace(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 + } + } + + insertTextAfterUnidentifiedChar() { + return this.isBeforeInputInsertText() && this.previousEventWasUnidentifiedKeydown() + } + + isBeforeInputInsertText() { + return this.event.type === "beforeinput" && this.event.inputType === "insertText" && this.event.data + } + + previousEventWasUnidentifiedKeydown() { + return this.previousEvent?.type === "keydown" && this.previousEvent?.key === "Unidentified" + } +} + +const differsInOneSpace = (text1, text2) => Math.abs(text1.length - text2.length) === 1 && normalize(text1) === normalize(text2) +const normalize = (text) => text.replace(/\s+/g, " ") From 696f4a7cc6f145d2866098b579a78d94de441101 Mon Sep 17 00:00:00 2001 From: Alberto Fernandez-Capel Date: Wed, 30 Nov 2022 11:16:54 +0000 Subject: [PATCH 2/2] Tune condition Sometimes the difference between the existing text and the inserted text is more than 1 char, but it's always a long string and they differ in whitespace only. --- .../models/flaky_android_keyboard_detector.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/trix/models/flaky_android_keyboard_detector.js b/src/trix/models/flaky_android_keyboard_detector.js index 73f8b9f87..fd97acf8a 100644 --- a/src/trix/models/flaky_android_keyboard_detector.js +++ b/src/trix/models/flaky_android_keyboard_detector.js @@ -27,7 +27,7 @@ export default class FlakyAndroidKeyboardDetector { // 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.insertTextAfterUnidentifiedChar() && differsInOneSpace(this.element.innerText, this.event.data)) { + if (this.insertingLongTextAfterUnidentifiedChar() && differsInWhitespace(this.element.innerText, this.event.data)) { this.buggyMode = true this.event.preventDefault() } @@ -40,12 +40,12 @@ export default class FlakyAndroidKeyboardDetector { } } - insertTextAfterUnidentifiedChar() { - return this.isBeforeInputInsertText() && this.previousEventWasUnidentifiedKeydown() + insertingLongTextAfterUnidentifiedChar() { + return this.isBeforeInputInsertText() && this.previousEventWasUnidentifiedKeydown() && this.event.data?.length > 100 } isBeforeInputInsertText() { - return this.event.type === "beforeinput" && this.event.inputType === "insertText" && this.event.data + return this.event.type === "beforeinput" && this.event.inputType === "insertText" } previousEventWasUnidentifiedKeydown() { @@ -53,5 +53,8 @@ export default class FlakyAndroidKeyboardDetector { } } -const differsInOneSpace = (text1, text2) => Math.abs(text1.length - text2.length) === 1 && normalize(text1) === normalize(text2) -const normalize = (text) => text.replace(/\s+/g, " ") +const differsInWhitespace = (text1, text2) => { + return normalize(text1) === normalize(text2) +} + +const normalize = (text) => text.replace(/\s+/g, " ").trim()