From b3390dac5f32001a18532748b46a8ffccae938d4 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 28 Oct 2024 12:16:17 +0100 Subject: [PATCH] Fix: duplicated newlines when using dictation on iOS 18+. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a bug that started to happen with iOS 18, where you would get duplicated newlines after completing the dictation. The bug happens because, upon dictation completion, iOS sends `insertParagraph` events and shifts the range offset to the next line, resulting in duplicated newlines. This patch ignores these events unless triggered by an Enter keypress. This workaround is necessary due to the inability to distinguish text entered in dictation mode, as iOS WebKit doesn’t trigger composition events during dictation (https://bugs.webkit.org/show_bug.cgi?id=261764). --- src/test/system/level_2_input_test.js | 4 ++++ .../controllers/level_2_input_controller.js | 18 ++++++++++++--- src/trix/core/helpers/events.js | 22 ++++++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/test/system/level_2_input_test.js b/src/test/system/level_2_input_test.js index 150419b74..bf6884b2c 100644 --- a/src/test/system/level_2_input_test.js +++ b/src/test/system/level_2_input_test.js @@ -100,7 +100,11 @@ testGroup("Level 2 Input", testOptions, () => { test("insertParagraph", async () => { await clickToolbarButton({ attribute: "quote" }) insertString("abc") + + // Insert Enter keydown events to deal with workaround for iOS 18 dictation issues. See `shouldInsertParagraph`. + triggerEvent(document.activeElement, "keydown", { key: "Enter", code: "Enter" }) await performInputTypeUsingExecCommand("insertParagraph", { inputType: "insertParagraph" }) + triggerEvent(document.activeElement, "keydown", { key: "Enter", code: "Enter" }) await performInputTypeUsingExecCommand("insertParagraph", { inputType: "insertParagraph" }) assert.blockAttributes([ 0, 4 ], [ "quote" ]) diff --git a/src/trix/controllers/level_2_input_controller.js b/src/trix/controllers/level_2_input_controller.js index 65b302157..d05f05d70 100644 --- a/src/trix/controllers/level_2_input_controller.js +++ b/src/trix/controllers/level_2_input_controller.js @@ -1,4 +1,4 @@ -import { getAllAttributeNames, squishBreakableWhitespace } from "trix/core/helpers" +import { getAllAttributeNames, shouldInsertParagraph, squishBreakableWhitespace } from "trix/core/helpers" import InputController from "trix/controllers/input_controller" import * as config from "trix/config" @@ -14,6 +14,8 @@ export default class Level2InputController extends InputController { static events = { keydown(event) { + this.saveLastEvent("lastKeydownEvent", event) + if (keyEventIsKeyboardCommand(event)) { const command = keyboardCommandFromKeyEvent(event) if (this.delegate?.inputControllerDidReceiveKeyboardCommand(command)) { @@ -71,6 +73,8 @@ export default class Level2InputController extends InputController { }, beforeinput(event) { + this.saveLastEvent("lastBeforeInputEvent", event) + const handler = this.constructor.inputTypes[event.inputType] // Handles bug with Siri dictation on iOS 18+. @@ -441,8 +445,10 @@ export default class Level2InputController extends InputController { insertParagraph() { this.delegate?.inputControllerWillPerformTyping() - return this.withTargetDOMRange(function() { - return this.responder?.insertLineBreak() + return this.withTargetDOMRange(() => { + if (shouldInsertParagraph(this.lastKeydownEvent, this.lastBeforeInputEvent)) { + return this.responder?.insertLineBreak() + } }) }, @@ -468,6 +474,12 @@ export default class Level2InputController extends InputController { }, } + saveLastEvent(name, event) { + // Native timestamp is not reliable as some browsers do not update it on keydown. + event._timeStamp = Date.now() + this[name] = event + } + elementDidMutate() { if (this.scheduledRender) { if (this.composing) { diff --git a/src/trix/core/helpers/events.js b/src/trix/core/helpers/events.js index 4248830d3..1f525db3c 100644 --- a/src/trix/core/helpers/events.js +++ b/src/trix/core/helpers/events.js @@ -1,6 +1,6 @@ const testTransferData = { "application/x-trix-feature-detection": "test" } -export const dataTransferIsPlainText = function(dataTransfer) { +export const dataTransferIsPlainText = function (dataTransfer) { const text = dataTransfer.getData("text/plain") const html = dataTransfer.getData("text/html") @@ -20,7 +20,7 @@ export const dataTransferIsMsOfficePaste = ({ dataTransfer }) => { dataTransfer.getData("text/html").includes("urn:schemas-microsoft-com:office:office") } -export const dataTransferIsWritable = function(dataTransfer) { +export const dataTransferIsWritable = function (dataTransfer) { if (!dataTransfer?.setData) return false for (const key in testTransferData) { @@ -36,10 +36,26 @@ export const dataTransferIsWritable = function(dataTransfer) { return true } -export const keyEventIsKeyboardCommand = (function() { +export const keyEventIsKeyboardCommand = (function () { if (/Mac|^iP/.test(navigator.platform)) { return (event) => event.metaKey } else { return (event) => event.ctrlKey } })() + +export function shouldInsertParagraph(keydownEvent, beforeInputEvent) { + if (/iPhone|iPad/i.test(navigator.userAgent)) { + // Handle duplicated newlines when using dictation on iOS 18+. Upon dictation completion, iOS sends `insertParagraph` events + // and shifts the range offset to the next line, resulting in duplicated newlines. This patch ignores these events unless triggered + // by an Enter keypress. This workaround is necessary due to the inability to distinguish text entered in dictation mode, as + // iOS WebKit doesn’t trigger composition events during dictation (https://bugs.webkit.org/show_bug.cgi?id=261764). + if (keydownEvent && beforeInputEvent && Math.abs(keydownEvent._timeStamp - beforeInputEvent._timeStamp) < 1000) { + return keydownEvent.key === "Enter" || keydownEvent.key === "Return" + } else { + return false + } + } else { + return true + } +}