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.

insertFragmentIntoContentState.js.flow 9.6 KiB

3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 { BlockMap } from "./BlockMap";
  13. import type { BlockNodeRecord } from "./BlockNodeRecord";
  14. import type ContentState from "./ContentState";
  15. import type SelectionState from "./SelectionState";
  16. const BlockMapBuilder = require("./BlockMapBuilder");
  17. const ContentBlockNode = require("./ContentBlockNode");
  18. const Immutable = require("immutable");
  19. const insertIntoList = require("./insertIntoList");
  20. const invariant = require("fbjs/lib/invariant");
  21. const randomizeBlockMapKeys = require("./randomizeBlockMapKeys");
  22. const {
  23. List
  24. } = Immutable;
  25. export type BlockDataMergeBehavior = 'REPLACE_WITH_NEW_DATA' | 'MERGE_OLD_DATA_TO_NEW_DATA';
  26. const updateExistingBlock = (contentState: ContentState, selectionState: SelectionState, blockMap: BlockMap, fragmentBlock: BlockNodeRecord, targetKey: string, targetOffset: number, mergeBlockData?: BlockDataMergeBehavior = 'REPLACE_WITH_NEW_DATA'): ContentState => {
  27. const targetBlock = blockMap.get(targetKey);
  28. const text = targetBlock.getText();
  29. const chars = targetBlock.getCharacterList();
  30. const finalKey = targetKey;
  31. const finalOffset = targetOffset + fragmentBlock.getText().length;
  32. let data = null;
  33. switch (mergeBlockData) {
  34. case 'MERGE_OLD_DATA_TO_NEW_DATA':
  35. data = fragmentBlock.getData().merge(targetBlock.getData());
  36. break;
  37. case 'REPLACE_WITH_NEW_DATA':
  38. data = fragmentBlock.getData();
  39. break;
  40. }
  41. const newBlock = targetBlock.merge({
  42. text: text.slice(0, targetOffset) + fragmentBlock.getText() + text.slice(targetOffset),
  43. characterList: insertIntoList(chars, fragmentBlock.getCharacterList(), targetOffset),
  44. data
  45. });
  46. return contentState.merge({
  47. blockMap: blockMap.set(targetKey, newBlock),
  48. selectionBefore: selectionState,
  49. selectionAfter: selectionState.merge({
  50. anchorKey: finalKey,
  51. anchorOffset: finalOffset,
  52. focusKey: finalKey,
  53. focusOffset: finalOffset,
  54. isBackward: false
  55. })
  56. });
  57. };
  58. /**
  59. * Appends text/characterList from the fragment first block to
  60. * target block.
  61. */
  62. const updateHead = (block: BlockNodeRecord, targetOffset: number, fragment: BlockMap): BlockNodeRecord => {
  63. const text = block.getText();
  64. const chars = block.getCharacterList(); // Modify head portion of block.
  65. const headText = text.slice(0, targetOffset);
  66. const headCharacters = chars.slice(0, targetOffset);
  67. const appendToHead = fragment.first();
  68. return block.merge({
  69. text: headText + appendToHead.getText(),
  70. characterList: headCharacters.concat(appendToHead.getCharacterList()),
  71. type: headText ? block.getType() : appendToHead.getType(),
  72. data: appendToHead.getData()
  73. });
  74. };
  75. /**
  76. * Appends offset text/characterList from the target block to the last
  77. * fragment block.
  78. */
  79. const updateTail = (block: BlockNodeRecord, targetOffset: number, fragment: BlockMap): BlockNodeRecord => {
  80. // Modify tail portion of block.
  81. const text = block.getText();
  82. const chars = block.getCharacterList(); // Modify head portion of block.
  83. const blockSize = text.length;
  84. const tailText = text.slice(targetOffset, blockSize);
  85. const tailCharacters = chars.slice(targetOffset, blockSize);
  86. const prependToTail = fragment.last();
  87. return prependToTail.merge({
  88. text: prependToTail.getText() + tailText,
  89. characterList: prependToTail.getCharacterList().concat(tailCharacters),
  90. data: prependToTail.getData()
  91. });
  92. };
  93. const getRootBlocks = (block: ContentBlockNode, blockMap: BlockMap): Array<string> => {
  94. const headKey = block.getKey();
  95. let rootBlock = block;
  96. const rootBlocks = []; // sometimes the fragment head block will not be part of the blockMap itself this can happen when
  97. // the fragment head is used to update the target block, however when this does not happen we need
  98. // to make sure that we include it on the rootBlocks since the first block of a fragment is always a
  99. // fragment root block
  100. if (blockMap.get(headKey)) {
  101. rootBlocks.push(headKey);
  102. }
  103. while (rootBlock && rootBlock.getNextSiblingKey()) {
  104. const lastSiblingKey = rootBlock.getNextSiblingKey();
  105. if (!lastSiblingKey) {
  106. break;
  107. }
  108. rootBlocks.push(lastSiblingKey);
  109. rootBlock = blockMap.get(lastSiblingKey);
  110. }
  111. return rootBlocks;
  112. };
  113. const updateBlockMapLinks = (blockMap: BlockMap, originalBlockMap: BlockMap, targetBlock: ContentBlockNode, fragmentHeadBlock: ContentBlockNode): BlockMap => {
  114. return blockMap.withMutations(blockMapState => {
  115. const targetKey = targetBlock.getKey();
  116. const headKey = fragmentHeadBlock.getKey();
  117. const targetNextKey = targetBlock.getNextSiblingKey();
  118. const targetParentKey = targetBlock.getParentKey();
  119. const fragmentRootBlocks = getRootBlocks(fragmentHeadBlock, blockMap);
  120. const lastRootFragmentBlockKey = fragmentRootBlocks[fragmentRootBlocks.length - 1];
  121. if (blockMapState.get(headKey)) {
  122. // update the fragment head when it is part of the blockMap otherwise
  123. blockMapState.setIn([targetKey, 'nextSibling'], headKey);
  124. blockMapState.setIn([headKey, 'prevSibling'], targetKey);
  125. } else {
  126. // update the target block that had the fragment head contents merged into it
  127. blockMapState.setIn([targetKey, 'nextSibling'], fragmentHeadBlock.getNextSiblingKey());
  128. blockMapState.setIn([fragmentHeadBlock.getNextSiblingKey(), 'prevSibling'], targetKey);
  129. } // update the last root block fragment
  130. blockMapState.setIn([lastRootFragmentBlockKey, 'nextSibling'], targetNextKey); // update the original target next block
  131. if (targetNextKey) {
  132. blockMapState.setIn([targetNextKey, 'prevSibling'], lastRootFragmentBlockKey);
  133. } // update fragment parent links
  134. fragmentRootBlocks.forEach(blockKey => blockMapState.setIn([blockKey, 'parent'], targetParentKey)); // update targetBlock parent child links
  135. if (targetParentKey) {
  136. const targetParent = blockMap.get(targetParentKey);
  137. const originalTargetParentChildKeys = targetParent.getChildKeys();
  138. const targetBlockIndex = originalTargetParentChildKeys.indexOf(targetKey);
  139. const insertionIndex = targetBlockIndex + 1;
  140. const newChildrenKeysArray = originalTargetParentChildKeys.toArray(); // insert fragment children
  141. newChildrenKeysArray.splice(insertionIndex, 0, ...fragmentRootBlocks);
  142. blockMapState.setIn([targetParentKey, 'children'], List(newChildrenKeysArray));
  143. }
  144. });
  145. };
  146. const insertFragment = (contentState: ContentState, selectionState: SelectionState, blockMap: BlockMap, fragment: BlockMap, targetKey: string, targetOffset: number): ContentState => {
  147. const isTreeBasedBlockMap = blockMap.first() instanceof ContentBlockNode;
  148. const newBlockArr = [];
  149. const fragmentSize = fragment.size;
  150. const target = blockMap.get(targetKey);
  151. const head = fragment.first();
  152. const tail = fragment.last();
  153. const finalOffset = tail.getLength();
  154. const finalKey = tail.getKey();
  155. const shouldNotUpdateFromFragmentBlock = isTreeBasedBlockMap && (!target.getChildKeys().isEmpty() || !head.getChildKeys().isEmpty());
  156. blockMap.forEach((block, blockKey) => {
  157. if (blockKey !== targetKey) {
  158. newBlockArr.push(block);
  159. return;
  160. }
  161. if (shouldNotUpdateFromFragmentBlock) {
  162. newBlockArr.push(block);
  163. } else {
  164. newBlockArr.push(updateHead(block, targetOffset, fragment));
  165. } // Insert fragment blocks after the head and before the tail.
  166. fragment // when we are updating the target block with the head fragment block we skip the first fragment
  167. // head since its contents have already been merged with the target block otherwise we include
  168. // the whole fragment
  169. .slice(shouldNotUpdateFromFragmentBlock ? 0 : 1, fragmentSize - 1).forEach(fragmentBlock => newBlockArr.push(fragmentBlock)); // update tail
  170. newBlockArr.push(updateTail(block, targetOffset, fragment));
  171. });
  172. let updatedBlockMap = BlockMapBuilder.createFromArray(newBlockArr);
  173. if (isTreeBasedBlockMap) {
  174. updatedBlockMap = updateBlockMapLinks(updatedBlockMap, blockMap, target, head);
  175. }
  176. return contentState.merge({
  177. blockMap: updatedBlockMap,
  178. selectionBefore: selectionState,
  179. selectionAfter: selectionState.merge({
  180. anchorKey: finalKey,
  181. anchorOffset: finalOffset,
  182. focusKey: finalKey,
  183. focusOffset: finalOffset,
  184. isBackward: false
  185. })
  186. });
  187. };
  188. const insertFragmentIntoContentState = (contentState: ContentState, selectionState: SelectionState, fragmentBlockMap: BlockMap, mergeBlockData?: BlockDataMergeBehavior = 'REPLACE_WITH_NEW_DATA'): ContentState => {
  189. invariant(selectionState.isCollapsed(), '`insertFragment` should only be called with a collapsed selection state.');
  190. const blockMap = contentState.getBlockMap();
  191. const fragment = randomizeBlockMapKeys(fragmentBlockMap);
  192. const targetKey = selectionState.getStartKey();
  193. const targetOffset = selectionState.getStartOffset();
  194. const targetBlock = blockMap.get(targetKey);
  195. if (targetBlock instanceof ContentBlockNode) {
  196. invariant(targetBlock.getChildKeys().isEmpty(), '`insertFragment` should not be called when a container node is selected.');
  197. } // When we insert a fragment with a single block we simply update the target block
  198. // with the contents of the inserted fragment block
  199. if (fragment.size === 1) {
  200. return updateExistingBlock(contentState, selectionState, blockMap, fragment.first(), targetKey, targetOffset, mergeBlockData);
  201. }
  202. return insertFragment(contentState, selectionState, blockMap, fragment, targetKey, targetOffset);
  203. };
  204. module.exports = insertFragmentIntoContentState;