diff --git a/src/component/base/DraftEditor.react.js b/src/component/base/DraftEditor.react.js index 04e7c4422f..4e362f4d43 100644 --- a/src/component/base/DraftEditor.react.js +++ b/src/component/base/DraftEditor.react.js @@ -42,6 +42,7 @@ const editOnSelect = require('editOnSelect'); const getScrollPosition = require('getScrollPosition'); const invariant = require('invariant'); const nullthrows = require('nullthrows'); +const areLevel2InputEventsSupported = require('areLevel2InputEventsSupported'); const isIE = UserAgent.isBrowser('IE'); @@ -96,6 +97,7 @@ class DraftEditor extends React.Component { _renderNativeContent: boolean; _updatedNativeInsertionBlock: boolean; _latestCommittedEditorState: EditorState; + _useNativeBeforeInput: boolean; /** * Define proxies that can route events to the current handler. @@ -136,6 +138,8 @@ class DraftEditor extends React.Component { constructor(props: DraftEditorProps) { super(props); + this._useNativeBeforeInput = props.useNativeBeforeInputIfAble && areLevel2InputEventsSupported(); + this._blockSelectEvents = false; this._clipboard = null; this._handler = null; @@ -235,15 +239,28 @@ class DraftEditor extends React.Component { // it is not possible to set up an onPaste handler through react. // Manually use addEventListener and removeEventListener below. // See the comments in editOnPaste for why this is needed. + // + // We also provide an option to manually manage our own onBeforeInput handler + // without going through React. React polyfills this event using `textInput`/`keypress`, + // but doesn't use the natively-available event when it can (see https://github.com/facebook/react/issues/11211) + // In rare circumstances, we want to provide the option to force the use of the native + // `beforeinput`, event. Slate does something similar https://github.com/ianstormtaylor/slate/commit/f812816b7dcb2d4b2efa0d4ba12d4feac31850c9 if (this._editor) { const editorNode = ReactDOM.findDOMNode(this._editor); editorNode.removeEventListener('paste', this._onPaste); + if (this._useNativeBeforeInput) { + editorNode.removeEventListener('beforeinput', this._onBeforeInput); + } } this._editor = ref; if (this._editor) { const editorNode = ReactDOM.findDOMNode(this._editor); + editorNode.addEventListener('paste', this._onPaste); + if (this._useNativeBeforeInput) { + editorNode.addEventListener('beforeinput', this._onBeforeInput); + } // Add ignore attribute for an IESpell, an obscure plugin that doesn't respect spellcheck="false" on a // contenteditable div @@ -307,7 +324,7 @@ class DraftEditor extends React.Component { })} contentEditable={!readOnly} data-testid={this.props.webDriverTestID} - onBeforeInput={this._onBeforeInput} + onBeforeInput={this._useNativeBeforeInput ? undefined : this._onBeforeInput} onBlur={this._onBlur} onCompositionEnd={this._onCompositionEnd} onCompositionStart={this._onCompositionStart} diff --git a/src/component/base/DraftEditorProps.js b/src/component/base/DraftEditorProps.js index 08b51d893c..6d874dee09 100644 --- a/src/component/base/DraftEditorProps.js +++ b/src/component/base/DraftEditorProps.js @@ -175,6 +175,8 @@ export type DraftEditorProps = { // an element tag and an optional react element wrapper. This configuration // is used for both rendering and paste processing. blockRenderMap: DraftBlockRenderMap, + + useNativeBeforeInputIfAble?: boolean, }; export type DraftEditorDefaultProps = { @@ -185,4 +187,5 @@ export type DraftEditorDefaultProps = { readOnly: boolean, spellCheck: boolean, stripPastedStyles: boolean, + useNativeBeforeInputIfAble: false, }; diff --git a/src/component/handlers/edit/editOnBeforeInput.js b/src/component/handlers/edit/editOnBeforeInput.js index 70a7187c1b..3b16e51089 100644 --- a/src/component/handlers/edit/editOnBeforeInput.js +++ b/src/component/handlers/edit/editOnBeforeInput.js @@ -78,7 +78,7 @@ function replaceText( * preserve spellcheck highlighting, which disappears or flashes if re-render * occurs on the relevant text nodes. */ -function editOnBeforeInput(editor: DraftEditor, e: SyntheticInputEvent): void { +function editOnBeforeInput(editor: DraftEditor, e: InputEvent | SyntheticInputEvent): void { // React doesn't fire a selection event until mouseUp, so it's possible to // click to change selection, hold the mouse down, and type a character diff --git a/src/component/utils/areLevel2InputEventsSupported.js b/src/component/utils/areLevel2InputEventsSupported.js new file mode 100644 index 0000000000..6e3dd98bfc --- /dev/null +++ b/src/component/utils/areLevel2InputEventsSupported.js @@ -0,0 +1,21 @@ +/** + * @providesModule areLevel2InputEventsSupported + * @typechecks + * @flow + * + * This method determines if we're in a browser which fires native 'beforeinput' + * events (Level 1 support) and allows that event to be cancellable (Level 2 support) + * + * It's borrowed from Slate https://github.com/ianstormtaylor/slate/blob/9694b228464d8b4d874074496a4c0a50f6ec4614/packages/slate-dev-environment/src/index.js#L74-L79 + */ + +'use strict'; + +function areLevel2InputEventsSupported(): boolean { + const element = window.document.createElement('div'); + element.contentEditable = true; + const support = 'onbeforeinput' in element; + return support; +} + +module.exports = areLevel2InputEventsSupported;