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:
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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 })
|
||||
}
|
||||
|
Reference in New Issue
Block a user