You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

219 lines
7.7 KiB

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