Skip to content

Commit

Permalink
Use the target range to insert the replacement text
Browse files Browse the repository at this point in the history
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
  • Loading branch information
afcapel committed Feb 2, 2024
1 parent 10573a8 commit cd9563c
Show file tree
Hide file tree
Showing 3 changed files with 16 additions and 4 deletions.
11 changes: 9 additions & 2 deletions src/test/system/level_2_input_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
Expand Down
8 changes: 6 additions & 2 deletions src/trix/controllers/level_2_input_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions src/trix/models/selection_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit cd9563c

Please sign in to comment.