From cd9563c4561f928460f832340110ee738e98965b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Fri, 2 Feb 2024 17:04:26 +0000 Subject: [PATCH] Use the target range to insert the replacement text When the browser auto-corrects a misspelled word, it dispatches a `beforeinput` event with the `insertReplacementText` input type and a `dataTransfer` object containing the replacement text. The event also contains a `getTargetRanges` method that returns an array of `DOMRange` objects representing the range of text that will be replaced. When the `Level2InputController` was originally implemented, not all browsers supported the `getTargetRanges` method, so it was not used. Now that all supported browsers do support `getTargetRanges`, we can use it to insert the replacement text at the correct location. This ensures editor state is updated correctly the cursor is positioned correctly. Ref. - https://w3c.github.io/input-events/#overview (search for "insertReplacementText") - https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/getTargetRanges --- src/test/system/level_2_input_test.js | 11 +++++++++-- src/trix/controllers/level_2_input_controller.js | 8 ++++++-- src/trix/models/selection_manager.js | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/test/system/level_2_input_test.js b/src/test/system/level_2_input_test.js index c2cfaccd7..98d03f1d6 100644 --- a/src/test/system/level_2_input_test.js +++ b/src/test/system/level_2_input_test.js @@ -139,15 +139,22 @@ testGroup("Level 2 Input", testOptions, () => { }) // https://input-inspector.now.sh/profiles/hVXS1cHYFvc2EfdRyTWQ - test("correcting a misspelled word in Chrome", async () => { + test("correcting a misspelled word", async () => { insertString("onr") getComposition().setSelectedRange([ 0, 3 ]) await nextFrame() const inputType = "insertReplacementText" const dataTransfer = createDataTransfer({ "text/plain": "one" }) - const event = createEvent("beforeinput", { inputType, dataTransfer }) + + const targetRange = document.createRange() + const textNode = getEditorElement().firstElementChild.lastChild + targetRange.setStart(textNode, 0) + targetRange.setEnd(textNode, 3) + + const event = createEvent("beforeinput", { inputType, dataTransfer, getTargetRanges: () => [ targetRange ] }) document.activeElement.dispatchEvent(event) + await nextFrame() expectDocument("one\n") }) diff --git a/src/trix/controllers/level_2_input_controller.js b/src/trix/controllers/level_2_input_controller.js index 25afe9bf6..db10fb381 100644 --- a/src/trix/controllers/level_2_input_controller.js +++ b/src/trix/controllers/level_2_input_controller.js @@ -443,8 +443,12 @@ export default class Level2InputController extends InputController { }, insertReplacementText() { - this.insertString(this.event.dataTransfer.getData("text/plain"), { updatePosition: false }) - this.requestRender() + const replacement = this.event.dataTransfer.getData("text/plain") + const domRange = this.event.getTargetRanges()[0] + + this.withTargetDOMRange(domRange, () => { + this.insertString(replacement, { updatePosition: false }) + }) }, insertText() { diff --git a/src/trix/models/selection_manager.js b/src/trix/models/selection_manager.js index a777e8807..0e0284d5f 100644 --- a/src/trix/models/selection_manager.js +++ b/src/trix/models/selection_manager.js @@ -29,6 +29,7 @@ export default class SelectionManager extends BasicObject { this.lockCount = 0 handleEvent("mousedown", { onElement: this.element, withCallback: this.didMouseDown }) } + getLocationRange(options = {}) { if (options.strict === false) { return this.createLocationRangeFromDOMRange(getDOMRange())