1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 18:09:49 +02:00

Fixed autocorrect causing dupe text (#655)

* Refactored out `getPoint` from components/content to utils so as to make it more reusable.

* Fixed autocorrect causing dupe text. (#540)

During autocorrection (in iOS’s Safari), `onSelect` event is triggered after `onBeforeInput` event.
The plugins/core updates the state during `onBeforeInput` event, thus causing selection triggered by autocorrect to be lost/overridden.
This behaviour caused dupe text bug during autocorrection.

To overcome this issue, we try to query the selection and conditionally fix out of sync cases with an additional transform before inserting the text.

* Removes Content#getPoint and use the new utility function instead.

* Renames local variable nextTransform to transform.

* Describe the solution to the autocorrect issue in a more descriptive manner.
This commit is contained in:
Stan Chang Khin Boon
2017-03-24 01:49:23 +08:00
committed by Ian Storm Taylor
parent 3d96d2a309
commit a28075edc1
3 changed files with 80 additions and 40 deletions

View File

@@ -2,7 +2,7 @@
import Base64 from '../serializers/base-64' import Base64 from '../serializers/base-64'
import Debug from 'debug' import Debug from 'debug'
import Node from './node' import Node from './node'
import OffsetKey from '../utils/offset-key' import getPoint from '../utils/get-point'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import Selection from '../models/selection' import Selection from '../models/selection'
@@ -139,36 +139,6 @@ class Content extends React.Component {
} }
} }
/**
* Get a point from a native selection's DOM `element` and `offset`.
*
* @param {Element} element
* @param {Number} offset
* @return {Object}
*/
getPoint(element, offset) {
const { state, editor } = this.props
const { document } = state
const schema = editor.getSchema()
// If we can't find an offset key, we can't get a point.
const offsetKey = OffsetKey.findKey(element, offset)
if (!offsetKey) return null
// COMPAT: If someone is clicking from one Slate editor into another, the
// select event fires two, once for the old editor's `element` first, and
// then afterwards for the correct `element`. (2017/03/03)
const { key } = offsetKey
const node = document.getDescendant(key)
if (!node) return null
const decorators = document.getDescendantDecorators(key, schema)
const ranges = node.getRanges(decorators)
const point = OffsetKey.findPoint(offsetKey, ranges)
return point
}
/** /**
* The React ref method to set the root content element locally. * The React ref method to set the root content element locally.
* *
@@ -424,7 +394,7 @@ class Content extends React.Component {
event.preventDefault() event.preventDefault()
const window = getWindow(event.target) const window = getWindow(event.target)
const { state } = this.props const { state, editor } = this.props
const { nativeEvent } = event const { nativeEvent } = event
const { dataTransfer, x, y } = nativeEvent const { dataTransfer, x, y } = nativeEvent
const data = getTransferData(dataTransfer) const data = getTransferData(dataTransfer)
@@ -441,7 +411,7 @@ class Content extends React.Component {
} }
const { startContainer, startOffset } = range const { startContainer, startOffset } = range
const point = this.getPoint(startContainer, startOffset) const point = getPoint(startContainer, startOffset, state, editor)
if (!point) return if (!point) return
const target = Selection.create({ const target = Selection.create({
@@ -481,16 +451,16 @@ class Content extends React.Component {
debug('onInput', { event }) debug('onInput', { event })
const window = getWindow(event.target) const window = getWindow(event.target)
const { state, editor } = this.props
// Get the selection point. // Get the selection point.
const native = window.getSelection() const native = window.getSelection()
const { anchorNode, anchorOffset } = native const { anchorNode, anchorOffset } = native
const point = this.getPoint(anchorNode, anchorOffset) const point = getPoint(anchorNode, anchorOffset, state, editor)
if (!point) return if (!point) return
// Get the range in question. // Get the range in question.
const { key, index, start, end } = point const { key, index, start, end } = point
const { state, editor } = this.props
const { document, selection } = state const { document, selection } = state
const schema = editor.getSchema() const schema = editor.getSchema()
const decorators = document.getDescendantDecorators(key, schema) const decorators = document.getDescendantDecorators(key, schema)
@@ -652,7 +622,7 @@ class Content extends React.Component {
if (!this.isInContentEditable(event)) return if (!this.isInContentEditable(event)) return
const window = getWindow(event.target) const window = getWindow(event.target)
const { state } = this.props const { state, editor } = this.props
const { document, selection } = state const { document, selection } = state
const native = window.getSelection() const native = window.getSelection()
const data = {} const data = {}
@@ -666,8 +636,8 @@ class Content extends React.Component {
// Otherwise, determine the Slate selection from the native one. // Otherwise, determine the Slate selection from the native one.
else { else {
const { anchorNode, anchorOffset, focusNode, focusOffset } = native const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchor = this.getPoint(anchorNode, anchorOffset) const anchor = getPoint(anchorNode, anchorOffset, state, editor)
const focus = this.getPoint(focusNode, focusOffset) const focus = getPoint(focusNode, focusOffset, state, editor)
if (!anchor || !focus) return if (!anchor || !focus) return
// There are valid situations where a select event will fire when we're // There are valid situations where a select event will fire when we're

View File

@@ -3,6 +3,7 @@ import Base64 from '../serializers/base-64'
import Content from '../components/content' import Content from '../components/content'
import Character from '../models/character' import Character from '../models/character'
import Debug from 'debug' import Debug from 'debug'
import getPoint from '../utils/get-point'
import Placeholder from '../components/placeholder' import Placeholder from '../components/placeholder'
import React from 'react' import React from 'react'
import getWindow from 'get-window' import getWindow from 'get-window'
@@ -97,9 +98,38 @@ function Plugin(options = {}) {
const chars = initialChars.insert(startOffset, char) const chars = initialChars.insert(startOffset, char)
let transform = state.transform()
// COMPAT: In iOS, when choosing from the predictive text suggestions, the
// native selection will be changed to span the existing word, so that the word
// is replaced. But the `select` event for this change doesn't fire until after
// the `beforeInput` event, even though the native selection is updated. So we
// need to manually adjust the selection to be in sync. (03/18/2017)
const window = getWindow(event.target)
const native = window.getSelection()
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchorPoint = getPoint(anchorNode, anchorOffset, state, editor)
const focusPoint = getPoint(focusNode, focusOffset, state, editor)
if (anchorPoint && focusPoint) {
const { selection } = state
if (
selection.anchorKey !== anchorPoint.key ||
selection.anchorOffset !== anchorPoint.offset ||
selection.focusKey !== focusPoint.key ||
selection.focusOffset !== focusPoint.offset
) {
transform = transform
.select({
anchorKey: anchorPoint.key,
anchorOffset: anchorPoint.offset,
focusKey: focusPoint.key,
focusOffset: focusPoint.offset
})
}
}
// Determine what the characters should be, if not natively inserted. // Determine what the characters should be, if not natively inserted.
let next = state let next = transform
.transform()
.insertText(e.data) .insertText(e.data)
.apply() .apply()

40
src/utils/get-point.js Normal file
View File

@@ -0,0 +1,40 @@
import OffsetKey from './offset-key'
/**
* Get a point from a native selection's DOM `element` and `offset`.
*
* @param {Element} element
* @param {Number} offset
* @param {State} state
* @param {Editor} editor
* @return {Object}
*/
function getPoint(element, offset, state, editor) {
const { document } = state
const schema = editor.getSchema()
// If we can't find an offset key, we can't get a point.
const offsetKey = OffsetKey.findKey(element, offset)
if (!offsetKey) return null
// COMPAT: If someone is clicking from one Slate editor into another, the
// select event fires two, once for the old editor's `element` first, and
// then afterwards for the correct `element`. (2017/03/03)
const { key } = offsetKey
const node = document.getDescendant(key)
if (!node) return null
const decorators = document.getDescendantDecorators(key, schema)
const ranges = node.getRanges(decorators)
const point = OffsetKey.findPoint(offsetKey, ranges)
return point
}
/**
* Export.
*
* @type {Function}
*/
export default getPoint