diff --git a/src/trix/controllers/level_2_input_controller.js b/src/trix/controllers/level_2_input_controller.js index 65b302157..5cf79c840 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, shouldRenderInmmediately, squishBreakableWhitespace } from "trix/core/helpers" import InputController from "trix/controllers/input_controller" import * as config from "trix/config" @@ -80,7 +80,12 @@ export default class Level2InputController extends InputController { if (handler) { this.withEvent(event, handler) - this.scheduleRender() + + if (shouldRenderInmmediately(event)) { + this.render() + } else { + this.scheduleRender() + } } }, diff --git a/src/trix/core/helpers/events.js b/src/trix/core/helpers/events.js index 4248830d3..35b7253dc 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,23 @@ 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 shouldRenderInmmediately(inputEvent) { + if (/iPhone|iPad/i.test(navigator.userAgent)) { + // Handle duplicated newlines when using dictation on iOS 18+. Upon dictation completion, iOS sends + // the list of insertText / insertParagraph events in a quick sequence. For newlines, if we don't render + // the editor synchronously, the internal range fails to update and results in duplicated newlines. + // This workaround is necessary because iOS doesn't send composing events as expected while dictating: + // https://bugs.webkit.org/show_bug.cgi?id=261764 + return inputEvent.inputType === "insertParagraph" + } else { + return false + } +}