From a28075edc14fa58908ede83304779a389192bd0f Mon Sep 17 00:00:00 2001 From: Stan Chang Khin Boon Date: Fri, 24 Mar 2017 01:49:23 +0800 Subject: [PATCH] Fixed autocorrect causing dupe text (#655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- src/components/content.js | 46 +++++++-------------------------------- src/plugins/core.js | 34 +++++++++++++++++++++++++++-- src/utils/get-point.js | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 40 deletions(-) create mode 100644 src/utils/get-point.js diff --git a/src/components/content.js b/src/components/content.js index 60ca0a964..e7379a64a 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -2,7 +2,7 @@ import Base64 from '../serializers/base-64' import Debug from 'debug' import Node from './node' -import OffsetKey from '../utils/offset-key' +import getPoint from '../utils/get-point' import React from 'react' import ReactDOM from 'react-dom' 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. * @@ -424,7 +394,7 @@ class Content extends React.Component { event.preventDefault() const window = getWindow(event.target) - const { state } = this.props + const { state, editor } = this.props const { nativeEvent } = event const { dataTransfer, x, y } = nativeEvent const data = getTransferData(dataTransfer) @@ -441,7 +411,7 @@ class Content extends React.Component { } const { startContainer, startOffset } = range - const point = this.getPoint(startContainer, startOffset) + const point = getPoint(startContainer, startOffset, state, editor) if (!point) return const target = Selection.create({ @@ -481,16 +451,16 @@ class Content extends React.Component { debug('onInput', { event }) const window = getWindow(event.target) + const { state, editor } = this.props // Get the selection point. const native = window.getSelection() const { anchorNode, anchorOffset } = native - const point = this.getPoint(anchorNode, anchorOffset) + const point = getPoint(anchorNode, anchorOffset, state, editor) if (!point) return // Get the range in question. const { key, index, start, end } = point - const { state, editor } = this.props const { document, selection } = state const schema = editor.getSchema() const decorators = document.getDescendantDecorators(key, schema) @@ -652,7 +622,7 @@ class Content extends React.Component { if (!this.isInContentEditable(event)) return const window = getWindow(event.target) - const { state } = this.props + const { state, editor } = this.props const { document, selection } = state const native = window.getSelection() const data = {} @@ -666,8 +636,8 @@ class Content extends React.Component { // Otherwise, determine the Slate selection from the native one. else { const { anchorNode, anchorOffset, focusNode, focusOffset } = native - const anchor = this.getPoint(anchorNode, anchorOffset) - const focus = this.getPoint(focusNode, focusOffset) + const anchor = getPoint(anchorNode, anchorOffset, state, editor) + const focus = getPoint(focusNode, focusOffset, state, editor) if (!anchor || !focus) return // There are valid situations where a select event will fire when we're diff --git a/src/plugins/core.js b/src/plugins/core.js index 01a03e47b..94bf30c30 100644 --- a/src/plugins/core.js +++ b/src/plugins/core.js @@ -3,6 +3,7 @@ import Base64 from '../serializers/base-64' import Content from '../components/content' import Character from '../models/character' import Debug from 'debug' +import getPoint from '../utils/get-point' import Placeholder from '../components/placeholder' import React from 'react' import getWindow from 'get-window' @@ -97,9 +98,38 @@ function Plugin(options = {}) { 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. - let next = state - .transform() + let next = transform .insertText(e.data) .apply() diff --git a/src/utils/get-point.js b/src/utils/get-point.js new file mode 100644 index 000000000..d35f9b487 --- /dev/null +++ b/src/utils/get-point.js @@ -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