172 lines
6.7 KiB
JavaScript
172 lines
6.7 KiB
JavaScript
/**
|
|
* 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 editOnInput
|
|
* @format
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var DraftFeatureFlags = require('./DraftFeatureFlags');
|
|
var DraftModifier = require('./DraftModifier');
|
|
var DraftOffsetKey = require('./DraftOffsetKey');
|
|
var EditorState = require('./EditorState');
|
|
var UserAgent = require('fbjs/lib/UserAgent');
|
|
|
|
var findAncestorOffsetKey = require('./findAncestorOffsetKey');
|
|
var nullthrows = require('fbjs/lib/nullthrows');
|
|
|
|
var isGecko = UserAgent.isEngine('Gecko');
|
|
|
|
var DOUBLE_NEWLINE = '\n\n';
|
|
|
|
/**
|
|
* This function is intended to handle spellcheck and autocorrect changes,
|
|
* which occur in the DOM natively without any opportunity to observe or
|
|
* interpret the changes before they occur.
|
|
*
|
|
* The `input` event fires in contentEditable elements reliably for non-IE
|
|
* browsers, immediately after changes occur to the editor DOM. Since our other
|
|
* handlers override or otherwise handle cover other varieties of text input,
|
|
* the DOM state should match the model in all controlled input cases. Thus,
|
|
* when an `input` change leads to a DOM/model mismatch, the change should be
|
|
* due to a spellcheck change, and we can incorporate it into our model.
|
|
*/
|
|
function editOnInput(editor) {
|
|
if (editor._pendingStateFromBeforeInput !== undefined) {
|
|
editor.update(editor._pendingStateFromBeforeInput);
|
|
editor._pendingStateFromBeforeInput = undefined;
|
|
}
|
|
|
|
var domSelection = global.getSelection();
|
|
|
|
var anchorNode = domSelection.anchorNode,
|
|
isCollapsed = domSelection.isCollapsed;
|
|
|
|
var isNotTextNode = anchorNode.nodeType !== Node.TEXT_NODE;
|
|
var isNotTextOrElementNode = anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.nodeType !== Node.ELEMENT_NODE;
|
|
|
|
if (DraftFeatureFlags.draft_killswitch_allow_nontextnodes) {
|
|
if (isNotTextNode) {
|
|
return;
|
|
}
|
|
} else {
|
|
if (isNotTextOrElementNode) {
|
|
// TODO: (t16149272) figure out context for this change
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (anchorNode.nodeType === Node.TEXT_NODE && (anchorNode.previousSibling !== null || anchorNode.nextSibling !== null)) {
|
|
// When typing at the beginning of a visual line, Chrome splits the text
|
|
// nodes into two. Why? No one knows. This commit is suspicious:
|
|
// https://chromium.googlesource.com/chromium/src/+/a3b600981286b135632371477f902214c55a1724
|
|
// To work around, we'll merge the sibling text nodes back into this one.
|
|
var span = anchorNode.parentNode;
|
|
anchorNode.nodeValue = span.textContent;
|
|
for (var child = span.firstChild; child !== null; child = child.nextSibling) {
|
|
if (child !== anchorNode) {
|
|
span.removeChild(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
var domText = anchorNode.textContent;
|
|
var editorState = editor._latestEditorState;
|
|
var offsetKey = nullthrows(findAncestorOffsetKey(anchorNode));
|
|
|
|
var _DraftOffsetKey$decod = DraftOffsetKey.decode(offsetKey),
|
|
blockKey = _DraftOffsetKey$decod.blockKey,
|
|
decoratorKey = _DraftOffsetKey$decod.decoratorKey,
|
|
leafKey = _DraftOffsetKey$decod.leafKey;
|
|
|
|
var _editorState$getBlock = editorState.getBlockTree(blockKey).getIn([decoratorKey, 'leaves', leafKey]),
|
|
start = _editorState$getBlock.start,
|
|
end = _editorState$getBlock.end;
|
|
|
|
var content = editorState.getCurrentContent();
|
|
var block = content.getBlockForKey(blockKey);
|
|
var modelText = block.getText().slice(start, end);
|
|
|
|
// Special-case soft newlines here. If the DOM text ends in a soft newline,
|
|
// we will have manually inserted an extra soft newline in DraftEditorLeaf.
|
|
// We want to remove this extra newline for the purpose of our comparison
|
|
// of DOM and model text.
|
|
if (domText.endsWith(DOUBLE_NEWLINE)) {
|
|
domText = domText.slice(0, -1);
|
|
}
|
|
|
|
// No change -- the DOM is up to date. Nothing to do here.
|
|
if (domText === modelText) {
|
|
// This can be buggy for some Android keyboards because they don't fire
|
|
// standard onkeydown/pressed events and only fired editOnInput
|
|
// so domText is already changed by the browser and ends up being equal
|
|
// to modelText unexpectedly
|
|
return;
|
|
}
|
|
|
|
var selection = editorState.getSelection();
|
|
|
|
// We'll replace the entire leaf with the text content of the target.
|
|
var targetRange = selection.merge({
|
|
anchorOffset: start,
|
|
focusOffset: end,
|
|
isBackward: false
|
|
});
|
|
|
|
var entityKey = block.getEntityAt(start);
|
|
var entity = entityKey && content.getEntity(entityKey);
|
|
var entityType = entity && entity.getMutability();
|
|
var preserveEntity = entityType === 'MUTABLE';
|
|
|
|
// Immutable or segmented entities cannot properly be handled by the
|
|
// default browser undo, so we have to use a different change type to
|
|
// force using our internal undo method instead of falling through to the
|
|
// native browser undo.
|
|
var changeType = preserveEntity ? 'spellcheck-change' : 'apply-entity';
|
|
|
|
var newContent = DraftModifier.replaceText(content, targetRange, domText, block.getInlineStyleAt(start), preserveEntity ? block.getEntityAt(start) : null);
|
|
|
|
var anchorOffset, focusOffset, startOffset, endOffset;
|
|
|
|
if (isGecko) {
|
|
// Firefox selection does not change while the context menu is open, so
|
|
// we preserve the anchor and focus values of the DOM selection.
|
|
anchorOffset = domSelection.anchorOffset;
|
|
focusOffset = domSelection.focusOffset;
|
|
startOffset = start + Math.min(anchorOffset, focusOffset);
|
|
endOffset = startOffset + Math.abs(anchorOffset - focusOffset);
|
|
anchorOffset = startOffset;
|
|
focusOffset = endOffset;
|
|
} else {
|
|
// Browsers other than Firefox may adjust DOM selection while the context
|
|
// menu is open, and Safari autocorrect is prone to providing an inaccurate
|
|
// DOM selection. Don't trust it. Instead, use our existing SelectionState
|
|
// and adjust it based on the number of characters changed during the
|
|
// mutation.
|
|
var charDelta = domText.length - modelText.length;
|
|
startOffset = selection.getStartOffset();
|
|
endOffset = selection.getEndOffset();
|
|
|
|
anchorOffset = isCollapsed ? endOffset + charDelta : startOffset;
|
|
focusOffset = endOffset + charDelta;
|
|
}
|
|
|
|
// Segmented entities are completely or partially removed when their
|
|
// text content changes. For this case we do not want any text to be selected
|
|
// after the change, so we are not merging the selection.
|
|
var contentWithAdjustedDOMSelection = newContent.merge({
|
|
selectionBefore: content.getSelectionAfter(),
|
|
selectionAfter: selection.merge({ anchorOffset: anchorOffset, focusOffset: focusOffset })
|
|
});
|
|
|
|
editor.update(EditorState.push(editorState, contentWithAdjustedDOMSelection, changeType));
|
|
}
|
|
|
|
module.exports = editOnInput; |