UNPKG

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