25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

editOnInput.js.flow 8.1 KiB

3 년 전
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow
  9. * @emails oncall+draft_js
  10. */
  11. 'use strict';
  12. import type DraftEditor from "./DraftEditor.react";
  13. const DraftModifier = require("./DraftModifier");
  14. const DraftOffsetKey = require("./DraftOffsetKey");
  15. const EditorState = require("./EditorState");
  16. const UserAgent = require("fbjs/lib/UserAgent");
  17. const {
  18. notEmptyKey
  19. } = require("./draftKeyUtils");
  20. const findAncestorOffsetKey = require("./findAncestorOffsetKey");
  21. const keyCommandPlainBackspace = require("./keyCommandPlainBackspace");
  22. const nullthrows = require("fbjs/lib/nullthrows");
  23. const isGecko = UserAgent.isEngine('Gecko');
  24. const DOUBLE_NEWLINE = '\n\n';
  25. function onInputType(inputType: string, editorState: EditorState): EditorState {
  26. switch (inputType) {
  27. case 'deleteContentBackward':
  28. return keyCommandPlainBackspace(editorState);
  29. }
  30. return editorState;
  31. }
  32. /**
  33. * This function serves two purposes
  34. *
  35. * 1. To update the editorState and call onChange method with the new
  36. * editorState. This editorState is calculated in editOnBeforeInput but the
  37. * onChange method is not called with the new state until this method does it.
  38. * It is done to handle a specific case where certain character inputs might
  39. * be replaced with something else. E.g. snippets ('rc' might be replaced
  40. * with boilerplate code for react component). More information on the
  41. * exact problem can be found here -
  42. * https://github.com/facebook/draft-js/commit/07892ba479bd4dfc6afd1e0ed179aaf51cd138b1
  43. *
  44. * 2. intended to handle spellcheck and autocorrect changes,
  45. * which occur in the DOM natively without any opportunity to observe or
  46. * interpret the changes before they occur.
  47. *
  48. * The `input` event fires in contentEditable elements reliably for non-IE
  49. * browsers, immediately after changes occur to the editor DOM. Since our other
  50. * handlers override or otherwise handle cover other varieties of text input,
  51. * the DOM state should match the model in all controlled input cases. Thus,
  52. * when an `input` change leads to a DOM/model mismatch, the change should be
  53. * due to a spellcheck change, and we can incorporate it into our model.
  54. */
  55. function editOnInput(editor: DraftEditor, e: SyntheticInputEvent<>): void {
  56. if (editor._pendingStateFromBeforeInput !== undefined) {
  57. editor.update(editor._pendingStateFromBeforeInput);
  58. editor._pendingStateFromBeforeInput = undefined;
  59. } // at this point editor is not null for sure (after input)
  60. const castedEditorElement: HTMLElement = (editor.editor: any);
  61. const domSelection = castedEditorElement.ownerDocument.defaultView.getSelection();
  62. const {
  63. anchorNode,
  64. isCollapsed
  65. } = domSelection;
  66. const isNotTextOrElementNode = (anchorNode === null || anchorNode === void 0 ? void 0 : anchorNode.nodeType) !== Node.TEXT_NODE && (anchorNode === null || anchorNode === void 0 ? void 0 : anchorNode.nodeType) !== Node.ELEMENT_NODE;
  67. if (isNotTextOrElementNode) {
  68. // TODO: (t16149272) figure out context for this change
  69. return;
  70. }
  71. if (anchorNode.nodeType === Node.TEXT_NODE && (anchorNode.previousSibling !== null || anchorNode.nextSibling !== null)) {
  72. // When typing at the beginning of a visual line, Chrome splits the text
  73. // nodes into two. Why? No one knows. This commit is suspicious:
  74. // https://chromium.googlesource.com/chromium/src/+/a3b600981286b135632371477f902214c55a1724
  75. // To work around, we'll merge the sibling text nodes back into this one.
  76. const span = anchorNode.parentNode;
  77. anchorNode.nodeValue = span.textContent;
  78. for (let child = span.firstChild; child !== null; child = child.nextSibling) {
  79. if (child !== anchorNode) {
  80. span.removeChild(child);
  81. }
  82. }
  83. }
  84. let domText = anchorNode.textContent;
  85. const editorState = editor._latestEditorState;
  86. const offsetKey = nullthrows(findAncestorOffsetKey(anchorNode));
  87. const {
  88. blockKey,
  89. decoratorKey,
  90. leafKey
  91. } = DraftOffsetKey.decode(offsetKey);
  92. const {
  93. start,
  94. end
  95. } = editorState.getBlockTree(blockKey).getIn([decoratorKey, 'leaves', leafKey]);
  96. const content = editorState.getCurrentContent();
  97. const block = content.getBlockForKey(blockKey);
  98. const modelText = block.getText().slice(start, end); // Special-case soft newlines here. If the DOM text ends in a soft newline,
  99. // we will have manually inserted an extra soft newline in DraftEditorLeaf.
  100. // We want to remove this extra newline for the purpose of our comparison
  101. // of DOM and model text.
  102. if (domText.endsWith(DOUBLE_NEWLINE)) {
  103. domText = domText.slice(0, -1);
  104. } // No change -- the DOM is up to date. Nothing to do here.
  105. if (domText === modelText) {
  106. // This can be buggy for some Android keyboards because they don't fire
  107. // standard onkeydown/pressed events and only fired editOnInput
  108. // so domText is already changed by the browser and ends up being equal
  109. // to modelText unexpectedly.
  110. // Newest versions of Android support the dom-inputevent-inputtype
  111. // and we can use the `inputType` to properly apply the state changes.
  112. /* $FlowFixMe inputType is only defined on a draft of a standard.
  113. * https://w3c.github.io/input-events/#dom-inputevent-inputtype */
  114. const {
  115. inputType
  116. } = e.nativeEvent;
  117. if (inputType) {
  118. const newEditorState = onInputType(inputType, editorState);
  119. if (newEditorState !== editorState) {
  120. editor.restoreEditorDOM();
  121. editor.update(newEditorState);
  122. return;
  123. }
  124. }
  125. return;
  126. }
  127. const selection = editorState.getSelection(); // We'll replace the entire leaf with the text content of the target.
  128. const targetRange = selection.merge({
  129. anchorOffset: start,
  130. focusOffset: end,
  131. isBackward: false
  132. });
  133. const entityKey = block.getEntityAt(start);
  134. const entity = notEmptyKey(entityKey) ? content.getEntity(entityKey) : null;
  135. const entityType = entity != null ? entity.getMutability() : null;
  136. const preserveEntity = entityType === 'MUTABLE'; // Immutable or segmented entities cannot properly be handled by the
  137. // default browser undo, so we have to use a different change type to
  138. // force using our internal undo method instead of falling through to the
  139. // native browser undo.
  140. const changeType = preserveEntity ? 'spellcheck-change' : 'apply-entity';
  141. const newContent = DraftModifier.replaceText(content, targetRange, domText, block.getInlineStyleAt(start), preserveEntity ? block.getEntityAt(start) : null);
  142. let anchorOffset, focusOffset, startOffset, endOffset;
  143. if (isGecko) {
  144. // Firefox selection does not change while the context menu is open, so
  145. // we preserve the anchor and focus values of the DOM selection.
  146. anchorOffset = domSelection.anchorOffset;
  147. focusOffset = domSelection.focusOffset;
  148. startOffset = start + Math.min(anchorOffset, focusOffset);
  149. endOffset = startOffset + Math.abs(anchorOffset - focusOffset);
  150. anchorOffset = startOffset;
  151. focusOffset = endOffset;
  152. } else {
  153. // Browsers other than Firefox may adjust DOM selection while the context
  154. // menu is open, and Safari autocorrect is prone to providing an inaccurate
  155. // DOM selection. Don't trust it. Instead, use our existing SelectionState
  156. // and adjust it based on the number of characters changed during the
  157. // mutation.
  158. const charDelta = domText.length - modelText.length;
  159. startOffset = selection.getStartOffset();
  160. endOffset = selection.getEndOffset();
  161. anchorOffset = isCollapsed ? endOffset + charDelta : startOffset;
  162. focusOffset = endOffset + charDelta;
  163. } // Segmented entities are completely or partially removed when their
  164. // text content changes. For this case we do not want any text to be selected
  165. // after the change, so we are not merging the selection.
  166. const contentWithAdjustedDOMSelection = newContent.merge({
  167. selectionBefore: content.getSelectionAfter(),
  168. selectionAfter: selection.merge({
  169. anchorOffset,
  170. focusOffset
  171. })
  172. });
  173. editor.update(EditorState.push(editorState, contentWithAdjustedDOMSelection, changeType));
  174. }
  175. module.exports = editOnInput;