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

284 lines
11 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. * @flow
  9. * @emails oncall+draft_js
  10. */
  11. 'use strict';
  12. import type ContentState from "./ContentState";
  13. import type { DraftBlockType } from "./DraftBlockType";
  14. import type { DraftEditorCommand } from "./DraftEditorCommand";
  15. import type { DataObjectForLink, RichTextUtils } from "./RichTextUtils";
  16. import type SelectionState from "./SelectionState";
  17. import type URI from "fbjs/lib/URI";
  18. const DraftModifier = require("./DraftModifier");
  19. const EditorState = require("./EditorState");
  20. const adjustBlockDepthForContentState = require("./adjustBlockDepthForContentState");
  21. const nullthrows = require("fbjs/lib/nullthrows");
  22. const RichTextEditorUtil: RichTextUtils = {
  23. currentBlockContainsLink: function (editorState: EditorState): boolean {
  24. const selection = editorState.getSelection();
  25. const contentState = editorState.getCurrentContent();
  26. const entityMap = contentState.getEntityMap();
  27. return contentState.getBlockForKey(selection.getAnchorKey()).getCharacterList().slice(selection.getStartOffset(), selection.getEndOffset()).some(v => {
  28. const entity = v.getEntity();
  29. return !!entity && entityMap.__get(entity).getType() === 'LINK';
  30. });
  31. },
  32. getCurrentBlockType: function (editorState: EditorState): DraftBlockType {
  33. const selection = editorState.getSelection();
  34. return editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType();
  35. },
  36. getDataObjectForLinkURL: function (uri: URI): DataObjectForLink {
  37. return {
  38. url: uri.toString()
  39. };
  40. },
  41. handleKeyCommand: function (editorState: EditorState, command: DraftEditorCommand | string, eventTimeStamp: ?number): ?EditorState {
  42. switch (command) {
  43. case 'bold':
  44. return RichTextEditorUtil.toggleInlineStyle(editorState, 'BOLD');
  45. case 'italic':
  46. return RichTextEditorUtil.toggleInlineStyle(editorState, 'ITALIC');
  47. case 'underline':
  48. return RichTextEditorUtil.toggleInlineStyle(editorState, 'UNDERLINE');
  49. case 'code':
  50. return RichTextEditorUtil.toggleCode(editorState);
  51. case 'backspace':
  52. case 'backspace-word':
  53. case 'backspace-to-start-of-line':
  54. return RichTextEditorUtil.onBackspace(editorState);
  55. case 'delete':
  56. case 'delete-word':
  57. case 'delete-to-end-of-block':
  58. return RichTextEditorUtil.onDelete(editorState);
  59. default:
  60. // they may have custom editor commands; ignore those
  61. return null;
  62. }
  63. },
  64. insertSoftNewline: function (editorState: EditorState): EditorState {
  65. const contentState = DraftModifier.insertText(editorState.getCurrentContent(), editorState.getSelection(), '\n', editorState.getCurrentInlineStyle(), null);
  66. const newEditorState = EditorState.push(editorState, contentState, 'insert-characters');
  67. return EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
  68. },
  69. /**
  70. * For collapsed selections at the start of styled blocks, backspace should
  71. * just remove the existing style.
  72. */
  73. onBackspace: function (editorState: EditorState): ?EditorState {
  74. const selection = editorState.getSelection();
  75. if (!selection.isCollapsed() || selection.getAnchorOffset() || selection.getFocusOffset()) {
  76. return null;
  77. } // First, try to remove a preceding atomic block.
  78. const content = editorState.getCurrentContent();
  79. const startKey = selection.getStartKey();
  80. const blockBefore = content.getBlockBefore(startKey);
  81. if (blockBefore && blockBefore.getType() === 'atomic') {
  82. const blockMap = content.getBlockMap().delete(blockBefore.getKey());
  83. const withoutAtomicBlock = content.merge({
  84. blockMap,
  85. selectionAfter: selection
  86. });
  87. if (withoutAtomicBlock !== content) {
  88. return EditorState.push(editorState, withoutAtomicBlock, 'remove-range');
  89. }
  90. } // If that doesn't succeed, try to remove the current block style.
  91. const withoutBlockStyle = RichTextEditorUtil.tryToRemoveBlockStyle(editorState);
  92. if (withoutBlockStyle) {
  93. return EditorState.push(editorState, withoutBlockStyle, 'change-block-type');
  94. }
  95. return null;
  96. },
  97. onDelete: function (editorState: EditorState): ?EditorState {
  98. const selection = editorState.getSelection();
  99. if (!selection.isCollapsed()) {
  100. return null;
  101. }
  102. const content = editorState.getCurrentContent();
  103. const startKey = selection.getStartKey();
  104. const block = content.getBlockForKey(startKey);
  105. const length = block.getLength(); // The cursor is somewhere within the text. Behave normally.
  106. if (selection.getStartOffset() < length) {
  107. return null;
  108. }
  109. const blockAfter = content.getBlockAfter(startKey);
  110. if (!blockAfter || blockAfter.getType() !== 'atomic') {
  111. return null;
  112. }
  113. const atomicBlockTarget = selection.merge({
  114. focusKey: blockAfter.getKey(),
  115. focusOffset: blockAfter.getLength()
  116. });
  117. const withoutAtomicBlock = DraftModifier.removeRange(content, atomicBlockTarget, 'forward');
  118. if (withoutAtomicBlock !== content) {
  119. return EditorState.push(editorState, withoutAtomicBlock, 'remove-range');
  120. }
  121. return null;
  122. },
  123. onTab: function (event: SyntheticKeyboardEvent<>, editorState: EditorState, maxDepth: number): EditorState {
  124. const selection = editorState.getSelection();
  125. const key = selection.getAnchorKey();
  126. if (key !== selection.getFocusKey()) {
  127. return editorState;
  128. }
  129. const content = editorState.getCurrentContent();
  130. const block = content.getBlockForKey(key);
  131. const type = block.getType();
  132. if (type !== 'unordered-list-item' && type !== 'ordered-list-item') {
  133. return editorState;
  134. }
  135. event.preventDefault();
  136. const depth = block.getDepth();
  137. if (!event.shiftKey && depth === maxDepth) {
  138. return editorState;
  139. }
  140. const withAdjustment = adjustBlockDepthForContentState(content, selection, event.shiftKey ? -1 : 1, maxDepth);
  141. return EditorState.push(editorState, withAdjustment, 'adjust-depth');
  142. },
  143. toggleBlockType: function (editorState: EditorState, blockType: DraftBlockType): EditorState {
  144. const selection = editorState.getSelection();
  145. const startKey = selection.getStartKey();
  146. let endKey = selection.getEndKey();
  147. const content = editorState.getCurrentContent();
  148. let target = selection; // Triple-click can lead to a selection that includes offset 0 of the
  149. // following block. The `SelectionState` for this case is accurate, but
  150. // we should avoid toggling block type for the trailing block because it
  151. // is a confusing interaction.
  152. if (startKey !== endKey && selection.getEndOffset() === 0) {
  153. const blockBefore = nullthrows(content.getBlockBefore(endKey));
  154. endKey = blockBefore.getKey();
  155. target = target.merge({
  156. anchorKey: startKey,
  157. anchorOffset: selection.getStartOffset(),
  158. focusKey: endKey,
  159. focusOffset: blockBefore.getLength(),
  160. isBackward: false
  161. });
  162. }
  163. const hasAtomicBlock = content.getBlockMap().skipWhile((_, k) => k !== startKey).reverse().skipWhile((_, k) => k !== endKey).some(v => v.getType() === 'atomic');
  164. if (hasAtomicBlock) {
  165. return editorState;
  166. }
  167. const typeToSet = content.getBlockForKey(startKey).getType() === blockType ? 'unstyled' : blockType;
  168. return EditorState.push(editorState, DraftModifier.setBlockType(content, target, typeToSet), 'change-block-type');
  169. },
  170. toggleCode: function (editorState: EditorState): EditorState {
  171. const selection = editorState.getSelection();
  172. const anchorKey = selection.getAnchorKey();
  173. const focusKey = selection.getFocusKey();
  174. if (selection.isCollapsed() || anchorKey !== focusKey) {
  175. return RichTextEditorUtil.toggleBlockType(editorState, 'code-block');
  176. }
  177. return RichTextEditorUtil.toggleInlineStyle(editorState, 'CODE');
  178. },
  179. /**
  180. * Toggle the specified inline style for the selection. If the
  181. * user's selection is collapsed, apply or remove the style for the
  182. * internal state. If it is not collapsed, apply the change directly
  183. * to the document state.
  184. */
  185. toggleInlineStyle: function (editorState: EditorState, inlineStyle: string): EditorState {
  186. const selection = editorState.getSelection();
  187. const currentStyle = editorState.getCurrentInlineStyle(); // If the selection is collapsed, toggle the specified style on or off and
  188. // set the result as the new inline style override. This will then be
  189. // used as the inline style for the next character to be inserted.
  190. if (selection.isCollapsed()) {
  191. return EditorState.setInlineStyleOverride(editorState, currentStyle.has(inlineStyle) ? currentStyle.remove(inlineStyle) : currentStyle.add(inlineStyle));
  192. } // If characters are selected, immediately apply or remove the
  193. // inline style on the document state itself.
  194. const content = editorState.getCurrentContent();
  195. let newContent; // If the style is already present for the selection range, remove it.
  196. // Otherwise, apply it.
  197. if (currentStyle.has(inlineStyle)) {
  198. newContent = DraftModifier.removeInlineStyle(content, selection, inlineStyle);
  199. } else {
  200. newContent = DraftModifier.applyInlineStyle(content, selection, inlineStyle);
  201. }
  202. return EditorState.push(editorState, newContent, 'change-inline-style');
  203. },
  204. toggleLink: function (editorState: EditorState, targetSelection: SelectionState, entityKey: ?string): EditorState {
  205. const withoutLink = DraftModifier.applyEntity(editorState.getCurrentContent(), targetSelection, entityKey);
  206. return EditorState.push(editorState, withoutLink, 'apply-entity');
  207. },
  208. /**
  209. * When a collapsed cursor is at the start of a styled block, changes block
  210. * type to 'unstyled'. Returns null if selection does not meet that criteria.
  211. */
  212. tryToRemoveBlockStyle: function (editorState: EditorState): ?ContentState {
  213. const selection = editorState.getSelection();
  214. const offset = selection.getAnchorOffset();
  215. if (selection.isCollapsed() && offset === 0) {
  216. const key = selection.getAnchorKey();
  217. const content = editorState.getCurrentContent();
  218. const block = content.getBlockForKey(key);
  219. const type = block.getType();
  220. const blockBefore = content.getBlockBefore(key);
  221. if (type === 'code-block' && blockBefore && blockBefore.getType() === 'code-block' && blockBefore.getLength() !== 0) {
  222. return null;
  223. }
  224. if (type !== 'unstyled') {
  225. return DraftModifier.setBlockType(content, selection, 'unstyled');
  226. }
  227. }
  228. return null;
  229. }
  230. };
  231. module.exports = RichTextEditorUtil;