1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-17 20:51:20 +02:00

refactor native beforeinput handling (#2063)

* refactor native beforeinput handling

* fix lint
This commit is contained in:
Ian Storm Taylor
2018-08-09 01:06:31 -07:00
committed by GitHub
parent a396d013ef
commit f812816b7d
4 changed files with 164 additions and 148 deletions

View File

@@ -2,12 +2,7 @@ import Debug from 'debug'
import React from 'react'
import Types from 'prop-types'
import getWindow from 'get-window'
import {
IS_FIREFOX,
IS_IOS,
IS_ANDROID,
SUPPORTED_EVENTS,
} from 'slate-dev-environment'
import { IS_FIREFOX, HAS_INPUT_EVENTS_LEVEL_2 } from 'slate-dev-environment'
import logger from 'slate-dev-logger'
import throttle from 'lodash/throttle'
@@ -97,9 +92,10 @@ class Content extends React.Component {
this.onNativeSelectionChange
)
// COMPAT: Restrict scope of `beforeinput` to mobile.
if ((IS_IOS || IS_ANDROID) && SUPPORTED_EVENTS.beforeinput) {
this.element.addEventListener('beforeinput', this.onNativeBeforeInput)
// COMPAT: Restrict scope of `beforeinput` to clients that support the
// Input Events Level 2 spec, since they are preventable events.
if (HAS_INPUT_EVENTS_LEVEL_2) {
this.element.addEventListener('beforeinput', this.onBeforeInput)
}
this.updateSelection()
@@ -119,9 +115,8 @@ class Content extends React.Component {
)
}
// COMPAT: Restrict scope of `beforeinput` to mobile.
if ((IS_IOS || IS_ANDROID) && SUPPORTED_EVENTS.beforeinput) {
this.element.removeEventListener('beforeinput', this.onNativeBeforeInput)
if (HAS_INPUT_EVENTS_LEVEL_2) {
this.element.removeEventListener('beforeinput', this.onBeforeInput)
}
}
@@ -342,83 +337,6 @@ class Content extends React.Component {
this.props[handler](event)
}
/**
* On a native `beforeinput` event, use the additional range information
* provided by the event to manipulate text exactly as the browser would.
*
* This is currently only used on iOS and Android.
*
* @param {InputEvent} event
*/
onNativeBeforeInput = event => {
if (this.props.readOnly) return
if (!this.isInEditor(event.target)) return
const [targetRange] = event.getTargetRanges()
if (!targetRange) return
const { editor } = this.props
switch (event.inputType) {
case 'deleteContentBackward': {
event.preventDefault()
const range = findRange(targetRange, editor.value)
editor.change(change => change.deleteAtRange(range))
break
}
case 'insertLineBreak': // intentional fallthru
case 'insertParagraph': {
event.preventDefault()
const range = findRange(targetRange, editor.value)
editor.change(change => {
if (change.value.isInVoid) {
change.moveToStartOfNextText()
} else {
change.splitBlockAtRange(range)
}
})
break
}
case 'insertReplacementText': // intentional fallthru
case 'insertText': {
// `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
event.preventDefault()
const { value } = editor
const { selection } = value
const range = findRange(targetRange, value)
editor.change(change => {
change.insertTextAtRange(range, text, selection.marks)
// If the text was successfully inserted, and the selection had marks
// on it, unset the selection's marks.
if (selection.marks && value.document != change.value.document) {
change.select({ marks: null })
}
})
break
}
}
}
/**
* On native `selectionchange` event, trigger the `onSelect` handler. This is
* needed to account for React's `onSelect` being non-standard and not firing

View File

@@ -36,7 +36,7 @@ function AfterPlugin() {
let isDraggingInternally = null
/**
* On before input, correct any browser inconsistencies.
* On before input.
*
* @param {Event} event
* @param {Change} change
@@ -46,8 +46,96 @@ function AfterPlugin() {
function onBeforeInput(event, change, editor) {
debug('onBeforeInput', { event })
const isSynthetic = !!event.nativeEvent
// If the event is synthetic, it's React's polyfill of `beforeinput` that
// isn't a true `beforeinput` event with meaningful information. It only
// gets triggered for character insertions, so we can just insert directly.
if (isSynthetic) {
event.preventDefault()
change.insertText(event.data)
return
}
// Otherwise, we can use the information in the `beforeinput` event to
// figure out the exact change that will occur, and prevent it.
const [targetRange] = event.getTargetRanges()
if (!targetRange) return
event.preventDefault()
change.insertText(event.data)
const { value } = change
const { selection } = value
const range = findRange(targetRange, value)
switch (event.inputType) {
case 'deleteByDrag':
case 'deleteByCut':
case 'deleteContent':
case 'deleteContentBackward':
case 'deleteContentForward': {
change.deleteAtRange(range)
return
}
case 'deleteWordBackward': {
change.deleteWordBackwardAtRange(range)
return
}
case 'deleteWordForward': {
change.deleteWordForwardAtRange(range)
return
}
case 'deleteSoftLineBackward':
case 'deleteHardLineBackward': {
change.deleteLineBackwardAtRange(range)
return
}
case 'deleteSoftLineForward':
case 'deleteHardLineForward': {
change.deleteLineForwardAtRange(range)
return
}
case 'insertLineBreak':
case 'insertParagraph': {
if (change.value.isInVoid) {
change.moveToStartOfNextText()
} else {
change.splitBlockAtRange(range)
}
return
}
case 'insertFromYank':
case 'insertReplacementText':
case 'insertText': {
// COMPAT: `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`. (2018/08/09)
const text =
event.data == null
? event.dataTransfer.getData('text/plain')
: event.data
if (text == null) return
change.insertTextAtRange(range, text, selection.marks)
// If the text was successfully inserted, and the selection had marks
// on it, unset the selection's marks.
if (selection.marks && value.document != change.value.document) {
change.select({ marks: null })
}
return
}
}
}
/**

View File

@@ -6,8 +6,7 @@ import {
IS_FIREFOX,
IS_IE,
IS_IOS,
IS_ANDROID,
SUPPORTED_EVENTS,
HAS_INPUT_EVENTS_LEVEL_2,
} from 'slate-dev-environment'
import findNode from '../utils/find-node'
@@ -44,15 +43,12 @@ function BeforePlugin() {
function onBeforeInput(event, change, editor) {
if (editor.props.readOnly) return true
// COMPAT: 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.
// Since native `onbeforeinput` mainly benefits autocorrect and spellcheck
// for mobile, on desktop it brings IME issue, limit its scope for now.
if ((IS_IOS || IS_ANDROID) && SUPPORTED_EVENTS.beforeinput) return true
const isSynthetic = !!event.nativeEvent
// COMPAT: If the browser supports Input Events Level 2, we will have
// attached a custom handler for the real `beforeinput` events, instead of
// allowing React's synthetic polyfill, so we need to ignore synthetics.
if (isSynthetic && HAS_INPUT_EVENTS_LEVEL_2) return true
debug('onBeforeInput', { event })
}