Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

editOnBeforeInput.js.flow 8.6 KiB

3 lat temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 strict-local
  9. * @emails oncall+draft_js
  10. */
  11. 'use strict';
  12. import type DraftEditor from "./DraftEditor.react";
  13. import type { DraftInlineStyle } from "./DraftInlineStyle";
  14. const DraftModifier = require("./DraftModifier");
  15. const EditorState = require("./EditorState");
  16. const UserAgent = require("fbjs/lib/UserAgent");
  17. const getEntityKeyForSelection = require("./getEntityKeyForSelection");
  18. const isEventHandled = require("./isEventHandled");
  19. const isSelectionAtLeafStart = require("./isSelectionAtLeafStart");
  20. const nullthrows = require("fbjs/lib/nullthrows");
  21. const setImmediate = require("fbjs/lib/setImmediate"); // When nothing is focused, Firefox regards two characters, `'` and `/`, as
  22. // commands that should open and focus the "quickfind" search bar. This should
  23. // *never* happen while a contenteditable is focused, but as of v28, it
  24. // sometimes does, even when the keypress event target is the contenteditable.
  25. // This breaks the input. Special case these characters to ensure that when
  26. // they are typed, we prevent default on the event to make sure not to
  27. // trigger quickfind.
  28. const FF_QUICKFIND_CHAR = "'";
  29. const FF_QUICKFIND_LINK_CHAR = '/';
  30. const isFirefox = UserAgent.isBrowser('Firefox');
  31. function mustPreventDefaultForCharacter(character: string): boolean {
  32. return isFirefox && (character == FF_QUICKFIND_CHAR || character == FF_QUICKFIND_LINK_CHAR);
  33. }
  34. /**
  35. * Replace the current selection with the specified text string, with the
  36. * inline style and entity key applied to the newly inserted text.
  37. */
  38. function replaceText(editorState: EditorState, text: string, inlineStyle: DraftInlineStyle, entityKey: ?string, forceSelection: boolean): EditorState {
  39. const contentState = DraftModifier.replaceText(editorState.getCurrentContent(), editorState.getSelection(), text, inlineStyle, entityKey);
  40. return EditorState.push(editorState, contentState, 'insert-characters', forceSelection);
  41. }
  42. /**
  43. * When `onBeforeInput` executes, the browser is attempting to insert a
  44. * character into the editor. Apply this character data to the document,
  45. * allowing native insertion if possible.
  46. *
  47. * Native insertion is encouraged in order to limit re-rendering and to
  48. * preserve spellcheck highlighting, which disappears or flashes if re-render
  49. * occurs on the relevant text nodes.
  50. */
  51. function editOnBeforeInput(editor: DraftEditor, e: SyntheticInputEvent<HTMLElement>): void {
  52. if (editor._pendingStateFromBeforeInput !== undefined) {
  53. editor.update(editor._pendingStateFromBeforeInput);
  54. editor._pendingStateFromBeforeInput = undefined;
  55. }
  56. const editorState = editor._latestEditorState;
  57. const chars = e.data; // In some cases (ex: IE ideographic space insertion) no character data
  58. // is provided. There's nothing to do when this happens.
  59. if (!chars) {
  60. return;
  61. } // Allow the top-level component to handle the insertion manually. This is
  62. // useful when triggering interesting behaviors for a character insertion,
  63. // Simple examples: replacing a raw text ':)' with a smile emoji or image
  64. // decorator, or setting a block to be a list item after typing '- ' at the
  65. // start of the block.
  66. if (editor.props.handleBeforeInput && isEventHandled(editor.props.handleBeforeInput(chars, editorState, e.timeStamp))) {
  67. e.preventDefault();
  68. return;
  69. } // If selection is collapsed, conditionally allow native behavior. This
  70. // reduces re-renders and preserves spellcheck highlighting. If the selection
  71. // is not collapsed, we will re-render.
  72. const selection = editorState.getSelection();
  73. const selectionStart = selection.getStartOffset();
  74. const anchorKey = selection.getAnchorKey();
  75. if (!selection.isCollapsed()) {
  76. e.preventDefault();
  77. editor.update(replaceText(editorState, chars, editorState.getCurrentInlineStyle(), getEntityKeyForSelection(editorState.getCurrentContent(), editorState.getSelection()), true));
  78. return;
  79. }
  80. let newEditorState = replaceText(editorState, chars, editorState.getCurrentInlineStyle(), getEntityKeyForSelection(editorState.getCurrentContent(), editorState.getSelection()), false); // Bunch of different cases follow where we need to prevent native insertion.
  81. let mustPreventNative = false;
  82. if (!mustPreventNative) {
  83. // Browsers tend to insert text in weird places in the DOM when typing at
  84. // the start of a leaf, so we'll handle it ourselves.
  85. mustPreventNative = isSelectionAtLeafStart(editor._latestCommittedEditorState);
  86. }
  87. if (!mustPreventNative) {
  88. // Let's say we have a decorator that highlights hashtags. In many cases
  89. // we need to prevent native behavior and rerender ourselves --
  90. // particularly, any case *except* where the inserted characters end up
  91. // anywhere except exactly where you put them.
  92. //
  93. // Using [] to denote a decorated leaf, some examples:
  94. //
  95. // 1. 'hi #' and append 'f'
  96. // desired rendering: 'hi [#f]'
  97. // native rendering would be: 'hi #f' (incorrect)
  98. //
  99. // 2. 'x [#foo]' and insert '#' before 'f'
  100. // desired rendering: 'x #[#foo]'
  101. // native rendering would be: 'x [##foo]' (incorrect)
  102. //
  103. // 3. '[#foobar]' and insert ' ' between 'foo' and 'bar'
  104. // desired rendering: '[#foo] bar'
  105. // native rendering would be: '[#foo bar]' (incorrect)
  106. //
  107. // 4. '[#foo]' and delete '#' [won't use this beforeinput codepath though]
  108. // desired rendering: 'foo'
  109. // native rendering would be: '[foo]' (incorrect)
  110. //
  111. // 5. '[#foo]' and append 'b'
  112. // desired rendering: '[#foob]'
  113. // native rendering would be: '[#foob]'
  114. // (native insertion here would be ok for decorators like simple spans,
  115. // but not more complex decorators. To be safe, we need to prevent it.)
  116. //
  117. // It is safe to allow native insertion if and only if the full list of
  118. // decorator ranges matches what we expect native insertion to give, and
  119. // the range lengths have not changed. We don't need to compare the content
  120. // because the only possible mutation to consider here is inserting plain
  121. // text and decorators can't affect text content.
  122. const oldBlockTree = editorState.getBlockTree(anchorKey);
  123. const newBlockTree = newEditorState.getBlockTree(anchorKey);
  124. mustPreventNative = oldBlockTree.size !== newBlockTree.size || oldBlockTree.zip(newBlockTree).some(([oldLeafSet, newLeafSet]) => {
  125. // selectionStart is guaranteed to be selectionEnd here
  126. const oldStart = oldLeafSet.get('start');
  127. const adjustedStart = oldStart + (oldStart >= selectionStart ? chars.length : 0);
  128. const oldEnd = oldLeafSet.get('end');
  129. const adjustedEnd = oldEnd + (oldEnd >= selectionStart ? chars.length : 0);
  130. const newStart = newLeafSet.get('start');
  131. const newEnd = newLeafSet.get('end');
  132. const newDecoratorKey = newLeafSet.get('decoratorKey');
  133. return (// Different decorators
  134. oldLeafSet.get('decoratorKey') !== newDecoratorKey || // Different number of inline styles
  135. oldLeafSet.get('leaves').size !== newLeafSet.get('leaves').size || // Different effective decorator position
  136. adjustedStart !== newStart || adjustedEnd !== newEnd || // Decorator already existed and its length changed
  137. newDecoratorKey != null && newEnd - newStart !== oldEnd - oldStart
  138. );
  139. });
  140. }
  141. if (!mustPreventNative) {
  142. mustPreventNative = mustPreventDefaultForCharacter(chars);
  143. }
  144. if (!mustPreventNative) {
  145. mustPreventNative = nullthrows(newEditorState.getDirectionMap()).get(anchorKey) !== nullthrows(editorState.getDirectionMap()).get(anchorKey);
  146. }
  147. if (mustPreventNative) {
  148. e.preventDefault();
  149. newEditorState = EditorState.set(newEditorState, {
  150. forceSelection: true
  151. });
  152. editor.update(newEditorState);
  153. return;
  154. } // We made it all the way! Let the browser do its thing and insert the char.
  155. newEditorState = EditorState.set(newEditorState, {
  156. nativelyRenderedContent: newEditorState.getCurrentContent()
  157. }); // The native event is allowed to occur. To allow user onChange handlers to
  158. // change the inserted text, we wait until the text is actually inserted
  159. // before we actually update our state. That way when we rerender, the text
  160. // we see in the DOM will already have been inserted properly.
  161. editor._pendingStateFromBeforeInput = newEditorState;
  162. setImmediate(() => {
  163. if (editor._pendingStateFromBeforeInput !== undefined) {
  164. editor.update(editor._pendingStateFromBeforeInput);
  165. editor._pendingStateFromBeforeInput = undefined;
  166. }
  167. });
  168. }
  169. module.exports = editOnBeforeInput;