|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- /**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @format
- * @flow
- * @emails oncall+draft_js
- */
- 'use strict';
-
- import type { BlockMap } from "./BlockMap";
- import type { BlockNodeRecord } from "./BlockNodeRecord";
- import type ContentState from "./ContentState";
- import type SelectionState from "./SelectionState";
-
- const BlockMapBuilder = require("./BlockMapBuilder");
-
- const ContentBlockNode = require("./ContentBlockNode");
-
- const Immutable = require("immutable");
-
- const insertIntoList = require("./insertIntoList");
-
- const invariant = require("fbjs/lib/invariant");
-
- const randomizeBlockMapKeys = require("./randomizeBlockMapKeys");
-
- const {
- List
- } = Immutable;
- export type BlockDataMergeBehavior = 'REPLACE_WITH_NEW_DATA' | 'MERGE_OLD_DATA_TO_NEW_DATA';
-
- const updateExistingBlock = (contentState: ContentState, selectionState: SelectionState, blockMap: BlockMap, fragmentBlock: BlockNodeRecord, targetKey: string, targetOffset: number, mergeBlockData?: BlockDataMergeBehavior = 'REPLACE_WITH_NEW_DATA'): ContentState => {
- const targetBlock = blockMap.get(targetKey);
- const text = targetBlock.getText();
- const chars = targetBlock.getCharacterList();
- const finalKey = targetKey;
- const finalOffset = targetOffset + fragmentBlock.getText().length;
- let data = null;
-
- switch (mergeBlockData) {
- case 'MERGE_OLD_DATA_TO_NEW_DATA':
- data = fragmentBlock.getData().merge(targetBlock.getData());
- break;
-
- case 'REPLACE_WITH_NEW_DATA':
- data = fragmentBlock.getData();
- break;
- }
-
- const newBlock = targetBlock.merge({
- text: text.slice(0, targetOffset) + fragmentBlock.getText() + text.slice(targetOffset),
- characterList: insertIntoList(chars, fragmentBlock.getCharacterList(), targetOffset),
- data
- });
- return contentState.merge({
- blockMap: blockMap.set(targetKey, newBlock),
- selectionBefore: selectionState,
- selectionAfter: selectionState.merge({
- anchorKey: finalKey,
- anchorOffset: finalOffset,
- focusKey: finalKey,
- focusOffset: finalOffset,
- isBackward: false
- })
- });
- };
- /**
- * Appends text/characterList from the fragment first block to
- * target block.
- */
-
-
- const updateHead = (block: BlockNodeRecord, targetOffset: number, fragment: BlockMap): BlockNodeRecord => {
- const text = block.getText();
- const chars = block.getCharacterList(); // Modify head portion of block.
-
- const headText = text.slice(0, targetOffset);
- const headCharacters = chars.slice(0, targetOffset);
- const appendToHead = fragment.first();
- return block.merge({
- text: headText + appendToHead.getText(),
- characterList: headCharacters.concat(appendToHead.getCharacterList()),
- type: headText ? block.getType() : appendToHead.getType(),
- data: appendToHead.getData()
- });
- };
- /**
- * Appends offset text/characterList from the target block to the last
- * fragment block.
- */
-
-
- const updateTail = (block: BlockNodeRecord, targetOffset: number, fragment: BlockMap): BlockNodeRecord => {
- // Modify tail portion of block.
- const text = block.getText();
- const chars = block.getCharacterList(); // Modify head portion of block.
-
- const blockSize = text.length;
- const tailText = text.slice(targetOffset, blockSize);
- const tailCharacters = chars.slice(targetOffset, blockSize);
- const prependToTail = fragment.last();
- return prependToTail.merge({
- text: prependToTail.getText() + tailText,
- characterList: prependToTail.getCharacterList().concat(tailCharacters),
- data: prependToTail.getData()
- });
- };
-
- const getRootBlocks = (block: ContentBlockNode, blockMap: BlockMap): Array<string> => {
- const headKey = block.getKey();
- let rootBlock = block;
- const rootBlocks = []; // sometimes the fragment head block will not be part of the blockMap itself this can happen when
- // the fragment head is used to update the target block, however when this does not happen we need
- // to make sure that we include it on the rootBlocks since the first block of a fragment is always a
- // fragment root block
-
- if (blockMap.get(headKey)) {
- rootBlocks.push(headKey);
- }
-
- while (rootBlock && rootBlock.getNextSiblingKey()) {
- const lastSiblingKey = rootBlock.getNextSiblingKey();
-
- if (!lastSiblingKey) {
- break;
- }
-
- rootBlocks.push(lastSiblingKey);
- rootBlock = blockMap.get(lastSiblingKey);
- }
-
- return rootBlocks;
- };
-
- const updateBlockMapLinks = (blockMap: BlockMap, originalBlockMap: BlockMap, targetBlock: ContentBlockNode, fragmentHeadBlock: ContentBlockNode): BlockMap => {
- return blockMap.withMutations(blockMapState => {
- const targetKey = targetBlock.getKey();
- const headKey = fragmentHeadBlock.getKey();
- const targetNextKey = targetBlock.getNextSiblingKey();
- const targetParentKey = targetBlock.getParentKey();
- const fragmentRootBlocks = getRootBlocks(fragmentHeadBlock, blockMap);
- const lastRootFragmentBlockKey = fragmentRootBlocks[fragmentRootBlocks.length - 1];
-
- if (blockMapState.get(headKey)) {
- // update the fragment head when it is part of the blockMap otherwise
- blockMapState.setIn([targetKey, 'nextSibling'], headKey);
- blockMapState.setIn([headKey, 'prevSibling'], targetKey);
- } else {
- // update the target block that had the fragment head contents merged into it
- blockMapState.setIn([targetKey, 'nextSibling'], fragmentHeadBlock.getNextSiblingKey());
- blockMapState.setIn([fragmentHeadBlock.getNextSiblingKey(), 'prevSibling'], targetKey);
- } // update the last root block fragment
-
-
- blockMapState.setIn([lastRootFragmentBlockKey, 'nextSibling'], targetNextKey); // update the original target next block
-
- if (targetNextKey) {
- blockMapState.setIn([targetNextKey, 'prevSibling'], lastRootFragmentBlockKey);
- } // update fragment parent links
-
-
- fragmentRootBlocks.forEach(blockKey => blockMapState.setIn([blockKey, 'parent'], targetParentKey)); // update targetBlock parent child links
-
- if (targetParentKey) {
- const targetParent = blockMap.get(targetParentKey);
- const originalTargetParentChildKeys = targetParent.getChildKeys();
- const targetBlockIndex = originalTargetParentChildKeys.indexOf(targetKey);
- const insertionIndex = targetBlockIndex + 1;
- const newChildrenKeysArray = originalTargetParentChildKeys.toArray(); // insert fragment children
-
- newChildrenKeysArray.splice(insertionIndex, 0, ...fragmentRootBlocks);
- blockMapState.setIn([targetParentKey, 'children'], List(newChildrenKeysArray));
- }
- });
- };
-
- const insertFragment = (contentState: ContentState, selectionState: SelectionState, blockMap: BlockMap, fragment: BlockMap, targetKey: string, targetOffset: number): ContentState => {
- const isTreeBasedBlockMap = blockMap.first() instanceof ContentBlockNode;
- const newBlockArr = [];
- const fragmentSize = fragment.size;
- const target = blockMap.get(targetKey);
- const head = fragment.first();
- const tail = fragment.last();
- const finalOffset = tail.getLength();
- const finalKey = tail.getKey();
- const shouldNotUpdateFromFragmentBlock = isTreeBasedBlockMap && (!target.getChildKeys().isEmpty() || !head.getChildKeys().isEmpty());
- blockMap.forEach((block, blockKey) => {
- if (blockKey !== targetKey) {
- newBlockArr.push(block);
- return;
- }
-
- if (shouldNotUpdateFromFragmentBlock) {
- newBlockArr.push(block);
- } else {
- newBlockArr.push(updateHead(block, targetOffset, fragment));
- } // Insert fragment blocks after the head and before the tail.
-
-
- fragment // when we are updating the target block with the head fragment block we skip the first fragment
- // head since its contents have already been merged with the target block otherwise we include
- // the whole fragment
- .slice(shouldNotUpdateFromFragmentBlock ? 0 : 1, fragmentSize - 1).forEach(fragmentBlock => newBlockArr.push(fragmentBlock)); // update tail
-
- newBlockArr.push(updateTail(block, targetOffset, fragment));
- });
- let updatedBlockMap = BlockMapBuilder.createFromArray(newBlockArr);
-
- if (isTreeBasedBlockMap) {
- updatedBlockMap = updateBlockMapLinks(updatedBlockMap, blockMap, target, head);
- }
-
- return contentState.merge({
- blockMap: updatedBlockMap,
- selectionBefore: selectionState,
- selectionAfter: selectionState.merge({
- anchorKey: finalKey,
- anchorOffset: finalOffset,
- focusKey: finalKey,
- focusOffset: finalOffset,
- isBackward: false
- })
- });
- };
-
- const insertFragmentIntoContentState = (contentState: ContentState, selectionState: SelectionState, fragmentBlockMap: BlockMap, mergeBlockData?: BlockDataMergeBehavior = 'REPLACE_WITH_NEW_DATA'): ContentState => {
- invariant(selectionState.isCollapsed(), '`insertFragment` should only be called with a collapsed selection state.');
- const blockMap = contentState.getBlockMap();
- const fragment = randomizeBlockMapKeys(fragmentBlockMap);
- const targetKey = selectionState.getStartKey();
- const targetOffset = selectionState.getStartOffset();
- const targetBlock = blockMap.get(targetKey);
-
- if (targetBlock instanceof ContentBlockNode) {
- invariant(targetBlock.getChildKeys().isEmpty(), '`insertFragment` should not be called when a container node is selected.');
- } // When we insert a fragment with a single block we simply update the target block
- // with the contents of the inserted fragment block
-
-
- if (fragment.size === 1) {
- return updateExistingBlock(contentState, selectionState, blockMap, fragment.first(), targetKey, targetOffset, mergeBlockData);
- }
-
- return insertFragment(contentState, selectionState, blockMap, fragment, targetKey, targetOffset);
- };
-
- module.exports = insertFragmentIntoContentState;
|