177 lines
6.4 KiB
Plaintext
177 lines
6.4 KiB
Plaintext
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* @providesModule DraftEditorCompositionHandler
|
|
* @format
|
|
* @flow
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import type DraftEditor from './DraftEditor.react';
|
|
|
|
const DraftFeatureFlags = require('./DraftFeatureFlags');
|
|
const DraftModifier = require('./DraftModifier');
|
|
const EditorState = require('./EditorState');
|
|
const Keys = require('fbjs/lib/Keys');
|
|
|
|
const getEntityKeyForSelection = require('./getEntityKeyForSelection');
|
|
const isEventHandled = require('./isEventHandled');
|
|
const isSelectionAtLeafStart = require('./isSelectionAtLeafStart');
|
|
|
|
/**
|
|
* Millisecond delay to allow `compositionstart` to fire again upon
|
|
* `compositionend`.
|
|
*
|
|
* This is used for Korean input to ensure that typing can continue without
|
|
* the editor trying to render too quickly. More specifically, Safari 7.1+
|
|
* triggers `compositionstart` a little slower than Chrome/FF, which
|
|
* leads to composed characters being resolved and re-render occurring
|
|
* sooner than we want.
|
|
*/
|
|
const RESOLVE_DELAY = 20;
|
|
|
|
/**
|
|
* A handful of variables used to track the current composition and its
|
|
* resolution status. These exist at the module level because it is not
|
|
* possible to have compositions occurring in multiple editors simultaneously,
|
|
* and it simplifies state management with respect to the DraftEditor component.
|
|
*/
|
|
let resolved = false;
|
|
let stillComposing = false;
|
|
let textInputData = '';
|
|
|
|
var DraftEditorCompositionHandler = {
|
|
onBeforeInput: function (editor: DraftEditor, e: SyntheticInputEvent<>): void {
|
|
textInputData = (textInputData || '') + e.data;
|
|
},
|
|
|
|
/**
|
|
* A `compositionstart` event has fired while we're still in composition
|
|
* mode. Continue the current composition session to prevent a re-render.
|
|
*/
|
|
onCompositionStart: function (editor: DraftEditor): void {
|
|
stillComposing = true;
|
|
},
|
|
|
|
/**
|
|
* Attempt to end the current composition session.
|
|
*
|
|
* Defer handling because browser will still insert the chars into active
|
|
* element after `compositionend`. If a `compositionstart` event fires
|
|
* before `resolveComposition` executes, our composition session will
|
|
* continue.
|
|
*
|
|
* The `resolved` flag is useful because certain IME interfaces fire the
|
|
* `compositionend` event multiple times, thus queueing up multiple attempts
|
|
* at handling the composition. Since handling the same composition event
|
|
* twice could break the DOM, we only use the first event. Example: Arabic
|
|
* Google Input Tools on Windows 8.1 fires `compositionend` three times.
|
|
*/
|
|
onCompositionEnd: function (editor: DraftEditor): void {
|
|
resolved = false;
|
|
stillComposing = false;
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
DraftEditorCompositionHandler.resolveComposition(editor);
|
|
}
|
|
}, RESOLVE_DELAY);
|
|
},
|
|
|
|
/**
|
|
* In Safari, keydown events may fire when committing compositions. If
|
|
* the arrow keys are used to commit, prevent default so that the cursor
|
|
* doesn't move, otherwise it will jump back noticeably on re-render.
|
|
*/
|
|
onKeyDown: function (editor: DraftEditor, e: SyntheticKeyboardEvent<>): void {
|
|
if (!stillComposing) {
|
|
// If a keydown event is received after compositionend but before the
|
|
// 20ms timer expires (ex: type option-E then backspace, or type A then
|
|
// backspace in 2-Set Korean), we should immediately resolve the
|
|
// composition and reinterpret the key press in edit mode.
|
|
DraftEditorCompositionHandler.resolveComposition(editor);
|
|
editor._onKeyDown(e);
|
|
return;
|
|
}
|
|
if (e.which === Keys.RIGHT || e.which === Keys.LEFT) {
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Keypress events may fire when committing compositions. In Firefox,
|
|
* pressing RETURN commits the composition and inserts extra newline
|
|
* characters that we do not want. `preventDefault` allows the composition
|
|
* to be committed while preventing the extra characters.
|
|
*/
|
|
onKeyPress: function (editor: DraftEditor, e: SyntheticKeyboardEvent<>): void {
|
|
if (e.which === Keys.RETURN) {
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Attempt to insert composed characters into the document.
|
|
*
|
|
* If we are still in a composition session, do nothing. Otherwise, insert
|
|
* the characters into the document and terminate the composition session.
|
|
*
|
|
* If no characters were composed -- for instance, the user
|
|
* deleted all composed characters and committed nothing new --
|
|
* force a re-render. We also re-render when the composition occurs
|
|
* at the beginning of a leaf, to ensure that if the browser has
|
|
* created a new text node for the composition, we will discard it.
|
|
*
|
|
* Resetting innerHTML will move focus to the beginning of the editor,
|
|
* so we update to force it back to the correct place.
|
|
*/
|
|
resolveComposition: function (editor: DraftEditor): void {
|
|
if (stillComposing) {
|
|
return;
|
|
}
|
|
|
|
resolved = true;
|
|
const composedChars = textInputData;
|
|
textInputData = '';
|
|
|
|
const editorState = EditorState.set(editor._latestEditorState, {
|
|
inCompositionMode: false
|
|
});
|
|
|
|
const currentStyle = editorState.getCurrentInlineStyle();
|
|
const entityKey = getEntityKeyForSelection(editorState.getCurrentContent(), editorState.getSelection());
|
|
|
|
const mustReset = !composedChars || isSelectionAtLeafStart(editorState) || currentStyle.size > 0 || entityKey !== null;
|
|
|
|
if (mustReset) {
|
|
editor.restoreEditorDOM();
|
|
}
|
|
|
|
editor.exitCurrentMode();
|
|
|
|
if (composedChars) {
|
|
if (DraftFeatureFlags.draft_handlebeforeinput_composed_text && editor.props.handleBeforeInput && isEventHandled(editor.props.handleBeforeInput(composedChars, editorState))) {
|
|
return;
|
|
}
|
|
// If characters have been composed, re-rendering with the update
|
|
// is sufficient to reset the editor.
|
|
const contentState = DraftModifier.replaceText(editorState.getCurrentContent(), editorState.getSelection(), composedChars, currentStyle, entityKey);
|
|
editor.update(EditorState.push(editorState, contentState, 'insert-characters'));
|
|
return;
|
|
}
|
|
|
|
if (mustReset) {
|
|
editor.update(EditorState.set(editorState, {
|
|
nativelyRenderedContent: null,
|
|
forceSelection: true
|
|
}));
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = DraftEditorCompositionHandler; |