Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

3 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. const DOMObserver = require("./DOMObserver");
  14. const DraftModifier = require("./DraftModifier");
  15. const DraftOffsetKey = require("./DraftOffsetKey");
  16. const EditorState = require("./EditorState");
  17. const Keys = require("fbjs/lib/Keys");
  18. const editOnSelect = require("./editOnSelect");
  19. const getContentEditableContainer = require("./getContentEditableContainer");
  20. const getDraftEditorSelection = require("./getDraftEditorSelection");
  21. const getEntityKeyForSelection = require("./getEntityKeyForSelection");
  22. const nullthrows = require("fbjs/lib/nullthrows");
  23. /**
  24. * Millisecond delay to allow `compositionstart` to fire again upon
  25. * `compositionend`.
  26. *
  27. * This is used for Korean input to ensure that typing can continue without
  28. * the editor trying to render too quickly. More specifically, Safari 7.1+
  29. * triggers `compositionstart` a little slower than Chrome/FF, which
  30. * leads to composed characters being resolved and re-render occurring
  31. * sooner than we want.
  32. */
  33. const RESOLVE_DELAY = 20;
  34. /**
  35. * A handful of variables used to track the current composition and its
  36. * resolution status. These exist at the module level because it is not
  37. * possible to have compositions occurring in multiple editors simultaneously,
  38. * and it simplifies state management with respect to the DraftEditor component.
  39. */
  40. let resolved = false;
  41. let stillComposing = false;
  42. let domObserver = null;
  43. function startDOMObserver(editor: DraftEditor) {
  44. if (!domObserver) {
  45. domObserver = new DOMObserver(getContentEditableContainer(editor));
  46. domObserver.start();
  47. }
  48. }
  49. const DraftEditorCompositionHandler = {
  50. /**
  51. * A `compositionstart` event has fired while we're still in composition
  52. * mode. Continue the current composition session to prevent a re-render.
  53. */
  54. onCompositionStart: function (editor: DraftEditor): void {
  55. stillComposing = true;
  56. startDOMObserver(editor);
  57. },
  58. /**
  59. * Attempt to end the current composition session.
  60. *
  61. * Defer handling because browser will still insert the chars into active
  62. * element after `compositionend`. If a `compositionstart` event fires
  63. * before `resolveComposition` executes, our composition session will
  64. * continue.
  65. *
  66. * The `resolved` flag is useful because certain IME interfaces fire the
  67. * `compositionend` event multiple times, thus queueing up multiple attempts
  68. * at handling the composition. Since handling the same composition event
  69. * twice could break the DOM, we only use the first event. Example: Arabic
  70. * Google Input Tools on Windows 8.1 fires `compositionend` three times.
  71. */
  72. onCompositionEnd: function (editor: DraftEditor): void {
  73. resolved = false;
  74. stillComposing = false;
  75. setTimeout(() => {
  76. if (!resolved) {
  77. DraftEditorCompositionHandler.resolveComposition(editor);
  78. }
  79. }, RESOLVE_DELAY);
  80. },
  81. onSelect: editOnSelect,
  82. /**
  83. * In Safari, keydown events may fire when committing compositions. If
  84. * the arrow keys are used to commit, prevent default so that the cursor
  85. * doesn't move, otherwise it will jump back noticeably on re-render.
  86. */
  87. onKeyDown: function (editor: DraftEditor, e: SyntheticKeyboardEvent<>): void {
  88. if (!stillComposing) {
  89. // If a keydown event is received after compositionend but before the
  90. // 20ms timer expires (ex: type option-E then backspace, or type A then
  91. // backspace in 2-Set Korean), we should immediately resolve the
  92. // composition and reinterpret the key press in edit mode.
  93. DraftEditorCompositionHandler.resolveComposition(editor);
  94. editor._onKeyDown(e);
  95. return;
  96. }
  97. if (e.which === Keys.RIGHT || e.which === Keys.LEFT) {
  98. e.preventDefault();
  99. }
  100. },
  101. /**
  102. * Keypress events may fire when committing compositions. In Firefox,
  103. * pressing RETURN commits the composition and inserts extra newline
  104. * characters that we do not want. `preventDefault` allows the composition
  105. * to be committed while preventing the extra characters.
  106. */
  107. onKeyPress: function (editor: DraftEditor, e: SyntheticKeyboardEvent<>): void {
  108. if (e.which === Keys.RETURN) {
  109. e.preventDefault();
  110. }
  111. },
  112. /**
  113. * Attempt to insert composed characters into the document.
  114. *
  115. * If we are still in a composition session, do nothing. Otherwise, insert
  116. * the characters into the document and terminate the composition session.
  117. *
  118. * If no characters were composed -- for instance, the user
  119. * deleted all composed characters and committed nothing new --
  120. * force a re-render. We also re-render when the composition occurs
  121. * at the beginning of a leaf, to ensure that if the browser has
  122. * created a new text node for the composition, we will discard it.
  123. *
  124. * Resetting innerHTML will move focus to the beginning of the editor,
  125. * so we update to force it back to the correct place.
  126. */
  127. resolveComposition: function (editor: DraftEditor): void {
  128. if (stillComposing) {
  129. return;
  130. }
  131. const mutations = nullthrows(domObserver).stopAndFlushMutations();
  132. domObserver = null;
  133. resolved = true;
  134. let editorState = EditorState.set(editor._latestEditorState, {
  135. inCompositionMode: false
  136. });
  137. editor.exitCurrentMode();
  138. if (!mutations.size) {
  139. editor.update(editorState);
  140. return;
  141. } // TODO, check if Facebook still needs this flag or if it could be removed.
  142. // Since there can be multiple mutations providing a `composedChars` doesn't
  143. // apply well on this new model.
  144. // if (
  145. // gkx('draft_handlebeforeinput_composed_text') &&
  146. // editor.props.handleBeforeInput &&
  147. // isEventHandled(
  148. // editor.props.handleBeforeInput(
  149. // composedChars,
  150. // editorState,
  151. // event.timeStamp,
  152. // ),
  153. // )
  154. // ) {
  155. // return;
  156. // }
  157. let contentState = editorState.getCurrentContent();
  158. mutations.forEach((composedChars, offsetKey) => {
  159. const {
  160. blockKey,
  161. decoratorKey,
  162. leafKey
  163. } = DraftOffsetKey.decode(offsetKey);
  164. const {
  165. start,
  166. end
  167. } = editorState.getBlockTree(blockKey).getIn([decoratorKey, 'leaves', leafKey]);
  168. const replacementRange = editorState.getSelection().merge({
  169. anchorKey: blockKey,
  170. focusKey: blockKey,
  171. anchorOffset: start,
  172. focusOffset: end,
  173. isBackward: false
  174. });
  175. const entityKey = getEntityKeyForSelection(contentState, replacementRange);
  176. const currentStyle = contentState.getBlockForKey(blockKey).getInlineStyleAt(start);
  177. contentState = DraftModifier.replaceText(contentState, replacementRange, composedChars, currentStyle, entityKey); // We need to update the editorState so the leaf node ranges are properly
  178. // updated and multiple mutations are correctly applied.
  179. editorState = EditorState.set(editorState, {
  180. currentContent: contentState
  181. });
  182. }); // When we apply the text changes to the ContentState, the selection always
  183. // goes to the end of the field, but it should just stay where it is
  184. // after compositionEnd.
  185. const documentSelection = getDraftEditorSelection(editorState, getContentEditableContainer(editor));
  186. const compositionEndSelectionState = documentSelection.selectionState;
  187. editor.restoreEditorDOM();
  188. const editorStateWithUpdatedSelection = EditorState.acceptSelection(editorState, compositionEndSelectionState);
  189. editor.update(EditorState.push(editorStateWithUpdatedSelection, contentState, 'insert-characters'));
  190. }
  191. };
  192. module.exports = DraftEditorCompositionHandler;