diff --git a/src/trix/controllers/input_controller.js b/src/trix/controllers/input_controller.js index 29187b646..5c3c0d032 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 SamsungAndroidKeyboardDetector from "../models/samsung_android_keybpard_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.samsungKeyboardDetector = new SamsungAndroidKeyboardDetector(this.element) for (const eventName in this.constructor.events) { handleEvent(eventName, { onElement: this.element, withCallback: this.handlerFor(eventName) }) } @@ -55,6 +57,9 @@ export default class InputController extends BasicObject { if (!event.defaultPrevented) { this.handleInput(() => { if (!innerElementIsActive(this.element)) { + this.samsungKeyboardDetector.process(event) + if (this.samsungKeyboardDetector.buggyMode) 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/samsung_android_keybpard_detector.js b/src/trix/models/samsung_android_keybpard_detector.js new file mode 100644 index 000000000..ca7d66839 --- /dev/null +++ b/src/trix/models/samsung_android_keybpard_detector.js @@ -0,0 +1,41 @@ +import * as config from "trix/config" + +// 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. This class detects when that flurry of events starts and ends so we can +// ignore those events in the input controller. +export default class SamsungAndroidKeyboardDetector { + constructor(element) { + this.element = element + } + + process(event) { + this.previousEvent = this.event + this.event = event + + this.checkBuggyModeStart() + this.checkBuggyModeEnd() + } + + // The buggy mode always starts with an insertText event with the same text as the editor element, except for an extra new line + // after the cursor + checkBuggyModeStart() { + if (config.browser.samsungAndroid && this.insertTextAfterUnidentifiedChar() && differsInOneCR(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 is over. + checkBuggyModeEnd() { + if (this.buggyMode && this.event.inputType !== "insertText") { + this.buggyMode = false + } + } + + insertTextAfterUnidentifiedChar() { + return this.event.type === "beforeinput" && this.event.inputType === "insertText" && this.event.data && this.previousEvent.key === "Unidentified" + } +} + +const differsInOneCR = (text1, text2) => Math.abs(text1.length - text2.length) === 1 && normalize(text1) === normalize(text2) +const normalize = (text) => text.replace(/\s+/g, " ")