mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-18 21:21:21 +02:00
Use native beforeinput
events to handle text insertion when possible (#1232)
* Add support for finding a Slate range from a native StaticRange * Add a `SUPPORTED_EVENTS` environment constant This is an object mapping of DOM event names to booleans indicating whether the browser supports that event. * Use native `beforeinput` events to handle text insertion when possible 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 #1177 for more background on this change. Fixes #1176 Fixes #1177 * Fix lint error.
This commit is contained in:
committed by
Ian Storm Taylor
parent
c64673f7af
commit
6378c12a98
@@ -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.
|
||||
@@ -96,11 +96,16 @@ 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) {
|
||||
@@ -108,6 +113,16 @@ class Content extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When unmounting, remove DOM event listeners.
|
||||
*/
|
||||
|
||||
componentWillUnmount() {
|
||||
if (SUPPORTED_EVENTS.beforeinput) {
|
||||
this.element.removeEventListener('beforeinput', this.onNativeBeforeInput)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On update, update the selection.
|
||||
*/
|
||||
@@ -226,6 +241,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.
|
||||
*
|
||||
|
@@ -20,6 +20,16 @@ const BROWSER_RULES = [
|
||||
['safari', /Version\/([0-9\._]+).*Safari/],
|
||||
]
|
||||
|
||||
/**
|
||||
* DOM event matching rules.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const EVENT_RULES = [
|
||||
['beforeinput', el => 'onbeforeinput' in el]
|
||||
]
|
||||
|
||||
/**
|
||||
* Operating system matching rules.
|
||||
*
|
||||
@@ -39,6 +49,7 @@ const OS_RULES = [
|
||||
*/
|
||||
|
||||
let BROWSER
|
||||
const EVENTS = {}
|
||||
let OS
|
||||
|
||||
/**
|
||||
@@ -63,6 +74,14 @@ if (browser) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const testEl = document.createElement('div')
|
||||
testEl.contentEditable = true
|
||||
|
||||
for (let i = 0; i < EVENT_RULES.length; i++) {
|
||||
const [ name, testFn ] = EVENT_RULES[i]
|
||||
EVENTS[name] = testFn(testEl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,3 +98,5 @@ export const IS_IE = BROWSER === 'ie'
|
||||
export const IS_IOS = OS === 'ios'
|
||||
export const IS_MAC = OS === 'macos'
|
||||
export const IS_WINDOWS = OS === 'windows'
|
||||
|
||||
export const SUPPORTED_EVENTS = EVENTS
|
||||
|
@@ -9,8 +9,7 @@ import { Block, Inline, coreSchema } from 'slate'
|
||||
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_IOS, IS_MAC, IS_SAFARI } from '../constants/environment'
|
||||
import { IS_CHROME, IS_MAC, IS_SAFARI, SUPPORTED_EVENTS } from '../constants/environment'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -68,27 +67,16 @@ function Plugin(options = {}) {
|
||||
|
||||
function onBeforeInput(e, data, change) {
|
||||
debug('onBeforeInput', { data })
|
||||
|
||||
// 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()
|
||||
|
||||
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. (10/16/2017)
|
||||
if (IS_IOS) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
change.insertText(e.data)
|
||||
}
|
||||
|
||||
|
@@ -17,9 +17,9 @@ function findRange(native, state) {
|
||||
const el = native.anchorNode || native.startContainer
|
||||
const window = getWindow(el)
|
||||
|
||||
// If the `native` object is a DOM `Range` object, change it into something
|
||||
// that looks like a DOM `Selection` instead.
|
||||
if (native instanceof window.Range) {
|
||||
// If the `native` object is a DOM `Range` or `StaticRange` object, change it
|
||||
// into something that looks like a DOM `Selection` instead.
|
||||
if (native instanceof window.Range || (window.StaticRange && native instanceof window.StaticRange)) {
|
||||
native = {
|
||||
anchorNode: native.startContainer,
|
||||
anchorOffset: native.startOffset,
|
||||
|
Reference in New Issue
Block a user