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

NestedRichTextEditorUtil.js 18 KiB

3 년 전
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. "use strict";
  2. /**
  3. * Copyright (c) Facebook, Inc. and its affiliates.
  4. *
  5. * This source code is licensed under the MIT license found in the
  6. * LICENSE file in the root directory of this source tree.
  7. *
  8. * @format
  9. *
  10. * @emails oncall+draft_js
  11. *
  12. * This is unstable and not part of the public API and should not be used by
  13. * production systems. This file may be update/removed without notice.
  14. */
  15. var ContentBlockNode = require("./ContentBlockNode");
  16. var DraftModifier = require("./DraftModifier");
  17. var DraftTreeOperations = require("./DraftTreeOperations");
  18. var EditorState = require("./EditorState");
  19. var RichTextEditorUtil = require("./RichTextEditorUtil");
  20. var adjustBlockDepthForContentState = require("./adjustBlockDepthForContentState");
  21. var generateRandomKey = require("./generateRandomKey");
  22. var invariant = require("fbjs/lib/invariant"); // Eventually we could allow to control this list by either allowing user configuration
  23. // and/or a schema in conjunction to DraftBlockRenderMap
  24. var NESTING_DISABLED_TYPES = ['code-block', 'atomic'];
  25. var NestedRichTextEditorUtil = {
  26. handleKeyCommand: function handleKeyCommand(editorState, command) {
  27. switch (command) {
  28. case 'bold':
  29. return NestedRichTextEditorUtil.toggleInlineStyle(editorState, 'BOLD');
  30. case 'italic':
  31. return NestedRichTextEditorUtil.toggleInlineStyle(editorState, 'ITALIC');
  32. case 'underline':
  33. return NestedRichTextEditorUtil.toggleInlineStyle(editorState, 'UNDERLINE');
  34. case 'code':
  35. return NestedRichTextEditorUtil.toggleCode(editorState);
  36. case 'backspace':
  37. case 'backspace-word':
  38. case 'backspace-to-start-of-line':
  39. return NestedRichTextEditorUtil.onBackspace(editorState);
  40. case 'delete':
  41. case 'delete-word':
  42. case 'delete-to-end-of-block':
  43. return NestedRichTextEditorUtil.onDelete(editorState);
  44. default:
  45. // they may have custom editor commands; ignore those
  46. return null;
  47. }
  48. },
  49. onDelete: function onDelete(editorState) {
  50. var selection = editorState.getSelection();
  51. if (!selection.isCollapsed()) {
  52. return null;
  53. }
  54. var content = editorState.getCurrentContent();
  55. var startKey = selection.getStartKey();
  56. var block = content.getBlockForKey(startKey);
  57. var length = block.getLength(); // The cursor is somewhere within the text. Behave normally.
  58. if (selection.getStartOffset() < length) {
  59. return null;
  60. }
  61. var blockAfter = content.getBlockAfter(startKey);
  62. if (!blockAfter || blockAfter.getType() !== 'atomic') {
  63. return null;
  64. }
  65. var atomicBlockTarget = selection.merge({
  66. focusKey: blockAfter.getKey(),
  67. focusOffset: blockAfter.getLength()
  68. });
  69. var withoutAtomicBlock = DraftModifier.removeRange(content, atomicBlockTarget, 'forward');
  70. if (withoutAtomicBlock !== content) {
  71. return EditorState.push(editorState, withoutAtomicBlock, 'remove-range');
  72. }
  73. return null;
  74. },
  75. /**
  76. * Ensures that if on the beginning of unstyled block and first child of
  77. * a nested parent we add its text to the neareast previous leaf node
  78. */
  79. onBackspace: function onBackspace(editorState) {
  80. var selection = editorState.getSelection();
  81. var content = editorState.getCurrentContent();
  82. var currentBlock = content.getBlockForKey(selection.getStartKey());
  83. var previousBlockKey = currentBlock.getPrevSiblingKey();
  84. if (!selection.isCollapsed() || selection.getAnchorOffset() || selection.getFocusOffset() || currentBlock.getType() === 'unstyled' && previousBlockKey && content.getBlockForKey(previousBlockKey).getType() !== 'atomic') {
  85. return null;
  86. }
  87. var startKey = selection.getStartKey();
  88. var blockBefore = content.getBlockBefore(startKey); // we want to delete that block completely
  89. if (blockBefore && blockBefore.getType() === 'atomic') {
  90. var withoutAtomicBlock = DraftModifier.removeRange(content, selection.merge({
  91. focusKey: blockBefore.getKey(),
  92. focusOffset: blockBefore.getText().length,
  93. anchorKey: startKey,
  94. anchorOffset: content.getBlockForKey(startKey).getText().length,
  95. isBackward: false
  96. }), 'forward').merge({
  97. selectionAfter: selection
  98. });
  99. if (withoutAtomicBlock !== content) {
  100. return EditorState.push(editorState, withoutAtomicBlock, 'remove-range');
  101. }
  102. } // if we have a next sibbling we should not allow the normal backspace
  103. // behaviour of moving this text into its parent
  104. // if (currentBlock.getPrevSiblingKey()) {
  105. // return editorState;
  106. // }
  107. // If that doesn't succeed, try to remove the current block style.
  108. var withoutBlockStyle = NestedRichTextEditorUtil.tryToRemoveBlockStyle(editorState);
  109. if (withoutBlockStyle) {
  110. return EditorState.push(editorState, withoutBlockStyle, withoutBlockStyle.getBlockMap().get(currentBlock.getKey()).getType() === 'unstyled' ? 'change-block-type' : 'adjust-depth');
  111. }
  112. return null;
  113. },
  114. // Todo (T32099101)
  115. // onSplitNestedBlock() {},
  116. // Todo (T32099101)
  117. // onSplitParent() {},
  118. /**
  119. * Ensures that we can create nested blocks by changing the block type of
  120. * a ranged selection
  121. */
  122. toggleBlockType: function toggleBlockType(editorState, blockType) {
  123. var selection = editorState.getSelection();
  124. var content = editorState.getCurrentContent();
  125. var currentBlock = content.getBlockForKey(selection.getStartKey());
  126. var haveChildren = !currentBlock.getChildKeys().isEmpty();
  127. var isSelectionCollapsed = selection.isCollapsed();
  128. var isMultiBlockSelection = selection.getAnchorKey() !== selection.getFocusKey();
  129. var isUnsupportedNestingBlockType = NESTING_DISABLED_TYPES.includes(blockType);
  130. var isCurrentBlockOfUnsupportedNestingBlockType = NESTING_DISABLED_TYPES.includes(currentBlock.getType()); // we don't allow this operations to avoid corrupting the document data model
  131. // to make sure that non nested blockTypes wont inherit children
  132. if ((isMultiBlockSelection || haveChildren) && isUnsupportedNestingBlockType) {
  133. return editorState;
  134. } // we can treat this operations the same way as we would for flat data structures
  135. if (isCurrentBlockOfUnsupportedNestingBlockType || isSelectionCollapsed || isUnsupportedNestingBlockType || isMultiBlockSelection || currentBlock.getType() === blockType || !currentBlock.getChildKeys().isEmpty()) {
  136. return RichTextEditorUtil.toggleBlockType(editorState, blockType);
  137. } // TODO
  138. // if we have full range selection on the block:
  139. // extract text and insert a block after it with the text as its content
  140. // else
  141. // split the block into before range and after unstyled blocks
  142. //
  143. return editorState;
  144. },
  145. currentBlockContainsLink: function currentBlockContainsLink(editorState) {
  146. var selection = editorState.getSelection();
  147. var contentState = editorState.getCurrentContent();
  148. var entityMap = contentState.getEntityMap();
  149. return contentState.getBlockForKey(selection.getAnchorKey()).getCharacterList().slice(selection.getStartOffset(), selection.getEndOffset()).some(function (v) {
  150. var entity = v.getEntity();
  151. return !!entity && entityMap.__get(entity).getType() === 'LINK';
  152. });
  153. },
  154. getCurrentBlockType: function getCurrentBlockType(editorState) {
  155. var selection = editorState.getSelection();
  156. return editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType();
  157. },
  158. getDataObjectForLinkURL: function getDataObjectForLinkURL(uri) {
  159. return {
  160. url: uri.toString()
  161. };
  162. },
  163. insertSoftNewline: function insertSoftNewline(editorState) {
  164. var contentState = DraftModifier.insertText(editorState.getCurrentContent(), editorState.getSelection(), '\n', editorState.getCurrentInlineStyle(), null);
  165. var newEditorState = EditorState.push(editorState, contentState, 'insert-characters');
  166. return EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
  167. },
  168. onTab: function onTab(event, editorState, maxDepth) {
  169. var selection = editorState.getSelection();
  170. var key = selection.getAnchorKey();
  171. if (key !== selection.getFocusKey()) {
  172. return editorState;
  173. }
  174. var content = editorState.getCurrentContent();
  175. var block = content.getBlockForKey(key);
  176. var type = block.getType();
  177. if (type !== 'unordered-list-item' && type !== 'ordered-list-item') {
  178. return editorState;
  179. }
  180. event.preventDefault();
  181. var depth = block.getDepth();
  182. if (!event.shiftKey && depth === maxDepth) {
  183. return editorState;
  184. } // implement nested tree behaviour for onTab
  185. var blockMap = editorState.getCurrentContent().getBlockMap();
  186. var prevSiblingKey = block.getPrevSiblingKey();
  187. var nextSiblingKey = block.getNextSiblingKey();
  188. if (!event.shiftKey) {
  189. // if there is no previous sibling, we do nothing
  190. if (prevSiblingKey == null) {
  191. return editorState;
  192. } // if previous sibling is a non-leaf move node as child of previous sibling
  193. var prevSibling = blockMap.get(prevSiblingKey);
  194. var nextSibling = nextSiblingKey != null ? blockMap.get(nextSiblingKey) : null;
  195. var prevSiblingNonLeaf = prevSibling != null && prevSibling.getChildKeys().count() > 0;
  196. var nextSiblingNonLeaf = nextSibling != null && nextSibling.getChildKeys().count() > 0;
  197. if (prevSiblingNonLeaf) {
  198. blockMap = DraftTreeOperations.updateAsSiblingsChild(blockMap, key, 'previous'); // if next sibling is also non-leaf, merge the previous & next siblings
  199. if (nextSiblingNonLeaf) {
  200. blockMap = DraftTreeOperations.mergeBlocks(blockMap, prevSiblingKey);
  201. } // else, if only next sibling is non-leaf move node as child of next sibling
  202. } else if (nextSiblingNonLeaf) {
  203. blockMap = DraftTreeOperations.updateAsSiblingsChild(blockMap, key, 'next'); // if none of the siblings are non-leaf, we need to create a new parent
  204. } else {
  205. blockMap = DraftTreeOperations.createNewParent(blockMap, key);
  206. } // on un-tab
  207. } else {
  208. // if the block isn't nested, do nothing
  209. if (block.getParentKey() == null) {
  210. return editorState;
  211. }
  212. blockMap = onUntab(blockMap, block);
  213. }
  214. content = editorState.getCurrentContent().merge({
  215. blockMap: blockMap
  216. });
  217. var withAdjustment = adjustBlockDepthForContentState(content, selection, event.shiftKey ? -1 : 1, maxDepth);
  218. return EditorState.push(editorState, withAdjustment, 'adjust-depth');
  219. },
  220. toggleCode: function toggleCode(editorState) {
  221. var selection = editorState.getSelection();
  222. var anchorKey = selection.getAnchorKey();
  223. var focusKey = selection.getFocusKey();
  224. if (selection.isCollapsed() || anchorKey !== focusKey) {
  225. return RichTextEditorUtil.toggleBlockType(editorState, 'code-block');
  226. }
  227. return RichTextEditorUtil.toggleInlineStyle(editorState, 'CODE');
  228. },
  229. /**
  230. * Toggle the specified inline style for the selection. If the
  231. * user's selection is collapsed, apply or remove the style for the
  232. * internal state. If it is not collapsed, apply the change directly
  233. * to the document state.
  234. */
  235. toggleInlineStyle: function toggleInlineStyle(editorState, inlineStyle) {
  236. var selection = editorState.getSelection();
  237. var currentStyle = editorState.getCurrentInlineStyle(); // If the selection is collapsed, toggle the specified style on or off and
  238. // set the result as the new inline style override. This will then be
  239. // used as the inline style for the next character to be inserted.
  240. if (selection.isCollapsed()) {
  241. return EditorState.setInlineStyleOverride(editorState, currentStyle.has(inlineStyle) ? currentStyle.remove(inlineStyle) : currentStyle.add(inlineStyle));
  242. } // If characters are selected, immediately apply or remove the
  243. // inline style on the document state itself.
  244. var content = editorState.getCurrentContent();
  245. var newContent; // If the style is already present for the selection range, remove it.
  246. // Otherwise, apply it.
  247. if (currentStyle.has(inlineStyle)) {
  248. newContent = DraftModifier.removeInlineStyle(content, selection, inlineStyle);
  249. } else {
  250. newContent = DraftModifier.applyInlineStyle(content, selection, inlineStyle);
  251. }
  252. return EditorState.push(editorState, newContent, 'change-inline-style');
  253. },
  254. toggleLink: function toggleLink(editorState, targetSelection, entityKey) {
  255. var withoutLink = DraftModifier.applyEntity(editorState.getCurrentContent(), targetSelection, entityKey);
  256. return EditorState.push(editorState, withoutLink, 'apply-entity');
  257. },
  258. /**
  259. * When a collapsed cursor is at the start of a styled block, changes block
  260. * type to 'unstyled'. Returns null if selection does not meet that criteria.
  261. */
  262. tryToRemoveBlockStyle: function tryToRemoveBlockStyle(editorState) {
  263. var selection = editorState.getSelection();
  264. var offset = selection.getAnchorOffset();
  265. if (selection.isCollapsed() && offset === 0) {
  266. var key = selection.getAnchorKey();
  267. var content = editorState.getCurrentContent();
  268. var block = content.getBlockForKey(key);
  269. var type = block.getType();
  270. var blockBefore = content.getBlockBefore(key);
  271. if (type === 'code-block' && blockBefore && blockBefore.getType() === 'code-block' && blockBefore.getLength() !== 0) {
  272. return null;
  273. }
  274. var depth = block.getDepth();
  275. if (type !== 'unstyled') {
  276. if ((type === 'unordered-list-item' || type === 'ordered-list-item') && depth > 0) {
  277. var newBlockMap = onUntab(content.getBlockMap(), block);
  278. newBlockMap = newBlockMap.set(key, newBlockMap.get(key).merge({
  279. depth: depth - 1
  280. }));
  281. return content.merge({
  282. blockMap: newBlockMap
  283. });
  284. }
  285. return DraftModifier.setBlockType(content, selection, 'unstyled');
  286. }
  287. }
  288. return null;
  289. }
  290. };
  291. var onUntab = function onUntab(blockMap, block) {
  292. var key = block.getKey();
  293. var parentKey = block.getParentKey();
  294. var nextSiblingKey = block.getNextSiblingKey();
  295. if (parentKey == null) {
  296. return blockMap;
  297. }
  298. var parent = blockMap.get(parentKey);
  299. var existingChildren = parent.getChildKeys();
  300. var blockIndex = existingChildren.indexOf(key);
  301. if (blockIndex === 0 || blockIndex === existingChildren.count() - 1) {
  302. blockMap = DraftTreeOperations.moveChildUp(blockMap, key);
  303. } else {
  304. // split the block into [0, blockIndex] in parent & the rest in a new block
  305. var prevChildren = existingChildren.slice(0, blockIndex + 1);
  306. var nextChildren = existingChildren.slice(blockIndex + 1);
  307. blockMap = blockMap.set(parentKey, parent.merge({
  308. children: prevChildren
  309. }));
  310. var newBlock = new ContentBlockNode({
  311. key: generateRandomKey(),
  312. text: '',
  313. depth: parent.getDepth(),
  314. type: parent.getType(),
  315. children: nextChildren,
  316. parent: parent.getParentKey()
  317. }); // add new block just before its the original next sibling in the block map
  318. // TODO(T33894878): Remove the map reordering code & fix converter after launch
  319. !(nextSiblingKey != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'block must have a next sibling here') : invariant(false) : void 0;
  320. var blocks = blockMap.toSeq();
  321. blockMap = blocks.takeUntil(function (block) {
  322. return block.getKey() === nextSiblingKey;
  323. }).concat([[newBlock.getKey(), newBlock]], blocks.skipUntil(function (block) {
  324. return block.getKey() === nextSiblingKey;
  325. })).toOrderedMap(); // set the nextChildren's parent to the new block
  326. blockMap = blockMap.map(function (block) {
  327. return nextChildren.includes(block.getKey()) ? block.merge({
  328. parent: newBlock.getKey()
  329. }) : block;
  330. }); // update the next/previous pointers for the children at the split
  331. blockMap = blockMap.set(key, block.merge({
  332. nextSibling: null
  333. })).set(nextSiblingKey, blockMap.get(nextSiblingKey).merge({
  334. prevSibling: null
  335. }));
  336. var parentNextSiblingKey = parent.getNextSiblingKey();
  337. if (parentNextSiblingKey != null) {
  338. blockMap = DraftTreeOperations.updateSibling(blockMap, newBlock.getKey(), parentNextSiblingKey);
  339. }
  340. blockMap = DraftTreeOperations.updateSibling(blockMap, parentKey, newBlock.getKey());
  341. blockMap = DraftTreeOperations.moveChildUp(blockMap, key);
  342. } // on untab, we also want to unnest any sibling blocks that become two levels deep
  343. // ensure that block's old parent does not have a non-leaf as its first child.
  344. var childWasUntabbed = false;
  345. if (parentKey != null) {
  346. var _parent = blockMap.get(parentKey);
  347. while (_parent != null) {
  348. var children = _parent.getChildKeys();
  349. var firstChildKey = children.first();
  350. !(firstChildKey != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'parent must have at least one child') : invariant(false) : void 0;
  351. var firstChild = blockMap.get(firstChildKey);
  352. if (firstChild.getChildKeys().count() === 0) {
  353. break;
  354. } else {
  355. blockMap = DraftTreeOperations.moveChildUp(blockMap, firstChildKey);
  356. _parent = blockMap.get(parentKey);
  357. childWasUntabbed = true;
  358. }
  359. }
  360. } // now, we may be in a state with two non-leaf blocks of the same type
  361. // next to each other
  362. if (childWasUntabbed && parentKey != null) {
  363. var _parent2 = blockMap.get(parentKey);
  364. var prevSiblingKey = _parent2 != null // parent may have been deleted
  365. ? _parent2.getPrevSiblingKey() : null;
  366. if (prevSiblingKey != null && _parent2.getChildKeys().count() > 0) {
  367. var prevSibling = blockMap.get(prevSiblingKey);
  368. if (prevSibling != null && prevSibling.getChildKeys().count() > 0) {
  369. blockMap = DraftTreeOperations.mergeBlocks(blockMap, prevSiblingKey);
  370. }
  371. }
  372. }
  373. return blockMap;
  374. };
  375. module.exports = NestedRichTextEditorUtil;