Skip to content

Commit

Permalink
Use native beforeinput events to handle text insertion when possible
Browse files Browse the repository at this point in the history
In browsers that support it (currently only Safari has full support),
the native `beforeinput` DOM event provides much more useful information
about text insertion than React's synthetic `onBeforeInput` event.

By handling text insertion with the native event instead of the
synthetic event when possible, we can fully support autocorrect,
spellcheck replacements, and related functionality on iOS without
resorting to hacks.

See the discussion in ianstormtaylor#1177 for more background on this change.

Fixes ianstormtaylor#1176
Fixes ianstormtaylor#1177
  • Loading branch information
rgrove committed Oct 15, 2017
1 parent 1fac036 commit 25977d4
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 19 deletions.
55 changes: 54 additions & 1 deletion packages/slate-react/src/components/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
import getTransferData from '../utils/get-transfer-data'
import scrollToSelection from '../utils/scroll-to-selection'
import setTransferData from '../utils/set-transfer-data'
import { IS_FIREFOX, IS_MAC, IS_IE } from '../constants/environment'
import { IS_FIREFOX, IS_MAC, IS_IE, SUPPORTED_EVENTS } from '../constants/environment'

/**
* Debug.
Expand Down Expand Up @@ -96,18 +96,33 @@ class Content extends React.Component {
/**
* When the editor first mounts in the DOM we need to:
*
* - Add native DOM event listeners.
* - Update the selection, in case it starts focused.
* - Focus the editor if `autoFocus` is set.
*/

componentDidMount = () => {
if (SUPPORTED_EVENTS.beforeinput) {
this.element.addEventListener('beforeinput', this.onNativeBeforeInput)
}

this.updateSelection()

if (this.props.autoFocus) {
this.element.focus()
}
}

/**
* When unmounting, remove DOM event listeners.
*/

componentWillUnmount() {
if (SUPPORTED_EVENTS.beforeinput) {
this.element.removeEventListener('beforeinput', this.onNativeBeforeInput)
}
}

/**
* On update, update the selection.
*/
Expand Down Expand Up @@ -224,6 +239,44 @@ class Content extends React.Component {
this.props.onBeforeInput(event, data)
}

/**
* On a native `beforeinput` event, use the additional range information
* provided by the event to insert text exactly as the browser would.
*
* @param {InputEvent} event
*/

onNativeBeforeInput = (event) => {
if (this.props.readOnly) return
if (!this.isInEditor(event.target)) return

const { inputType } = event
if (inputType !== 'insertText' && inputType !== 'insertReplacementText') return

const [ targetRange ] = event.getTargetRanges()
if (!targetRange) return

// `data` should have the text for the `insertText` input type and
// `dataTransfer` should have the text for the `insertReplacementText` input
// type, but Safari uses `insertText` for spell check replacements and sets
// `data` to `null`.
const text = event.data == null
? event.dataTransfer.getData('text/plain')
: event.data

if (text == null) return

debug('onNativeBeforeInput', { event, text })
event.preventDefault()

const { editor, state } = this.props
const range = findRange(targetRange, state)

editor.change((change) => {
change.insertTextAtRange(range, text)
})
}

/**
* On blur, update the selection to be not focused.
*
Expand Down
27 changes: 9 additions & 18 deletions packages/slate-react/src/plugins/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Content from '../components/content'
import Placeholder from '../components/placeholder'
import findDOMNode from '../utils/find-dom-node'
import findRange from '../utils/find-range'
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
import { IS_CHROME, IS_MAC, IS_SAFARI, SUPPORTED_EVENTS } from '../constants/environment'

/**
* Debug.
Expand Down Expand Up @@ -68,25 +68,16 @@ function Plugin(options = {}) {

function onBeforeInput(e, data, change) {
debug('onBeforeInput', { data })
e.preventDefault()

const { state } = change
const { selection } = state

// COMPAT: In iOS, when using predictive text suggestions, the native
// selection will be changed to span the existing word, so that the word is
// replaced. But the `select` fires after the `beforeInput` event, even
// though the native selection is updated. So we need to manually check if
// the selection has gotten out of sync, and adjust it if so. (03/18/2017)
const window = getWindow(e.target)
const native = window.getSelection()
const range = findRange(native, state)
const hasMismatch = range && !range.equals(selection)

if (hasMismatch) {
change.select(range)
}
// React's `onBeforeInput` synthetic event is based on the native `keypress`
// and `textInput` events. In browsers that support the native `beforeinput`
// event, we instead use that event to trigger text insertion, since it
// provides more useful information about the range being affected and also
// preserves compatibility with iOS autocorrect, which would be broken if we
// called `preventDefault()` on React's synthetic event here.
if (SUPPORTED_EVENTS.beforeinput) return

e.preventDefault()
change.insertText(e.data)
}

Expand Down

0 comments on commit 25977d4

Please sign in to comment.